save.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <template>
  2. <div class="container">
  3. <!-- TODO 样式优化:表单宽度、表单项对齐、hr 粗细; -->
  4. <el-tabs v-model="activeName" class="tabs">
  5. <!-- 基础设置 -->
  6. <!-- TODO @luowenfeng:基础设置,分成基础信息、配送信息 -->
  7. <el-tab-pane label="基础设置" name="basic">
  8. <el-form ref="basic" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
  9. <el-form-item label="商品名称" prop="name">
  10. <el-input v-model="baseForm.name" placeholder="请输入商品名称" />
  11. </el-form-item>
  12. <el-form-item label="促销语">
  13. <el-input type="textarea" v-model="baseForm.sellPoint" placeholder="请输入促销语"/>
  14. </el-form-item>
  15. <el-form-item label="商品主图" prop="picUrls">
  16. <ImageUpload v-model="baseForm.picUrls" :value="baseForm.picUrls" :limit="10" class="mall-image"/>
  17. </el-form-item>
  18. <el-form-item label="商品视频" prop="videoUrl">
  19. <VideoUpload v-model="baseForm.videoUrl" :value="baseForm.videoUrl"/>
  20. </el-form-item>
  21. <el-form-item label="商品品牌" prop="brandId">
  22. <el-select v-model="baseForm.brandId" placeholder="请选择商品品牌">
  23. <el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id"/>
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="商品分类" prop="categoryIds">
  27. <el-cascader v-model="baseForm.categoryIds" placeholder="商品分类" style="width: 100%"
  28. :options="categoryList" :props="propName" clearable/>
  29. </el-form-item>
  30. <el-form-item label="是否上架" prop="status">
  31. <el-radio-group v-model="baseForm.status">
  32. <el-radio :label="1">立即上架</el-radio>
  33. <el-radio :label="0">放入仓库</el-radio>
  34. </el-radio-group>
  35. </el-form-item>
  36. </el-form>
  37. </el-tab-pane>
  38. <!-- 价格库存 -->
  39. <!-- TODO @luowenfeng:rates=》priceStack 会更好哈 -->
  40. <el-tab-pane label="价格库存" name="rates" class="rates">
  41. <el-form ref="rates" :model="ratesForm" :rules="rules">
  42. <el-form-item label="启用多规格">
  43. <el-switch v-model="specSwitch" @change="changeSpecSwitch"/>
  44. </el-form-item>
  45. <!-- 动态添加规格属性 -->
  46. <div v-show="ratesForm.spec === 2">
  47. <div v-for="(specs, index) in dynamicSpec" :key="index" class="dynamic-spec">
  48. <!-- 删除按钮 -->
  49. <el-button type="danger" icon="el-icon-delete" circle class="spec-delete" @click="removeSpec(index)"/>
  50. <div class="spec-header">
  51. 规格项:
  52. <el-select v-model="specs.specId" filterable placeholder="请选择" @change="changeSpec">
  53. <el-option v-for="item in propertyPageList" :key="item.id" :label="item.name" :value="item.id"/>
  54. </el-select>
  55. </div>
  56. <div class="spec-values">
  57. <template v-for="(v, i) in specs.specValue">
  58. <el-input v-model="v.name" class="spec-value" :key="i" disabled/>
  59. </template>
  60. </div>
  61. </div>
  62. <el-button type="primary" @click="dynamicSpec.push({specValue: []}); ratesForm.rates = []">添加规格项目</el-button>
  63. </div>
  64. <!-- 规格明细 -->
  65. <el-form-item label="规格明细">
  66. <el-table :data="ratesForm.rates" border style="width: 100%" ref="ratesTable">
  67. <template v-if="this.specSwitch">
  68. <el-table-column :key="index" v-for="(item, index) in dynamicSpec.filter(v => v.specName !== undefined)"
  69. :label="item.specName">
  70. <template v-slot="scope">
  71. <el-input v-if="scope.row.spec" v-model="scope.row.spec[index]" disabled/>
  72. </template>
  73. </el-table-column>
  74. </template>
  75. <el-table-column label="规格图片" width="120px" :render-header="addRedStar" key="90">
  76. <template v-slot="scope">
  77. <ImageUpload v-model="scope.row.picUrl" :limit="1" :isShowTip="false" style="width: 100px; height: 50px"/>
  78. </template>
  79. </el-table-column>
  80. <el-table-column label="市场价(元)" :render-header="addRedStar" key="92">
  81. <template v-slot="scope">
  82. <el-form-item :prop="'rates.'+ scope.$index + '.marketPrice'" :rules="[{required: true, trigger: 'change'}]">
  83. <el-input v-model="scope.row.marketPrice"
  84. oninput="value= value.match(/\d+(\.\d{0,2})?/) ? value.match(/\d+(\.\d{0,2})?/)[0] : ''"/>
  85. </el-form-item>
  86. </template>
  87. </el-table-column>
  88. <el-table-column label="销售价(元)" :render-header="addRedStar" key="93">
  89. <template v-slot="scope">
  90. <el-form-item :prop="'rates.'+ scope.$index + '.price'"
  91. :rules="[{required: true, trigger: 'change'}]">
  92. <el-input v-model="scope.row.price"
  93. oninput="value= value.match(/\d+(\.\d{0,2})?/) ? value.match(/\d+(\.\d{0,2})?/)[0] : ''" />
  94. </el-form-item>
  95. </template>
  96. </el-table-column>
  97. <el-table-column label="成本价" :render-header="addRedStar" key="94">
  98. <template v-slot="scope">
  99. <el-form-item :prop="'rates.'+ scope.$index + '.costPrice'"
  100. :rules="[{required: true, trigger: 'change'}]">
  101. <el-input v-model="scope.row.costPrice"
  102. oninput="value= value.match(/\d+(\.\d{0,2})?/) ? value.match(/\d+(\.\d{0,2})?/)[0] : ''" />
  103. </el-form-item>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="库存" :render-header="addRedStar" key="95">
  107. <template v-slot="scope">
  108. <el-form-item :prop="'rates.'+ scope.$index + '.stock'" :rules="[{required: true, trigger: 'change'}]">
  109. <el-input v-model="scope.row.stock" oninput="value=value.replace(/^(0+)|[^\d]+/g,'')"></el-input>
  110. </el-form-item>
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="预警库存" key="96">
  114. <template v-slot="scope">
  115. <el-input v-model="scope.row.warnStock" oninput="value=value.replace(/^(0+)|[^\d]+/g,'')"></el-input>
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="体积" key="97">
  119. <template v-slot="scope">
  120. <el-input v-model="scope.row.volume" />
  121. </template>
  122. </el-table-column>
  123. <el-table-column label="重量" key="98">
  124. <template v-slot="scope">
  125. <el-input v-model="scope.row.weight" />
  126. </template>
  127. </el-table-column>
  128. <el-table-column label="条码" key="99">
  129. <template v-slot="scope">
  130. <el-input v-model="scope.row.barCode" />
  131. </template>
  132. </el-table-column>
  133. <template v-if="this.specSwitch">
  134. <el-table-column fixed="right" label="操作" width="50" key="100">
  135. <template v-slot="scope">
  136. <el-button @click="scope.row.status = 1" type="text" size="small"
  137. v-show="scope.row.status === undefined || scope.row.status === 0 ">禁用
  138. </el-button>
  139. <el-button @click="scope.row.status = 0" type="text" size="small" v-show="scope.row.status === 1">
  140. 启用
  141. </el-button>
  142. </template>
  143. </el-table-column>
  144. </template>
  145. </el-table>
  146. </el-form-item>
  147. <el-form-item label="虚拟销量" prop="virtualSalesCount">
  148. <el-input v-model="baseForm.virtualSalesCount" placeholder="请输入虚拟销量"
  149. oninput="value=value.replace(/^(0+)|[^\d]+/g,'')"/>
  150. </el-form-item>
  151. </el-form>
  152. </el-tab-pane>
  153. <!-- 商品详情 -->
  154. <el-tab-pane label="商品详情" name="detail">
  155. <el-form ref="detail" :model="baseForm" :rules="rules">
  156. <el-form-item prop="description">
  157. <editor v-model="baseForm.description" :min-height="380"/>
  158. </el-form-item>
  159. </el-form>
  160. </el-tab-pane>
  161. <!-- 销售设置 -->
  162. <el-tab-pane label="高级设置" name="senior">
  163. <el-form ref="senior" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
  164. <el-form-item label="排序字段">
  165. <el-input v-model="baseForm.sort" placeholder="请输入排序字段" oninput="value=value.replace(/^(0+)|[^\d]+/g,'')"/>
  166. </el-form-item>
  167. <el-form-item label="是否展示库存" prop="showStock">
  168. <el-radio-group v-model="baseForm.showStock">
  169. <el-radio :label="true">是</el-radio>
  170. <el-radio :label="false">否</el-radio>
  171. </el-radio-group>
  172. </el-form-item>
  173. </el-form>
  174. </el-tab-pane>
  175. </el-tabs>
  176. <div class="buttons">
  177. <el-button type="info" round @click="cancel">取消</el-button>
  178. <el-button type="success" round @click="submit">确认</el-button>
  179. </div>
  180. </div>
  181. </template>
  182. <script>
  183. import {getBrandList} from "@/api/mall/product/brand";
  184. import {getProductCategoryList} from "@/api/mall/product/category";
  185. import {createSpu, getSpuDetail, updateSpu} from "@/api/mall/product/spu";
  186. import {getPropertyListAndValue,} from "@/api/mall/product/property";
  187. import Editor from "@/components/Editor";
  188. import ImageUpload from "@/components/ImageUpload";
  189. import VideoUpload from "@/components/VideoUpload";
  190. export default {
  191. name: "ProductSave",
  192. components: {
  193. Editor,
  194. ImageUpload,
  195. VideoUpload
  196. },
  197. data() {
  198. return {
  199. specSwitch: false,
  200. activeName: "basic",
  201. propName: {
  202. checkStrictly: true,
  203. label: "name",
  204. value: "id",
  205. },
  206. // 基础设置
  207. baseForm: {
  208. id: null,
  209. name: null,
  210. sellPoint: null,
  211. categoryIds: null,
  212. sort: null,
  213. description: null,
  214. picUrls: null,
  215. videoUrl: null,
  216. status: 0,
  217. virtualSalesCount: 0,
  218. showStock: true,
  219. brandId: null,
  220. },
  221. categoryList: [],
  222. // 价格库存
  223. ratesForm: {
  224. spec: 1,
  225. // 规格明细
  226. rates: [{}]
  227. },
  228. dynamicSpec: [
  229. // {
  230. // specId: 86,
  231. // specName: "颜色",
  232. // specValue:[{
  233. // name: "红色",
  234. // id: 225,
  235. // }]
  236. // },
  237. ],
  238. propertyPageList: [],
  239. brandList: [],
  240. specValue: null,
  241. // 表单校验
  242. rules: {
  243. name: [{required: true, message: "商品名称不能为空", trigger: "blur"},],
  244. description: [{required: true, message: "描述不能为空", trigger: "blur"},],
  245. categoryIds: [{required: true, message: "分类id不能为空", trigger: "blur"},],
  246. status: [{required: true, message: "商品状态不能为空", trigger: "blur"}],
  247. brandId: [{required: true, message: "商品品牌不能为空", trigger: "blur"}],
  248. picUrls: [{required: true, message: "商品轮播图地址不能为空", trigger: "blur"}],
  249. },
  250. };
  251. },
  252. created() {
  253. this.getListBrand();
  254. this.getListCategory();
  255. this.getPropertyPageList();
  256. const spuId = this.$route.params && this.$route.params.spuId;
  257. if (spuId != null) {
  258. this.updateType(spuId)
  259. }
  260. },
  261. methods: {
  262. removeSpec(index) {
  263. this.dynamicSpec.splice(index, 1);
  264. this.changeSpecSwitch()
  265. },
  266. // 必选标识
  267. addRedStar(h, {column}) {
  268. return [
  269. h('span', {style: 'color: #F56C6C'}, '*'),
  270. h('span', ' ' + column.label)
  271. ];
  272. },
  273. changeSpecSwitch() {
  274. this.specSwitch ? this.ratesForm.spec = 2 : this.ratesForm.spec = 1;
  275. this.$refs.ratesTable.doLayout();
  276. if (this.ratesForm.spec === 1) {
  277. this.ratesForm.rates = [{}]
  278. } else {
  279. this.ratesForm.rates = []
  280. if (this.dynamicSpec.length > 0) {
  281. this.buildRatesFormRates()
  282. }
  283. }
  284. },
  285. // 构建规格明细笛卡尔积
  286. buildRatesFormRates() {
  287. let rates = [];
  288. this.dynamicSpec.map(v => v.specValue.map(m => m.name))
  289. .reduce((last, current) => {
  290. const array = [];
  291. last.forEach(par1 => {
  292. current.forEach(par2 => {
  293. let v
  294. // 当两个对象合并时,需使用[1,2]方式生成数组,而当数组和对象合并时,需使用concat
  295. if (par1 instanceof Array) {
  296. v = par1.concat(par2)
  297. } else {
  298. v = [par1, par2];
  299. }
  300. array.push(v)
  301. });
  302. });
  303. return array;
  304. })
  305. .forEach(v => {
  306. let spec = v;
  307. // 当v为单个规格项时,会变成字符串。造成表格只截取第一个字符串,而不是数组的第一个元素
  308. if (typeof v == 'string') {
  309. spec = Array.of(v)
  310. }
  311. rates.push({spec: spec, status: 0, name: Array.of(v).join()})
  312. });
  313. this.ratesForm.rates = rates
  314. },
  315. /** 查询分类 */
  316. getListCategory() {
  317. // 执行查询
  318. getProductCategoryList().then((response) => {
  319. this.categoryList = this.handleTree(response.data, "id", "parentId");
  320. });
  321. },
  322. /** 查询品牌列表 */
  323. getListBrand() {
  324. // 执行查询
  325. getBrandList().then((response) => {
  326. this.brandList = response.data;
  327. });
  328. },
  329. // 取消按钮
  330. cancel() {
  331. var currentView = this.$store.state.tagsView.visitedViews[0]
  332. for (currentView of this.$store.state.tagsView.visitedViews) {
  333. if (currentView.path === this.$route.path) {
  334. break
  335. }
  336. }
  337. this.$store.dispatch('tagsView/delView', currentView)
  338. .then(() => {
  339. this.$router.push("/product/spu")
  340. })
  341. },
  342. submit() {
  343. this.$refs[this.activeName].validate((valid) => {
  344. if (!valid) {
  345. return;
  346. }
  347. let rates = JSON.parse(JSON.stringify(this.ratesForm.rates));
  348. // 价格元转分
  349. rates.forEach(r => {
  350. r.marketPrice = r.marketPrice * 100;
  351. r.price = r.price * 100;
  352. r.costPrice = r.costPrice * 100;
  353. })
  354. // 动态规格调整字段
  355. if (this.specSwitch) {
  356. rates.forEach(r => {
  357. let properties = []
  358. Array.of(r.spec).forEach(s => {
  359. let obj;
  360. if (s instanceof Array) {
  361. obj = s;
  362. } else {
  363. obj = Array.of(s);
  364. }
  365. obj.forEach((v, i) => {
  366. let specValue = this.dynamicSpec[i].specValue.find(o => o.name === v);
  367. let propertie = {};
  368. propertie.propertyId = this.dynamicSpec[i].specId;
  369. propertie.valueId = specValue.id;
  370. properties.push(propertie);
  371. })
  372. })
  373. r.properties = properties;
  374. })
  375. } else {
  376. rates[0].name = this.baseForm.name;
  377. rates[0].status = this.baseForm.status;
  378. }
  379. let form = this.baseForm
  380. if (form.picUrls instanceof Array) {
  381. form.picUrls = form.picUrls.flatMap(m => m.split(','))
  382. } else if (form.picUrls.split(',') instanceof Array) {
  383. form.picUrls = form.picUrls.split(',').flatMap(m => m.split(','))
  384. } else {
  385. form.picUrls = Array.of(form.picUrls)
  386. }
  387. form.skus = rates;
  388. form.specType = this.ratesForm.spec;
  389. let category = form.categoryIds instanceof Array ? form.categoryIds: Array.of(form.categoryIds)
  390. console.log(category)
  391. form.categoryId = category[category.length - 1];
  392. if (form.id == null) {
  393. createSpu(form).then(() => {
  394. this.$modal.msgSuccess("新增成功");
  395. }).then(()=>{
  396. this.cancel();
  397. })
  398. } else {
  399. updateSpu(form).then(() => {
  400. this.$modal.msgSuccess("修改成功");
  401. }).then(()=>{
  402. this.cancel();
  403. })
  404. }
  405. });
  406. },
  407. /** 查询规格 */
  408. getPropertyPageList() {
  409. // 执行查询
  410. getPropertyListAndValue().then((response) => {
  411. this.propertyPageList = response.data;
  412. });
  413. },
  414. // 添加规格项目
  415. changeSpec(val) {
  416. let obj = this.propertyPageList.find(o => o.id === val);
  417. let spec = this.dynamicSpec.find(o => o.specId === val)
  418. spec.specId = obj.id;
  419. spec.specName = obj.name;
  420. spec.specValue = obj.values;
  421. this.buildRatesFormRates();
  422. },
  423. updateType(id) {
  424. getSpuDetail(id).then((response) => {
  425. let data = response.data;
  426. this.baseForm.id = data.id;
  427. this.baseForm.name = data.name;
  428. this.baseForm.sellPoint = data.sellPoint;
  429. this.baseForm.categoryIds = data.categoryId;
  430. this.baseForm.videoUrl = data.videoUrl;
  431. this.baseForm.sort = data.sort;
  432. this.baseForm.description = data.description;
  433. this.baseForm.picUrls = data.picUrls;
  434. this.baseForm.status = data.status;
  435. this.baseForm.virtualSalesCount = data.virtualSalesCount;
  436. this.baseForm.showStock = data.showStock;
  437. this.baseForm.brandId = data.brandId;
  438. this.ratesForm.spec = data.specType;
  439. data.skus.forEach(r => {
  440. r.marketPrice = this.divide(r.marketPrice, 100)
  441. r.price = this.divide(r.price, 100)
  442. r.costPrice = this.divide(r.costPrice, 100)
  443. })
  444. if (this.ratesForm.spec === 2) {
  445. this.specSwitch = true;
  446. data.productPropertyViews.forEach(p => {
  447. let obj = {};
  448. obj.specId = p.propertyId;
  449. obj.specName = p.name;
  450. obj.specValue = p.propertyValues;
  451. this.dynamicSpec.push(obj);
  452. })
  453. data.skus.forEach(s => {
  454. s.spec = [];
  455. s.properties.forEach(sp => {
  456. let spec = data.productPropertyViews.find(o => o.propertyId === sp.propertyId).propertyValues.find(v => v.id === sp.valueId).name;
  457. s.spec.push(spec)
  458. })
  459. })
  460. }
  461. this.ratesForm.rates = data.skus
  462. })
  463. },
  464. },
  465. };
  466. </script>
  467. <style lang="scss">
  468. .container{
  469. padding: 20px;
  470. }
  471. .dynamic-spec {
  472. background-color: #f2f2f2;
  473. width: 85%;
  474. margin: auto;
  475. margin-bottom: 10px;
  476. .spec-header {
  477. padding: 30px;
  478. padding-bottom: 20px;
  479. .spec-name {
  480. display: inline;
  481. input {
  482. width: 30%;
  483. }
  484. }
  485. }
  486. .spec-values {
  487. width: 84%;
  488. padding: 25px;
  489. margin: auto;
  490. padding-top: 5px;
  491. .spec-value {
  492. display: inline-block;
  493. margin-right: 10px;
  494. margin-bottom: 10px;
  495. width: 13%;
  496. }
  497. }
  498. .spec-delete {
  499. float: right;
  500. margin-top: 10px;
  501. margin-right: 10px;
  502. }
  503. }
  504. .tabs {
  505. border-bottom: 2px solid #f2f2f2;
  506. .el-tab-pane {
  507. overflow-y: auto;
  508. }
  509. }
  510. // 库存价格图片样式修改
  511. .rates {
  512. .component-upload-image {
  513. margin: auto;
  514. }
  515. .el-upload--picture-card {
  516. width: 100px;
  517. height: 50px;
  518. line-height: 60px;
  519. margin: auto;
  520. }
  521. .el-upload-list__item {
  522. width: 100px !important;
  523. height: 50px !important;
  524. }
  525. }
  526. .buttons {
  527. margin-top: 20px;
  528. height: 36px;
  529. button {
  530. float: right;
  531. margin-left: 15px;
  532. }
  533. }
  534. .mall-image {
  535. .el-upload--picture-card {
  536. width: 80px;
  537. height: 80px;
  538. line-height: 90px;
  539. }
  540. .el-upload-list__item {
  541. width: 80px;
  542. height: 80px;
  543. }
  544. }
  545. </style>