├── .gitattributes
├── jsconfig.json
├── screen.png
├── src
├── mixins
│ ├── vpd.js
│ └── index.js
├── _variables.scss
├── plugins
│ ├── i18n.js
│ ├── inter
│ │ ├── template.js
│ │ ├── get-prop.js
│ │ └── index.js
│ ├── widget.js
│ ├── messages.js
│ └── store.js
├── store
│ ├── index.js
│ ├── actions.js
│ ├── state.js
│ └── mutation.js
├── index.js
├── utils
│ ├── offset.js
│ ├── load-sprite.js
│ └── css-generate.js
├── components
│ ├── panel
│ │ ├── event.vue
│ │ ├── page.vue
│ │ ├── index.vue
│ │ ├── style.vue
│ │ └── animation.vue
│ ├── icon.vue
│ ├── toast.vue
│ ├── popbox.vue
│ ├── viewport
│ │ ├── size-control.vue
│ │ ├── ref-lines.vue
│ │ └── index.vue
│ ├── navbar.vue
│ ├── toolbar.vue
│ ├── uploader.vue
│ └── slider.vue
├── app.scss
└── App.vue
├── example
├── widgets
│ ├── index.js
│ └── button
│ │ ├── style.vue
│ │ └── index.vue
├── index.js
├── index.html
├── App.vue
└── webpack.config.js
├── .editorconfig
├── .babelrc
├── .gitignore
├── .eslintrc.js
├── scripts
├── icon.js
├── build.js
└── config.js
├── circle.yml
├── .github
└── workflows
│ └── build.yml
├── package.json
├── README.md
├── CHANGELOG.md
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "./src/**/*"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fireyy/vue-page-designer/HEAD/screen.png
--------------------------------------------------------------------------------
/src/mixins/vpd.js:
--------------------------------------------------------------------------------
1 | import vpd from '../store'
2 |
3 | export default {
4 | beforeCreate () {
5 | this.$vpd = vpd
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/example/widgets/index.js:
--------------------------------------------------------------------------------
1 | import braidButton from './button/index.vue'
2 |
3 | export default {
4 | [braidButton.name]: braidButton
5 | }
6 |
--------------------------------------------------------------------------------
/src/_variables.scss:
--------------------------------------------------------------------------------
1 | // Define variables to override default ones
2 | $primary-color: #000;
3 |
4 | @import "../node_modules/spectre.css/src/variables";
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/src/plugins/i18n.js:
--------------------------------------------------------------------------------
1 | import messages from './messages'
2 | import Inter from './inter'
3 | import Vue from 'vue'
4 |
5 | Vue.use(Inter)
6 |
7 | export default new Inter({
8 | locale: 'cn', // setup locale
9 | messages: messages
10 | })
11 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import vuePageDesigner from '../src'
4 |
5 | Vue.use(vuePageDesigner)
6 |
7 | new Vue({ // eslint-disable-line no-new
8 | el: '#app',
9 | render: h => h(App)
10 | })
11 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "modules": false }]
4 | ],
5 | "env": {
6 | "test": {
7 | "presets": [
8 | ["@babel/preset-env", { "targets": { "node": "current" }}]
9 | ]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | /example/dist/
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | /test/unit/coverage/
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | // import Vue from 'vue'
2 | import Store from '../plugins/store'
3 |
4 | import state from './state'
5 | import actions from './actions'
6 | import mutations from './mutation'
7 |
8 | // Vue.use(Store)
9 |
10 | export default new Store({
11 | state,
12 | actions,
13 | mutations
14 | })
15 |
--------------------------------------------------------------------------------
/src/plugins/inter/template.js:
--------------------------------------------------------------------------------
1 | export default (tpl, data) => {
2 | if (!data) return tpl
3 |
4 | const re = /{(.*?)}/g
5 |
6 | return tpl.replace(re, (_, key) => {
7 | let ret = data
8 |
9 | key.split('.').forEach((prop) => {
10 | ret = ret ? ret[prop] : ''
11 | })
12 |
13 | return ret || ''
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue page designer demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | "extends": [
5 | "standard",
6 | "plugin:vue/recommended"
7 | ],
8 |
9 | "rules": {
10 | "semi": "off",
11 | "no-new": "off",
12 | "vue/valid-template-root": "off",
13 | "vue/require-default-prop": "off",
14 | "vue/require-prop-types": "off"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | addWidget ({ state, commit, store }, item) {
3 | if (item.setting.isUpload) {
4 | store.$emit('upload', (payload) => {
5 | commit('addWidget', { data: payload, item })
6 | }, true)
7 | } else {
8 | commit('addWidget', { item })
9 | // 设置选中
10 | commit('select', {
11 | uuid: state.widgets[state.widgets.length - 1].uuid
12 | })
13 | }
14 | },
15 | save ({ state, store }) {
16 | store.$emit('save', state)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import VuePageDesigner from './App.vue'
2 |
3 | import slider from './components/slider.vue'
4 | import icon from './components/icon.vue'
5 |
6 | import './app.scss'
7 |
8 | const install = function (Vue, opts = {}) {
9 | Vue.component('VpdSlider', slider)
10 | Vue.component('VpdIcon', icon)
11 |
12 | Vue.component('VuePageDesigner', VuePageDesigner)
13 | };
14 |
15 | if (typeof window !== 'undefined' && window.Vue) {
16 | install(window.Vue);
17 | }
18 |
19 | export default {
20 | install,
21 | VuePageDesigner
22 | }
23 |
--------------------------------------------------------------------------------
/src/plugins/inter/get-prop.js:
--------------------------------------------------------------------------------
1 | function getPathSegments (path) {
2 | const pathArr = path.split('.')
3 | const parts = []
4 |
5 | for (let i = 0; i < pathArr.length; i++) {
6 | let p = pathArr[i]
7 |
8 | while (p[p.length - 1] === '\\' && pathArr[i + 1] !== undefined) {
9 | p = p.slice(0, -1) + '.'
10 | p += pathArr[++i]
11 | }
12 |
13 | parts.push(p)
14 | }
15 |
16 | return parts
17 | }
18 |
19 | export default function (data, path) {
20 | return getPathSegments(path).reduce((obj, k) => obj && obj[k], data)
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/offset.js:
--------------------------------------------------------------------------------
1 | export function cumulativeOffset (element) {
2 | let top = 0
3 | let left = 0
4 |
5 | do {
6 | top += element.offsetTop || 0
7 | left += element.offsetLeft || 0
8 | element = element.offsetParent
9 | } while (element)
10 |
11 | return {
12 | top: top,
13 | left: left
14 | }
15 | }
16 |
17 | export function checkInView (el) {
18 | let rect = el.getBoundingClientRect()
19 | return (
20 | rect.top < window.innerHeight &&
21 | (rect.left < window.innerWidth &&
22 | rect.right > 0)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/scripts/icon.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var path = require('path')
3 | var feather = require('feather-icons')
4 |
5 | var icons = Object.keys(feather.icons)
6 | .map(key => `${feather.icons[key].toString()} `);
7 |
8 | fs.writeFileSync(path.resolve(__dirname, '../static/icons.svg'), `${icons.join('')} `);
9 |
10 | console.log(Object.keys(feather.icons).length + ' icon generated.')
11 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:latest
6 | branches:
7 | ignore:
8 | - gh-pages # list of branches to ignore
9 | - /release\/.*/ # or ignore regexes
10 | steps:
11 | - checkout
12 | - restore_cache:
13 | key: dependency-cache-{{ checksum "yarn.lock" }}
14 | - run:
15 | name: install dependences
16 | command: yarn
17 | - save_cache:
18 | key: dependency-cache-{{ checksum "yarn.lock" }}
19 | paths:
20 | - ./node_modules
21 | - run:
22 | name: test
23 | command: yarn test
24 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | top: 0, // 添加元件的初始纵坐标
3 | zoom: 64, // 画布缩放百分比
4 | type: 'page', // 选中元素类型
5 | index: -1, // 选中元素索引
6 | uuid: null, // 选中元素uuid
7 | counter: 0, // 容器副本命名时避免重名所用的计数器
8 |
9 | originX: 0, // 选中元件的横向初始值
10 | originY: 0, // 选中元件的纵向初始值
11 | startX: 0, // 鼠标摁下时的横坐标
12 | startY: 0, // 鼠标摁下时的纵坐标
13 | moving: false, // 是否正在移动元件(参考线仅在移动元件时显示)
14 |
15 | animation: [], // 动画库
16 | playState: false, // 动画播放状态
17 |
18 | activeElement: {}, // 选中对象,要么是元件,要么是页面
19 | page: {
20 | page: true,
21 | title: '测试页面', // 页面 title
22 | height: 1500, // 画布高度
23 | endTime: new Date(), // 截止日期
24 | backgroundColor: '#fff'
25 | },
26 | widgets: [] // 元件
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/panel/event.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
{{ $t('data.events.onclick') }}
8 |
9 |
10 |
13 |
{{ $t('data.events.linkTo') }}
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/src/utils/load-sprite.js:
--------------------------------------------------------------------------------
1 | // Load a sprite
2 | export default function (url, id) {
3 | var x = new XMLHttpRequest()
4 |
5 | // If the id is set and sprite exists, bail
6 | if (document.querySelector('#' + id)) {
7 | return
8 | }
9 |
10 | // Create placeholder (to prevent loading twice)
11 | var container = document.createElement('div')
12 | container.setAttribute('hidden', '')
13 | container.setAttribute('id', id)
14 | document.body.insertBefore(container, document.body.childNodes[0])
15 |
16 | // Check for CORS support
17 | if ('withCredentials' in x) {
18 | x.open('GET', url, true)
19 | } else {
20 | return
21 | }
22 |
23 | // Inject hidden div with sprite on load
24 | x.onload = function () {
25 | container.innerHTML = x.responseText
26 | }
27 |
28 | x.send()
29 | }
30 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const mkdirpNode = require('mkdirp');
3 | const { promisify } = require('util');
4 | const { rollup } = require('rollup');
5 | const { paths, configs, utils } = require('./config');
6 | const mkdirp = promisify(mkdirpNode);
7 |
8 | async function buildConfig (build) {
9 | await mkdirp(paths.dist);
10 | const bundleName = build.output.file.replace(paths.dist, '');
11 | console.log(chalk.cyan(`📦 Generating ${bundleName}...`));
12 |
13 | const bundle = await rollup(build.input);
14 | await bundle.write(build.output);
15 |
16 | console.log(chalk.green(`👍 ${bundleName} ${utils.stats({ path: build.output.file })}`));
17 | }
18 |
19 | async function build () {
20 | await Promise.all(Object.keys(configs).map(key => {
21 | return buildConfig(configs[key]).catch(err => {
22 | console.log(err);
23 | });
24 | }));
25 | process.exit(0);
26 | }
27 |
28 | build();
29 |
--------------------------------------------------------------------------------
/src/components/icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
29 |
41 |
--------------------------------------------------------------------------------
/src/plugins/widget.js:
--------------------------------------------------------------------------------
1 | // 默认 widgets
2 | import defaultWidgets from 'vue-page-designer-widgets'
3 | import vpd from '../mixins/vpd'
4 |
5 | var widgets
6 | var widgetStyle = {}
7 |
8 | const install = (Vue, config = {}) => {
9 | if (install.installed) return
10 |
11 | widgets = Object.assign({}, defaultWidgets, config.widgets)
12 |
13 | Object.keys(widgets).forEach(key => {
14 | Vue.component(key, widgets[key])
15 | Vue.component(key, Vue.extend(widgets[key]).extend(vpd))
16 | // style panel
17 | if (widgets[key]['panel']) {
18 | let panel = Object.assign({}, widgets[key]['panel'], {
19 | type: key
20 | })
21 | Vue.component(panel.name, Vue.extend(panel).extend(vpd))
22 | widgetStyle[panel.name] = panel
23 | // remove panel from object
24 | delete widgets[key]['panel']
25 | }
26 | })
27 | }
28 |
29 | export default {
30 | install,
31 | getWidgets () {
32 | return widgets
33 | },
34 | getWidgetStyle () {
35 | return widgetStyle
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/mixins/index.js:
--------------------------------------------------------------------------------
1 | var move = {
2 | methods: {
3 | initmovement (e) {
4 | var target = this.$vpd.state.activeElement
5 |
6 | // 设置移动状态初始值
7 | this.$vpd.commit('initmove', {
8 | startX: e.pageX,
9 | startY: e.pageY,
10 | originX: target.left,
11 | originY: target.top
12 | })
13 |
14 | // 绑定鼠标移动事件
15 | document.addEventListener('mousemove', this.handlemousemove, true)
16 |
17 | // 取消鼠标移动事件
18 | document.addEventListener('mouseup', this.handlemouseup, true)
19 | },
20 |
21 | handlemousemove (e) {
22 | e.stopPropagation()
23 | e.preventDefault()
24 |
25 | this.$vpd.commit('move', {
26 | x: e.pageX,
27 | y: e.pageY
28 | })
29 | },
30 |
31 | handlemouseup () {
32 | document.removeEventListener('mousemove', this.handlemousemove, true)
33 | document.removeEventListener('mouseup', this.handlemouseup, true)
34 | this.$vpd.commit('stopmove')
35 | }
36 | }
37 | }
38 |
39 | export { move }
40 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ master ]
10 | pull_request:
11 | branches: [ master ]
12 |
13 | env:
14 | NODE_VERSION: '10.x'
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v2
27 | - name: Use Node.js ${{ env.NODE_VERSION }}
28 | uses: actions/setup-node@v1
29 | with:
30 | node-version: ${{ env.NODE_VERSION }}
31 | - name: 'Build'
32 | run: |
33 | npm install
34 | npm run build
35 |
--------------------------------------------------------------------------------
/example/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/src/components/toast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ info }}
7 |
8 |
9 |
10 |
49 |
50 |
64 |
--------------------------------------------------------------------------------
/example/widgets/button/style.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
背景色
7 |
{{ activeElement.bgColor }}
8 |
9 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
32 |
文字颜色
33 |
{{ activeElement.color }}
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
56 |
--------------------------------------------------------------------------------
/src/utils/css-generate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于生成动画 keyframes 字符串
3 | *
4 | * @name { String } animation-name
5 | * @animation { Object } animation's properties
6 | * @stops { Array } key frames
7 | * @needFormat { Boolean } default is true
8 | *
9 | * @return { String }
10 | */
11 | export function getAnimateCss (name, animation, stops, needFormat = true) {
12 | var properties = ['duration', 'timing', 'delay', 'iteration', 'direction', 'fill']
13 | var values = [name]
14 |
15 | properties.map(val => {
16 | if (animation[val] === undefined) return
17 | if (val === 'duration' || val === 'delay') {
18 | values.push(animation[val] + 's')
19 | } else if (val === 'iteration') {
20 | values.push(animation[val] === 0 ? 'infinite' : animation[val])
21 | } else {
22 | values.push(animation[val])
23 | }
24 | })
25 |
26 | var animateCss = 'animation: ' + values.join(' ') + ';'
27 |
28 | // 生成 keyframes 代码
29 | var keyframes = []
30 | if (needFormat) {
31 | stops.map(val => {
32 | keyframes.push('\t' + val.stop + '% {\n')
33 | keyframes.push('\t\t' + val.css + '\n\t}\n')
34 | })
35 | } else {
36 | stops.map(val => {
37 | keyframes.push(val.stop + '% {')
38 | keyframes.push(val.css + '}')
39 | })
40 | }
41 | var keyframeCss = keyframes.join('')
42 |
43 | var output =
44 | `
45 | .anm-${name} {
46 | -webkit-${animateCss}
47 | ${animateCss}
48 | }
49 | @keyframes ${name} {
50 | ${keyframeCss}}
51 | @-webkit-keyframes ${name} {
52 | ${keyframeCss}}
53 | `
54 | return output
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/panel/page.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
{{ $t('messages.page.name') }}
10 |
11 |
14 |
15 |
16 |
17 |
20 |
21 |
{{ $t('messages.page.height') }}
22 |
23 |
26 |
27 |
28 |
29 |
32 |
33 |
{{ $t('messages.page.background') }}
34 |
35 |
38 |
39 |
40 |
41 |
44 |
45 |
{{ $t('messages.page.endTime') }}
46 |
47 |
50 |
51 |
52 |
53 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | // Define variables to override default ones
2 | @import "variables";
3 |
4 | @import "node_modules/spectre.css/src/mixins";
5 | @import "node_modules/spectre.css/src/base";
6 | @import "node_modules/spectre.css/src/utilities";
7 |
8 | // Layout
9 | @import "node_modules/spectre.css/src/layout";
10 | @import "node_modules/spectre.css/src/navbar";
11 |
12 | // Import only the needed components
13 | @import "node_modules/spectre.css/src/buttons";
14 | @import "node_modules/spectre.css/src/forms";
15 | @import "node_modules/spectre.css/src/tabs";
16 | @import "node_modules/spectre.css/src/tooltips";
17 | @import "node_modules/spectre.css/src/toasts";
18 |
19 | html,
20 | body,
21 | .app {
22 | height: 100%;
23 | }
24 | html {
25 | font-size: 18px;
26 | }
27 | body {
28 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", sans-serif;
29 | padding: 0;
30 | margin: 0;
31 | }
32 |
33 | input[type="text"],
34 | input[type="date"],
35 | textarea {
36 | @extend .form-input;
37 | @extend .input-sm;
38 | }
39 | input[type="color"] {
40 | cursor: pointer;
41 | width: 24px;
42 | vertical-align: middle;
43 | border: none;
44 | &::-webkit-color-swatch {
45 | border: none;
46 | border-radius: 4px;
47 | }
48 | &::-webkit-color-swatch-wrapper {
49 | padding: 1px;
50 | border-radius: 4px;
51 | border: 1px solid $primary-color;
52 | }
53 | }
54 | select {
55 | @extend .form-select;
56 | @extend .select-sm;
57 | }
58 | .layer {
59 | &:hover {
60 | outline: 1px solid #ddd !important;
61 | }
62 | }
63 | .g-active {
64 | outline: 1px solid #2196f3 !important;
65 | &:hover {
66 | outline: 1px solid #2196f3 !important;
67 | }
68 | &::after {
69 | content: attr(data-title);
70 | background: #2196f3;
71 | color: #fff;
72 | position: absolute;
73 | top: 0;
74 | right: 0;
75 | padding: 0 5px;
76 | font-size: 12px;
77 | border-radius: 0 0 0 4px;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/example/widgets/button/index.vue:
--------------------------------------------------------------------------------
1 |
2 | updateText(e, val.uuid)"
17 | v-html="val.text"/>
18 |
19 |
20 |
65 |
66 |
73 |
--------------------------------------------------------------------------------
/src/plugins/inter/index.js:
--------------------------------------------------------------------------------
1 | import defaultTemplate from './template'
2 | import getProp from './get-prop'
3 |
4 | let Vue
5 |
6 | export default class Inter {
7 | static install (_Vue) {
8 | Vue = _Vue
9 | Vue.mixin({
10 | beforeCreate () {
11 | this.$inter =
12 | this.$options.inter || (this.$parent && this.$parent.$inter)
13 | }
14 | })
15 | Vue.prototype.$t = function (key) {
16 | const inter = this.$inter
17 | return inter.formatMessage(
18 | {
19 | path: key
20 | }
21 | )
22 | }
23 | }
24 |
25 | constructor ({ locale, messages = {}, template = defaultTemplate }) {
26 | if (process.env.NODE_ENV === 'development' && !Vue) {
27 | throw new Error('You have to install `vue-inter` first: Vue.use(Inter)')
28 | }
29 |
30 | this.template = template
31 | this.messages = messages
32 |
33 | Vue.util.defineReactive(this, '__locale', locale)
34 | }
35 |
36 | formatMessage (messageDescriptor, ...data) {
37 | if (typeof messageDescriptor !== 'object') {
38 | throw new TypeError(
39 | 'messageDescriptor in .formatMessage must be an object.'
40 | )
41 | }
42 |
43 | const { path, defaultMessage } = messageDescriptor
44 | const localeData = this.messages[this.currentLocale]
45 | // Get message from path
46 | let message = path && getProp(localeData, path)
47 | if (typeof message === 'function') {
48 | return message(...data)
49 | }
50 | if (typeof message === 'undefined') {
51 | // Fallback to defaultMessage
52 | // Fallback to path literal
53 | message = typeof defaultMessage === 'undefined' ? path : defaultMessage
54 | }
55 | return this.template(message, ...data)
56 | }
57 |
58 | get currentLocale () {
59 | return this.__locale
60 | }
61 |
62 | setCurrentLocale (locale) {
63 | this.__locale = locale
64 | return this
65 | }
66 |
67 | setLocaleData (locale, localData) {
68 | this.messages[locale] = localData
69 | return this
70 | }
71 |
72 | get availableLocales () {
73 | return Object.keys(this.messages)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/popbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | {{ title }}
8 |
12 |
13 |
14 | {{ content }}
15 |
16 |
24 |
25 |
26 |
27 |
46 |
113 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const VueLoaderPlugin = require('vue-loader/lib/plugin')
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 | const CleanWebpackPlugin = require('clean-webpack-plugin')
6 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
7 | const ProgressBarPlugin = require('progress-bar-webpack-plugin')
8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
9 |
10 | const env = process.env.NODE_ENV
11 | const production = env === 'production'
12 |
13 | // render page
14 | const page = (name) => {
15 | return new HtmlWebpackPlugin({
16 | inject: true,
17 | template: path.join(__dirname, `./${name}.html`),
18 | filename: path.join(__dirname, `./dist/${name}.html`)
19 | })
20 | }
21 |
22 | const config = {
23 | mode: production ? 'production' : 'development',
24 | devtool: production ? 'source-map' : 'cheap-source-map',
25 | entry: {
26 | app: path.join(__dirname, './index.js')
27 | },
28 | output: {
29 | path: path.join(__dirname, 'dist'),
30 | filename: 'js/[name].js'
31 | },
32 | plugins: [
33 | new MiniCssExtractPlugin({
34 | filename: 'css/style.css'
35 | }),
36 | new CleanWebpackPlugin(['./dist']),
37 | new VueLoaderPlugin(),
38 | new webpack.LoaderOptionsPlugin({ options: {} }),
39 | new FriendlyErrorsWebpackPlugin(),
40 | new ProgressBarPlugin(),
41 | page('index')
42 | ],
43 | watchOptions: {
44 | aggregateTimeout: 300,
45 | poll: 1000
46 | },
47 | devServer: {
48 | historyApiFallback: true,
49 | hot: true,
50 | inline: true,
51 | stats: 'errors-only',
52 | host: '0.0.0.0',
53 | port: 8080
54 | },
55 | module: {
56 | rules: [
57 | {
58 | test: /.js$/,
59 | exclude: /node_modules/,
60 | loader: 'eslint-loader',
61 | enforce: 'pre'
62 | },
63 | {
64 | test: /.js$/,
65 | exclude: /node_modules/,
66 | use: {
67 | loader: 'babel-loader',
68 | options: { babelrc: true }
69 | }
70 | },
71 | {
72 | test: /\.vue$/,
73 | loader: 'eslint-loader',
74 | enforce: 'pre'
75 | },
76 | {
77 | test: /\.vue$/,
78 | loader: 'vue-loader'
79 | },
80 | {
81 | test: /\.css$/,
82 | loader: ['style-loader', 'css-loader']
83 | },
84 | {
85 | test: /\.scss?$/,
86 | use: [
87 | MiniCssExtractPlugin.loader,
88 | 'css-loader',
89 | 'sass-loader'
90 | ]
91 | },
92 | {
93 | test: /\.(ttf|eot|svg)(\?.*)?$/,
94 | loader: 'file-loader',
95 | options: {
96 | name: 'font/[name].[ext]'
97 | }
98 | }
99 | ]
100 | },
101 | resolve: {
102 | extensions: ['.js', '.vue', '.json']
103 | }
104 | }
105 |
106 | module.exports = config
107 |
--------------------------------------------------------------------------------
/scripts/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const replace = require('rollup-plugin-replace');
4 | const vue = require('rollup-plugin-vue');
5 | const resolve = require('rollup-plugin-node-resolve');
6 | const postcss = require('rollup-plugin-postcss');
7 | const buble = require('rollup-plugin-buble');
8 | const commonjs = require('rollup-plugin-commonjs');
9 | const filesize = require('filesize');
10 | const gzipSize = require('gzip-size');
11 | const { uglify } = require('rollup-plugin-uglify');
12 | const { minify } = require('terser');
13 |
14 | const version = process.env.VERSION || require('../package.json').version;
15 |
16 | const common = {
17 | banner:
18 | `/**
19 | * Vue-page-designer v${version}
20 | * (c) ${new Date().getFullYear()} fireyy
21 | * @license WTFPL
22 | */`,
23 | paths: {
24 | input: path.join(__dirname, '../src/index.js'),
25 | src: path.join(__dirname, '../src/'),
26 | dist: path.join(__dirname, '../dist/')
27 | },
28 | builds: {
29 | umd: {
30 | file: 'vue-page-designer.js',
31 | format: 'umd',
32 | name: 'vuePageDesigner',
33 | env: 'development'
34 | },
35 | umdMin: {
36 | file: 'vue-page-designer.min.js',
37 | format: 'umd',
38 | name: 'vuePageDesigner',
39 | env: 'production'
40 | },
41 | esm: {
42 | file: 'vue-page-designer.esm.js',
43 | format: 'es'
44 | }
45 | }
46 | };
47 |
48 | function genConfig (options) {
49 | const config = {
50 | description: '',
51 | input: {
52 | external: ['vue'],
53 | input: options.input || common.paths.input,
54 | plugins: [
55 | commonjs(),
56 | replace({ __VERSION__: version }),
57 | postcss({
58 | extract: true
59 | }),
60 | vue({ css: false }),
61 | resolve(),
62 | buble({ exclude: 'node_modules/**' })
63 | ]
64 | },
65 | output: {
66 | globals: {
67 | 'vue': 'Vue'
68 | },
69 | banner: common.banner,
70 | name: options.name,
71 | format: options.format,
72 | file: path.join(common.paths.dist, options.file)
73 | }
74 | };
75 |
76 | if (options.env) {
77 | config.input.plugins.unshift(replace({
78 | 'process.env.NODE_ENV': JSON.stringify(options.env)
79 | }));
80 | }
81 |
82 | if (options.env === 'production') {
83 | config.input.plugins.push(uglify({}, minify));
84 | }
85 |
86 | return config;
87 | };
88 |
89 | const configs = Object.keys(common.builds).reduce((prev, key) => {
90 | prev[key] = genConfig(common.builds[key]);
91 |
92 | return prev;
93 | }, {});
94 |
95 | module.exports = {
96 | configs,
97 | uglifyOptions: common.uglifyOptions,
98 | paths: common.paths,
99 | utils: {
100 | stats ({ path }) {
101 | const code = fs.readFileSync(path);
102 | const { size } = fs.statSync(path);
103 | const gzipped = gzipSize.sync(code);
104 |
105 | return `| Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`;
106 | }
107 | }
108 | };
109 |
--------------------------------------------------------------------------------
/src/plugins/messages.js:
--------------------------------------------------------------------------------
1 | const messages = {
2 | en: {
3 | data: {
4 | no: 'no',
5 | name: 'Name',
6 | duration: 'Duration',
7 | delay: 'Delay',
8 | iteration: 'Iteration',
9 | timing: 'Timing',
10 | direction: 'Direction',
11 | levels: 'Z index',
12 | components: 'Components',
13 | added_components: 'Structure',
14 |
15 | actions: {
16 | add: 'Add',
17 | determine: 'Determine',
18 | cancel: 'Cancel',
19 | copy: 'Copy',
20 | save: 'Save',
21 | delete: 'Delete'
22 | },
23 |
24 | names: {
25 | params: 'Params',
26 | event: 'Events',
27 | animation: 'Animation',
28 |
29 | width: 'Width',
30 | height: 'Height',
31 | left: 'Left',
32 | top: 'Top',
33 |
34 | belonging: 'Belonging container'
35 | },
36 |
37 | events: {
38 | onclick: 'On click',
39 | linkTo: 'Link to'
40 | }
41 | },
42 | messages: {
43 | panel: {
44 | animation: {
45 | select: 'Select animation'
46 | },
47 |
48 | alerts: {
49 | imageUploadApiConfigurator: 'Please configure the picture upload api address',
50 | unnamed_animations: 'There are unnamed animations, please name them first',
51 | animation_name_required: 'Please name the animation first',
52 | animation_name_validate: 'Do not use characters other than English and numbers'
53 | }
54 | },
55 |
56 | page: {
57 | name: 'Page name',
58 | height: 'Page height',
59 | background: 'Background',
60 | endTime: 'End time'
61 | }
62 | }
63 | },
64 | cn: {
65 | data: {
66 | no: '无',
67 | name: '名称',
68 | duration: '时长',
69 | delay: '延迟',
70 | iteration: '循环',
71 | timing: '缓动函数',
72 | direction: '方向',
73 | levels: '层级',
74 | components: '组件',
75 | added_components: '结构',
76 |
77 | actions: {
78 | add: '添加',
79 | determine: '确定',
80 | cancel: '取消',
81 | copy: '复制',
82 | save: '保存',
83 | delete: '删除'
84 | },
85 |
86 | names: {
87 | params: '参数',
88 | event: '交互',
89 | animation: '动画',
90 |
91 | width: '宽度',
92 | height: '高度',
93 | left: '横坐标',
94 | top: '纵坐标',
95 |
96 | belonging: '所属容器'
97 | },
98 |
99 | events: {
100 | onclick: '点击时',
101 | linkTo: '链接至'
102 | }
103 | },
104 | messages: {
105 | panel: {
106 | animation: {
107 | select: '选择动画'
108 | },
109 |
110 | alerts: {
111 | imageUploadApiConfigurator: '请配置图片上传api地址',
112 | unnamed_animations: '还有未命名动画,请先命名',
113 | animation_name_required: '请先为动画命名',
114 | animation_name_validate: '动画名称必须以英文开头'
115 | }
116 | },
117 |
118 | page: {
119 | name: '页面标题',
120 | height: '页面高度',
121 | background: '页面背景色',
122 | endTime: '截止日期'
123 | }
124 | }
125 | }
126 | }
127 |
128 | export default messages
129 |
--------------------------------------------------------------------------------
/src/components/panel/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
21 |
24 |
27 |
30 |
31 |
32 |
33 |
63 |
64 |
140 |
--------------------------------------------------------------------------------
/src/components/viewport/size-control.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
26 |
27 |
28 |
38 |
39 |
40 |
50 |
51 |
52 |
53 |
106 |
107 |
132 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-page-designer",
3 | "version": "1.1.1",
4 | "description": "A vue component for drag-and-drop to design and build mobile website.",
5 | "repository": {
6 | "url": "fireyy/vue-page-designer",
7 | "type": "git"
8 | },
9 | "module": "dist/vue-page-designer.esm.js",
10 | "unpkg": "dist/vue-page-designer.min.js",
11 | "main": "dist/vue-page-designer.js",
12 | "style": "dist/vue-page-designer.css",
13 | "license": "WTFPL",
14 | "files": [
15 | "dist/*.js",
16 | "dist/*.css"
17 | ],
18 | "scripts": {
19 | "bump": "standard-version",
20 | "test": "npm run unit",
21 | "lint": "eslint --ext .js,.vue ./src ./example --fix",
22 | "icon": "node scripts/icon.js",
23 | "build": "cross-env NODE_ENV=production node scripts/build.js",
24 | "dev": "webpack-dev-server --hot --inline --config ./example/webpack.config.js",
25 | "start": "npm run dev",
26 | "build:example": "cross-env NODE_ENV=production webpack --config ./example/webpack.config.js",
27 | "gh": "gh-pages -d example/dist",
28 | "deploy": "npm run build:example && npm run gh",
29 | "release": "npm run build && npm run bump && git push --follow-tags origin master && npm publish"
30 | },
31 | "author": "fireyy ",
32 | "dependencies": {
33 | "nanoid": "^1.0.1",
34 | "spectre.css": "^0.5.0",
35 | "vue": "^2.5.22",
36 | "vue-page-designer-widgets": "^0.1.4",
37 | "vue-server-renderer": "^2.5.17"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "^7.0.0-rc.1",
41 | "@babel/plugin-proposal-class-properties": "^7.0.0-rc.1",
42 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1",
43 | "@babel/preset-env": "^7.0.0-rc.1",
44 | "babel-loader": "^8.0.0-beta",
45 | "chalk": "^2.0.1",
46 | "clean-webpack-plugin": "^0.1.19",
47 | "copy-webpack-plugin": "^4.0.1",
48 | "cross-env": "^5.2.0",
49 | "css-loader": "^1.0.0",
50 | "eslint": "^5.3.0",
51 | "eslint-config-standard": "^11.0.0",
52 | "eslint-loader": "^2.1.0",
53 | "eslint-plugin-import": "^2.7.0",
54 | "eslint-plugin-node": "^7.0.1",
55 | "eslint-plugin-promise": "^3.5.0",
56 | "eslint-plugin-standard": "^3.0.1",
57 | "eslint-plugin-vue": "^4.7.1",
58 | "feather-icons": "^4.5.0",
59 | "file-loader": "^1.1.11",
60 | "filesize": "^3.6.1",
61 | "friendly-errors-webpack-plugin": "^1.6.1",
62 | "gh-pages": "^1.0.0",
63 | "gzip-size": "^5.0.0",
64 | "html-webpack-plugin": "^4.0.0-alpha",
65 | "mini-css-extract-plugin": "^0.4.4",
66 | "mkdirp": "^0.5.1",
67 | "node-sass": "^4.12.0",
68 | "progress-bar-webpack-plugin": "^1.10.0",
69 | "rollup": "^0.64.1",
70 | "rollup-plugin-buble": "^0.19.2",
71 | "rollup-plugin-commonjs": "^9.1.5",
72 | "rollup-plugin-node-resolve": "^3.0.0",
73 | "rollup-plugin-postcss": "^1.6.3",
74 | "rollup-plugin-replace": "^2.0.0",
75 | "rollup-plugin-uglify": "^4.0.0",
76 | "rollup-plugin-vue": "^4.3.2",
77 | "sass-loader": "^6.0.6",
78 | "standard-version": "^4.2.0",
79 | "style-loader": "^0.22.1",
80 | "terser": "^3.16.1",
81 | "util": "^0.11.0",
82 | "vue-loader": "^15.3.0",
83 | "vue-template-compiler": "^2.5.17",
84 | "webpack": "^4.29.0",
85 | "webpack-cli": "^3.1.0",
86 | "webpack-dev-server": "^3.1.14"
87 | },
88 | "engines": {
89 | "node": ">= 4.0.0",
90 | "npm": ">= 3.0.0"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-page-designer
2 |
3 |
4 |
5 |
6 |
7 | Live Demo
8 |
9 |
10 |
11 | A drag-and-drop mobile website builder base on Vue.
12 |
13 | ## Install
14 |
15 | ```bash
16 | yarn add vue-page-designer
17 | ```
18 |
19 | You can start it quickly, in main.js:
20 |
21 | ```js
22 | import Vue from 'vue';
23 | import vuePageDesigner from 'vue-page-designer'
24 | import 'vue-page-designer/dist/vue-page-designer.css'
25 | import App from './App.vue';
26 |
27 | Vue.use(vuePageDesigner);
28 |
29 | new Vue({
30 | el: '#app',
31 | render: h => h(App)
32 | });
33 | ```
34 |
35 | Next, use it:
36 |
37 | ```html
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 | ```
50 |
51 | A [example](https://fireyy.github.io/vue-page-designer/) ▶️, and [source](./example/). Also a [custom widget source](./example/widgets)
52 |
53 | # Options
54 |
55 | You can add custom components, save callback.
56 |
57 | | Props | Type | Description |
58 | | -------- | -------- | -------- |
59 | | value | `Object` | Editor initial value, you can pass the value of the save callback and resume the draft |
60 | | locale | `String` | Editor default locale. Now support 'cn' and 'en', default 'cn'. |
61 | | widgets | `Object` | Vue Components. Custom components for editor. see [Example](https://github.com/fireyy/vue-page-designer-widgets/blob/master/src/index.js) |
62 | | save | `(data) => void` | When you click the Save button, feed back to you to save the data |
63 | | upload | `(files) => Promise` | Editor upload function, allowing you to implement your own upload-file's request |
64 |
65 | ## Parameter: `value`
66 |
67 | The `value` came from `save`.
68 |
69 | ```html
70 |
71 |
72 |
73 |
74 |
75 | ```
76 |
77 | ## Parameter: `widgets`
78 |
79 | You can install default widget in `vue-page-designer-widgets`
80 |
81 | ```bash
82 | yarn add vue-page-designer-widgets
83 | ```
84 |
85 | Import and use it
86 |
87 | ```html
88 |
89 |
90 |
91 |
92 |
93 |
104 | ```
105 |
106 | Set locale to EN
107 |
108 | ```html
109 |
110 |
111 |
112 |
113 |
114 | ```
115 |
116 | ## Parameter: `save`
117 |
118 | ```html
119 |
120 |
121 | { console.log('send the value data to your server', data) }" />
122 |
123 |
124 | ```
125 |
126 | ## Parameter: `upload`
127 |
128 | ```html
129 |
130 |
131 |
132 |
133 |
134 |
147 | ```
148 |
--------------------------------------------------------------------------------
/src/plugins/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | function resolveSource (source, type) {
4 | return typeof type === 'function' ? type : source[type]
5 | }
6 |
7 | function normalizeMap (map) {
8 | return Array.isArray(map)
9 | ? map.map(k => ({ k, v: k }))
10 | : Object.keys(map).map(k => ({ k, v: map[k] }))
11 | }
12 |
13 | const createMapState = _store => states => {
14 | const res = {}
15 | const db = normalizeMap(states)
16 | for (const k in db) {
17 | let v = db[k]
18 | res[k] = function () {
19 | const store = _store || this.$vpd
20 | return typeof v === 'function'
21 | ? v.call(this, store.state)
22 | : store.state[v]
23 | }
24 | }
25 | return res
26 | }
27 |
28 | const mapToMethods = (sourceName, runnerName, _store) => map => {
29 | const res = {}
30 | const db = normalizeMap(map)
31 | for (const k in db) {
32 | let v = db[k]
33 | res[k] = function (payload) {
34 | const store = _store || this.$vpd
35 | const source = store[sourceName]
36 | const runner = store[runnerName]
37 | const actualSource = typeof v === 'function' ? v.call(this, source) : v
38 | return runner.call(store, actualSource, payload)
39 | }
40 | }
41 | return res
42 | }
43 |
44 | export default class Store {
45 | constructor (
46 | { state, mutations = {}, actions = {}, plugins, subscribers = [] } = {}
47 | ) {
48 | this.vm = new Vue({
49 | data: {
50 | $$state: typeof state === 'function' ? state() : state
51 | }
52 | })
53 | this.mutations = mutations
54 | this.actions = actions
55 | this.subscribers = subscribers
56 |
57 | if (plugins) {
58 | plugins.forEach(p => this.use(p))
59 | }
60 |
61 | this.mapState = createMapState(this)
62 | this.mapActions = mapToMethods('actions', 'dispatch', this)
63 | this.mapMutations = mapToMethods('mutations', 'commit', this)
64 | }
65 |
66 | get state () {
67 | return this.vm.$data.$$state
68 | }
69 |
70 | set state (v) {
71 | if (process.env.NODE_ENV === 'development') {
72 | throw new Error(
73 | '[puex] store.state is read-only, use store.replaceState(state) instead'
74 | )
75 | }
76 | }
77 |
78 | $emit (event, ...args) {
79 | return this.vm.$emit(event, ...args)
80 | }
81 |
82 | $on (event, callback) {
83 | return this.vm.$on(event, callback)
84 | }
85 |
86 | subscribe (sub) {
87 | this.subscribers.push(sub)
88 | return () => this.subscribers.splice(this.subscribers.indexOf(sub), 1)
89 | }
90 |
91 | commit (type, payload) {
92 | this.subscribers.forEach(sub => sub({ type, payload }, this.state))
93 | const mutation = resolveSource(this.mutations, type)
94 | return mutation && mutation(this.state, payload)
95 | }
96 |
97 | dispatch (type, payload) {
98 | const action = resolveSource(this.actions, type)
99 | const ctx = {
100 | state: this.state,
101 | dispatch: this.dispatch.bind(this),
102 | commit: this.commit.bind(this),
103 | store: this
104 | }
105 | return Promise.resolve(action && action(ctx, payload))
106 | }
107 |
108 | use (fn) {
109 | fn(this)
110 | return this
111 | }
112 |
113 | replaceState (state) {
114 | this.vm.$data.$$state = state
115 | return this
116 | }
117 | }
118 |
119 | export const mapState = createMapState()
120 | export const mapActions = mapToMethods('actions', 'dispatch')
121 | export const mapMutations = mapToMethods('mutations', 'commit')
122 |
--------------------------------------------------------------------------------
/src/components/navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
113 |
114 |
147 |
--------------------------------------------------------------------------------
/src/components/panel/style.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
{{ $t('data.levels') }}
9 |
{{ activeElement.z }}
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
{{ $t('data.names.width') }}
21 |
{{ activeElement.width }}
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
{{ $t('data.names.height') }}
33 |
{{ activeElement.height }}
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
{{ $t('data.names.left') }}
45 |
{{ activeElement.left }}
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
{{ $t('data.names.top') }}
57 |
{{ activeElement.top }}
58 |
59 |
63 |
64 |
65 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
{{ $t('data.names.belonging') }}
80 |
81 |
82 | page
83 | {{ val }}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
126 |
--------------------------------------------------------------------------------
/src/components/toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
85 |
86 |
149 |
--------------------------------------------------------------------------------
/src/components/uploader.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
151 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
17 |
{{ zoom }}%
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
104 |
105 |
167 |
--------------------------------------------------------------------------------
/src/components/viewport/ref-lines.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
145 |
146 |
170 |
--------------------------------------------------------------------------------
/src/components/viewport/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
14 |
15 |
28 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
188 |
189 |
222 |
--------------------------------------------------------------------------------
/src/components/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
32 |
33 |
34 |
185 |
186 |
248 |
--------------------------------------------------------------------------------
/src/store/mutation.js:
--------------------------------------------------------------------------------
1 | const generate = require('nanoid/generate')
2 |
3 | export default {
4 | // 选中元件与取消选中
5 | select (state, payload) {
6 | state.uuid = payload.uuid
7 | if (payload.uuid === -1) {
8 | state.activeElement = state.page
9 | state.type = 'page'
10 | } else {
11 | let widget = state.widgets.find(w => w.uuid === payload.uuid)
12 | state.activeElement = widget
13 | state.type = widget.type
14 | }
15 | },
16 |
17 | // 设置 mousemove 操作的初始值
18 | initmove (state, payload) {
19 | state.startX = payload.startX
20 | state.startY = payload.startY
21 | state.originX = payload.originX
22 | state.originY = payload.originY
23 | state.moving = true
24 | },
25 |
26 | // 元件移动结束
27 | stopmove (state) {
28 | state.moving = false
29 | },
30 |
31 | // 移动元件
32 | move (state, payload) {
33 | var target = state.activeElement
34 | var dx = payload.x - state.startX
35 | var dy = payload.y - state.startY
36 | var left = state.originX + Math.floor(dx * 100 / state.zoom)
37 | var top = state.originY + Math.floor(dy * 100 / state.zoom)
38 |
39 | target.left = left > 0 ? left : 0
40 | target.top = top > 0 ? top : 0
41 | },
42 |
43 | // 调整元件尺寸
44 | resize (state, payload) {
45 | var dx = payload.x - state.startX
46 | var dy = payload.y - state.startY
47 | var value
48 |
49 | if (payload.type === 'right') {
50 | value = state.originX + Math.floor(dx * 100 / state.zoom)
51 | state.activeElement.width = value > 10 ? value : 10
52 | return
53 | }
54 |
55 | if (payload.type === 'down') {
56 | value = state.originX + Math.floor(dy * 100 / state.zoom)
57 | state.activeElement.height = value > 10 ? value : 10
58 | return
59 | }
60 |
61 | if (payload.type === 'left') {
62 | var left = state.originX + Math.floor(dx * 100 / state.zoom)
63 | var width = state.originY - Math.floor(dx * 100 / state.zoom)
64 | state.activeElement.left = left > 0 ? left : 0
65 | state.activeElement.width = width > 10 ? width : 10
66 | return
67 | }
68 |
69 | if (payload.type === 'up') {
70 | var top = state.originX + Math.floor(dy * 100 / state.zoom)
71 | var height = state.originY - Math.floor(dy * 100 / state.zoom)
72 | state.activeElement.top = top > 0 ? top : 0
73 | state.activeElement.height = height > 10 ? height : 10
74 | }
75 | },
76 |
77 | // 复制元件
78 | copy (state, payload) {
79 | if (state.type !== 'page') {
80 | var copy = Object.assign({}, state.activeElement, {top: state.top, uuid: generate('1234567890abcdef', 10)})
81 |
82 | // 由于容器的名称必须是唯一的,故复制容器需作处理
83 | if (state.activeElement.isContainer) {
84 | var name = state.activeElement.name
85 | if (name) {
86 | // 设置容器副本的名称
87 | var copyName = name.split('-')[0] + '-' + state.counter
88 | copy.name = copyName
89 |
90 | // 复制容器内的图片和文本
91 | for (var i = 0, len = state.widgets.length; i < len; i++) {
92 | if (state.widgets[i].belong === name) {
93 | state.widgets.push(
94 | Object.assign({}, state.widgets[i], { belong: copyName })
95 | )
96 | }
97 | }
98 |
99 | state.counter += 1
100 | }
101 | }
102 |
103 | state.widgets.push(copy)
104 | }
105 | },
106 |
107 | // 更新元件初始 top 值
108 | updateSrollTop (state, top) {
109 | state.top = top
110 | },
111 |
112 | // 页面缩放
113 | zoom (state, val) {
114 | state.zoom = val
115 | },
116 |
117 | // 初始化选中对象
118 | initActive (state) {
119 | state.activeElement = state.page
120 | },
121 |
122 | // 删除选中元件
123 | delete (state) {
124 | var type = state.type
125 | if (type === 'page') return
126 |
127 | // 如果删除的是容器,须将内部元件一并删除
128 | if (state.activeElement.isContainer) {
129 | var name = state.activeElement.name
130 |
131 | for (var i = 0; i < state.widgets.length; i++) {
132 | if (state.widgets[i].belong === name) {
133 | state.widgets.splice(i, 1)
134 | }
135 | }
136 | }
137 |
138 | // 删除元件
139 | state.widgets.splice(state.index, 1)
140 |
141 | // 重置 activeElement
142 | state.activeElement = state.page
143 | // state.type = 'page'
144 | state.uuid = -1
145 | },
146 |
147 | // 添加组件
148 | addWidget (state, { data: data = null, item }) {
149 | let def = { top: state.top, uuid: generate('1234567890abcdef', 10) }
150 | let setting = JSON.parse(JSON.stringify(item.setting))
151 |
152 | if (setting.isContainer) {
153 | setting.name = def.uuid
154 | }
155 |
156 | if (data) {
157 | data.forEach(function (val) {
158 | state.widgets.push(Object.assign(setting, val, def))
159 | })
160 | } else {
161 | state.widgets.push(Object.assign(setting, def))
162 | }
163 | },
164 |
165 | // 替换图片
166 | replaceImage (state, payload) {
167 | state.activeElement.width = payload[0].width
168 | state.activeElement.url = payload[0].url
169 | },
170 |
171 | // 添加容器背景图
172 | addContainerBackPic (state, payload) {
173 | state.activeElement.backPic = payload[0].url
174 | state.activeElement.backPicUrl = payload[0].src
175 | state.activeElement.width = payload[0].width
176 | state.activeElement.height = payload[0].height
177 | },
178 |
179 | // 添加背景图
180 | addBackPic (state, payload) {
181 | state.activeElement.backPic = payload[0].url
182 | state.activeElement.backPicUrl = payload[0].src
183 | },
184 |
185 | // 添加动画
186 | addAnimation (state) {
187 | state.animation.push({
188 | name: '',
189 | duration: 3,
190 | delay: 0,
191 | iteration: 1,
192 | timing: 'linear',
193 | direction: 'normal',
194 | fill: 'none',
195 | keyframes: [
196 | {
197 | stop: 0,
198 | css: ''
199 | }
200 | ]
201 | })
202 | },
203 |
204 | // 为动画添加 keyframe
205 | addkeyframe (state, name) {
206 | state.animation.map(val => {
207 | if (val.name === name) {
208 | val.keyframes.push({
209 | stop: 0,
210 | css: ''
211 | })
212 | }
213 | })
214 | },
215 |
216 | // 动画的播放与停止
217 | setAnimation (state, status) {
218 | state.playState = status
219 | },
220 |
221 | // 更新数据
222 | updateData (state, {uuid, key, value}) {
223 | let widget = state.widgets.find(w => w.uuid === uuid)
224 | widget[key] = value
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | ## [1.1.1](https://github.com/fireyy/vue-page-designer/compare/v1.1.0...v1.1.1) (2021-03-22)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * [#33](https://github.com/fireyy/vue-page-designer/issues/33) remove vue-i18n, use inter plugin ([8c1e9de](https://github.com/fireyy/vue-page-designer/commit/8c1e9de))
12 | * for of ([d19f41f](https://github.com/fireyy/vue-page-designer/commit/d19f41f))
13 | * regx ([3cf96fe](https://github.com/fireyy/vue-page-designer/commit/3cf96fe))
14 |
15 |
16 |
17 |
18 | # [1.1.0](https://github.com/fireyy/vue-page-designer/compare/v1.0.1...v1.1.0) (2020-09-09)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * Github Security Alerts ([6dba672](https://github.com/fireyy/vue-page-designer/commit/6dba672))
24 | * use terser instead of uglify-es ([88b09a4](https://github.com/fireyy/vue-page-designer/commit/88b09a4))
25 |
26 |
27 | ### Features
28 |
29 | * [#17](https://github.com/fireyy/vue-page-designer/issues/17) add i18n support ([e6cda99](https://github.com/fireyy/vue-page-designer/commit/e6cda99))
30 | * add parameter locale ([8000512](https://github.com/fireyy/vue-page-designer/commit/8000512))
31 |
32 |
33 |
34 |
35 | ## [1.0.1](https://github.com/fireyy/vue-page-designer/compare/v1.0.0...v1.0.1) (2019-01-22)
36 |
37 |
38 |
39 |
40 | # [1.0.0](https://github.com/fireyy/vue-page-designer/compare/v0.7.1...v1.0.0) (2019-01-22)
41 |
42 |
43 | ### Bug Fixes
44 |
45 | * container fixed-width 1280px ([4a9cd1f](https://github.com/fireyy/vue-page-designer/commit/4a9cd1f))
46 | * ref line duplicate keys ([8339f14](https://github.com/fireyy/vue-page-designer/commit/8339f14))
47 | * ref line duplicate keys ([5adf53b](https://github.com/fireyy/vue-page-designer/commit/5adf53b))
48 | * rollup bundle external vue ([e06412d](https://github.com/fireyy/vue-page-designer/commit/e06412d))
49 | * sass import path ([dad291b](https://github.com/fireyy/vue-page-designer/commit/dad291b))
50 | * use vpd instead of store ([7e4be36](https://github.com/fireyy/vue-page-designer/commit/7e4be36))
51 |
52 |
53 | ### Features
54 |
55 | * layer-list show widget title ([4892aa0](https://github.com/fireyy/vue-page-designer/commit/4892aa0))
56 |
57 |
58 |
59 |
60 | ## [0.7.1](https://github.com/fireyy/vue-page-designer/compare/v0.7.0...v0.7.1) (2018-01-03)
61 |
62 |
63 | ### Bug Fixes
64 |
65 | * **remove:** .cleafix hack ([84e5627](https://github.com/fireyy/vue-page-designer/commit/84e5627))
66 | * copy widget ([876d05b](https://github.com/fireyy/vue-page-designer/commit/876d05b))
67 | * keycode ([49ccaed](https://github.com/fireyy/vue-page-designer/commit/49ccaed))
68 | * resize control ([d74afee](https://github.com/fireyy/vue-page-designer/commit/d74afee))
69 | * style config ([cdc9093](https://github.com/fireyy/vue-page-designer/commit/cdc9093))
70 |
71 |
72 |
73 |
74 | # [0.7.0](https://github.com/fireyy/vue-page-designer/compare/v0.6.0...v0.7.0) (2017-12-29)
75 |
76 |
77 | ### Bug Fixes
78 |
79 | * color picker ([c9ad358](https://github.com/fireyy/vue-page-designer/commit/c9ad358))
80 | * depend vue-page-designer-widgets ([fa820af](https://github.com/fireyy/vue-page-designer/commit/fa820af))
81 |
82 |
83 | ### Features
84 |
85 | * demo widgets ([9acfa02](https://github.com/fireyy/vue-page-designer/commit/9acfa02))
86 | * use store instead of window. ([f4ed3b8](https://github.com/fireyy/vue-page-designer/commit/f4ed3b8))
87 |
88 |
89 |
90 |
91 | # [0.6.0](https://github.com/fireyy/vue-page-designer/compare/v0.5.4...v0.6.0) (2017-12-27)
92 |
93 |
94 | ### Bug Fixes
95 |
96 | * child widget data-title ([4742551](https://github.com/fireyy/vue-page-designer/commit/4742551))
97 |
98 |
99 | ### Features
100 |
101 | * add updateData api ([27f6c7c](https://github.com/fireyy/vue-page-designer/commit/27f6c7c))
102 | * container use slot ([b7825c9](https://github.com/fireyy/vue-page-designer/commit/b7825c9))
103 |
104 |
105 |
106 |
107 | ## [0.5.4](https://github.com/fireyy/vue-page-designer/compare/v0.5.3...v0.5.4) (2017-12-26)
108 |
109 |
110 | ### Bug Fixes
111 |
112 | * svg remote path ([7e6d249](https://github.com/fireyy/vue-page-designer/commit/7e6d249))
113 |
114 |
115 |
116 |
117 | ## [0.5.3](https://github.com/fireyy/vue-page-designer/compare/v0.5.2...v0.5.3) (2017-12-26)
118 |
119 |
120 | ### Bug Fixes
121 |
122 | * svg remote path ([c71d997](https://github.com/fireyy/vue-page-designer/commit/c71d997))
123 |
124 |
125 |
126 |
127 | ## [0.5.2](https://github.com/fireyy/vue-page-designer/compare/v0.5.1...v0.5.2) (2017-12-26)
128 |
129 |
130 |
131 |
132 | ## [0.5.1](https://github.com/fireyy/vue-page-designer/compare/v0.5.0...v0.5.1) (2017-12-26)
133 |
134 |
135 |
136 |
137 | # [0.5.0](https://github.com/fireyy/vue-page-designer/compare/v0.4.0...v0.5.0) (2017-12-26)
138 |
139 |
140 | ### Bug Fixes
141 |
142 | * example static path ([e30f5bf](https://github.com/fireyy/vue-page-designer/commit/e30f5bf))
143 | * svg remote path ([85ffbe9](https://github.com/fireyy/vue-page-designer/commit/85ffbe9))
144 |
145 |
146 | ### Features
147 |
148 | * simple defaultUpload function ([69064fa](https://github.com/fireyy/vue-page-designer/commit/69064fa))
149 |
150 |
151 |
152 |
153 | # [0.4.0](https://github.com/fireyy/vue-page-designer/compare/v0.3.0...v0.4.0) (2017-12-22)
154 |
155 |
156 | ### Features
157 |
158 | * use external svg file ([a22a419](https://github.com/fireyy/vue-page-designer/commit/a22a419))
159 |
160 |
161 |
162 |
163 | # [0.3.0](https://github.com/fireyy/vue-page-designer/compare/v0.2.0...v0.3.0) (2017-12-22)
164 |
165 |
166 | ### Features
167 |
168 | * add upload function for use your owner upload api ([becf9eb](https://github.com/fireyy/vue-page-designer/commit/becf9eb))
169 | * widget icon use svg string ([67e6b05](https://github.com/fireyy/vue-page-designer/commit/67e6b05))
170 |
171 |
172 |
173 |
174 | # [0.2.0](https://github.com/fireyy/vue-page-designer/compare/v0.1.0...v0.2.0) (2017-12-21)
175 |
176 |
177 | ### Features
178 |
179 | * move widgetStyle to widget ([37550b6](https://github.com/fireyy/vue-page-designer/commit/37550b6))
180 |
181 |
182 |
183 |
184 | # [0.1.0](https://github.com/fireyy/vue-page-designer/compare/v0.0.1...v0.1.0) (2017-12-21)
185 |
186 |
187 | ### Bug Fixes
188 |
189 | * clean ([ec7bcff](https://github.com/fireyy/vue-page-designer/commit/ec7bcff))
190 | * drag move active element ([f0f68fd](https://github.com/fireyy/vue-page-designer/commit/f0f68fd))
191 | * Icon unit test ([e608893](https://github.com/fireyy/vue-page-designer/commit/e608893))
192 | * remove ([00a1d47](https://github.com/fireyy/vue-page-designer/commit/00a1d47))
193 | * remove hoverPic ([869a7db](https://github.com/fireyy/vue-page-designer/commit/869a7db))
194 | * remove unused slider.scss ([c905a58](https://github.com/fireyy/vue-page-designer/commit/c905a58))
195 | * remove unused switcher ([581b2ed](https://github.com/fireyy/vue-page-designer/commit/581b2ed))
196 | * scroll to element when not in view ([78494de](https://github.com/fireyy/vue-page-designer/commit/78494de))
197 | * scroll to layer ([827fc3b](https://github.com/fireyy/vue-page-designer/commit/827fc3b))
198 | * use uuid to check active element ([1d45405](https://github.com/fireyy/vue-page-designer/commit/1d45405))
199 |
200 |
201 | ### Features
202 |
203 | * add Icon unit test ([0e4ce11](https://github.com/fireyy/vue-page-designer/commit/0e4ce11))
204 | * add jsconfig ([5eadf69](https://github.com/fireyy/vue-page-designer/commit/5eadf69))
205 | * add release script ([aceb55c](https://github.com/fireyy/vue-page-designer/commit/aceb55c))
206 | * add toast type ([350573d](https://github.com/fireyy/vue-page-designer/commit/350573d))
207 | * export save event ([a463347](https://github.com/fireyy/vue-page-designer/commit/a463347))
208 | * pass initial value to editor ([8b72c11](https://github.com/fireyy/vue-page-designer/commit/8b72c11))
209 |
--------------------------------------------------------------------------------
/src/components/panel/animation.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
{{ $t('panel.animation.select') }}
20 |
21 |
22 | {{ $t('data.no') }}
23 | {{ val }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
{{ $t('data.name') }}
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
{{ $t('data.duration') }}
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
{{ $t('data.delay') }}
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
{{ $t('data.iteration') }}
70 |
71 |
75 |
76 |
77 |
78 |
79 |
80 |
{{ $t('data.timing') }}
81 |
82 |
83 | linear
84 | ease
85 | ease-in
86 | ease-out
87 | ease-in-out
88 |
89 |
90 |
91 |
92 |
93 |
94 |
{{ $t('data.direction') }}
95 |
96 |
97 | normal
98 | reverse
99 | alternate
100 | alternate-reverse
101 |
102 |
103 |
104 |
105 |
106 |
107 |
fill-mode
108 |
109 |
110 | none
111 | forwards
112 | backwards
113 | both
114 |
115 |
116 |
117 |
118 |
119 |
120 |
123 |
124 |
125 |
stop - {{ i }}
126 |
{{ val.stop }}%
127 |
128 |
131 |
132 |
133 |
136 |
137 |
141 | {{ $t('data.actions.add') }}
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
281 |
282 |
293 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2025] [fireyy]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------