├── mock ├── list.js └── index.js ├── docs-assets ├── er.png ├── models.jpg └── framework.png ├── public ├── favicon.ico └── index.html ├── babel.config.js ├── src ├── assets │ └── images │ │ └── logo.png ├── shims-vue.d.ts ├── components │ ├── index.ts │ ├── common │ │ ├── Slider.vue │ │ ├── Switch.vue │ │ ├── Progress.vue │ │ ├── Tag.vue │ │ ├── Alert.vue │ │ ├── DatePicker.vue │ │ ├── Layout.vue │ │ ├── Pagination.vue │ │ ├── TimePicker.vue │ │ ├── Col.vue │ │ ├── Row.vue │ │ ├── Radio.vue │ │ ├── Block.vue │ │ ├── Checkbox.vue │ │ ├── Select.vue │ │ ├── TableColumnAction.vue │ │ ├── Text.vue │ │ ├── FormItem.vue │ │ ├── Input.vue │ │ ├── Form.vue │ │ ├── index.ts │ │ ├── Button.vue │ │ └── Table.vue │ ├── options │ │ ├── BaseOptions.ts │ │ ├── index.ts │ │ ├── Layout.vue │ │ ├── Slider.vue │ │ ├── DatePicker.vue │ │ ├── TimePicker.vue │ │ ├── Switch.vue │ │ ├── Col.vue │ │ ├── Block.vue │ │ ├── Pagination.vue │ │ ├── FormItem.vue │ │ ├── Table.vue │ │ ├── Radio.vue │ │ ├── Select.vue │ │ ├── Checkbox.vue │ │ ├── Form.vue │ │ ├── Progress.vue │ │ ├── Text.vue │ │ ├── Alert.vue │ │ ├── Tag.vue │ │ ├── Row.vue │ │ ├── Button.vue │ │ └── Input.vue │ ├── Test.vue │ ├── actions │ │ ├── components │ │ │ ├── GlobalActionDialog.vue │ │ │ └── GlobalActionFetch.vue │ │ └── Index.vue │ ├── icon-select │ │ ├── Index.vue │ │ └── icon.json │ ├── DragLayout.vue │ ├── aside-page │ │ └── Index.vue │ ├── DragContainer.vue │ ├── models │ │ └── Index.vue │ └── aside │ │ └── Index.vue ├── router │ └── index.ts ├── shims-tsx.d.ts ├── utils │ ├── page.ts │ └── index.ts ├── plugins │ ├── db │ │ └── index.ts │ └── actions │ │ ├── index.ts │ │ └── fetch.ts ├── main.ts ├── types │ └── Element.ts ├── store │ ├── entities │ │ └── Page.ts │ ├── modules │ │ └── page.ts │ └── index.ts ├── mixins │ └── index.ts └── App.vue ├── .gitignore ├── vue.config.js ├── tsconfig.json ├── package.json └── README.md /mock/list.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-assets/er.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notbucai/lowcode/HEAD/docs-assets/er.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notbucai/lowcode/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs-assets/models.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notbucai/lowcode/HEAD/docs-assets/models.jpg -------------------------------------------------------------------------------- /docs-assets/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notbucai/lowcode/HEAD/docs-assets/framework.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notbucai/lowcode/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | declare module '*.jpg'; 6 | declare module '*.png'; 7 | declare module '*.ts'; -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './common'; 3 | import './options'; 4 | import DragContainer from './DragContainer.vue'; 5 | 6 | Vue.component('drag-container', DragContainer) -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | Vue.use(Router); 5 | 6 | export default new Router({ 7 | mode: 'history', 8 | base: process.env.BASE_URL, 9 | routes: [], 10 | }); -------------------------------------------------------------------------------- /src/components/common/Slider.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/common/Switch.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/common/Progress.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/common/Tag.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/options/BaseOptions.ts: -------------------------------------------------------------------------------- 1 | import { LowElement } from '@/types/Element'; 2 | import { Prop, Vue } from 'vue-property-decorator'; 3 | 4 | export default class BaseOptions extends Vue{ 5 | @Prop({ 6 | required: true, 7 | }) 8 | element?: LowElement; 9 | 10 | } -------------------------------------------------------------------------------- /src/components/common/Alert.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/common/DatePicker.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/common/Layout.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | -------------------------------------------------------------------------------- /src/components/common/Pagination.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/common/TimePicker.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/components/Test.vue: -------------------------------------------------------------------------------- 1 | 4 | 16 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/actions/components/GlobalActionDialog.vue: -------------------------------------------------------------------------------- 1 | 6 | 19 | -------------------------------------------------------------------------------- /src/components/common/Col.vue: -------------------------------------------------------------------------------- 1 | 6 | 22 | -------------------------------------------------------------------------------- /src/components/common/Row.vue: -------------------------------------------------------------------------------- 1 | 6 | 24 | -------------------------------------------------------------------------------- /src/utils/page.ts: -------------------------------------------------------------------------------- 1 | export const getDataByModel = (data: any, key: string) => { 2 | if (!data) return undefined; 3 | return key.split('.').reduce((pv, key) => { 4 | if (!pv) return undefined; 5 | return pv[key]; 6 | }, data); 7 | } 8 | export const setDataByModel = (data: any, key: string, value: any) => { 9 | const keys = key.split('.'); 10 | const _key = keys.pop(); 11 | if (!_key) return; 12 | keys.reduce((pv, key) => { 13 | return pv[key]; 14 | }, data)[_key] = value; 15 | } -------------------------------------------------------------------------------- /src/components/common/Radio.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/plugins/db/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: bucai 3 | * @Date: 2021-04-13 11:17:31 4 | * @LastEditors: bucai 5 | * @LastEditTime: 2021-06-25 16:07:08 6 | * @Description: 7 | */ 8 | 9 | import lowdb from 'lowdb'; 10 | 11 | import LocalStorage from 'lowdb/adapters/LocalStorage' 12 | 13 | const adapter = new LocalStorage('db') 14 | const db = lowdb(adapter) 15 | 16 | db.defaults({ 17 | pages: [], 18 | globalModels: [], 19 | globalActions: {} 20 | }) 21 | .write() 22 | 23 | 24 | export default db; -------------------------------------------------------------------------------- /src/components/common/Block.vue: -------------------------------------------------------------------------------- 1 | 6 | 19 | -------------------------------------------------------------------------------- /src/components/common/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: bucai 3 | * @Date: 2021-05-19 16:28:21 4 | * @LastEditors: bucai 5 | * @LastEditTime: 2021-05-19 16:42:59 6 | * @Description: 7 | */ 8 | const path = require('path') 9 | const apiMocker = require('mocker-api') 10 | 11 | module.exports = { 12 | devServer: { 13 | before (app) { // 注意,此处引用的是自定义的接口文件 14 | apiMocker(app, path.resolve('./mock'), { 15 | proxy: { 16 | // '/repos/*': 'https://api.github.com/', 17 | }, 18 | changeHost: true, 19 | }) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/common/Select.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/common/TableColumnAction.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 35 | 39 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import vuedraggable from 'vuedraggable'; 4 | import ElementUI from 'element-ui'; 5 | import 'element-ui/lib/theme-chalk/index.css'; 6 | Vue.use(ElementUI); 7 | Vue.component('draggable', vuedraggable); 8 | import './components'; 9 | import store from './store' 10 | import router from './router' 11 | import actions from './plugins/actions'; 12 | 13 | Vue.config.productionTip = process.env.NODE_ENV !== 'production'; 14 | 15 | // store.subscribe((mutation, state) => { 16 | // console.log(mutation.type) 17 | // console.log(mutation.payload) 18 | // }) 19 | 20 | actions(store); 21 | 22 | new Vue({ 23 | store, 24 | router, 25 | render: h => h(App) 26 | }).$mount('#app') 27 | -------------------------------------------------------------------------------- /src/components/common/Text.vue: -------------------------------------------------------------------------------- 1 | 4 | 30 | -------------------------------------------------------------------------------- /src/components/options/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | // 处理首字母大写 abc => Abc 4 | function changeStr (str: string) { 5 | return str.charAt(0).toUpperCase() + str.slice(1) 6 | } 7 | 8 | const requireComponent = require.context('.', false, /\.vue$/) 9 | 10 | requireComponent.keys().forEach(fileName => { 11 | const config = requireComponent(fileName) 12 | // console.log('config:', config) // 打印 13 | const componentName = changeStr( 14 | fileName.replace(/^\.\//, '').replace(/\.\w+$/, '') // ./child1.vue => child1 15 | ) 16 | 17 | Vue.component('Bc' + componentName + 'Option', config.default || config) // 动态注册该目录下的所有.vue文件 18 | }); 19 | 20 | console.log('%c成功加载配置组件数量:' + requireComponent.keys().length, "color:#409EFF;background-color:#ecf5ff;padding:0 10px;line-height:2;margin-bottom:4px;"); 21 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs'); 2 | 3 | module.exports = { 4 | 'GET /api/list': (req, res) => { 5 | return res.json({ 6 | total: 10, 7 | list: [{ 8 | id: 1, 9 | name: Mock.mock('@ctitle') + '-' + req.query.keywords, 10 | age: '23', 11 | job: '前端工程师' 12 | }, { 13 | id: 2, 14 | name: Mock.mock('@ctitle') + '-' + req.query.keywords, 15 | age: '24', 16 | job: '后端工程师' 17 | }] 18 | }); 19 | }, 20 | 'DELETE /api/delete': (req, res) => { 21 | return res.json({ 22 | total: 10, 23 | list: [{ 24 | id: 1, 25 | name: Mock.mock('@ctitle') + '-' + "删除", 26 | age: '23', 27 | job: '前端工程师' 28 | }] 29 | }); 30 | }, 31 | _proxy: { 32 | changeHost: true, 33 | } 34 | } -------------------------------------------------------------------------------- /src/components/common/FormItem.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/types/Element.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 单个动作处理配置 3 | */ 4 | export type LowElementActionHandle = { 5 | key: string, 6 | name: string, 7 | link: string, 8 | data?: { 9 | bind?: string, 10 | recv?: string, 11 | } 12 | }; 13 | /** 14 | * 单个动作 15 | */ 16 | export type LowElementAction = { 17 | key: string; 18 | name: string; 19 | event: string; 20 | handle: LowElementActionHandle[]; 21 | }; 22 | 23 | /** 24 | * 元素类型 25 | */ 26 | export type LowElement = { 27 | id: string; 28 | element: string; 29 | type: 'element' | 'container'; 30 | models?: { 31 | [key: string]: string; 32 | }, 33 | props?: { 34 | [key: string]: any; 35 | [key: number]: any; 36 | }; 37 | children?: LowElement[]; 38 | actions?: LowElementAction[] 39 | } 40 | 41 | export type LowDrapElement = LowElement & { 42 | clone?: boolean; 43 | disabled?: boolean; 44 | } -------------------------------------------------------------------------------- /src/components/icon-select/Index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 44 | -------------------------------------------------------------------------------- /src/components/common/Input.vue: -------------------------------------------------------------------------------- 1 | 4 | 38 | -------------------------------------------------------------------------------- /src/components/options/Layout.vue: -------------------------------------------------------------------------------- 1 | 11 | 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strictFunctionTypes": false, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "types": [ 18 | "webpack-env" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /src/components/DragLayout.vue: -------------------------------------------------------------------------------- 1 | 11 | 41 | -------------------------------------------------------------------------------- /src/store/entities/Page.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: bucai 3 | * @Date: 2021-06-24 14:31:33 4 | * @LastEditors: bucai 5 | * @LastEditTime: 2021-06-25 16:39:44 6 | * @Description: 7 | */ 8 | 9 | import { LowElement } from "@/types/Element"; 10 | import { generateUUID } from "@/utils"; 11 | 12 | 13 | export type EntityType = { 14 | key: string; // 绑定的字段 输入 15 | name: string; // 实体名称 输入 16 | type: string, // 数据类型 选择 17 | value: string, // 默认值 输入 18 | } 19 | 20 | export type ModelType = { 21 | name: string; // 数据源名称 22 | key: string; // 绑定的字段 该字段创建的时候生成 23 | // 实体 24 | entities: EntityType[]; 25 | } 26 | 27 | export default class PageEntity { 28 | 29 | id: string = ''; 30 | title: string = 'HOME'; 31 | icon: string = 'el-icon-s-home'; 32 | 33 | elements: LowElement = { 34 | "id": "c6174605-fb74-4fcb-884e-1a52926d55f3", 35 | "element": "layout", // 元素名称 or 类型 36 | "type": "container", // container or element 37 | "children": [] 38 | }; 39 | 40 | constructor() { 41 | this.id = 'page_' + generateUUID(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/common/Form.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export type ModelType = { 4 | key: string, 5 | name: string 6 | }; 7 | export type ModelConfigType = { 8 | [key: string]: ModelType[] 9 | }; 10 | 11 | export const modelConfig: ModelConfigType = {}; 12 | 13 | // 处理首字母大写 abc => Abc 14 | function changeStr (str: string) { 15 | return str.charAt(0).toUpperCase() + str.slice(1) 16 | } 17 | 18 | const requireComponent = require.context('.', false, /\.vue$/) 19 | 20 | export default requireComponent.keys().map(fileName => { 21 | const config = requireComponent(fileName) 22 | // console.log('config:', config) // 打印 23 | const componentName = changeStr( 24 | fileName.replace(/^\.\//, '').replace(/\.\w+$/, '') // ./child1.vue => child1 25 | ) 26 | const component = config.default || config; 27 | 28 | const models: ModelType[] = component.modelOptions; 29 | const element = componentName.replace(/([A-Z])/, (item) => { 30 | return item.toLowerCase() 31 | }).replace(/([A-Z])/g, (item) => { 32 | return '-' + item.toLowerCase() 33 | }) 34 | // componentName 35 | modelConfig[element] = models; 36 | Vue.component('Bc' + componentName, component); // 动态注册该目录下的所有.vue文件 37 | return { 38 | name: element, 39 | component 40 | } 41 | }); 42 | 43 | console.log('%c成功加载组件数量:' + requireComponent.keys().length, "color:#409EFF;background-color:#ecf5ff;padding:0 10px;line-height:2;margin-bottom:4px;"); 44 | 45 | // 在这里需要处理 一些东西 -------------------------------------------------------------------------------- /src/plugins/actions/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: bucai 3 | * @Date: 2021-03-22 20:59:50 4 | * @LastEditors: bucai 5 | * @LastEditTime: 2021-03-25 15:38:33 6 | * @Description: 7 | */ 8 | import Vue from 'vue'; 9 | import { Store } from 'vuex'; 10 | 11 | import fetch from './fetch'; 12 | 13 | import { getFinderFunctionByChildKeyFromTree } from '../../utils' 14 | import { LowElement } from '@/types/Element'; 15 | 16 | export default (store: Store) => { 17 | const state = store.state; 18 | 19 | const execHandle = async (handle: any, options: unknown = {}) => { 20 | const { link, data, key } = handle; 21 | const [type, typeKey, handleKey] = link.split(/_|\./); 22 | if (type === 'global') { 23 | return fetch(store, link, options); 24 | } else if (type === 'element') { 25 | // 1. 找到element 26 | // 2. 获取exec 27 | const find = getFinderFunctionByChildKeyFromTree(state.elements); 28 | const findData = find(typeKey); 29 | const element = Array.isArray(findData) ? findData[1] : findData; 30 | if (!element) { 31 | throw new Error('element not found'); 32 | } 33 | const actions = (element as LowElement).actions; 34 | return execAction(actions, options); 35 | } 36 | }; 37 | 38 | const execAction = async (action: any, options: unknown = {}) => { 39 | for (let i = 0; i < action.handle.length; i++) { 40 | const handle = action.handle[i]; 41 | await execHandle(handle, options); 42 | } 43 | }; 44 | 45 | Vue.prototype.$actions = execAction; 46 | 47 | } -------------------------------------------------------------------------------- /src/components/common/Button.vue: -------------------------------------------------------------------------------- 1 | 4 | 61 | -------------------------------------------------------------------------------- /src/plugins/actions/fetch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: bucai 3 | * @Date: 2021-03-22 20:59:57 4 | * @LastEditors: bucai 5 | * @LastEditTime: 2021-06-25 16:26:50 6 | * @Description: 7 | */ 8 | import { Store } from "vuex"; 9 | import axios from 'axios'; 10 | import { getDataByModel, setDataByModel } from "@/utils/page"; 11 | 12 | export default async (store: Store<{ globalActions: any, globalData: any }>, handle: string, options: any = {}) => { 13 | 14 | const { globalActions: actions, globalData: modelData } = store.state; 15 | const [location, key] = handle.split('.'); 16 | const [, namespace] = location.split('_') 17 | console.log('key', key); 18 | 19 | const action: any = actions[namespace].actions.find((item: { key: string }) => { 20 | return item.key === key; 21 | }); 22 | 23 | if (!action) return; 24 | const { handle: order, data: orderData } = action; 25 | const { bind, recv, replace } = orderData || {}; 26 | const [, method, url] = order.match(/([A-z]+)\[(.*?)\]/) as [string, string, string]; 27 | const requestOptions: any = { 28 | method, 29 | url, 30 | } 31 | if (bind) { 32 | // 对bind 处理 33 | let newBind = bind; 34 | if (Array.isArray(replace)) { 35 | replace.forEach(key => { 36 | newBind = bind.replace('$[' + key + ']', options[key]); 37 | }); 38 | } 39 | const bindData = getDataByModel(modelData, newBind) 40 | // 简单处理一下 41 | if (method.toLowerCase() === 'get') { 42 | requestOptions.params = bindData; 43 | } else { 44 | requestOptions.data = bindData; 45 | } 46 | } 47 | const response = await axios.request(requestOptions); 48 | if (recv) { 49 | const recvData = response.data; 50 | // const recvLocalData = getDataByModel(modelData, recv); 51 | setDataByModel(modelData, recv, recvData); 52 | } 53 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { LowElement } from '@/types/Element'; 2 | 3 | interface AnyObjectType { 4 | [key: string]: any 5 | } 6 | 7 | export function findByCallback (object: AnyObjectType, currentKey: string, callback: (current: AnyObjectType, key: string, value: any) => boolean): any { 8 | const current = object[currentKey]; 9 | for (let key in current) { 10 | if (callback(current, key, current[key])) return [object, currentKey]; 11 | if (typeof current[key] === 'object') { 12 | const value = findByCallback(current, key, callback); 13 | if (value) return value; 14 | } 15 | } 16 | } 17 | /** 18 | * 通过子元素key获取查找器函数 19 | * @param childKey 子元素KEY 20 | */ 21 | export function getFinderFunctionByChildKeyFromTree (tree: LowElement) { 22 | let current: LowElement | undefined; 23 | let index = -1; 24 | /** 25 | * 查找数据 26 | * @param id 元素ID 27 | * @param item 当前遍历的数据 28 | */ 29 | function findElementByIdFromTree (id: string, item: LowElement): LowElement | undefined { 30 | if (item.id === id) { 31 | return item; 32 | } 33 | if (Array.isArray(item.children)) { 34 | for (let i = 0; i < item.children.length; i++) { 35 | const resData = findElementByIdFromTree(id, item.children[i]); 36 | if (!current && resData) { 37 | current = item; 38 | index = i; 39 | } 40 | if (resData) return resData; 41 | } 42 | } 43 | } 44 | 45 | return function getElementById (id: string): (LowElement | undefined | number)[] { 46 | const element = findElementByIdFromTree(id, tree); 47 | const res = [current, element, index]; 48 | current = undefined; 49 | index = -1; 50 | return res; 51 | } 52 | } 53 | 54 | export function generateUUID (noSymbol: boolean = false) { 55 | let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 56 | let r = Math.random() * 16 | 0, 57 | v = c == 'x' ? r : (r & 0x3 | 0x8); 58 | return v.toString(16); 59 | }); 60 | if (noSymbol) { 61 | uuid = uuid.replace(/-/g, ''); 62 | } 63 | return uuid; 64 | } -------------------------------------------------------------------------------- /src/components/common/Table.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lowcode", 3 | "version": "0.1.0", 4 | "private": true, 5 | "types": "./types", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "commit": "git cz" 11 | }, 12 | "dependencies": { 13 | "@types/lodash": "^4.14.163", 14 | "@types/prettier": "^2.1.5", 15 | "axios": "^0.21.1", 16 | "core-js": "^3.6.5", 17 | "element-ui": "^2.13.2", 18 | "ladash": "^1.2.0", 19 | "lodash": "^4.17.20", 20 | "lowdb": "^1.0.0", 21 | "prettier": "^2.1.2", 22 | "stateshot": "^1.3.3", 23 | "tinykeys": "^1.1.1", 24 | "v-clipboard": "^2.2.3", 25 | "vue": "^2.6.11", 26 | "vue-class-component": "^7.2.3", 27 | "vue-property-decorator": "^8.4.2", 28 | "vue-router": "^3.4.9", 29 | "vuedraggable": "^2.24.2", 30 | "vuex": "^3.4.0", 31 | "vuex-class": "^0.3.2" 32 | }, 33 | "devDependencies": { 34 | "@types/lowdb": "^1.0.9", 35 | "@typescript-eslint/eslint-plugin": "^2.33.0", 36 | "@typescript-eslint/parser": "^2.33.0", 37 | "@vue/cli-plugin-babel": "~4.5.0", 38 | "@vue/cli-plugin-eslint": "~4.5.0", 39 | "@vue/cli-plugin-typescript": "^4.5.4", 40 | "@vue/cli-plugin-vuex": "^4.5.7", 41 | "@vue/cli-service": "~4.5.0", 42 | "@vue/eslint-config-typescript": "^5.0.2", 43 | "babel-eslint": "^10.1.0", 44 | "eslint": "^6.7.2", 45 | "eslint-plugin-vue": "^6.2.2", 46 | "git-cz": "^4.7.1", 47 | "mocker-api": "^2.8.3", 48 | "mockjs": "^1.1.0", 49 | "sass": "^1.26.10", 50 | "sass-loader": "^10.0.1", 51 | "typescript": "~3.9.3", 52 | "vue-template-compiler": "^2.6.11", 53 | "vuex-module-decorators": "^1.0.1" 54 | }, 55 | "eslintConfig": { 56 | "root": true, 57 | "env": { 58 | "node": true 59 | }, 60 | "extends": [ 61 | "plugin:vue/essential", 62 | "eslint:recommended", 63 | "@vue/typescript" 64 | ], 65 | "parserOptions": { 66 | "parser": "@typescript-eslint/parser" 67 | }, 68 | "rules": { 69 | "no-unused-vars": "off" 70 | } 71 | }, 72 | "browserslist": [ 73 | "> 1%", 74 | "last 2 versions", 75 | "not dead" 76 | ], 77 | "config": { 78 | "commitizen": { 79 | "path": "./node_modules/git-cz" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/options/Slider.vue: -------------------------------------------------------------------------------- 1 | 35 | 107 | -------------------------------------------------------------------------------- /src/components/options/DatePicker.vue: -------------------------------------------------------------------------------- 1 | 35 | 107 | -------------------------------------------------------------------------------- /src/components/options/TimePicker.vue: -------------------------------------------------------------------------------- 1 | 35 | 107 | -------------------------------------------------------------------------------- /src/components/options/Switch.vue: -------------------------------------------------------------------------------- 1 | 35 | 107 | -------------------------------------------------------------------------------- /src/mixins/index.ts: -------------------------------------------------------------------------------- 1 | type MoveEventType = { 2 | dragged: HTMLElement; 3 | to: HTMLElement; 4 | draggedContext: any; 5 | relatedContext: any; 6 | } 7 | // TODO: 这里应该放在一个单独的配置中 8 | // 可容纳 or 不可容纳 9 | const putRules: any = { 10 | 'row': { 11 | allow: ['col'], // 二选一 ? 12 | notallow: [], // 二选一 ? 13 | }, 14 | 15 | // 'col': { 16 | // allow: [], // 二选一 ? 17 | // notallow: ['row'] // 二选一 ? 18 | // } 19 | }; 20 | // 可归属 or 不可归属 21 | const pullRules: any = { 22 | 'form-item': { 23 | // 父元素 24 | allow: [], 25 | notallow: [], 26 | // 爷父 元素 27 | parent: ['form'], 28 | }, 29 | 'col': { 30 | // 子元素 31 | allow: ['row'], 32 | notallow: [] 33 | }, 34 | }; 35 | /** 36 | * 验证元素的拖动状态 37 | * @param to 要拖动到的容器标签 38 | * @param from 来源的元素标签 39 | */ 40 | function hasElementDragState (to: string, from: string, toPath: string, fromPath: string) { 41 | 42 | const putHas = [true, false, true]; 43 | const pullHas = [true, false, true]; 44 | 45 | // TODO: 这里代码需要重写一下了,逻辑混乱 46 | const putNode = putRules[to]; 47 | const pullNode = pullRules[from]; 48 | 49 | if (putNode) { 50 | putHas[0] = putNode.allow.length ? putNode.allow.includes(from) : true; 51 | putHas[1] = putNode.notallow.length ? putNode.notallow.includes(from) : false; 52 | } 53 | 54 | if (pullNode) { 55 | pullHas[0] = pullNode.allow.length ? pullNode.allow.includes(to) : true; 56 | pullHas[1] = pullNode.notallow.length ? pullNode.notallow.includes(to) : false; 57 | 58 | if (Array.isArray(pullNode.parent) && pullNode.parent.length > 0) { 59 | const toPathArr = toPath.split('_'); 60 | const list = pullNode.parent.filter((item: string) => { 61 | return toPathArr.includes(item); 62 | }); 63 | 64 | pullHas[2] = list.length === pullNode.parent.length; 65 | 66 | } 67 | 68 | } 69 | 70 | return putHas[0] && pullHas[0] && !putHas[1] && !pullHas[1] && pullHas[2] && putHas[2]; 71 | } 72 | 73 | 74 | export default { 75 | methods: { 76 | checkMove (e: MoveEventType) { 77 | const { dragged, to } = e; 78 | 79 | const fromContextElement = e.draggedContext.element; // 当前拖拽的元素 80 | const toContextElement = e.relatedContext.element; // 被替换 or 换位置的元素 81 | 82 | if (toContextElement) { 83 | const { type: toType, clone: toClone } = toContextElement; 84 | const { type: fromType } = fromContextElement; 85 | // 类型不同 86 | 87 | if (toType !== fromType) return false; 88 | if (toClone) return false; 89 | } 90 | // 验证元素是否可以拖动到当前元素 91 | const toTag = to.dataset.tag || ''; 92 | const fromTag = dragged.dataset.tag || ''; 93 | const toPath = to.dataset.path || ''; 94 | const fromPath = dragged.dataset.path || ''; 95 | 96 | const has = hasElementDragState(toTag, fromTag, toPath, fromPath); 97 | return has; 98 | } 99 | }, 100 | } -------------------------------------------------------------------------------- /src/components/options/Col.vue: -------------------------------------------------------------------------------- 1 | 41 | 104 | -------------------------------------------------------------------------------- /src/components/options/Block.vue: -------------------------------------------------------------------------------- 1 | 33 | 108 | -------------------------------------------------------------------------------- /src/components/options/Pagination.vue: -------------------------------------------------------------------------------- 1 | 45 | 127 | -------------------------------------------------------------------------------- /src/components/options/FormItem.vue: -------------------------------------------------------------------------------- 1 | 47 | 129 | -------------------------------------------------------------------------------- /src/components/options/Table.vue: -------------------------------------------------------------------------------- 1 | 33 | 141 | -------------------------------------------------------------------------------- /src/components/options/Radio.vue: -------------------------------------------------------------------------------- 1 | 60 | 134 | -------------------------------------------------------------------------------- /src/components/options/Select.vue: -------------------------------------------------------------------------------- 1 | 60 | 134 | -------------------------------------------------------------------------------- /src/components/options/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 60 | 134 | -------------------------------------------------------------------------------- /src/components/options/Form.vue: -------------------------------------------------------------------------------- 1 | 53 | 138 | -------------------------------------------------------------------------------- /src/components/options/Progress.vue: -------------------------------------------------------------------------------- 1 | 58 | 137 | -------------------------------------------------------------------------------- /src/components/options/Text.vue: -------------------------------------------------------------------------------- 1 | 59 | 147 | -------------------------------------------------------------------------------- /src/components/options/Alert.vue: -------------------------------------------------------------------------------- 1 | 57 | 141 | -------------------------------------------------------------------------------- /src/components/options/Tag.vue: -------------------------------------------------------------------------------- 1 | 64 | 145 | -------------------------------------------------------------------------------- /src/components/aside-page/Index.vue: -------------------------------------------------------------------------------- 1 | 57 | 123 | 137 | -------------------------------------------------------------------------------- /src/components/options/Row.vue: -------------------------------------------------------------------------------- 1 | 56 | 138 | -------------------------------------------------------------------------------- /src/components/DragContainer.vue: -------------------------------------------------------------------------------- 1 | 35 | 182 | -------------------------------------------------------------------------------- /src/components/icon-select/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "platform-eleme", 3 | "eleme", 4 | "delete-solid", 5 | "delete", 6 | "s-tools", 7 | "setting", 8 | "user-solid", 9 | "user", 10 | "phone", 11 | "phone-outline", 12 | "more", 13 | "more-outline", 14 | "star-on", 15 | "star-off", 16 | "s-goods", 17 | "goods", 18 | "warning", 19 | "warning-outline", 20 | "question", 21 | "info", 22 | "remove", 23 | "circle-plus", 24 | "success", 25 | "error", 26 | "zoom-in", 27 | "zoom-out", 28 | "remove-outline", 29 | "circle-plus-outline", 30 | "circle-check", 31 | "circle-close", 32 | "s-help", 33 | "help", 34 | "minus", 35 | "plus", 36 | "check", 37 | "close", 38 | "picture", 39 | "picture-outline", 40 | "picture-outline-round", 41 | "upload", 42 | "upload2", 43 | "download", 44 | "camera-solid", 45 | "camera", 46 | "video-camera-solid", 47 | "video-camera", 48 | "message-solid", 49 | "bell", 50 | "s-cooperation", 51 | "s-order", 52 | "s-platform", 53 | "s-fold", 54 | "s-unfold", 55 | "s-operation", 56 | "s-promotion", 57 | "s-home", 58 | "s-release", 59 | "s-ticket", 60 | "s-management", 61 | "s-open", 62 | "s-shop", 63 | "s-marketing", 64 | "s-flag", 65 | "s-comment", 66 | "s-finance", 67 | "s-claim", 68 | "s-custom", 69 | "s-opportunity", 70 | "s-data", 71 | "s-check", 72 | "s-grid", 73 | "menu", 74 | "share", 75 | "d-caret", 76 | "caret-left", 77 | "caret-right", 78 | "caret-bottom", 79 | "caret-top", 80 | "bottom-left", 81 | "bottom-right", 82 | "back", 83 | "right", 84 | "bottom", 85 | "top", 86 | "top-left", 87 | "top-right", 88 | "arrow-left", 89 | "arrow-right", 90 | "arrow-down", 91 | "arrow-up", 92 | "d-arrow-left", 93 | "d-arrow-right", 94 | "video-pause", 95 | "video-play", 96 | "refresh", 97 | "refresh-right", 98 | "refresh-left", 99 | "finished", 100 | "sort", 101 | "sort-up", 102 | "sort-down", 103 | "rank", 104 | "loading", 105 | "view", 106 | "c-scale-to-original", 107 | "date", 108 | "edit", 109 | "edit-outline", 110 | "folder", 111 | "folder-opened", 112 | "folder-add", 113 | "folder-remove", 114 | "folder-delete", 115 | "folder-checked", 116 | "tickets", 117 | "document-remove", 118 | "document-delete", 119 | "document-copy", 120 | "document-checked", 121 | "document", 122 | "document-add", 123 | "printer", 124 | "paperclip", 125 | "takeaway-box", 126 | "search", 127 | "monitor", 128 | "attract", 129 | "mobile", 130 | "scissors", 131 | "umbrella", 132 | "headset", 133 | "brush", 134 | "mouse", 135 | "coordinate", 136 | "magic-stick", 137 | "reading", 138 | "data-line", 139 | "data-board", 140 | "pie-chart", 141 | "data-analysis", 142 | "collection-tag", 143 | "film", 144 | "suitcase", 145 | "suitcase-1", 146 | "receiving", 147 | "collection", 148 | "files", 149 | "notebook-1", 150 | "notebook-2", 151 | "toilet-paper", 152 | "office-building", 153 | "school", 154 | "table-lamp", 155 | "house", 156 | "no-smoking", 157 | "smoking", 158 | "shopping-cart-full", 159 | "shopping-cart-1", 160 | "shopping-cart-2", 161 | "shopping-bag-1", 162 | "shopping-bag-2", 163 | "sold-out", 164 | "sell", 165 | "present", 166 | "box", 167 | "bank-card", 168 | "money", 169 | "coin", 170 | "wallet", 171 | "discount", 172 | "price-tag", 173 | "news", 174 | "guide", 175 | "male", 176 | "female", 177 | "thumb", 178 | "cpu", 179 | "link", 180 | "connection", 181 | "open", 182 | "turn-off", 183 | "set-up", 184 | "chat-round", 185 | "chat-line-round", 186 | "chat-square", 187 | "chat-dot-round", 188 | "chat-dot-square", 189 | "chat-line-square", 190 | "message", 191 | "postcard", 192 | "position", 193 | "turn-off-microphone", 194 | "microphone", 195 | "close-notification", 196 | "bangzhu", 197 | "time", 198 | "odometer", 199 | "crop", 200 | "aim", 201 | "switch-button", 202 | "full-screen", 203 | "copy-document", 204 | "mic", 205 | "stopwatch", 206 | "medal-1", 207 | "medal", 208 | "trophy", 209 | "trophy-1", 210 | "first-aid-kit", 211 | "discover", 212 | "place", 213 | "location", 214 | "location-outline", 215 | "location-information", 216 | "add-location", 217 | "delete-location", 218 | "map-location", 219 | "alarm-clock", 220 | "timer", 221 | "watch-1", 222 | "watch", 223 | "lock", 224 | "unlock", 225 | "key", 226 | "service", 227 | "mobile-phone", 228 | "bicycle", 229 | "truck", 230 | "ship", 231 | "basketball", 232 | "football", 233 | "soccer", 234 | "baseball", 235 | "wind-power", 236 | "light-rain", 237 | "lightning", 238 | "heavy-rain", 239 | "sunrise", 240 | "sunrise-1", 241 | "sunset", 242 | "sunny", 243 | "cloudy", 244 | "partly-cloudy", 245 | "cloudy-and-sunny", 246 | "moon", 247 | "moon-night", 248 | "dish", 249 | "dish-1", 250 | "food", 251 | "chicken", 252 | "fork-spoon", 253 | "knife-fork", 254 | "burger", 255 | "tableware", 256 | "sugar", 257 | "dessert", 258 | "ice-cream", 259 | "hot-water", 260 | "water-cup", 261 | "coffee-cup", 262 | "cold-drink", 263 | "goblet", 264 | "goblet-full", 265 | "goblet-square", 266 | "goblet-square-full", 267 | "refrigerator", 268 | "grape", 269 | "watermelon", 270 | "cherry", 271 | "apple", 272 | "pear", 273 | "orange", 274 | "coffee", 275 | "ice-tea", 276 | "ice-drink", 277 | "milk-tea", 278 | "potato-strips", 279 | "lollipop", 280 | "ice-cream-square", 281 | "ice-cream-round" 282 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 又开一坑推荐跨iframe组件拖拽 [d2g](https://github.com/notbucai/d2g) 2 | 3 | # Low Code 4 | 5 | 个人低代码(可能是伪低代码,毕竟不是很懂)的一个玩具 6 | 7 | ![./docs-assets/framework.png](./docs-assets/framework.png) 8 | 9 | ![./docs-assets/er.png](./docs-assets/er.png) 10 | 11 | 12 | ## 项目依赖 13 | 14 | ### Vuejs 15 | 渐进式 JavaScript 框架 16 | 17 | ### ElementUI 18 | Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库 19 | 20 | ### Vue.Draggable 21 | 基于 Sortable.js 的 Vue 拖放组件 22 | 23 | ## 元素结构 24 | > 使用JSON作为结构化的处理 是不是最终可以抛弃组件库的依赖 只需要做兼容性的 `代理组件`? 25 | 26 | ```TypeScript 27 | // 分两种 用type作为区分 28 | // 1. 功能元素(组件) 29 | // 2. 容器元素(组件) 30 | type Element = { 31 | // 唯一ID便于操作 一切以 id 为主 32 | id: string; 33 | // (功能 / 容器) 名称 34 | element: string; 35 | // 字面意思 36 | type: 'element' | 'container'; 37 | // props 这里放元素(组件)的 参数 38 | props?: { 39 | // 这里由于其他一些问题暂时这样写了 40 | [key: string]: any; 41 | [key: number]: any; 42 | }; 43 | // 容器组件 存放的子元素 44 | children?: LowElement[]; 45 | models: { 46 | [key: string]: '[model_key].?[key].?[key]', // key 为自定义 47 | }, 48 | // 动作,这一块比较复杂 49 | // 原本设计为[key:value],由于可能存在描述信息,所以目前改成{xxx} 50 | actions: [ 51 | { 52 | key: string, // 随机生成 53 | // 事件名称 按组件动态创建 54 | name: string, 55 | // event: 'click' | 'change' | 'submit', // 对应的可能是 @[event]="" 这个操作 56 | event?: string, // 目前只有组件中才有意义 57 | // 动作 目前按同步执行 58 | handle: [ 59 | { 60 | key: string, // 随机生成 61 | name: string, 62 | // location表示动作存在的位置 一般为element、global, type 表示类型 (如 fetch 发送请求) 63 | // location 可能还有些复杂的设计 如元素可能绑定一个id,全局中是global_[namespace] 元素中可能是 element_[id] 64 | // 例子: global_dialog.b2d12 | element_1e4sa.12342 65 | link: '[location].[key]', // 关联 的触发动作 可能是发送请求 可能是 触发其他元素的动作 66 | // 这个可用于接口方法和传输 以及 Open dialog 67 | data?: { 68 | // 参考 models 69 | bind?: string, // 绑定的数据源 接口请求、打开dialog或者其他需要数据关联的操作 70 | recv?: string, // 接受响应的数据源 71 | } 72 | }, 73 | // ... more 74 | ] 75 | }, 76 | // ... more 77 | ] 78 | } 79 | ``` 80 | 81 | ## 数据源结构 82 | 83 | > 模型、类型 的值(默认值) 均为序列数据 需要反序列成对应类型数据 84 | 85 | ```TypeScript 86 | /** 87 | * 数据类型 88 | */ 89 | types = [ 90 | { 91 | type: 'string', 92 | value: '""', 93 | label: "字符串" 94 | }, 95 | { 96 | type: 'number', 97 | value: '0', 98 | label: "数字" 99 | }, 100 | { 101 | type: 'boolean', 102 | value: 'false', 103 | label: "布尔" 104 | }, 105 | { 106 | type: 'array', 107 | value: '[]', 108 | label: "数组" 109 | }, 110 | ]; 111 | /** 112 | * 模型 113 | */ 114 | models = [ 115 | { 116 | name: "表单", // 数据源名称 117 | key: 'model_1a52926d55f3', // 绑定的字段 该字段创建的时候生成 118 | // 实体 119 | entities: [ 120 | { 121 | key: 'username', // 绑定的字段 输入 122 | name: "用户名", // 实体名称 输入 123 | type: 'string', // 数据类型 选择 124 | value: '""', // 默认值 输入 125 | }, 126 | { 127 | key: 'password', // 绑定的字段 输入 128 | name: "密码", // 实体名称 输入 129 | type: 'string', // 数据类型 选择 130 | value: '""', // 默认值 输入 131 | }, 132 | // ...more 133 | ] 134 | } 135 | ] 136 | 137 | /** 138 | * 数据 通过该 模型生成的结构体 139 | */ 140 | data = { 141 | model_1a52926d55f3: { 142 | username: '', 143 | password: '' 144 | } 145 | } 146 | 147 | /** 148 | * 全局动作 149 | * 用于定义如打开dialog, 发送请求 150 | */ 151 | actions = { 152 | // namespace为命名空间 一般已经内置命名空间 如 dialog fetch 等 153 | // 每个命名空间对应一个 解析/执行 器 154 | [namespace: string]: { 155 | name: string, 156 | actions: [ 157 | { 158 | key: string, // 随机生成 159 | // eventName 事件名称 按组件/自定义名称动态创建 160 | name: string, 161 | // event: 'click' | 'change' | 'submit', // 对应的可能是 @[event]="" 这个操作 162 | event?: string, // 目前只有组件中才有意义 163 | // 动作 目前按同步执行 164 | handle: [ 165 | // 需要根据解析器具体操作 166 | // ... more 167 | ], 168 | data: { 169 | bind: '', 170 | recv: '', // 获取 171 | replace: [], // 需要取代的值,目前用于数组中(项)的下标 172 | } 173 | } 174 | ], 175 | // ... more 176 | } 177 | } 178 | ``` 179 | 180 | ## 操作方面 181 | 182 | ### FIRST 产生的一些问题/想法 183 | 184 | 采用拖拽的方式,非容器组件不能存在其他组件,指定组件只能存在指定组件中? 185 | 点击元素进行一些数据关联、属性增删、事件监听? 186 | 187 | 突然发现嵌套关系应该有祖先关系的 比如 `el-form-item` 只要在 `el-form` 的子孙就行 188 | 189 | 固定画布元素避免匹配排序问题 190 | 191 | 拖动节点的时候带走子孙节点 间接导致出现的问题 192 | 193 | 最终还是需要模版化才是最优的 194 | 195 | 历史栈/快照 196 | 197 | 多Slot的情况 198 | 199 | 实际上从待定组件列表中拖动到画布中的时候有些待定组件的属性应该去除或者更改 200 | 201 | 必须有一个可以选择边距的元素才行 202 | 203 | ### NEXT 产生的问题/想法 204 | 205 | 想这个的时候脑子爆栈了, 所以用笔辅助了一下,字很差,看个灵魂就好。 206 | ![models](./docs-assets/models.jpg) 207 | 208 | 感觉操作逐渐复杂起来 209 | 210 | 感觉还得有个替换父级的功能 211 | 212 | 213 | 需要做到职责分明 214 | 如果提交、重置应该放在表单 215 | 表格只需要绑定数据源剩下数据应该由其他相关事件获取(如分页操作、搜索、初始化) 216 | 217 | 所以表单完成后需要一些操作如清空表单、更新数据源 218 | 219 | 一个接口可以对应多个数据源 220 | 221 | 数据源添加/设置中模型可能会有点问题, 222 | 223 | 数据源的序列数据 目前使用JSON进行序列还存在一些问题。 224 | 225 | 页面管理基本完成 226 | 227 | ### 动作 228 | 229 | 一、类型 230 | 1. 组件创建进入获取初始化数据 231 | 2. 增删改,分页 232 | 3. 弹框 233 | 4. 表单提交 234 | 5. 页面跳转 可能带参数 235 | 236 | 二、其他 237 | 1. 时机 无非分为多个动作 238 | 2. 动作 无非是(操作/提交)数据源 和 API调用 239 | 240 | ## 进度 241 | 242 | ### ✅ FIRST 生成前端代码 243 | 244 | 目前正在做第一步,生成Vue代码。 245 | 预计 `十月底` 完成。 246 | 247 | > 目前进度: 248 | > 1. 拖拽使用`Vue.Draggable`库基本功能完善 249 | > 2. 丰富 `代理组件` 便于以后解耦 250 | > 3. JSON 转换成Vue + element 组件代码 251 | 252 | ### 🙆 NEXT ING 253 | 254 | 预计`十一月中旬`动工 (有些个人的事情要处理) 255 | 256 | 1. 数据源 (用于表单数据绑定) 257 | 2. 事件 258 | 3. 简单关联的接口 259 | 4. 权限 260 | 5. thinking 261 | -------------------------------------------------------------------------------- /src/components/options/Button.vue: -------------------------------------------------------------------------------- 1 | 81 | 169 | -------------------------------------------------------------------------------- /src/components/options/Input.vue: -------------------------------------------------------------------------------- 1 | 99 | 206 | -------------------------------------------------------------------------------- /src/store/modules/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 模型、类型 的值(默认值) 均为序列数据 需要反序列成对应类型数据 3 | */ 4 | import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators'; 5 | import { cloneDeep } from 'lodash' 6 | import { History } from 'stateshot' 7 | 8 | import { getFinderFunctionByChildKeyFromTree } from '@/utils' 9 | import { LowElement } from '@/types/Element'; 10 | import PageEntity from '../entities/Page'; 11 | 12 | const history = new History(); 13 | 14 | const flattenElementsDeep = (element: LowElement) => { 15 | const arr = [element]; 16 | 17 | element.children?.forEach(item => { 18 | const list = flattenElementsDeep(item); 19 | arr.push(...list); 20 | }); 21 | 22 | return arr; 23 | } 24 | 25 | @Module({ 26 | namespaced: true, 27 | }) 28 | class PageStore extends VuexModule { 29 | 30 | id: string = ''; 31 | title: string = ''; 32 | icon: string = ''; 33 | update_time = Date.now(); 34 | /** 35 | * 页面当前选中id 36 | */ 37 | currentId: string | undefined | null = undefined; 38 | /** 39 | * 页面当前元素列表 40 | */ 41 | elements: LowElement = { 42 | "id": "c6174605-fb74-4fcb-884e-1a52926d55f3", 43 | "element": "layout", // 元素名称 or 类型 44 | "type": "container", // container or element 45 | "children": [] 46 | }; 47 | 48 | // --------------------------------------------- 49 | // -----------------getters-------------------- 50 | // --------------------------------------------- 51 | /** 52 | * 当前选择元素 53 | */ 54 | get current () { 55 | const state = this; 56 | if (typeof state.currentId === 'string') { 57 | const find = getFinderFunctionByChildKeyFromTree(state.elements); 58 | const resData = find(state.currentId) 59 | return Array.isArray(resData) ? resData[1] : resData; 60 | } 61 | return null; 62 | } 63 | 64 | /** 65 | * 扁平后元素列表 66 | */ 67 | get flatElements () { 68 | const state = this; 69 | return flattenElementsDeep(state.elements); 70 | } 71 | 72 | // --------------------------------------------- 73 | // -----------------Action-------------------- 74 | // --------------------------------------------- 75 | 76 | /** 77 | * 初始化页面 78 | */ 79 | @Action 80 | init (pageEntity: PageEntity) { 81 | this.context.commit('INIT_PAGE', pageEntity); 82 | } 83 | 84 | /** 85 | * 刷新 86 | */ 87 | @Action 88 | refresh () { 89 | this.context.commit('REFRESH_ELEMENTS'); 90 | } 91 | 92 | /** 93 | * 保存 94 | */ 95 | @Action 96 | save () { 97 | // 发送保存请求 98 | this.context.commit('SAVE'); 99 | } 100 | 101 | 102 | /** 103 | * 清空画布 104 | */ 105 | @Action 106 | clear_canvas () { 107 | this.context.commit('CLEAR_ELEMENTS') 108 | } 109 | // --------------------------------------------- 110 | // -----------------Mutation-------------------- 111 | // --------------------------------------------- 112 | 113 | @Mutation 114 | SAVE () { 115 | } 116 | @Mutation 117 | INIT_ELEMENT (payload: LowElement) { 118 | this.elements = payload; 119 | } 120 | @Mutation 121 | INIT_PAGE (payload: PageEntity) { 122 | this.id = payload.id; 123 | this.icon = payload.icon; 124 | this.title = payload.title; 125 | this.elements = payload.elements; 126 | 127 | history.reset(); 128 | } 129 | /** 130 | * 清除元素 131 | */ 132 | @Mutation 133 | CLEAR_ELEMENTS () { 134 | const state = this; 135 | history.reset(); 136 | 137 | state.elements = { 138 | "id": "c6174605-fb74-4fcb-884e-1a52926d55f3", 139 | "element": "layout", // 元素名称 or 类型 140 | "type": "container", // container or element 141 | "children": [] 142 | }; 143 | } 144 | /** 145 | * 更新历史 146 | */ 147 | @Mutation 148 | UPDATE () { 149 | const state = this; 150 | // 添加到历史 151 | // console.log('history', history.get()); 152 | history.pushSync(state.elements); 153 | // console.log('history', history.get()); 154 | 155 | } 156 | /** 157 | * 撤回 158 | */ 159 | @Mutation 160 | UNDO () { 161 | this.elements = history.undo().get(); 162 | } 163 | /** 164 | * 取消撤回(反相撤回) 165 | */ 166 | @Mutation 167 | REDO () { 168 | this.elements = history.redo().get(); 169 | } 170 | /** 171 | * 设置当前元素 172 | * @param payload 173 | */ 174 | @Mutation 175 | SET_CURRENT (payload: string) { 176 | this.currentId = payload; 177 | } 178 | /** 179 | * 绑定模型 180 | * @param payload 181 | */ 182 | @Mutation 183 | BIND_MODELS (payload: any) { 184 | if (typeof this.currentId !== 'string') return; 185 | const find = getFinderFunctionByChildKeyFromTree(this.elements); 186 | let [parent, current] = find(this.currentId); 187 | 188 | if (current) { 189 | current = current as LowElement; 190 | current.models = payload; 191 | } 192 | } 193 | /** 194 | * 更新当前选中元素的参数 195 | * @param payload 196 | */ 197 | @Mutation 198 | UPDATE_CURRENT_PROPS (payload: any) { 199 | if (typeof this.currentId !== 'string') return; 200 | const find = getFinderFunctionByChildKeyFromTree(this.elements); 201 | let [parent, current, index] = find(this.currentId); 202 | if (current) { 203 | current = current as LowElement; 204 | parent = parent as LowElement; 205 | // index = index as number; 206 | 207 | current.props = payload; 208 | // const id = current.id; 209 | // TODO: 这里有问题 210 | // if (parent && Array.isArray(parent.children)) { 211 | // // index = parent.children.findIndex(item => item.id === id) 212 | // // parent.children.splice(index, 1, current); 213 | // } 214 | } 215 | } 216 | /** 217 | * 移除当前元素 218 | */ 219 | @Mutation 220 | REMOVE_CURRENT () { 221 | if (typeof this.currentId !== 'string') return; 222 | const find = getFinderFunctionByChildKeyFromTree(this.elements); 223 | let [parent, current, index] = find(this.currentId); 224 | if (current) { 225 | current = current as LowElement; 226 | parent = parent as LowElement; 227 | // index = index as number; 228 | const id = current.id; 229 | 230 | if (parent && Array.isArray(parent.children)) { 231 | index = parent.children.findIndex(item => item.id === id) 232 | parent.children.splice(index, 1); 233 | } 234 | } 235 | } 236 | /** 237 | * 刷新缓存 238 | */ 239 | @Mutation 240 | REFRESH_ELEMENTS () { 241 | this.elements = cloneDeep(this.elements); 242 | } 243 | } 244 | 245 | export default PageStore; 246 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 208 | 209 | 270 | -------------------------------------------------------------------------------- /src/components/actions/components/GlobalActionFetch.vue: -------------------------------------------------------------------------------- 1 | 115 | 263 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { LowElement, LowElementAction } from '@/types/Element' 2 | // import LowElement from '../types/Element' 3 | import Vue from 'vue' 4 | import Vuex from 'vuex' 5 | import PageStore from './modules/page'; 6 | import db from '@/plugins/db' 7 | import PageEntity, { ModelType } from './entities/Page'; 8 | import { generateUUID } from '@/utils'; 9 | import { cloneDeep, throttle } from 'lodash'; 10 | 11 | export type TypeType = { 12 | type: string; 13 | value: string; 14 | label: string; 15 | } 16 | 17 | Vue.use(Vuex); 18 | 19 | const pages = db.get('pages').value(); 20 | const globalModels = db.get('globalModels').value(); 21 | const globalActions = db.get('globalActions').value(); 22 | 23 | const flattenElementsDeep = (element: LowElement) => { 24 | const arr = [element]; 25 | 26 | element.children?.forEach(item => { 27 | const list = flattenElementsDeep(item); 28 | arr.push(...list); 29 | }); 30 | 31 | return arr; 32 | } 33 | 34 | type StateType = { 35 | pages: PageEntity[], 36 | globalData: any, 37 | globalActions: any, 38 | globalModels: ModelType[], 39 | types: TypeType[], 40 | } 41 | 42 | const store = new Vuex.Store({ 43 | modules: { 44 | page: PageStore, 45 | }, 46 | state: { 47 | 48 | /** 49 | * 数据类型 50 | */ 51 | types: [ 52 | { 53 | type: 'string', 54 | value: '""', 55 | label: "字符串" 56 | }, 57 | { 58 | type: 'number', 59 | value: '0', 60 | label: "数字" 61 | }, 62 | { 63 | type: 'boolean', 64 | value: 'false', 65 | label: "布尔" 66 | }, 67 | { 68 | type: 'array', 69 | value: '[]', 70 | label: "数组" 71 | }, 72 | ], 73 | pages: [], 74 | /** 75 | * 数据 76 | */ 77 | globalData: { 78 | // model_1a52926d55f3: { 79 | // username: '默认1', 80 | // password: '默认2' 81 | // }, 82 | // model_42124s1ar2: { 83 | // token: '' 84 | // } 85 | }, 86 | globalActions: { 87 | fetch: { 88 | name: '接口请求', 89 | actions: [] 90 | }, 91 | dialog: { 92 | name: '对话框', 93 | actions: [] 94 | } 95 | }, 96 | /** 97 | * 模型 98 | */ 99 | globalModels: [ 100 | // { 101 | // name: "表单", // 数据源名称 102 | // key: 'model_1a52926d55f3', // 绑定的字段 该字段创建的时候生成 103 | // // 实体 104 | // entities: [ 105 | // { 106 | // key: 'username', // 绑定的字段 输入 107 | // name: "用户名", // 实体名称 输入 108 | // type: 'string', // 数据类型 选择 109 | // value: '""', // 默认值 输入 110 | // }, 111 | // { 112 | // key: 'password', // 绑定的字段 输入 113 | // name: "密码", // 实体名称 输入 114 | // type: 'string', // 数据类型 选择 115 | // value: '""', // 默认值 输入 116 | // }, 117 | // // ...more 118 | // ] 119 | // }, 120 | // { 121 | // name: "测试", // 数据源名称 122 | // key: 'model_42124s1ar2', // 绑定的字段 该字段创建的时候生成 123 | // // 实体 124 | // entities: [ 125 | // { 126 | // key: 'token', // 绑定的字段 输入 127 | // name: "token", // 实体名称 输入 128 | // type: 'string', // 数据类型 选择 129 | // value: '""', // 默认值 输入 130 | // }, 131 | // // ...more 132 | // ] 133 | // } 134 | ], 135 | }, 136 | getters: { 137 | 138 | }, 139 | mutations: { 140 | INIT_PAGE (state, payload) { 141 | state.pages = payload; 142 | }, 143 | 144 | ADD_PAGE (state, payload) { 145 | state.pages.push(payload); 146 | }, 147 | 148 | UPDATE_PAGE (state, payload) { 149 | const index = state.pages.findIndex(item => item.id === payload.id); 150 | const page = state.pages[index]; 151 | if (page) { 152 | page.title = payload.title; 153 | page.icon = payload.icon; 154 | state.pages.splice(index, 1, payload); 155 | } 156 | }, 157 | 158 | REMOVE_PAGE (state, id) { 159 | const index = state.pages.findIndex(item => item.id === id); 160 | state.pages.splice(index, 1); 161 | }, 162 | 163 | INIT_GLOBAL_ACTIONS (state) { 164 | 165 | }, 166 | /** 167 | * 删除动作 168 | */ 169 | REMOVE_ACTION (state, { type, key }: { type: string, key: string }) { 170 | const index = state.globalActions[type].actions.findIndex((item: any) => { 171 | return item.key == key; 172 | }); 173 | state.globalActions[type].actions.splice(index, 1); 174 | // db.set('actions', state.globalActions).write(); 175 | }, 176 | 177 | /** 178 | * 更新动作 179 | */ 180 | UPDATE_ACTION (state, { type, key, data }: { 181 | type: string, key: string, data: object 182 | }) { 183 | const index = state.globalActions[type].actions.findIndex((item: any) => { 184 | return item.key == key; 185 | }); 186 | if (index === -1) { 187 | state.globalActions[type].actions.push(data); 188 | } else { 189 | state.globalActions[type].actions.splice(index, 1, data); 190 | } 191 | // db.set('actions', state.globalActions).write(); 192 | }, 193 | /** 194 | * 清空数据 195 | */ 196 | CLEAR_MODELS (state) { 197 | state.globalModels = []; 198 | state.globalData = {}; 199 | }, 200 | 201 | /** 202 | * 生成数据结构 203 | * @param model 模型 204 | */ 205 | GENERATE_DATA (state, model: ModelType) { 206 | // 生成数据 207 | return 208 | }, 209 | /** 210 | * 增加模型 211 | * @param model 模型 212 | */ 213 | ADD_MODEL (state, model: ModelType) { 214 | state.globalModels.push(model); 215 | // 生成数据 216 | state.globalData[model.key] = model.entities.reduce((pv: any, cv) => { 217 | // 反序列数据 218 | pv[cv.key] = JSON.parse(cv.value); 219 | return pv; 220 | }, {}); 221 | state.globalData = Object.assign({}, state.globalData); 222 | // db.set('models', state.globalModels).write(); 223 | }, 224 | /** 225 | * 更新模型 226 | * @param model 模型 227 | */ 228 | UPDATE_MODEL (state, model: ModelType) { 229 | const index = state.globalModels.findIndex(item => { 230 | return item.key === model.key; 231 | }); 232 | 233 | if (index === -1) { 234 | return; 235 | } 236 | // 修改模型 237 | state.globalModels.splice(index, 1, model); 238 | // 生成数据 239 | state.globalData[model.key] = model.entities.reduce((pv: any, cv) => { 240 | // 反序列数据 241 | pv[cv.key] = JSON.parse(cv.value); 242 | return pv; 243 | }, {}); 244 | state.globalData = Object.assign({}, state.globalData); 245 | // db.set('models', state.globalModels).write(); 246 | }, 247 | /** 248 | * 删除模型 249 | * @param model 模型 250 | */ 251 | REMOVE_MODEL (state, key: string) { 252 | const index = state.globalModels.findIndex(item => { 253 | return item.key === key; 254 | }); 255 | state.globalModels.splice(index, 1); 256 | // db.set('models', state.globalModels).write(); 257 | } 258 | }, 259 | actions: { 260 | init ({ dispatch }) { 261 | // 初始化models 262 | dispatch('initPages'); 263 | dispatch('initGlobalModels'); 264 | dispatch('initGlobalActions'); 265 | 266 | let page = new PageEntity(); 267 | const pages = db.get('pages').value(); 268 | if (Array.isArray(pages) && pages.length) { 269 | page = pages[0]; 270 | } else { 271 | dispatch('addPage', page); 272 | } 273 | dispatch('page/init', page); 274 | }, 275 | 276 | initPages (state) { 277 | // const pages = db.get('pages').value(); 278 | // INIT_PAGE 279 | if (pages && Object.keys(pages).length) { 280 | state.commit('INIT_PAGE', pages); 281 | } 282 | }, 283 | addPage (state, payload) { 284 | let page = new PageEntity(); 285 | page.title = payload.title; 286 | page.icon = payload.icon; 287 | 288 | state.commit('ADD_PAGE', page); 289 | }, 290 | updatePage (state, payload) { 291 | state.commit('UPDATE_PAGE', payload); 292 | }, 293 | removePage (state, id) { 294 | state.commit('REMOVE_PAGE', id); 295 | }, 296 | async selectPage (state, id) { 297 | const index = state.state.pages.findIndex(item => item.id === id); 298 | const page = state.state.pages[index]; 299 | if (!page) throw new Error('page is no find'); 300 | // 保存当前页面 301 | await state.dispatch('page/save'); 302 | // 切换到新页面 303 | await state.dispatch('page/init', page); 304 | }, 305 | 306 | 307 | initGlobalModels (state) { 308 | state.commit('CLEAR_MODELS'); 309 | 310 | const models = globalModels; 311 | if (models && Object.keys(models).length) { 312 | models.forEach((item: ModelType) => { 313 | state.commit('ADD_MODEL', item); 314 | }); 315 | } 316 | }, 317 | 318 | /** 319 | * 编辑或添加模型 320 | * @param payload 321 | */ 322 | handleEditOrAddModel (store, payload: ModelType) { 323 | const findModel = store.state.globalModels.find(model => { 324 | return model.key === payload.key; 325 | }); 326 | 327 | if (findModel) { 328 | store.commit('UPDATE_MODEL', payload); 329 | } else { 330 | store.commit('ADD_MODEL', payload); 331 | } 332 | }, 333 | 334 | initGlobalActions (state) { 335 | // 初始化示例动作 336 | const actions = globalActions; 337 | if (actions && Object.keys(actions).length) { 338 | Object.keys(actions).forEach((key: any) => { 339 | actions[key].actions.forEach((aItem: any) => { 340 | state.commit('UPDATE_ACTION', { 341 | type: key, 342 | key: aItem.key, 343 | data: aItem 344 | }); 345 | }); 346 | 347 | }); 348 | } else { 349 | const globalActions: any = { 350 | fetch: { 351 | name: "接口请求", 352 | actions: [ 353 | { 354 | key: 'action_efhj123sadufk235ur', 355 | name: "登录", 356 | handle: 'POST[/api/login]', 357 | data: { 358 | bind: 'model_1a52926d55f3', 359 | recv: 'model_2345423.token' 360 | } 361 | }, 362 | { 363 | key: 'action_sjy723nju431sew234d', 364 | name: "删除单个用户", 365 | handle: 'POST[/api/table/delete]', 366 | data: { 367 | bind: 'model_123midn3u1s.list.$[index]', 368 | replace: ['index'] 369 | } 370 | } 371 | ] 372 | }, 373 | dialog: { 374 | name: "对话框", 375 | handle: [] 376 | } 377 | }; 378 | Object.keys(globalActions).forEach((key: string) => { 379 | 380 | globalActions[key].actions.forEach((aItem: any) => { 381 | state.commit('UPDATE_ACTION', { 382 | type: key, 383 | key: aItem.key, 384 | data: aItem 385 | }); 386 | }); 387 | }); 388 | 389 | } 390 | } 391 | }, 392 | 393 | }); 394 | 395 | store.subscribe(throttle((mutation, state: any) => { 396 | 397 | const currentPage: PageEntity = state.page; 398 | const pages: PageEntity[] = cloneDeep(state.pages); 399 | 400 | const findPage = pages.find(item => item.id === currentPage.id); 401 | 402 | if (findPage) { 403 | findPage.title = currentPage.title; 404 | findPage.icon = currentPage.icon; 405 | findPage.elements = currentPage.elements; 406 | } 407 | console.log('subscribe -> throttle -> state'); 408 | 409 | db.set('pages', state.pages).write(); 410 | db.set('globalModels', state.globalModels).write(); 411 | db.set('globalActions', state.globalActions).write(); 412 | 413 | }, 300)); 414 | 415 | export default store; 416 | -------------------------------------------------------------------------------- /src/components/models/Index.vue: -------------------------------------------------------------------------------- 1 | 210 | 414 | -------------------------------------------------------------------------------- /src/components/actions/Index.vue: -------------------------------------------------------------------------------- 1 | 8 | 206 | 482 | -------------------------------------------------------------------------------- /src/components/aside/Index.vue: -------------------------------------------------------------------------------- 1 | 73 | 562 | --------------------------------------------------------------------------------