<script setup lang="tsx">
|
import noDataImg from '@/assets/element-img/no-data.png'
|
import type { DataTableColumns } from 'naive-ui'
|
import { useBoolean } from '@/hooks'
|
import { computed, ref, watch } from 'vue'
|
import { NButton, NCard, NDataTable, NInput, NModal, NSelect, NTag } from 'naive-ui'
|
import Pagination from '@/components/common/Pagination.vue'
|
import { fetchWhitelistPageList } from '@/service/api/whitelist'
|
import { getMultiSelectRowClassName, shouldIgnoreTableRowClick, toggleMultiSelectRowKey } from '@/utils/table'
|
import dayjs from 'dayjs'
|
|
interface UserItem {
|
id: number
|
userName: string
|
account: string
|
status: number | string
|
priorityDispatch: boolean
|
creator: string
|
createTime: string
|
priority?: number
|
desc?: string
|
[key: string]: any
|
}
|
|
interface Props {
|
existingUsers?: UserItem[]
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
existingUsers: () => [],
|
})
|
|
const emit = defineEmits<{
|
confirm: [selectedUsers: UserItem[]]
|
}>()
|
const visible = defineModel<boolean>('visible', { default: false })
|
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
|
// 搜索条件
|
const searchModel = ref({
|
nickName: '',
|
account: '',
|
status: null,
|
})
|
|
const isStatusOpen = ref(false)
|
|
const statusOptions = [
|
{ label: '正常', value: 1 },
|
{ label: '冻结', value: 0 },
|
]
|
|
// 选中的行
|
const checkedRowKeys = ref<number[]>([])
|
|
const listData = ref<UserItem[]>([])
|
|
// 缓存所有加载过的数据
|
const allLoadedData = ref<Map<number, UserItem>>(new Map())
|
|
const count = ref(0)
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
|
function toggleRowSelection(row: UserItem) {
|
checkedRowKeys.value = toggleMultiSelectRowKey(checkedRowKeys.value, row.id)
|
}
|
|
function getRowClassName(row: UserItem) {
|
return getMultiSelectRowClassName(row.id, checkedRowKeys.value)
|
}
|
|
function getRowProps(row: UserItem) {
|
return {
|
style: 'cursor: pointer;',
|
onClick: (event: MouseEvent) => {
|
if (shouldIgnoreTableRowClick(event.target))
|
return
|
toggleRowSelection(row)
|
},
|
}
|
}
|
|
// 表格列定义
|
const columns = computed<DataTableColumns<UserItem>>(() => [
|
{
|
type: 'selection',
|
width: 50,
|
},
|
{
|
title: '序号',
|
key: 'index',
|
width: 80,
|
render: (row, index) => <span>{(currentPage.value - 1) * pageSize.value + index + 1}</span>,
|
},
|
{
|
title: '用户名称',
|
key: 'nickName',
|
ellipsis: true,
|
},
|
{
|
title: '白名单账号',
|
key: 'account',
|
ellipsis: true,
|
},
|
{
|
title: '状态',
|
key: 'status',
|
render: (row) => {
|
return (
|
<NTag
|
type={row.status ? 'primary' : 'warning'}
|
bordered={false}
|
>
|
{row.status ? '正常' : '冻结'}
|
</NTag>
|
)
|
},
|
},
|
{
|
title: '优先派单',
|
key: 'priorityDispatch',
|
render: row => <span>{row.priorityDispatch ? '是' : '否'}</span>,
|
},
|
{
|
title: '创建人',
|
key: 'creator',
|
ellipsis: true,
|
},
|
{
|
title: '创建时间',
|
key: 'createTime',
|
ellipsis: true,
|
},
|
])
|
|
// 获取用户列表
|
async function getUserList() {
|
startLoading()
|
try {
|
const res = await fetchWhitelistPageList({
|
pageNum: currentPage.value,
|
pageSize: pageSize.value,
|
nickName: searchModel.value.nickName || undefined,
|
account: searchModel.value.account || undefined,
|
status: searchModel.value.status,
|
})
|
|
if (res.code === 200 && res.data) {
|
listData.value = (res.data.records || []).map((item: any) => ({
|
...item,
|
nickName: item.nickName,
|
priorityDispatch: item.isPriorityDispatch === 1 || item.isPriorityDispatch === true,
|
creator: item.createBy,
|
createTime: item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm') : '-',
|
}))
|
count.value = res.data.total || 0
|
|
// 存入缓存
|
listData.value.forEach((item: UserItem) => {
|
allLoadedData.value.set(item.id, item)
|
})
|
}
|
}
|
catch (error) {
|
console.error('获取用户列表失败', error)
|
}
|
finally {
|
endLoading()
|
}
|
}
|
|
// 搜索
|
function handleSearch() {
|
currentPage.value = 1
|
getUserList()
|
}
|
|
function resetSearchModel() {
|
searchModel.value = {
|
nickName: '',
|
account: '',
|
status: null,
|
}
|
}
|
|
// 重置
|
function handleReset() {
|
resetSearchModel()
|
currentPage.value = 1
|
pageSize.value = 10
|
getUserList()
|
}
|
|
// 取消
|
function handleCancel() {
|
visible.value = false
|
}
|
|
// 确认
|
function handleConfirm() {
|
if (checkedRowKeys.value.length === 0) {
|
window.$message?.warning('请至少选择一个用户')
|
return
|
}
|
|
// 从缓存中获取选中的完整对象
|
const selectedUsers: UserItem[] = []
|
checkedRowKeys.value.forEach((id) => {
|
const item = allLoadedData.value.get(id)
|
if (item) {
|
selectedUsers.push(item)
|
}
|
})
|
|
emit('confirm', selectedUsers)
|
visible.value = false
|
}
|
|
// 分页改变
|
function changePage(page: number, size: number) {
|
const isPageSizeChanged = size !== pageSize.value
|
pageSize.value = size
|
currentPage.value = isPageSizeChanged ? 1 : page
|
getUserList()
|
}
|
|
// 监听弹窗打开
|
watch(visible, (val) => {
|
if (val) {
|
resetSearchModel()
|
currentPage.value = 1
|
pageSize.value = 10
|
checkedRowKeys.value = props.existingUsers.map(v => v.id)
|
// 清空并初始化 allLoadedData
|
allLoadedData.value.clear()
|
props.existingUsers.forEach((v) => {
|
allLoadedData.value.set(v.id, v)
|
})
|
|
getUserList()
|
}
|
else {
|
// 关闭时清空
|
checkedRowKeys.value = []
|
allLoadedData.value.clear()
|
}
|
})
|
</script>
|
|
<template>
|
<NModal v-model:show="visible">
|
<NCard
|
style="
|
width: 1000px;
|
height: 80vh;
|
max-height: 800px;
|
border-radius: 8px;
|
display: flex;
|
flex-direction: column;
|
"
|
:bordered="false"
|
:segmented="{ content: true }"
|
content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;"
|
footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;"
|
>
|
<template #header>
|
<span style="font-size: 16px; font-weight: bold; color: #1F1F1F;">选择用户</span>
|
</template>
|
<template #header-extra>
|
<div text class="cursor-pointer modal-close-trigger" @click="handleCancel">
|
<span class="modal-close-text" style="color: #999;">×</span>
|
</div>
|
</template>
|
|
<div class="flex flex-col h-full overflow-hidden">
|
<!-- 搜索区域 -->
|
<div class="mb-3 flex items-end gap-24px" style="flex-shrink: 0; flex-wrap: wrap;">
|
<div class="w-300px">
|
<div class="mb-2 text-sm text-[#1F1F1F]">
|
用户名称
|
</div>
|
<NInput
|
v-model:value="searchModel.nickName"
|
placeholder="请输入"
|
clearable
|
/>
|
</div>
|
<div class="w-300px">
|
<div class="mb-2 text-sm text-[#1F1F1F]">
|
白名单账号
|
</div>
|
<NInput
|
v-model:value="searchModel.account"
|
placeholder="请输入"
|
clearable
|
/>
|
</div>
|
<div class="w-300px">
|
<div class="mb-2 text-sm text-[#1F1F1F]">
|
状态
|
</div>
|
<NSelect
|
v-model:value="searchModel.status"
|
:options="statusOptions"
|
placeholder="请搜索"
|
clearable
|
@update:show="(val: any) => isStatusOpen = val"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon" :class="{ 'arrow-icon--open': isStatusOpen }"
|
:style="{ width: '16px', height: '16px' }" alt="arrow"
|
>
|
</template>
|
</NSelect>
|
</div>
|
<div class="flex gap-8px ml-auto">
|
<NButton style="width: 84px; height: 32px" @click="handleReset">
|
重置
|
</NButton>
|
<NButton
|
type="primary"
|
style="width: 84px; height: 32px"
|
@click="handleSearch"
|
>
|
查询
|
</NButton>
|
</div>
|
</div>
|
|
<!-- 表格区域 -->
|
<div class="flex-1 overflow-hidden" style="min-height: 0">
|
<NDataTable
|
v-model:checked-row-keys="checkedRowKeys"
|
style="height: 100%"
|
flex-height
|
:columns="columns"
|
:data="listData"
|
:loading="loading"
|
:bordered="false"
|
:row-key="(row: UserItem) => row.id"
|
:row-class-name="getRowClassName"
|
:row-props="getRowProps"
|
:pagination="false"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width: 123px; height: 125px"
|
>
|
</div>
|
</template>
|
</NDataTable>
|
</div>
|
<Pagination
|
:count="count"
|
:page="currentPage"
|
:page-size="pageSize"
|
style="margin-top: 12px; flex-shrink: 0"
|
@change="changePage"
|
/>
|
</div>
|
|
<template #footer>
|
<div class="flex justify-end gap-3">
|
<NButton class="footer-btn" @click="handleCancel">
|
取消
|
</NButton>
|
<NButton type="primary" class="footer-btn" @click="handleConfirm">
|
确认
|
</NButton>
|
</div>
|
</template>
|
</NCard>
|
</NModal>
|
</template>
|
|
<style scoped lang="scss">
|
.mb-4 {
|
margin-bottom: 1rem;
|
}
|
.flex {
|
display: flex;
|
}
|
.items-end {
|
align-items: flex-end;
|
}
|
.gap-4 {
|
gap: 1rem;
|
}
|
.gap-2 {
|
gap: 0.5rem;
|
}
|
.gap-3 {
|
gap: 0.75rem;
|
}
|
.flex-1 {
|
flex: 1 1 0%;
|
}
|
.justify-end {
|
justify-content: flex-end;
|
}
|
.mb-2 {
|
margin-bottom: 0.5rem;
|
}
|
.text-sm {
|
font-size: 0.875rem;
|
line-height: 1.25rem;
|
}
|
|
.footer-btn {
|
padding: 7px 29px;
|
height: auto;
|
font-size: 13px;
|
line-height: 18px;
|
}
|
</style>
|