├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── main │ ├── assets │ │ ├── js │ │ │ ├── api │ │ │ │ ├── config.js │ │ │ │ ├── index.js │ │ │ │ ├── roseApi │ │ │ │ │ ├── instance.js │ │ │ │ │ └── index.js │ │ │ │ └── util.js │ │ │ ├── template.js │ │ │ └── helper.js │ │ ├── font │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.scss │ │ │ └── iconfont.svg │ │ ├── images │ │ │ ├── arrows.png │ │ │ └── iphone.png │ │ └── style │ │ │ ├── var.scss │ │ │ └── index.scss │ ├── store │ │ ├── helper.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── state.js │ │ ├── actions.js │ │ └── mutations.js │ ├── index.html │ ├── index.js │ ├── App.vue │ └── components │ │ ├── right-menu.vue │ │ ├── left-menu.vue │ │ ├── main-container.vue │ │ └── editor-jsx.vue └── preview │ ├── index.html │ ├── index.js │ └── App.vue ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/main/assets/js/api/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // env: 'production' 3 | env: 'dev' 4 | }; 5 | -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.eot -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /src/main/assets/images/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/images/arrows.png -------------------------------------------------------------------------------- /src/main/assets/images/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/images/iphone.png -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.woff -------------------------------------------------------------------------------- /src/main/assets/js/api/index.js: -------------------------------------------------------------------------------- 1 | import * as roseApi from './roseApi/index.js' 2 | 3 | export { 4 | roseApi, 5 | }; 6 | -------------------------------------------------------------------------------- /src/main/assets/style/var.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #21374B; 2 | $success-color: rgb(0, 168, 84); 3 | $error-color: #e96900; 4 | $white: #fff; 5 | $info: #2973b7; 6 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /src/main/assets/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "../font/iconfont.scss"; 2 | 3 | body,html { 4 | margin: 0; 5 | padding: 0; 6 | height: 100%; 7 | } 8 | #app { 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | rose 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/assets/js/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author monkeywang 3 | * Date: 2018/2/2 4 | * 存放模板仓库,如果需要新增模板,在这里进行添加 5 | * previewImg 指定模板的预览图片 6 | */ 7 | export default [{ 8 | gitName: 'tmp1', 9 | previewImg: 'https://pic.51zhangdan.com/u51/storage/17/1092cb38-12df-d0ab-f370-878f969ff2a7.png', 10 | desc: '活动基础模板' 11 | }] 12 | -------------------------------------------------------------------------------- /src/main/store/helper.js: -------------------------------------------------------------------------------- 1 | export default { 2 | postMsgToParent (message) { 3 | window.parent.postMessage( 4 | message, 5 | `${location.origin}/` 6 | ) 7 | }, 8 | postMsgToChild (message) { 9 | if (!window.frames[0]) return 10 | window.frames[0].postMessage( 11 | message, 12 | location.origin 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/store/getters.js: -------------------------------------------------------------------------------- 1 | export const getters = { 2 | currentEditor: (state) => state.currentEditor, 3 | components: (state) => { return state.sortUI[state.currentPage] ? state.sortUI[state.currentPage].components : [] }, 4 | sortUI: (state) => state.sortUI[state.currentPage], 5 | state: (state) => state, 6 | currentPage: (state) => state.currentPage, 7 | save: (state) => state.save 8 | } 9 | -------------------------------------------------------------------------------- /src/main/assets/js/api/roseApi/instance.js: -------------------------------------------------------------------------------- 1 | import { createAPI } from '../util'; 2 | import config from '../config'; 3 | 4 | const baseUrl = { 5 | mock: ['https://www.easy-mock.com/mock/5c6ead0e37202863fa565925'], 6 | dev: ['http://localhost:7001'], 7 | production: ['https://www.easy-mock.com/mock/5c6ead0e37202863fa565925'] 8 | }[process.env.NODE_ENV === 'development' ? config.env : process.env.NODE_ENV]; 9 | 10 | export default createAPI(...baseUrl); 11 | -------------------------------------------------------------------------------- /src/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rose 7 | 8 | 9 |
10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/preview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rose 7 | 8 | 9 |
10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/assets/js/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author monkeywang 3 | * Date: 2018/3/27 4 | */ 5 | export function getHashQuery() { 6 | let url = location.hash; //获取url中"?"符后的字串 7 | let theRequest = {}; 8 | if (url.indexOf("?") != -1) { 9 | let str = url.substring(url.indexOf("?") + 1, url.length); 10 | let strs = str.split("&"); 11 | for(var i = 0; i < strs.length; i ++) { 12 | theRequest[strs[i].split("=")[0]]=unescape(strs[i].split("=")[1]); 13 | } 14 | } 15 | return theRequest; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import store from './store' 6 | import iView from 'iview' 7 | import 'iview/dist/styles/iview.css' 8 | 9 | Vue.config.productionTip = false 10 | 11 | Vue.use(iView) 12 | 13 | /* eslint-disable no-new */ 14 | new Vue({ 15 | el: '#app', 16 | store, 17 | components: { App }, 18 | template: '' 19 | }) 20 | -------------------------------------------------------------------------------- /src/preview/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author muwoo 3 | * Date: 2019/2/24 4 | */ 5 | import Vue from 'vue' 6 | import App from './App.vue' 7 | import iView from 'iview' 8 | import 'iview/dist/styles/iview.css' 9 | 10 | import '../main/assets/style/index.scss' 11 | import store from '../main/store' 12 | 13 | import RButton from '@muwoo.cn/r-button' 14 | 15 | Vue.use(RButton) 16 | 17 | Vue.use(iView) 18 | 19 | Vue.config.productionTip = false 20 | 21 | /* tslint:disable */ 22 | new Vue({ 23 | el: '#app', 24 | store, 25 | template: '', 26 | components: { App } 27 | }) 28 | -------------------------------------------------------------------------------- /src/main/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author monkeywang 3 | * Date: 2018/2/1 4 | */ 5 | import Vuex from 'vuex' 6 | import { state } from './state' 7 | import { mutations } from './mutations' 8 | import { actions } from './actions' 9 | import { getters } from './getters' 10 | import Vue from 'vue' 11 | 12 | Vue.use(Vuex) 13 | 14 | const store = new Vuex.Store({ 15 | state, 16 | mutations, 17 | actions, 18 | getters 19 | }) 20 | 21 | export default store 22 | 23 | window.addEventListener('message',(e) => { 24 | // 不接受消息源来自于当前窗口的消息 25 | if (e.source === window || e.data === 'loaded') { 26 | return 27 | } 28 | store.dispatch(e.data.type, e.data.value) 29 | }) 30 | -------------------------------------------------------------------------------- /src/main/store/state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author muwoo 3 | * Date: 2019/2/24 4 | */ 5 | export const state = { 6 | save: true, 7 | /** 8 | * current editor index 9 | */ 10 | currentEditor: -1, 11 | currentPage: 0, 12 | /** 13 | * project UI config 14 | */ 15 | sortUI: [{ 16 | /** 17 | * page title 18 | */ 19 | "repoUrl": "direct:https://github.com/muwoo/rose-simple.git", // 对应的git仓库 20 | "repoName": "luban", 21 | title: '标题', 22 | /** 23 | * the project container background 24 | */ 25 | bgColor: '#fff', 26 | /** 27 | * the project container background image 28 | */ 29 | bgImage: '', 30 | /** 31 | * the project container background image's size 32 | */ 33 | bgSize: null, 34 | /** 35 | * for bury service 36 | */ 37 | service: '', 38 | /** 39 | * what's components does the project contains 40 | */ 41 | components: [] 42 | }] 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/assets/js/api/util.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | function createAPI(baseURL, Authorization) { 4 | return function (conf) { 5 | conf = conf || {}; 6 | if (conf.opts && conf.opts.data && conf.opts.data.experiment_id) { 7 | conf.opts.data.hash = CryptoJS.AES.encrypt(JSON.stringify(conf.opts.data), key).toString(); 8 | } 9 | return axios(Object.assign({}, { 10 | url: conf.url, 11 | baseURL: baseURL, 12 | method: conf.method, 13 | Authorization 14 | }, conf.opts)).then(res => { 15 | return res.data 16 | }); 17 | }; 18 | } 19 | 20 | function convertRESTAPI(url, opts) { 21 | if (!opts || !opts.path) return url; 22 | 23 | const pathKeys = Object.keys(opts.path); 24 | 25 | pathKeys.forEach((key) => { 26 | const r = new RegExp('(:' + key + '|{' + key + '})', 'g'); 27 | url = url.replace(r, opts.path[key]); 28 | }); 29 | 30 | return url; 31 | } 32 | 33 | export { 34 | createAPI, 35 | convertRESTAPI 36 | }; 37 | -------------------------------------------------------------------------------- /src/main/assets/js/api/roseApi/index.js: -------------------------------------------------------------------------------- 1 | import instance from './instance'; 2 | 3 | /** 获取以保存页面详情 */ 4 | function rose_page_detail_get(opts = {}) { 5 | return instance({ 6 | method: 'get', 7 | url: '/api/v1/page/detail', 8 | opts: opts 9 | }); 10 | } 11 | 12 | /** 获取已保存的模板列表 */ 13 | function rose_page_list_get(opts = {}) { 14 | return instance({ 15 | method: 'get', 16 | url: '/api/v1/page/list', 17 | opts: opts 18 | }); 19 | } 20 | 21 | /** 根据component name获取component详情 */ 22 | function rose_component_detail_get(opts = {}) { 23 | return instance({ 24 | method: 'get', 25 | url: '/tpl/component', 26 | opts: opts 27 | }); 28 | } 29 | 30 | /** 渲染 */ 31 | function rose_page_operate_post(opts = {}) { 32 | return instance({ 33 | method: 'post', 34 | url: '/tpl/renderTpl', 35 | opts: opts 36 | }); 37 | } 38 | 39 | /** 根据templateId获取模板信息 */ 40 | function rose_tpl_detail_get(opts = {}) { 41 | return instance({ 42 | method: 'get', 43 | url: '/tpl/detail', 44 | opts: opts 45 | }); 46 | } 47 | 48 | /** 根据start limit获取模版信息列表 */ 49 | function rose_tpl_list_get(opts = {}) { 50 | return instance({ 51 | method: 'get', 52 | url: '/api/v1/tpl/list', 53 | opts: opts 54 | }); 55 | } 56 | export { 57 | rose_page_detail_get, 58 | rose_page_list_get, 59 | rose_component_detail_get, 60 | rose_page_operate_post, 61 | rose_tpl_detail_get, 62 | rose_tpl_list_get, 63 | }; 64 | -------------------------------------------------------------------------------- /src/main/store/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author muwoo 3 | * Date: 2019/2/24 4 | */ 5 | import helper from './helper' 6 | import {roseApi} from '../assets/js/api' 7 | export const actions = { 8 | setCurrentPage ({commit, state}, page) { 9 | commit('setCurrentPage',page) 10 | helper.postMsgToChild({type: 'syncState', value: state}) 11 | }, 12 | saveConfig ({commit, state}, value = true) { 13 | commit('saveConfig', value) 14 | }, 15 | clearWorkbench ({commit}) { 16 | commit('clearWorkbench') 17 | }, 18 | selectComponent({commit, state}, index) { 19 | commit('currentEditor', index) 20 | commit('saveConfig', false) 21 | helper.postMsgToParent({type: 'syncState', value: state}) 22 | }, 23 | async addComponent ({commit, state}, value) { 24 | let res = await roseApi.rose_component_detail_get({ 25 | params: { 26 | name: value.name 27 | } 28 | }) 29 | res.data.style = res.data.style || {container: {}} 30 | commit('addComponent', Object.assign({} , value, res)) 31 | commit('saveConfig', false) 32 | helper.postMsgToChild({type: 'syncState', value: state}) 33 | }, 34 | removeComponent({commit, state}, index) { 35 | commit('removeComponent', index) 36 | commit('saveConfig', false) 37 | helper.postMsgToParent({type: 'syncState', value: state}) 38 | }, 39 | updateSortUIProps({commit, state}, {key, value}) { 40 | commit('updateSortUIProps', {key, value}); 41 | commit('saveConfig', false) 42 | helper.postMsgToChild({type: 'syncState', value: state}) 43 | }, 44 | syncState({commit, state}, value) { 45 | commit('syncState', value); 46 | commit('saveConfig', false) 47 | helper.postMsgToChild({type: 'syncState', value: state}) 48 | }, 49 | addConfig ({commit, state}) { 50 | commit('addConfig'); 51 | helper.postMsgToChild({type: 'syncState', value: state}) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 84 | 85 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/rose/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rose", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "monkeyWangs <2424880409@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "@muwoo.cn/r-button": "^0.0.2", 14 | "html2canvas": "^1.0.0-alpha.12", 15 | "iview": "^3.1.1", 16 | "rose-text": "^0.0.2", 17 | "vue": "^2.5.2", 18 | "vue-router": "^3.0.1", 19 | "vuedraggable": "^2.17.0", 20 | "vuex": "^3.1.0" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.2", 24 | "axios": "^0.18.0", 25 | "babel-core": "^6.22.1", 26 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 27 | "babel-loader": "^7.1.1", 28 | "babel-plugin-syntax-jsx": "^6.18.0", 29 | "babel-plugin-transform-runtime": "^6.22.0", 30 | "babel-plugin-transform-vue-jsx": "^3.5.0", 31 | "babel-preset-env": "^1.3.2", 32 | "babel-preset-stage-2": "^6.22.0", 33 | "chalk": "^2.0.1", 34 | "copy-webpack-plugin": "^4.0.1", 35 | "css-loader": "^0.28.0", 36 | "extract-text-webpack-plugin": "^3.0.0", 37 | "file-loader": "^1.1.4", 38 | "friendly-errors-webpack-plugin": "^1.6.1", 39 | "html-webpack-plugin": "^2.30.1", 40 | "node-notifier": "^5.1.2", 41 | "node-sass": "^4.11.0", 42 | "optimize-css-assets-webpack-plugin": "^3.2.0", 43 | "ora": "^1.2.0", 44 | "portfinder": "^1.0.13", 45 | "postcss-import": "^11.0.0", 46 | "postcss-loader": "^2.0.8", 47 | "postcss-url": "^7.2.1", 48 | "rimraf": "^2.6.0", 49 | "sass-loader": "^7.1.0", 50 | "semver": "^5.3.0", 51 | "shelljs": "^0.7.6", 52 | "uglifyjs-webpack-plugin": "^1.1.1", 53 | "url-loader": "^0.5.8", 54 | "vue-loader": "^13.3.0", 55 | "vue-style-loader": "^3.0.1", 56 | "vue-template-compiler": "^2.5.2", 57 | "webpack": "^3.6.0", 58 | "webpack-bundle-analyzer": "^2.9.0", 59 | "webpack-dev-server": "^2.9.1", 60 | "webpack-merge": "^4.1.0" 61 | }, 62 | "engines": { 63 | "node": ">= 6.0.0", 64 | "npm": ">= 3.0.0" 65 | }, 66 | "browserslist": [ 67 | "> 1%", 68 | "last 2 versions", 69 | "not ie <= 8" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/main/components/right-menu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 41 | 92 | -------------------------------------------------------------------------------- /src/main/store/mutations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author muwoo 3 | * Date: 2019/2/24 4 | */ 5 | 6 | export const mutations = { 7 | setCurrentPage (state, page) { 8 | state.currentPage = page 9 | state.currentEditor = -1 10 | }, 11 | saveConfig (state, value) { 12 | state.save = value 13 | }, 14 | /** 15 | * 设置当前正在编辑组建的序号 16 | * @param {State} state 17 | * @param {number} value 18 | */ 19 | currentEditor (state, value) { 20 | state.currentEditor = value 21 | }, 22 | /** 23 | * 根据选择组建的类型来新增组件 24 | * @param {State} state 25 | * @param {any} value 26 | */ 27 | addComponent (state, value) { 28 | state.currentEditor >= 0 ? 29 | state.sortUI[state.currentPage].components.splice(state.currentEditor + 1, 0, JSON.parse(JSON.stringify(value))) : 30 | state.sortUI[state.currentPage].components.push(JSON.parse(JSON.stringify(value))) 31 | state.currentEditor = state.currentEditor >= 0 ? 32 | state.currentEditor + 1 : 33 | state.sortUI[state.currentPage].components.length - 1; 34 | }, 35 | /** 36 | * 通过 sortUI 的 属性 37 | * @param {State} state 38 | * @param {any} key 39 | * @param {any} value 40 | */ 41 | updateSortUIProps (state, {key, value}) { 42 | state.sortUI[state.currentPage][key] = value 43 | }, 44 | /** 45 | * 删除特定组件 46 | * @param {State} state 47 | * @param {number} index 48 | */ 49 | removeComponent (state, index) { 50 | state.currentEditor = -1 51 | state.sortUI[state.currentPage].components.splice(index, 1) 52 | }, 53 | /** 54 | * 同步 state 状态 55 | * @param {State} state 56 | * @param {State} value 57 | */ 58 | syncState (state, value) { 59 | value.currentPage !== undefined ? state.currentPage = value.currentPage : 0 60 | value.currentEditor !== undefined ? state.currentEditor = value.currentEditor : '' 61 | value.sortUI !== undefined ? state.sortUI = value.sortUI : '' 62 | }, 63 | clearWorkbench (state) { 64 | state.currentEditor = -1 65 | state.save = true 66 | state.currentPage = 0 67 | state.sortUI = [{ 68 | title: '标题', 69 | bgColor: '#fe2f41', 70 | bgImage: '', 71 | bgSize: null, 72 | service: '', 73 | channel: '', 74 | components: [] 75 | }] 76 | }, 77 | addConfig (state) { 78 | state.sortUI.push({ 79 | title: '标题', 80 | bgColor: '#fe2f41', 81 | bgImage: '', 82 | bgSize: null, 83 | service: '', 84 | channel: '', 85 | components: [] 86 | }) 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/main/components/left-menu.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 52 | 97 | -------------------------------------------------------------------------------- /src/main/components/main-container.vue: -------------------------------------------------------------------------------- 1 | 12 | 53 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.scss: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('/src/assets/font/iconfont.eot?t=1535976220136'); /* IE9*/ 3 | src: url('/src/assets/font/iconfont.eot?t=1535976220136#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), 5 | url('/src/assets/font/iconfont.ttf?t=1535976220136') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 6 | url('/src/assets/font/iconfont.svg?t=1535976220136#iconfont') format('svg'); /* iOS 4.1- */ 7 | } 8 | 9 | .iconfont { 10 | font-family:"iconfont" !important; 11 | font-size:16px; 12 | font-style:normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | .icon-data:before { content: "\e67b"; } 17 | 18 | .icon-delete:before { content: "\e656"; } 19 | 20 | .icon-editorder:before { content: "\e672"; } 21 | 22 | .icon-iphone:before { content: "\e62e"; } 23 | 24 | .icon-add:before { content: "\e657"; } 25 | 26 | .icon-info:before { content: "\e611"; } 27 | 28 | .icon-publish:before { content: "\e635"; } 29 | 30 | .icon-link:before { content: "\e608"; } 31 | 32 | .icon-save:before { content: "\e6f0"; } 33 | 34 | .icon-center:before { content: "\e6dd"; } 35 | 36 | .icon-copy:before { content: "\e678"; } 37 | 38 | .icon-left:before { content: "\e63b"; } 39 | 40 | .icon-right:before { content: "\e63c"; } 41 | 42 | .icon-ceshi:before { content: "\e6da"; } 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/components/editor-jsx.vue: -------------------------------------------------------------------------------- 1 | 248 | 254 | -------------------------------------------------------------------------------- /src/preview/App.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 129 | 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Rose 3 | 活动页构建利器,用于快速搭建活动页 4 | 5 | [示例DEMO](https://muwoo.github.io/rose/) (逐步完善中...) 6 | 7 | [node构建服务](https://github.com/roooses/rose-server) 8 | 9 | [template 模板](https://github.com/roooses/rose-template) 10 | 11 | [文本组件示例](https://github.com/roooses/rose-text) 12 | 13 | ## 前言 14 | 如果你经常接触一些公司的活动页,可能会经常头疼以下问题:这些项目周期短,需求频繁,迭代快,技术要求不高,成长空间也小。但是我们还是马不停蹄的赶着产品提来的一个个需求,随着公司规模的增加,我们不可能无限制的增加人手不断地重复着这些活动。这里我就不具体介绍一些有的没的的一些概念了,因为要介绍的概念实在太多了,作为一个前端的我们,直接上代码撸就好了!!!! 15 | 16 | ## 目标 17 | 我们的目标是实现一个页面制作后台,在后台中我们可以对页面进行 `组件选择 --> 布局样式调整 --> 发布上线 --> 编辑修改`这样的流程操作。 18 | 19 | ## 架构设计 20 | 首先是要能提供组件给用户进行选择,那么我们需要一个`组件库`,然后需要对选择的组件进行布局样式调整,所以我们需要一个`页面编辑后台`接着我们需要将编辑产出的数据渲染成真实的页面,所以我们需要一个`node服务`和用于填充的`template 模板`。发布上线,这个直接对接各个公司内部的发布系统就好了,这里我们不做过多阐述。最后的编辑修改功能也就是针对配置的修改,所以我们需要一个数据库,这里我选择的是用了`mysql`。当然你也可以顺便做做权限管理,页面管理....等等之类的活。 21 | 啰嗦了这么长,我们来画个图,了解下大概的流程: 22 | 23 | 24 | 25 | ## 开撸 26 | #### 组件的实现 27 | 首先我们来实现组件这一部分,因为组件关联着后台编辑的预览和最后发布的使用。组件设计我们应该尽量保持组件的对外一致性,这样在进行渲染的时候,我们可以提供一个统一的对外数据接口。这里我们的技术选型是基于 Vue 的,所以下面的代码部分也主要是基于 Vue 的,但是万变不离其宗,其他语言也类似。 28 | 29 | 根据上图,我们的组件是会被一个个拆分单独发布到 `npm`仓库的,为什么这么设计呢?其实之前也考虑过设计成一个组件库,所有组件都包含在一个组件库内,这样只需要发布一个组件库包,用的时候按需加载就好了。后来在实践的过程中发现这样并不合适协同开发,其他前端如果想贡献组件,接入的改造成本也很大。举个🌰:小明在业务中写了个`Button`组件,这个组件经常会被其他项目复用,他想把这个组件贡献到我们的系统中,被模板使用,如果是一个组件库的话,他首先得拉取我们组件库的代码,然后按照组件库的规范格式进行提交。这样一来,偷懒的小明可能就不太愿意这么干,最爽的方法当然是在本地构建一个npm库,开发选用的是用`TypeScript`还是其他的我们不关心,选用的 Css 预处理器我们也不关心,甚至编码规范的`ESLint`我们也不关心。最后只需通过编译后的文件即可。这样就避免了一个组件库的约束。依托于NPM完善的发布/拉取,以及版本控制机制,可以让我们少做一些额外的工作,也可以快速的把平台搭建起来。 30 | 31 | 说了这么多,代码呢?,我们以一个`Button`为例,我们对外提供这样的形式组件: 32 | ```html 33 | 38 | 50 | ``` 51 | 可以看到我们只对外暴露了一个`props`,这样做法的好处是可以统一组件对外暴露的数据,组件内部爱怎么玩怎么玩。注意,这里我们也可以引入一些第三方组件库,比如`mint-ui`之类的。 52 | 53 | #### 后台编辑的实现 54 | 在写代码前,我们先考虑一下需要实现哪些功能: 55 | 1. 一个属性编辑区,提供给使用者编辑组件内部`props`的功能 56 | 2. 一个组件选择区,提供使用者选择需要的组件 57 | 3. 一个组件预览区,提供使用者拖拽排序页面预览的功能 58 | 59 | ###### 编辑区的实现 60 | 按照顺序,我们先来实现组件的属性编辑功能。我们要考虑,一个组件暴露出哪些可配置的信息。这些可配置的信息如何同步到后台编辑区,让使用者进行编辑,一个按钮的可配置信息可能是这样: 61 | 62 | ![image](https://user-images.githubusercontent.com/21073039/47420478-1cf80680-d7b1-11e8-8a22-be62f6da577d.png) 63 | 64 | 如果把这些配置全部写在后台库里面,根据当前选择的组件加载不同的配置,维护起来会相当麻烦,而且随着组件数量的增加,也会变得臃肿,所以我们可以将这些配置存储在服务端,后台只需要根据存储的规则进行解析便可,举个例子,我们其实可以存储这样的编辑配置: 65 | ```json 66 | [ 67 | { 68 | "blockName": "按钮布局设置", 69 | "settings": { 70 | "src": { 71 | "type": "input", 72 | "require": true, 73 | "label": "按钮文案" 74 | } 75 | } 76 | } 77 | ] 78 | ``` 79 | 我们在编辑后台,通过接口请求到这些配置,便可以进行规则渲染: 80 | ```js 81 | /** 82 | * 根据类型,选择创建对应的组件 83 | * @param {VNode} vm 84 | * @returns {any} 85 | */ 86 | createEditorElement (vm: VNode) { 87 | let dom = null 88 | switch (vm.config.type) { 89 | case 'align': 90 | dom = this.createAlignElement(vm) 91 | break; 92 | case 'select': 93 | dom = this.createSelectElement(vm) 94 | break; 95 | case 'actions': 96 | dom = this.createActionElement(vm) 97 | break; 98 | case 'vue-editor': 99 | dom = this.createVueEditor(vm) 100 | break; 101 | default: 102 | dom = this.createBasicElement(vm) 103 | } 104 | return dom 105 | } 106 | ``` 107 | 108 | #### 组件选择功能 109 | 首先我们需要考虑的是,组件怎么进行注册?因为组件被用户选用的时候,我们是需要渲染该组件的,所以我们可以提供一段 node 脚本来遍历所需组件,进行组件的安装注册: 110 | ```js 111 | // 定义渲染模板和路径 112 | var OUTPUT_PATH = path.join(__dirname, '../packages/index.js'); 113 | console.log(chalk.yellow('正在生成包引用文件...')) 114 | var INSTALL_COMPONENT_TEMPLATE = ' {{name}}'; 115 | var IMPORT_TEMPLATE = 'import {{componentName}} from \'{{name}}\''; 116 | var MAIN_TEMPLATE = `/* Automatic generated by './compiler/build-entry.js' */ 117 | 118 | {{include}} 119 | 120 | const components = [ 121 | {{install}} 122 | ] 123 | 124 | const install = function(Vue) { 125 | components.map((component) => { 126 | Vue.component(component.name, component) 127 | }) 128 | } 129 | 130 | /* istanbul ignore if */ 131 | if (typeof window !== 'undefined' && window.Vue) { 132 | install(window.Vue) 133 | } 134 | 135 | export { 136 | install, 137 | {{list}} 138 | } 139 | `; 140 | // 渲染引用文件 141 | var template = render(MAIN_TEMPLATE, { 142 | include: includeComponentTemplate.join(endOfLine), 143 | install: installTemplate.join(`,${endOfLine}`), 144 | version: process.env.VERSION || require('../package.json').version, 145 | list: listTemplate.join(`,${endOfLine}`) 146 | }); 147 | 148 | // 写入引用 149 | fs.writeFileSync(OUTPUT_PATH, template); 150 | ``` 151 | 152 | 最后渲染出来的文件大概是这样: 153 | ```js 154 | import WButton from 'w-button' 155 | const components = [ 156 | WButton 157 | ] 158 | const install = function(Vue) { 159 | components.map((component) => { 160 | Vue.component(component.name, component) 161 | }) 162 | } 163 | /* istanbul ignore if */ 164 | if (typeof window !== 'undefined' && window.Vue) { 165 | install(window.Vue) 166 | } 167 | export { 168 | install, 169 | WButton 170 | } 171 | ``` 172 | 这个也是组件库的通用写法,所以这里的思想就是把发布到`npm`上的组件,进行聚合,聚合成一个组件包引用,我们在后台编辑的时候,是需要全量引入的: 173 | ```js 174 | import * as W_UI from '../../packages' 175 | 176 | Vue.use(W_UI) 177 | ``` 178 | 这样,我们组件便注册完了,组件选择区,主要是提供组件的可选项,我们可以遍历组件,提供一个个 List 让用户选择,当然如果我们每个组件如果只提供一个组件名,用户可能并不知道组件长什么样,所以我们最好可以提供一下组件长什么样的缩略图。这里我们可以在组件发布的时候,也通过 node 脚本进行。这里要实现的代码比较多,我就大致说一下过程,因为也不是核心逻辑,可有可无,只能说有了体验上会好一点: 179 | 1. 用户启用 dev-server 进行代码编写测试 180 | 2. server 脚本使用 Chrome 工具 `puppeteer`,调整页面到手机端模式, 进行当前 dev-server 截图。 181 | 3. 生成截图文件,上传到node服务,关联组件 182 | 183 | 这样,就可以在加载组件选择区的时候,为组件附上缩略图。 184 | 185 | #### 组件预览区 186 | 当用户在选择区选择了组件,我们需要展示在预览区域,那么我们怎么知道用户选择了哪些组件呢?总不能提前全部把组件写入渲染区域,通过 `v-if`来判断选择吧?当然没有这么蠢,Vue 已经提供了动态组件的功能了: 187 | ```html 188 |
192 |
193 | ``` 194 | 为什么我们不用缩略图代替真实组件?一方面生成的缩略图尺寸存在问题,另一方面,我们需要编辑的联动性,就是编辑区的编辑需要及时的反馈给用户。 195 | 196 | #### 额外的问题 197 | 说了这么多,貌似一切都很顺利,但是这样在实践的时候,发现了存在一个明显的问题就是:我们中间的预览区域其实就是为了尽可能模拟移动端页面效果。但是如果我们加入了一些包含类似 `position: fixed` 样式的组件,会发现样式上就出现了明显的问题。典型的比如`Dialog Loading` 等。 198 | 所以我们参考了 `m-ui`组件库的设计,将中间预览操作容器展示为一个`iframe`。将`iframe`大小调整为`375 * 667`,模拟 iPhone 6 的手机端。这样就不会存在样式问题了。可是这样又出现了另一个难点,那就是左侧的编辑数据如何及时的反应到`iframe`中?没错,就是`postMessgae`,大致思路如下: 199 | 200 | 201 | 202 | 利用 `vuex` 做数据存储池,所有的变化,通过 `postMessgae`进行同步,这样我们只用确保数据池中的数据变化,便可以映射到渲染层的变化。比如,我们在预览区进行了组件选择和拖拽排序,那么我们只需通过`vuex`出发同步信息便可: 203 | ```ts 204 | // action.ts 205 | const action = { 206 | setCurrentPage ({commit, state}, page: number) { 207 | // 更新当前store 208 | commit('setCurrentPage',page) 209 | // 对应postMessage 210 | helper.postMsgToChild({type: 'syncState', value: state}) 211 | }, 212 | // ... 213 | } 214 | ```` 215 | 216 | ## Template 模板的实现 217 | 模板的设计实现,我参考了 `Vue-cli 2.x` 版本的思想,把这里的模板,存在了对应的 `git` 仓库中。当用户需要进行页面构建的时候,直接从 git 仓库中拉取对应的模板即可。当然拉取完,也会缓存一份在本地,以后渲染,直接从本地缓存中读取即可。我们现在把中心放在模板的格式和规范上。模板我们采用什么样的语法无所谓,这里我才用了和 `Vue-cli`一样的`Handlerbars`引擎。这里直接上我们模板的设计: 218 | ```html 219 | 235 | 236 | 249 | 250 | ``` 251 | 为了简化逻辑,我们把模板都设计成流式布局,所有组件一个个堆叠往下顺序排列。这个文件便是我们`vue-webpack-simple`的模板中的`App.vue`。我们对其进行了改写。这样在数据填充万,便可以渲染出一个 Vue 单文件。这里我只举着一个例子,我们还可以实现多页模板等等复杂的模板,根据需求拉取不同的模板即可。 252 | 253 | ## Node 渲染服务 254 | 当后台提交渲染请求的时候,我们的 node 服务所做的工作主要是: 255 | 1. 拉取对应模板 256 | 2. 渲染数据 257 | 3. 编译 258 | 259 | 拉取也就是去指定仓库中通过`download-git-repo`插件进行拉取模板。编译其实也就是通过`metalsmith`静态模板生成器把模板作为输入,数据作为填充,按照`handlebars`的语法进行规则渲染。最后产出`build`构建好的目录。在这一步,我们之前所需的组件,会被渲染进`package.json`文件。我们来看一下核心代码: 260 | ```js 261 | // 这里就像一个管道,以数据入口为生成源,通过renderTemplateFiles编译产出到目标目录 262 | function build(data, temp_dest, source, dest, cb) { 263 | let metalsmith = Metalsmith(temp_dest) 264 | .use(renderTemplateFiles(data)) 265 | .source(source) 266 | .destination(dest) 267 | .clean(false) 268 | 269 | return metalsmith.build((error, files) => { 270 | if (error) console.log(error); 271 | let f = Object.keys(files) 272 | .filter(o => fs.existsSync(path.join(dest, o))) 273 | .map(o => path.join(dest, o)) 274 | cb(error, f) 275 | }) 276 | } 277 | 278 | 279 | function renderTemplateFiles(data) { 280 | return function (files) { 281 | Object.keys(files).forEach((fileName) => { 282 | let file = files[fileName] 283 | // 渲染方法 284 | file.contents = Handlebars.compile(file.contents.toString())(data) 285 | }) 286 | } 287 | } 288 | ``` 289 | 290 | 最后我们得到的是一个 Vue 项目,此时还不能直接跑在浏览器端,这里就涉及到当前发布系统所支持的形式了。怎么说?如果你的公司发布系统需要在线编译,那么你可以把源文件直接上传到git仓库,触发仓库的 WebHook 让发布系统替你发掉这个项目即可。如果你们的发布系统是需要你编译后提交编译文件进行发布的,那么你可以通过 node 命令,进行本地构建,产出 HTML,CSS,JS。直接提交给发布系统即可。 291 | 到这里,我们的任务就差不多了~具体的核心实心大多已经阐述清楚,如果实现当中有什么问题和不妥,也欢迎一起探讨交流!! 292 | 293 | ## 题外话 294 | 实现这样一套页面构建系统,其实我这里简化了很多东西,旨在给大家提供一种思路。另外,其实我们的页面全部在服务端构建的时候产出,我们可以再服务端这一层做很多工作,比如页面的性能优化,因为页面数据我们全部都有,我们也可以做页面的预渲染,骨架屏,ssr,编译时优化等等。而且我们也可以对产出的活动页做数据分析~有很多想象的空间。 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | --------------------------------------------------------------------------------