editor.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. <template>
  2. <div class="editor">
  3. <draggable
  4. v-model="comList"
  5. @add="onAdd"
  6. @update="onDragEnd"
  7. @end="onDragComplete"
  8. group="itxst"
  9. animation="300"
  10. :style="draggableStyle"
  11. handle=".drag-handle"
  12. :scroll="true"
  13. :scrollSensitivity="100"
  14. :scrollSpeed="20"
  15. >
  16. <transition-group style="display: block; min-height: 100vh">
  17. <template v-for="(it, index) in comList">
  18. <div
  19. :key="index"
  20. class="layers"
  21. :class="comIndex == index ? 'active-layer' : ''"
  22. >
  23. <div class="drag-handle" draggable>
  24. <img src="@/icons/svg/drag.svg" style="width: 20px;" icon-class="drag" />
  25. </div>
  26. <!-- :options="categoryList":props="props" -->
  27. <div style="padding: 10px 0">
  28. <el-form label-position="left" class="inline-form">
  29. <el-row :gutter="24" style="margin-left: 0; margin-right: 0">
  30. <!-- <el-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
  31. <el-form-item label="模块分类:">
  32. <span @click="enableSelect(it)" style="cursor: pointer">{{
  33. getCategory(it.category_id)
  34. }}</span>
  35. </el-form-item>
  36. </el-col>
  37. <el-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
  38. <el-form-item label="模块名称:">
  39. <span @click="enableName(it)" style="cursor: pointer">{{
  40. it.dcb_name
  41. }}</span>
  42. </el-form-item>
  43. </el-col> -->
  44. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  45. <el-form-item
  46. ><!-- v-if="$store.state.user.roleInfo.id == 1" -->
  47. <div class="btn-save">
  48. <el-tooltip
  49. v-if="type !== 'document'"
  50. class="item"
  51. effect="dark"
  52. content="保存模块"
  53. placement="top"
  54. >
  55. <el-button
  56. @click="onSaveTemplate(it)"
  57. icon="el-icon-document-add"
  58. circle
  59. >
  60. </el-button>
  61. </el-tooltip>
  62. <!-- <el-tooltip
  63. class="item"
  64. effect="dark"
  65. content="另存为"
  66. placement="top"
  67. >
  68. <el-button
  69. circle
  70. size="mini"
  71. @click="onSaveAs(it)"
  72. v-if="it.id > 0"
  73. icon="el-icon-document-copy"
  74. >
  75. </el-button>
  76. </el-tooltip> && type !== 'document'-->
  77. <el-button
  78. circle
  79. v-if="it.isEdit !== 1"
  80. @click="onEdit(index, 1)"
  81. icon="el-icon-edit"
  82. >
  83. <!-- <svg-icon icon-class="edit" /> -->
  84. </el-button>
  85. <el-button
  86. circle
  87. @click="onEdit(index, 2)"
  88. icon="el-icon-view"
  89. v-else
  90. >
  91. </el-button>
  92. <!-- v-if="!templateId && type !== 'document'" -->
  93. <el-tooltip
  94. class="item"
  95. effect="dark"
  96. content="删除"
  97. placement="top"
  98. >
  99. <el-button
  100. circle
  101. @click="onRemove(index)"
  102. icon="el-icon-delete"
  103. >
  104. <!-- <svg-icon icon-class="el-icon-delete"
  105. />--></el-button
  106. >
  107. </el-tooltip>
  108. </div>
  109. </el-form-item>
  110. </el-col>
  111. </el-row>
  112. </el-form>
  113. </div>
  114. <div class="main" @click="onSetActive(index)">
  115. <component
  116. :is="it.type"
  117. :ref="`component_${index}`"
  118. :com="it"
  119. :coms="comList"
  120. :currentIndex="index"
  121. :isAdmin="isAdmin"
  122. :insertCmd="insertCmd"
  123. @onUpdate="onUpdate"
  124. @onUpdateAttr="onUpdateAttr"
  125. @onUpdata="onUpdata"
  126. @onUpdateProdAttr="onUpdateProdAttr"
  127. @updateComContent="handleComContentUpdate"
  128. :isEdit="it.isEdit"
  129. />
  130. </div>
  131. <!-- <div class="label">
  132. {{ it.name }}
  133. <div class="del">
  134. <svg-icon
  135. @click="onEdit(index, 1)"
  136. v-if="it.isEdit !== 1"
  137. icon-class="edit"
  138. style="color: #fff; margin-right: 10px"
  139. />
  140. <svg-icon
  141. @click="onEdit(index, 2)"
  142. v-else
  143. icon-class="view"
  144. style="color: #fff; margin-right: 10px"
  145. />
  146. <svg-icon
  147. @click="onRemove(index)"
  148. icon-class="delete"
  149. style="color: #fff; margin-left: 10px"
  150. />
  151. </div>
  152. </div> -->
  153. </div>
  154. </template>
  155. <template v-if="comList.length <= 0">
  156. <div class="editor-empty" :key="-1">
  157. <el-empty description="请拖放组件至当前页面" />
  158. </div>
  159. </template>
  160. </transition-group>
  161. </draggable>
  162. </div>
  163. </template>
  164. <script>
  165. import draggable from "vuedraggable";
  166. import Table from "./components/Table";
  167. import TextArea from "./components/TextArea";
  168. import Empty from "./components/Empty";
  169. import {
  170. getAllList,
  171. createTemplate,
  172. updateTemplate,
  173. treeCategory,
  174. } from "@/api/template";
  175. export default {
  176. name: "editor",
  177. emits: [
  178. "onSetActiveIndex",
  179. "onDelete",
  180. "onInsert",
  181. "onLoadArticle",
  182. "onRebuild",
  183. "onSetComs",
  184. ],
  185. components: {
  186. Table,
  187. TextArea,
  188. draggable,
  189. Empty,
  190. },
  191. props: {
  192. coms: {
  193. type: Array,
  194. default: null,
  195. },
  196. comIndex: {
  197. type: Number,
  198. default: 0,
  199. },
  200. templateCate: {
  201. type: Array,
  202. default: () => {
  203. return [];
  204. },
  205. },
  206. insertCmd: {
  207. type: Object,
  208. default: null,
  209. },
  210. isAdmin: {
  211. type: Number,
  212. default: 2,
  213. },
  214. },
  215. watch: {
  216. coms: {
  217. handler(val) {
  218. if (val == null) return;
  219. // 处理拖拽数据,确保属性正确转换
  220. this.comList = val.map((item) => {
  221. // 首先创建基础对象
  222. const processedItem = {
  223. ...item,
  224. // 确保 attrs 是数组
  225. dcb_attrs: Array.isArray(item.attrs)
  226. ? item.attrs
  227. : JSON.parse(item.attrs || "[]"),
  228. // 同步最新内容,优先使用当前显示的内容
  229. content: item.content || item.dcb_nr || "",
  230. dcb_nr: item.content || item.dcb_nr || "",
  231. // 确保 type 使用 dcb_type
  232. type: item.dcb_type || item.type,
  233. // 确保 name 使用 dcb_name
  234. name: item.dcb_name || item.name,
  235. };
  236. console.log("processedItem:", processedItem);
  237. // 确保 content 和 dcb_nr 同步
  238. if (processedItem.content !== processedItem.dcb_nr) {
  239. processedItem.dcb_nr = processedItem.content;
  240. }
  241. return processedItem;
  242. });
  243. this.$emit("onSetComs:", this.comList);
  244. },
  245. immediate: true,
  246. deep: true,
  247. },
  248. comIndex: {
  249. handler(val) {
  250. if (val <= 0) return;
  251. this.comList = this.comList.map((it) => {
  252. it.isEdit = 2;
  253. it.selDisabled = true;
  254. it.valDisabled = true;
  255. if (
  256. typeof it.category_id === "string" &&
  257. it.category_id.includes(" ")
  258. ) {
  259. it.category_id = it.category_id.split(" ");
  260. } else if (!Array.isArray(it.category_id)) {
  261. // 如果不是数组,将其转换为包含单个元素的数组
  262. it.category_id = [it.category_id].filter(Boolean);
  263. }
  264. return it;
  265. });
  266. },
  267. immediate: true, //立即执行
  268. deep: true,
  269. },
  270. },
  271. data() {
  272. return {
  273. showEditor: false,
  274. operation: "",
  275. id: 0,
  276. comList: [],
  277. categoryList: [],
  278. props: {
  279. value: "id",
  280. label: "name",
  281. children: "children",
  282. checkStrictly: true,
  283. },
  284. saveAs: false,
  285. draggableStyle: {
  286. width: "100%",
  287. overflowY: "auto",
  288. },
  289. type: "",
  290. };
  291. },
  292. mounted() {
  293. console.log(this.$store.state);
  294. this.initCategoryList();
  295. this.type = this.$route.query.type;
  296. this.templateId = this.$route.query.templateId;
  297. this.$nextTick(() => {
  298. this.updateDirectoryNumbers(); // 初始化目录序号
  299. });
  300. },
  301. methods: {
  302. /* 拖拽 修改目录序号*/
  303. onDragComplete() {
  304. console.log("拖动结束");
  305. this.$nextTick(() => {
  306. this.updateDirectoryNumbers();
  307. this.$emit("onRebuild", this.comList);
  308. });
  309. },
  310. updateDirectoryNumbers() {
  311. let levelCounters = {};
  312. this.comList.forEach((module, moduleIndex) => {
  313. if (module.attrs) {
  314. module.attrs.forEach((attr) => {
  315. if (attr.type === "Directory") {
  316. const newNumber = this.generateNumberedContent(
  317. moduleIndex,
  318. attr.level,
  319. levelCounters
  320. );
  321. // 只更新 number 属性,完全不触及 content
  322. if (!attr.hasOwnProperty("number")) {
  323. this.$set(attr, "number", newNumber);
  324. } else {
  325. attr.number = newNumber;
  326. }
  327. }
  328. });
  329. }
  330. });
  331. },
  332. generateNumberedContent(moduleIndex, level, levelCounters) {
  333. return this.generateLevelPrefix(moduleIndex, level, levelCounters);
  334. },
  335. generateLevelPrefix(moduleIndex, level, levelCounters) {
  336. let prefix = [];
  337. for (let i = 1; i <= level; i++) {
  338. if (i === 1) {
  339. prefix.push(moduleIndex + 1);
  340. } else {
  341. if (!levelCounters[i]) {
  342. levelCounters[i] = 1;
  343. } else {
  344. levelCounters[i]++;
  345. }
  346. prefix.push(levelCounters[i]);
  347. }
  348. }
  349. return prefix.join(".");
  350. },
  351. carefulCopy(items) {
  352. return items.map((item) => {
  353. const newItem = { ...item };
  354. // 深拷贝 attrs 数组
  355. if (Array.isArray(newItem.attrs)) {
  356. newItem.attrs = newItem.attrs.map((attr) => ({ ...attr }));
  357. }
  358. // 深拷贝 content,如果它是一个对象或数组
  359. if (typeof newItem.content === "object" && newItem.content !== null) {
  360. newItem.content = JSON.parse(JSON.stringify(newItem.content));
  361. }
  362. // 处理特殊的 type,如 Table
  363. if (newItem.type === "Table") {
  364. newItem.tableHeader = [...newItem.tableHeader];
  365. newItem.tableData = newItem.tableData.map((row) => ({ ...row }));
  366. }
  367. // 保留其他属性的引用
  368. return newItem;
  369. });
  370. },
  371. handleComContentUpdate(index, updatedCom) {
  372. // 更新本地的 coms 数组
  373. this.$set(this.coms, index, updatedCom);
  374. // 触发父组件的更新
  375. this.$emit("onRebuild", this.coms);
  376. },
  377. showCategoryName(item) {
  378. return (
  379. this.type == "module" &&
  380. /* this.$store.state.user.roleInfo.id == 1 && */
  381. !item.valDisabled
  382. );
  383. },
  384. enableName(item) {
  385. item.valDisabled = false;
  386. this.$forceUpdate();
  387. },
  388. showCategorySelect(item) {
  389. return (
  390. this.type === "module" &&
  391. /* this.$store.state.user.roleInfo.id === 1 && */
  392. !item.selDisabled
  393. );
  394. },
  395. /* 选择分类 */
  396. enableSelect(item) {
  397. item.selDisabled = true;
  398. console.log(item);
  399. this.$forceUpdate();
  400. },
  401. /* 获取分类名称 */
  402. getCategory(item) {
  403. const findCategory = (list, id) => {
  404. for (const el of list) {
  405. if (el.id === id) {
  406. return el.name;
  407. }
  408. if (el.children) {
  409. const name = findCategory(el.children, id);
  410. if (name) {
  411. return name;
  412. }
  413. }
  414. }
  415. return null;
  416. };
  417. return findCategory(this.categoryList, item) || "请选择分类";
  418. /* return (
  419. this.categoryList.find((el) => el.id === item)?.name || "请选择分类"
  420. ); */
  421. },
  422. onSaveAs(e) {
  423. this.saveAs = true;
  424. this.onSaveTemplate(e);
  425. },
  426. /* 保存模块 */
  427. onSaveTemplate(e) {
  428. let _this = this;
  429. // 创建一个新对象来存储过滤后的数据
  430. let data = {};
  431. // 遍历原始对象,只保留 dcb_ 开头的属性
  432. Object.keys(e).forEach((key) => {
  433. if (key.startsWith("dcb_")) {
  434. data[key] = e[key];
  435. }
  436. });
  437. // 特殊字段映射
  438. data.dcb_nr = e.content;
  439. data.dcb_attrs = JSON.stringify(e.attrs);
  440. data.dcb_type = e.type;
  441. data.dcb_name = e.name; // 如果需要保存名称
  442. // 处理分类ID
  443. if (Array.isArray(e.category_id)) {
  444. data.category_id = e.category_id[e.category_id.length - 1];
  445. } else {
  446. data.category_id = e.category_id;
  447. }
  448. data.status = 5;
  449. // 根据是否存在 dcb_id 决定创建还是更新
  450. if (e.dcb_id == undefined || this.saveAs) {
  451. createTemplate(data).then((res) => {
  452. if (res.code != 200) return;
  453. e.id = res.data;
  454. _this.$alert("模块信息保存成功");
  455. _this.$emit("onRefresh");
  456. this.saveAs = false;
  457. e.selDisabled = false;
  458. e.valDisabled = false;
  459. this.$forceUpdate();
  460. });
  461. } else {
  462. updateTemplate(data).then((res) => {
  463. if (res.code != 200) return;
  464. _this.$alert("模块信息更新成功");
  465. _this.$emit("onRefresh");
  466. e.selDisabled = false;
  467. e.valDisabled = false;
  468. this.$forceUpdate();
  469. });
  470. }
  471. },
  472. /* 获取模块分类 */
  473. //获取模板分类信息
  474. async initCategoryList() {
  475. let _this = this;
  476. let res = await treeCategory(); //await getAllList();
  477. console.log(res);
  478. this.categoryList =
  479. res.data; /* //this.processDataForCascader(res.data); */
  480. },
  481. processDataForCascader(data) {
  482. console.log("Raw data:", data); // 调试原始数据
  483. const idMap = new Map();
  484. const rootItems = [];
  485. // First pass: create a map of all items
  486. data.forEach((item) => {
  487. const processedItem = { ...item, children: [] };
  488. idMap.set(item.id, processedItem);
  489. });
  490. // Second pass: build the tree structure
  491. data.forEach((item) => {
  492. if (item.parent_id === "0" || item.parent_id === 0) {
  493. rootItems.push(idMap.get(item.id));
  494. } else {
  495. const parent = idMap.get(parseInt(item.parent_id));
  496. if (parent) {
  497. parent.children.push(idMap.get(item.id));
  498. } else {
  499. console.warn(
  500. `Parent with id ${item.parent_id} not found for item:`,
  501. item
  502. );
  503. rootItems.push(idMap.get(item.id)); // 如果找不到父项,作为根项添加
  504. }
  505. }
  506. });
  507. console.log("Processed data:", rootItems); // 调试处理后的数据
  508. return rootItems;
  509. },
  510. onChangeCategory(item, value) {
  511. const selectedId = value[value.length - 1];
  512. item.category_id = selectedId;
  513. // You can now use the selected ID as needed
  514. },
  515. //更新产品属性
  516. onUpdateProdAttr(comIndex, attrIndex, data) {
  517. this.comList[comIndex].attrs[attrIndex].content = data;
  518. let attrName = this.comList[comIndex].attrs[attrIndex].attrs.name;
  519. let attrType = this.comList[comIndex].attrs[attrIndex].attrs.type;
  520. for (var i = 0; i < this.comList.length; i++) {
  521. for (var j = 0; j < this.comList[i].attrs.length; j++) {
  522. if (this.comList[i].attrs[j].type == "ProductAttr") {
  523. if (
  524. this.comList[i].attrs[j].attrs.name == attrName &&
  525. this.comList[i].attrs[j].attrs.type == attrType
  526. ) {
  527. this.comList[i].attrs[j].content = data;
  528. }
  529. }
  530. }
  531. }
  532. this.$emit("onRebuild", this.comList);
  533. },
  534. onUpdateAttr(comIndex, attrIndex, data) {
  535. console.log(comIndex, attrIndex, data);
  536. this.comList[comIndex].attrs[attrIndex].content = data;
  537. let dataType = this.comList[comIndex].attrs[attrIndex].type;
  538. let dataId = this.comList[comIndex].attrs[attrIndex].data;
  539. for (var i = 0; i < this.comList.length; i++) {
  540. for (var j = 0; j < this.comList[i].attrs.length; j++) {
  541. if (
  542. this.comList[i].attrs[j].type == dataType &&
  543. this.comList[i].attrs[j].id == dataId
  544. ) {
  545. this.comList[i].attrs[j].content = data;
  546. }
  547. }
  548. }
  549. this.$emit("onRebuild", this.comList);
  550. },
  551. /* 修改单个值 */
  552. onUpdata(comIndex, attrIndex, attrId, data, type) {
  553. let _this = this;
  554. let com = _this.coms[comIndex];
  555. if (type === "variableNull") {
  556. // 只更新特定的 variableNull 属性
  557. if (
  558. com &&
  559. com.attrs &&
  560. com.attrs[attrIndex] &&
  561. com.attrs[attrIndex].id === attrId
  562. ) {
  563. this.$set(com.attrs[attrIndex], "content", data);
  564. }
  565. } else {
  566. // 原有的逻辑,更新所有相同类型的属性
  567. let dataType = com.attrs[attrIndex].type;
  568. for (var i = 0; i < _this.coms.length; i++) {
  569. for (var j = 0; j < _this.coms[i].attrs.length; j++) {
  570. if (
  571. _this.coms[i].attrs[j].type == dataType &&
  572. _this.coms[i].attrs[j].id == attrId
  573. ) {
  574. _this.coms[i].attrs[j].content = data;
  575. }
  576. }
  577. }
  578. }
  579. _this.$emit("onRebuild", _this.coms);
  580. },
  581. onRemove(index) {
  582. this.$confirm("此操作将删除该模块, 是否继续?", "提示", {
  583. confirmButtonText: "确定",
  584. cancelButtonText: "取消",
  585. type: "warning",
  586. })
  587. .then(() => {
  588. this.$emit("onDelete", index);
  589. })
  590. .catch(() => {
  591. this.$message({
  592. type: "info",
  593. message: "已取消删除",
  594. });
  595. });
  596. },
  597. onEdit(e, state) {
  598. this.comList = this.comList.map((it, index) => {
  599. it.isEdit = 2;
  600. // 当切换到预览状态时(state === 2),触发保存
  601. if (index === e && state === 2) {
  602. // 找到对应的TextArea组件实例并调用save方法
  603. this.$nextTick(() => {
  604. console.log(111);
  605. const componentRef = this.$refs[`component_${index}`];
  606. if (componentRef && componentRef.save) {
  607. componentRef.save();
  608. }
  609. });
  610. }
  611. // 当切换到编辑状态时,对当前项进行 attrs 过滤
  612. if (index === e && state === 1) {
  613. this.filterAttrs(it);
  614. }
  615. return it;
  616. });
  617. if (state == 1) {
  618. this.comList[e].isEdit = 1;
  619. this.onSetActive(e);
  620. }
  621. this.$emit("onRebuild", this.comList);
  622. },
  623. // 新增 filterAttrs 方法
  624. filterAttrs(item) {
  625. // 从 content 中提取所有 {{}} 包裹的 ID
  626. const contentIds = (item.dcb_nr.match(/{{([^}]+)}}/g) || []).map(
  627. (match) => match.slice(2, -2).trim()
  628. );
  629. // 过滤 attrs,只保留在 content 中出现的 ID
  630. item.attrs = item.attrs.filter((attr) => contentIds.includes(attr.id));
  631. },
  632. onAdd(e) {
  633. e.preventDefault();
  634. e.stopPropagation();
  635. this.$emit("onRebuild", this.comList);
  636. },
  637. onDragEnd(e) {
  638. e.preventDefault();
  639. e.stopPropagation();
  640. this.$emit("onRebuild", this.comList);
  641. },
  642. onUpdate(index, content) {
  643. let _this = this;
  644. this.coms[index].content = content;
  645. // 触发父组件的更新
  646. this.$emit("onRebuild", this.coms);
  647. },
  648. //激活当前层
  649. onSetActive(e) {
  650. this.$emit("onSetActiveIndex", e);
  651. },
  652. },
  653. };
  654. </script>
  655. <style lang="scss">
  656. @import "./editor.scss";
  657. .layers {
  658. margin-left: 26px;
  659. .drag-handle {
  660. position: absolute;
  661. top: 0;
  662. left: -27px;
  663. cursor: move;
  664. padding: 0 5px;
  665. display: flex;
  666. align-items: center;
  667. /* background-color: #f0f0f0; */
  668. &:hover {
  669. /* background-color: #e0e0e0; */
  670. }
  671. }
  672. .content-wrapper {
  673. flex-grow: 1;
  674. display: flex;
  675. flex-direction: column;
  676. }
  677. .main {
  678. flex-grow: 1;
  679. }
  680. }
  681. .inline-form {
  682. display: block;
  683. width: 100%;
  684. /* flex-wrap: wrap;
  685. gap: 10px; */
  686. }
  687. .inline-form .el-form-item {
  688. display: flex;
  689. align-items: center;
  690. margin-bottom: 0;
  691. flex: 1;
  692. min-width: 300px;
  693. }
  694. .inline-form .el-form-item__label {
  695. flex-shrink: 0;
  696. width: auto;
  697. padding-right: 12px;
  698. }
  699. .inline-form .el-form-item__content {
  700. flex-grow: 1;
  701. margin-left: 0 !important;
  702. }
  703. .input-item {
  704. width: 200px;
  705. }
  706. </style>