Răsfoiți Sursa

Merge branch 'spu-sku' into redesign‌-260706

shizhongqi 2 zile în urmă
părinte
comite
5840422e99

+ 101 - 105
hdApp/src/admin/goods/add.vue

@@ -1,3 +1,8 @@
+<!--
+  花店后台新增商品
+  谁用:hdApp 管理端创建花束
+  解决问题:单规格写主记录;多规格主记录承载首规格(specName/price/stock),其余规格提交 specList 由后端拆子行
+-->
 <template>
   <view class="app-content">
     
@@ -215,7 +220,6 @@
 <script>
 import TuiListCell from "@/components/plugin/list-cell";
 import AppUploader from "@/components/app-uploader";
-const form = require("@/utils/formValidation.js");
 import { getClass } from "@/api/category";
 import { addGoodsData, useCaseList } from "@/api/goods"
 import { delImage } from "@/api/pic-text";
@@ -427,7 +431,7 @@ export default {
       });
     },
     toggleSpecs(e) {
-      this.form.specEnabled=e.detail.value?1:0;
+      this.form.specEnabled = e.detail.value ? 1 : 0;
       if(this.form.specEnabled && !this.form.specList.length) {
         this.form.specList.push(this.createSpecData(this.form))
       }
@@ -448,54 +452,94 @@ export default {
       spec.stock = status === '0' ? 9999 : '';
       this.$set(this.form.specList, index, { ...spec });
     },
+    /**
+     * 校验单个数值字段。空值按 required 决定是否报错;exclusiveMin 时必须大于 min。
+     * @param {*} value 表单原始值
+     * @param {{label:string, required?:boolean, min?:number, exclusiveMin?:boolean}} options 校验配置
+     * @returns {string} 错误文案,空字符串表示通过
+     */
+    validateNumberField(value, options) {
+      const label = options.label;
+      const required = !!options.required;
+      const min = options.min !== undefined ? options.min : 0;
+      const exclusiveMin = !!options.exclusiveMin;
+      const isEmpty = value === '' || value === null || value === undefined;
+      if (isEmpty) {
+        return required ? `请输入${label}` : '';
+      }
+      const num = parseFloat(value);
+      if (isNaN(num)) {
+        return `${label}必须是数字`;
+      }
+      if (exclusiveMin && num <= min) {
+        return `${label}必须是大于${min}的数字`;
+      }
+      if (!exclusiveMin && num < min) {
+        return `${label}必须是大于等于${min}的数字`;
+      }
+      return '';
+    },
+    /**
+     * 校验一组商品数值字段。单规格与规格组共用,避免两套规则不一致。
+     * 有价格时价格必填且 >0;自定义库存时库存必填且 >=0;花支数/重量/已售选填,填了必须 >=0。
+     * @param {Object} data 单规格 form 或某个规格组
+     * @param {string} prefix 错误文案前缀,如「规格组1」
+     * @returns {string} 第一条错误文案
+     */
+    validateGoodsNumberFields(data, prefix) {
+      prefix = prefix || '';
+      const withPrefix = (label) => prefix ? `${prefix}${label}` : label;
+      const rules = [];
+      if (String(data.priceType) === '1') {
+        rules.push({ key: 'price', label: withPrefix('价格'), required: true, min: 0, exclusiveMin: true });
+      }
+      if (String(data.stockSet) === '1') {
+        rules.push({ key: 'stock', label: withPrefix('库存数量'), required: true, min: 0 });
+      }
+      rules.push(
+        { key: 'flowerNum', label: withPrefix('花支数'), required: false, min: 0 },
+        { key: 'weight', label: withPrefix('重量'), required: false, min: 0 },
+        { key: 'sold', label: withPrefix('已售数量'), required: false, min: 0 }
+      );
+      for (let i = 0; i < rules.length; i++) {
+        const rule = rules[i];
+        const msg = this.validateNumberField(data[rule.key], rule);
+        if (msg) {
+          return msg;
+        }
+      }
+      return '';
+    },
+    /**
+     * 多规格开启时校验规格列表。返回错误文案,空字符串表示通过。
+     */
     validateSpecList() {
-      if (this.form.specEnabled != 1) return true;
+      if (this.form.specEnabled != 1) {
+        return '';
+      }
       if (!this.form.specList.length) {
-        this.$msg('请至少添加一个规格组');
-        return false;
+        return '请至少添加一个规格组';
       }
       for (let i = 0; i < this.form.specList.length; i++) {
         const spec = this.form.specList[i];
         const name = `规格组${i + 1}`;
         if (!spec.specName || !String(spec.specName).trim()) {
-          this.$msg(`请输入${name}名称`);
-          return false;
-        }
-        if (String(spec.priceType) === '1') {
-          const price = parseFloat(spec.price);
-          if (spec.price === '' || isNaN(price) || price <= 0) {
-            this.$msg(`${name}价格必须是大于0的数字`);
-            return false;
-          }
-        }
-        if (String(spec.stockSet) === '1') {
-          const stock = parseFloat(spec.stock);
-          if (spec.stock === '' || isNaN(stock) || stock < 0) {
-            this.$msg(`${name}库存数量必须是大于等于0的数字`);
-            return false;
-          }
+          return `请输入${name}名称`;
         }
-        const flowerNum = parseFloat(spec.flowerNum || 0);
-        if (isNaN(flowerNum) || flowerNum < 0) {
-          this.$msg(`${name}花支数必须是大于等于0的数字`);
-          return false;
-        }
-        const weight = parseFloat(spec.weight);
-        if (spec.weight === '' || isNaN(weight) || weight < 0) {
-          this.$msg(`${name}重量必须是大于等于0的数字`);
-          return false;
-        }
-        const sold = parseFloat(spec.sold || 0);
-        if (isNaN(sold) || sold < 0) {
-          this.$msg(`${name}已售数量必须是大于等于0的数字`);
-          return false;
+        const numErr = this.validateGoodsNumberFields(spec, name);
+        if (numErr) {
+          return numErr;
         }
       }
-      return true;
+      return '';
     },
+    /**
+     * 提交前规范化规格:多规格把首规格价库和名称回写主表单字段,specList 仍整表提交由后端拆主/子
+     */
     normalizeSpecSubmitData(formData) {
       if (formData.specEnabled != 1) {
         formData.specList = [];
+        formData.specName = '';
         return formData;
       }
       formData.specList = formData.specList.map(item => {
@@ -514,6 +558,7 @@ export default {
         return spec;
       });
       const firstSpec = formData.specList[0];
+      formData.specName = firstSpec.specName || '';
       formData.priceType = String(firstSpec.priceType);
       formData.price = firstSpec.price;
       formData.stockSet = String(firstSpec.stockSet);
@@ -629,78 +674,29 @@ export default {
         })
       })
     },
-    commitForm(e) {
-   
-      let rules = [
-        {
-          name: "name",
-          rule: ["required"],
-          msg: ["请输入名称"]
-        }
-      ];
-      
-      if (!this.validateSpecList()) {
-        return false;
+    /**
+     * 提交前统一校验。用 this.form,避免原生 form 取值漏掉无 name 的规格字段。
+     * @returns {string} 第一条错误文案,空字符串表示通过
+     */
+    getFormValidateError() {
+      if (!this.form.name || !String(this.form.name).trim()) {
+        return '请输入名称';
       }
-
-      // 只有在有价格类型时才验证价格
-      if (this.form.specEnabled != 1 && this.form.priceType === '1') {
-        rules.push({
-          name: "price",
-          rule: ["required"],
-          msg: ["请输入价格"]
-        });
-        
-        // 检查价格是否为有效数字且大于0
-        if (this.form.price !== "" && (isNaN(parseFloat(this.form.price)) || parseFloat(this.form.price) <= 0)) {
-          this.$msg('价格必须是大于0的数字!');
-          return false;
-        }
-      }
-      
-      // 验证库存数量
-      if (this.form.specEnabled != 1 && this.form.stockSet === '1' && this.form.stock !== "") {
-        const stockNum = parseFloat(this.form.stock);
-        if (isNaN(stockNum) || stockNum < 0) {
-          this.$msg('库存数量必须是大于等于0的数字!');
-          return false;
-        }
-      }
-      
-      // 验证已售数量
-      if (this.form.specEnabled != 1 && this.form.sold !== "") {
-        const soldNum = parseFloat(this.form.sold);
-        if (isNaN(soldNum) || soldNum < 0) {
-          this.$msg('已售数量必须是大于等于0的数字!');
-          return false;
-        }
+      if (this.form.specEnabled == 1) {
+        return this.validateSpecList();
       }
-      
-      // 验证花支数
-      if (this.form.specEnabled != 1 && this.form.flowerNum !== "" && this.form.flowerNum !== 0) {
-        const flowerNum = parseFloat(this.form.flowerNum);
-        if (isNaN(flowerNum) || flowerNum < 0) {
-          this.$msg('花支数必须是大于0的数字!');
-          return false;
-        }
-      }
-
-      // 验证重量(千克)
-      if (this.form.specEnabled != 1 && this.form.weight !== "") {
-        const weightNum = parseFloat(this.form.weight);
-        if (isNaN(weightNum) || weightNum < 0) {
-          this.$msg('重量必须是大于等于0的数字!');
-          return false;
-        }
-      }
-      
-      let formData = e.detail.value;
-      let checkRes = form.validation(formData, rules);
-      if (!checkRes) {
-         this.confirmFn();
-      } else {
-        this.$msg(checkRes);
+      return this.validateGoodsNumberFields(this.form, '');
+    },
+    /**
+     * 表单提交入口。先校验再走确认弹窗,校验失败只提示第一条错误。
+     */
+    commitForm() {
+      const err = this.getFormValidateError();
+      if (err) {
+        this.$msg(err);
+        return false;
       }
+      this.confirmFn();
     },
     // 库存输入框聚焦
     onStockFocus() {

+ 102 - 105
hdApp/src/admin/goods/detail.vue

@@ -1,3 +1,8 @@
+<!--
+  花店后台编辑商品
+  谁用:hdApp 管理端修改花束
+  解决问题:详情 specList 首项即主记录自身;提交时把首规格名称回写主表单 specName
+-->
 <template>
   <view class="app-content">
 
@@ -259,7 +264,6 @@
 </template>
 <script>
 import TuiListCell from "@/components/plugin/list-cell";
-const form = require("@/utils/formValidation.js");
 import { getClass } from "@/api/category";
 import { getGoodsDetail, updateGoodsData, createSn, useCaseList } from "@/api/goods";
 import { delImages } from "@/api/pic-text";
@@ -442,7 +446,8 @@ export default {
         this.form.useCaseIdList = Array.isArray(useCaseIdList)
           ? useCaseIdList.map(id => Number(id)).filter(id => id > 0)
           : [];
-        this.form.specEnabled = Number(res.data.specEnabled || 0);
+        this.form.specEnabled = Number(res.data.specEnabled || res.data.goodsStyle || 0);
+        // 详情 specList 首项即主记录自身(id=主商品id),createSpecData 直接兼容
         this.form.specList = (res.data.specList || []).map(item => this.createSpecData(item));
 
         // 设置价格类型
@@ -603,54 +608,94 @@ export default {
       spec.stock = status === '0' ? 9999 : '';
       this.$set(this.form.specList, index, { ...spec });
     },
+    /**
+     * 校验单个数值字段。空值按 required 决定是否报错;exclusiveMin 时必须大于 min。
+     * @param {*} value 表单原始值
+     * @param {{label:string, required?:boolean, min?:number, exclusiveMin?:boolean}} options 校验配置
+     * @returns {string} 错误文案,空字符串表示通过
+     */
+    validateNumberField(value, options) {
+      const label = options.label;
+      const required = !!options.required;
+      const min = options.min !== undefined ? options.min : 0;
+      const exclusiveMin = !!options.exclusiveMin;
+      const isEmpty = value === '' || value === null || value === undefined;
+      if (isEmpty) {
+        return required ? `请输入${label}` : '';
+      }
+      const num = parseFloat(value);
+      if (isNaN(num)) {
+        return `${label}必须是数字`;
+      }
+      if (exclusiveMin && num <= min) {
+        return `${label}必须是大于${min}的数字`;
+      }
+      if (!exclusiveMin && num < min) {
+        return `${label}必须是大于等于${min}的数字`;
+      }
+      return '';
+    },
+    /**
+     * 校验一组商品数值字段。单规格与规格组共用,避免两套规则不一致。
+     * 有价格时价格必填且 >0;自定义库存时库存必填且 >=0;花支数/重量/已售选填,填了必须 >=0。
+     * @param {Object} data 单规格 form 或某个规格组
+     * @param {string} prefix 错误文案前缀,如「规格组1」
+     * @returns {string} 第一条错误文案
+     */
+    validateGoodsNumberFields(data, prefix) {
+      prefix = prefix || '';
+      const withPrefix = (label) => prefix ? `${prefix}${label}` : label;
+      const rules = [];
+      if (String(data.priceType) === '1') {
+        rules.push({ key: 'price', label: withPrefix('价格'), required: true, min: 0, exclusiveMin: true });
+      }
+      if (String(data.stockSet) === '1') {
+        rules.push({ key: 'stock', label: withPrefix('库存数量'), required: true, min: 0 });
+      }
+      rules.push(
+        { key: 'flowerNum', label: withPrefix('花支数'), required: false, min: 0 },
+        { key: 'weight', label: withPrefix('重量'), required: false, min: 0 },
+        { key: 'sold', label: withPrefix('已售数量'), required: false, min: 0 }
+      );
+      for (let i = 0; i < rules.length; i++) {
+        const rule = rules[i];
+        const msg = this.validateNumberField(data[rule.key], rule);
+        if (msg) {
+          return msg;
+        }
+      }
+      return '';
+    },
+    /**
+     * 多规格开启时校验规格列表。返回错误文案,空字符串表示通过。
+     */
     validateSpecList() {
-      if (this.form.specEnabled != 1) return true;
+      if (this.form.specEnabled != 1) {
+        return '';
+      }
       if (!this.form.specList.length) {
-        this.$msg('请至少添加一个规格组');
-        return false;
+        return '请至少添加一个规格组';
       }
       for (let i = 0; i < this.form.specList.length; i++) {
         const spec = this.form.specList[i];
         const name = `规格组${i + 1}`;
         if (!spec.specName || !String(spec.specName).trim()) {
-          this.$msg(`请输入${name}名称`);
-          return false;
-        }
-        if (String(spec.priceType) === '1') {
-          const price = parseFloat(spec.price);
-          if (spec.price === '' || isNaN(price) || price <= 0) {
-            this.$msg(`${name}价格必须是大于0的数字`);
-            return false;
-          }
-        }
-        if (String(spec.stockSet) === '1') {
-          const stock = parseFloat(spec.stock);
-          if (spec.stock === '' || isNaN(stock) || stock < 0) {
-            this.$msg(`${name}库存数量必须是大于等于0的数字`);
-            return false;
-          }
+          return `请输入${name}名称`;
         }
-        const flowerNum = parseFloat(spec.flowerNum || 0);
-        if (isNaN(flowerNum) || flowerNum < 0) {
-          this.$msg(`${name}花支数必须是大于等于0的数字`);
-          return false;
-        }
-        const weight = parseFloat(spec.weight);
-        if (spec.weight === '' || isNaN(weight) || weight < 0) {
-          this.$msg(`${name}重量必须是大于等于0的数字`);
-          return false;
-        }
-        const sold = parseFloat(spec.sold || 0);
-        if (isNaN(sold) || sold < 0) {
-          this.$msg(`${name}已售数量必须是大于等于0的数字`);
-          return false;
+        const numErr = this.validateGoodsNumberFields(spec, name);
+        if (numErr) {
+          return numErr;
         }
       }
-      return true;
+      return '';
     },
+    /**
+     * 提交前规范化规格:多规格把首规格价库和名称回写主表单字段,specList 仍整表提交由后端拆主/子
+     */
     normalizeSpecSubmitData(formData) {
       if (formData.specEnabled != 1) {
         formData.specList = [];
+        formData.specName = '';
         return formData;
       }
       formData.specList = formData.specList.map(item => {
@@ -669,6 +714,7 @@ export default {
         return spec;
       });
       const firstSpec = formData.specList[0];
+      formData.specName = firstSpec.specName || '';
       formData.priceType = String(firstSpec.priceType);
       formData.price = firstSpec.price;
       formData.stockSet = String(firstSpec.stockSet);
@@ -678,78 +724,29 @@ export default {
       formData.sold = firstSpec.sold;
       return formData;
     },
-    formSubmit(e) {
-
-      let rules = [
-        {
-          name: "name",
-          rule: ["required"],
-          msg: ["请输入名称"]
-        }
-      ];
-
-      if (!this.validateSpecList()) {
-        return false;
-      }
-
-      // 只有在有价格类型时才验证价格
-      if (this.form.specEnabled != 1 && this.form.priceType === '1') {
-        rules.push({
-          name: "price",
-          rule: ["required"],
-          msg: ["请输入价格"]
-        });
-
-        // 检查价格是否为有效数字且大于0
-        if (this.form.price !== "" && (isNaN(parseFloat(this.form.price)) || parseFloat(this.form.price) <= 0)) {
-          this.$msg('价格必须是大于0的数字!');
-          return false;
-        }
+    /**
+     * 提交前统一校验。用 this.form,避免原生 form 取值漏掉无 name 的规格字段。
+     * @returns {string} 第一条错误文案,空字符串表示通过
+     */
+    getFormValidateError() {
+      if (!this.form.name || !String(this.form.name).trim()) {
+        return '请输入名称';
       }
-
-      // 验证库存数量
-      if (this.form.specEnabled != 1 && this.form.stockSet === '1' && this.form.stock !== "") {
-        const stockNum = parseFloat(this.form.stock);
-        if (isNaN(stockNum) || stockNum < 0) {
-          this.$msg('库存数量必须是大于等于0的数字!');
-          return false;
-        }
-      }
-
-      // 验证已售数量
-      if (this.form.specEnabled != 1 && this.form.sold !== "") {
-        const soldNum = parseFloat(this.form.sold);
-        if (isNaN(soldNum) || soldNum < 0) {
-          this.$msg('已售数量必须是大于等于0的数字!');
-          return false;
-        }
+      if (this.form.specEnabled == 1) {
+        return this.validateSpecList();
       }
-
-      // 验证花支数
-      if (this.form.specEnabled != 1 && this.form.flowerNum !== "" && this.form.flowerNum !== 0) {
-        const flowerNum = parseFloat(this.form.flowerNum);
-        if (isNaN(flowerNum) || flowerNum < 0) {
-          this.$msg('花支数必须是大于0的数字!');
-          return false;
-        }
-      }
-
-      // 验证重量(千克)
-      if (this.form.specEnabled != 1 && this.form.weight !== "") {
-        const weightNum = parseFloat(this.form.weight);
-        if (isNaN(weightNum) || weightNum < 0) {
-          this.$msg('重量必须是大于等于0的数字!');
-          return false;
-        }
-      }
-
-      let formData = e.detail.value;
-      let checkRes = form.validation(formData, rules);
-      if (!checkRes) {
-        this.confirmFn();
-      } else {
-        this.$msg(checkRes);
+      return this.validateGoodsNumberFields(this.form, '');
+    },
+    /**
+     * 表单提交入口。先校验再走确认保存,校验失败只提示第一条错误。
+     */
+    formSubmit() {
+      const err = this.getFormValidateError();
+      if (err) {
+        this.$msg(err);
+        return false;
       }
+      this.confirmFn();
     },
     confirmFn() {
       if (this.form.categoryIdList == 0) {

+ 12 - 500
hdApp/src/admin/goods/result.vue

@@ -1,308 +1,27 @@
+<!--
+  花店后台新增商品成功页
+  谁用:hdApp 管理端创建花束后跳转
+  解决问题:提示添加成功,并提供返回入口
+-->
 <template>
 	<view class="app-content">
 		<view class="billing_box_bg">
-			<view class="top-view"> </view>
+			<view class="top-view"></view>
 			<view class="result-view">
-			<view class="iconfont iconchenggong"></view>
-			<view class="result-title">添加成功</view>
-			<button class="admin-button-com middle blue" style="width:20vw;" @click="goBack">返回</button>
-			<view class="form-container">
-				<form>
-					<view class="module-com input-line-wrap">
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">名称</view>
-							<view>{{ goodsName }}</view>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">数量</view>
-							<input v-model="form.num" placeholder-class="phcolor" class="tui-input" name="num" @focus="form.num=''" placeholder="填写数量" maxlength="50" type="number" />
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false" :arrow="false" >
-							<view class="tui-title">单价</view>
-							<input type="digit" @focus="goodsPrice=''" style="width:260upx;" v-model="goodsPrice" placeholder-class="tui-placeholder" placeholder="填写金额" />
-							<text style="color:#3385FF;font-weight:bold;">合计 ¥<text>{{orderTotalPrice}}</text></text>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false" :arrow="false">
-							<view class="tui-title">收款方式</view>
-                			<button @click="form.hasPay = 4" class="admin-button-com middle" :class="form.hasPay == 4 ? 'blue' : 'default'">余额</button>
-                			<button @click="form.hasPay = 0" class="admin-button-com middle" :class="form.hasPay == 0 ? 'blue' : 'default'" style="margin-left:6upx;">扫码</button>
-							<button @tap="form.hasPay = 1" class="admin-button-com middle" :class="form.hasPay == 1 ? 'blue' : 'default'" style="margin-left:6upx;">线下</button>
-							<button @tap="form.hasPay = 2" class="admin-button-com middle" :class="form.hasPay == 2 ? 'blue' : 'default'" style="margin-left:6upx;">挂账</button>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :arrow="true" v-if="form.hasPay == 1">
-							<view class="tui-title">线下渠道</view>
-							<picker mode="selector" :value="form.payWay" :range="payWayList" range-key="name" @change="payWayChange" class="tui-input" >
-								<input v-model="form.payWay" name="payWay" type="text" hidden />
-								<view style="padding:6upx 0 6upx 0;">{{payWayList[payWayindex].name}}</view>
-							</picker>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">配送方式</view>
-							<button @tap="setSendType(0)" class="admin-button-com middle" :class="form.sendType == 0 ? 'blue' : 'default'">送货</button>
-							<button @tap="setSendType(1)" class="admin-button-com middle" :class="form.sendType == 1 ? 'blue' : 'default'" style="margin-left:6upx;">自取</button>
-							<button @click="setSendType(2)" class="admin-button-com middle" :class="form.sendType == 2 ? 'blue' : 'default'" style="margin-left:6upx;">跑腿</button>
-							<button @click="setSendType(3)" class="admin-button-com middle" :class="form.sendType == 3 ? 'blue' : 'default'" style="margin-left:6upx;">物流</button>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false" :arrow="true">
-							<view class="tui-title" v-if="form.sendType == 1">取花日期</view>
-							<view class="tui-title" v-else>配送日期</view>
-							<view class="uni-input" v-if="form.reachDate==''" @click="bindReachDateChange">
-								<view style="width:410upx;font-size:32upx;color:#CCCCCC;">今天</view>
-							</view>
-							<view class="uni-input" @click="bindReachDateChange">
-								<view style="width:410upx;font-size:32upx;">{{form.reachDate}}</view>
-							</view>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false" :arrow="true">
-							<view class="tui-title" v-if="form.sendType == 1">取花时间</view>
-							<view class="tui-title" v-else>配送时间</view>
-							<view class="uni-input" style="width:450upx;font-size:32upx;" @click="openTimePopup">{{form.reachPeriod}}</view>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">备注内容</view>
-							<input v-model="form.remark" placeholder-class="phcolor" class="tui-input" placeholder-style="color:#ccc" name="remark" placeholder="选填" />
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">客户</view>
-							<view>快捷开单</view>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false">
-							<view class="tui-title">制作</view>
-							<button @tap="form.needWork = 1" class="admin-button-com middle" :class="form.needWork == 1 ? 'blue' : 'default'">需要</button>
-							<button @tap="form.needWork = 0" class="admin-button-com middle" :class="form.needWork == 0 ? 'blue' : 'default'" style="margin-left:30upx;">不要</button>
-						</tui-list-cell>
-
-						<tui-list-cell class="line-cell" :hover="false" :arrow="false">
-							<view class="tui-title">打印小票</view>
-							<button @tap="form.needPrint = 2" class="admin-button-com middle" :class="form.needPrint == 2? 'blue' : 'default'">不打</button>
-							<button @click="form.needPrint = 1" class="admin-button-com middle" :class="form.needPrint == 1 ? 'blue' : 'default'" style="margin-left:30upx;">打印</button>
-						</tui-list-cell>
-
-				</view>
-			</form>
-			<view class="button-group">
-				<button class="admin-button-com big default" @click="goBack" style="width:200upx;">取消</button>
-				<button class="admin-button-com big blue" @click="confirmOrder" style="width:200upx;margin-left:50upx;">确认</button>
+				<view class="iconfont iconchenggong"></view>
+				<view class="result-title">添加成功</view>
+				<button class="admin-button-com middle blue" style="width:20vw;" @click="goBack">返回</button>
 			</view>
 		</view>
-		</view>
-	</view>
-
-	<!-- 配送时间选择弹框 -->
-	<uni-popup ref="timePopup" type="bottom" background-color="#fff">
-		<view class="time-picker-popup">
-			<view class="popup-header">
-				<text class="popup-title">选择配送时间</text>
-				<text class="popup-close" @click="closeTimePopup">×</text>
-			</view>
-			
-			<view class="time-sections">
-				<!-- 小时选择 -->
-				<view class="time-section">
-					<view class="section-title">小时</view>
-					<view class="hours-wrapper">
-						<scroll-view 
-							scroll-y="true" 
-							class="hours-scroll"
-							enhanced="true"
-							show-scrollbar="true"
-						>
-							<view class="hours-buttons">
-								<button 
-									v-for="(hour, index) in timeOptions[0]" 
-									:key="index"
-									class="time-button" 
-									:class="{ active: selectedHour == hour }"
-									@click="selectedHour = hour"
-								>{{ hour }}</button>
-							</view>
-						</scroll-view>
-					</view>
-				</view>
-				
-				<!-- 分钟选择 -->
-				<view class="time-section">
-					<view class="section-title">分钟</view>
-					<view class="minutes-wrapper">
-						<scroll-view 
-							scroll-y="true" 
-							class="minutes-scroll"
-							enhanced="true"
-							show-scrollbar="true"
-						>
-							<view class="minutes-buttons">
-								<button 
-									v-for="(minute, index) in timeOptions[1]" 
-									:key="index"
-									class="time-button" 
-									:class="{ active: selectedMinute == minute }"
-									@click="selectedMinute = minute"
-								>{{ minute }}</button>
-							</view>
-						</scroll-view>
-					</view>
-				</view>
-			</view>
-			
-			<!-- 操作按钮 -->
-			<view class="popup-footer">
-				<button class="footer-button default" @click="closeTimePopup">取消</button>
-				<button class="footer-button blue" @click="confirmTime">确认</button>
-			</view>
-		</view>
-	</uni-popup>
-
-	<!-- 配送日期选择器蒙板 -->
-	<view v-if="reachDatePickerShow" class="date-picker-mask" @click="reachDatePickerShow = false"></view>
-
-	<mx-date-picker :show="reachDatePickerShow" format="yyyy-mm-dd" type="date" :value="reachDatePickerValue" :show-tips="true" @confirm="confirmReachDatePicker" @cancel="reachDatePickerShow = false" />
 	</view>
 </template>
 <script>
-import TuiListCell from "@/components/plugin/list-cell";
-import { createOrder } from "@/api/order";
-import { mapGetters } from "vuex";
-import MxDatePicker from '@/components/mx-datepicker/mx-datepicker.vue';
-
 export default {
 	name: "result",
-	components: {
-		TuiListCell,
-		MxDatePicker
-	},
-		data() {
-			return {
-				form: {
-					hasPay: 4,
-					payWay: 0,
-					num: 1,
-					sendType: 0,
-					reachDate: '',
-					reachPeriod: "12:00",
-					remark: '',
-					needWork:1,
-					needPrint:2
-				},
-				goodsPrice: '',
-				payWayindex: 0,
-				payWayList: [
-					{name: "线下微信", id: 0},
-					{name: "线下支付宝", id: 1},
-					{name: "现金", id: 4},
-					{name: "银行卡", id: 5}
-				],
-				timeOptions: [
-					['00', '01', '02','03','04','05','06','07','08','09','10','11','12','13', '14','15','16','17','18','19','20','21','22','23'],
-					['00', '05', '10','15','20','25','30','35','40','45','50','55']
-				],
-				goodsName:'',
-				goodsId:0,
-				selectedHour: '12', // 选中的小时
-				selectedMinute: '00', // 选中的分钟
-				reachDatePickerShow: false,
-				reachDatePickerValue: ''
-			};
-		},
-	computed: {
-		...mapGetters(["getLoginInfo", "getMyShopInfo"]),
-		orderTotalPrice() {
-			let price = 0;
-			if (Number(this.goodsPrice) > 0 && Number(this.form.num) > 0) {
-				price = Number(this.goodsPrice) * Number(this.form.num);
-				price = parseFloat(price.toFixed(2));
-			}
-			return price;
-		}
-	},
-	onLoad(options) {
-		this.goodsName = options.name?options.name:''
-		this.goodsId = options.id?options.id:0
-	},
 	methods: {
-		init() {
-
-		},
-		setSendType (type) {
-			this.form.sendType = type
-		},
+		// 返回上一页,关闭当前成功提示
 		goBack() {
 			uni.navigateBack({})
-		},
-		confirmOrder() {
-			let product = [{productId:this.goodsId,bigNum:this.form.num,smallNum:null,property:0,num:this.form.num,unitType:0,unitPrice:this.goodsPrice}]
-			let json = JSON.stringify(product)
-			let customId = this.getMyShopInfo && Number(this.getMyShopInfo.defaultCustomId)>0 ? this.getMyShopInfo.defaultCustomId : 0
-			if(Number(customId)<=0){
-				this.$msg('请设置快捷开单的客户')
-				return false
-			}
-			//dealPrice=1 表示花束卖的价跟原价不一致,也要强制提交
-      		let params = {...this.form,modifyPrice: this.orderTotalPrice,version:3,orderType:1,product:json,customId:customId,dealPrice:1};
-
-			this.$util.confirmModal({content:'确认提交'},() => {        
-				uni.showLoading({mask:true})
-				createOrder(params).then((res) => {
-					uni.hideLoading()
-					if(res.code == 1){
-						const {data: { id, orderSn,actPrice }} = res;
-						if(this.form.hasPay == 0){
-							this.$util.pageTo({ url: '/admin/billing/toPay',type:2,query: {orderId:id,orderSn:orderSn,actPrice:actPrice}})
-						}else{
-							this.$util.pageTo({ url: '/admin/billing/result?orderId='+id,type:2})					
-						}
-					}
-				})
-			})
-		},
-		payWayChange(e) {
-			let index = e.detail.value;
-			this.payWayindex = index;
-			this.form.payWay = this.payWayList[index].id;
-		},
-		selTimeFn(e) {
-			this.form.reachDate = e.detail.value;
-		},
-		bindReachDateChange(){
-			this.reachDatePickerShow = true
-			this.reachDatePickerValue = this.form.reachDate
-		},
-		confirmReachDatePicker(e) {
-			this.form.reachDate = e.value
-			this.reachDatePickerShow = false
-		},
-		bindTimeChange(event) {
-			let val = event.detail.value;
-			let before = Number(val[0]);
-			let after = Number(val[1]);
-			let timeOptions = this.timeOptions;
-			this.form.reachPeriod = timeOptions[0][before]+':'+timeOptions[1][after];
-		},
-		openTimePopup() {
-			// 初始化为当前选中的时间
-			const [hour, minute] = this.form.reachPeriod.split(':')
-			this.selectedHour = hour
-			this.selectedMinute = minute
-			this.$refs.timePopup.open()
-		},
-		// 关闭时间选择弹框
-		closeTimePopup() {
-			this.$refs.timePopup.close()
-		},
-		// 确认时间选择
-		confirmTime() {
-			this.form.reachPeriod = `${this.selectedHour}:${this.selectedMinute}`
-			this.closeTimePopup()
 		}
 	}
 };
@@ -321,7 +40,7 @@ export default {
 		left: 0;
 		width: 100%;
 		height: 299upx;
-		background: #3385ff;
+		background: #09C567;
 		border-radius: 0upx 0upx 83upx 83upx;
 	}
 	.result-view {
@@ -342,213 +61,6 @@ export default {
 			margin: 10upx 0 10upx;
 			font-size: 32upx;
 		}
-		.form-container {
-			width: 100%;
-			.line-cell {
-				.tui-title {
-					width: 210upx;
-					color: $fontColor2;
-				}
-				.tui-input {
-					width: calc(100% - 210upx);
-					font-size: 28upx;
-				}
-				.phcolor {
-					color: #ccc;
-				}
-			}
-			.button-group {
-				display: flex;
-				justify-content: center;
-				align-items: center;
-				margin-top: 40upx;
-				padding: 0 30upx;
-			}
-		}
-	}
-}
-
-/* 时间选择弹窗样式 */
-.time-picker-popup {
-	width: 100%;
-	background: #fff;
-	border-radius: 20upx 20upx 0 0;
-	display: flex;
-	flex-direction: column;
-	max-height: 94vh;
-	padding: 0 0 120upx 0;
-}
-
-.popup-header {
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	margin-bottom: 0;
-	padding: 30upx 20upx 20upx 20upx;
-	border-bottom: 2upx solid #f0f2f6;
-	flex-shrink: 0;
-}
-
-.popup-title {
-	font-size: 36upx;
-	color: #333;
-	font-weight: bold;
-}
-
-.popup-close {
-	font-size: 48upx;
-	color: #999;
-	line-height: 1;
-	cursor: pointer;
-}
-
-.time-sections {
-	display: flex;
-	flex-direction: column;
-	gap: 20upx;
-	margin-bottom: 0;
-	flex: 1;
-	padding: 20upx 20upx 0 20upx;
-	overflow: hidden;
-}
-
-.time-section {
-	flex: 1;
-	display: flex;
-	flex-direction: column;
-	min-height: 200upx;
-	
-	.section-title {
-		font-size: 28upx;
-		color: #333;
-		font-weight: 600;
-		margin-bottom: 12upx;
-		text-align: left;
-	}
-}
-
-.hours-wrapper {
-	width: 100%;
-	flex: 1;
-	background: #f8f9fa;
-	border: 2upx solid #e9ecef;
-	border-radius: 8upx;
-	position: relative;
-	overflow: hidden;
-	display: flex;
-	flex-direction: column;
-	min-height: 150upx;
-}
-
-.minutes-wrapper {
-	width: 100%;
-	flex: 1;
-	background: #f8f9fa;
-	border: 2upx solid #e9ecef;
-	border-radius: 8upx;
-	position: relative;
-	overflow: hidden;
-	display: flex;
-	flex-direction: column;
-	min-height: 150upx;
-}
-
-.hours-scroll,
-.minutes-scroll {
-	width: 100%;
-	height: 100%;
-	padding: 8upx;
-	box-sizing: border-box;
-}
-
-.hours-buttons,
-.minutes-buttons {
-	display: flex;
-	flex-wrap: wrap;
-	gap: 8upx;
-	justify-content: flex-start;
-}
-
-.time-button {
-	width: calc(25% - 6upx);
-	height: 73upx;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	background: #ffffff;
-	border: 2upx solid #e9ecef;
-	border-radius: 6upx;
-	font-size: 26upx;
-	color: #666;
-	box-sizing: border-box;
-	transition: all 0.2s;
-	flex-shrink: 0;
-	
-	&.active {
-		background: #3385FF;
-		border-color: #3385FF;
-		color: #fff;
-		font-weight: 500;
-	}
-	
-	&:active {
-		background: #e8f5ff;
-	}
-}
-
-.popup-footer {
-	display: flex;
-	gap: 12upx;
-	padding: 15upx 20upx 20upx 20upx;
-	border-top: 2upx solid #f0f2f6;
-	flex-shrink: 0;
-	
-	.footer-button {
-		flex: 1;
-		height: 93upx;
-		display: flex;
-		align-items: center;
-		justify-content: center;
-		border: none;
-		border-radius: 8upx;
-		font-size: 28upx;
-		font-weight: 500;
-		transition: all 0.2s;
-		
-		&.default {
-			background: #f5f5f5;
-			color: #666;
-		}
-		
-		&.blue {
-			background: #3385FF;
-			color: #fff;
-		}
-		
-		&:active {
-			opacity: 0.8;
-		}
-	}
-}
-
-/* 日期选择器蒙板 */
-.date-picker-mask {
-	position: fixed;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	background-color: rgba(0, 0, 0, 0.5);
-	z-index: 98;
-	animation: fadeIn 0.3s ease-in-out;
-}
-
-@keyframes fadeIn {
-	from {
-		opacity: 0;
-	}
-	to {
-		opacity: 1;
 	}
 }
-</style>
+</style>

+ 3 - 2
hdApp/src/admin/homePageConfig/groupBuyGoodsEdit.vue

@@ -1,7 +1,8 @@
 <!--
   团购商品编辑页
   从团购配置页跳转,单选活动商品并填写团购价/成团人数/库存等;通过 groupBuyDraft 与列表页同步。
-  单规格:一套表单;多规格:按规格数动态生成多套表单,保存时每规格各写一条团购商品(goodsId 用子规格 id)。
+  单规格:一套表单;多规格:按 specList 动态生成多套表单,保存时每规格各写一条团购商品。
+  多规格 goodsId:首规格用主商品 id,其余用子规格 id。
   自动退款不再可配,统一默认开启(活动结束未拼成自动退款、拼成自动退差价)。
 -->
 <template>
@@ -313,7 +314,7 @@ export default {
         this.goodsBase.name = data.name || this.goodsBase.name
         this.goodsBase.cover = cover
         const specList = Array.isArray(data.specList) ? data.specList : []
-        // 启用多规格且存在子规格:一套规格一张填写表单
+        // 启用多规格:specList 首项是主记录自身(id=主商品id),其后是子规格
         if (Number(data.specEnabled) === 1 && specList.length > 0) {
           this.isMultiSpec = true
           this.specForms = specList.map((spec) => ({

+ 1 - 1
hdApp/src/admin/homePageConfig/seckill.vue

@@ -222,7 +222,7 @@ export default {
     },
     /** 将接口数据规范为表单结构 */
     normalizeForm(data) {
-      // specName 仅草稿/编辑页使用;多规格保存时每规格一条,goodsId 为子规格 id
+      // specName 仅草稿/编辑页使用;多规格保存时每规格一条,首规格 goodsId 为主商品 id
       const goods = Array.isArray(data.goods) ? data.goods.map((g) => ({
         goodsId: g.goodsId || 0,
         price: g.price || '',

+ 4 - 3
hdApp/src/admin/homePageConfig/seckillGoodsEdit.vue

@@ -1,7 +1,8 @@
 <!--
   秒杀商品编辑页
   从秒杀配置页跳转,单选活动商品并填写秒杀价/库存/限购;通过 seckillDraft 与列表页同步。
-  单规格:一套表单;多规格:按规格数动态生成多套表单,保存时每规格各写一条秒杀商品(goodsId 用子规格 id)。
+  单规格:一套表单;多规格:按 specList 动态生成多套表单,保存时每规格各写一条秒杀商品。
+  多规格 goodsId:首规格用主商品 id,其余用子规格 id。
 -->
 <template>
   <view class="page-container">
@@ -239,7 +240,7 @@ export default {
         this.goodsBase.name = data.name || this.goodsBase.name
         this.goodsBase.cover = cover
         const specList = Array.isArray(data.specList) ? data.specList : []
-        // 启用多规格且存在子规格:一套规格一张填写表单
+        // 启用多规格:specList 首项是主记录自身(id=主商品id),其后是子规格
         if (Number(data.specEnabled) === 1 && specList.length > 0) {
           this.isMultiSpec = true
           this.specForms = specList.map((spec) => ({
@@ -272,7 +273,7 @@ export default {
     },
     /**
      * 校验全部规格表单后写回 seckillDraft
-     * 多规格保存为多条(每规格 goodsId 为子商品 id,后端按该 id 校验实际库存)
+     * 多规格保存为多条:首规格 goodsId 为主商品 id,其余为子规格 id
      */
     saveFn() {
       if (!this.goodsBase.masterGoodsId && !(this.specForms[0] && this.specForms[0].goodsId)) {

+ 2 - 2
mallApp/src/components/home/goodsSection.vue

@@ -126,10 +126,10 @@ export default {
     goDetail(g) {
       this.pageTo({ url: `/pages/goods/detail?id=${g.id}&account=${this.account}&hdId=${this.hdId}` })
     },
-    /** 单规格直接加购,多规格跳转详情选规格;无价格商品拦截并提示询价 */
+    /** 单规格直接加购,多规格(goodsStyle=1)跳转详情选规格;无价格商品拦截并提示询价 */
     handleCartClick(g) {
       if (!g || !g.id) return
-      if (Number(g.specEnabled) === 1) {
+      if (Number(g.goodsStyle) === 1 || Number(g.specEnabled) === 1) {
         this.goDetail(g)
         return
       }

+ 10 - 13
mallApp/src/pages/goods/components/sel-popup.vue

@@ -132,10 +132,12 @@ export default {
     maxStock() {
       const spec = this.info.specEnabled == 1 && this.info.specList ? this.info.specList[this.selIndex] : this.info;
       let max = Number(spec && spec.stockSet == 1 ? spec.stock : 9999) || 9999;
-      const goodsStock = Number(this.info && this.info.stock);
-      // 商品本身有库存时,数量上限不超过 data.stock(未开启库存限制时后端会写成 9999)
-      if (goodsStock > 0) {
-        max = Math.min(max, goodsStock);
+      // 多规格按当前规格库存限制;主记录承载首规格后,不能再用主记录库存截断其他规格
+      if (this.info.specEnabled != 1) {
+        const goodsStock = Number(this.info && this.info.stock);
+        if (goodsStock > 0) {
+          max = Math.min(max, goodsStock);
+        }
       }
       if (this.isSeckill) {
         const actStock = Number(this.activityStock) || 0;
@@ -212,19 +214,14 @@ export default {
         return false;
       }
       const sale = spec || this.info || {};
-      const goodsStock = Number(this.info && this.info.stock);
-      const specStock = Number(sale.stock);
+      const saleStock = Number(sale.stock);
       const stockSet = Number(sale.stockSet != null ? sale.stockSet : (this.info && this.info.stockSet));
-      // 商品库存不足,或本次数量超过库存
-      if (!(goodsStock > 0) || (stockSet === 1 && !(specStock > 0))) {
-        this.$msg('库存不足哦');
-        return false;
-      }
-      if (stockSet === 1 && buyNum > specStock) {
+      // 按当前售卖 SKU 校验库存:多规格时不能拿主记录(首规格)库存去卡其他规格
+      if (!(saleStock > 0) || (stockSet === 1 && buyNum > saleStock)) {
         this.$msg('库存不足哦');
         return false;
       }
-      if (goodsStock > 0 && buyNum > goodsStock) {
+      if (stockSet !== 1 && buyNum > saleStock) {
         this.$msg('库存不足哦');
         return false;
       }

+ 22 - 2
mallApp/src/pages/goods/detail.vue

@@ -1,3 +1,8 @@
+<!--
+  商城商品详情
+  谁用:mallApp 顾客查看花束并加购/购买
+  解决问题:多规格 specList 首项即主记录可售规格,选中后 specGoodsId 可等于主商品 id
+-->
 <template>
   <view class="page-container">
     <scroll-view class="app-content" scroll-y="true" @scroll="scrollFn">
@@ -27,9 +32,9 @@
           <view class="tui-pro-title-wrap">
             <view class="tui-pro-title app-size-32">{{ data.name }}</view>
             <view
-              v-if="data.masterId && data.masterId !== '0'"
+              v-if="displaySpecName"
               class="shop-spec app-color-3"
-            >{{ data.specName }}</view>
+            >{{ displaySpecName }}</view>
           </view>
           <button open-type="share" class="share-button" @click="shareFn">
             <i class="iconfont iconfenxiang"></i>
@@ -258,6 +263,21 @@ export default {
       });
       return count;
     },
+    /**
+     * 标题下规格名:多规格展示当前选中规格(默认首规格),规格 SKU 详情展示自身 specName
+     */
+    displaySpecName() {
+      if (this.data.selectedSpecName) {
+        return this.data.selectedSpecName;
+      }
+      if (Number(this.data.specEnabled) === 1 && this.data.specList && this.data.specList[0]) {
+        return this.data.specList[0].specName || '';
+      }
+      if (this.data.masterId && String(this.data.masterId) !== '0') {
+        return this.data.specName || '';
+      }
+      return '';
+    },
   },
   methods: {
     /** 按登录态驱动 wangCg:0 未登录弹层 / 1 已登录关闭 */

+ 3 - 3
mallApp/src/pages/goods/list.vue

@@ -568,13 +568,13 @@ export default {
       })
     },
     /**
-     * 列表加购:单规格(普通)直接加入购物车;多规格跳转详情选规格后再加购
-     * @param {Object} item 列表商品行(含 specEnabled / price)
+     * 列表加购:单规格直接加入购物车;多规格(goodsStyle=1)跳转详情选规格后再加购
+     * @param {Object} item 列表商品行(含 goodsStyle / specEnabled / price)
      */
     handleCartClick(item) {
       if (!item || !item.id) return
       // 多规格:进入详情页选择规格
-      if (Number(item.specEnabled) === 1) {
+      if (Number(item.goodsStyle) === 1 || Number(item.specEnabled) === 1) {
         this.goDetail(item)
         return
       }

+ 3 - 3
mallApp/src/pages/goods/section-list.vue

@@ -224,13 +224,13 @@ export default {
       this.pageTo({ url })
     },
     /**
-     * 列表加购:单规格(普通)直接加入购物车;多规格跳转详情选规格后再加购
-     * @param {Object} item 列表商品行(含 specEnabled / price)
+     * 列表加购:单规格直接加入购物车;多规格(goodsStyle=1)跳转详情选规格后再加购
+     * @param {Object} item 列表商品行(含 goodsStyle / specEnabled / price)
      */
     handleCartClick(item) {
       if (!item || !(item.id || item.goodsId)) return
       // 多规格:进入详情页选择规格
-      if (Number(item.specEnabled) === 1) {
+      if (Number(item.goodsStyle) === 1 || Number(item.specEnabled) === 1) {
         this.goDetail(item)
         return
       }

+ 2 - 2
mallApp/src/pages/home/shop-category.vue

@@ -876,12 +876,12 @@ export default {
         }
       })
     },
-    /** 花束加购:单规格直接加入购物车,多规格跳转详情选规格 */
+    /** 花束加购:单规格直接加入购物车,多规格(goodsStyle=1)跳转详情选规格 */
     handleBouquetAddToCart(item) {
       if (!item || !item.id) {
         return
       }
-      if (Number(item.specEnabled) === 1) {
+      if (Number(item.goodsStyle) === 1 || Number(item.specEnabled) === 1) {
         this.toBouquetDetail(item)
         return
       }