save.vue 20 KB

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