供应商管理数据同步, tms系统产品,服务渠道,产品渠道关系数据同步

This commit is contained in:
wuweihua
2026-10-12 10:28:06 +08:00
parent d08485d738
commit 2a9825c498
29 changed files with 1646 additions and 127 deletions
@@ -0,0 +1,216 @@
<template>
<div class="channel-select">
<el-button type="primary" plain icon="Connection" @click="openDialog">
{{ selectedCodes.length ? '重新选择渠道' : '选择渠道' }}
</el-button>
<span class="cs-summary">
已绑定 <b>{{ selectedCodes.length }}</b> 个渠道
<el-button v-if="selectedCodes.length" link type="danger" @click="clearSelection">清空</el-button>
</span>
<el-dialog
v-model="dialogVisible"
title="选择绑定渠道"
width="960px"
append-to-body
:close-on-click-modal="false"
@open="handleOpen"
>
<el-form :inline="true" class="cs-filter" @submit.prevent>
<el-form-item label="渠道编码">
<el-input v-model="queryParams.channelCode" placeholder="如 SK_DEPDHLL" clearable style="width: 160px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="渠道名称">
<el-input v-model="queryParams.channelName" placeholder="渠道名称" clearable style="width: 170px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="服务商">
<el-input v-model="queryParams.serviceName" placeholder="服务商名称" clearable style="width: 130px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.blOpen" placeholder="全部" clearable style="width: 110px">
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
ref="tableRef"
v-loading="loading"
:data="filteredList"
row-key="channelCode"
height="380"
border
@select="handleSelect"
@select-all="handleSelectAll"
>
<el-table-column type="selection" width="48" align="center" />
<el-table-column label="渠道编码" prop="channelCode" width="150" show-overflow-tooltip />
<el-table-column label="渠道名称" prop="channelName" min-width="170" show-overflow-tooltip />
<el-table-column label="服务商" prop="serviceName" width="120" show-overflow-tooltip />
<el-table-column label="付款方式" prop="payType" width="90" align="center" show-overflow-tooltip />
<el-table-column label="货物类型" prop="cargoType" width="90" align="center" show-overflow-tooltip />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.blOpen === '1'" type="success" size="small">启用</el-tag>
<el-tag v-else-if="row.blOpen === '0'" type="info" size="small">停用</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无渠道数据" :image-size="80" />
</template>
</el-table>
<template #footer>
<div class="cs-footer">
<span>已选 <b>{{ draftCodes.size }}</b> 个渠道</span>
<div>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSelection">确定</el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="ChannelSelect">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { useChannels } from './useChannels'
const props = defineProps({
/** v-model 绑定值:逗号分隔的渠道编码串,如 "SK_DEPDHLL,015A" */
modelValue: { type: [String, Number], default: '' }
})
const emit = defineEmits(['update:modelValue', 'change'])
const { channelList, ensureChannels } = useChannels()
const dialogVisible = ref(false)
const loading = ref(false)
const tableRef = ref(null)
const draftCodes = reactive(new Set())
const queryParams = reactive({
channelCode: '',
channelName: '',
serviceName: '',
blOpen: ''
})
const selectedCodes = computed(() =>
String(props.modelValue || '').split(',').map(s => s.trim()).filter(Boolean)
)
const filteredList = computed(() => {
return channelList.value.filter(c => {
if (queryParams.channelCode && !(c.channelCode || '').toLowerCase().includes(queryParams.channelCode.trim().toLowerCase())) return false
if (queryParams.channelName && !(c.channelName || '').includes(queryParams.channelName.trim())) return false
if (queryParams.serviceName && !(c.serviceName || '').includes(queryParams.serviceName.trim())) return false
if (queryParams.blOpen !== '' && queryParams.blOpen !== null && queryParams.blOpen !== undefined && String(c.blOpen) !== String(queryParams.blOpen)) return false
return true
})
})
function openDialog() {
dialogVisible.value = true
}
function handleOpen() {
loading.value = true
draftCodes.clear()
selectedCodes.value.forEach(code => draftCodes.add(code))
resetQuery(false)
// 拉取最新渠道后,按 draftCodes 全量同步一次勾选(覆盖上次弹窗遗留状态)
ensureChannels(true).finally(() => {
loading.value = false
nextTick(syncSelection)
})
}
/** 按 draftCodes 同步当前渲染行的勾选状态(draftCodes 为唯一数据源,筛选切换不丢选择) */
function syncSelection() {
if (!tableRef.value) return
filteredList.value.forEach(row => {
tableRef.value.toggleRowSelection(row, draftCodes.has(String(row.channelCode)))
})
}
/** 数据/筛选变化后同步表格勾选状态 */
watch(filteredList, () => {
if (!dialogVisible.value) return
nextTick(syncSelection)
}, { flush: 'post' })
function handleSelect(selection, row) {
const code = String(row.channelCode)
if (draftCodes.has(code)) draftCodes.delete(code)
else draftCodes.add(code)
}
function handleSelectAll(selection) {
// selection 为当前过滤后页面被选中的行;长度>0 表示全选,否则取消全选
if (selection.length) {
filteredList.value.forEach(row => draftCodes.add(String(row.channelCode)))
} else {
filteredList.value.forEach(row => draftCodes.delete(String(row.channelCode)))
}
}
function handleQuery() {
// 纯前端过滤(全量已缓存),watch filteredList 自动同步勾选
}
function resetQuery(reload = true) {
queryParams.channelCode = ''
queryParams.channelName = ''
queryParams.serviceName = ''
queryParams.blOpen = ''
if (reload) ensureChannels(true)
}
function confirmSelection() {
const codes = Array.from(draftCodes)
const value = codes.join(',')
emit('update:modelValue', value)
emit('change', codes)
dialogVisible.value = false
}
function clearSelection() {
emit('update:modelValue', '')
emit('change', [])
}
/** 供父组件预加载渠道缓存(tab 内明细表格使用) */
defineExpose({ ensureChannels })
</script>
<style scoped>
.channel-select {
display: flex;
align-items: center;
gap: 12px;
}
.cs-summary {
color: #606266;
font-size: 14px;
}
.cs-summary b {
color: #409eff;
font-size: 15px;
margin: 0 2px;
}
.cs-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.cs-footer b {
color: #409eff;
}
</style>
@@ -0,0 +1,72 @@
import { ref } from 'vue'
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
/**
* 服务渠道列表模块级共享缓存:全应用只拉取一次
* 用于供应商绑定渠道的选择与已绑渠道编码/名称回显
*
* 口径说明:供应商绑定关系统一存「渠道编码 channelCode」逗号串
* (sup_supplier.channel_ids 历史字段名沿用,实际存的是渠道编码),
* 与老系统 TAB_SERVICE_CHANNEL.CHANNEL_CODE 稳定对应。
*/
const channelList = ref([])
let loadingPromise = null
/** 逗号分隔编码串 -> 字符串编码数组 */
export function parseChannelCodes(value) {
if (Array.isArray(value)) return value.map(v => String(v)).filter(Boolean)
if (value === null || value === undefined) return []
return String(value).split(',').map(s => s.trim()).filter(Boolean)
}
/** 编码数组 -> 逗号分隔串(去重,保持原顺序) */
export function joinChannelCodes(codes) {
const seen = new Set()
const out = []
;(codes || []).forEach(code => {
const key = String(code)
if (key && !seen.has(key)) {
seen.add(key)
out.push(key)
}
})
return out.join(',')
}
export function useChannels() {
/** 确保渠道列表已加载(force=true 强制刷新),拉取全部状态以便回显已停用渠道 */
function ensureChannels(force = false) {
if (!force && loadingPromise) return loadingPromise
if (!force && channelList.value.length) return Promise.resolve(channelList.value)
loadingPromise = listChannelOptions({})
.then(res => {
channelList.value = res.data || []
return channelList.value
})
.finally(() => {
loadingPromise = null
})
return loadingPromise
}
/** 按渠道编码查渠道对象 */
function findChannel(channelCode) {
if (channelCode === null || channelCode === undefined || channelCode === '') return null
const code = String(channelCode).trim()
return channelList.value.find(c => String(c.channelCode) === code) || null
}
/** 渠道展示名:编码 + 名称;查不到的渠道回显原编码(镜像未同步/已删除渠道不致空白) */
function getChannelLabel(channelCode) {
const c = findChannel(channelCode)
if (!c) return String(channelCode)
return c.channelCode + ' ' + c.channelName
}
return {
channelList,
ensureChannels,
findChannel,
getChannelLabel
}
}