├── .editorconfig ├── .env.production ├── .eslintignore ├── .gitignore ├── .npmignore ├── .vscode ├── extensions.json └── settings.json ├── CNAME ├── LICENSE ├── README.md ├── index.html ├── introduction.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── icon │ │ ├── add-outline.svg │ │ ├── code-download-outline.svg │ │ ├── code-working-outline.svg │ │ ├── copy-outline.svg │ │ ├── eye-outline.svg │ │ ├── login-after.svg │ │ ├── login-before.svg │ │ ├── power-outline.svg │ │ ├── trash-outline.svg │ │ └── tree-structure.png │ ├── logo.png │ ├── logo │ │ ├── antd-n.svg │ │ ├── element-n.png │ │ ├── html-n.png │ │ ├── iview-n.png │ │ ├── quasar-n.png │ │ └── vant-n.png │ └── nestable.css ├── components-v2 │ ├── ToolsBar.vue │ └── VCC.vue ├── components │ ├── AttributeInput.vue │ ├── Code.vue │ ├── CodeEditor.vue │ ├── CodeStructure.vue │ ├── JSCodeEditorDialog.vue │ ├── Main.vue │ ├── Preview.vue │ ├── RawComponents.vue │ ├── VueCodeParseDialog.vue │ ├── halower-tree.min.css │ ├── nested.vue │ └── prism.css ├── libs │ ├── UIComponentInit.js │ ├── bro-ele-config.js │ ├── bundle-core-esm.js │ ├── bundle-html2json-common.js │ ├── bundle-html2json-esm.js │ ├── bundle-json2html-common.js │ ├── code-generator-factory.js │ ├── directiveCheck.js │ ├── main-panel.js │ ├── presetAttribute.js │ ├── singleIndexOutput.js │ ├── split-init.js │ ├── store.js │ ├── stringify-object.js │ └── v2-tree.js ├── main.js ├── map │ ├── data.index.js │ ├── load.js │ ├── method.index.js │ ├── style.index.js │ └── template.index.js ├── rawComponents │ ├── antd │ │ ├── button.vue │ │ └── index.vue │ ├── echart │ │ ├── chart.vue │ │ ├── index.vue │ │ └── init.vue │ ├── element │ │ ├── button.vue │ │ ├── container.vue │ │ ├── dialog.vue │ │ ├── final.vue │ │ ├── form-base.vue │ │ ├── form.vue │ │ ├── icon.vue │ │ ├── image.vue │ │ ├── index.vue │ │ ├── layout.vue │ │ ├── menu.vue │ │ └── table.vue │ ├── iview │ │ ├── button.vue │ │ ├── icon.vue │ │ ├── index.vue │ │ ├── layout.vue │ │ ├── other.vue │ │ └── template.vue │ ├── quasar │ │ ├── base.vue │ │ ├── button.vue │ │ └── index.vue │ ├── raw │ │ └── index.vue │ └── vant │ │ ├── button.vue │ │ ├── display.vue │ │ ├── form.vue │ │ ├── icon.vue │ │ ├── index.vue │ │ ├── layout.vue │ │ ├── nav.vue │ │ └── template.vue ├── script │ ├── compile.js │ ├── compileComponent.js │ └── distClear.js ├── test │ └── compileVueOnline.vue └── utils │ ├── common.js │ ├── forCode.js │ ├── initRawComponent.js │ └── lineHelper.js ├── vite.config.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | PUBLIC_PATH=https://static.imonkey.xueersi.com/vue-code-creater/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.vue -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | src 3 | .env.* 4 | vue.config.js 5 | .DS_Store 6 | .editorconfig 7 | .vscode/launch.json 8 | dist/vcc.umd.js 9 | dist/vcc.common.js 10 | 11 | *.html -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["eamodio.gitlens", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.tabSize": 2, 4 | "prettier.printWidth": 120, 5 | "prettier.tabWidth": 2, 6 | "prettier.useTabs": false, 7 | "prettier.semi": true, 8 | "prettier.singleQuote": true, 9 | "prettier.quoteProps": "as-needed", 10 | "prettier.jsxSingleQuote": false, 11 | "prettier.trailingComma": "all", 12 | "prettier.bracketSpacing": true, 13 | "prettier.arrowParens": "always", 14 | "prettier.requirePragma": false, 15 | "prettier.insertPragma": false, 16 | "prettier.proseWrap": "preserve", 17 | "prettier.htmlWhitespaceSensitivity": "css", 18 | "prettier.vueIndentScriptAndStyle": false, 19 | "prettier.endOfLine": "lf", 20 | "prettier.embeddedLanguageFormatting": "auto", 21 | "prettier.singleAttributePerLine": true 22 | } 23 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | vcc.surge.sh 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sahadev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VCC 3 2 | 3 | VCC(Vue Compontent Creator)是 Low Code Generator 中独立的 Vue 组件代码编辑器。可以独立运行。当前你看到的是基于 Vue3 的 VCC 3 版本。 4 | 5 | **通过它可以通过拖拽快速完成 Vue 组件代码骨架的搭建。详见后文视频介绍链接。** 6 | 7 | > 点击这里快速预览效果:[https://vcc3.surge.sh/](https://vcc3.surge.sh/) 当前已经升级至 Vue3 + Vite。 8 | 9 | #### 使用示例 10 | 11 | 请移步至使用 Demo:[https://github.com/sahadev/vcc3-use-demo](https://github.com/sahadev/vcc3-use-demo) 12 | 13 | ## 本地如何运行此项目 14 | 15 | 首先进行安装: 16 | 17 | ```sh 18 | npm i 19 | ``` 20 | 21 | 再进行启动(Vite): 22 | 23 | ``` 24 | npm run dev 25 | ``` 26 | 27 | 运行完成后,就可以访问[http://localhost:9818/](http://localhost:9818/)预览效果了. 28 | 29 | ## 使用介绍 30 | 31 | 此前在 B 站上录了两段视频。可以通过这两段视频简单了解如何使用它: 32 | [【拖拽式Vue组件代码生成平台(LCG)介绍视频】 https://www.bilibili.com/video/BV17h411f7g2/) 33 | [【拖拽式Vue组件代码生成平台(LCG)介绍视频】 https://www.bilibili.com/video/BV17h411f7g2/) 34 | 35 | ### 说明文档 36 | 37 | [https://vcc3-docs.surge.sh/#/](https://vcc3-docs.surge.sh/#/) 38 | 39 | ### 功能更新日志 40 | 41 | - 2022 年 03 月 16 日 支持生成单页 Html,支持 Vue2 以及 Vue3,并支持一键部署至 VCC 服务器。 42 | - 2023 年 12 月 06 日 更新 Element 组件库版本、更新 Vue 框架版本。 43 | 44 | ### 核心仓库 45 | 46 | VCC 依赖于一个核心的代码转换库:[vue-component-code-creater](https://github.com/sahadev/vue-component-code-creater),通过这个库来完成 Vue 文件的解析和 Vue 文件的生成。如果需要更改核心实现,可通过此库提供的源码进行修改。 47 | 48 | ## 贡献 49 | 50 | 1. Fork 仓库 51 | 2. 创建分支 (`git checkout -b my-new-feature`) 52 | 3. 提交修改 (`git commit -am 'Add some feature'`) 53 | 4. 推送 (`git push origin my-new-feature`) 54 | 5. 创建 PR 55 | 56 | ## 欢迎 fork 和反馈 57 | 58 | 如有建议或意见,欢迎在 github [issues](https://github.com/sahadev/vue-component-creater-ui/issues) 区提问 59 | 60 | ## 协议 61 | 62 | 本仓库遵循 [MIT 协议](http://www.opensource.org/licenses/MIT) 63 | 64 | ## 有疑问? 65 | 66 | 可以通过sahadev@foxmail.com给我发送邮件,我会及时回复的。 67 | 68 | 或者加群和大家一起讨论吧! 可以加我微信:SAHADEV-smile,我拉你入群。加我微信时请备注 VCC。 69 | 70 | 另外我也特别希望可以和大家一起做这个项目。这个项目目前主要面对的是前端开发者。后期可以面向后端开发者与产品与 UE。 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 拖拽式Vue组件代码生成编辑器(VCC) 8 | 9 | 10 | 11 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | # vue-component-creater 2 | 3 | 拖拽式的Vue组件生成平台 4 | 5 | 正式环境地址:https://lc.100tal.com/#/ 6 | 7 | ## Project setup 8 | 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ### Compiles and hot-reloads for development 14 | 15 | ``` 16 | yarn run serve 17 | ``` 18 | 19 | ### Compiles and minifies for production 20 | 21 | ``` 22 | yarn run build 23 | ``` 24 | 25 | ### Lints and fixes files 26 | 27 | ``` 28 | yarn run lint 29 | ``` 30 | 31 | ### 优势: 32 | 33 | - 搭建速度快。 34 | - 效果可视化。 35 | - 代码实时生成。 36 | - 提供主流预制模板。 37 | - 无需每次查找组件源代码,节省查找时间。 38 | - 提供主流预制属性。 39 | - 支持属性、模板自行扩展。 40 | 41 | ### 规划: 42 | 43 | 目前仅集成了部分市面上流行的组件库。组件库的集成希望可以通过开源的方式让大家自助添加。 44 | 45 | 添加的流程为: 46 | 在 src/rawComponents 目录下建立以组件名为名称的文件夹(例如:element),并在该文件夹内提供一个 index.vue 文件,然后再将此文件引入到 src/components/RawComponents.vue 文件中。 47 | 48 | \*.vue 文件建议以组件类型划分,例如 Botton 类的组件就可以都放在 button.vue 文件中,然后由 index.vue 文件引用。这样 Botton 的路径为:src/rawComponents/element/button.vue 49 | 50 | 当组件添加完毕后,需要给组件内的元素添加 lc-mark 的标志,这样这个组件才可以被**拖拽**。例如 element 的 Button: 51 | 52 | ```html 53 | 默认按钮 54 | ``` 55 | 56 | 当一切就就绪后,需要对所有的组件进行重新编译才可以正常使用,需执行 npm run compileComponent 命令。此命令会对 src/rawComponents 下所有的组件进行重新编译。如果有不希望编译的,请在 src/script/compile.js 中修改 ignore 属性。 57 | 58 | > **需要注意的是:** 一类组件的 index.vue 文件最好不要编译,因为对这个文件内容的顺序是有要求的,比如**分割线**。而编译会使组件内元素的顺序重新排布(2020年12月28日已经将此问题修复)。 59 | 60 | ### TODO: 61 | 62 | - 常用模板 63 | - 预制常用属性 64 | - 高亮当前属性编辑元素-跑马灯效果 -> 左下箭头高亮展示(2020 年 09 月 21 日效果不好,2020年12月28日已通过outline实现)✔️ 65 | - 尝试从 vue 运行时入手,获取更多原始信息(2020 年 09 月 21 日开始已经废弃,通过迂回方式获得更多信息)✔️ 66 | - 提供调试控制台,输出当前代码结构关系图,类似 vue-tools(2020 年 10 月 02 日 已实现基本的文本形式的结构输出,需要树形的可折叠结构)。✔️ 67 | - 将组件库、预制属性、属性解析规则挪至单独的项目中。 68 | - 支持生成预制的 data,methods。✔️ 69 | - 我的常用:组件元素、整个组件。 70 | - 接入自有的数据统计工具:神策。✔️ 71 | 72 | --- 73 | 74 | 使用过程中发现的问题: 75 | 76 | - 1.不支持删除某个元素。 ✔️ 77 | - 2.编辑 input 会造成高亮框混乱。 ✔️ 78 | - 3.没有下拉组件。✔️ 79 | - 4.删除外框内容子元素会一同被清空(两个 div 嵌套)。✔️ 80 | - 5.input 标签无法生成代码。 ✔️ 81 | - 6.提供一部分常用的组合组件。key:input ✔️ 82 | - 7.取消对 Html 元素的排序。需要更改 json2xml 的数据结构。Json 数据生成规则也需要变动。 ✔️ 83 | - 8.提供一些常见的模板。✔️ 84 | - 9.对复合组件还不支持 ✔️ 85 | - 10.针对某些组件预设样式。例如分页组件一定是在最下面且宽度为 100%的。 86 | - 11.要对大型表单的搭建良好支持(例如课程管理页面的搭建https://wiki.zhiyinlou.com/pages/viewpage.action?pageId=60305227)。 87 | - 12.预制更多常用的复合组件:{ key: value }。 88 | - 13.更改父组件的属性,子组件被删除。✔️ 89 | - 14.对于:inline="true"的解析与生成需要完整支持,目前只能输出:inline。✔️ 90 | 91 | --- 92 | 93 | 2020 年 09 月 16 日 目前的进展: 94 | - 1.当下已支持通过更改 Vue-loader 实现在运行时拿到组件源代码。具体可以查看 CommitID:c66593ff87c07c60670e634f50f49e030f68b63b,该次提交已可以在控制台看到源代码输出。 95 | - 2.重新定义源代码的获取方式: 通过将源代码解析为 Json 结构, 然后在编辑时通过对对象的操作实现代码的生成。所以这里涉及到对 html 的解析与生成。解析部分需要重写,生成逻辑之前已完成。 96 | 97 | 2020 年 09 月 17 日 目前进展: 98 | 嵌套结构基本稳定,可进行各种组合 99 | 输出代码已移除 ID 属性 100 | TODO: 稳固属性编辑,输出代码值类型默认为字符串 101 | 102 | 2020 年 09 月 19 日 103 | 编辑的基本单位应当以最小组件,即不能再拆分的单位 104 | 105 | 2020 年 09 月 22 日 106 | 右上角上显示的统计: 107 | 108 | - 1.累计访问次数(PV)。 109 | - 2.生成的组件数(点击复制的统计)。 110 | - 3.基础组件数(统计可拖拽的基础组件)。 111 | 112 | 2020 年 09 月 23 日 113 | 开始规划组件库,分设常用、组件厂商,下设组件类型 114 | 需要彻底修改 xml 解析库,保证原始顺序不出错,或者不要编译 index.vue 115 | 116 | 2020 年 10 月 16 日 1.支持拖入时位置确认,可以在前、在中、在后,目前只能追加最后。 2.支持已拖入的拖动调整。 117 | 118 | 2020年11月10日 119 | 120 | - 需要在成熟的组件库下添加超链接,方便直接去该网站获得具体的属性说明。 121 | - 完善iView组件库 122 | - 集成Quasar组件库:http://www.quasarchs.com/ 123 | - 这个东西是主要针对什么人群,要解决什么痛点? 124 | 125 | 2020年11月17日 126 | 127 | - 属性编辑增加按钮需要调整:因为最后一个属性是无法删除的。 128 | - 元素的删除支持快捷键. 129 | - Form表单增加内容为按钮的。 130 | - 属性的输入要全部为字符串,不能为boolean。 131 | - 无法在div内部的后面再追加。:center中 132 | - 快速搜索输入框已有内容无法再次弹出 133 | - 复制代码有问题,内容复制不上 ✔️ 134 | 135 | 2020年12月04日 136 | 在编译form-base文件时,发现html与js都出了问题,问题发生在没有闭合标签的img以及下面这段JS代码上: 137 | ```js 138 | 139 | const isJPG = file.type === 'image/jpeg'; 140 | const isLt2M = file.size / 1024 / 1024 < 2; 141 | 142 | if (!isJPG) { 143 | this.$message.error('上传头像图片只能是 JPG 格式!'); 144 | } 145 | if (!isLt2M) { 146 | this.$message.error('上传头像图片大小不能超过 2MB!'); 147 | } 148 | return isJPG && isLt2M; 149 | 150 | ``` 151 | 152 | **注意:** 在编辑组件库时,在script标签内部慎用大于小于号,会导致预编译阶段文本内容解析出错! 153 | 154 | 2021年01月08日10:27:25 155 | 在组件的css中不能使用scss的写法,否则编译Vue文件会不通过 156 | 157 | 2021年01月11日 158 | 今天支持重新命名文件中重复声明的变量,但**在文件中不能声明与prop相同的声明** 159 | 160 | --- 161 | 162 | ### 下一步重点工作 163 | 164 | 1.做大量实践,探索组件单元最合适的组成。✔️ 165 | 2.hover 方式标记组件的区域范围。✔️ 166 | 3.增加沟通群二维码。❎ 167 | 4.增加UI库vuetifyjs:https://vuetifyjs.com/zh-Hans/ 168 | 5.增加UI库kendo:https://www.telerik.com/kendo-vue-ui 169 | 170 | --- 171 | 172 | ### 核心原理介绍 173 | 174 | 我们知道,在编写后的 vue 代码在运行时会生成实际的 Html 代码,而组件生成平台的职责是将这些 Html 再转换为 vue 代码。 175 | 176 | 为了达到这样的目的,我们目前可行的思路是:将原始的代码文件进行预编译:对指定的 vue 组件分配一个随机 ID,并将这个 vue 文件的代码结构转换为 Json,以 map 的形式存储于 map.js 文件中。在运行时,将 map.js 文件加载进内存。当拖动某个被 lc-mark 标记的元素时,我们可获得这个元素相应的 ID,再通过这个 ID 到 map 中查找,于是获得了对应的原始代码结构。当拖入到某个元素中时,也通过相同的方式获得目标元素的原始代码,再将这两部分原始代码合并,并建立上下级关系。随后,通过新的代码结构,分析对应的@click、v-model、class 等我们所关注的属性,然后再将其生成对应的代码插入到将要生成的 Vue 组件中。如此,便形成了一个较为完整的 Vue 组件代码。 177 | 178 | 为了实现以上思路,有几项关键技术: 179 | 180 | - 对 Vue 组件的解析与生成 181 | - 上下级组件之间的数据结构关系 182 | - html 元素与 Vue 代码之间吻合的对应关系 183 | - Vue 代码的关键字解析,如@click 184 | - 将代码转换为对象,将对象转换为代码 185 | - 辅助线的定位与绘制 186 | 187 | ### 标准 188 | 189 | 本项目中所使用的标准色为: 190 | 191 | - 蓝色:#435466; 192 | - 绿色:#4dba87; 193 | - 红色:#ff6159; 194 | 195 | --- 196 | 197 | #### 不能接入到本平台的或者会出现不符合预期的元素 198 | 199 | - el-dialog 无法直接显示在左侧组件栏中(2020年12月28日已通过一个按钮代表解决) 200 | - el-input-number 拖拽时会抢占焦点,无法拖拽 ✔️ 201 | - el-select lc-mark标记的元素与实际运行时的元素不是同一个,无法找到其原始代码 ✔️ 202 | - el-switch 原因与el-select一致 ✔️ 通过检视图解决 203 | - el-time-select 原因与el-select一致 ✔️ 204 | - el-image 因为某种原因,没有正常完成初始化工作 ✔️ 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lcg-vcc3", 3 | "description": "Low Code Generator -> Vue Component Creater", 4 | "version": "0.5.3", 5 | "private": false, 6 | "keywords": [ 7 | "low-code", 8 | "editor" 9 | ], 10 | "main": "./dist/vcc3.umd.min.js", 11 | "scripts": { 12 | "dev": "vite --port 9818", 13 | "serve": "vite --port 9818", 14 | "build:release": "vite build --base=https://static.imonkey.xueersi.com/vcc3/", 15 | "build": "vue-cli-service build --report --target lib --name vcc3 './src/components-v2/VCC.vue'", 16 | "build:win": "vue-cli-service build --report --target lib --name vcc3 ./src/components-v2/VCC.vue && node ./src/script/distClear.js", 17 | "compileAndbuild:dev": "npm run compileComponent && vue-cli-service build", 18 | "lint": "vue-cli-service lint", 19 | "build:prod": "vue-cli-service build --mode production", 20 | "compileAndBuild:prod": "npm run compileComponent && vue-cli-service build --mode production", 21 | "compileComponent": "node ./src/script/compile.js", 22 | "debugParser": "node ./src/test/parserJsCode.js" 23 | }, 24 | "dependencies": { 25 | "@element-plus/icons-vue": "^2.3.1", 26 | "@vitejs/plugin-vue": "^1.10.0", 27 | "@vue/compiler-sfc": "^3.2.22", 28 | "ant-design-vue": "^3.0.0-alpha.14", 29 | "axios": "^0.21.4", 30 | "codemirror-editor-vue3": "^0.2.4", 31 | "copy-to-clipboard": "^3.3.1", 32 | "crypto-random-string": "^3.3.1", 33 | "css": "^3.0.0", 34 | "css-scoped": "^1.0.0", 35 | "echarts": "^5.4.3", 36 | "ejs": "^3.1.6", 37 | "element-plus": "^2.4.3", 38 | "escodegen": "^2.0.0", 39 | "espree": "^7.3.0", 40 | "eventemitter3": "^4.0.7", 41 | "file-saver": "^2.0.2", 42 | "fs-extra": "^9.0.1", 43 | "glob": "^7.1.6", 44 | "keymaster": "^1.6.2", 45 | "lodash-es": "^4.17.21", 46 | "nanoid": "^3.1.30", 47 | "prettier": "^2.4.0", 48 | "split.js": "^1.6.2", 49 | "vant": "^3.3.7", 50 | "vue": "^3.3.10", 51 | "vue-echarts": "^6.6.2", 52 | "vue-github-button": "^1.3.0", 53 | "vue-template-compiler": "^2.6.14", 54 | "vuedraggable": "^4.1.0", 55 | "vuex": "^4.0.2" 56 | }, 57 | "devDependencies": { 58 | "@babel/generator": "^7.11.6", 59 | "@rollup/plugin-dynamic-import-vars": "^1.4.1", 60 | "@vue/eslint-config-airbnb": "^5.0.2", 61 | "babel-eslint": "^10.0.3", 62 | "eslint": "^6.7.2", 63 | "eslint-plugin-import": "^2.20.1", 64 | "eslint-plugin-vue": "^6.1.2", 65 | "is-obj": "^3.0.0", 66 | "is-regexp": "^3.0.0", 67 | "lint-staged": "^9.5.0", 68 | "sass": "^1.25.0", 69 | "sass-loader": "^8.0.2", 70 | "@vue/cli-service": "^5.0.8", 71 | "vite": "^2.6.14" 72 | }, 73 | "eslintConfig": { 74 | "root": true, 75 | "env": { 76 | "node": true 77 | }, 78 | "extends": [ 79 | "plugin:vue/essential" 80 | ], 81 | "parserOptions": { 82 | "parser": "babel-eslint" 83 | }, 84 | "rules": {} 85 | }, 86 | "browserslist": [ 87 | "> 1%", 88 | "last 2 versions" 89 | ], 90 | "gitHooks": { 91 | "pre-commit": "lint-staged" 92 | }, 93 | "lint-staged": { 94 | "*.{js,jsx,vue}": [ 95 | "vue-cli-service lint", 96 | "git add" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 拖拽式Vue组件代码生成编辑器(VCC) 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icon/add-outline.svg: -------------------------------------------------------------------------------- 1 | Add -------------------------------------------------------------------------------- /src/assets/icon/code-download-outline.svg: -------------------------------------------------------------------------------- 1 | Code Download -------------------------------------------------------------------------------- /src/assets/icon/code-working-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | Code Working 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icon/copy-outline.svg: -------------------------------------------------------------------------------- 1 | Copy -------------------------------------------------------------------------------- /src/assets/icon/eye-outline.svg: -------------------------------------------------------------------------------- 1 | Eye -------------------------------------------------------------------------------- /src/assets/icon/login-after.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon/login-before.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon/power-outline.svg: -------------------------------------------------------------------------------- 1 | Power -------------------------------------------------------------------------------- /src/assets/icon/trash-outline.svg: -------------------------------------------------------------------------------- 1 | Trash -------------------------------------------------------------------------------- /src/assets/icon/tree-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/icon/tree-structure.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo/antd-n.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vue 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/logo/element-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo/element-n.png -------------------------------------------------------------------------------- /src/assets/logo/html-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo/html-n.png -------------------------------------------------------------------------------- /src/assets/logo/iview-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo/iview-n.png -------------------------------------------------------------------------------- /src/assets/logo/quasar-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo/quasar-n.png -------------------------------------------------------------------------------- /src/assets/logo/vant-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahadev/vue-component-creater-ui/ddcdf5d08ab33592cc2981ee81279b0394ce95d7/src/assets/logo/vant-n.png -------------------------------------------------------------------------------- /src/assets/nestable.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style for nestable 3 | */ 4 | .nestable { 5 | position: relative; 6 | } 7 | 8 | .nestable-rtl { 9 | direction: rtl; 10 | } 11 | 12 | .nestable .nestable-list { 13 | margin: 0; 14 | padding: 0 0 0 40px; 15 | list-style-type: none; 16 | } 17 | 18 | .nestable-rtl .nestable-list { 19 | padding: 0 40px 0 0; 20 | } 21 | 22 | .nestable>.nestable-list { 23 | padding: 0; 24 | } 25 | 26 | .nestable-item, 27 | .nestable-item-copy { 28 | margin: 10px 0 0; 29 | } 30 | 31 | .nestable-item:first-child, 32 | .nestable-item-copy:first-child { 33 | margin-top: 0; 34 | } 35 | 36 | .nestable-item .nestable-list, 37 | .nestable-item-copy .nestable-list { 38 | margin-top: 10px; 39 | } 40 | 41 | .nestable-item { 42 | position: relative; 43 | } 44 | 45 | .nestable-item.is-dragging .nestable-list { 46 | pointer-events: none; 47 | } 48 | 49 | .nestable-item.is-dragging * { 50 | opacity: 0; 51 | filter: alpha(opacity=0); 52 | } 53 | 54 | .nestable-item.is-dragging:before { 55 | content: ' '; 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | right: 0; 60 | bottom: 0; 61 | background-color: rgba(106, 127, 233, 0.274); 62 | border: 1px dashed rgb(73, 100, 241); 63 | -webkit-border-radius: 5px; 64 | border-radius: 5px; 65 | } 66 | 67 | .nestable-drag-layer { 68 | position: fixed; 69 | top: 0; 70 | left: 0; 71 | z-index: 100; 72 | pointer-events: none; 73 | } 74 | 75 | .nestable-rtl .nestable-drag-layer { 76 | left: auto; 77 | right: 0; 78 | } 79 | 80 | .nestable-drag-layer>.nestable-list { 81 | position: absolute; 82 | top: 0; 83 | left: 0; 84 | padding: 0; 85 | background-color: rgba(106, 127, 233, 0.274); 86 | } 87 | 88 | .nestable-rtl .nestable-drag-layer>.nestable-list { 89 | padding: 0; 90 | } 91 | 92 | .nestable [draggable="true"] { 93 | cursor: move; 94 | } 95 | 96 | .nestable-handle { 97 | display: inline; 98 | } 99 | 100 | .expandable .has-children ol { 101 | display: none; 102 | } 103 | 104 | .expandable .has-children.is-active>ol { 105 | display: block; 106 | } -------------------------------------------------------------------------------- /src/components-v2/ToolsBar.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 97 | 98 | 116 | 117 | 149 | -------------------------------------------------------------------------------- /src/components/Code.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 185 | 186 | -------------------------------------------------------------------------------- /src/components/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 125 | 126 | 143 | 144 | -------------------------------------------------------------------------------- /src/components/CodeStructure.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 119 | 120 | -------------------------------------------------------------------------------- /src/components/JSCodeEditorDialog.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 129 | 130 | -------------------------------------------------------------------------------- /src/components/Main.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 32 | 33 | 62 | -------------------------------------------------------------------------------- /src/components/Preview.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/VueCodeParseDialog.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/nested.vue: -------------------------------------------------------------------------------- 1 | 12 | 81 | -------------------------------------------------------------------------------- /src/components/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.20.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript */ 3 | /** 4 | * okaidia theme for JavaScript, CSS and HTML 5 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 6 | * @author ocodia 7 | */ 8 | 9 | code, 10 | pre { 11 | color: #f8f8f2; 12 | background: none; 13 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 14 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | /* Code blocks */ 34 | pre { 35 | padding: 1em; 36 | margin: 0.5em 0; 37 | overflow: auto; 38 | border-radius: 0.3em; 39 | } 40 | 41 | :not(pre) > code, 42 | pre { 43 | background: #272822; 44 | } 45 | 46 | /* Inline code */ 47 | :not(pre) > code { 48 | padding: 0.1em; 49 | border-radius: 0.3em; 50 | white-space: normal; 51 | } 52 | 53 | .token.comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: slategray; 58 | } 59 | 60 | .token.punctuation { 61 | color: #f8f8f2; 62 | } 63 | 64 | .token.namespace { 65 | opacity: 0.7; 66 | } 67 | 68 | .token.property, 69 | .token.tag, 70 | .token.constant, 71 | .token.symbol, 72 | .token.deleted { 73 | color: #f92672; 74 | } 75 | 76 | .token.boolean, 77 | .token.number { 78 | color: #ae81ff; 79 | } 80 | 81 | .token.selector, 82 | .token.attr-name, 83 | .token.string, 84 | .token.char, 85 | .token.builtin, 86 | .token.inserted { 87 | color: #a6e22e; 88 | } 89 | 90 | .token.operator, 91 | .token.entity, 92 | .token.url, 93 | .language-css .token.string, 94 | .style .token.string, 95 | .token.variable { 96 | color: #f8f8f2; 97 | } 98 | 99 | .token.atrule, 100 | .token.attr-value, 101 | .token.function, 102 | .token.class-name { 103 | color: #e6db74; 104 | } 105 | 106 | .token.keyword { 107 | color: #66d9ef; 108 | } 109 | 110 | .token.regex, 111 | .token.important { 112 | color: #fd971f; 113 | } 114 | 115 | .token.important, 116 | .token.bold { 117 | font-weight: bold; 118 | } 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | 123 | .token.entity { 124 | cursor: help; 125 | } 126 | -------------------------------------------------------------------------------- /src/libs/UIComponentInit.js: -------------------------------------------------------------------------------- 1 | // 其它UI组件库应该在这里集成 2 | function loadVant() { 3 | (() => import("vant/lib/index.css"))(); 4 | const vantLoadPromise = (() => import("vant"))(); 5 | return vantLoadPromise; 6 | } 7 | 8 | function loadAntD() { 9 | (() => import("ant-design-vue/dist/antd.css"))(); 10 | const vantLoadPromise = (() => import("ant-design-vue"))(); 11 | return vantLoadPromise; 12 | } 13 | 14 | export default function loadCompontents() { 15 | return Promise.all([loadAntD(), loadVant()]); 16 | } 17 | -------------------------------------------------------------------------------- /src/libs/bro-ele-config.js: -------------------------------------------------------------------------------- 1 | 2 | import { getRawComponentKey } from '@/utils/common' 3 | import { replaceRowID, removeAllID, updateLinkTree, linkRelationShip } from '@/utils/forCode' 4 | 5 | export function brotherEleEnum() { 6 | return [{ 7 | name: 'el-option', 8 | ifInDoc: false // 这个组件是否默认在Dom上展示,如果不展示,则添加后不需要更新Dom,否则需要更新Dom 9 | }, 10 | { 11 | name: 'el-table-column', 12 | ifInDoc: false 13 | }, 14 | { 15 | name: 'el-checkbox', 16 | ifInDoc: true 17 | }, 18 | { 19 | name: 'el-radio', 20 | ifInDoc: true 21 | }]; 22 | } 23 | 24 | function checkIsInVaildElement(event) { 25 | return new Promise((resolve, reject) => { 26 | const target = event.path.find(item => item.attributes.lc_id.nodeValue); 27 | // 获取带有ID的原始结构,这个ID用于生成一个不同ID的副本 28 | const __rawVueInfo__ = window.treeWithID && window.treeWithID[target.attributes.lc_id.nodeValue]; 29 | if (target && __rawVueInfo__) { 30 | const key = getRawComponentKey(__rawVueInfo__); 31 | const result = brotherEleEnum().find(item => item.name === key && item.ifInDoc); 32 | if (result) { 33 | resolve({ target, __rawVueInfo__ }); 34 | } else { 35 | reject(); 36 | } 37 | } 38 | }) 39 | } 40 | 41 | export function copyBroCode(__rawVueInfo__){ 42 | // 初始化原型 43 | let newDropObj = Object.create(__rawVueInfo__.__proto__); 44 | // 拷贝内部属性 45 | Object.assign(newDropObj, JSON.parse(JSON.stringify(__rawVueInfo__))); 46 | 47 | newDropObj.__proto__.parentCodeNode.__children.push(newDropObj); 48 | } 49 | 50 | /** 51 | * 这个方法是给控制区域使用的,增加兄弟组件时,控制区域会实时更新 52 | * @param {*} element 53 | */ 54 | export function initElementHoverAction(element) { 55 | let currentBroInfo = null; 56 | 57 | const addBroIcon = document.querySelector('.add-bro'); 58 | 59 | let isInBroIcon = false; 60 | addBroIcon.addEventListener('mouseover', event => { 61 | isInBroIcon = true; 62 | }) 63 | addBroIcon.addEventListener('mouseout', event => { 64 | isInBroIcon = false; 65 | }) 66 | 67 | element.addEventListener('mouseover', event => { 68 | event.stopPropagation(); 69 | }) 70 | 71 | return function () { 72 | // 初始化原型 73 | let newDropObj = Object.create(currentBroInfo.__rawVueInfo__.__proto__); 74 | // 拷贝内部属性 75 | Object.assign(newDropObj, JSON.parse(JSON.stringify(currentBroInfo.__rawVueInfo__))); 76 | 77 | // 有__key__键可以使在通过updateLinkTree更新结构时,内部的原型指向外部的原型 78 | newDropObj.__key__ = "__children"; 79 | 80 | // 使新拖入的代码与原来的做脱离 81 | const newHtmlCode = replaceRowID(newDropObj, currentBroInfo.target.outerHTML); 82 | // 这里不能是任意的target,必须是已存在代码树,有引用链的节点 83 | currentBroInfo.target.parentNode.insertAdjacentHTML("beforeend", newHtmlCode); 84 | 85 | newDropObj.__proto__.parentCodeNode.__children.push(newDropObj); 86 | 87 | // 将所有子节点指向父节点 88 | linkRelationShip(newDropObj); 89 | 90 | // 更新到一个tree上面,维持引用 91 | updateLinkTree(newDropObj); 92 | 93 | // 删除所有的ID 94 | removeAllID(newDropObj); 95 | } 96 | } -------------------------------------------------------------------------------- /src/libs/bundle-html2json-common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //该文件用于解析HTML,输出为Object对象 4 | 5 | Object.defineProperty(exports, '__esModule', { value: true }); 6 | 7 | const htmlparser2 = require("htmlparser2"); 8 | 9 | function getNodeContent(node) { 10 | return node[Object.keys(node)[0]]; 11 | } 12 | 13 | /**每个节点的表示方法为: 14 | { 15 | tagname: { 16 | key1: value1, 17 | key2: value2, 18 | __children: [ 19 | { 20 | 21 | } 22 | ] 23 | } 24 | }*/ 25 | function generateNewNode(tagName, attributes = {}) { 26 | // 构建新节点 27 | const newNode = {}; 28 | newNode[tagName] = attributes; 29 | attributes.__children = []; 30 | return newNode; 31 | } 32 | 33 | function parseHtml(htmlData) { 34 | return new Promise((resolve, reject) => { 35 | // 根节点 36 | const root = generateNewNode('root'); 37 | // 当前访问的节点 38 | let currentAccessObject = root; 39 | // 之前访问的节点数组 40 | let lastAccessStack = [root]; 41 | 42 | // options docment: https://github.com/fb55/htmlparser2/wiki/Parser-options 43 | const parser = new htmlparser2.Parser({ 44 | onopentag(tagname, attributes) { 45 | const newNode = generateNewNode(tagname, attributes); 46 | lastAccessStack.push(newNode); 47 | getNodeContent(currentAccessObject).__children.push(newNode); 48 | currentAccessObject = newNode; 49 | }, 50 | ontext(text) { 51 | if (text.trim()) { 52 | getNodeContent(currentAccessObject).__text__ = text.trim(); 53 | } 54 | }, 55 | onclosetag(tagname) { 56 | lastAccessStack.pop(); 57 | currentAccessObject = lastAccessStack[lastAccessStack.length - 1]; 58 | }, 59 | onend() { 60 | resolve(root); 61 | }, 62 | 63 | onerror(error) { 64 | reject(error); 65 | } 66 | }, { 67 | lowerCaseAttributeNames: false, 68 | lowerCaseTags: false, 69 | }); 70 | parser.write( 71 | htmlData 72 | ); 73 | 74 | parser.end(); 75 | }) 76 | } 77 | 78 | async function html2Json(htmlData) { 79 | return await parseHtml(htmlData); 80 | } 81 | 82 | exports.html2Json = html2Json; 83 | -------------------------------------------------------------------------------- /src/libs/bundle-html2json-esm.js: -------------------------------------------------------------------------------- 1 | //该文件用于解析HTML,输出为Object对象 2 | import { Parser } from "htmlparser2" 3 | 4 | function getNodeContent(node) { 5 | return node[Object.keys(node)[0]]; 6 | } 7 | 8 | /**每个节点的表示方法为: 9 | { 10 | tagname: { 11 | key1: value1, 12 | key2: value2, 13 | __children: [ 14 | { 15 | 16 | } 17 | ] 18 | } 19 | }*/ 20 | function generateNewNode(tagName, attributes = {}) { 21 | // 构建新节点 22 | const newNode = {}; 23 | newNode[tagName] = attributes; 24 | attributes.__children = []; 25 | return newNode; 26 | } 27 | 28 | function parseHtml(htmlData) { 29 | return new Promise((resolve, reject) => { 30 | // 根节点 31 | const root = generateNewNode('root'); 32 | // 当前访问的节点 33 | let currentAccessObject = root; 34 | // 之前访问的节点数组 35 | let lastAccessStack = [root]; 36 | 37 | // options docment: https://github.com/fb55/htmlparser2/wiki/Parser-options 38 | const parser = new Parser({ 39 | onopentag(tagname, attributes) { 40 | const newNode = generateNewNode(tagname, attributes); 41 | lastAccessStack.push(newNode); 42 | getNodeContent(currentAccessObject).__children.push(newNode); 43 | currentAccessObject = newNode; 44 | }, 45 | ontext(text) { 46 | if (text.trim()) { 47 | getNodeContent(currentAccessObject).__text__ = text.trim(); 48 | } 49 | }, 50 | onclosetag(tagname) { 51 | lastAccessStack.pop(); 52 | currentAccessObject = lastAccessStack[lastAccessStack.length - 1]; 53 | }, 54 | onend() { 55 | resolve(root); 56 | }, 57 | 58 | onerror(error) { 59 | reject(error); 60 | } 61 | }, { 62 | lowerCaseAttributeNames: false, 63 | lowerCaseTags: false, 64 | }); 65 | parser.write( 66 | htmlData 67 | ); 68 | parser.end(); 69 | }) 70 | } 71 | 72 | async function html2Json(htmlData) { 73 | return await parseHtml(htmlData); 74 | } 75 | 76 | export { html2Json }; 77 | -------------------------------------------------------------------------------- /src/libs/code-generator-factory.js: -------------------------------------------------------------------------------- 1 | // 代码生成对象工厂,每次初始化需要获取一个新的实例,所以工厂方法模式最为适用 2 | import { CodeGenerator } from "./bundle-core-esm"; 3 | import { checkIsDataDirectives, checkIsMethodDirectives } from '@/libs/directiveCheck'; 4 | import stringifyObject from '@/libs/stringify-object' 5 | 6 | export function createNewCodeGenerator() { 7 | return new CodeGenerator({ 8 | convertDataResult: function (dataCodeArr) { 9 | let result = dataCodeArr; 10 | // 干扰数据结果 11 | if (dataCodeArr.length > 0) { 12 | result = dataCodeArr.map((item) => { 13 | const kav = item.split(":"); 14 | const key = kav[0]; 15 | // 这里获取的是原始data数据 16 | if (window.dataSourceMap[key] || window.dataSourceMap[key] == 0) { 17 | return `${key}: ${stringifyObject(window.dataSourceMap[key], { 18 | indent: " ", 19 | singleQuotes: false, 20 | })}`; 21 | } else { 22 | return item; 23 | } 24 | }); 25 | } 26 | 27 | return result; 28 | }, 29 | convertMethodResult(methodCodeArr) { 30 | let result = methodCodeArr; 31 | if (methodCodeArr && methodCodeArr.length > 0) { 32 | result = methodCodeArr.map(methodItem => { 33 | const kav = methodItem.split(":"); 34 | const key = kav[0]; 35 | // 这里获取的是原始data数据 36 | if (window.methodSourceMap && window.methodSourceMap[key]) { 37 | return `${key}: ${window.methodSourceMap[key]}`; 38 | } else { 39 | return methodItem; 40 | } 41 | }) 42 | } 43 | return result; 44 | }, 45 | preConvertStyleResult(selectorSet) { 46 | let result = ''; 47 | const selectorValues = selectorSet.values(); 48 | const selectorKeys = Object.keys(window.styleSourceMap); 49 | 50 | for (const selector of selectorValues) { 51 | // styleSourceMap中保留了所有的原始选择器,这里只处理class的 52 | const findResults = selectorKeys.filter(key => { 53 | return key === `.${selector}` || key.indexOf(`.${selector} `) >= 0 || key.indexOf(` .${selector}`) >= 0; 54 | }) 55 | 56 | if (findResults && findResults.length > 0) { 57 | findResults.forEach(findResult => { 58 | result += `${findResult} { ${window.styleSourceMap[findResult]} }\n`; 59 | }) 60 | selectorSet.delete(selector); 61 | } 62 | } 63 | return result; 64 | }, 65 | checkIsDataDirectives, 66 | checkIsMethodDirectives, 67 | unSupportedKey: function (key, value) { 68 | // 对于这一类需要警示,因为可能是被遗漏的 69 | if (/^v-/g.test(key) || /^:+/g.test(key)) { 70 | console.warn(`可能遗漏了这些: ${key}: ${value}`); 71 | } else { 72 | console.info(`unsupport key: ${key}: ${value}`); 73 | } 74 | } 75 | }); 76 | } -------------------------------------------------------------------------------- /src/libs/directiveCheck.js: -------------------------------------------------------------------------------- 1 | export function checkIsDataDirectives(key) { 2 | return [ 3 | ':prop', 4 | 'v-for', 5 | ':data', 6 | ':src', 7 | ':model', 8 | ':rules', 9 | ':total', 10 | ':current-page', 11 | 'v-model', 12 | 'v-if', 13 | ':reverse', 14 | ':show-file-list', 15 | ':file-list'].includes(key) || /^:+/g.test(key); 16 | } 17 | 18 | export function checkIsMethodDirectives(key) { 19 | return [ 20 | ':before-close', 21 | ':on-preview', 22 | ':on-remove', 23 | ':before-remove', 24 | ':on-exceed', 25 | ':on-success', 26 | ':format', 27 | ':before-upload'].includes(key); 28 | } -------------------------------------------------------------------------------- /src/libs/presetAttribute.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "el-button": { 3 | "@click": "onButtonClick", 4 | size: "small", 5 | }, 6 | "el-radio-group": { 7 | "v-model": "radio", 8 | }, 9 | "el-radio": { 10 | ":label": 0, 11 | }, 12 | "el-checkbox-group": { 13 | }, 14 | "el-link": { 15 | "@click": "onClickLink", 16 | }, 17 | "el-select": { 18 | size: "small", 19 | }, 20 | "el-input": { 21 | "v-model": "input", 22 | placeholder: "请输入内容", 23 | size: "small", 24 | class: "input", 25 | }, 26 | "el-pagination": { 27 | "@size-change": "handleSizeChange", 28 | "@current-change": "handleCurrentChange", 29 | ":current-page": "currentPage", 30 | ":page-sizes": "[10, 20, 50, 100]", 31 | ":page-size": "10", 32 | layout: "total, sizes, prev, pager, next, jumper", 33 | ":total": "234", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/libs/singleIndexOutput.js: -------------------------------------------------------------------------------- 1 | import { parseComponent } from "vue-template-compiler/browser"; 2 | import ejs from "ejs"; 3 | 4 | const outputVueTemplate = ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Page 13 | 16 | <% for(var i = 0; i < cssLibs.length; ++i) { %> 17 | <% } %> 18 | 19 | 20 |
21 | <%- templateHolder %> 22 |
23 | 24 | <% for(var i = 0; i < scriptLibs.length; ++i) { %> 25 | <% } %> 26 | 33 | `; 34 | 35 | const libAddressMap = { 36 | vue: { 37 | js: ["https://cdn.bootcdn.net/ajax/libs/vue/3.2.31/vue.global.min.js"], 38 | css: "", 39 | }, 40 | ele: { 41 | js: ["https://cdn.bootcdn.net/ajax/libs/element-plus/2.1.0/index.full.min.js"], 42 | css: "https://cdn.bootcdn.net/ajax/libs/element-plus/2.1.0/theme-chalk/index.min.css", 43 | libName: "ElementPlus", 44 | }, 45 | antd: { 46 | js: [ 47 | "https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.8/dayjs.min.js", 48 | "https://cdn.bootcdn.net/ajax/libs/ant-design-vue/3.0.0-alpha.14/antd.min.js", 49 | ], 50 | css: "https://cdn.bootcdn.net/ajax/libs/ant-design-vue/3.0.0-alpha.14/antd.min.css", 51 | libName: "antd", 52 | }, 53 | vant: { 54 | js: ["https://cdn.bootcdn.net/ajax/libs/vant/3.3.7/vant.min.js"], 55 | css: "https://cdn.bootcdn.net/ajax/libs/vant/3.3.7/index.min.css", 56 | libName: "vant", 57 | }, 58 | }; 59 | 60 | export default function (vueCode, dependenciesLibs, vue3 = true) { 61 | const { template, script, styles, customBlocks } = parseComponent(vueCode); 62 | 63 | let newScript = script.content.replace(/\s*export default\s*/, ""); 64 | 65 | const tempDependenciesLibs = dependenciesLibs.slice(); 66 | const tempLibAddressMap = vue3 ? libAddressMap: libAddressMapForVue2 67 | 68 | tempDependenciesLibs.unshift("vue"); 69 | 70 | const output = ejs.render(outputVueTemplate, { 71 | cssLibs: tempDependenciesLibs.map((item) => tempLibAddressMap[item].css).filter((item) => !!item), 72 | scriptLibs: tempDependenciesLibs 73 | .map((item) => tempLibAddressMap[item].js) 74 | .flat() 75 | .filter((item) => !!item), 76 | vue3, 77 | vue3UseLib: tempDependenciesLibs 78 | .filter((item) => item != "vue") 79 | .map((item) => tempLibAddressMap[item].libName), 80 | style: styles[0].content, 81 | templateHolder: template.content, 82 | logicHolder: newScript, 83 | }); 84 | 85 | return output; 86 | } 87 | 88 | const libAddressMapForVue2 = { 89 | vue: { 90 | js: ["https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js"], 91 | css: "", 92 | }, 93 | ele: { 94 | js: ["https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.7/index.min.js"], 95 | css: "https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.7/theme-chalk/index.min.css", 96 | }, 97 | antd: { 98 | js: [ 99 | "https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js", 100 | "https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.js", 101 | ], 102 | css: "https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.css", 103 | }, 104 | vant: { 105 | js: ["https://cdn.bootcdn.net/ajax/libs/vant/2.12.44/vant.min.js"], 106 | css: "https://cdn.bootcdn.net/ajax/libs/vant/2.12.44/index.min.css", 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /src/libs/split-init.js: -------------------------------------------------------------------------------- 1 | import Split from 'split.js' 2 | 3 | export function splitInit() { 4 | let sizes = localStorage.getItem('split-sizes') 5 | let sizeCodes = localStorage.getItem('split-sizes-code') 6 | 7 | if (sizes) { 8 | sizes = JSON.parse(sizes) 9 | } else { 10 | sizes = [50, 50] // default sizes 11 | } 12 | 13 | if (sizeCodes) { 14 | sizeCodes = JSON.parse(sizeCodes) 15 | } else { 16 | sizeCodes = [70, 30] // default sizes 17 | } 18 | 19 | Split(['.base-component-container', '.main-container'], { 20 | sizes: sizes, 21 | gutterSize: 4, 22 | minSize: [400, 375], 23 | gutter: (index, direction) => { 24 | const gutter = document.createElement('div') 25 | gutter.className = `gutter gutter-${direction}` 26 | gutter.addEventListener("mousemove", () => { 27 | gutter.style.cursor = 'col-resize'; 28 | }) 29 | return gutter 30 | }, onDragEnd: function (sizes) { 31 | localStorage.setItem('split-sizes', JSON.stringify(sizes)) 32 | }, 33 | }) 34 | 35 | // Split(['.preview-container', '.render-wrap'], { 36 | // sizes: sizeCodes, 37 | // gutterSize: 4, 38 | // direction: 'vertical', 39 | // minSize: [100, 100], 40 | // gutter: (index, direction) => { 41 | // const gutter = document.createElement('div') 42 | // gutter.className = `gutter gutter-${direction}` 43 | // gutter.addEventListener("mousemove", () => { 44 | // gutter.style.cursor = 'row-resize'; 45 | // }) 46 | // return gutter 47 | // }, onDragEnd: function (sizes) { 48 | // localStorage.setItem('split-sizes-code', JSON.stringify(sizes)) 49 | // }, 50 | // }) 51 | } -------------------------------------------------------------------------------- /src/libs/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | export const store = createStore({ 4 | state() { 5 | return { 6 | currentEditComp: null, 7 | renderCount: 0 8 | } 9 | }, 10 | mutations: { 11 | storeCurrentEditComp(state, newComp) { 12 | state.currentEditComp = newComp; 13 | }, 14 | onDragEnd(state) { 15 | state.renderCount++; 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/libs/stringify-object.js: -------------------------------------------------------------------------------- 1 | import isRegexp from 'is-regexp'; 2 | import isObject from 'is-obj'; 3 | 4 | const getOwnEnumPropSymbols = (object) => Object 5 | .getOwnPropertySymbols(object) 6 | .filter((keySymbol) => Object.prototype.propertyIsEnumerable.call(object, keySymbol)); 7 | 8 | export default function stringifyObject(input, options, pad) { 9 | const seen = []; 10 | 11 | return (function stringify(input, options = {}, pad = '') { 12 | const indent = options.indent || '\t'; 13 | 14 | let tokens; 15 | if (options.inlineCharacterLimit === undefined) { 16 | tokens = { 17 | newline: '\n', 18 | newlineOrSpace: '\n', 19 | pad, 20 | indent: pad + indent, 21 | }; 22 | } else { 23 | tokens = { 24 | newline: '@@__STRINGIFY_OBJECT_NEW_LINE__@@', 25 | newlineOrSpace: '@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@', 26 | pad: '@@__STRINGIFY_OBJECT_PAD__@@', 27 | indent: '@@__STRINGIFY_OBJECT_INDENT__@@', 28 | }; 29 | } 30 | 31 | const expandWhiteSpace = string => { 32 | if (options.inlineCharacterLimit === undefined) { 33 | return string; 34 | } 35 | 36 | const oneLined = string 37 | .replace(new RegExp(tokens.newline, 'g'), '') 38 | .replace(new RegExp(tokens.newlineOrSpace, 'g'), ' ') 39 | .replace(new RegExp(tokens.pad + '|' + tokens.indent, 'g'), ''); 40 | 41 | if (oneLined.length <= options.inlineCharacterLimit) { 42 | return oneLined; 43 | } 44 | 45 | return string 46 | .replace(new RegExp(tokens.newline + '|' + tokens.newlineOrSpace, 'g'), '\n') 47 | .replace(new RegExp(tokens.pad, 'g'), pad) 48 | .replace(new RegExp(tokens.indent, 'g'), pad + indent); 49 | }; 50 | 51 | if (seen.includes(input)) { 52 | return '"[Circular]"'; 53 | } 54 | 55 | if ( 56 | input === null 57 | || input === undefined 58 | || typeof input === 'number' 59 | || typeof input === 'boolean' 60 | || typeof input === 'function' 61 | || typeof input === 'symbol' 62 | || isRegexp(input) 63 | ) { 64 | return String(input); 65 | } 66 | 67 | if (input instanceof Date) { 68 | return `new Date('${input.toISOString()}')`; 69 | } 70 | 71 | if (Array.isArray(input)) { 72 | if (input.length === 0) { 73 | return '[]'; 74 | } 75 | 76 | seen.push(input); 77 | 78 | const returnValue = '[' + tokens.newline + input.map((element, i) => { 79 | const eol = input.length - 1 === i ? tokens.newline : ',' + tokens.newlineOrSpace; 80 | 81 | let value = stringify(element, options, pad + indent); 82 | if (options.transform) { 83 | value = options.transform(input, i, value); 84 | } 85 | 86 | return tokens.indent + value + eol; 87 | }).join('') + tokens.pad + ']'; 88 | 89 | seen.pop(); 90 | 91 | return expandWhiteSpace(returnValue); 92 | } 93 | 94 | if (isObject(input)) { 95 | let objectKeys = [ 96 | ...Object.keys(input), 97 | ...getOwnEnumPropSymbols(input), 98 | ]; 99 | 100 | if (options.filter) { 101 | objectKeys = objectKeys.filter(element => options.filter(input, element)); 102 | } 103 | 104 | if (objectKeys.length === 0) { 105 | return '{}'; 106 | } 107 | 108 | seen.push(input); 109 | 110 | const returnValue = '{' + tokens.newline + objectKeys.map((element, i) => { 111 | const eol = objectKeys.length - 1 === i ? tokens.newline : ',' + tokens.newlineOrSpace; 112 | const isSymbol = typeof element === 'symbol'; 113 | const isClassic = !isSymbol && /^[a-z$_][$\w]*$/i.test(element); 114 | const key = isSymbol || isClassic ? element : stringify(element, options); 115 | 116 | let value = stringify(input[element], options, pad + indent); 117 | if (options.transform) { 118 | value = options.transform(input, element, value); 119 | } 120 | 121 | return tokens.indent + String(key) + ': ' + value + eol; 122 | }).join('') + tokens.pad + '}'; 123 | 124 | seen.pop(); 125 | 126 | return expandWhiteSpace(returnValue); 127 | } 128 | 129 | input = String(input).replace(/[\r\n]/g, x => x === '\n' ? '\\n' : '\\r'); 130 | 131 | if (options.singleQuotes === false) { 132 | input = input.replace(/"/g, '\\"'); 133 | return `"${input}"`; 134 | } 135 | 136 | input = input.replace(/\\?'/g, '\\\''); 137 | return `'${input}'`; 138 | })(input, options, pad); 139 | } 140 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import ElementPlus from "element-plus"; 3 | import { 4 | QuestionFilled, 5 | CirclePlus, 6 | DocumentCopy, 7 | Delete, 8 | Refresh, 9 | Minus, 10 | } from "@element-plus/icons-vue"; 11 | import "element-plus/dist/index.css"; 12 | 13 | import APP from "./App.vue"; 14 | import loadCompontents from "@/libs/UIComponentInit.js"; 15 | 16 | import Chart from './rawComponents/echart/init'; 17 | 18 | /** 19 | * 创建实例基础方法 20 | * @param {*} renderComponent 21 | * @param {*} loadFinished 22 | * @returns 23 | */ 24 | function loadTemplate(renderComponent, loadFinished = () => { }) { 25 | const app = createApp(renderComponent); 26 | app.use(ElementPlus); 27 | app.component('VChart', Chart); 28 | loadCompontents().then((modules) => { 29 | for (let index = 0; index < modules.length; index++) { 30 | const module = modules[index]; 31 | app.use(module); 32 | loadFinished(app); 33 | } 34 | }); 35 | return app; 36 | } 37 | 38 | /** 39 | * 同步创建实例 40 | * @param {*} renderComponent 41 | * @returns 42 | */ 43 | function createBaseAppSync(renderComponent = {}) { 44 | return loadTemplate(renderComponent); 45 | } 46 | 47 | /** 48 | * 异步创建实例 49 | * @param {*} renderComponent 50 | * @returns 51 | */ 52 | function createBaseAppAsync(renderComponent = {}) { 53 | return new Promise((resolve, reject) => { 54 | loadTemplate(renderComponent, (app) => { 55 | resolve(app); 56 | }); 57 | }); 58 | } 59 | 60 | const app = createBaseAppSync(APP); 61 | 62 | app.component("question-filled", QuestionFilled); 63 | app.component("circle-plus", CirclePlus); 64 | app.component("l-refresh", Refresh); 65 | app.component("l-delete", Delete); 66 | app.component("document-copy", DocumentCopy); 67 | app.component("l-minus", Minus); 68 | 69 | app.mount("#app"); 70 | 71 | // 内部需要同样配置的全局Vue 72 | self.createBaseAppAsync = createBaseAppAsync; -------------------------------------------------------------------------------- /src/map/data.index.js: -------------------------------------------------------------------------------- 1 | export default {"echartPieOption":{"title":{"text":"Traffic Sources","left":"center"},"legend":{"orient":"vertical","left":"left","data":["Direct","Email","Ad Networks","Video Ads","Search Engines"]},"series":[{"name":"Traffic Sources","type":"pie","radius":"55%","center":["50%","60%"],"data":[{"value":335,"name":"Direct"},{"value":310,"name":"Email"},{"value":234,"name":"Ad Networks"},{"value":135,"name":"Video Ads"},{"value":1548,"name":"Search Engines"}],"emphasis":{"itemStyle":{"shadowBlur":10,"shadowOffsetX":0,"shadowColor":"rgba(0, 0, 0, 0.5)"}}}]},"echartBarOption":{"xAxis":{"type":"category","data":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]},"yAxis":{"type":"value"},"series":[{"data":[120,200,150,80,70,110,130],"type":"bar"}]},"circleUrl":"https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png","squareUrl":"https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png","dialogVisible":false,"drawer":false,"direction":"rtl","count":0,"transferData":[{"key":0,"label":"备选项 0"},{"key":1,"label":"备选项 1"},{"key":2,"label":"备选项 2"},{"key":3,"label":"备选项 3"},{"key":4,"label":"备选项 4"}],"transferValue":[1,4],"textarea2":"","value1":["2016-10-10T00:40:00.000Z","2016-10-10T01:40:00.000Z"],"dateValue1":"","num":1,"imageUrl":"","color1":"#409EFF","checkList":["选中且禁用","复选框 A"],"radio":3,"input":"","fileList":[{"name":"food.jpeg","url":"https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100"},{"name":"food2.jpeg","url":"https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100"}],"options":[{"value":"选项1","label":"黄金糕"},{"value":"选项2","label":"双皮奶"},{"value":"选项3","label":"蚵仔煎"},{"value":"选项4","label":"龙须面"},{"value":"选项5","label":"北京烤鸭"}],"value":3,"value2":[],"options2":[{"value":"zhinan","label":"指南","children":[{"value":"shejiyuanze","label":"设计原则","children":[{"value":"yizhi","label":"一致"},{"value":"fankui","label":"反馈"},{"value":"xiaolv","label":"效率"},{"value":"kekong","label":"可控"}]},{"value":"daohang","label":"导航","children":[{"value":"cexiangdaohang","label":"侧向导航"},{"value":"dingbudaohang","label":"顶部导航"}]}]},{"value":"zujian","label":"组件","children":[{"value":"basic","label":"Basic","children":[{"value":"layout","label":"Layout 布局"},{"value":"color","label":"Color 色彩"},{"value":"typography","label":"Typography 字体"},{"value":"icon","label":"Icon 图标"},{"value":"button","label":"Button 按钮"}]},{"value":"form","label":"Form","children":[{"value":"radio","label":"Radio 单选框"},{"value":"checkbox","label":"Checkbox 多选框"},{"value":"input","label":"Input 输入框"},{"value":"input-number","label":"InputNumber 计数器"},{"value":"select","label":"Select 选择器"},{"value":"cascader","label":"Cascader 级联选择器"},{"value":"switch","label":"Switch 开关"},{"value":"slider","label":"Slider 滑块"},{"value":"time-picker","label":"TimePicker 时间选择器"},{"value":"date-picker","label":"DatePicker 日期选择器"},{"value":"datetime-picker","label":"DateTimePicker 日期时间选择器"},{"value":"upload","label":"Upload 上传"},{"value":"rate","label":"Rate 评分"},{"value":"form","label":"Form 表单"}]},{"value":"data","label":"Data","children":[{"value":"table","label":"Table 表格"},{"value":"tag","label":"Tag 标签"},{"value":"progress","label":"Progress 进度条"},{"value":"tree","label":"Tree 树形控件"},{"value":"pagination","label":"Pagination 分页"},{"value":"badge","label":"Badge 标记"}]},{"value":"notice","label":"Notice","children":[{"value":"alert","label":"Alert 警告"},{"value":"loading","label":"Loading 加载"},{"value":"message","label":"Message 消息提示"},{"value":"message-box","label":"MessageBox 弹框"},{"value":"notification","label":"Notification 通知"}]},{"value":"navigation","label":"Navigation","children":[{"value":"menu","label":"NavMenu 导航菜单"},{"value":"tabs","label":"Tabs 标签页"},{"value":"breadcrumb","label":"Breadcrumb 面包屑"},{"value":"dropdown","label":"Dropdown 下拉菜单"},{"value":"steps","label":"Steps 步骤条"}]},{"value":"others","label":"Others","children":[{"value":"dialog","label":"Dialog 对话框"},{"value":"tooltip","label":"Tooltip 文字提示"},{"value":"popover","label":"Popover 弹出框"},{"value":"card","label":"Card 卡片"},{"value":"carousel","label":"Carousel 走马灯"},{"value":"collapse","label":"Collapse 折叠面板"}]}]},{"value":"ziyuan","label":"资源","children":[{"value":"axure","label":"Axure Components"},{"value":"sketch","label":"Sketch Templates"},{"value":"jiaohu","label":"组件交互文档"}]}],"value3":true,"value4":28,"value5":"","value6":"","value7":1,"radio1":3,"ruleForm":{"name":"","region":"","date1":"","date2":"","delivery":false,"type":[],"resource":"","desc":""},"rules":{"name":[{"required":true,"message":"请输入活动名称","trigger":"blur"},{"min":3,"max":5,"message":"长度在 3 到 5 个字符","trigger":"blur"}],"region":[{"required":true,"message":"请选择活动区域","trigger":"change"}],"date1":[{"type":"date","required":true,"message":"请选择日期","trigger":"change"}],"date2":[{"type":"date","required":true,"message":"请选择时间","trigger":"change"}],"type":[{"type":"array","required":true,"message":"请至少选择一个活动性质","trigger":"change"}],"resource":[{"required":true,"message":"请选择活动资源","trigger":"change"}],"desc":[{"required":true,"message":"请填写活动形式","trigger":"blur"}]},"formInline":{"user":"","region":""},"url":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","calendarValue":"2023-12-06T10:59:26.558Z","reverse":true,"activities":[{"content":"活动按期开始","timestamp":"2018-04-15"},{"content":"通过审核","timestamp":"2018-04-13"},{"content":"创建成功","timestamp":"2018-04-11"}],"collapseActiveNames":["1"],"active":1,"activeName":"second","activeIndex2":"1","data":[{"label":"一级 1","children":[{"label":"二级 1-1","children":[{"label":"三级 1-1-1"}]}]},{"label":"一级 2","children":[{"label":"二级 2-1","children":[{"label":"三级 2-1-1"}]},{"label":"二级 2-2","children":[{"label":"三级 2-2-1"}]}]},{"label":"一级 3","children":[{"label":"二级 3-1","children":[{"label":"三级 3-1-1"}]},{"label":"二级 3-2","children":[{"label":"三级 3-2-1"}]}]}],"defaultProps":{"children":"children","label":"label"},"url4":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","currentPage":1,"tableData":[{"date":"2016-05-02","name":"王小虎","address":"上海市普陀区金沙江路 1518 弄"},{"date":"2016-05-04","name":"王小虎","address":"上海市普陀区金沙江路 1517 弄"},{"date":"2016-05-01","name":"王小虎","address":"上海市普陀区金沙江路 1519 弄"},{"date":"2016-05-03","name":"王小虎","address":"上海市普陀区金沙江路 1516 弄"}],"propList":[{"label":"姓名","prop":"name"},{"label":"日期","prop":"date"},{"label":"地址","prop":"address"},{"label":"动态组件","prop":"lc-component","component":"el-switch"}],"time1":1701860186580,"time2":1701601166580,"currentRate":0,"gradientColor":{"0%":"#3fecff","100%":"#6149f6"},"activeNames":["1"],"time":108000000,"list":[1,2,3,4,5,6,7,8,9,10,11,12,13,14],"loading":false,"finished":false,"stepValue":1,"username":"","password":"","slideValue":30,"slideArrValue":[10,60],"search":"","show":false,"columns":["杭州","宁波","温州","绍兴","湖州","嘉兴","金华","衢州"],"checked":true,"date":"","checked9":true,"minDate":"2019-12-31T16:00:00.000Z","maxDate":"2025-10-31T16:00:00.000Z","currentDate":"2023-12-06T10:59:26.615Z","tel":"","text":"","digit":"","number":"","result":[],"radio10":"1","checked12":false,"currentPage13":1,"activeKey":0,"active14":2,"areaList":{},"searchResult":[],"chosenAddressId":"1","list15":[{"lc_id":"1","name":"张三","tel":"13000000000","address":"浙江省杭州市西湖区文三路 138 号东方通信大厦 7 楼 501 室","isDefault":true},{"lc_id":"2","name":"李四","tel":"1310000000","address":"浙江省杭州市拱墅区莫干山路 50 号"}],"disabledList":[{"lc_id":"3","name":"王五","tel":"1320000000","address":"浙江省杭州市滨江区江南大道 15 号"}]} -------------------------------------------------------------------------------- /src/map/load.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 加载外部代码资源 3 | */ 4 | import styleData from "../map/style.index.js"; 5 | import methodData from "../map/method.index.js"; 6 | import dataData from "../map/data.index.js"; 7 | import templateData from "../map/template.index.js"; 8 | 9 | window.templateSourceMap = templateData; 10 | window.dataSourceMap = dataData; 11 | window.methodSourceMap = methodData; 12 | window.styleSourceMap = styleData; 13 | -------------------------------------------------------------------------------- /src/map/method.index.js: -------------------------------------------------------------------------------- 1 | export default {"handleClose":"function (done) {\n this.$confirm('确认关闭\\uFF1F').then(_ => {\n done();\n }).catch(_ => {\n });\n}","handleDrawerClose":"function (done) {\n this.$confirm('确认关闭\\uFF1F').then(_ => {\n done();\n }).catch(_ => {\n });\n}","load":"function () {\n this.count += 2;\n}","handleChange":"function (value) {\n}","handleRemove":"function (file, fileList) {\n console.log(file, fileList);\n}","handlePreview":"function (file) {\n console.log(file);\n}","handleExceed":"function (files, fileList) {\n this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${ files.length } 个文件,共选择了 ${ files.length + fileList.length } 个文件`);\n}","beforeRemove":"function (file, fileList) {\n return this.$confirm(`确定移除 ${ file.name }?`);\n}","handleAvatarSuccess":"function (res, file) {\n this.imageUrl = URL.createObjectURL(file.raw);\n}","beforeAvatarUpload":"function (file) {\n}","next":"function () {\n}","goBack":"function () {\n console.log('go back');\n}","onSubmit":"function () {\n console.log('submit!');\n}","onReset":"function () {\n console.log('submit!');\n}","submitForm":"function () {\n console.log('submit!');\n}","resetForm":"function () {\n console.log('submit!');\n}","format":"function (percentage) {\n return percentage === 100 ? '满' : `${ percentage }%`;\n}","handleNodeClick":"function () {\n}","handleSelect":"function () {\n}","handleClick":"function () {\n}","goBack2":"function () {\n}","handleChange3":"function (val) {\n console.log(val);\n}","handleSizeChange":"function () {\n}","handleCurrentChange":"function () {\n}","onLoad":"function () {\n}","onSubmit6":"function (values) {\n console.log('submit', values);\n}","onConfirm":"function (value, index) {\n}","onChange":"function (picker, value, index) {\n}","onCancel":"function () {\n}","afterRead":"function (file) {\n console.log(file);\n}","formatDate":"function (date) {\n return `${ date.getMonth() + 1 }/${ date.getDate() }`;\n}","onConfirm8":"function (date) {\n this.show = false;\n this.date = this.formatDate(date);\n}","checkAll":"function () {\n this.$refs.checkboxGroup.toggleAll(true);\n}","toggleAll":"function () {\n this.$refs.checkboxGroup.toggleAll();\n}","onClickIcon":"function () {\n Toast('点击图标');\n}","onClickButton":"function () {\n Toast('点击按钮');\n}","onAdd":"function () {\n Toast('新增地址');\n}","onEdit":"function (item, index) {\n Toast('编辑地址:' + index);\n}","onClickLeft":"function () {\n Toast('返回');\n}","onClickRight":"function () {\n Toast('按钮');\n}","onSave":"function () {\n Toast('save');\n}","onDelete":"function () {\n Toast('delete');\n}","onChangeDetail":"function (val) {\n if (val) {\n this.searchResult = [{\n name: '黄龙万科中心',\n address: '杭州市西湖区'\n }];\n } else {\n this.searchResult = [];\n }\n}","onSubmit11":"function () {\n}","onClickEditAddress":"function () {\n}"} -------------------------------------------------------------------------------- /src/map/style.index.js: -------------------------------------------------------------------------------- 1 | export default {".container":"",".echart-class":"padding: 20px; display: flex; flex-direction: column; gap: 10px;",".chart":"height: 300px; width: 100% !important; border: 1px dashed #c6c6c6; border-radius: 5px;",".row":"margin-bottom: 10px;",".icon":"margin-right: 10px; margin-left: 10px; font-size: 18px;",".el-header":"background-color: #b3c0d1; color: #333; text-align: center; line-height: 60px;",".el-footer":"background-color: #b3c0d1; color: #333; text-align: center; line-height: 60px;",".el-aside":"background-color: #d3dce6; color: #333; text-align: center; line-height: 200px;",".el-main":"background-color: #e9eef3; color: #333; text-align: center; line-height: 160px;","body > .el-container":"margin-bottom: 40px;",".el-container:nth-child(5) .el-aside":"line-height: 260px;",".el-container:nth-child(6) .el-aside":"line-height: 260px;",".el-container:nth-child(7) .el-aside":"line-height: 320px;",".avatar-uploader :v-deep(.el-upload)":"border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden;",".avatar-uploader .el-upload:hover":"border-color: #409eff;",".avatar-uploader-icon":"font-size: 28px; color: #8c939d; width: 178px; height: 178px; line-height: 178px; text-align: center;",".avatar":"width: 178px; height: 178px; display: block;",".demo-border":"border: 1px grey dashed; min-height: 1rem; border-radius: 5px;","[label-lc-mark]":"display: inline-block; width: 200px; border: 1px grey dashed; min-height: 1rem; border-radius: 5px;",".item":"margin-top: 10px; margin-right: 40px;",".el-carousel__item h3":"color: #475669; font-size: 14px; opacity: 0.75; line-height: 150px; margin: 0;",".el-carousel__item:nth-child(2n)":"background-color: #99a9bf;",".el-carousel__item:nth-child(2n + 1)":"background-color: #d3dce6;",".el-row":"margin-bottom: 20px;",".el-row:last-child":"margin-bottom: 0;",".el-col":"border-radius: 4px;",".bg-purple-dark":"background: #99a9bf;",".bg-purple":"background: #d3dce6 !important;",".bg-purple-light":"background: #e5e9f2;",".grid-content":"border-radius: 4px; min-height: 36px;",".row-bg":"padding: 10px 0; background-color: #f9fafc;",".icon1":"margin-left: 10px; margin-right: 10px;",".layout":"border: 1px solid #d7dde4; background: #f5f7f9; position: relative; border-radius: 4px; overflow: hidden;",".layout-logo":"width: 100px; height: 30px; background: #5b6270; border-radius: 3px; float: left; position: relative; top: 15px; left: 20px;",".layout-nav":"width: 420px; margin: 0 auto; margin-right: 20px;",".demonstration":"font-size: 12px; color: #1f2f3d; padding: 10px 0 0 0;",".title":"width: 120px;",".demonstration-raw":"padding: 10px 0;","[div-lc-mark]":"border: 1px grey dashed; min-height: 1rem; border-radius: 5px;","button + button":"margin-top: 10px;","#vant-button > *":"margin: 0 5px 5px 0;",".colon":"display: inline-block; margin: 0 4px; color: #ee0a24;",".block":"display: inline-block; width: 22px; color: #fff; font-size: 12px; text-align: center; background-color: #ee0a24;",".my-swipe .van-swipe-item":"color: #fff; font-size: 20px; line-height: 150px; text-align: center; background-color: #39a9ed;",".icon-c":"margin-left: 10px; margin-right: 10px;",":v-deep(.van-tabbar--fixed)":"position: initial;",":v-deep(.van-address-list__bottom)":"position: relative;",":v-deep(.van-submit-bar)":"position: initial;"} -------------------------------------------------------------------------------- /src/rawComponents/antd/button.vue: -------------------------------------------------------------------------------- 1 | 27 | 49 | 51 | -------------------------------------------------------------------------------- /src/rawComponents/antd/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 41 | 42 | -------------------------------------------------------------------------------- /src/rawComponents/echart/chart.vue: -------------------------------------------------------------------------------- 1 | 26 | 84 | 99 | -------------------------------------------------------------------------------- /src/rawComponents/echart/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /src/rawComponents/echart/init.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/rawComponents/element/button.vue: -------------------------------------------------------------------------------- 1 | 66 | 76 | 85 | -------------------------------------------------------------------------------- /src/rawComponents/element/container.vue: -------------------------------------------------------------------------------- 1 | 94 | 101 | 135 | -------------------------------------------------------------------------------- /src/rawComponents/element/dialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 35 | 36 | -------------------------------------------------------------------------------- /src/rawComponents/element/final.vue: -------------------------------------------------------------------------------- 1 | 54 | 93 | 94 | -------------------------------------------------------------------------------- /src/rawComponents/element/image.vue: -------------------------------------------------------------------------------- 1 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /src/rawComponents/element/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 57 | 59 | 89 | -------------------------------------------------------------------------------- /src/rawComponents/element/layout.vue: -------------------------------------------------------------------------------- 1 | 119 | 126 | 154 | -------------------------------------------------------------------------------- /src/rawComponents/element/menu.vue: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /src/rawComponents/element/table.vue: -------------------------------------------------------------------------------- 1 | 30 | 83 | 84 | -------------------------------------------------------------------------------- /src/rawComponents/iview/button.vue: -------------------------------------------------------------------------------- 1 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/rawComponents/iview/icon.vue: -------------------------------------------------------------------------------- 1 | 19 | 26 | 30 | -------------------------------------------------------------------------------- /src/rawComponents/iview/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 28 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /src/rawComponents/iview/layout.vue: -------------------------------------------------------------------------------- 1 | 2 | 66 | 73 | -------------------------------------------------------------------------------- /src/rawComponents/iview/other.vue: -------------------------------------------------------------------------------- 1 | 43 | 50 | 51 | -------------------------------------------------------------------------------- /src/rawComponents/iview/template.vue: -------------------------------------------------------------------------------- 1 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /src/rawComponents/quasar/base.vue: -------------------------------------------------------------------------------- 1 | 17 | 59 | 60 | -------------------------------------------------------------------------------- /src/rawComponents/quasar/button.vue: -------------------------------------------------------------------------------- 1 | 47 | 74 | -------------------------------------------------------------------------------- /src/rawComponents/quasar/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 38 | 43 | -------------------------------------------------------------------------------- /src/rawComponents/raw/index.vue: -------------------------------------------------------------------------------- 1 | 70 | 87 | 100 | -------------------------------------------------------------------------------- /src/rawComponents/vant/button.vue: -------------------------------------------------------------------------------- 1 | 41 | 48 | 55 | -------------------------------------------------------------------------------- /src/rawComponents/vant/display.vue: -------------------------------------------------------------------------------- 1 | 98 | 119 | 139 | -------------------------------------------------------------------------------- /src/rawComponents/vant/form.vue: -------------------------------------------------------------------------------- 1 | 49 | 78 | 79 | -------------------------------------------------------------------------------- /src/rawComponents/vant/icon.vue: -------------------------------------------------------------------------------- 1 | 21 | 28 | 32 | -------------------------------------------------------------------------------- /src/rawComponents/vant/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 43 | 46 | 47 | 77 | -------------------------------------------------------------------------------- /src/rawComponents/vant/layout.vue: -------------------------------------------------------------------------------- 1 | 61 | 96 | 97 | -------------------------------------------------------------------------------- /src/rawComponents/vant/nav.vue: -------------------------------------------------------------------------------- 1 | 62 | 140 | 149 | 158 | -------------------------------------------------------------------------------- /src/rawComponents/vant/template.vue: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /src/script/compile.js: -------------------------------------------------------------------------------- 1 | const { compiler } = require("./compileComponent.js"); 2 | 3 | const glob = require("glob"); 4 | const path = require("path"); 5 | const process = require("process"); 6 | 7 | const componentsPath = path.join(process.cwd(), "src/rawComponents"); 8 | 9 | console.info(`当前正在读取${componentsPath}中的vue原始组件`); 10 | 11 | // options is optional 12 | glob( 13 | "**/*.vue", 14 | { 15 | cwd: componentsPath, 16 | absolute: true, 17 | ignore: ["**/element/index.vue", "**/vant/index.vue", "**/iview/index.vue", "**/antd/index.vue", "**/echart/index.vue", "**/echart/chart.vue"], 18 | }, 19 | function (er, files) { 20 | console.info(`正在对${files.length}个文件进行编译...`); 21 | files.forEach((filePath) => { 22 | console.info(`正在编译${filePath}`); 23 | compiler(filePath); 24 | }); 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /src/script/distClear.js: -------------------------------------------------------------------------------- 1 | const glob = require("glob"); 2 | const file = require("fs"); 3 | 4 | glob( 5 | "**/*.js", 6 | { 7 | cwd: "./dist/", 8 | absolute: true, 9 | ignore: ["**/*.min.*"], 10 | }, 11 | function (er, files) { 12 | console.info(`正在清理多余文件...`); 13 | files.forEach((filePath) => { 14 | file.rm(filePath, function (er) {}); 15 | }); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /src/test/compileVueOnline.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 58 | 59 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import isEqual from "lodash-es/isEqual"; 2 | import { customAlphabet, nanoid } from 'nanoid'; 3 | 4 | export function getRawComponentKey(__rawVueInfo__) { 5 | return Object.keys(__rawVueInfo__)[0]; 6 | } 7 | 8 | export function getRawComponentContent(__rawVueInfo__) { 9 | return __rawVueInfo__[getRawComponentKey(__rawVueInfo__)]; 10 | } 11 | 12 | /** 13 | * 获得一个数据节点的父节点 14 | * @param {*} __rawVueInfo__ 15 | * @returns 16 | */ 17 | export function findParentNode(__rawVueInfo__) { 18 | const targetDom = findTargetDom(__rawVueInfo__); 19 | if (targetDom) { 20 | const parentDom = findParentDom(targetDom.parentNode); 21 | return findVueInfo(parentDom); 22 | } else { 23 | throw new Error(`没有找到目标DOM节点,数据节点信息:`, __rawVueInfo__); 24 | } 25 | } 26 | 27 | /** 28 | * 将一个节点从其节点中移除 29 | * @param {*} __rawVueInfo__ 30 | */ 31 | export function deleteNodeFromParent(__rawVueInfo__) { 32 | const parentNode = findParentNode(__rawVueInfo__); 33 | const children = getRawComponentContent(parentNode).__children; 34 | const index = children.findIndex(item => isEquals(item, __rawVueInfo__)); 35 | children.splice(index, 1); 36 | } 37 | 38 | /** 39 | * 获得一个节点对应的数据信息,这个函数不负责向上递归查找 40 | * @param {*} element 41 | * @returns 42 | */ 43 | export function findVueInfo(element) { 44 | if (element) { 45 | const lcid = element.attributes.lc_id.nodeValue; 46 | // 获取源代码结构 47 | let rowCode = window.templateSourceMap[lcid]; 48 | 49 | if (!rowCode) { 50 | // 如果不在templateSourceMap,则可能当前的操作是在渲染面板上,这部分数据存放在tree中 51 | rowCode = window.tree[lcid]; 52 | } 53 | return rowCode; 54 | } else { 55 | return null; 56 | } 57 | } 58 | 59 | /** 60 | * 是组件库的组件 61 | * @param {*} __rawVueInfo__ 62 | * @returns 63 | */ 64 | export function isRawComponents(__rawVueInfo__) { 65 | const lcid = getVueInfoLcid(__rawVueInfo__); 66 | return !!window.templateSourceMap[lcid]; 67 | } 68 | 69 | /** 70 | * 是已经被拖入面板的组件 71 | * @param {*} __rawVueInfo__ 72 | * @returns 73 | */ 74 | export function isActiveComponents(__rawVueInfo__) { 75 | const lcid = getVueInfoLcid(__rawVueInfo__); 76 | return !!window.tree[lcid]; 77 | } 78 | 79 | /** 80 | * 校验两个数据节点是否相等。由于vue代理的存在,用简单===相比的方式已经失效 81 | * @param {*} o1 82 | * @param {*} o2 83 | */ 84 | export function isEquals(o1, o2) { 85 | if (o1 && o2) { 86 | return getVueInfoLcid(o1) == getVueInfoLcid(o2); 87 | } else { 88 | return false; 89 | } 90 | } 91 | 92 | /** 93 | * 获得一个DOM节点的组件父DOM节点 94 | * @param {*} parentNode 要传入parentDom 95 | * @returns 96 | */ 97 | export function findParentDom(parentNode) { 98 | if (parentNode.attributes && parentNode.attributes.lc_id) { 99 | return parentNode; 100 | } else if (parentNode.parentNode) { 101 | return findParentDom(parentNode.parentNode); 102 | } else { 103 | return null; 104 | } 105 | } 106 | 107 | /** 108 | * 获得一个数据节点的lc_id属性值 109 | * @param {*} __rawVueInfo__ 110 | * @returns 111 | */ 112 | export function getVueInfoLcid(__rawVueInfo__) { 113 | const lcid = getRawComponentContent(__rawVueInfo__).lc_id; 114 | return lcid; 115 | } 116 | 117 | /** 118 | * 获得一个数据节点的DOM节点 119 | * @param {*} __rawVueInfo__ 120 | * @returns 121 | */ 122 | export function findTargetDom(__rawVueInfo__) { 123 | const targetDom = document.querySelector(`[lc_id="${getVueInfoLcid(__rawVueInfo__)}"]`); 124 | return targetDom; 125 | } 126 | 127 | /** 128 | * 比较两个对象是否完全相等 129 | */ 130 | export function compareTwoObjectIsEqual(o1, o2) { 131 | return isEqual(o1, o2); 132 | } 133 | 134 | export function isArray(arr) { 135 | return Object.prototype.toString.apply(arr) === "[object Array]"; 136 | } 137 | 138 | export function isObject(obj) { 139 | return Object.prototype.toString.apply(obj) === "[object Object]"; 140 | } 141 | 142 | /** 143 | * @description 生成唯一ID 144 | */ 145 | export function createUniqueId() { 146 | const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); 147 | return nanoid(); 148 | } 149 | 150 | /** 151 | * 遍历对象,添加ID 152 | * @param {*} jsonObj 153 | */ 154 | export function ergodic(jsonObj) { 155 | if (jsonObj) { 156 | for (const key in jsonObj) { 157 | if (jsonObj.hasOwnProperty(key)) { 158 | const element = jsonObj[key]; 159 | 160 | if (isArray(element)) { 161 | element.forEach((item) => { 162 | if (isObject(item)) { 163 | ergodic(item); 164 | delete item.lc_id; 165 | } 166 | }); 167 | } else if (isObject(element)) { 168 | ergodic(element); 169 | } else { 170 | } 171 | } 172 | } 173 | 174 | // 添加ID 175 | if (!jsonObj["lc_id"]) { 176 | jsonObj["lc_id"] = createUniqueId(); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * 从解析后的Vue结构中找到关键的根节点 183 | * 根节点分包是:template/script/style 184 | * 185 | * @param {*} array 186 | * @param {*} propertyName 187 | * @returns 188 | */ 189 | export function findAObject(array, propertyName) { 190 | const module = array.find(function (ele) { 191 | return ele[propertyName]; 192 | }); 193 | return module || null; 194 | } -------------------------------------------------------------------------------- /src/utils/forCode.js: -------------------------------------------------------------------------------- 1 | 2 | import { isObject, isArray, getRawComponentKey, createUniqueId, findParentDom, findVueInfo } from '@/utils/common'; 3 | import presetAttribute from "../libs/presetAttribute"; 4 | 5 | // 将预生成的ID替换,否则当有两个组件挂在同一个树上时,后一个会将前一个的属性覆盖 6 | export function replaceRowID(codeObj, html) { 7 | // 生成一个唯一的ID,使Code和Dom一一对应,与原代码脱离关系 8 | let newHtml = html; 9 | function deep(obj) { 10 | if (isObject(obj)) { 11 | for (const key in obj) { 12 | if (obj.hasOwnProperty(key)) { 13 | const element = obj[key]; 14 | if (key == "lc_id") { 15 | const oldID = obj[key]; 16 | const newID = createUniqueId(); 17 | newHtml = newHtml.replace(oldID, newID); 18 | obj[key] = newID; 19 | } else if (isObject(element)) { 20 | deep(element); 21 | } else if (isArray(element)) { 22 | element.forEach((item) => deep(item)); 23 | } 24 | } 25 | } 26 | } 27 | } 28 | deep(codeObj); 29 | return newHtml; 30 | } 31 | 32 | 33 | /** 在这里维护一棵以ID为KEY的树 */ 34 | export function updateLinkTree(codeObj) { 35 | if (!window.tree) { 36 | window.tree = {}; 37 | } 38 | 39 | flatCodeObj(codeObj); 40 | } 41 | 42 | /** 43 | * 获取这个元素所对应的代码结构 44 | */ 45 | export function findRawVueInfo(element) { 46 | const parentDom = findParentDom(element); 47 | return findVueInfo(parentDom); 48 | } 49 | 50 | export function flatCodeObj(codeObj) { 51 | function deep(object) { 52 | for (const key in object) { 53 | if (object.hasOwnProperty(key)) { 54 | const element = object[key]; 55 | 56 | // 如果对__children的子属性遍历时,它内部的元素需要指向外层的节点作为父节点 57 | if ( 58 | object.hasOwnProperty("__key__") && 59 | object["__key__"] === "__children" && 60 | isObject(element) 61 | ) { 62 | delete object["__key__"]; 63 | } 64 | 65 | if (key === "lc_id" && object.hasOwnProperty("__key__")) { 66 | const outerKey = object["__key__"]; 67 | const newObj = { 68 | [outerKey]: object 69 | }; 70 | 71 | // 这个关系也需要链接 72 | newObj.__proto__ = object.__proto__; 73 | delete object.__key__; 74 | window.tree[element] = newObj; 75 | } else if (key === "__children") { 76 | object.__children.forEach((child) => { 77 | child["__key__"] = key; 78 | deep(child); 79 | }); 80 | } else if (isObject(element)) { 81 | element["__key__"] = key; 82 | deep(element); 83 | } 84 | } 85 | } 86 | } 87 | deep(codeObj); 88 | } 89 | 90 | // 将所有子节点指向其父节点,父节点指向子节点容易,而子节点找到具体的父节点不容易 91 | export function linkRelationShip(unitRootCodeObj) { 92 | function deep(obj) { 93 | if (isObject(obj)) { 94 | for (const key in obj) { 95 | if (obj.hasOwnProperty(key)) { 96 | const element = obj[key]; 97 | if (isObject(element)) { 98 | element.__proto__ = { parentCodeNode: obj }; 99 | deep(element); 100 | } else if (isArray(element) && key === "__children") { 101 | element.forEach((item) => { 102 | item.__proto__ = { parentCodeNode: obj }; 103 | 104 | deep(item); 105 | }); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | deep(unitRootCodeObj); 113 | } 114 | 115 | // 使生成的代码不携带ID属性 116 | export function removeAllID(codeObj) { 117 | function deep(obj) { 118 | if (isObject(obj)) { 119 | for (const key in obj) { 120 | if (obj.hasOwnProperty(key)) { 121 | const element = obj[key]; 122 | if (key == "lc_id" || key == "lc-mark") { 123 | delete obj[key]; 124 | } else if (isObject(element)) { 125 | deep(element); 126 | } else if (isArray(element)) { 127 | element.forEach((item) => deep(item)); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | deep(codeObj); 135 | } 136 | 137 | export function generateRawInfo(target) { 138 | if (target.attributes.lc_id) { 139 | return findVueInfo(target); 140 | } else { 141 | // 这是一个普通的元素 142 | const temp = { 143 | [target.localName]: { 144 | __text__: target.innerText, 145 | class: target.className, 146 | } 147 | }; 148 | return temp; 149 | } 150 | } 151 | 152 | /** 153 | * 这里需要将o2作为o1的子值 这里使用回调方法而不是用Promise的原因为需要严格保证外部的调用时序 154 | */ 155 | export function merge(o1, o2, currentPointPositionAfterIndex = -1, onFinish = () => { }) { 156 | if (o1 && o2) { 157 | if (!o1["__children"]) { 158 | o1["__children"] = []; 159 | } 160 | // 更新结构关系,将插入到指定的位置 161 | if (currentPointPositionAfterIndex > -1) { 162 | o1["__children"].splice(currentPointPositionAfterIndex, 0, o2); 163 | } else { 164 | o1["__children"].push(o2); 165 | } 166 | onFinish(); 167 | 168 | // 这里踩了一个坑,所有的对象的默认属性__proto__都指向同一个对象,会引起意外的问题 169 | o2.__proto__ = { parentCodeNode: o1 }; 170 | } 171 | } 172 | 173 | // 特殊分隔符 174 | export function getSplitTag() { 175 | return "@#$!"; 176 | } 177 | 178 | export function insertPresetAttribute(vueInfo) { 179 | const key = getRawComponentKey(vueInfo); 180 | const value = vueInfo[key]; 181 | const presetAttr = presetAttribute[key]; 182 | if (presetAttr) { 183 | for (const key in presetAttr) { 184 | if (presetAttr.hasOwnProperty(key)) { 185 | // 将原先的属性做新增或者替换操作 186 | const element = presetAttr[key]; 187 | value[key] = element; 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * 寻找实际的可以代表整个复合组件Dom,这是个核心方法,根据某个元素查找实际的以Vue组件为单位的最小元素 195 | */ 196 | export function findCodeElemNode(element) { 197 | return findParentDom(element); 198 | } -------------------------------------------------------------------------------- /src/utils/initRawComponent.js: -------------------------------------------------------------------------------- 1 | import { generateRawInfo, getSplitTag } from './forCode'; 2 | 3 | 4 | // 遍历DOM树,初始化lc-mark标记的元素 5 | export function deepLCEle(rootElement, onCountIncrease = () => { }) { 6 | // 对dragParent下所有含有lc-mark属性的Element实现可拖拽能力 7 | function deep(ele) { 8 | if (ele.attributes["lc-mark"]) { 9 | // 统计标记组件数量 10 | onCountIncrease(); 11 | initElement(ele); 12 | } 13 | 14 | if (ele.children.length > 0) { 15 | const length = ele.children.length; 16 | for (let i = 0; i < length; i++) { 17 | deep(ele.children.item(i)); 18 | } 19 | } 20 | } 21 | 22 | deep(rootElement); 23 | } 24 | 25 | // 对组件初始化,使组件可以拖拽 26 | export function initElement(element) { 27 | element.draggable = true; 28 | // 给每个组件添加拖拽事件 29 | element.addEventListener("dragstart", function (event) { 30 | event.dataTransfer.effectAllowed = "copy"; 31 | const raw = generateRawInfo(element); 32 | const str = `${element.localName}${getSplitTag()}${element.innerText 33 | }${getSplitTag()}${0}${getSplitTag()}${element.style.cssText 34 | }${getSplitTag()}${JSON.stringify(raw)}`; 35 | event.dataTransfer.setData("text/plain", str); 36 | 37 | event.stopPropagation(); 38 | }); 39 | 40 | // 处理组件标记 41 | element.addEventListener("mouseenter", (event) => { 42 | const parentClassList = element.parentElement.classList; 43 | if (parentClassList && parentClassList.contains("mark-element-unit")) { 44 | parentClassList.remove("mark-element-unit"); 45 | element.isRemoveParentStyle = true; 46 | } 47 | 48 | element.classList.add("mark-element-unit"); 49 | event.stopPropagation(); 50 | }); 51 | 52 | element.addEventListener("mouseleave", (event) => { 53 | element.classList.remove("mark-element-unit"); 54 | if (element.isRemoveParentStyle) { 55 | element.parentElement.classList.add("mark-element-unit"); 56 | } 57 | 58 | event.stopPropagation(); 59 | }); 60 | } -------------------------------------------------------------------------------- /src/utils/lineHelper.js: -------------------------------------------------------------------------------- 1 | let currentPostion = null, 2 | currentTarget = null, 3 | preSelectTarget = null; // 记录上一次鼠标所在位置 4 | const TOP = 1, 5 | MIDDLE = 2, 6 | BOTTOM = 3; 7 | 8 | import { findCodeElemNode, findRawVueInfo } from "@/utils/forCode"; 9 | import { getRawComponentContent } from "@/utils/common"; 10 | 11 | export function initContainerForLine(targetElement, _currentPointer = () => { }) { 12 | const crossX = document.querySelector(".x"); 13 | 14 | const currentPointer = (...args) => { 15 | _currentPointer(...args); 16 | }; 17 | 18 | targetElement.addEventListener("dragover", (event) => { 19 | event.preventDefault(); 20 | drawLine(event); 21 | }); 22 | 23 | targetElement.addEventListener("dragleave", (event) => { 24 | if (event.target === targetElement) { 25 | targetElement.classList.remove("in-element"); 26 | crossX.style = "display: none;"; 27 | } else { 28 | clearTargetOutline(); 29 | } 30 | }); 31 | 32 | /** 33 | * 获得一个元素在父元素中的索引 34 | * @param {*} element 35 | * @returns 36 | */ 37 | function findElementIndex(element) { 38 | // 根据代码结构查找 39 | const parentElementNode = findCodeElemNode(element.parentElement); 40 | const lc_id = element.getAttribute("lc_id"); 41 | 42 | if (parentElementNode) { 43 | const parentRawInfo = findRawVueInfo(parentElementNode); 44 | 45 | if (parentRawInfo) { 46 | const attributes = getRawComponentContent(parentRawInfo); 47 | if (attributes) { 48 | const childrenArray = attributes.__children; 49 | 50 | const index = childrenArray.findIndex((item) => { 51 | return getRawComponentContent(item).lc_id == lc_id; 52 | }); 53 | return index; 54 | } 55 | } 56 | } 57 | return -1; 58 | } 59 | 60 | function clearTargetOutline() { 61 | if (preSelectTarget) { 62 | preSelectTarget.classList.remove("in-element"); 63 | } 64 | } 65 | 66 | function drawLine(event) { 67 | const realTarget = event.target; 68 | 69 | // 2021年03月26日15:56:35 新的逻辑是:只有上下定位辅助线,不再计算左右辅助线 70 | const directionObj = judgeTopOrBottom(realTarget, event.clientX, event.clientY); 71 | const position = getElCoordinate(realTarget); 72 | 73 | // 如果鼠标点在目标的上部分则绘制上部分辅助线 74 | if (directionObj.top && targetElement !== realTarget) { 75 | if (currentPostion === TOP && currentTarget === realTarget) { 76 | return; 77 | } 78 | currentPostion = TOP; 79 | currentTarget = realTarget; 80 | 81 | clearTargetOutline(); 82 | 83 | crossX.style = `top:${position.top}px;width:${position.width}px;left:${position.left}px;display:block;`; 84 | 85 | currentPointer(realTarget.parentElement, findElementIndex(realTarget)); 86 | } else if (directionObj.bottom && targetElement !== realTarget) { 87 | // 如果鼠标点在目标的下部分,则绘制下部分辅助线 88 | if (currentPostion === BOTTOM && currentTarget === realTarget) { 89 | return; 90 | } 91 | currentPostion = BOTTOM; 92 | currentTarget = realTarget; 93 | 94 | clearTargetOutline(); 95 | 96 | crossX.style = `top:${position.bottom}px;width:${position.width}px;left:${position.left}px;display:block;`; 97 | 98 | currentPointer(realTarget.parentElement, findElementIndex(realTarget) + 1); 99 | } else { 100 | currentPostion = MIDDLE; 101 | currentTarget = realTarget; 102 | 103 | realTarget.classList.add("in-element"); 104 | preSelectTarget = realTarget; 105 | 106 | crossX.style = `display:none;`; 107 | 108 | currentPointer(realTarget, -1); 109 | } 110 | } 111 | } 112 | 113 | // 获取一个元素在屏幕上的坐标点 114 | function getElCoordinate(e) { 115 | const rect = e.getBoundingClientRect(); 116 | return rect; 117 | } 118 | 119 | // 判断一个点是否在该元素内 120 | function judgeEleIsContentPoint(e, x, y) { 121 | const position = getElCoordinate(e); 122 | 123 | return x >= position.left && x <= position.right && y >= position.top && y <= position.bottom; 124 | } 125 | 126 | /** 127 | * 判断上还是下 128 | * @param {*} e 129 | * @param {*} x 130 | * @param {*} y 131 | * @returns 132 | */ 133 | function judgeTopOrBottom(e, x, y) { 134 | const position = getElCoordinate(e); 135 | 136 | const cutDistance = Math.round((position.bottom - position.top) / 3); 137 | 138 | return { 139 | top: y < position.top + cutDistance, 140 | middle: y >= position.top + cutDistance && y <= position.top + cutDistance * 2, 141 | bottom: y > position.top + cutDistance * 2, 142 | }; 143 | } 144 | 145 | // 判断点在元素的方向 146 | function direction(e, x, y) { 147 | const position = getElCoordinate(e); 148 | 149 | // 基本方向判定 150 | const direction = { 151 | left: x < position.left ? position.left - x : 0, 152 | right: x > position.right ? x - position.right : 0, 153 | top: y < position.top ? position.top - y : 0, 154 | bottom: y > position.bottom ? y - position.bottom : 0, 155 | }; 156 | 157 | // 判定方向更靠近哪个 158 | let count = 0; 159 | let directionStrArray = []; 160 | for (const key in direction) { 161 | const element = direction[key]; 162 | if (element) { 163 | directionStrArray.push(key); 164 | count++; 165 | } 166 | } 167 | if (count === 2) { 168 | // 进一步判定更靠近哪个方向 169 | const num1 = direction[directionStrArray[0]]; 170 | const num2 = direction[directionStrArray[1]]; 171 | 172 | direction[directionStrArray[0]] = num1 > num2; 173 | direction[directionStrArray[1]] = num2 > num1; 174 | } 175 | 176 | direction.position = position; 177 | 178 | return direction; 179 | } 180 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | import path from "path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | resolve: { 12 | alias: [ 13 | { 14 | find: "@", 15 | replacement: path.resolve(__dirname, "src"), 16 | }, 17 | { 18 | find: "vue", 19 | replacement: "vue/dist/vue.esm-bundler.js" 20 | } 21 | ], 22 | extensions: [".vue", ".js"], 23 | }, 24 | server: { 25 | fs: { 26 | // 可以为项目根目录的上一级提供服务 27 | allow: [".."], 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | css: { extract: false }, 5 | chainWebpack: (config) => { 6 | // config.resolve.alias.set("vue$", "vue/dist/vue.esm.js"); 7 | // config.resolve.alias.set('vue', '@vue/compat/dist/vue.esm-browser.prod.js') 8 | }, 9 | 10 | publicPath: process.env.PUBLIC_PATH, 11 | productionSourceMap: false, 12 | 13 | pluginOptions: { 14 | quasar: { 15 | importStrategy: "kebab", 16 | rtlSupport: false, 17 | }, 18 | }, 19 | 20 | transpileDependencies: ["quasar"], 21 | }; 22 | --------------------------------------------------------------------------------