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>
This commit is contained in:
zlwuandCursor committed 2026-07-31 06:54:43 +08:00
1 parent 156e4b63e5
commit b28cf4a335
1 file changed
+64 -37
@@ -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 {