Просмотр исходного кода

fix(web-admin): allow multi-brand chips when plan brand is required

Treat required like any in BrandQtyChipsField so preference brand is not locked.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 месяцев назад
Родитель
Сommit
b28cf4a335
1 измененных файлов с 64 добавлено и 37 удалено
  1. 64 37
      web-admin/src/components/business/brand-qty-chips-field/index.vue

+ 64 - 37
web-admin/src/components/business/brand-qty-chips-field/index.vue

@@ -1,9 +1,7 @@
 <template>
   <div v-if="brandMode" class="brand-qty-chips" :class="`brand-qty-chips--${variant}`">
     <div class="brand-qty-chips__head">
-      <h4 class="brand-qty-chips__title">{{
-        brandMode === 'required' ? '指定品牌填量' : '选择品牌并填量'
-      }}</h4>
+      <h4 class="brand-qty-chips__title">选择品牌并填量</h4>
       <span
         class="brand-qty-chips__sum"
         :class="{ 'brand-qty-chips__sum--ok': sumMatch, 'brand-qty-chips__sum--bad': !sumMatch }"
@@ -12,7 +10,10 @@
       </span>
     </div>
 
-    <div v-if="brandMode === 'any'" class="brand-qty-chips__chips">
+    <div
+      v-if="brandMode === 'any' || brandMode === 'required'"
+      class="brand-qty-chips__chips"
+    >
       <button
         v-for="opt in brandOptions"
         :key="opt.brandId"
@@ -29,12 +30,6 @@
       </button>
     </div>
 
-    <div v-else-if="brandMode === 'required'" class="brand-qty-chips__chips">
-      <span class="brand-qty-chips__chip brand-qty-chips__chip--on brand-qty-chips__chip--locked">
-        {{ requiredBrandLabel }}
-      </span>
-    </div>
-
     <div class="brand-qty-chips__rows">
       <div v-for="row in activeRows" :key="row.brandId" class="brand-qty-chips__row">
         <span class="brand-qty-chips__row-label">{{ row.brandName }}</span>
@@ -52,15 +47,14 @@
         </template>
         <template v-else>
           <input
-            type="number"
+            type="text"
             inputmode="decimal"
-            step="0.001"
-            min="0"
             class="brand-qty-chips__qty-input"
-            :value="row.qty === 0 ? '' : row.qty"
+            :value="qtyDisplay(row.brandId)"
             :disabled="disabled"
             placeholder="0"
             @input="onMobileQtyInput(row.brandId, $event)"
+            @blur="onMobileQtyBlur(row.brandId)"
           />
         </template>
         <span v-if="unit" class="brand-qty-chips__unit">{{ unit }}</span>
@@ -107,23 +101,7 @@
     'update:actualQty': [value: number]
   }>()
 
-  const requiredBrandLabel = computed(() => {
-    const fromOptions = props.brandOptions.find((o) => o.brandId === props.requiredBrandId)
-    if (fromOptions?.brandName) return fromOptions.brandName
-    const fromRows = props.modelValue.find((r) => r.brandId === props.requiredBrandId)
-    return fromRows?.brandName || '指定品牌'
-  })
-
-  const activeRows = computed(() => {
-    if (props.brandMode === 'required') {
-      const id = props.requiredBrandId || props.brandOptions[0]?.brandId || ''
-      const existing = props.modelValue.find((r) => r.brandId === id)
-      if (existing) return [existing]
-      const opt = props.brandOptions.find((o) => o.brandId === id)
-      return [{ brandId: id, brandName: opt?.brandName || requiredBrandLabel.value, qty: 0 }]
-    }
-    return props.modelValue
-  })
+  const activeRows = computed(() => props.modelValue)
 
   function isSelected(brandId: string) {
     return props.modelValue.some((r) => r.brandId === brandId)
@@ -160,15 +138,37 @@
   }
 
   function toggleChip(opt: BrandOption) {
-    if (props.disabled || props.brandMode !== 'any') return
+    if (props.disabled || (props.brandMode !== 'any' && props.brandMode !== 'required')) return
     const selected = isSelected(opt.brandId)
     if (selected) {
+      clearQtyDraft(opt.brandId)
       emitRows(props.modelValue.filter((r) => r.brandId !== opt.brandId))
       return
     }
     emitRows([...props.modelValue, { brandId: opt.brandId, brandName: opt.brandName, qty: 0 }])
   }
 
+  /** 输入中草稿:保留「20.」「20.0」等中间态,避免 Number() 立刻收成 20 */
+  const qtyDrafts = ref<Record<string, string>>({})
+
+  /** 最多 3 位小数的非负数量中间态(允许空、单独小数点、尾随小数点) */
+  const QTY_DRAFT_RE = /^\d*\.?\d{0,3}$/
+
+  function qtyDisplay(brandId: string) {
+    if (Object.prototype.hasOwnProperty.call(qtyDrafts.value, brandId)) {
+      return qtyDrafts.value[brandId]
+    }
+    const qty = qtyOf(brandId)
+    return qty === 0 ? '' : String(qty)
+  }
+
+  function parseQtyDraft(raw: string): number {
+    if (raw === '' || raw === '.') return 0
+    const n = Number(raw)
+    if (Number.isNaN(n) || n < 0) return 0
+    return Number(n.toFixed(3))
+  }
+
   function setQty(brandId: string, qty: number) {
     const next = props.modelValue.map((r) =>
       r.brandId === brandId ? { ...r, qty: Math.max(0, qty) } : r
@@ -176,10 +176,29 @@
     emitRows(next)
   }
 
+  function clearQtyDraft(brandId: string) {
+    if (!Object.prototype.hasOwnProperty.call(qtyDrafts.value, brandId)) return
+    const next = { ...qtyDrafts.value }
+    delete next[brandId]
+    qtyDrafts.value = next
+  }
+
   function onMobileQtyInput(brandId: string, e: Event) {
-    const raw = (e.target as HTMLInputElement).value
-    const qty = raw === '' ? 0 : Math.max(0, Number(raw) || 0)
-    setQty(brandId, qty)
+    const raw = (e.target as HTMLInputElement).value.trim()
+    if (raw !== '' && !QTY_DRAFT_RE.test(raw)) {
+      // 非法字符:保持草稿/已有展示,阻止浏览器写入非法值
+      ;(e.target as HTMLInputElement).value = qtyDisplay(brandId)
+      return
+    }
+    qtyDrafts.value = { ...qtyDrafts.value, [brandId]: raw }
+    setQty(brandId, parseQtyDraft(raw))
+  }
+
+  function onMobileQtyBlur(brandId: string) {
+    if (!Object.prototype.hasOwnProperty.call(qtyDrafts.value, brandId)) return
+    const normalized = parseQtyDraft(qtyDrafts.value[brandId])
+    clearQtyDraft(brandId)
+    setQty(brandId, normalized)
   }
 
   watch(
@@ -192,9 +211,10 @@
       const existing = props.modelValue.find((r) => r.brandId === id)
       if (!existing) {
         emitRows([
+          ...props.modelValue,
           {
             brandId: id,
-            brandName: opt?.brandName || requiredBrandLabel.value,
+            brandName: opt?.brandName || '指定品牌',
             qty: 0
           }
         ])
@@ -294,6 +314,7 @@
     grid-template-columns: 72px 1fr auto;
     gap: 6px;
     align-items: center;
+    min-width: 0;
   }
 
   .brand-qty-chips__row-label {
@@ -334,8 +355,14 @@
   }
 
   .brand-qty-chips--admin {
+    .brand-qty-chips__rows {
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 8px 16px;
+    }
+
     .brand-qty-chips__row {
-      grid-template-columns: 100px 1fr auto;
+      grid-template-columns: minmax(64px, 100px) minmax(0, 1fr) auto;
     }
 
     .brand-qty-chips__admin-input {