瀏覽代碼

feat(admin): SKU编辑器移除关联商品模式(旧关联自动转单条组合)、组合弹窗高度自适应窗口、修复单选框v-model绑定

Xiaogang Liao 1 周之前
父節點
當前提交
48cf2ceb07

+ 40 - 0
cfc-web/src/__tests__/components/ProductPicker.radio.test.js

@@ -0,0 +1,40 @@
+import { mount, createLocalVue } from "@vue/test-utils"
+import ElementUI from "element-ui"
+import ProductPicker from "@/views/admin/components/ProductPicker.vue"
+
+jest.mock("@/api/admin", () => ({
+  linkedProductList: jest.fn().mockResolvedValue({
+    code: 200,
+    data: {
+      total: 2,
+      list: [
+        { id: 11, name: "商品A", price: 1000, stock: 10, hasSkus: false, skus: [] },
+        { id: 12, name: "商品B", price: 2000, stock: 20, hasSkus: false, skus: [] }
+      ]
+    }
+  })
+}))
+
+const localVue = createLocalVue()
+localVue.use(ElementUI)
+
+const flush = () => new Promise(r => setTimeout(r, 60))
+
+describe("ProductPicker 单选(关联商品)模式", () => {
+  it("点击单选框后该radio应显示勾选态(is-checked)", async () => {
+    const wrapper = mount(ProductPicker, {
+      localVue,
+      propsData: { multiple: false, crossCategory: false, selected: null },
+      attachTo: document.body
+    })
+    wrapper.vm.open()
+    await flush()
+    const radios = wrapper.findAll(".el-radio")
+    expect(radios.length).toBe(2)
+    const radio = radios.at(0)
+    await radio.trigger("click")
+    await wrapper.vm.$nextTick()
+    expect(radio.classes()).toContain("is-checked")
+    wrapper.destroy()
+  })
+})

+ 30 - 0
cfc-web/src/__tests__/components/ProductSkuEditor.mode.test.js

@@ -0,0 +1,30 @@
+import { mount, createLocalVue } from "@vue/test-utils"
+import ElementUI from "element-ui"
+import ProductSkuEditor from "@/views/admin/components/ProductSkuEditor.vue"
+
+jest.mock("@/api/admin", () => ({
+  getProductSkuList: jest.fn().mockResolvedValue({ code: 200, data: [] }),
+  createProductSku: jest.fn(),
+  updateProductSku: jest.fn(),
+  deleteProductSku: jest.fn()
+}))
+
+const localVue = createLocalVue()
+localVue.use(ElementUI)
+
+describe("ProductSkuEditor 模式选择", () => {
+  it("添加SKU时不再提供「关联商品」单选模式", async () => {
+    const wrapper = mount(ProductSkuEditor, {
+      localVue,
+      propsData: { productId: null },
+      stubs: { AssetPicker: true, ProductPicker: true },
+      attachTo: document.body
+    })
+    wrapper.vm.showDialog()
+    await wrapper.vm.$nextTick()
+    const buttons = wrapper.findAll(".el-radio-button").wrappers.map(w => w.text())
+    expect(buttons).not.toContain("关联商品")
+    expect(buttons).toEqual(expect.arrayContaining(["规格变体", "商品组合"]))
+    wrapper.destroy()
+  })
+})

+ 5 - 2
cfc-web/src/views/admin/components/ProductPicker.vue

@@ -16,13 +16,14 @@
       :selectable="rowSelectable"
       :selectable="rowSelectable"
       highlight-current-row
       highlight-current-row
       size="small"
       size="small"
+      :max-height="tableMaxHeight"
       @current-change="onSelect"
       @current-change="onSelect"
       @selection-change="onSelectionChange"
       @selection-change="onSelectionChange"
     >
     >
       <el-table-column v-if="multiple" type="selection" width="42" :reserve-selection="true" />
       <el-table-column v-if="multiple" type="selection" width="42" :reserve-selection="true" />
       <el-table-column v-else width="50">
       <el-table-column v-else width="50">
         <template slot-scope="{ row }">
         <template slot-scope="{ row }">
-          <el-radio :value="selectedId" :label="row.id" @change="onSelect(row)">&nbsp;</el-radio>
+          <el-radio v-model="selectedId" :label="row.id" @change="onSelect(row)">&nbsp;</el-radio>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
 
 
@@ -106,7 +107,8 @@ export default {
       query: { keyword: '', page: 1 },
       query: { keyword: '', page: 1 },
       selectedProduct: null,
       selectedProduct: null,
       picked: [],
       picked: [],
-      _pickedMap: {}
+      _pickedMap: {},
+      tableMaxHeight: 400
     }
     }
   },
   },
   computed: {
   computed: {
@@ -119,6 +121,7 @@ export default {
   methods: {
   methods: {
     open() {
     open() {
       this.visible = true
       this.visible = true
+      this.tableMaxHeight = Math.max(320, (window.innerHeight || 900) - 300)
       this.query.page = 1
       this.query.page = 1
       this.selectedProduct = this.multiple ? null : (this.selected || null)
       this.selectedProduct = this.multiple ? null : (this.selected || null)
       // 多选:用 selected 预填 picked,load() 后对已选行回勾
       // 多选:用 selected 预填 picked,load() 后对已选行回勾

+ 21 - 83
cfc-web/src/views/admin/components/ProductSkuEditor.vue

@@ -49,30 +49,11 @@
       <el-form ref="skuForm" :model="skuForm" :rules="skuRules" label-width="80px">
       <el-form ref="skuForm" :model="skuForm" :rules="skuRules" label-width="80px">
         <el-radio-group v-model="skuMode" size="small" class="mb-16">
         <el-radio-group v-model="skuMode" size="small" class="mb-16">
           <el-radio-button label="spec">规格变体</el-radio-button>
           <el-radio-button label="spec">规格变体</el-radio-button>
-          <el-radio-button label="linked">关联商品</el-radio-button>
           <el-radio-button label="combo">商品组合</el-radio-button>
           <el-radio-button label="combo">商品组合</el-radio-button>
         </el-radio-group>
         </el-radio-group>
 
 
-        <!-- 关联商品模式 -->
-        <template v-if="skuMode === 'linked'">
-          <el-form-item label="显示标签">
-            <el-input v-model="skuForm.label" placeholder="默认使用关联商品名称" />
-          </el-form-item>
-          <el-form-item label="关联商品">
-            <el-button size="small" @click="openProductPicker">选择商品</el-button>
-            <div v-if="skuForm.linkedProductId" class="linked-card">
-              <img v-if="skuForm.linkedProductImage" :src="skuForm.linkedProductImage" class="linked-thumb" />
-              <div>
-                <div>{{ skuForm.linkedProductName }}</div>
-                <div class="linked-info">¥{{ skuForm.linkedProductPrice ? (skuForm.linkedProductPrice/100).toFixed(2) : '0.00' }} | 库存 {{ skuForm.linkedProductStock }}</div>
-              </div>
-              <el-button size="mini" type="text" @click="openProductPicker">更换</el-button>
-            </div>
-          </el-form-item>
-        </template>
-
         <!-- 规格变体模式(原有) -->
         <!-- 规格变体模式(原有) -->
-        <template v-else-if="skuMode === 'spec'">
+        <template v-if="skuMode === 'spec'">
         <el-form-item label="规格" prop="specs">
         <el-form-item label="规格" prop="specs">
           <el-input v-model="skuForm.specs" placeholder="例如:颜色:红色;尺码:M" />
           <el-input v-model="skuForm.specs" placeholder="例如:颜色:红色;尺码:M" />
         </el-form-item>
         </el-form-item>
@@ -275,23 +256,22 @@ export default {
           price: it.childProductPrice || 0,
           price: it.childProductPrice || 0,
           childSkuSpecs: it.childSkuSpecs
           childSkuSpecs: it.childSkuSpecs
         }))
         }))
-        this.skuMode = comboRaw.length ? 'combo' : (sku.linkedProductId != null ? 'linked' : 'spec')
-        if (this.skuMode === 'linked') {
-          this.skuForm = {
-            label: sku.label || '',
-            linkedProductId: sku.linkedProductId,
-            linkedProductName: sku.linkedProductName || '',
-            linkedProductPrice: sku.linkedProductPrice || 0,
-            linkedProductImage: sku.linkedProductImage || '',
-            linkedProductStock: sku.linkedProductStock || 0,
-            enabled: sku.enabled !== undefined ? (sku.enabled ? 1 : 0) : 1,
-            specs: '',
-            price: 0,
-            originalPrice: 0,
-            stock: 0,
-            image: ''
-          }
-        } else if (this.skuMode === 'spec') {
+        if (sku.linkedProductId != null && !comboRaw.length) {
+          // 旧「关联商品」SKU → 转换为单条商品组合(保留关联,不丢数据)
+          this.skuMode = 'combo'
+          this.comboItems = [{
+            childProductId: sku.linkedProductId,
+            childSkuId: null,
+            quantity: 1,
+            name: sku.linkedProductName || ('商品#' + sku.linkedProductId),
+            coverImage: sku.linkedProductImage || '',
+            price: sku.linkedProductPrice || 0,
+            childSkuSpecs: ''
+          }]
+        } else {
+          this.skuMode = comboRaw.length ? 'combo' : 'spec'
+        }
+        if (this.skuMode === 'spec') {
           this.skuForm = {
           this.skuForm = {
             specs: this.formatSpecsForInput(sku.specs),
             specs: this.formatSpecsForInput(sku.specs),
             price: sku.price ? (sku.price / 100) : 0,
             price: sku.price ? (sku.price / 100) : 0,
@@ -321,6 +301,10 @@ export default {
             linkedProductImage: '',
             linkedProductImage: '',
             linkedProductStock: 0
             linkedProductStock: 0
           }
           }
+          if (sku.linkedProductId != null && !comboRaw.length) {
+            if (!this.skuForm.price) this.skuForm.price = (sku.linkedProductPrice || 0) / 100
+            if (!this.skuForm.stock) this.skuForm.stock = sku.linkedProductStock || 0
+          }
         }
         }
       } else {
       } else {
         this.editingId = null
         this.editingId = null
@@ -475,52 +459,6 @@ export default {
       }))
       }))
     },
     },
     async handleSave() {
     async handleSave() {
-      // Linked SKU 模式不校验 specs/price/stock
-      if (this.skuMode === 'linked') {
-        if (!this.skuForm.linkedProductId) {
-          this.$message.warning('请选择关联商品')
-          return
-        }
-        this.saving = true
-        try {
-          const payload = {
-            productId: this.productId,
-            linkedProductId: this.skuForm.linkedProductId,
-            label: this.skuForm.label || null,
-            enabled: this.skuForm.enabled,
-            comboItems: []
-          }
-          if (!this.productId) {
-            payload.id = Date.now()
-            delete payload.productId
-            this.pendingSkus.push(payload)
-            this.dialogVisible = false
-            this.$message.success('添加成功,保存商品时将自动创建SKU')
-            this.saving = false
-            return
-          }
-          let res
-          if (this.editingId) {
-            payload.id = this.editingId
-            delete payload.productId
-            res = await updateProductSku(payload)
-          } else {
-            res = await createProductSku(payload)
-          }
-          if (res.code === 200) {
-            await this.loadSkus()
-            this.$message.success('保存成功')
-            this.dialogVisible = false
-          } else {
-            this.$message.error(res.message || '保存失败')
-          }
-        } catch (e) {
-          this.$message.error('保存失败')
-        } finally {
-          this.saving = false
-        }
-        return
-      }
       // 商品组合模式:手填售价 + 组合明细(多商品+数量),不校验 specs
       // 商品组合模式:手填售价 + 组合明细(多商品+数量),不校验 specs
       if (this.skuMode === 'combo') {
       if (this.skuMode === 'combo') {
         if (!this.comboItems.length) { this.$message.warning('请至少添加一个组合商品'); return }
         if (!this.comboItems.length) { this.$message.warning('请至少添加一个组合商品'); return }