├── .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 | 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 | 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 | 12 | 13 | 51 | 52 | 57 | -------------------------------------------------------------------------------- /src/components/toast.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 49 | 50 | 64 | -------------------------------------------------------------------------------- /example/widgets/button/style.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 32 | 33 | 63 | 64 | 140 | -------------------------------------------------------------------------------- /src/components/viewport/size-control.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 93 | 104 | ``` 105 | 106 | Set locale to EN 107 | 108 | ```html 109 | 114 | ``` 115 | 116 | ## Parameter: `save` 117 | 118 | ```html 119 | 124 | ``` 125 | 126 | ## Parameter: `upload` 127 | 128 | ```html 129 | 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 | 37 | 38 | 113 | 114 | 147 | -------------------------------------------------------------------------------- /src/components/panel/style.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 126 | -------------------------------------------------------------------------------- /src/components/toolbar.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 85 | 86 | 149 | -------------------------------------------------------------------------------- /src/components/uploader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 151 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 104 | 105 | 167 | -------------------------------------------------------------------------------- /src/components/viewport/ref-lines.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 145 | 146 | 170 | -------------------------------------------------------------------------------- /src/components/viewport/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 188 | 189 | 222 | -------------------------------------------------------------------------------- /src/components/slider.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |