|
|
@@ -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>
|