feat(special-zone): 优化专区白名单弹窗及添加/移除功能

- 修复白名单弹窗复选框不显示的问题,替换 ele-pro-table 为 antd a-table 实现
- 实现弹窗打开即加载已选用户并勾选,完善分页及搜索功能
- 解决弹窗搜索事件传参错误导致后端参数异常的问题
- 修复保存白名单时报唯一键冲突异常,改用物理删除规避逻辑删除死锁
- 重构白名单弹窗为商品式交互,支持即时单条添加和移除,无需覆盖式保存
- 后端新增接口支持白名单用户追加和删除,前端调用对应新增接口
- 统一用户模型字段,优化查询及展示,提升用户体验
- 清理前端无用代码,简化白名单弹窗调用逻辑,减少页面复杂度
This commit is contained in:
2026-08-17 03:10:30 +08:00
parent 6e57fba192
commit 4ba0427417
4 changed files with 246 additions and 151 deletions
+33 -5
View File
@@ -4,9 +4,9 @@ import type {
HomeSection,
HomeSectionParam,
SectionPermission,
SectionGoods,
SectionUser
SectionGoods
} from './model';
import type { User } from '@/api/system/user/model';
import { MODULES_API_URL } from '@/config/setting';
/**
@@ -96,10 +96,10 @@ export async function removeHomeSection(id?: number) {
}
/**
* 查询专区白名单用户
* 查询专区白名单用户(返回用户详情:userId/realName/mobile/nickname/organizationName
*/
export async function listSectionUsers(id: number) {
const res = await request.get<ApiResult<SectionUser[]>>(
const res = await request.get<ApiResult<User[]>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users'
);
if (res.data.code === 0 && res.data.data) {
@@ -109,7 +109,7 @@ export async function listSectionUsers(id: number) {
}
/**
* 保存专区白名单用户(覆盖式)
* 保存专区白名单用户(覆盖式,保留以备兼容
*/
export async function setSectionUsers(id: number, userIds: number[]) {
const res = await request.put<ApiResult<unknown>>(
@@ -122,6 +122,34 @@ export async function setSectionUsers(id: number, userIds: number[]) {
return Promise.reject(new Error(res.data.message));
}
/**
* 添加白名单用户到专区(单条/批量追加)
*/
export async function addSectionUsers(id: number, userIds: number[]) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
userIds
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 从专区移除白名单用户(单条/批量)
*/
export async function removeSectionUsers(id: number, userIds: number[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
{ data: userIds }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询专区商品分页
*/
@@ -1,161 +1,185 @@
<!-- 白名单用户多选弹窗 -->
<!-- 白名单用户管理弹窗商品式打开只列已添加搜索手机/姓名昵称追加单条即时增删 -->
<template>
<ele-modal
<a-modal
:width="820"
:visible="visible"
:maskClosable="false"
title="设置专区白名单用户"
:body-style="{ paddingBottom: '28px' }"
title="专区白名单用户"
:footer="null"
:body-style="{ paddingBottom: '16px' }"
@update:visible="updateVisible"
@ok="confirm"
>
<ele-pro-table
ref="tableRef"
row-key="userId"
:datasource="datasource"
:columns="columns"
:row-selection="rowSelection"
selection-type="checkbox"
:pagination="true"
:page-size="10"
>
<template #toolbar>
<a-space>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="姓名/手机号/昵称"
style="width: 220px"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
</ele-pro-table>
<div class="mt-2 ele-text-secondary">
已选 {{ selectedRowKeys.length }}
<!-- 搜索可添加的用户 -->
<div style="margin-bottom: 12px; display: flex; gap: 8px; align-items: center;">
<a-input-search
v-model:value="searchText"
placeholder="搜索手机号码 / 姓名 / 昵称"
allow-clear
style="flex: 1; max-width: 360px;"
@search="searchToAdd"
@pressEnter="searchToAdd"
/>
<a-button type="primary" :loading="searchLoading" @click="searchToAdd">
搜索
</a-button>
</div>
</ele-modal>
<!-- 搜索结果点击 + 添加 -->
<div
v-if="candidates.length > 0"
style="margin-bottom: 12px; border: 1px dashed #d9d9d9; border-radius: 6px; padding: 8px;"
>
<div style="font-size: 12px; color: #999; margin-bottom: 6px;">搜索结果点击 + 添加</div>
<div
v-for="u in candidates"
:key="u.userId"
style="display: flex; align-items: center; padding: 4px 0; gap: 8px; border-bottom: 1px solid #f0f0f0;"
>
<span style="flex: 1; font-size: 13px;">{{ u.realName || '-' }}</span>
<span style="color: #999; font-size: 12px;">{{ u.mobile || '-' }}</span>
<span style="color: #999; font-size: 12px;">{{ u.nickname || '' }}</span>
<a-button type="link" size="small" style="padding: 0;" @click="addOne(u)">+ 添加</a-button>
</div>
</div>
<!-- 已添加的白名单用户 -->
<a-divider style="margin: 8px 0;">已添加{{ addedUsers.length }}</a-divider>
<a-spin :spinning="addedLoading">
<a-table
v-if="addedUsers.length > 0"
row-key="userId"
:columns="columns"
:data-source="addedUsers"
:pagination="false"
:scroll="{ y: 320 }"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-popconfirm title="确定移出该白名单用户吗?" @confirm="removeOne(record)">
<a-button type="link" danger size="small">移除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<a-empty v-else description="暂无白名单用户,请用上方搜索添加" />
</a-spin>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { listSectionUsers, addSectionUsers, removeSectionUsers } from '@/api/shop/shopZone';
import { pageUsers } from '@/api/system/user';
import { EleProTable } from 'ele-admin-pro';
import type { User } from '@/api/system/user/model';
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 专区ID
sectionId?: number;
// 已选用户ID
selectedUserIds?: number[];
}>();
const emit = defineEmits<{
(e: 'confirm', userIds: number[]): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 搜索内容
const searchText = ref<string>('');
// 已添加的白名单用户
const addedUsers = ref<User[]>([]);
const addedLoading = ref(false);
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 搜索结果(候选)
const searchText = ref('');
const candidates = ref<User[]>([]);
const searchLoading = ref(false);
// 表格配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 80
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center'
},
{
title: '手机号码',
dataIndex: 'mobile',
key: 'mobile',
align: 'center'
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center'
},
{
title: '所属部门',
dataIndex: 'organizationName',
key: 'organizationName',
align: 'center'
}
]);
const columns = [
{ title: 'ID', dataIndex: 'userId', key: 'userId', align: 'center', width: 80 },
{ title: '姓名', dataIndex: 'realName', key: 'realName', align: 'center' },
{ title: '手机号码', dataIndex: 'mobile', key: 'mobile', align: 'center' },
{ title: '昵称', dataIndex: 'nickname', key: 'nickname', align: 'center' },
{ title: '所属部门', dataIndex: 'organizationName', key: 'organizationName', align: 'center' },
{ title: '操作', key: 'action', align: 'center', width: 90 }
];
// 已选用户ID
const selectedRowKeys = ref<number[]>([]);
// 多选配置(与 shopCoupon 一致:computed + columnWidth
const rowSelection = computed(() => ({
columnWidth: 48,
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: (string | number)[]) => {
selectedRowKeys.value = keys.map((k) => Number(k));
}
}));
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if (searchText.value) {
where.keywords = searchText.value;
}
return pageUsers({
keywords: searchText.value,
...where,
...orders,
page,
limit
});
/* 加载已添加的白名单 */
const loadAdded = () => {
if (!props.sectionId) return;
addedLoading.value = true;
listSectionUsers(props.sectionId)
.then((list: User[]) => {
addedUsers.value = list || [];
})
.catch((e: Error) => {
message.error(e.message || '加载白名单失败');
})
.finally(() => {
addedLoading.value = false;
});
};
/* 搜索 */
const reload = () => {
tableRef?.value?.reload({ page: 1 });
/* 搜索可添加的用户(排除已添加的) */
const searchToAdd = () => {
const kw = searchText.value.trim();
if (!kw) {
message.warning('请输入手机号码 / 姓名 / 昵称');
return;
}
searchLoading.value = true;
pageUsers({ keywords: kw, page: 1, limit: 20 } as any)
.then((res: any) => {
const addedIds = new Set(addedUsers.value.map((u) => u.userId));
candidates.value = (res?.list || []).filter((u: User) => !addedIds.has(u.userId));
if (candidates.value.length === 0) {
message.info('没有可添加的新用户');
}
})
.catch((e: Error) => {
message.error(e.message || '搜索失败');
})
.finally(() => {
searchLoading.value = false;
});
};
/* 确认保存 */
const confirm = () => {
emit('confirm', [...selectedRowKeys.value]);
updateVisible(false);
/* 添加单个用户到白名单 */
const addOne = (u: User) => {
if (!props.sectionId) return;
addSectionUsers(props.sectionId, [u.userId as number])
.then((msg: string) => {
message.success(msg || '添加成功');
candidates.value = candidates.value.filter((c) => c.userId !== u.userId);
loadAdded();
})
.catch((e: Error) => {
message.error(e.message || '添加失败');
});
};
// 打开弹窗时初始化已选
/* 从白名单移除单个用户 */
const removeOne = (u: User) => {
if (!props.sectionId) return;
removeSectionUsers(props.sectionId, [u.userId as number])
.then((msg: string) => {
message.success(msg || '移除成功');
loadAdded();
})
.catch((e: Error) => {
message.error(e.message || '移除失败');
});
};
// 打开弹窗时加载已添加名单
watch(
() => props.visible,
(visible) => {
if (visible) {
selectedRowKeys.value = props.selectedUserIds
? [...props.selectedUserIds]
: [];
if (tableRef.value) {
// 等待表格挂载后刷新
setTimeout(() => reload(), 0);
}
searchText.value = '';
candidates.value = [];
loadAdded();
}
},
{ immediate: true }
+2 -26
View File
@@ -87,12 +87,10 @@
<!-- 编辑弹窗 -->
<ZoneEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 白名单用户选择弹窗 -->
<!-- 白名单用户管理弹窗 -->
<UserSelectModal
v-model:visible="showUser"
:sectionId="currentSectionId"
:selectedUserIds="currentUserIds"
@confirm="onSaveUsers"
/>
<!-- 专区商品抽屉 -->
@@ -230,8 +228,6 @@
pageHomeSections,
removeHomeSection,
updateHomeSection,
listSectionUsers,
setSectionUsers,
listSectionGoods,
addSectionGoods,
removeSectionGoods,
@@ -254,7 +250,6 @@
// 白名单弹窗
const showUser = ref(false);
const currentSectionId = ref<number>(0);
const currentUserIds = ref<number[]>([]);
// 商品抽屉
const showGoods = ref(false);
@@ -420,26 +415,7 @@
/* 打开白名单弹窗 */
const openUsers = (row: HomeSection) => {
currentSectionId.value = row.sectionId || 0;
listSectionUsers(row.sectionId || 0)
.then((list) => {
currentUserIds.value = (list || []).map((u) => u.userId || 0).filter(Boolean);
showUser.value = true;
})
.catch((e) => {
message.error(e.message);
});
};
/* 保存白名单 */
const onSaveUsers = (userIds: number[]) => {
setSectionUsers(currentSectionId.value, userIds)
.then((msg) => {
message.success(msg || '白名单已保存');
showUser.value = false;
})
.catch((e) => {
message.error(e.message);
});
showUser.value = true;
};
/* 打开商品抽屉 */