save.vue 19 KB

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