Files
BMS/bms-front/src/views/price/label/components/LabelVersionProductDrawer.vue
T

464 lines
14 KiB
Vue

<template>
<el-drawer
:model-value="visible"
:title="title"
size="78%"
destroy-on-close
append-to-body
@close="emit('update:visible', false)"
>
<div class="toolbar">
<el-input v-model="keyword" clearable :placeholder="phSearch" style="width: 300px" />
</div>
<el-table
v-loading="loading"
:data="filteredRows"
border
height="calc(100vh - 220px)"
row-key="termId"
>
<el-table-column prop="productId" :label="colId" width="90" align="center" />
<el-table-column prop="productName" :label="colName" min-width="150" show-overflow-tooltip />
<el-table-column :label="colTerm" min-width="180" align="center">
<template #default="{ row }">{{ termText(row) }}</template>
</el-table-column>
<!-- <el-table-column :label="colStatus" width="90" align="center">
<template #default="{ row }">
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
</template>
</el-table-column> -->
<el-table-column prop="weightCount" :label="colWeight" width="80" align="center" />
<el-table-column prop="surchargeCount" :label="colSc" width="80" align="center" />
<el-table-column :label="colBoundChannels" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ boundChannelText(row) }}</template>
</el-table-column>
<el-table-column :label="colAction" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query', 'price:label:edit']">编辑</el-button>
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
<pay-config-detail
v-loading="detailLoading"
:product="detail"
:editable="canEditDetail"
:saving-row-key="savingRowKey"
@save-row="saveDetailRow"
/>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
v-if="canEditDetail"
type="primary"
:loading="detailSaving"
v-hasPermi="['price:label:edit']"
@click="saveDetail"
>保存明细</el-button>
</template>
</el-drawer>
<el-drawer
v-model="bindListVisible"
:title="bindListTitle"
size="520px"
append-to-body
destroy-on-close
@closed="onBindListClosed"
>
<div class="bind-list-toolbar">
<el-button type="primary" plain @click="openAddChannels" v-hasPermi="['price:label:edit']">添加渠道</el-button>
</div>
<el-table
v-loading="bindListLoading"
:data="boundChannels"
border
height="calc(100vh - 240px)"
:empty-text="emptyBound"
>
<el-table-column prop="channelCode" :label="colChannelCode" min-width="120" show-overflow-tooltip />
<el-table-column prop="channelName" :label="colChannelName" min-width="160" show-overflow-tooltip />
<el-table-column :label="colAction" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="removeBoundChannel(row)" v-hasPermi="['price:label:edit']">移除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="bindListVisible = false">关闭</el-button>
</template>
</el-drawer>
<el-dialog
v-model="addVisible"
:title="addTitle"
width="560px"
append-to-body
destroy-on-close
@closed="onAddClosed"
>
<div v-loading="addLoading">
<el-form label-width="88px">
<el-form-item :label="lblProduct">
<span>{{ bindProductName }}</span>
</el-form-item>
<el-form-item :label="lblChannels">
<el-select
v-model="addChannelIds"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
:placeholder="phChannels"
style="width: 100%"
>
<el-option
v-for="item in addableChannelOptions"
:key="item.id"
:label="channelOptionLabel(item)"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<div class="bind-hint">{{ bindHint }}</div>
</el-form>
</div>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" :loading="addSaving" @click="submitAddChannels">保存</el-button>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup>
import {
listLabelProducts,
getLabelProduct,
updateLabelProduct,
listLabelProductChannels,
replaceLabelProductChannels
} from '@/api/price/label'
import { checkPermi } from '@/utils/permission'
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
const props = defineProps({
visible: { type: Boolean, default: false },
priceSetId: { type: [Number, String], default: null },
version: { type: Object, default: null }
})
const emit = defineEmits(['update:visible', 'changed'])
const { proxy } = getCurrentInstance()
const loading = ref(false)
const rows = ref([])
const keyword = ref('')
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailSaving = ref(false)
const savingRowKey = ref('')
const detail = ref(null)
const canEditDetail = computed(() => checkPermi(['price:label:edit']))
const bindListVisible = ref(false)
const bindListLoading = ref(false)
const bindProductId = ref(null)
const bindProductName = ref('')
const boundChannels = ref([])
const channelOptions = ref([])
const addVisible = ref(false)
const addLoading = ref(false)
const addSaving = ref(false)
const addChannelIds = ref([])
const phSearch = '产品名称 / 绑定渠道'
const colId = '产品编号'
const colName = '产品名称'
const colTerm = '有效期'
const colStatus = '状态'
const colWeight = '重量段'
const colSc = '附加费'
const colBoundChannels = '绑定渠道'
const colAction = '操作'
const colChannelCode = '渠道编码'
const colChannelName = '渠道名称'
const lblProduct = '产品'
const lblChannels = '添加渠道'
const phChannels = '请选择要添加的渠道(可多选)'
const bindHint = '绑定为收款(AR),落库 srv_channel_product_map;一个渠道同一收付类型仅保留一条产品映射。'
const emptyBound = '暂无绑定渠道'
const addTitle = '添加渠道'
const title = computed(() => {
const v = props.version || {}
const label = v.versionLabel || (v.versionNo != null ? `V${v.versionNo}` : '')
return label ? `版本明细 · ${label}` : '版本明细'
})
const detailTitle = computed(() => {
if (!detail.value) return '产品明细'
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
return `${detail.value.productName || '产品'} ${no}`.trim()
})
const bindListTitle = computed(() => {
return bindProductName.value ? `绑定渠道 · ${bindProductName.value}` : '绑定渠道'
})
const filteredRows = computed(() => {
const kw = keyword.value.trim().toLowerCase()
if (!kw) return rows.value
return rows.value.filter(row => {
const text = [row.productName, boundChannelText(row)].join(' ').toLowerCase()
return text.includes(kw)
})
})
const addableChannelOptions = computed(() => {
const bound = new Set((boundChannels.value || []).map(item => String(item.channelId)))
return (channelOptions.value || []).filter(item => item && item.id != null && !bound.has(String(item.id)))
})
function formatHour(val) {
if (!val) return ''
const text = String(val).replace('T', ' ')
if (text.length >= 16) return text.slice(0, 13) + ':00'
if (text.length >= 10) return text.slice(0, 10) + ' 00:00'
return text
}
function termText(row) {
const start = formatHour(row.termStart || row.effectiveDate || '')
const end = row.termEnd ? formatHour(row.termEnd) : '不限'
if (!start) return '未设置'
return start + ' ~ ' + end
}
function termStatusText(status) {
const map = { '0': '待生效', '1': '生效', '2': '已失效' }
return map[String(status)] || '-'
}
function termStatusTag(status) {
const map = { '0': 'warning', '1': 'success', '2': 'danger' }
return map[String(status)] || 'info'
}
function channelOptionLabel(item) {
const code = item?.channelCode || item?.id || ''
const name = item?.channelName || ''
if (code && name && code !== name) return code + ' / ' + name
return code || name || '-'
}
function boundChannelText(row) {
if (!row) return '-'
if (row.boundChannelText) return row.boundChannelText
return '-'
}
async function load() {
if (!props.priceSetId || props.version?.versionNo == null) {
rows.value = []
return
}
loading.value = true
try {
const res = await listLabelProducts(props.priceSetId, props.version.versionNo)
rows.value = res.data || []
} finally {
loading.value = false
}
}
function cleanMoney(product) {
if (!product) return
;(product.weightBands || []).forEach(row => {
if (row && row.price === '') row.price = null
})
;(product.surcharges || []).forEach(row => {
if (row && row.amount === '') row.amount = null
})
}
async function persistDetail() {
if (!detail.value?.productId) {
throw new Error('NO_PRODUCT')
}
cleanMoney(detail.value)
await updateLabelProduct(detail.value)
const termId = detail.value.termId
const res = await getLabelProduct(detail.value.productId, termId)
detail.value = res.data || detail.value
await load()
emit('changed')
}
async function saveDetail() {
if (!detail.value || detailSaving.value || savingRowKey.value) return
detailSaving.value = true
try {
await persistDetail()
proxy.$modal.msgSuccess('明细已保存')
} catch (e) {
// request 拦截器已提示
} finally {
detailSaving.value = false
}
}
async function saveDetailRow(payload) {
if (!detail.value || savingRowKey.value || detailSaving.value) {
payload?.done?.(false)
return
}
savingRowKey.value = payload?.rowKey || 'row'
let ok = false
try {
await persistDetail()
ok = true
proxy.$modal.msgSuccess('该行已保存')
} catch (e) {
ok = false
} finally {
savingRowKey.value = ''
payload?.done?.(ok)
}
}
async function openProduct(row) {
detailVisible.value = true
detailLoading.value = true
detailSaving.value = false
savingRowKey.value = ''
detail.value = null
try {
const res = await getLabelProduct(row.productId, row.termId)
detail.value = res.data || null
} finally {
detailLoading.value = false
}
}
async function ensureChannelOptions() {
if (channelOptions.value.length) return
const res = await listChannelOptions({ blOpen: '1' })
channelOptions.value = res.data || []
}
async function loadBoundChannels() {
if (!bindProductId.value) {
boundChannels.value = []
return
}
bindListLoading.value = true
try {
const res = await listLabelProductChannels(bindProductId.value, 'AR')
boundChannels.value = res.data || []
} finally {
bindListLoading.value = false
}
}
async function openBindList(row) {
if (!row?.productId) return
bindProductId.value = row.productId
bindProductName.value = row.productName || String(row.productId)
boundChannels.value = []
bindListVisible.value = true
await loadBoundChannels()
}
function onBindListClosed() {
bindProductId.value = null
bindProductName.value = ''
boundChannels.value = []
addVisible.value = false
addChannelIds.value = []
}
async function openAddChannels() {
addChannelIds.value = []
addVisible.value = true
addLoading.value = true
try {
await ensureChannelOptions()
} finally {
addLoading.value = false
}
}
function onAddClosed() {
addChannelIds.value = []
}
function currentBoundIds() {
return (boundChannels.value || []).map(item => String(item.channelId)).filter(Boolean)
}
function submitAddChannels() {
if (!bindProductId.value) return
const selected = (addChannelIds.value || []).map(String).filter(Boolean)
if (!selected.length) {
proxy.$modal.msgWarning('请先选择要添加的渠道')
return
}
const merged = Array.from(new Set(currentBoundIds().concat(selected)))
addSaving.value = true
replaceLabelProductChannels(bindProductId.value, {
payType: 'AR',
channelIds: merged
}).then((res) => {
proxy.$modal.msgSuccess((res && res.msg) || '添加成功')
addVisible.value = false
return loadBoundChannels()
}).then(() => load()).catch(() => {}).finally(() => {
addSaving.value = false
})
}
function removeBoundChannel(row) {
if (!bindProductId.value || !row?.channelId) return
const label = channelOptionLabel(row)
proxy.$modal.confirm(`确认移除渠道「${label}」?`).then(() => {
const remain = currentBoundIds().filter(id => id !== String(row.channelId))
return replaceLabelProductChannels(bindProductId.value, {
payType: 'AR',
channelIds: remain
})
}).then((res) => {
proxy.$modal.msgSuccess((res && res.msg) || '已移除')
return loadBoundChannels()
}).then(() => load()).catch(() => {})
}
watch(() => [props.visible, props.priceSetId, props.version?.versionNo], ([vis]) => {
if (vis) load()
}, { immediate: true })
</script>
<style scoped>
.toolbar {
display: flex;
gap: 12px;
margin-bottom: 12px;
align-items: center;
}
.bind-list-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.bind-hint {
margin: -4px 0 0 88px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
</style>