Files
BMS/bms-front/src/views/price/set/components/create/PriceEditDrawer.vue
T
2026-11-02 16:41:47 +08:00

322 lines
9.7 KiB
Vue

<template>
<div>
<div class="mask" :class="{ open: modelValue }" @click="close"></div>
<div class="drawer" :class="{ open: modelValue }">
<div class="drawer-hd">
<span>{{ title }}</span>
<div class="hd-actions">
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="drawer-bd">
<el-alert type="info" :closable="false" show-icon class="mb12" :title="hint" />
<div class="filters">
<el-input v-model="keyword" clearable :placeholder="phSearch" style="width: 240px" />
</div>
<el-table
:data="displayRows"
border
size="small"
:span-method="spanMethod"
>
<el-table-column :label="colCountry" min-width="100">
<template #default="{ row }">{{ countryLabel(row) }}</template>
</el-table-column>
<el-table-column :label="colZone" min-width="90">
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
</el-table-column>
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
<el-table-column :label="colSetFv" width="160" align="center">
<template #default="{ row }">
<div class="price-cell">
<template v-if="editingKey !== cellKey(row, 'fv')">
<span class="price-text">{{ formatNum(row.fvValue, 4) }}</span>
<el-button link type="primary" icon="Edit" class="edit-ico" @click="startEdit(row, 'fv')" />
</template>
<el-input-number
v-else
:ref="el => setEditRef(el, row, 'fv')"
v-model="row.fvValue"
:min="0"
:precision="4"
:controls="false"
size="small"
style="width: 110px"
@blur="() => commitEdit(row, 'fv')"
@keyup.enter="() => commitEdit(row, 'fv')"
/>
</div>
</template>
</el-table-column>
<el-table-column :label="colSetReg" width="150" align="center">
<template #default="{ row }">
<div class="price-cell">
<template v-if="editingKey !== cellKey(row, 'reg')">
<span class="price-text">{{ formatNum(row.fvRegistrationFee, 2) }}</span>
<el-button link type="primary" icon="Edit" class="edit-ico" @click="startEdit(row, 'reg')" />
</template>
<el-input-number
v-else
:ref="el => setEditRef(el, row, 'reg')"
v-model="row.fvRegistrationFee"
:min="0"
:precision="2"
:controls="false"
size="small"
style="width: 100px"
@blur="() => commitEdit(row, 'reg')"
@keyup.enter="() => commitEdit(row, 'reg')"
/>
</div>
</template>
</el-table-column>
<el-table-column :label="colSurcharge" width="110" align="center">
<template #default="{ row }">
<surcharge-count-cell :list="row.surcharges || []" @remove="s => removeSurcharge(row, s)" />
</template>
</el-table-column>
<el-table-column :label="colAction" width="90" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="pickSurcharge(row)">附加费</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup name="PriceEditDrawer">
import SurchargeCountCell from './SurchargeCountCell.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
productCode: { type: String, default: '' },
productName: { type: String, default: '' },
rows: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'pick-surcharge', 'row-change', 'surcharge-change'])
const keyword = ref('')
const editingKey = ref('')
const committing = ref(false)
const editInputRefs = {}
const title = computed(() => {
const code = props.productCode || ''
const name = props.productName || ''
return code + ' · ' + name + ' · 国家服务价格'
})
const hint = '点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
const phSearch = '搜索国家/分区'
const colCountry = '国家'
const colZone = '分区'
const colWeight = '重量段'
const colSetFv = '套餐运费'
const colSetReg = '套餐挂号费'
const colSurcharge = '附加费'
const colAction = '操作'
function cellKey(row, field) {
if (!row) return 'none:' + field
// Prefer DB id; local Excel draft uses _localKey (psdId cleared in applyLocalDraft).
if (row.psdId != null && row.psdId !== '') return String(row.psdId) + ':' + field
if (row._localKey) return String(row._localKey) + ':' + field
const parts = [
row.productCode,
row.countryCode || row.countryName,
row.zoneCode || row.zoneName,
row.weightLabel,
row.weightMin,
row.weightMax,
row.productCountryId
]
return parts.map(v => (v == null ? '' : String(v))).join('|') + ':' + field
}
function formatNum(v, precision) {
if (v == null || v === '') return '-'
const n = Number(v)
if (Number.isNaN(n)) return '-'
return n.toFixed(precision)
}
function setEditRef(el, row, field) {
if (el) editInputRefs[cellKey(row, field)] = el
}
function startEdit(row, field) {
if (!row) return
row._editPrevFv = row.fvValue
row._editPrevReg = row.fvRegistrationFee
editingKey.value = cellKey(row, field)
nextTick(() => {
const el = editInputRefs[editingKey.value]
if (el && typeof el.focus === 'function') el.focus()
})
}
async function commitEdit(row, field) {
if (committing.value) return
if (editingKey.value !== cellKey(row, field)) return
committing.value = true
editingKey.value = ''
try {
if (!row) return
// Always emit: create/IMPORT_FRESH local draft has no psdId until finish;
// parent persist path decides DB write vs local-only.
emit('row-change', row)
} finally {
committing.value = false
}
}
function pickSurcharge(row) {
emit('pick-surcharge', row || null)
}
function removeSurcharge(row, sc) {
if (!row || !sc || sc.scId == null) return
const scId = String(sc.scId)
const next = (row.surcharges || []).filter(s => s && String(s.scId) !== scId)
row.surcharges = next
emit('surcharge-change', row)
}
function countryKey(row) {
return String(row.countryCode || row.countryName || '').toUpperCase()
}
function countryLabel(row) {
return row.countryName || row.countryCode || '-'
}
const displayRows = computed(() => {
const q = (keyword.value || '').trim().toLowerCase()
let list = [...(props.rows || [])]
if (q) {
list = list.filter(r => {
const blob = [r.countryName, r.countryCode, r.zoneName, r.zoneCode, r.weightLabel]
.filter(Boolean).join(' ').toLowerCase()
return blob.includes(q)
})
}
list.sort((a, b) => {
const ca = countryKey(a)
const cb = countryKey(b)
if (ca !== cb) return ca.localeCompare(cb)
const za = String(a.zoneCode || a.zoneName || '')
const zb = String(b.zoneCode || b.zoneName || '')
if (za !== zb) return za.localeCompare(zb)
return String(a.weightLabel || '').localeCompare(String(b.weightLabel || ''))
})
return list
})
function buildSpans(rows, keyOf) {
const spans = new Array(rows.length).fill(1)
let i = 0
while (i < rows.length) {
let j = i + 1
const key = keyOf(rows[i])
while (j < rows.length && keyOf(rows[j]) === key) j++
spans[i] = j - i
for (let k = i + 1; k < j; k++) spans[k] = 0
i = j
}
return spans
}
function zoneKey(row) {
return String(row.zoneCode || row.zoneName || '')
}
const countrySpans = computed(() => buildSpans(displayRows.value, countryKey))
const zoneSpans = computed(() => buildSpans(displayRows.value, r => countryKey(r) + '|' + zoneKey(r)))
function spanMethod({ columnIndex, rowIndex }) {
if (columnIndex === 0) return spanOf(countrySpans.value, rowIndex)
if (columnIndex === 1) return spanOf(zoneSpans.value, rowIndex)
return { rowspan: 1, colspan: 1 }
}
function spanOf(spans, rowIndex) {
const rowspan = spans[rowIndex] || 0
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
}
function close() {
editingKey.value = ''
emit('update:modelValue', false)
}
watch(() => props.modelValue, v => {
if (v) {
keyword.value = ''
editingKey.value = ''
}
})
</script>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 1999;
display: none;
}
.mask.open { display: block; }
.drawer {
position: fixed;
top: 0;
right: 0;
width: min(1180px, 98vw);
height: 100%;
background: #fff;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
transform: translateX(100%);
transition: 0.25s;
z-index: 2000;
display: flex;
flex-direction: column;
}
.drawer.open { transform: translateX(0); }
.drawer-hd {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.hd-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.drawer-bd {
padding: 16px 20px;
overflow: auto;
flex: 1;
}
.mb12 { margin-bottom: 12px; }
.filters { margin-bottom: 12px; }
.price-cell {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
min-height: 28px;
}
.price-text {
font-variant-numeric: tabular-nums;
min-width: 64px;
text-align: right;
}
.edit-ico {
padding: 0 2px !important;
}
</style>