add.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <script >
  2. import RoundedTitle from "@/components/public/roundedTitle.vue";
  3. import TableSelect from "@/components/public/tableSelect.vue";
  4. import InputRow from "@/components/public/form/inputRow.vue";
  5. import ImageViewer from "@/components/public/imageViewer.vue";
  6. import Pop from "@/components/public/pop.vue";
  7. import PopCard from "@/components/public/popCard.vue";
  8. import ImageTable from "@/components/public/imageTable.vue";
  9. import {escapeHtml, unescapeHtml} from "@/until/unescapeHtml";
  10. import langMap from "@/map/langMap";
  11. import {initForm} from "@/until/formTool";
  12. import {FormVerify} from "kind-form-verify";
  13. import {fieldCheck} from "@/until/form/fieldVerify";
  14. import {isEmpty} from "@/until/typeTool";
  15. import {apiMap} from "@/map/apiMap";
  16. import {handle} from "@/until/handle";
  17. import {rCode} from "@/map/rcodeMap_esm";
  18. let ClassicEditor;
  19. if (process.client) {
  20. ClassicEditor = require('../../../model/ckeditor/ckeditor');
  21. }
  22. let formVerify = null;
  23. let baseEditConfig = {
  24. language: 'zh',
  25. customText: "插入图片",
  26. }
  27. const imageSelectType = {
  28. main: "main",
  29. sub: "sub",
  30. editor: "editor",
  31. }
  32. export default {
  33. name: "productAdd",
  34. components: {ImageTable, PopCard, Pop, ImageViewer, InputRow, TableSelect, RoundedTitle},
  35. props: ['product'],
  36. data() {
  37. return {
  38. isEdit: false,
  39. editId: null,
  40. overviewEditorConfig: { ...baseEditConfig },
  41. detailEditorConfig: { ...baseEditConfig },
  42. parameterEditorConfig: { ...baseEditConfig },
  43. overviewEditor: ClassicEditor,
  44. detailEditor: ClassicEditor,
  45. parameterEditor: ClassicEditor,
  46. overviewEditorData: '<p>这是产品的简介</p>',
  47. detailEditorData: '<p>这是产品的详细介绍</p>',
  48. parameterEditorData: '<p>这是产品的参数介绍, 推荐使用表格详细介绍</p>',
  49. langMap: langMap.lang,
  50. form: {
  51. type: {
  52. val: 'all',
  53. init: '',
  54. msg: '',
  55. state: 0,
  56. options: this.$store.getters.productTypes,
  57. disables: ['all'],
  58. },
  59. name: {
  60. val: '',
  61. init: '',
  62. msg: '',
  63. state: 0
  64. },
  65. remark: {
  66. val: '',
  67. init: '',
  68. msg: '',
  69. state: 0
  70. },
  71. sort: {
  72. val: 0,
  73. init: 0,
  74. msg: '',
  75. state: 0
  76. },
  77. image: {
  78. val: '',
  79. init: '',
  80. msg: '',
  81. reCheckField: 'fileData',
  82. showText: '',
  83. state: 0
  84. },
  85. seo_key: {
  86. val: "",
  87. oldVal: "",
  88. init: "",
  89. msg: "",
  90. state: 0,
  91. }
  92. },
  93. sub_images: [],
  94. imageSelectType: imageSelectType,
  95. // 图片选择的参数
  96. imagePopShow: false,
  97. imagePopLoading: false,
  98. imageSelect: {
  99. type: '',
  100. param: ''
  101. },
  102. previewShow: false,
  103. previewLoading: false,
  104. previewData: {
  105. },
  106. }
  107. },
  108. watch: {
  109. product(val){
  110. // 更新文章数据
  111. this.editModel()
  112. }
  113. },
  114. computed: {
  115. tags () {
  116. // 拆分tags
  117. let tags = this.form.seo_key.val
  118. if (!tags) {
  119. return [];
  120. }
  121. // 去除空字符串
  122. tags = tags.replace(/\s/g,'')
  123. tags = tags.replace(/,|;/g,',')
  124. let tagArr = tags.split(',')
  125. // 标签去重
  126. tagArr = [...new Set(tagArr)]
  127. // 移除空标签
  128. tagArr = tagArr.filter(item => item)
  129. return tagArr
  130. }
  131. },
  132. beforeMount() {
  133. this.detailEditorConfig.customFunction = () => {
  134. this.openImageSelect(imageSelectType.editor, 'detail');
  135. };
  136. this.overviewEditorConfig.customFunction = () => {
  137. this.openImageSelect(imageSelectType.editor, 'overview');
  138. };
  139. this.parameterEditorConfig.customFunction = () => {
  140. this.openImageSelect(imageSelectType.editor, 'parameter');
  141. };
  142. },
  143. mounted() {
  144. formVerify = new FormVerify(
  145. this.form,
  146. fieldCheck,
  147. )
  148. // fieldCheck.checkField('type',this.form.type.val)
  149. // formVerify.checkForm(this.form, true);
  150. console.log(formVerify);
  151. formVerify.onLog = (msg) => {
  152. console.log(msg);
  153. };
  154. // 判断是否为编辑文章
  155. if(!isEmpty(this.product)){
  156. this.isEdit = true;
  157. }
  158. this.initForm();
  159. },
  160. beforeDestroy() {
  161. formVerify = null;
  162. },
  163. methods: {
  164. initForm(){
  165. initForm(this.form);
  166. this.form.image.showText = this.form.image.val;
  167. // 强制更新页面
  168. this.$forceUpdate()
  169. },
  170. /**
  171. * 图片路径处理, 兼容手动上传的图片与后期自动增加的图片
  172. * @param text
  173. * @return {*|string|string}
  174. */
  175. imagePathBabel(text){
  176. return text?text.charAt(0) == '/'? text : '/public/'+text : ''
  177. },
  178. openImageSelect(type, param){
  179. console.log('打开文章封面选择窗口');
  180. this.imageSelect = {type, param}
  181. this.imagePopShow = true;
  182. },
  183. deleteTag(tag){
  184. let tagArr = this.tags
  185. let index = tagArr.indexOf(tag)
  186. tagArr.splice(index, 1)
  187. this.form.seo_key.val = tagArr.join(',')
  188. this.form.seo_key.msg = ''
  189. },
  190. // 子图转换
  191. translateSubImage(arr){
  192. let sub_images = arr.map((item)=>{
  193. return {
  194. id: item.id,
  195. isAdd: false,
  196. path: item.path
  197. }
  198. })
  199. return sub_images
  200. },
  201. addSubImage(){
  202. this.sub_images.push('')
  203. },
  204. deleteSubImage(i){
  205. this.sub_images.splice(i, 1)
  206. },
  207. editorAddImage(editor, fileData){
  208. switch (editor) {
  209. case 'overview':
  210. this.overviewEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
  211. break;
  212. case 'detail':
  213. this.detailEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
  214. break;
  215. case 'parameter':
  216. this.parameterEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
  217. break;
  218. }
  219. },
  220. selectImageHandle(fileData){
  221. console.log('选择图片')
  222. console.log(fileData)
  223. this.imagePopShow = false;
  224. switch (this.imageSelect.type) {
  225. case imageSelectType.main:
  226. console.log('选择产品主图');
  227. this.form.image.val = fileData.filePath
  228. this.form.image.msg = '';
  229. this.form.image.state = 0;
  230. this.form.image.showText = fileData.filePath
  231. console.log(fileData.path)
  232. console.log(this.form.image)
  233. break;
  234. case imageSelectType.sub:
  235. console.log(`选择产品子图 param:${this.imageSelect.param}`);
  236. this.sub_images[this.imageSelect.param] = fileData.filePath
  237. break;
  238. case imageSelectType.editor:
  239. this.editorAddImage(this.imageSelect.param, fileData)
  240. break;
  241. default:
  242. console.log('未知类型选择图片');
  243. break;
  244. }
  245. },
  246. editModel(){
  247. console.log(`editModel`)
  248. this.isEdit = true;
  249. this.editId = this.product.proid;
  250. this.form.type.init = this.product.type_key;
  251. this.form.name.init = this.product.name;
  252. this.form.sort.init = this.product.sort;
  253. this.form.remark.init = this.product.remark;
  254. this.form.image.init = this.product.image;
  255. this.form.seo_key.init = this.product.seo_key;
  256. this.sub_images = this.product.sub_images;
  257. this.overviewEditorData = unescapeHtml(this.product.overview) || "";
  258. this.detailEditorData = unescapeHtml(this.product.detail) || "";
  259. this.parameterEditorData = unescapeHtml(this.product.parameter) || "";
  260. this.initForm()
  261. },
  262. async onSubmitHandle(){
  263. console.log('提交产品');
  264. let isPass = formVerify.checkForm(this.form, true);
  265. if(!isPass){
  266. console.log(this.form);
  267. this.$message.error('数据验证不通过');
  268. return console.log('数据验证不通过');
  269. }
  270. let formData = formVerify.getFormData();
  271. formData.detail = escapeHtml(this.detailEditorData);
  272. formData.overview = escapeHtml(this.overviewEditorData);
  273. formData.parameter = escapeHtml(this.parameterEditorData);
  274. formData.sub_images = this.sub_images;
  275. let queryPath = this.isEdit? apiMap.productEdit : apiMap.productAdd;
  276. let actionText = this.isEdit? '修改产品': '新增产品';
  277. if(this.isEdit){
  278. formData.proid = this.editId;
  279. }
  280. let [err,res] = await handle(this.$axios.post(
  281. queryPath.path,
  282. formData
  283. ));
  284. if(err){
  285. console.log(err);
  286. return this.$message.error(`${actionText}失败`);
  287. }
  288. let result = res.data;
  289. if (result.code === rCode.OK){
  290. this.$message.success(`${actionText}成功`);
  291. }else{
  292. this.$message.error(`${actionText}失败,${result.msg}`);
  293. }
  294. },
  295. preview(){
  296. console.log('预览产品');
  297. this.previewShow = true;
  298. this.previewLoading = true;
  299. setTimeout(()=>{
  300. this.$nextTick(()=>{
  301. this.previewLoading = false;
  302. this.previewData = this.getPreviewData();
  303. // 强制刷新
  304. this.$forceUpdate()
  305. })
  306. }, 500)
  307. },
  308. cancelPreview(){
  309. console.log('关闭预览')
  310. this.previewLoading = false;
  311. this.previewShow = false;
  312. },
  313. getPreviewData(){
  314. let data = {
  315. proid: this.editId,
  316. type_key: this.form.type.val,
  317. name: this.form.name.val,
  318. sort: this.form.sort.val,
  319. remark: this.form.remark.val,
  320. image: this.form.image.val,
  321. seo_key: this.form.seo_key.val,
  322. overview: this.overviewEditorData,
  323. detail: this.detailEditorData,
  324. parameter: this.parameterEditorData,
  325. sub_images: this.sub_images
  326. }
  327. return data
  328. }
  329. }
  330. }
  331. </script>
  332. <template>
  333. <div class="w-full p-2">
  334. <rounded-title class="text-xl flex ">
  335. <span>{{ isEdit? "编辑产品" : "新增产品" }}</span>
  336. <a href="/manger/product"
  337. class="edit-btn custom-btn btn-13 ml-2">
  338. 产品列表
  339. </a>
  340. </rounded-title>
  341. <div class="page-content">
  342. <input-row class="mt-3"
  343. :msg="form.type.msg"
  344. :label-width="120"
  345. label="产品分类">
  346. <table-select
  347. class="w-48 flex-shrink-0"
  348. :options="form.type.options"
  349. v-model="form.type.val"
  350. />
  351. </input-row>
  352. <input-row class="mt-3"
  353. :msg="form.name.msg"
  354. :label-width="120"
  355. label="产品名称">
  356. <a-input class="!w-48 "
  357. v-model="form.name.val"
  358. placeholder="请输入产品名称"></a-input>
  359. </input-row>
  360. <input-row class="mt-3"
  361. :msg="form.remark.msg"
  362. :label-width="120"
  363. label="产品简述">
  364. <a-input
  365. v-model="form.remark.val"
  366. placeholder="请输入产品简述"></a-input>
  367. </input-row>
  368. <input-row class="mt-3"
  369. :msg="form.sort.msg"
  370. :label-width="120"
  371. label="产品排序优先级">
  372. <a-input-number class="!w-48 "
  373. v-model="form.sort.val"
  374. :min="0"
  375. :max="9999"
  376. ></a-input-number>
  377. </input-row>
  378. <input-row class="mt-3"
  379. :msg="form.seo_key.msg"
  380. :label-width="120"
  381. label="seo关键字"
  382. remark="用于增加产品再搜索引擎中的关键字, 要确保准确有效"
  383. >
  384. <a-tooltip>
  385. <template slot="title">
  386. 搜索引擎关注的关键字,多个用英文逗号隔开
  387. </template>
  388. <a-input v-model="form.seo_key.val" placeholder="seo关键字"></a-input>
  389. </a-tooltip>
  390. <div class="tags">
  391. <a-tag
  392. v-for="tag in tags"
  393. :key="tag"
  394. color="#2db7f5"
  395. closable @close="deleteTag"
  396. >
  397. {{ tag }}
  398. </a-tag>
  399. </div>
  400. </input-row>
  401. <input-row class="mt-3"
  402. :msg="form.image.msg"
  403. :label-width="120"
  404. label="产品主图"
  405. remark="也会作为产品会显示的的封面"
  406. >
  407. <div class="rounded relative" style="width: 524px;height: 360px" >
  408. <image-viewer class="" :src="imagePathBabel(form.image.showText)"
  409. :alt="form.image.showText" ></image-viewer>
  410. <div class="absolute w-full h-full left-0 top-0
  411. justify-center text-white bg-gray-400
  412. items-center text-2xl flex opacity-0 hover:opacity-70"
  413. @click="openImageSelect(imageSelectType.main)"
  414. >
  415. 点击选择产品图片
  416. </div>
  417. </div>
  418. </input-row>
  419. <input-row class="mt-3"
  420. :label-width="120"
  421. label="产品子图"
  422. remark="产品更多的缩略图, 可以为空"
  423. >
  424. <div class="sub-image">
  425. <!-- 新增按钮 -->
  426. <div class="image-add">
  427. <a-button type="primary" @click="addSubImage">
  428. <a-icon type="plus" />
  429. 添加子图
  430. </a-button>
  431. </div>
  432. <div class="image-list">
  433. <div class="image-item"
  434. v-for="(item, i) in sub_images"
  435. :key="`sub_img_${item}`"
  436. >
  437. <div class="image-item-content">
  438. <image-viewer
  439. :src="imagePathBabel(item)"
  440. ></image-viewer>
  441. <div class="absolute w-full h-full left-0 top-0
  442. justify-center text-white bg-gray-400
  443. items-center text-xl flex opacity-0 hover:opacity-70"
  444. @click="openImageSelect(imageSelectType.sub, i)"
  445. >
  446. 选择图片
  447. </div>
  448. </div>
  449. <div class="option">
  450. <a-button type="danger"
  451. @click="deleteSubImage(i)">
  452. 移除
  453. </a-button>
  454. </div>
  455. </div>
  456. </div>
  457. </div>
  458. </input-row>
  459. <input-row class="mt-3"
  460. :label-width="120"
  461. label="产品简介"
  462. remark="产品的简要介绍"
  463. >
  464. <ckeditor class="mt-2" ref="ckeditor"
  465. :editor="overviewEditor"
  466. v-model="overviewEditorData"
  467. :config="overviewEditorConfig"></ckeditor>
  468. </input-row>
  469. <input-row class="mt-3"
  470. :label-width="120"
  471. label="产品详情"
  472. remark="产品的详细介绍"
  473. >
  474. <ckeditor class="mt-2" ref="ckeditor"
  475. :editor="detailEditor"
  476. v-model="detailEditorData"
  477. :config="detailEditorConfig"></ckeditor>
  478. </input-row>
  479. <input-row class="mt-3"
  480. :label-width="120"
  481. label="产品参数"
  482. remark="产品的参数配置信息, 推荐使用表格"
  483. >
  484. <ckeditor class="mt-2" ref="ckeditor"
  485. :editor="parameterEditor"
  486. v-model="parameterEditorData"
  487. :config="parameterEditorConfig"></ckeditor>
  488. </input-row>
  489. <div class="mt-2">
  490. <a-button type="primary" @click="onSubmitHandle">
  491. {{isEdit?"保存修改":"新增产品"}}
  492. </a-button>
  493. <a-button class="ml-2" @click="preview">
  494. 预览
  495. </a-button>
  496. </div>
  497. </div>
  498. <!-- 图片选择弹窗 -->
  499. <pop :show="imagePopShow" :loading="imagePopLoading">
  500. <pop-card>
  501. <template slot="header">
  502. <span>选择需要插入的图片</span>
  503. </template>
  504. <image-table
  505. class="w-full h-full"
  506. @cancel="imagePopShow = false"
  507. @ok="selectImageHandle">
  508. </image-table>
  509. </pop-card>
  510. </pop>
  511. <pop :show="previewShow" :loading="previewLoading"
  512. >
  513. <div class="product-show">
  514. <!-- 关闭按钮 -->
  515. <product-info
  516. :product="previewData"
  517. :unescape="false"
  518. lang="lang"
  519. :productId="editId"
  520. />
  521. <div class="absolute top-0 right-0 p-2 text-red-500 text-xl">
  522. <a-icon type="close" @click="cancelPreview"/>
  523. </div>
  524. </div>
  525. </pop>
  526. </div>
  527. </template>
  528. <style scoped>
  529. .page-content{
  530. margin-top: 20px;
  531. width: 100%;
  532. height: calc(100% - 50px);
  533. overflow: auto;
  534. background-color: #fff;
  535. border-radius: 10px;
  536. box-sizing: border-box;
  537. padding: 10px;
  538. }
  539. .edit-btn{
  540. font-size: 14px;
  541. }
  542. .tags{
  543. width: 100%;
  544. display: flex;
  545. flex-wrap: wrap;
  546. justify-content: flex-start;
  547. align-items: center;
  548. padding: 5px;
  549. box-sizing: border-box;
  550. }
  551. .sub-image{
  552. width: 100%;
  553. max-width: 520px;
  554. height: auto;
  555. padding: 0.5rem;
  556. border-radius: 10px;
  557. box-sizing: border-box;
  558. background-color: #f5f5f5;
  559. border: 1px solid #e5e5e5;
  560. }
  561. .image-add{
  562. width: 100%;
  563. height: 40px;
  564. border-bottom: 1px solid #e5e5e5;
  565. }
  566. .image-list{
  567. width: 100%;
  568. height: auto;
  569. max-height: 550px;
  570. overflow: auto;
  571. display: flex;
  572. flex-direction: column;
  573. justify-content: center;
  574. }
  575. .image-item{
  576. width: calc(100% - 10px);
  577. margin: 5px 0;
  578. height: 100px;
  579. display: flex;
  580. justify-content: space-between;
  581. }
  582. .image-item-content{
  583. width: 100px;
  584. height: 100px;
  585. position: relative;
  586. }
  587. .option{
  588. width: 100px;
  589. height: 100px;
  590. display: flex;
  591. justify-content: center;
  592. align-items: center;
  593. }
  594. .product-show{
  595. width: 80%;
  596. height: calc(100% - 50px);
  597. overflow: auto;
  598. background-color: #fff;
  599. border-radius: 10px;
  600. position: relative;
  601. }
  602. </style>