浏览代码

feat: api- 登录相关

zhangjie 1 周之前
父节点
当前提交
36d25c73be

+ 1 - 0
components.d.ts

@@ -37,6 +37,7 @@ declare module '@vue/runtime-core' {
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
     ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']

+ 22 - 2
src/api/types/user.ts

@@ -1,7 +1,7 @@
 import { RoleType, BatchAddRole, UserSource } from '@/constants/enumerate';
 
 export interface LoginData {
-  account: string;
+  loginName: string;
   password: string;
 }
 
@@ -18,6 +18,8 @@ export interface UserItem {
   source: UserSource;
   // 角色 (例如:学校管理员, 扫描员)
   role: RoleType;
+  // 角色名称
+  roleName: string;
   // 状态 (启用/禁用)
   enable: boolean;
   // 关联账号
@@ -26,8 +28,18 @@ export interface UserItem {
   schoolId: number;
   // 最后登录IP
   lastLoginIp: string;
+  // 最后登录时间
+  lastLoginTime: string;
   // 描述
   description: string;
+  // 访问令牌
+  accessToken: string;
+  // 扫描令牌
+  scanToken: string;
+  // 学校名称
+  schoolName: string;
+  // 学校ID
+  schoolId: number;
 }
 export type UserListPageRes = PageResult<UserItem>;
 
@@ -70,7 +82,7 @@ export interface EnableUserParam {
   enabled: boolean;
 }
 
-export interface UpdatePwdData {
+export interface UpdatePasswordParam {
   // id: number;
   // oldPassword: string;
   password: string;
@@ -90,3 +102,11 @@ export interface BatchCreateUserParam {
   // 科目
   subjectCodeString: string;
 }
+
+// 系统版本等信息
+export interface SystemInfo {
+  // 版本名称
+  versionName: string;
+  // 版本日期
+  versionDate: string;
+}

+ 19 - 7
src/api/user.ts

@@ -2,13 +2,15 @@ import axios, { AxiosResponse } from 'axios';
 import { UserState } from '@/store/modules/user/types';
 import type {
   LoginData,
-  UpdatePwdData,
+  UserItem,
+  UpdatePasswordParam,
   UserListPageParam,
   UserListPageRes,
   UserUpdateParam,
   ResetPasswordParam,
   EnableUserParam,
   BatchCreateUserParam,
+  SystemInfo,
 } from './types/user';
 
 // 登录
@@ -16,7 +18,7 @@ export function login(data: LoginData): Promise<UserState> {
   return axios.post('/api/user/login', data);
 }
 // 修改密码
-export function updatePwd(datas: UpdatePwdData): Promise<UserState> {
+export function updatePwd(datas: UpdatePasswordParam): Promise<UserState> {
   return axios.post('/api/user/password/modify', {}, { params: datas });
 }
 // 退出登录
@@ -24,6 +26,12 @@ export function userLogout() {
   return axios.post('/api/user/logout', {});
 }
 
+// 系统信息
+// 系统版本
+export function systemVersion(): Promise<SystemInfo> {
+  return axios.post('/api/sys/version', {});
+}
+
 // 用户管理
 // 获取用户列表 (分页)
 export function userListPage(
@@ -31,27 +39,31 @@ export function userListPage(
 ): Promise<UserListPageRes> {
   return axios.post('/api/admin/user/query', { params });
 }
+// 获取指定用户
+export function getUser(id: number): Promise<UserItem> {
+  return axios.post(`/api/admin/user/find`, {}, { params: { id } });
+}
 // 新增或编辑用户
-export function updateUser(data: UserUpdateParam): Promise<any> {
+export function updateUser(data: UserUpdateParam): Promise<boolean> {
   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> {
+export function batchAddUser(datas: BatchCreateUserParam): Promise<boolean> {
   return axios.post('/api/admin/user/batchSave', datas);
 }
 // 删除用户: 暂时没有此功能
-export function deleteUser(id: number): Promise<any> {
+export function deleteUser(id: number): Promise<boolean> {
   return axios.post(`/api/admin/user/delete/${id}`);
 }
 // 重置用户密码
-export function resetUserPassword(data: ResetPasswordParam): Promise<any> {
+export function resetUserPassword(data: ResetPasswordParam): Promise<boolean> {
   return axios.post('/api/admin/user/reset', data);
 }
 // 启用禁用用户
-export function enableUser(datas: EnableUserParam): Promise<any> {
+export function enableUser(datas: EnableUserParam): Promise<boolean> {
   return axios.post(`/api/admin/user/enable`, datas);
 }
 // 导入评卷员班级-导入模板下载

+ 14 - 0
src/assets/style/home.scss

@@ -221,6 +221,15 @@
   }
 }
 
+.el-popover.el-popper.user-popover {
+  min-width: 0;
+  width: 120px !important;
+  padding: 10px;
+  .el-button {
+    width: 100%;
+  }
+}
+
 // menu-dialog
 .menu-dialog {
   .arco-dialog.is-fullscreen {
@@ -360,6 +369,11 @@
   .home-header {
     left: 64px;
   }
+  .home-exam {
+    .el-button {
+      transform: rotate(180deg);
+    }
+  }
 }
 
 // .home-guide

+ 30 - 14
src/layout/default-layout.vue

@@ -12,15 +12,23 @@
         <span>AI检测预计耗时12小时35分后完成</span>
       </div>
       <div class="home-action">
-        <el-tooltip content="修改密码" placement="bottom-end">
-          <el-button text @click="toResetPwd">
-            <svg-icon name="icon-user" />
-            <!-- <span :title="userStore.name">{{ userStore.name }}</span> -->
-            <span :title="userStore.name" style="margin-left: 6px"
-              >姓名(角色)</span
-            >
-          </el-button>
-        </el-tooltip>
+        <el-popover popper-class="user-popover" placement="bottom-end">
+          <template #reference>
+            <el-button text>
+              <svg-icon name="icon-user" />
+              <!-- <span :title="userStore.name">{{ userStore.name }}</span> -->
+              <span :title="userStore.name" style="margin-left: 6px"
+                >姓名(角色)</span
+              >
+            </el-button>
+          </template>
+          <div>
+            <el-button text @click="toUserInfo">用户信息 </el-button>
+          </div>
+          <div>
+            <el-button text @click="toResetPwd">修改密码 </el-button>
+          </div>
+        </el-popover>
         <el-button text @click="toLogout">
           <svg-icon name="icon-notice" />
         </el-button>
@@ -98,8 +106,10 @@
     </div>
   </div>
 
-  <!-- ResetPwd -->
-  <ResetPwd ref="fesetPwdRef" @modified="resetPwdModified" />
+  <!-- ResetPwdDialog -->
+  <ResetPwdDialog ref="resetPwdDialogRef" @modified="resetPwdModified" />
+  <!-- UserInfoDialog -->
+  <UserInfoDialog ref="userInfoDialogRef" :user-id="userStore.id" />
 </template>
 
 <script lang="ts" setup>
@@ -108,7 +118,8 @@
   import { useAppStore, useUserStore } from '@/store';
   import { ElMessageBox } from 'element-plus';
 
-  import ResetPwd from '@/views/login/ResetPwd.vue';
+  import ResetPwdDialog from '@/views/login/components/ResetPwdDialog.vue';
+  import UserInfoDialog from '@/views/login/components/UserInfoDialog.vue';
 
   defineOptions({
     name: 'DefaultLayout',
@@ -120,7 +131,8 @@
   const router = useRouter();
 
   const curRouteName = ref('');
-  const fesetPwdRef = ref();
+  const resetPwdDialogRef = ref();
+  const userInfoDialogRef = ref();
   const isCollapse = ref(false);
 
   const curExamName = computed(() => {
@@ -167,7 +179,11 @@
   }
 
   function toResetPwd() {
-    fesetPwdRef.value?.open();
+    resetPwdDialogRef.value?.open();
+  }
+
+  function toUserInfo() {
+    userInfoDialogRef.value?.open();
   }
 
   function resetPwdModified() {

+ 9 - 0
src/router/routes/modules/login.ts

@@ -31,6 +31,15 @@ const routes: AppRouteRecordRaw = {
         requiresAuth: true,
       },
     },
+    {
+      path: '/select-mark-group',
+      name: 'SelectMarkGroup',
+      component: () => import('@/views/login/SelectMarkGroup.vue'),
+      meta: {
+        title: '选择评卷科目',
+        requiresAuth: true,
+      },
+    },
   ],
 };
 

+ 2 - 1
src/store/modules/app/index.ts

@@ -44,7 +44,8 @@ function getMenu(privilegeData: UserMenuItem[]): {
 
 const useAppStore = defineStore('app', {
   state: (): AppState => ({
-    version: '',
+    versionName: '',
+    versionDate: '',
     appMenus: [],
     validRoutes: [],
     breadcrumbs: [],

+ 2 - 1
src/store/modules/app/types.ts

@@ -24,7 +24,8 @@ export type AppMenuItem = {
 };
 
 export interface AppState {
-  version: string;
+  versionName: string;
+  versionDate: string;
   appMenus: AppMenuItem[];
   validRoutes: string[];
   breadcrumbs: string[];

+ 13 - 13
src/views/login/ResetInfo.vue

@@ -18,25 +18,25 @@
           clearable
         />
       </el-form-item>
-      <el-form-item label="" prop="employeeId">
+      <el-form-item label="" prop="empno">
         <el-input
-          v-model.trim="formData.employeeId"
+          v-model.trim="formData.empno"
           placeholder="请输入工号"
           clearable
         />
       </el-form-item>
-      <el-form-item label="" prop="newPassword">
+      <el-form-item label="" prop="password">
         <el-input
-          v-model.trim="formData.newPassword"
+          v-model.trim="formData.password"
           type="password"
           placeholder="输入6位以上新密码"
           show-password
           clearable
         />
       </el-form-item>
-      <el-form-item label="" prop="confirmPassword">
+      <el-form-item label="" prop="password2">
         <el-input
-          v-model.trim="formData.confirmPassword"
+          v-model.trim="formData.password2"
           type="password"
           placeholder="再次输入密码"
           show-password
@@ -77,15 +77,15 @@
 
   const formData = reactive({
     name: '',
-    employeeId: '',
-    newPassword: '',
-    confirmPassword: '',
+    empno: '',
+    password: '',
+    password2: '',
   });
 
   const validateConfirmPassword = (rule: any, value: any, callback: any) => {
     if (value === '') {
       callback(new Error('请再次输入密码'));
-    } else if (value !== formData.newPassword) {
+    } else if (value !== formData.password) {
       callback(new Error('两次输入的密码不一致'));
     } else {
       callback();
@@ -100,14 +100,14 @@
         trigger: 'change',
       },
     ],
-    employeeId: [
+    empno: [
       {
         required: true,
         message: '请输入工号',
         trigger: 'change',
       },
     ],
-    newPassword: [
+    password: [
       {
         required: true,
         message: '请输入新密码',
@@ -119,7 +119,7 @@
         trigger: 'change',
       },
     ],
-    confirmPassword: [
+    password2: [
       {
         required: true,
         validator: validateConfirmPassword,

+ 171 - 0
src/views/login/SelectMarkGroup.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="login-title">
+    <h1>请选择评卷科目</h1>
+    <p></p>
+  </div>
+  <div class="login-form">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="rules"
+      size="large"
+      label-position="top"
+    >
+      <el-form-item prop="examId">
+        <SelectExam
+          v-model="formData.examId"
+          size="large"
+          @change="onExamChange"
+        />
+      </el-form-item>
+
+      <el-form-item prop="subjectCode">
+        <SelectSubject
+          v-model="formData.subjectCode"
+          size="large"
+          :disabled="!formData.examId"
+          @change="onSubjectChange"
+        />
+      </el-form-item>
+
+      <el-form-item prop="groupId">
+        <el-select
+          v-model="formData.groupId"
+          placeholder="请选择分组"
+          size="large"
+          :disabled="!formData.subjectCode"
+          filterable
+          clearable
+          @change="onGroupChange"
+        >
+          <el-option
+            v-for="item in groupList"
+            :key="item.id"
+            :label="`分组:${item.groupNumber} - ${item.title} ${item.percent}%`"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button class="submit-btn" type="primary" @click="handleSubmit"
+          >确认</el-button
+        >
+        <el-button @click="handleGoBack">退出</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive } from 'vue';
+  import { useRouter } from 'vue-router';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ExamItem } from '@/api/types/exam';
+  import { MarkGroupItem } from '@/api/types/mark';
+  import { getMarkGroupList } from '@/api/mark';
+  import SelectExam from '@/components/select-exam/index.vue';
+  import SelectSubject from '@/components/select-subject/index.vue';
+
+  const router = useRouter();
+  const formRef = ref<FormInstance>();
+  const formData = reactive({
+    examId: null,
+    subjectCode: null,
+    groupId: null,
+  });
+
+  const rules: FormRules = {
+    examId: [
+      {
+        required: true,
+        message: '请选择考试',
+        trigger: 'change',
+      },
+    ],
+    subjectCode: [
+      {
+        required: true,
+        message: '请选择科目',
+        trigger: 'change',
+      },
+    ],
+    groupId: [
+      {
+        required: true,
+        message: '请选择分组',
+        trigger: 'change',
+      },
+    ],
+  };
+
+  const selectExam = ref({} as ExamItem);
+  const selectSubject = ref({} as any);
+  const selectGroup = ref({} as MarkGroupItem);
+  const groupList = ref<MarkGroupItem[]>([]);
+
+  const onExamChange = (val: ExamItem) => {
+    console.log('选择的考试:', val);
+    selectExam.value = val || ({} as ExamItem);
+    // 重置科目和分组
+    formData.subjectCode = null;
+    formData.groupId = null;
+    groupList.value = [];
+  };
+
+  // 加载分组列表
+  const loadGroupList = async () => {
+    if (!formData.subjectCode) {
+      groupList.value = [];
+      return;
+    }
+
+    try {
+      const res = await getMarkGroupList({
+        pageNumber: 1,
+        pageSize: 1000,
+        subjectCode: formData.subjectCode,
+      });
+      groupList.value = res || [];
+    } catch (error) {
+      console.error('加载分组列表失败:', error);
+      groupList.value = [];
+    }
+  };
+
+  const onSubjectChange = (val: any) => {
+    console.log('选择的科目:', val);
+    selectSubject.value = val || {};
+    // 重置分组
+    formData.groupId = null;
+    loadGroupList();
+  };
+
+  const onGroupChange = (val: number) => {
+    console.log('选择的分组ID:', val);
+    const group = groupList.value.find((item) => item.id === val);
+    selectGroup.value = group || ({} as MarkGroupItem);
+  };
+
+  // 确认按钮点击事件
+  const handleSubmit = async () => {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    console.log('选择的信息:', {
+      exam: selectExam.value,
+      subject: selectSubject.value,
+      group: selectGroup.value,
+    });
+
+    // 保存选择的信息到
+
+    // 提交成功后的操作,例如跳转到评卷页面
+    // router.push({ name: 'MarkPage' });
+  };
+
+  // 回退按钮点击事件
+  const handleGoBack = () => {
+    router.push({ name: 'Login' });
+  };
+</script>

+ 3 - 11
src/views/login/SwitchExam.vue

@@ -20,18 +20,10 @@
       </el-form-item>
 
       <el-form-item>
-        <el-button
-          class="submit-btn"
-          type="primary"
-          style="width: 100%"
-          @click="handleSubmit"
+        <el-button class="submit-btn" type="primary" @click="handleSubmit"
           >确认</el-button
         >
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" style="width: 100%" link @click="handleGoBack"
-          >退出</el-button
-        >
+        <el-button @click="handleGoBack">退出</el-button>
       </el-form-item>
     </el-form>
   </div>
@@ -41,8 +33,8 @@
   import { ref, reactive } from 'vue';
   import { useRouter } from 'vue-router';
   import type { FormInstance, FormRules } from 'element-plus';
-  import { ExamItem } from '@/api/types/base';
   import { useAppStore } from '@/store';
+  import { ExamItem } from '@/api/types/exam';
 
   const appStore = useAppStore();
   const router = useRouter();

+ 9 - 8
src/views/login/ResetPwd.vue → src/views/login/components/ResetPwdDialog.vue

@@ -4,8 +4,9 @@
     title="修改密码"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
-    :show-close="false"
     width="500px"
+    top="10vh"
+    append-to-body
     @open="modalBeforeOpen"
   >
     <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
@@ -19,9 +20,9 @@
         >
         </el-input>
       </el-form-item>
-      <el-form-item prop="password" label="新密码">
+      <el-form-item prop="newPassword" label="新密码">
         <el-input
-          v-model.trim="formData.password"
+          v-model.trim="formData.newPassword"
           type="password"
           placeholder="请输入新密码"
           show-password
@@ -61,7 +62,7 @@
   import useModal from '@/hooks/modal';
 
   defineOptions({
-    name: 'ResetPwd',
+    name: 'ResetPwdDialog',
   });
 
   /* modal */
@@ -72,7 +73,7 @@
 
   const defaultFormData = {
     oldPassword: '',
-    password: '',
+    newPassword: '',
     rePassword: '',
   };
   type FormDataType = typeof defaultFormData;
@@ -82,7 +83,7 @@
   const passwordRule: FormRules['password'] = [...password];
   const rules: FormRules = {
     oldPassword: password,
-    password: [
+    newPassword: [
       ...passwordRule,
       {
         validator: (value, callback) => {
@@ -98,7 +99,7 @@
       ...passwordRule,
       {
         validator: (value, callback) => {
-          if (value !== formData.password) {
+          if (value !== formData.newPassword) {
             callback('两次输入的密码不一致');
           } else {
             callback();
@@ -117,7 +118,7 @@
     setLoading(true);
     const datas = {
       oldPassword: formData.oldPassword,
-      password: formData.password,
+      newPassword: formData.newPassword,
     };
     let res = true;
     await updatePwd(datas).catch(() => {

+ 103 - 0
src/views/login/components/UserInfoDialog.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="用户信息"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    width="500px"
+    top="10vh"
+    append-to-body
+    @open="modalBeforeOpen"
+  >
+    <div v-loading="loading">
+      <el-descriptions :column="1" border label-width="140px">
+        <el-descriptions-item label="归属学校">
+          {{ userInfo.schoolName || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="学校ID">
+          {{ userInfo.schoolId || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="姓名">
+          {{ userInfo.name || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="用户角色">
+          {{ userInfo.roleName || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="最后登录IP">
+          {{ userInfo.lastLoginIp || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="最后登录时间">
+          {{ userInfo.lastLoginTime || '-' }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </div>
+
+    <template #footer> </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import useLoading from '@/hooks/loading';
+  import useModal from '@/hooks/modal';
+  import { getUser } from '@/api/user';
+  import type { UserItem } from '@/api/types/user';
+
+  defineOptions({
+    name: 'UserInfoDialog',
+  });
+
+  interface Props {
+    userId?: number; // 编辑时传入,新增时为空
+  }
+
+  const props = defineProps<Props>();
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const { loading, setLoading } = useLoading();
+
+  // 用户信息
+  const userInfo = reactive<Partial<UserItem>>({
+    schoolName: '',
+    schoolId: undefined,
+    name: '',
+    roleName: '',
+    lastLoginIp: '',
+    lastLoginTime: '',
+  });
+
+  // 加载用户信息
+  const loadUserInfo = async () => {
+    if (!props.userId) {
+      ElMessage.error('用户ID不能为空');
+      return;
+    }
+
+    setLoading(true);
+    try {
+      const data = await getUser(props.userId);
+      Object.assign(userInfo, {
+        schoolName: data.schoolName,
+        schoolId: data.schoolId,
+        name: data.name,
+        roleName: data.roleName,
+        lastLoginIp: data.lastLoginIp,
+        lastLoginTime: data.lastLoginTime,
+      });
+    } catch (error) {
+      ElMessage.error('获取用户信息失败');
+      console.error('获取用户信息失败:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 弹窗打开前的处理
+  const modalBeforeOpen = async () => {
+    await loadUserInfo();
+  };
+</script>

+ 18 - 4
src/views/login/home.vue

@@ -13,11 +13,11 @@
     </div>
 
     <p class="login-footer">
-      <a href="http://www.qmth.com.cn" target="_blank"
-        >Copyright © 2024 启明泰和
+      <a href="http://www.qmth.com.cn" target="_blank">
+        Copyright © 2024 启明泰和
       </a>
-      <span>v2.0</span>
-      <span v-if="appStore.version"> build{{ appStore.version }}</span>
+      <span v-if="appStore.versionName"> {{ appStore.versionName }} </span>
+      <span v-if="appStore.versionDate"> {{ appStore.versionDate }} </span>
       <a href="https://beian.miit.gov.cn/" target="_blank">
         鄂ICP备12000033号-3</a
       >
@@ -26,11 +26,25 @@
 </template>
 
 <script setup lang="ts">
+  import { onMounted } from 'vue';
   import { useAppStore } from '@/store';
+  import { systemVersion } from '@/api/user';
 
   defineOptions({
     name: 'LoginHome',
   });
 
   const appStore = useAppStore();
+
+  async function getVersion() {
+    const version = await systemVersion();
+    appStore.setInfo({
+      versionName: version.versionName,
+      versionDate: version.versionDate,
+    });
+  }
+
+  onMounted(() => {
+    getVersion();
+  });
 </script>

+ 4 - 4
src/views/login/login.vue

@@ -11,9 +11,9 @@
       :rules="rules"
       label-position="top"
     >
-      <el-form-item prop="account">
+      <el-form-item prop="loginName">
         <el-input
-          v-model.trim="formData.account"
+          v-model.trim="formData.loginName"
           placeholder="请输入登录账号"
           clearable
         >
@@ -69,11 +69,11 @@
 
   const formRef = ref<FormInstance>();
   const formData = reactive({
-    account: '',
+    loginName: '',
     password: '',
   });
   const rules: FormRules = {
-    account: [
+    loginName: [
       {
         required: true,
         message: '请输入登录账号',