322 lines
9.7 KiB
Vue
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>
|