add.vue 17 KB

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