Files
guilixu-admin/src/views/system/user/pending/index.vue
赵忠林 503b921ece feat(system-user): 合并用户信息列并拆分审核状态列表
- 删除用户列表中的头像、手机号和真实姓名列,新增用户信息列,显示头像、姓名和手机号
- 更新用户信息列布局和样式,姓名加粗,手机号显示灰色小字
- 前后端支持按审核状态拆分用户列表与待审列表
- 后端UserParam新增auditPending字段,实现“待审核/被驳回”状态查询
- UserMapper.xml调整查询条件逻辑,支持auditStatus和auditPending条件
- 用户列表固定auditStatus为1,待审列表使用auditPending=true过滤数据
- 待审列表审核按钮显示条件扩展至状态0和2(被驳回)
- 清理用户列表的未使用auditStatus相关代码与绑定
- 前后端均通过类型检查和功能验证,保证改动正确落地
2026-07-12 00:56:47 +08:00

682 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="userId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 1300 }"
:where="defaultWhere"
cache-key="proSystemUserTable"
>
<template #toolbar>
<a-space>
<a-button
type="primary"
class="ele-btn-icon"
@click="openBatchAudit"
:disabled="selection.length === 0"
>
<template #icon>
<CheckCircleOutlined />
</template>
<span>批量审核</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
:disabled="selection.length === 0"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>批量删除</span>
</a-button>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="handleSearch"
@pressEnter="handleSearch"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar
:size="30"
:src="`${record.avatar}`"
style="margin-right: 4px"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
</template>
<template v-if="column.key === 'nickname'">
<span>{{ record.nickname }}</span>
</template>
<template v-if="column.key === 'storeName'">
<span :title="record.storeName">{{ record.storeName || '-' }}</span>
</template>
<template v-if="column.key === 'mobile'">
<span v-if="hasRole('superAdmin')">{{ record.phone }}</span>
<span v-else>{{ record.mobile }}</span>
</template>
<template v-if="column.key === 'roles'">
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-else-if="record.status === 1" color="red">冻结</a-tag>
<span v-else>-</span>
</template>
<template v-if="column.key === 'auditStatus'">
<a-tag v-if="record.auditStatus === 0" color="orange">待审核</a-tag>
<a-tag v-else-if="record.auditStatus === 1" color="green">通过</a-tag>
<a-tag v-else-if="record.auditStatus === 2" color="red">拒绝</a-tag>
<span v-else>-</span>
</template>
<template v-if="column.key === 'platform'">
<WechatOutlined v-if="record.platform === 'MP-WEIXIN'" />
<Html5Outlined v-if="record.platform === 'H5'" />
<ChromeOutlined v-if="record.platform === 'WEB'" />
</template>
<template v-if="column.key === 'balance'">
<span class="ele-text-success">
{{ formatNumber(record.balance) }}
</span>
</template>
<template v-if="column.key === 'expendMoney'">
<span class="ele-text-warning">
{{ formatNumber(record.expendMoney) }}
</span>
</template>
<template v-if="column.key === 'isAdmin'">
<a-tag v-if="record.isAdmin"></a-tag>
<a-tag v-else></a-tag>
<!-- <a-switch-->
<!-- :checked="record.isAdmin == 1"-->
<!-- @change="updateIsAdmin(record)"-->
<!-- />-->
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" v-if="record.auditStatus === 0 || record.auditStatus === 2" />
<a v-if="record.auditStatus === 0 || record.auditStatus === 2" @click.stop="openAudit(record)">审核</a>
<a-divider type="vertical" />
<!-- <a @click="resetPsw(record)">重置</a>-->
<a-popconfirm
placement="topRight"
title="确定要删除此用户吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<user-edit
v-model:visible="showEdit"
:data="current"
:organization-list="data"
@done="reload"
/>
<!-- 导入弹窗 -->
<user-import v-model:visible="showImport" @done="reload" />
<!-- 用户详情 -->
<user-info v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 审核弹窗 -->
<a-modal
v-model:visible="showAudit"
:title="auditTitle"
@ok="submitAudit"
>
<a-form :model="auditForm" layout="vertical">
<div
v-if="!batchAudit && currentAuditUser"
class="audit-user-info"
style="margin-bottom: 16px; padding: 12px; background: #f5f5f5; border-radius: 4px;"
>
<p style="margin-bottom: 8px;">
<strong>店名</strong>{{ currentAuditUser.storeName || '-' }}
</p>
<p style="margin-bottom: 8px;">
<strong>姓名</strong>{{ currentAuditUser.realName || currentAuditUser.nickname || '-' }}
</p>
<p style="margin-bottom: 0;">
<strong>所在地区</strong>
{{ [currentAuditUser.province, currentAuditUser.city, currentAuditUser.region].filter(Boolean).join(' ') || '-' }}
</p>
</div>
<a-form-item label="审核结果">
<a-radio-group v-model:value="auditForm.status">
<a-radio :value="1">通过</a-radio>
<a-radio :value="2">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审核备注">
<a-textarea
v-model:value="auditForm.remark"
:rows="3"
placeholder="请输入审核备注(拒绝时建议填写)"
/>
</a-form-item>
</a-form>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed, reactive } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
EditOutlined,
UserOutlined,
Html5Outlined,
ChromeOutlined,
WechatOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, formatNumber } from 'ele-admin-pro/es';
import UserEdit from '../components/user-edit.vue';
import UserImport from '../components/user-import.vue';
import UserInfo from '../components/user-info.vue';
import { toDateString } from 'ele-admin-pro';
import {
pageUsers,
removeUser,
removeUsers,
updateUserPassword,
updateUser,
listUsers,
auditUser
} from '@/api/system/user';
import type { User, UserParam } from '@/api/system/user/model';
import { toTreeData, uuid } from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
import { listOrganizations } from '@/api/system/organization';
import { Organization } from '@/api/system/organization/model';
import { hasRole } from '@/utils/permission';
import { getPageTitle } from '@/utils/common';
import { useUserStore } from '@/store/modules/user';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<User[]>([]);
// 当前编辑数据
const current = ref<User | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户详情
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 是否显示审核弹窗
const showAudit = ref(false);
// 是否批量审核
const batchAudit = ref(false);
// 当前审核用户(单条审核时显示详情)
const currentAuditUser = ref<User | null>(null);
// 审核弹窗标题
const auditTitle = ref('审核用户');
// 审核表单
const auditForm = reactive({
userId: undefined as number | undefined,
status: 1,
remark: ''
});
const searchText = ref('');
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 加载机构
listOrganizations()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.organizationId;
d.value = d.organizationId;
d.title = d.organizationName;
if (typeof d.key === 'number') {
eks.push(d.key);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'organizationId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].key === 'number') {
selectedRowKeys.value = [list[0].key];
}
// current.value = list[0];
} else {
selectedRowKeys.value = [];
// current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'userId',
width: 90,
showSorterTooltip: false
},
{
title: '店名',
key: 'shopName',
dataIndex: 'shopName',
width: 180
},
{
title: '手机号码',
dataIndex: 'mobile',
key: 'mobile',
showSorterTooltip: false
},
{
title: '真实姓名',
dataIndex: 'realName',
showSorterTooltip: false
},
{
title: '省份',
dataIndex: 'province',
key: 'province',
},
{
title: '城市',
dataIndex: 'city',
key: 'city',
showSorterTooltip: false
},
{
title: '地区',
dataIndex: 'region',
key: 'region',
showSorterTooltip: false
},
{
title: '邮箱',
dataIndex: 'email',
align: 'center',
hideInTable: true,
showSorterTooltip: false
},
{
title: '证件号码',
dataIndex: 'idCard',
align: 'center',
hideInTable: true
},
{
title: '出生日期',
dataIndex: 'birthday',
key: 'birthday',
hideInTable: true
},
{
title: '邮箱认证',
dataIndex: 'emailVerified',
hideInTable: true,
showSorterTooltip: false,
customRender: ({ text }) => ['未认证', '已认证'][text]
},
{
title: '实名认证',
dataIndex: 'certification',
sorter: true,
hideInTable: true,
customRender: ({ text }) => ['未认证', '已认证'][text]
},
{
title: '审核状态',
dataIndex: 'auditStatus',
key: 'auditStatus',
align: 'center',
width: 100
},
{
title: '注册时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 160,
fixed: 'right',
align: 'center'
}
]);
// 默认搜索条件
const defaultWhere = reactive({
username: '',
nickname: ''
});
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
const params = {
...where,
keywords: searchText.value,
auditPending: true
};
return pageUsers({ page, limit, ...params, ...orders });
};
/* 搜索 */
const reload = (where?: UserParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: User) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.value = true;
};
/* 删除单个 */
const remove = (row: User) => {
const hide = messageLoading('请求中..', 0);
removeUser(row.userId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的用户吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeUsers(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateUserPassword(row.userId, password)
.then(() => {
hide();
Modal.success({
title: '密码重置成功',
content: `新密码:${password}(请妥善保管并及时通知用户)`
});
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 修改用户状态 */
const updateIsAdmin = (row: User) => {
row.isAdmin = !row.isAdmin;
updateUser(row)
.then((msg) => {
message.success(msg);
})
.catch((e) => {
message.error(e.message);
});
};
/* 打开单条审核弹窗 */
const openAudit = (row: User) => {
batchAudit.value = false;
currentAuditUser.value = row;
auditForm.userId = row.userId;
auditForm.status = 1;
auditForm.remark = '';
auditTitle.value = `审核用户 - ${row.nickname || row.username}`;
showAudit.value = true;
};
/* 打开批量审核弹窗 */
const openBatchAudit = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
batchAudit.value = true;
currentAuditUser.value = null;
auditForm.userId = undefined;
auditForm.status = 1;
auditForm.remark = '';
auditTitle.value = `批量审核(已选 ${selection.value.length} 条)`;
showAudit.value = true;
};
/* 提交审核 */
const submitAudit = () => {
const hide = message.loading('提交中..', 0);
const doOne = (userId: number) =>
auditUser(userId, { status: auditForm.status, remark: auditForm.remark });
const tasks = batchAudit.value
? selection.value.map((d) => doOne(d.userId as number))
: [doOne(auditForm.userId as number)];
Promise.all(tasks)
.then(() => {
hide();
message.success('审核完成');
showAudit.value = false;
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 搜索处理 */
const handleSearch = () => {
reload();
};
/* 导出功能 */
const handleExport = async () => {
if (loading.value) {
return;
}
loading.value = true;
message.loading('正在准备导出数据...', 0);
try {
const array: (string | number)[][] = [
[
'用户ID',
'头像',
'手机号码',
'真实姓名',
'昵称',
'性别',
'邮箱',
'可用余额',
'可用积分',
'注册来源',
'注册时间'
]
];
// 构建查询参数
const queryParams = {
keywords: searchText.value
};
// 按搜索结果导出
const list = await listUsers(queryParams);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
loading.value = false;
return;
}
list.forEach((user: User) => {
array.push([
`${user.userId || ''}`,
`${user.avatar ? '有头像' : '无头像'}`,
`${user.phone || ''}`,
`${user.realName || ''}`,
`${user.nickname || ''}`,
`${user.sex === '1' ? '男' : user.sex === '2' ? '女' : ''}`,
`${user.email || ''}`,
`${user.balance || '0'}`,
`${user.points || '0'}`,
`${user.platform || ''}`,
`${user.createTime || ''}`
]);
});
const sheetName = `导出用户列表${dayjs(new Date()).format('YYYYMMDD')}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 }, // 用户ID
{ wch: 10 }, // 头像
{ wch: 15 }, // 手机号码
{ wch: 15 }, // 真实姓名
{ wch: 15 }, // 昵称
{ wch: 8 }, // 性别
{ wch: 20 }, // 邮箱
{ wch: 12 }, // 可用余额
{ wch: 12 }, // 可用积分
{ wch: 12 }, // 注册来源
{ wch: 20 } // 注册时间
];
message.destroy();
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
loading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
loading.value = false;
message.destroy();
message.error(error.message || '导出失败,请重试');
}
};
</script>
<script lang="ts">
export default {
name: 'SystemUser'
};
</script>
<style lang="less" scoped>
.user-box {
display: flex;
align-items: center;
.user-info {
display: flex;
flex-direction: column;
align-items: start;
}
}
</style>