feat(system): 实现用户审核功能

- 新增用户模型字段:auditStatus、auditRemark、auditTime
- 添加用户审核接口 auditUser,支持单条审核
- 用户列表页增加审核状态筛选项和对应列展示
- 列表行内仅待审核用户显示“审核”操作链接
- 工具栏添加批量审核按钮,实现批量审核功能
- 审核弹窗支持选择通过或拒绝及填写备注
- 审核提交支持单条和批量调用接口
- 保持代码类型检查无新增错误,符合项目字段命名规范
This commit is contained in:
2026-07-11 19:43:24 +08:00
parent dceb030712
commit bf259ea8ff
4 changed files with 172 additions and 7 deletions

View File

@@ -35,6 +35,15 @@
- 不改:`admin` 第294行 `isAdmin: 1` 是查询条件(`UserParam.isAdmin?: number`),语义正确。
- 验证:`vue-tsc --noEmit` 确认改动无新增错误;旧写法已无残留。
### 审核功能19:27system/user 用户审核
- 需求audit_status=0待审核时实现审核功能。已与用户确认后端接口已有`POST /system/user/audit/{id}`,入参 `{status, remark}`,与 shop 模块一致);状态 `0待审核 / 1通过 / 2拒绝`;支持行内单条 + 勾选批量审核。
- 改动文件:
- `src/api/system/user/model/index.ts`User 加 `auditStatus / auditRemark / auditTime`UserParam 加 `auditStatus`;新增 `UserAuditRequest { status: number; remark?: string }`
- `src/api/system/user/index.ts`:新增 `auditUser(id, data)``POST /system/user/audit/{id}`
- `src/views/system/user/index.vue`高级筛选加「审核状态」下拉列配置加「审核状态」列a-tag 展示);行内仅 `auditStatus===0` 显示「审核」链接(`@click.stop` 防冒泡触发行详情);工具栏加「批量审核」按钮;新增 `a-modal` 审核弹窗radio 通过/拒绝 + 备注 textarea脚本 `openAudit / openBatchAudit / submitAudit`(批量用 `Promise.all` 循环调单条接口)。
- 验证vue-tsc 仅剩项目既有错误(`updateUserRecommend` 的 form any、`$router``loginUser/updateIsAdmin` 未使用等),本功能无新增类型错误。
- 注意点:字段用 camelCase `auditStatus`(与项目约定一致);若后端实际返回下划线 `audit_status` 需同步改模型字段名。批量审核为循环调单条接口(不依赖后端批量接口)。
### 备注
- 方案中的「Batch 3 长期维护项」admin/user 组件合并、导入模板配置化、search.vue 重命名)本次未执行,属可选重构,风险较高,留待后续。
- Grep 工具在当前环境因 pcre2 动态库缺失报错,改用 bash grep 替代。

View File

@@ -1,6 +1,6 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { User, UserParam } from './model';
import type { User, UserParam, UserAuditRequest } from './model';
import { SERVER_API_URL } from '@/config/setting';
/**
@@ -293,3 +293,19 @@ export async function exportUsers(params?: UserParam) {
});
return res.data;
}
/**
* 审核用户
* @param id 用户id
* @param data 审核结果
*/
export async function auditUser(id: number, data: UserAuditRequest) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/system/user/audit/' + id,
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -129,6 +129,12 @@ export interface User {
followers?: number;
// 推荐人ID
dealerId?: number;
// 审核状态: 0待审核 1通过 2拒绝
auditStatus?: number;
// 审核备注
auditRemark?: string;
// 审核时间
auditTime?: string;
}
/**
@@ -158,4 +164,16 @@ export interface UserParam extends PageParam {
showProfile?: boolean;
isStaff?: boolean;
templateId?: number;
// 审核状态: 0待审核 1通过 2拒绝
auditStatus?: number;
}
/**
* 用户审核请求
*/
export interface UserAuditRequest {
// 审核状态: 1-通过 2-拒绝
status: number;
// 审核备注
remark?: string;
}

View File

@@ -40,6 +40,18 @@
<a-select-option :value="2">普通用户</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchWhere.auditStatus"
placeholder="全部"
allow-clear
style="width: 140px"
>
<a-select-option :value="0">待审核</a-select-option>
<a-select-option :value="1">通过</a-select-option>
<a-select-option :value="2">拒绝</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="reload()">搜索</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
@@ -84,6 +96,17 @@
:disabled="selection.length === 0"
>重置密码
</a-button>
<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"
@@ -142,6 +165,12 @@
<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'" />
@@ -168,7 +197,9 @@
<template v-if="column.key === 'action'">
<div>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-divider type="vertical" v-if="record.auditStatus === 0" />
<a v-if="record.auditStatus === 0" @click.stop="openAudit(record)">审核</a>
<a-divider type="vertical" v-if="record.auditStatus === 0" />
<!-- <a @click="resetPsw(record)">重置</a>-->
<!-- <a-divider type="vertical" />-->
<a-popconfirm
@@ -194,6 +225,28 @@
<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">
<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>
@@ -209,7 +262,8 @@
Html5Outlined,
ChromeOutlined,
WechatOutlined,
ExclamationCircleOutlined
ExclamationCircleOutlined,
CheckCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
@@ -227,7 +281,8 @@
removeUsers,
updateUserPassword,
updateUser,
listUsers
listUsers,
auditUser
} from '@/api/system/user';
import type { User, UserParam } from '@/api/system/user/model';
import { toTreeData, uuid } from 'ele-admin-pro';
@@ -258,17 +313,31 @@
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 是否显示审核弹窗
const showAudit = ref(false);
// 是否批量审核
const batchAudit = ref(false);
// 审核弹窗标题
const auditTitle = ref('审核用户');
// 审核表单
const auditForm = reactive({
userId: undefined as number | undefined,
status: 1,
remark: ''
});
const searchText = ref('');
// 高级筛选条件
const searchWhere = reactive({
roleId: undefined,
platform: undefined,
isAdmin: undefined
isAdmin: undefined,
auditStatus: undefined
});
const resetSearch = () => {
searchWhere.roleId = undefined;
searchWhere.platform = undefined;
searchWhere.isAdmin = undefined;
searchWhere.auditStatus = undefined;
reload();
};
const userStore = useUserStore();
@@ -457,6 +526,13 @@
align: 'center',
width: 90
},
{
title: '审核状态',
dataIndex: 'auditStatus',
key: 'auditStatus',
align: 'center',
width: 100
},
{
title: '管理员',
key: 'isAdmin',
@@ -477,7 +553,7 @@
{
title: '操作',
key: 'action',
width: 120,
width: 160,
fixed: 'right',
align: 'center'
}
@@ -496,7 +572,8 @@
keywords: searchText.value,
roleId: searchWhere.roleId,
platform: searchWhere.platform,
isAdmin: searchWhere.isAdmin || undefined
isAdmin: searchWhere.isAdmin || undefined,
auditStatus: searchWhere.auditStatus || undefined
};
return pageUsers({ page, limit, ...params, ...orders });
};
@@ -604,6 +681,51 @@
});
};
/* 打开单条审核弹窗 */
const openAudit = (row: User) => {
batchAudit.value = false;
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;
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();