客户管理编辑页修改
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="product-select">
|
||||
<el-button type="primary" plain icon="Connection" @click="openDialog">
|
||||
{{ selectedIds.length ? '重新选择产品' : '选择产品' }}
|
||||
</el-button>
|
||||
<span class="ps-summary">
|
||||
已绑定 <b>{{ selectedIds.length }}</b> 个产品
|
||||
<el-button v-if="selectedIds.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="ps-filter" @submit.prevent>
|
||||
<el-form-item label="产品代码">
|
||||
<el-input v-model="queryParams.productCode" placeholder="如 WSP037" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="queryParams.productName" placeholder="产品名称" clearable style="width: 160px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目的区域">
|
||||
<el-input v-model="queryParams.destRegion" placeholder="如 欧盟" clearable style="width: 120px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="走货属性">
|
||||
<el-input v-model="queryParams.cargoAttr" placeholder="普货/带电等" clearable style="width: 120px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 110px">
|
||||
<el-option label="待生效" value="0" />
|
||||
<el-option label="已生效" value="1" />
|
||||
<el-option label="已失效" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredList"
|
||||
row-key="productId"
|
||||
height="380"
|
||||
border
|
||||
@select="handleSelect"
|
||||
@select-all="handleSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column label="产品代码" prop="productCode" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="产品名称" prop="productName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="时效等级" prop="serviceLevel" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="走货属性" prop="cargoAttr" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="目的区域" prop="destRegion" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="包税" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.taxMode === 'TAX_INCLUDED'" type="success" size="small">包税</el-tag>
|
||||
<el-tag v-else-if="row.taxMode === 'TAX_EXCLUDED'" type="warning" size="small">不包税</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType[row.status] || 'info'" size="small">{{ statusMap[row.status] || '未知' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<div slot="empty" class="ps-empty">暂无产品数据</div>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="ps-footer">
|
||||
<span>已选 <b>{{ draftIds.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="ProductSelect">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { useProducts } from './useProducts'
|
||||
|
||||
const props = defineProps({
|
||||
/** v-model 绑定值:逗号分隔的产品ID串,如 "3,6" */
|
||||
modelValue: { type: [String, Number], default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const { productList, ensureProducts } = useProducts()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const tableRef = ref(null)
|
||||
const draftIds = reactive(new Set())
|
||||
const queryParams = reactive({
|
||||
productCode: '',
|
||||
productName: '',
|
||||
destRegion: '',
|
||||
cargoAttr: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const selectedIds = computed(() =>
|
||||
String(props.modelValue || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
)
|
||||
|
||||
const statusMap = { 0: '待生效', 1: '已生效', 2: '已失效' }
|
||||
const statusTagType = { 0: 'info', 1: 'success', 2: 'danger' }
|
||||
|
||||
const filteredList = computed(() => {
|
||||
return productList.value.filter(p => {
|
||||
if (queryParams.productCode && !(p.productCode || '').toLowerCase().includes(queryParams.productCode.trim().toLowerCase())) return false
|
||||
if (queryParams.productName && !(p.productName || '').includes(queryParams.productName.trim())) return false
|
||||
if (queryParams.destRegion && !(p.destRegion || '').includes(queryParams.destRegion.trim())) return false
|
||||
if (queryParams.cargoAttr && !(p.cargoAttr || '').includes(queryParams.cargoAttr.trim())) return false
|
||||
if (queryParams.status !== '' && queryParams.status !== null && queryParams.status !== undefined && String(p.status) !== String(queryParams.status)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
function openDialog() {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
loading.value = true
|
||||
draftIds.clear()
|
||||
selectedIds.value.forEach(id => draftIds.add(id))
|
||||
resetQuery(false)
|
||||
// 拉取最新产品后,按 draftIds 全量同步一次勾选(覆盖上次弹窗遗留状态)
|
||||
ensureProducts(true).finally(() => {
|
||||
loading.value = false
|
||||
nextTick(syncSelection)
|
||||
})
|
||||
}
|
||||
|
||||
/** 按 draftIds 同步当前渲染行的勾选状态(draftIds 为唯一数据源,筛选切换不丢选择) */
|
||||
function syncSelection() {
|
||||
if (!tableRef.value) return
|
||||
filteredList.value.forEach(row => {
|
||||
tableRef.value.toggleRowSelection(row, draftIds.has(String(row.productId)))
|
||||
})
|
||||
}
|
||||
|
||||
/** 数据/筛选变化后同步表格勾选状态 */
|
||||
watch(filteredList, () => {
|
||||
if (!dialogVisible.value) return
|
||||
nextTick(syncSelection)
|
||||
}, { flush: 'post' })
|
||||
|
||||
function handleSelect(selection, row) {
|
||||
const id = String(row.productId)
|
||||
if (draftIds.has(id)) draftIds.delete(id)
|
||||
else draftIds.add(id)
|
||||
}
|
||||
|
||||
function handleSelectAll(selection) {
|
||||
// selection 为当前过滤后页面被选中的行;长度>0 表示全选,否则取消全选
|
||||
if (selection.length) {
|
||||
filteredList.value.forEach(row => draftIds.add(String(row.productId)))
|
||||
} else {
|
||||
filteredList.value.forEach(row => draftIds.delete(String(row.productId)))
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
// 纯前端过滤(全量已缓存),watch filteredList 自动同步勾选
|
||||
}
|
||||
|
||||
function resetQuery(reload = true) {
|
||||
queryParams.productCode = ''
|
||||
queryParams.productName = ''
|
||||
queryParams.destRegion = ''
|
||||
queryParams.cargoAttr = ''
|
||||
queryParams.status = ''
|
||||
if (reload) ensureProducts(true)
|
||||
}
|
||||
|
||||
function confirmSelection() {
|
||||
const ids = Array.from(draftIds)
|
||||
const value = ids.join(',')
|
||||
emit('update:modelValue', value)
|
||||
emit('change', ids)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
emit('update:modelValue', '')
|
||||
emit('change', [])
|
||||
}
|
||||
|
||||
/** 供父组件预加载产品缓存(tab 内明细表格使用) */
|
||||
defineExpose({ ensureProducts })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.ps-summary {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ps-summary b {
|
||||
color: #409eff;
|
||||
font-size: 15px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
.ps-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.ps-footer b {
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ref } from 'vue'
|
||||
import { listProduct } from '@/api/price/product'
|
||||
|
||||
/**
|
||||
* 产品列表模块级共享缓存:全应用只拉取一次
|
||||
* 用于客户绑定产品的选择与已绑产品名称/明细回显
|
||||
*/
|
||||
const productList = ref([])
|
||||
let loadingPromise = null
|
||||
|
||||
/** 逗号分隔 id 串 -> 字符串 id 数组 */
|
||||
export function parseProductIds(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)
|
||||
}
|
||||
|
||||
/** id 数组 -> 逗号分隔串(去重,保持原顺序) */
|
||||
export function joinProductIds(ids) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
;(ids || []).forEach(id => {
|
||||
const key = String(id)
|
||||
if (key && !seen.has(key)) {
|
||||
seen.add(key)
|
||||
out.push(key)
|
||||
}
|
||||
})
|
||||
return out.join(',')
|
||||
}
|
||||
|
||||
export function useProducts() {
|
||||
/** 确保产品列表已加载(force=true 强制刷新),拉取全部状态以便回显已失效产品 */
|
||||
function ensureProducts(force = false) {
|
||||
if (!force && loadingPromise) return loadingPromise
|
||||
if (!force && productList.value.length) return Promise.resolve(productList.value)
|
||||
loadingPromise = listProduct({ pageNum: 1, pageSize: 1000 })
|
||||
.then(res => {
|
||||
productList.value = res.rows || []
|
||||
return productList.value
|
||||
})
|
||||
.finally(() => {
|
||||
loadingPromise = null
|
||||
})
|
||||
return loadingPromise
|
||||
}
|
||||
|
||||
/** 按产品ID查产品对象 */
|
||||
function findProduct(productId) {
|
||||
if (productId === null || productId === undefined || productId === '') return null
|
||||
return productList.value.find(p => String(p.productId) === String(productId)) || null
|
||||
}
|
||||
|
||||
/** 产品展示名:代码 + 名称 */
|
||||
function getProductLabel(productId) {
|
||||
const p = findProduct(productId)
|
||||
if (!p) return String(productId)
|
||||
return p.productCode + ' ' + p.productName
|
||||
}
|
||||
|
||||
return {
|
||||
productList,
|
||||
ensureProducts,
|
||||
findProduct,
|
||||
getProductLabel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>{{ displayName }}</template>
|
||||
|
||||
<script setup name="UserName">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useUsers } from '@/components/UserSelect/useUsers'
|
||||
|
||||
const props = defineProps({
|
||||
/** 用户ID */
|
||||
userId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const { ensureUsers, getUserName } = useUsers()
|
||||
|
||||
// getUserName 内部读取共享 userList,列表加载完成后计算属性自动刷新
|
||||
const displayName = computed(() => getUserName(props.userId))
|
||||
|
||||
onMounted(() => {
|
||||
ensureUsers()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-model="innerValue"
|
||||
:placeholder="placeholder"
|
||||
:clearable="clearable"
|
||||
:filterable="filterable"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
:multiple="false"
|
||||
value-on-clear="null"
|
||||
style="width: 100%;"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.userId"
|
||||
:label="item.nickName || item.userName"
|
||||
:value="String(item.userId)"
|
||||
>
|
||||
<span>{{ item.nickName || item.userName }}</span>
|
||||
<span
|
||||
v-if="showCode && item.userName"
|
||||
class="user-select-sub"
|
||||
>{{ item.userName }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup name="UserSelect">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useUsers } from './useUsers'
|
||||
|
||||
const props = defineProps({
|
||||
/** 绑定值:用户ID(字符串/数字),清空时为 null */
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择用户'
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 选项右侧是否显示登录账号 */
|
||||
showCode: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const { userList, ensureUsers, findUser } = useUsers()
|
||||
const loading = ref(false)
|
||||
const innerValue = ref(props.modelValue === null || props.modelValue === undefined ? null : String(props.modelValue))
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
innerValue.value = val === null || val === undefined || val === '' ? null : String(val)
|
||||
})
|
||||
|
||||
/** 兜底:当前值在用户列表中不存在(停用/未加载到)时补占位项,避免显示空白 */
|
||||
const options = computed(() => {
|
||||
const list = userList.value
|
||||
if (innerValue.value && !findUser(innerValue.value)) {
|
||||
return [{ userId: innerValue.value, userName: '', nickName: String(innerValue.value) }, ...list]
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
function handleChange(val) {
|
||||
const value = val === '' ? null : val
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value, value === null ? null : findUser(value))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
ensureUsers().finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
reload: () => ensureUsers(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-select-sub {
|
||||
float: right;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ref } from 'vue'
|
||||
import { listUser } from '@/api/system/user'
|
||||
|
||||
/**
|
||||
* 用户列表模块级共享缓存:整个应用只拉取一次,UserSelect / UserName 共用
|
||||
*/
|
||||
const userList = ref([])
|
||||
let loadingPromise = null
|
||||
|
||||
export function useUsers() {
|
||||
/** 确保用户列表已加载(force=true 强制刷新) */
|
||||
function ensureUsers(force = false) {
|
||||
if (!force && loadingPromise) return loadingPromise
|
||||
if (!force && userList.value.length) return Promise.resolve(userList.value)
|
||||
loadingPromise = listUser({ pageNum: 1, pageSize: 1000 })
|
||||
.then(res => {
|
||||
userList.value = res.rows || []
|
||||
return userList.value
|
||||
})
|
||||
.finally(() => {
|
||||
loadingPromise = null
|
||||
})
|
||||
return loadingPromise
|
||||
}
|
||||
|
||||
/** 按用户ID查用户对象 */
|
||||
function findUser(userId) {
|
||||
if (userId === null || userId === undefined || userId === '') return null
|
||||
return userList.value.find(u => String(u.userId) === String(userId)) || null
|
||||
}
|
||||
|
||||
/** 按用户ID取显示名(nickName 优先,找不到时回退显示ID本身) */
|
||||
function getUserName(userId) {
|
||||
if (userId === null || userId === undefined || userId === '') return ''
|
||||
const user = findUser(userId)
|
||||
return user ? (user.nickName || user.userName || String(userId)) : String(userId)
|
||||
}
|
||||
|
||||
return {
|
||||
userList,
|
||||
ensureUsers,
|
||||
findUser,
|
||||
getUserName
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user