Pārlūkot izejas kodu

feat: api-用户管理

zhangjie 4 dienas atpakaļ
vecāks
revīzija
386884c6fe

+ 36 - 13
src/api/types/user.ts

@@ -5,15 +5,26 @@ export interface LoginData {
   password: string;
 }
 
+// 登录名	名称	来源	角色	状态	关联账号
 export interface UserItem {
   id: number;
-  loginName: string; // 登录名
-  name: string; // 名称
-  employeeId?: string; // 工号 (可选)
-  source: string; // 来源 (例如:内部用户)
-  role: RoleType; // 角色 (例如:学校管理员, 扫描员)
-  enable: boolean; // 状态 (启用/禁用)
-  wechatLinked?: boolean; // 关联微信 (可选, 可能为显示字段)
+  // 登录名
+  loginName: string;
+  // 名称
+  name: string;
+  // 工号 (可选)
+  empno: string;
+  // 来源 (例如:内部用户)
+  source: string;
+  // 角色 (例如:学校管理员, 扫描员)
+  role: RoleType;
+  // 状态 (启用/禁用)
+  enable: boolean;
+  // 关联账号
+  relatedAccount: string;
+  schoolId: number;
+  lastLoginIp: string;
+  description: string;
 }
 export type UserListPageRes = PageResult<UserItem>;
 
@@ -22,22 +33,28 @@ export interface UserListFilter {
   loginName?: string; // 按登录名搜索
   enable: boolean; // 按状态筛选
   role?: RoleType; // 按角色筛选
+  source?: string; // 按来源筛选
 }
 export type UserListPageParam = PageParams<UserListFilter>;
 
 export interface UserUpdateParam {
   id?: number;
+  // 登录名
   loginName: string;
+  // 名称
   name: string;
-  employeeId: string;
+  // 工号 (可选)
+  empno: string;
+  // 状态 (启用/禁用)
   enable: boolean;
+  // 角色 (例如:学校管理员, 扫描员)
   role: RoleType;
   password: string;
 }
 
 export interface ResetPasswordParam {
   ids: number[];
-  password?: string; // 如果不传,后端可能会生成随机密码
+  password: string; // 如果不传,后端可能会生成随机密码
 }
 export interface EnableUserParam {
   ids: number[];
@@ -52,10 +69,16 @@ export interface UpdatePwdData {
 
 export interface BatchCreateUserParam {
   examId: number;
+  // 角色
   role: RoleType;
-  namingRule: string;
-  accountsPerGroup: number;
-  randomPassword: boolean;
+  // 命名规则
+  prefix: string;
+  // 每分组账号数
+  number: number;
+  // 是否随机密码
+  random: boolean;
+  // 密码
   password?: string;
-  subjectIds: number[];
+  // 科目
+  subjectCodeString: string;
 }

+ 82 - 9
src/api/user.ts

@@ -1,4 +1,4 @@
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import { UserState } from '@/store/modules/user/types';
 import type {
   LoginData,
@@ -29,25 +29,98 @@ export function userLogout() {
 export function userListPage(
   params: UserListPageParam
 ): Promise<UserListPageRes> {
-  return axios.get('/api/user/list', { params });
+  return axios.get('/api/admin/user/query', { params });
 }
 // 新增或编辑用户
 export function updateUser(data: UserUpdateParam): Promise<any> {
-  return axios.post('/api/user/update', data);
+  if (data.id) {
+    return axios.post('/api/admin/user/update', data);
+  }
+  return axios.post('/api/admin/user/save', data);
 }
 // 批量新增用户
-export function batchAddUser(datas: BatchCreateUserParam[]): Promise<any> {
-  return axios.post('/api/user/addList', datas);
+export function batchAddUser(datas: BatchCreateUserParam): Promise<any> {
+  return axios.post('/api/admin/user/batchSave', datas);
 }
-// 删除用户
+// 删除用户: 暂时没有此功能
 export function deleteUser(id: number): Promise<any> {
-  return axios.post(`/api/user/delete/${id}`);
+  return axios.post(`/api/admin/user/delete/${id}`);
 }
 // 重置用户密码
 export function resetUserPassword(data: ResetPasswordParam): Promise<any> {
-  return axios.post('/api/user/resetPassword', data);
+  return axios.post('/api/admin/user/reset', data);
 }
 // 启用禁用用户
 export function enableUser(datas: EnableUserParam): Promise<any> {
-  return axios.post(`/api/user/enable`, datas);
+  return axios.post(`/api/admin/user/enable`, datas);
+}
+// 导入评卷员班级-导入模板下载
+export function markerClassTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/class/import',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+// 导出用户
+export function exportUser(
+  params: Record<string, any>
+): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/export',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}
+
+// 按考试导出用户
+export function exportUserByExam(
+  params: Record<string, any>
+): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/exportExam',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}
+// 按考试导出科目分表用户
+export function exportUserBySubject(
+  params: Record<string, any>
+): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/exportSubject',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}
+// 绑定考生导入模板下载
+export function bindStudentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/student/template',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+// 科组长-复核员导入模板下载
+export function headerInspectorTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/user/subject/template',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
 }

+ 1 - 1
src/components/svg-icon/index.vue

@@ -62,7 +62,7 @@
     defineProps<{
       name: string;
       fill?: string;
-      inheritColor: boolean;
+      inheritColor?: boolean;
     }>(),
     {
       fill: 'currentColor',

+ 15 - 6
src/constants/enumerate.ts

@@ -12,17 +12,26 @@ export const ENABLE_TYPE = {
 // 基础 -------------->
 // 角色
 export const ROLE_TYPE = {
+  // "SYS_ADMIN"|"SCANNER"|"SUBJECT_HEADER"|"INSPECTOR"|"MARKER"|"SCHOOL_VIEWER"|"SCHOOL_ADMIN"|"COLLEGE_ADMIN"|"SCAN_ADMIN"
+  // SYS_ADMIN: '系统管理员',
   SCANNER: '扫描员',
-  COORDINATOR: '科组长',
-  REVIEWER: '复核员',
-  EVALUATOR: '评卷员',
-  ADMIN: '学校管理员',
-  SCHOOL: '学校查询员',
-  COLLEGE: '学院查询员',
+  SUBJECT_HEADER: '科组长',
+  INSPECTOR: '复核员',
+  MARKER: '评卷员',
+  SCHOOL_ADMIN: '学校管理员',
+  SCHOOL_VIEWER: '学校查询员',
+  COLLEGE_ADMIN: '学院查询员',
 };
 
 export type RoleType = keyof typeof ROLE_TYPE;
 
+// 用户来源:"INTERNAL"|"EXTERNAL"
+export const USER_SOURCE = {
+  INTERNAL: '内部用户',
+  EXTERNAL: '外部用户',
+};
+export type UserSource = keyof typeof USER_SOURCE;
+
 // 考试类型
 export const EXAM_TYPE = {
   SCAN: '扫描图片类型',

+ 53 - 0
src/utils/download-export.ts

@@ -0,0 +1,53 @@
+import { ElMessage } from 'element-plus';
+import {
+  markerClassTemplate,
+  exportUserByExam,
+  exportUserBySubject,
+  exportUser,
+  bindStudentTemplate,
+  headerInspectorTemplate,
+} from '@/api/user';
+import useLoading from '@/hooks/loading';
+
+import { downloadByApi } from './download';
+
+const { loading, setLoading } = useLoading();
+
+const downloadConfig = {
+  // 评卷员班级导入模板下载
+  markerClassTemplate,
+  // 按考试导出用户
+  exportUserByExam,
+  // 按考试导出科目分表用户
+  exportUserBySubject,
+  // 导出用户
+  exportUser,
+  // 绑定考生导入模板下载
+  bindStudentTemplate,
+  // 科组长-复核员导入模板下载
+  headerInspectorTemplate,
+};
+
+type DownloadType = keyof typeof downloadConfig;
+
+export async function downloadExport(
+  type: DownloadType,
+  params?: Record<string, any>
+) {
+  if (loading.value) return;
+
+  if (!downloadConfig[type]) {
+    ElMessage.error('下载类型错误!');
+    return;
+  }
+
+  setLoading(true);
+  const res = await downloadByApi(() => downloadConfig[type](params)).catch(
+    (e) => {
+      ElMessage.error(e || '下载失败,请重新尝试!');
+    }
+  );
+  setLoading(false);
+  if (!res) return;
+  ElMessage.success('下载成功!');
+}

+ 3 - 0
src/utils/filter.ts

@@ -6,6 +6,7 @@ import {
   MARKING_MODE,
   LOG_TYPE,
   OPTIONAL_SCORE_RULE,
+  USER_SOURCE,
 } from '@/constants/enumerate';
 import { formatDate } from './utils';
 
@@ -24,6 +25,8 @@ export const dictFilter = {
   optionalScoreRule: (val: string) =>
     OPTIONAL_SCORE_RULE[val as keyof typeof OPTIONAL_SCORE_RULE] ||
     DEFAULT_LABEL,
+  userSource: (val: string) =>
+    USER_SOURCE[val as keyof typeof USER_SOURCE] || DEFAULT_LABEL,
 };
 
 // 时间戳过滤器

+ 5 - 5
src/views/user/ModifyUser.vue

@@ -26,8 +26,8 @@
       <el-form-item label="名称" prop="name">
         <el-input v-model="formModel.name" placeholder="请输入名称" />
       </el-form-item>
-      <el-form-item label="工号" prop="employeeId">
-        <el-input v-model="formModel.employeeId" placeholder="请输入工号" />
+      <el-form-item label="工号" prop="empno">
+        <el-input v-model="formModel.empno" placeholder="请输入工号" />
       </el-form-item>
       <el-form-item label="密码" prop="password">
         <el-input
@@ -37,8 +37,8 @@
           placeholder="请输入密码"
         />
       </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-select v-model="formModel.status" placeholder="请选择状态">
+      <el-form-item label="状态" prop="enable">
+        <el-select v-model="formModel.enable" placeholder="请选择状态">
           <el-option label="启用" :value="true" />
           <el-option label="禁用" :value="false" />
         </el-select>
@@ -132,7 +132,7 @@
     password: [
       { required: !isEdit.value, validator: validatePass, trigger: 'change' },
     ],
-    status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    enable: [{ required: true, message: '请选择状态', trigger: 'change' }],
     role: [{ required: true, message: '请选择角色', trigger: 'change' }],
   };
 

+ 53 - 41
src/views/user/UserManage.vue

@@ -15,15 +15,19 @@
           clearable
         />
       </el-form-item>
-      <el-form-item label="状态">
+      <el-form-item label="来源">
         <el-select
-          v-model="searchModel.enable"
-          placeholder="请选择状态"
+          v-model="searchModel.source"
+          placeholder="请选择来源"
           clearable
-          style="width: 100px"
+          style="width: 120px"
         >
-          <el-option label="启用" :value="true" />
-          <el-option label="禁用" :value="false" />
+          <el-option
+            v-for="(val, key) in USER_SOURCE"
+            :key="key"
+            :label="val"
+            :value="key"
+          />
         </el-select>
       </el-form-item>
       <el-form-item label="角色">
@@ -41,6 +45,17 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="状态">
+        <el-select
+          v-model="searchModel.enable"
+          placeholder="请选择状态"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="启用" :value="true" />
+          <el-option label="禁用" :value="false" />
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-space wrap>
           <el-button type="primary" @click="toPage(1)">查询</el-button>
@@ -69,11 +84,11 @@
             </el-button>
             <template #dropdown>
               <el-dropdown-menu>
-                <el-dropdown-item command="kzz">导出</el-dropdown-item>
-                <el-dropdown-item command="fhy"
+                <el-dropdown-item command="exportUser">导出</el-dropdown-item>
+                <el-dropdown-item command="exportUserByExam"
                   >按考试导出全部</el-dropdown-item
                 >
-                <el-dropdown-item command="fhy"
+                <el-dropdown-item command="exportUserBySubject"
                   >按考试导出科目拆分表</el-dropdown-item
                 >
               </el-dropdown-menu>
@@ -117,10 +132,14 @@
       <el-table-column prop="loginName" label="登录名" />
       <el-table-column prop="name" label="名称" />
       <el-table-column prop="employeeId" label="工号" />
-      <el-table-column prop="source" label="来源" />
+      <el-table-column prop="source" label="来源">
+        <template #default="{ row }">
+          {{ dictFilter.userSource(row.source) }}
+        </template>
+      </el-table-column>
       <el-table-column prop="role" label="角色">
         <template #default="{ row }">
-          {{ formatRole(row.role) }}
+          {{ dictFilter.role(row.role) }}
         </template>
       </el-table-column>
       <el-table-column prop="enable" label="状态">
@@ -161,19 +180,25 @@
   <!-- 导入用户 -->
   <ImportDialog
     ref="importUserDialogRef"
-    title="导入用户"
+    :title="importData.isHeader ? '导入科组长' : '导入复核员'"
     upload-url="/api/admin/site/import"
+    :upload-data="importData"
     :format="['xls', 'xlsx']"
-    :download-handle="downloadTemplate"
-    download-filename="用户导入模板.xlsx"
+    :download-handle="
+      () =>
+        downloadExport('headerInspectorTemplate', {
+          isHeader: importData.isHeader,
+        })
+    "
+    download-filename="导入模板.xlsx"
   />
   <!-- 导入班级评卷员 -->
   <ImportDialog
     ref="importMarkerDialogRef"
     title="导入班级评卷员"
-    upload-url="/api/admin/site/import"
+    upload-url="/api/admin/user/class/import"
     :format="['xls', 'xlsx']"
-    :download-handle="downloadTemplate"
+    :download-handle="() => downloadExport('markerClassTemplate')"
     download-filename="导入班级评卷员模板.xlsx"
   />
 
@@ -189,7 +214,9 @@
   import type { UserItem, UserListFilter } from '@/api/types/user';
   import useTable from '@/hooks/table';
   import { modalConfirm } from '@/utils/ui';
-  import { ROLE_TYPE } from '@/constants/enumerate';
+  import { dictFilter } from '@/utils/filter';
+  import { ROLE_TYPE, USER_SOURCE } from '@/constants/enumerate';
+  import { downloadExport } from '@/utils/download-export';
 
   import ModifyUser from './ModifyUser.vue'; // 引入弹窗组件
   import BatchCreateUserDialog from './components/BatchCreateUserDialog.vue';
@@ -248,8 +275,7 @@
     if (!confirm) return;
 
     try {
-      // 调用重置密码接口,不传递新密码,由后端处理
-      await resetUserPassword({ ids: [row.id] });
+      await resetUserPassword({ ids: [row.id], password: '123456' });
       ElMessage.success('密码重置成功!');
     } catch (error) {
       console.error('操作失败:', error);
@@ -275,32 +301,15 @@
   //   }
   // };
 
-  const formatRole = (roleKey: string) => {
-    const roleMap: Record<string, string> = {
-      school_admin: '学校管理员',
-      scanner: '扫描员',
-      user: '普通用户',
-    };
-    return roleMap[roleKey] || roleKey;
-  };
-
   // 导入用户
   const importUserDialogRef = ref();
   const importData = reactive({
-    importType: 'kzz',
+    isHeader: false,
   });
-  const onImportCommand = (command: string) => {
-    console.log('导入命令:', command);
-    importData.importType = command;
+  const onImportCommand = (command: 'kzz' | 'fhy') => {
+    importData.isHeader = command === 'kzz';
     importUserDialogRef.value?.open();
   };
-  async function downloadTemplate() {
-    // const res = await downloadByApi(() => agentTemplate()).catch((e) => {
-    //   Message.error(e || '下载失败,请重新尝试!');
-    // });
-    // if (!res) return;
-    // Message.success('下载成功!');
-  }
 
   // 导入班级评卷员
   const importMarkerDialogRef = ref();
@@ -309,8 +318,10 @@
   };
 
   // 导出
-  const onExportCommand = (command: string) => {
-    console.log('导出命令:', command);
+  const onExportCommand = async (
+    command: 'exportUser' | 'exportUserByExam' | 'exportUserBySubject'
+  ) => {
+    await downloadExport(command);
   };
 
   // 批量启用、禁用
@@ -355,6 +366,7 @@
       // 调用重置密码接口,不传递新密码,由后端处理
       await resetUserPassword({
         ids: selectedRows.value.map((user) => user.id),
+        password: '123456',
       });
       ElMessage.success('操作成功!');
     } catch (error) {

+ 21 - 30
src/views/user/components/BatchCreateUserDialog.vue

@@ -29,14 +29,14 @@
           <el-option label="评卷员" value="marker" />
         </el-select>
       </el-form-item>
-      <el-form-item label="命名规则" prop="namingRule">
-        <el-input v-model="formModel.namingRule" placeholder="自定义前缀">
+      <el-form-item label="命名规则" prop="prefix">
+        <el-input v-model="formModel.prefix" placeholder="自定义前缀">
           <template #append>+科目代码+流水号</template>
         </el-input>
       </el-form-item>
-      <el-form-item label="每分组账号数" prop="accountsPerGroup">
+      <el-form-item label="每分组账号数" prop="number">
         <el-input-number
-          v-model="formModel.accountsPerGroup"
+          v-model="formModel.number"
           :min="1"
           :max="20"
           :step="1"
@@ -45,26 +45,19 @@
           step-strictly
         />
       </el-form-item>
-      <el-form-item label="随机密码" prop="randomPassword">
-        <el-switch
-          v-model="formModel.randomPassword"
-          @change="randomPasswordChange"
-        />
+      <el-form-item label="是否随机密码" prop="random">
+        <el-switch v-model="formModel.random" @change="randomPasswordChange" />
       </el-form-item>
-      <el-form-item
-        v-if="!formModel.randomPassword"
-        label="密码"
-        prop="password"
-      >
+      <el-form-item v-if="!formModel.random" label="密码" prop="password">
         <el-input
           v-model="formModel.password"
           type="password"
           placeholder="请输入密码"
-          :disabled="formModel.randomPassword"
+          :disabled="formModel.random"
           show-password
         />
       </el-form-item>
-      <el-form-item label="选择科目" prop="subjectIds">
+      <el-form-item label="选择科目" prop="subjectCodeString">
         <el-button type="primary" @click="openSelectSubjectDialog"
           >设置</el-button
         >
@@ -122,10 +115,8 @@
 
   const rules: FormRules<keyof BatchCreateUserParam> = {
     role: [{ required: true, message: '请选择角色', trigger: 'change' }],
-    namingRule: [
-      { required: true, message: '请输入命名规则', trigger: 'change' },
-    ],
-    accountsPerGroup: [
+    prefix: [{ required: true, message: '请输入命名规则', trigger: 'change' }],
+    number: [
       { required: true, message: '请输入每分组账号数', trigger: 'change' },
     ],
     password: [
@@ -135,11 +126,11 @@
         trigger: 'change',
       },
     ],
-    subjectIds: [
+    subjectCodeString: [
       {
         required: true,
-        validator: (rule: any, value: number[], callback: any) => {
-          if (!value || value.length === 0) {
+        validator: (rule: any, value: string, callback: any) => {
+          if (!value) {
             callback(new Error('请选择科目'));
           } else {
             callback();
@@ -154,11 +145,11 @@
     return {
       examId: appStore.curExam?.id || 0,
       role: '',
-      namingRule: '',
-      accountsPerGroup: 1,
-      randomPassword: false,
+      prefix: '',
+      number: undefined,
+      random: false,
       password: '',
-      subjectIds: [],
+      subjectCodeString: '',
     };
   }
 
@@ -169,7 +160,7 @@
   > | null>(null);
 
   function randomPasswordChange() {
-    if (formModel.randomPassword) {
+    if (formModel.random) {
       formModel.password = '';
     }
   }
@@ -184,9 +175,9 @@
 
   const handleSubjectsSelected = (subjects: SubjectItem[]) => {
     selectedSubjects.value = subjects;
-    formModel.subjectIds = subjects.map((s) => s.id);
+    formModel.subjectCodeString = subjects.map((s) => s.code).join(',');
     ElMessage.success(`已选择 ${subjects.length} 个科目`);
-    formRef.value?.validateField('subjectIds');
+    formRef.value?.validateField('subjectCodeString');
   };
 
   const { loading, setLoading } = useLoading();