index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. <template>
  2. <div class="container">
  3. <div class="left-board">
  4. <div class="logo-wrapper">
  5. <div class="logo">
  6. <img :src="logo" alt="logo" /> Form Generator
  7. <a
  8. class="github"
  9. href="https://github.com/JakHuang/form-generator"
  10. target="_blank"
  11. >
  12. <img src="https://github.githubassets.com/pinned-octocat.svg" alt />
  13. </a>
  14. </div>
  15. </div>
  16. <el-scrollbar class="left-scrollbar">
  17. <div class="components-list">
  18. <div v-for="(item, listIndex) in leftComponents" :key="listIndex">
  19. <div class="components-title">
  20. <svg-icon icon-class="component" />
  21. {{ item.title }}
  22. </div>
  23. <draggable
  24. class="components-draggable"
  25. :list="item.list"
  26. :group="{ name: 'componentsGroup', pull: 'clone', put: false }"
  27. :clone="cloneComponent"
  28. draggable=".components-item"
  29. :sort="false"
  30. @end="onEnd"
  31. >
  32. <div
  33. v-for="(element, index) in item.list"
  34. :key="index"
  35. class="components-item"
  36. @click="addComponent(element)"
  37. >
  38. <div class="components-body">
  39. <svg-icon :icon-class="element.__config__.tagIcon" />
  40. {{ element.__config__.label }}
  41. </div>
  42. </div>
  43. </draggable>
  44. </div>
  45. </div>
  46. </el-scrollbar>
  47. </div>
  48. <div class="center-board">
  49. <div class="action-bar">
  50. <slot name="action"> </slot>
  51. <!-- <el-button icon="el-icon-video-play" type="text" @click="run">
  52. 运行
  53. </el-button> -->
  54. <el-button icon="el-icon-view" type="text" @click="preview">
  55. 预览
  56. </el-button>
  57. <el-button icon="el-icon-view" type="text" @click="showJson">
  58. 查看json
  59. </el-button>
  60. <!-- <el-button icon="el-icon-download" type="text" @click="download">
  61. 导出vue文件
  62. </el-button>
  63. <el-button class="copy-btn-main" icon="el-icon-document-copy" type="text" @click="copy">
  64. 复制代码
  65. </el-button> -->
  66. <el-button
  67. class="delete-btn"
  68. icon="el-icon-delete"
  69. type="text"
  70. @click="empty"
  71. >
  72. 清空
  73. </el-button>
  74. </div>
  75. <el-scrollbar class="center-scrollbar">
  76. <el-row class="center-board-row" :gutter="formConf.gutter">
  77. <el-form
  78. :size="formConf.size"
  79. :label-position="formConf.labelPosition"
  80. :disabled="formConf.disabled"
  81. :label-width="formConf.labelWidth + 'px'"
  82. >
  83. <draggable
  84. class="drawing-board"
  85. :list="drawingList"
  86. :animation="340"
  87. group="componentsGroup"
  88. >
  89. <draggable-item
  90. v-for="(item, index) in drawingList"
  91. :key="item.renderKey"
  92. :drawing-list="drawingList"
  93. :current-item="item"
  94. :index="index"
  95. :active-id="activeId"
  96. :form-conf="formConf"
  97. @activeItem="activeFormItem"
  98. @copyItem="drawingItemCopy"
  99. @deleteItem="drawingItemDelete"
  100. />
  101. </draggable>
  102. <div v-show="!drawingList.length" class="empty-info">
  103. 从左侧拖入或点选组件进行表单设计
  104. </div>
  105. </el-form>
  106. </el-row>
  107. </el-scrollbar>
  108. </div>
  109. <right-panel
  110. :active-data="activeData"
  111. :form-conf="formConf"
  112. :show-field="!!drawingList.length"
  113. @tag-change="tagChange"
  114. @fetch-data="fetchData"
  115. />
  116. <form-drawer
  117. :visible.sync="drawerVisible"
  118. :form-data="formData"
  119. size="100%"
  120. :generate-conf="generateConf"
  121. />
  122. <!-- <json-drawer
  123. size="60%"
  124. :visible.sync="jsonDrawerVisible"
  125. :json-str="JSON.stringify(formData)"
  126. @refresh="refreshJson"
  127. /> -->
  128. <el-dialog
  129. :visible.sync="jsonDrawerVisible"
  130. title="查看json"
  131. fullscreen
  132. center
  133. >
  134. <vue-ace-editor
  135. v-model="JSON.stringify(formData, null, 2)"
  136. @init="editorInit"
  137. lang="json"
  138. theme="chrome"
  139. width="100%"
  140. height="calc(100vh - 214px)"
  141. :options="{ wrap: true, readOnly: true }"
  142. >
  143. </vue-ace-editor>
  144. <span slot="footer">
  145. <el-button
  146. icon="el-icon-document"
  147. type="primary"
  148. v-clipboard:copy="JSON.stringify(formData)"
  149. v-clipboard:success="onCopy"
  150. >复 制</el-button
  151. >
  152. <el-button icon="el-icon-close" @click="jsonDrawerVisible = false"
  153. >关闭</el-button
  154. >
  155. </span>
  156. </el-dialog>
  157. <code-type-dialog
  158. :visible.sync="dialogVisible"
  159. title="选择生成类型"
  160. :show-file-name="showFileName"
  161. @confirm="generate"
  162. />
  163. <parser-dialog
  164. :visible.sync="previewVisible"
  165. title="预览"
  166. :formConf="formData"
  167. />
  168. <input id="copyNode" type="hidden" />
  169. </div>
  170. </template>
  171. <script>
  172. import draggable from 'vuedraggable';
  173. import { debounce } from 'throttle-debounce';
  174. // import { saveAs } from 'file-saver'
  175. import ClipboardJS from 'clipboard';
  176. import render from '@/components/FormGenerator/components/render/render';
  177. import FormDrawer from './FormDrawer';
  178. import JsonDrawer from './JsonDrawer';
  179. import RightPanel from './RightPanel';
  180. import ParserDialog from './ParserDialog';
  181. import {
  182. inputComponents,
  183. selectComponents,
  184. layoutComponents,
  185. formConf
  186. } from '@/components/FormGenerator/components/generator/config';
  187. import {
  188. exportDefault,
  189. beautifierConf,
  190. isNumberStr,
  191. titleCase,
  192. deepClone,
  193. isObjectObject
  194. } from '@/components/FormGenerator/utils/index';
  195. import {
  196. makeUpHtml,
  197. vueTemplate,
  198. vueScript,
  199. cssStyle
  200. } from '@/components/FormGenerator/components/generator/html';
  201. import { makeUpJs } from '@/components/FormGenerator/components/generator/js';
  202. import { makeUpCss } from '@/components/FormGenerator/components/generator/css';
  203. import drawingDefalut from '@/components/FormGenerator/components/generator/drawingDefalut';
  204. import logo from './assets/logo.png';
  205. import CodeTypeDialog from './CodeTypeDialog';
  206. import DraggableItem from './DraggableItem';
  207. import {
  208. getDrawingList,
  209. saveDrawingList,
  210. getIdGlobal,
  211. saveIdGlobal,
  212. getFormConf
  213. } from '@/components/FormGenerator/utils/db';
  214. import loadBeautifier from '@/components/FormGenerator/utils/loadBeautifier';
  215. import VueAceEditor from 'vue2-ace-editor';
  216. let beautifier;
  217. const emptyActiveData = { style: {}, autosize: {} };
  218. let oldActiveId;
  219. let tempActiveData;
  220. const drawingListInDB = getDrawingList();
  221. const formConfInDB = getFormConf();
  222. const idGlobal = getIdGlobal();
  223. export default {
  224. name: 'FormGenerator',
  225. components: {
  226. draggable,
  227. render,
  228. FormDrawer,
  229. JsonDrawer,
  230. RightPanel,
  231. CodeTypeDialog,
  232. DraggableItem,
  233. ParserDialog,
  234. VueAceEditor
  235. },
  236. props: ['formDataProp'],
  237. data() {
  238. return {
  239. logo,
  240. idGlobal,
  241. formConf,
  242. inputComponents,
  243. selectComponents,
  244. layoutComponents,
  245. labelWidth: 100,
  246. drawingList: drawingDefalut,
  247. drawingData: {},
  248. activeId: drawingDefalut[0].formId,
  249. drawerVisible: false,
  250. formData: {},
  251. dialogVisible: false,
  252. previewVisible: false,
  253. jsonDrawerVisible: false,
  254. generateConf: null,
  255. showFileName: false,
  256. activeData: drawingDefalut[0],
  257. saveDrawingListDebounce: debounce(340, saveDrawingList),
  258. saveIdGlobalDebounce: debounce(340, saveIdGlobal),
  259. leftComponents: [
  260. {
  261. title: '输入型组件',
  262. list: inputComponents
  263. },
  264. {
  265. title: '选择型组件',
  266. list: selectComponents
  267. },
  268. {
  269. title: '布局型组件',
  270. list: layoutComponents
  271. }
  272. ]
  273. };
  274. },
  275. computed: {},
  276. watch: {
  277. // eslint-disable-next-line func-names
  278. 'activeData.__config__.label': function (val, oldVal) {
  279. if (
  280. this.activeData.placeholder === undefined ||
  281. !this.activeData.__config__.tag ||
  282. oldActiveId !== this.activeId
  283. ) {
  284. return;
  285. }
  286. this.activeData.placeholder =
  287. this.activeData.placeholder.replace(oldVal, '') + val;
  288. },
  289. activeId: {
  290. handler(val) {
  291. oldActiveId = val;
  292. },
  293. immediate: true
  294. },
  295. drawingList: {
  296. handler(val) {
  297. this.saveDrawingListDebounce(val);
  298. if (val.length === 0) this.idGlobal = 100;
  299. },
  300. deep: true
  301. },
  302. idGlobal: {
  303. handler(val) {
  304. this.saveIdGlobalDebounce(val);
  305. },
  306. immediate: true
  307. }
  308. },
  309. mounted() {
  310. if (Array.isArray(drawingListInDB) && drawingListInDB.length > 0) {
  311. this.drawingList = drawingListInDB;
  312. } else {
  313. this.drawingList = drawingDefalut;
  314. }
  315. this.activeFormItem(this.drawingList[0]);
  316. if (formConfInDB) {
  317. this.formConf = formConfInDB;
  318. }
  319. loadBeautifier((btf) => {
  320. beautifier = btf;
  321. });
  322. const clipboard = new ClipboardJS('#copyNode', {
  323. text: (trigger) => {
  324. const codeStr = this.generateCode();
  325. this.$notify({
  326. title: '成功',
  327. message: '代码已复制到剪切板,可粘贴。',
  328. type: 'success'
  329. });
  330. return codeStr;
  331. }
  332. });
  333. clipboard.on('error', (e) => {
  334. this.$message.error('代码复制失败');
  335. });
  336. },
  337. methods: {
  338. setObjectValueReduce(obj, strKeys, data) {
  339. const arr = strKeys.split('.');
  340. arr.reduce((pre, item, i) => {
  341. if (arr.length === i + 1) {
  342. pre[item] = data;
  343. } else if (!isObjectObject(pre[item])) {
  344. pre[item] = {};
  345. }
  346. return pre[item];
  347. }, obj);
  348. },
  349. setRespData(component, resp) {
  350. const { dataPath, renderKey, dataConsumer } = component.__config__;
  351. if (!dataPath || !dataConsumer) return;
  352. const respData = dataPath
  353. .split('.')
  354. .reduce((pre, item) => pre[item], resp);
  355. // 将请求回来的数据,赋值到指定属性。
  356. // 以el-tabel为例,根据Element文档,应该将数据赋值给el-tabel的data属性,所以dataConsumer的值应为'data';
  357. // 此时赋值代码可写成 component[dataConsumer] = respData;
  358. // 但为支持更深层级的赋值(如:dataConsumer的值为'options.data'),使用setObjectValueReduce
  359. this.setObjectValueReduce(component, dataConsumer, respData);
  360. const i = this.drawingList.findIndex(
  361. (item) => item.__config__.renderKey === renderKey
  362. );
  363. if (i > -1) this.$set(this.drawingList, i, component);
  364. },
  365. fetchData(component) {
  366. const { dataType, method, url } = component.__config__;
  367. if (dataType === 'dynamic' && method && url) {
  368. this.setLoading(component, true);
  369. this.$axios({
  370. method,
  371. url
  372. }).then((resp) => {
  373. this.setLoading(component, false);
  374. this.setRespData(component, resp.data);
  375. });
  376. }
  377. },
  378. setLoading(component, val) {
  379. const { directives } = component;
  380. if (Array.isArray(directives)) {
  381. const t = directives.find((d) => d.name === 'loading');
  382. if (t) t.value = val;
  383. }
  384. },
  385. activeFormItem(currentItem) {
  386. if (currentItem) {
  387. this.activeData = currentItem;
  388. this.activeId = currentItem.__config__.formId;
  389. }
  390. },
  391. onEnd(obj) {
  392. if (obj.from !== obj.to) {
  393. this.fetchData(tempActiveData);
  394. this.activeData = tempActiveData;
  395. this.activeId = this.idGlobal;
  396. }
  397. },
  398. addComponent(item) {
  399. const clone = this.cloneComponent(item);
  400. this.fetchData(clone);
  401. this.drawingList.push(clone);
  402. this.activeFormItem(clone);
  403. },
  404. cloneComponent(origin) {
  405. const clone = deepClone(origin);
  406. const config = clone.__config__;
  407. config.span = this.formConf.span; // 生成代码时,会根据span做精简判断
  408. this.createIdAndKey(clone);
  409. clone.placeholder !== undefined && (clone.placeholder += config.label);
  410. tempActiveData = clone;
  411. return tempActiveData;
  412. },
  413. createIdAndKey(item) {
  414. const config = item.__config__;
  415. config.formId = ++this.idGlobal;
  416. config.renderKey = `${config.formId}${+new Date()}`; // 改变renderKey后可以实现强制更新组件
  417. if (config.layout === 'colFormItem') {
  418. item.__vModel__ = `field${this.idGlobal}`;
  419. } else if (config.layout === 'rowFormItem') {
  420. config.componentName = `row${this.idGlobal}`;
  421. !Array.isArray(config.children) && (config.children = []);
  422. delete config.label; // rowFormItem无需配置label属性
  423. }
  424. if (Array.isArray(config.children)) {
  425. config.children = config.children.map((childItem) =>
  426. this.createIdAndKey(childItem)
  427. );
  428. }
  429. return item;
  430. },
  431. AssembleFormData() {
  432. this.formData = {
  433. fields: deepClone(this.drawingList),
  434. ...this.formConf
  435. };
  436. },
  437. generate(data) {
  438. const func = this[`exec${titleCase(this.operationType)}`];
  439. this.generateConf = data;
  440. func && func(data);
  441. },
  442. execRun(data) {
  443. this.AssembleFormData();
  444. this.drawerVisible = true;
  445. },
  446. execDownload(data) {
  447. const codeStr = this.generateCode();
  448. const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' });
  449. // saveAs(blob, data.fileName)
  450. },
  451. execCopy(data) {
  452. document.getElementById('copyNode').click();
  453. },
  454. empty() {
  455. this.$confirm('确定要清空所有组件吗?', '提示', {
  456. type: 'warning'
  457. }).then(() => {
  458. this.drawingList = [];
  459. this.idGlobal = 100;
  460. });
  461. },
  462. drawingItemCopy(item, list) {
  463. let clone = deepClone(item);
  464. clone = this.createIdAndKey(clone);
  465. list.push(clone);
  466. this.activeFormItem(clone);
  467. },
  468. drawingItemDelete(index, list) {
  469. list.splice(index, 1);
  470. this.$nextTick(() => {
  471. const len = this.drawingList.length;
  472. if (len) {
  473. this.activeFormItem(this.drawingList[len - 1]);
  474. }
  475. });
  476. },
  477. generateCode() {
  478. const { type } = this.generateConf;
  479. this.AssembleFormData();
  480. const script = vueScript(makeUpJs(this.formData, type));
  481. const html = vueTemplate(makeUpHtml(this.formData, type));
  482. const css = cssStyle(makeUpCss(this.formData));
  483. return beautifier.html(html + script + css, beautifierConf.html);
  484. },
  485. editorInit: function () {
  486. // require('brace/ext/language_tools')
  487. // require('brace/mode/json')
  488. // require('brace/theme/chrome')
  489. },
  490. showJson() {
  491. this.AssembleFormData();
  492. this.jsonDrawerVisible = true;
  493. },
  494. onCopy() {
  495. this.$message.success('内容复制成功');
  496. },
  497. preview() {
  498. this.AssembleFormData();
  499. this.previewVisible = true;
  500. },
  501. download() {
  502. this.dialogVisible = true;
  503. this.showFileName = true;
  504. this.operationType = 'download';
  505. },
  506. run() {
  507. this.dialogVisible = true;
  508. this.showFileName = false;
  509. this.operationType = 'run';
  510. },
  511. copy() {
  512. this.dialogVisible = true;
  513. this.showFileName = false;
  514. this.operationType = 'copy';
  515. },
  516. tagChange(newTag) {
  517. newTag = this.cloneComponent(newTag);
  518. const config = newTag.__config__;
  519. newTag.__vModel__ = this.activeData.__vModel__;
  520. config.formId = this.activeId;
  521. config.span = this.activeData.__config__.span;
  522. this.activeData.__config__.tag = config.tag;
  523. this.activeData.__config__.tagIcon = config.tagIcon;
  524. this.activeData.__config__.document = config.document;
  525. if (
  526. typeof this.activeData.__config__.defaultValue ===
  527. typeof config.defaultValue
  528. ) {
  529. config.defaultValue = this.activeData.__config__.defaultValue;
  530. }
  531. Object.keys(newTag).forEach((key) => {
  532. if (this.activeData[key] !== undefined) {
  533. newTag[key] = this.activeData[key];
  534. }
  535. });
  536. this.activeData = newTag;
  537. this.updateDrawingList(newTag, this.drawingList);
  538. },
  539. updateDrawingList(newTag, list) {
  540. const index = list.findIndex(
  541. (item) => item.__config__.formId === this.activeId
  542. );
  543. if (index > -1) {
  544. list.splice(index, 1, newTag);
  545. } else {
  546. list.forEach((item) => {
  547. if (Array.isArray(item.__config__.children))
  548. this.updateDrawingList(newTag, item.__config__.children);
  549. });
  550. }
  551. },
  552. refreshJson(data) {
  553. this.drawingList = deepClone(data.fields);
  554. delete data.fields;
  555. this.formConf = data;
  556. },
  557. setJSON(json) {
  558. let formDataTemp = deepClone(json);
  559. this.drawingList = formDataTemp.fields;
  560. if (this.drawingList && this.drawingList.length) {
  561. this.activeFormItem(this.drawingList[0]);
  562. }
  563. delete formDataTemp.fields;
  564. this.formConf = formDataTemp;
  565. },
  566. getJSON() {
  567. this.AssembleFormData();
  568. return this.formData;
  569. }
  570. }
  571. };
  572. </script>
  573. <style lang="scss">
  574. @import './styles/home';
  575. </style>