feat(web-admin): site public page brand qty chips entry
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
006ed6586c
commit
6dde2cec07
3 files changed
+465
-16
No files matched your search
@@ -0,0 +1,345 @@
|
||||
<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>
|
||||
<span
|
||||
class="brand-qty-chips__sum"
|
||||
:class="{ 'brand-qty-chips__sum--ok': sumMatch, 'brand-qty-chips__sum--bad': !sumMatch }"
|
||||
>
|
||||
{{ sumLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="brandMode === 'any'" class="brand-qty-chips__chips">
|
||||
<button
|
||||
v-for="opt in brandOptions"
|
||||
:key="opt.brandId"
|
||||
type="button"
|
||||
class="brand-qty-chips__chip"
|
||||
:class="{ 'brand-qty-chips__chip--on': isSelected(opt.brandId) }"
|
||||
:disabled="disabled"
|
||||
@click="toggleChip(opt)"
|
||||
>
|
||||
{{ opt.brandName }}
|
||||
<span v-if="isSelected(opt.brandId)" class="brand-qty-chips__chip-qty">
|
||||
{{ qtyOf(opt.brandId) || '' }}
|
||||
</span>
|
||||
</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>
|
||||
<template v-if="variant === 'admin'">
|
||||
<el-input-number
|
||||
:model-value="row.qty"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
:step="0.001"
|
||||
:disabled="disabled"
|
||||
controls-position="right"
|
||||
class="brand-qty-chips__admin-input"
|
||||
@update:model-value="(val) => setQty(row.brandId, Number(val ?? 0))"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
type="number"
|
||||
inputmode="decimal"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="brand-qty-chips__qty-input"
|
||||
:value="row.qty === 0 ? '' : row.qty"
|
||||
:disabled="disabled"
|
||||
placeholder="0"
|
||||
@input="onMobileQtyInput(row.brandId, $event)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="unit" class="brand-qty-chips__unit">{{ unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface BrandQtyRow {
|
||||
brandId: string
|
||||
brandName: string
|
||||
qty: number
|
||||
}
|
||||
|
||||
export interface BrandOption {
|
||||
brandId: string
|
||||
brandName: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
brandMode?: 'any' | 'required' | null
|
||||
requiredBrandId?: string | null
|
||||
brandOptions: BrandOption[]
|
||||
modelValue: BrandQtyRow[]
|
||||
disabled?: boolean
|
||||
unit?: string
|
||||
variant?: 'mobile' | 'admin'
|
||||
actualQty?: number
|
||||
}>(),
|
||||
{
|
||||
brandMode: null,
|
||||
requiredBrandId: null,
|
||||
disabled: false,
|
||||
unit: '',
|
||||
variant: 'mobile',
|
||||
actualQty: undefined
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: BrandQtyRow[]]
|
||||
'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
|
||||
})
|
||||
|
||||
function isSelected(brandId: string) {
|
||||
return props.modelValue.some((r) => r.brandId === brandId)
|
||||
}
|
||||
|
||||
function qtyOf(brandId: string) {
|
||||
return props.modelValue.find((r) => r.brandId === brandId)?.qty ?? 0
|
||||
}
|
||||
|
||||
function sumRows(rows: BrandQtyRow[]) {
|
||||
return rows.reduce((s, r) => s + (Number(r.qty) || 0), 0)
|
||||
}
|
||||
|
||||
const totalQty = computed(() => Number(sumRows(props.modelValue).toFixed(3)))
|
||||
|
||||
const sumMatch = computed(() => {
|
||||
if (props.actualQty == null) return true
|
||||
return Math.abs(totalQty.value - Number(props.actualQty)) < 0.0005
|
||||
})
|
||||
|
||||
const sumLabel = computed(() => {
|
||||
if (props.actualQty == null) {
|
||||
return `合计 ${totalQty.value}${props.unit ? ` ${props.unit}` : ''}`
|
||||
}
|
||||
if (sumMatch.value) {
|
||||
return `合计 ${totalQty.value} = 实际数量 ✓`
|
||||
}
|
||||
return `合计 ${totalQty.value} ≠ 实际数量 ${props.actualQty}`
|
||||
})
|
||||
|
||||
function emitRows(rows: BrandQtyRow[]) {
|
||||
emit('update:modelValue', rows)
|
||||
emit('update:actualQty', Number(sumRows(rows).toFixed(3)))
|
||||
}
|
||||
|
||||
function toggleChip(opt: BrandOption) {
|
||||
if (props.disabled || props.brandMode !== 'any') return
|
||||
const selected = isSelected(opt.brandId)
|
||||
if (selected) {
|
||||
emitRows(props.modelValue.filter((r) => r.brandId !== opt.brandId))
|
||||
return
|
||||
}
|
||||
emitRows([...props.modelValue, { brandId: opt.brandId, brandName: opt.brandName, qty: 0 }])
|
||||
}
|
||||
|
||||
function setQty(brandId: string, qty: number) {
|
||||
const next = props.modelValue.map((r) =>
|
||||
r.brandId === brandId ? { ...r, qty: Math.max(0, qty) } : r
|
||||
)
|
||||
emitRows(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)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.brandMode, props.requiredBrandId, props.brandOptions] as const,
|
||||
() => {
|
||||
if (props.brandMode !== 'required') return
|
||||
const id = props.requiredBrandId || props.brandOptions[0]?.brandId
|
||||
if (!id) return
|
||||
const opt = props.brandOptions.find((o) => o.brandId === id)
|
||||
const existing = props.modelValue.find((r) => r.brandId === id)
|
||||
if (!existing) {
|
||||
emitRows([
|
||||
{
|
||||
brandId: id,
|
||||
brandName: opt?.brandName || requiredBrandLabel.value,
|
||||
qty: 0
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.brand-qty-chips {
|
||||
margin-top: 10px;
|
||||
border-top: 1px dashed #bbf7d0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.brand-qty-chips__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.brand-qty-chips__title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-qty-chips__sum {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-qty-chips__sum--ok {
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-qty-chips__sum--bad {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chip {
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
line-height: 1.2;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chip--on {
|
||||
border-color: #059669;
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chip--locked {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.brand-qty-chips__chip-qty {
|
||||
margin-left: 4px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.brand-qty-chips__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.brand-qty-chips__row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand-qty-chips__row-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding-left: 4px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.brand-qty-chips__qty-input {
|
||||
height: 36px;
|
||||
border: 1.5px solid #6ee7b7;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-qty-chips__unit {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.brand-qty-chips--admin {
|
||||
.brand-qty-chips__row {
|
||||
grid-template-columns: 100px 1fr auto;
|
||||
}
|
||||
|
||||
.brand-qty-chips__admin-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user