464 lines
14 KiB
Vue
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>
|