浏览代码

登录,修改密码,人员管理,部门管理。

jxq 4 月之前
父节点
当前提交
f7565be67c

+ 11 - 0
src/api/auth/index.ts

@@ -44,6 +44,17 @@ export function getCaptchaApi(): AxiosPromise<CaptchaResult> {
   });
 }
 
+/**
+ * 登录成功后获取用户字典
+ */
+export function getUserDicts(data: string[]): AxiosPromise {
+  return request({
+    url: "/api/v1/sys/dictData/queryByTypes",
+    method: "post",
+    data: data,
+  });
+}
+
 export function getOrgListApi(): AxiosPromise<any[]> {
   return request({
     url: "/api/v1/sys/dept/orgList",

+ 3 - 1
src/api/auth/types.ts

@@ -21,7 +21,9 @@ export interface LoginData {
    */
   captchaCode?: string;
 
-  orgId: number;
+  orgId?: number;
+
+  token?: string;
 }
 
 /**

+ 68 - 0
src/api/system/dept/index.ts

@@ -0,0 +1,68 @@
+import request from "@/utils/request";
+import { AxiosPromise } from "axios";
+
+
+/**
+ * 部门树形列表
+ * @param queryParams
+ */
+export function treeList(): AxiosPromise<any[]> {
+  return request({
+    url: "/api/v1/sys/dept/orgTree",
+    method: "get"
+  });
+}
+
+/**
+ * 部门树形表格
+ *
+ * @param queryParams
+ */
+export function queryTreeList(queryParams?: any): AxiosPromise<any> {
+  return request({
+    url: "/api/v1/sys/dept/queryOrgTree",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+
+/**
+ * 新增部门
+ *
+ * @param data
+ */
+export function addDept(data: any) {
+  return request({
+    url: "/api/v1/sys/dept/add",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ *  修改部门
+ *
+ * @param id
+ * @param data
+ */
+export function updateDept(data: any) {
+  return request({
+    url: "/api/v1/sys/dept/update",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ * 删除部门
+ *
+ * @param ids
+ */
+export function deleteDept(ids: any) {
+  return request({
+    url: "/api/v1/sys/dept/batch-del",
+    method: "post",
+    data: ids
+  });
+}

+ 99 - 0
src/api/system/role/index.ts

@@ -0,0 +1,99 @@
+import request from "@/utils/request";
+import { AxiosPromise } from "axios";
+
+/**
+ * 获取角色分页数据
+ *
+ * @param queryParams
+ */
+export function getRolePage(
+  queryParams?: any
+): AxiosPromise<any> {
+  return request({
+    url: "/api/v1/sys/role/page",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 获取角色下拉数据
+ *
+ * @param queryParams
+ */
+export function getRoleOptions(
+  queryParams?: any
+): AxiosPromise<OptionType[]> {
+  return request({
+    url: "/api/v1/sys/role/list",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 获取角色的菜单ID集合
+ *
+ * @param queryParams
+ */
+export function getRoleMenuIds(roleId: number): AxiosPromise<any> {
+  return request({
+    url: "/api/v1/sys/role/queryMenuIds/" + roleId,
+    method: "get",
+  });
+}
+
+/**
+ * 分配菜单权限给角色
+ *
+ * @param queryParams
+ */
+export function updateRoleMenus(
+  roleId: number,
+  data: number[]
+): AxiosPromise<any> {
+  return request({
+    url: "/api/v1/sys/role/saveMenus",
+    method: "post",
+    data: {id: roleId,menuIds: data},
+  });
+}
+
+/**
+ * 添加角色
+ *
+ * @param data
+ */
+export function addRole(data: any) {
+  return request({
+    url: "/api/v1/sys/role/add",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ * 更新角色
+ *
+ * @param id
+ * @param data
+ */
+export function updateRole( data: any) {
+  return request({
+    url: "/api/v1/sys/role/update",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ *
+ * @param ids
+ */
+export function deleteRoles(ids: any) {
+  return request({
+    url: "/api/v1/sys/role/batch-del",
+    method: "post",
+    data: {ids: ids}
+  });
+}

+ 178 - 0
src/api/system/user/index.ts

@@ -0,0 +1,178 @@
+import request from "@/utils/request";
+import { AxiosPromise } from "axios";
+import { UserForm, UserInfo, UserPageVO, UserQuery } from "./types";
+
+/**
+ * 登录成功后获取用户信息(昵称、头像、权限集合和角色集合)
+ */
+export function getUserInfoApi(): AxiosPromise {
+  return request({
+    url: "/api/auth",
+    method: "get",
+  });
+}
+
+// 新增的获取用户详细信息的API
+export function getUserDetailApi(p: object): AxiosPromise {
+  return request({
+    url: "/api/v1/sys/user/get",
+    method: "post",
+    data: p,
+  });
+}
+/**
+ * 获取用户分页列表
+ *
+ * @param queryParams
+ */
+export function getUserList(queryParams: any): AxiosPromise<any[]> {
+  return request({
+    url: "/api/v1/sys/user/list",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+export function getUserTree(): AxiosPromise {
+  return request({
+    url: "/api/v1/sys/user/tree",
+    method: "get",
+  });
+}
+
+export function getPostOptions(queryParams?: any): AxiosPromise<OptionType[]> {
+  return request({
+    url: "/api/v1/sys/post/list",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 获取用户分页列表
+ *
+ * @param queryParams
+ */
+export function getUserPage(queryParams: any): AxiosPromise<PageResult<any[]>> {
+  return request({
+    url: "/api/v1/sys/user/page",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 添加用户
+ *
+ * @param data
+ */
+export function addUser(data: any) {
+  return request({
+    url: "/api/v1/sys/user/add",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ * 修改用户
+ *
+ * @param id
+ * @param data
+ */
+export function updateUser(id: number, data: any) {
+  return request({
+    url: "/api/v1/sys/user/update",
+    method: "post",
+    data: { id: id, ...data },
+  });
+}
+
+export function updateBaseInfo(id: number, data: any) {
+  return request({
+    url: "/api/v1/sys/user/updateBaseInfo",
+    method: "post",
+    data: { id: id, ...data },
+  });
+}
+export function updateHeadImg(id: number, data: any) {
+  return request({
+    url: "/api/v1/sys/user/updateHeadImg",
+    method: "post",
+    data: { id: id, ...data },
+  });
+}
+
+/**
+ * 修改用户密码
+ *
+ * @param id
+ * @param password
+ */
+export function updateUserPassword(obj: any) {
+  return request({
+    url: "/api/v1/sys/user/resetPwd",
+    method: "post",
+    data: obj,
+  });
+}
+
+/**
+ * 删除用户
+ *
+ * @param ids
+ */
+export function deleteUsers(ids: any) {
+  return request({
+    url: "/api/v1/sys/user/del",
+    method: "post",
+    data: { id: ids },
+  });
+}
+
+/**
+ * 下载用户导入模板
+ *
+ * @returns
+ */
+export function downloadTemplateApi() {
+  return request({
+    url: "/api/v1/sys/user/template",
+    method: "get",
+    responseType: "arraybuffer",
+  });
+}
+
+/**
+ * 导出用户
+ *
+ * @param queryParams
+ * @returns
+ */
+export function exportUser(queryParams: any) {
+  return request({
+    url: "/api/v1/sys/user/export",
+    method: "post",
+    data: queryParams,
+    responseType: "arraybuffer",
+  });
+}
+
+/**
+ * 导入用户
+ *
+ * @param file
+ */
+export function importUser(deptId: number, file: File) {
+  const formData = new FormData();
+  formData.append("file", file);
+  return request({
+    url: "/api/v1/sys/user/import",
+    method: "post",
+    params: { deptId: deptId },
+    data: formData,
+    headers: {
+      "Content-Type": "multipart/form-data",
+    },
+  });
+}

+ 118 - 0
src/api/system/user/types.ts

@@ -0,0 +1,118 @@
+/**
+ * 登录用户信息
+ */
+export interface UserInfo {
+  userId?: number;
+  username?: string;
+  nickname?: string;
+  avatar?: string;
+  roles: string[];
+  perms: string[];
+  deptId?: string;
+}
+
+/**
+ * 用户查询对象类型
+ */
+export interface UserQuery extends PageQuery {
+  keywords?: string;
+  status?: number;
+  deptId?: number;
+  startTime?: string;
+  endTime?: string;
+}
+
+/**
+ * 用户分页对象
+ */
+export interface UserPageVO {
+  /**
+   * 用户头像地址
+   */
+  avatar?: string;
+  /**
+   * 创建时间
+   */
+  createTime?: Date;
+  /**
+   * 部门名称
+   */
+  deptName?: string;
+  /**
+   * 用户邮箱
+   */
+  email?: string;
+  /**
+   * 性别
+   */
+  genderLabel?: string;
+  /**
+   * 用户ID
+   */
+  id?: number;
+  /**
+   * 手机号
+   */
+  mobile?: string;
+  /**
+   * 用户昵称
+   */
+  nickname?: string;
+  /**
+   * 角色名称,多个使用英文逗号(,)分割
+   */
+  roleNames?: string;
+  /**
+   * 用户状态(1:启用;0:禁用)
+   */
+  status?: number;
+  /**
+   * 用户名
+   */
+  username?: string;
+}
+
+/**
+ * 用户表单类型
+ */
+export interface UserForm {
+  /**
+   * 用户头像
+   */
+  avatar?: string;
+  /**
+   * 部门ID
+   */
+  deptId?: number;
+  /**
+   * 邮箱
+   */
+  email?: string;
+  /**
+   * 性别
+   */
+  gender?: number;
+  /**
+   * 用户ID
+   */
+  id?: number;
+  mobile?: string;
+  /**
+   * 昵称
+   */
+  nickname?: string;
+  /**
+   * 角色ID集合
+   */
+  roleIds?: number[];
+
+  deptIds?: number[];
+  /**
+   * 用户状态(1:正常;0:禁用)
+   */
+  status?: number;
+  /**
+   * 用户名
+   */
+  username?: string;
+}

+ 1 - 30
src/plugins/permission.ts

@@ -4,7 +4,6 @@ import { usePermissionStore } from "@/store/modules/permission";
 import NProgress from "@/utils/nprogress";
 
 export function setupPermission() {
-  return;
   // 白名单路由
   const whiteList = ["/login"];
 
@@ -17,35 +16,7 @@ export function setupPermission() {
         next({ path: "/" });
         NProgress.done();
       } else {
-        const userStore = useUserStore();
-        // const hasRoles =
-        //   userStore.user.roles && userStore.user.roles.length > 0;
-        if (userStore.isGetAuth) {
-          // 未匹配到任何路由,跳转404
-          if (to.matched.length === 0) {
-            from.name ? next({ name: from.name }) : next("/404");
-          } else {
-            next();
-          }
-        } else {
-          const permissionStore = usePermissionStore();
-          try {
-            const { menus } = await userStore.getUserInfo();
-
-            const accessRoutes = await permissionStore.generateRoutes(menus);
-
-            accessRoutes.forEach((route) => {
-              router.addRoute(route);
-            });
-            next({ ...to, replace: true });
-          } catch (error) {
-            console.error("beforeEach error", error);
-            // 移除 token 并跳转登录页
-            await userStore.resetToken();
-            next(`/login?redirect=${to.path}`);
-            NProgress.done();
-          }
-        }
+        next();
       }
     } else {
       // 未登录可以访问白名单页面

+ 8 - 2
src/router/index.ts

@@ -17,7 +17,7 @@ export const constantRoutes: RouteRecordRaw[] = [
   // },
   {
     path: "/",
-    redirect: "/main/home",
+    redirect: "/login",
     meta: { hidden: true },
   },
 
@@ -71,6 +71,12 @@ export const constantRoutes: RouteRecordRaw[] = [
       import("@/views/modules/global-config/deviceResources.vue"),
     meta: { hidden: true },
   },
+  {
+    path: "/detp-management",
+    component: () =>
+      import("@/views/modules/person-manager/com/dept-manage.vue"),
+    meta: { hidden: true },
+  },
   // {
   // path: "/test",
   // component: () => import("@/views/modules/home/test/test.vue"),
@@ -85,7 +91,7 @@ export const constantRoutes: RouteRecordRaw[] = [
   // },
   {
     path: "/login",
-    component: () => import("@/views/demo/test-dark.vue"),
+    component: () => import("@/views/login/index.vue"),
     meta: { hidden: true },
   },
 

+ 1 - 0
src/store/index.ts

@@ -17,4 +17,5 @@ export * from "./modules/settings";
 export * from "./modules/tagsView";
 export * from "./modules/user";
 export * from "./modules/common";
+export * from "./modules/dictionary";
 export { store };

+ 53 - 0
src/store/modules/dictionary.ts

@@ -0,0 +1,53 @@
+import { store } from "@/store";
+import { defineStore } from "pinia";
+import { getUserDicts } from "@/api/auth";
+
+export const useDictionaryStore = defineStore("dictionaryStore", () => {
+  const types = [
+    "station_type",
+    "station_operate_type",
+    "applicable_platforms",
+    "material_properties",
+    "quality_testing_plan",
+    "material_level",
+    "packaging_method",
+    "quality_grade",
+    "selection_type",
+    "device_type",
+    "stage",
+    "danwei_type",
+    "process_type",
+    "workshop_section",
+    "skill_requirements",
+    "accessories_type",
+    "trace_type",
+    "skill_type",
+    "drawing_type",
+    "vehicle_level",
+    "fault_current_state",
+    "escalation_fault_state",
+    "defect_mana",
+    "disposal_measures_type",
+    "process_check_result",
+    "excel_type",
+    "excel_states",
+    "process_state",
+    "form_params",
+    "bill_type",
+    "warehouse_type",
+    "signature_type",
+    "signature_attribution",
+    "accessories_property",
+  ];
+  const dicts = ref<{ [key: string]: any[] }>({});
+
+  return {
+    types,
+    dicts,
+  };
+});
+
+export function useDictionaryStoreHook() {
+  // console.log('dicts:',useDictionaryStore(store))
+  return useDictionaryStore(store);
+}

+ 0 - 1
src/styles/index.scss

@@ -34,7 +34,6 @@
   height: calc(100vh - 48px);
   padding: 10px;
   overflow: auto;
-  border: 2px solid black;
 }
 
 //二级页面公共样式,上下结构,上面是childHeader

+ 71 - 29
src/views/login/index.vue

@@ -110,8 +110,8 @@
 </template>
 
 <script setup lang="ts">
-import { useSettingsStore, useUserStore } from "@/store";
-import { getCaptchaApi, getOrgListApi } from "@/api/auth";
+import { useSettingsStore, useUserStore, useDictionaryStore } from "@/store";
+import { getCaptchaApi, getOrgListApi, getUserDicts } from "@/api/auth";
 import { LoginData } from "@/api/auth/types";
 import { Sunny, Moon } from "@element-plus/icons-vue";
 import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
@@ -122,6 +122,8 @@ import { usePermissionStore } from "@/store/modules/permission";
 // Stores
 const userStore = useUserStore();
 const settingsStore = useSettingsStore();
+// 数据字典相关
+const dictStore = useDictionaryStore();
 
 // Internationalization
 const { t } = useI18n();
@@ -129,7 +131,7 @@ const { t } = useI18n();
 // Reactive states
 const isDark = ref(settingsStore.theme === ThemeEnum.DARK);
 const icpVisible = ref(true);
-const orgList = ref([]);
+const orgList = ref<any>([]);
 const loading = ref(false); // 按钮loading
 const isCapslock = ref(false); // 是否大写锁定
 const captchaBase64 = ref(); // 验证码图片Base64字符串
@@ -137,8 +139,8 @@ const loginFormRef = ref(ElForm); // 登录表单ref
 const { height } = useWindowSize();
 
 const loginData = ref<LoginData>({
-  userName: "admin",
-  password: "admin@123",
+  userName: "",
+  password: "",
 });
 
 const loginRules = computed?.(() => {
@@ -197,34 +199,50 @@ const route = useRoute();
 function handleLogin() {
   loginFormRef.value.validate((valid: boolean) => {
     if (valid) {
-      loading.value = true;
-      userStore
-        .login(loginData.value)
-        .then(async () => {
-          const query: LocationQuery = route.query;
-          const redirect = (query.redirect as LocationQueryValue) ?? "/";
-          const otherQueryParams = Object.keys(query).reduce(
-            (acc: any, cur: string) => {
-              if (cur !== "redirect") {
-                acc[cur] = query[cur];
-              }
-              return acc;
-            },
-            {}
-          );
-          router.push({ path: redirect, query: otherQueryParams });
-        })
-        .catch(() => {
-          // getCaptcha();
-          console.log("catch");
-        })
-        .finally(() => {
-          loading.value = false;
-        });
+      toLogin();
     }
   });
 }
 
+const toLogin = () => {
+  //保存用户名和密码
+  localStorage.setItem("local_name", loginData.value.userName);
+  localStorage.setItem("local_pwd", loginData.value.password);
+
+  loading.value = true;
+  userStore
+    .login(loginData.value)
+    .then(async () => {
+      // const query: LocationQuery = route.query;
+      // const redirect = (query.redirect as LocationQueryValue) ?? "/";
+      // const otherQueryParams = Object.keys(query).reduce(
+      //   (acc: any, cur: string) => {
+      //     if (cur !== "redirect") {
+      //       acc[cur] = query[cur];
+      //     }
+      //     return acc;
+      //   },
+      //   {}
+      // );
+      // 获取字典
+      // getUserDicts(dictStore.types).then((res) => {
+      //   if (res.data) {
+      //     dictStore.dicts = res?.data ?? [];
+      //   }
+      // });
+
+      router.push("/main/home");
+      // router.push("/welcome");
+    })
+    .catch(() => {
+      // getCaptcha();
+      console.log("catch");
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+};
+
 /**
  * 主题切换
  */
@@ -253,8 +271,32 @@ function checkCapslock(e: any) {
 }
 
 onMounted?.(() => {
+  // 处理SSO
+  const query: LocationQuery = route.query;
+  if (query.token) {
+    loginData.value.token = query.token + "";
+    toLogin();
+    // localStorage.setItem("token", query.token + "");
+    // const redirect = (query.redirect as LocationQueryValue) ?? "/";
+    // // 获取字典
+    // getUserDicts(dictStore.types).then((res) => {
+    //   if (res.data) {
+    //     dictStore.dicts = res?.data ?? [];
+    //   }
+    // });
+    //
+    // router.push({ path: redirect });
+  }
+
   getOrgList();
   toggleTheme();
+  if (
+    localStorage.getItem("local_name") &&
+    localStorage.getItem("local_name") !== "null"
+  ) {
+    loginData.value.userName = localStorage.getItem("local_name");
+    loginData.value.password = localStorage.getItem("local_pwd");
+  }
 });
 </script>
 

+ 16 - 3
src/views/main/components/header.vue

@@ -16,14 +16,19 @@
       title=""
       virtual-triggering
     >
-      <div class="loginOut" @click="loginOutFun">退出登录</div>
       <div class="loginOut" @click="deviceResourceFun">仪器资源</div>
+      <div class="loginOut" @click="goToManange">部门管理</div>
+      <div class="loginOut" @click="userCenter">修改密码</div>
+      <div class="loginOut" @click="loginOutFun">退出登录</div>
     </el-popover>
+
+    <UserCenter ref="userCenterRef" />
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, unref } from "vue";
+import UserCenter from "@/views/modules/person-manager/com/userCenter.vue";
 import { ClickOutside as vClickOutside } from "element-plus";
 
 const router = useRouter();
@@ -33,13 +38,21 @@ const onClickOutside = () => {
   unref(popoverRef).popperRef?.delayHide?.();
 };
 const loginOutFun = () => {
-  alert("退出登录");
-  // window.location.href = "/login";
+  localStorage.setItem("token", "");
+  router.replace("/login");
 };
 
 const deviceResourceFun = () => {
   router.push("/deviceResources");
 };
+const goToManange = () => {
+  router.push("/detp-management");
+};
+
+const userCenterRef = ref();
+const userCenter = () => {
+  userCenterRef.value && userCenterRef.value?.show();
+};
 </script>
 
 <style scoped lang="scss">

+ 316 - 0
src/views/modules/person-manager/com/dept-manage.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="manager-container">
+    <SecondHeader>仪器资源配置</SecondHeader>
+    <div class="search-container">
+      <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+        <el-form-item label="关键字" prop="keywords">
+          <el-input
+            v-model="queryParams.keywords"
+            placeholder="名称/编码"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+
+        <el-form-item label="组织状态" prop="state">
+          <el-select
+            v-model="queryParams.state"
+            placeholder="全部"
+            clearable
+            class="!w-[100px]"
+          >
+            <el-option :value="0" label="正常" />
+            <el-option :value="1" label="禁用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button class="filter-item" type="primary" @click="handleQuery">
+            <i-ep-search />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery"> <i-ep-refresh />重置 </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <el-card shadow="never" class="table-container">
+      <template #header>
+        <el-button type="primary" @click="openDialog(undefined, undefined)"
+          ><i-ep-plus />新增</el-button
+        >
+      </template>
+
+      <el-table
+        v-loading="loading"
+        :data="deptList"
+        row-key="id"
+        default-expand-all
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      >
+        <el-table-column prop="deptName" align="center" label="组织名称" />
+        <el-table-column prop="deptCode" align="center" label="组织编码" />
+        <el-table-column prop="orgType" label="组织类型">
+          <template #default="scope">
+            <el-tag v-if="scope.row.orgType == 0" type="success">公司</el-tag>
+            <el-tag v-else type="primary">部门</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="state" label="状态">
+          <template #default="scope">
+            <el-tag v-if="scope.row.state == 0" type="success">正常</el-tag>
+            <el-tag v-else type="info">禁用</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="created" align="center" label="创建时间" />
+        <el-table-column prop="creator" align="center" label="创建人员" />
+        <el-table-column label="操作" fixed="right" align="left">
+          <template #default="scope">
+            <el-button
+              type="primary"
+              link
+              size="small"
+              @click.stop="openDialog(scope.row.id, undefined)"
+              ><i-ep-plus />新增
+            </el-button>
+            <el-button
+              type="primary"
+              link
+              size="small"
+              @click.stop="openDialog(scope.row.parentId, scope.row)"
+              ><i-ep-edit />编辑
+            </el-button>
+            <el-button
+              type="primary"
+              link
+              size="small"
+              @click.stop="handleDelete(scope.row.id)"
+            >
+              <i-ep-delete />删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog
+      v-model="dialog.visible"
+      :title="dialog.title"
+      width="600px"
+      @closed="closeDialog"
+    >
+      <el-form
+        ref="deptFormRef"
+        :model="formData"
+        :rules="rules"
+        label-width="80px"
+      >
+        <el-form-item label="上级组织" prop="parentId">
+          <el-tree-select
+            v-model="formData.parentId"
+            placeholder="选择上级组织"
+            :data="deptOptions"
+            :props="{ value: 'id', label: 'deptName' }"
+            filterable
+            check-strictly
+            :render-after-expand="false"
+          />
+        </el-form-item>
+        <el-form-item label="组织编码" prop="deptCode">
+          <el-input v-model="formData.deptCode" placeholder="请输入组织编码" />
+        </el-form-item>
+        <el-form-item label="组织名称" prop="deptName">
+          <el-input v-model="formData.deptName" placeholder="请输入组织名称" />
+        </el-form-item>
+        <el-form-item label="组织类别">
+          <el-radio-group v-model="formData.orgType">
+            <el-radio :value="0">公司</el-radio>
+            <el-radio :value="1">部门</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="组织状态">
+          <el-radio-group v-model="formData.state">
+            <el-radio :value="0">正常</el-radio>
+            <el-radio :value="1">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSubmit"> 确 定 </el-button>
+          <el-button @click="closeDialog"> 取 消 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {
+  deleteDept,
+  updateDept,
+  addDept,
+  queryTreeList,
+  treeList,
+} from "@/api/system/dept";
+import SecondHeader from "@/views/modules/conmon/SecondHeader.vue";
+defineOptions({
+  name: "Dept",
+  inheritAttrs: false,
+});
+
+const queryFormRef = ref(ElForm);
+const deptFormRef = ref(ElForm);
+const loading = ref(false);
+const ids = ref<number[]>([]);
+const dialog = reactive({
+  title: "",
+  visible: false,
+});
+
+const queryParams = reactive<any>({});
+const deptList = ref([]);
+
+const deptOptions = ref([]);
+
+const formData = reactive<any>({
+  status: 1,
+  parentId: 0,
+  sort: 1,
+});
+
+const rules = reactive({
+  parentId: [{ required: true, message: "顶级组织不能为空", trigger: "blur" }],
+  deptName: [{ required: true, message: "组织名称不能为空", trigger: "blur" }],
+  deptCode: [{ required: true, message: "组织编码不能为空", trigger: "blur" }],
+});
+
+/** 查询 */
+function handleQuery() {
+  loading.value = true;
+  queryTreeList(queryParams).then((data) => {
+    deptList.value = data.data;
+    loading.value = false;
+  });
+}
+
+/**重置查询 */
+function resetQuery() {
+  queryFormRef.value.resetFields();
+  handleQuery();
+}
+
+/** 行复选框选中记录选中ID集合 */
+function handleSelectionChange(selection: any) {
+  ids.value = selection.map((item: any) => item.id);
+}
+
+/** 获取组织下拉数据  */
+async function loadDeptOptions() {
+  treeList().then((response) => {
+    deptOptions.value = [
+      {
+        id: "0",
+        deptName: "顶级组织",
+        children: response.data,
+      },
+    ];
+  });
+}
+
+/**
+ * 打开弹窗
+ *
+ * @param parentId 父组织ID
+ * @param dept 组织
+ */
+async function openDialog(parentId?: number, dept?: any) {
+  await loadDeptOptions();
+  dialog.visible = true;
+  if (dept !== undefined) {
+    dialog.title = "修改组织";
+    Object.assign(formData, dept);
+  } else {
+    dialog.title = "新增组织";
+    formData.deptCode = "";
+    formData.deptName = "";
+    formData.orgType = 0;
+    formData.state = 0;
+    formData.parentId = parentId ?? "0";
+  }
+}
+
+/** 表单提交 */
+function handleSubmit() {
+  deptFormRef.value.validate((valid: any) => {
+    if (valid) {
+      const deptId = formData.id;
+      loading.value = true;
+      if (deptId) {
+        updateDept(formData)
+          .then(() => {
+            ElMessage.success("修改成功");
+            closeDialog();
+            handleQuery();
+          })
+          .finally(() => (loading.value = false));
+      } else {
+        addDept(formData)
+          .then(() => {
+            ElMessage.success("新增成功");
+            closeDialog();
+            handleQuery();
+          })
+          .finally(() => (loading.value = false));
+      }
+    }
+  });
+}
+
+/** 删除组织 */
+function handleDelete(deptId?: number) {
+  const deptIds = [deptId || ids.value];
+
+  if (!deptIds) {
+    ElMessage.warning("请勾选删除项");
+    return;
+  }
+  const params = { ids: deptIds };
+  ElMessageBox.confirm(`确认删除已选中的数据项?`, "警告", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(() => {
+    deleteDept(params).then(() => {
+      ElMessage.success("删除成功");
+      resetQuery();
+    });
+  });
+}
+
+/** 关闭弹窗 */
+function closeDialog() {
+  dialog.visible = false;
+  resetForm();
+}
+
+/** 重置表单  */
+function resetForm() {
+  deptFormRef.value.resetFields();
+  deptFormRef.value.clearValidate();
+
+  formData.id = undefined;
+  formData.parentId = "0";
+  formData.status = 1;
+}
+
+onMounted?.(() => {
+  handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+.manager-container {
+  width: 100%;
+  height: 100vh;
+}
+</style>

+ 74 - 0
src/views/modules/person-manager/com/dept-tree.vue

@@ -0,0 +1,74 @@
+<!-- 部门树 -->
+<template>
+  <el-card shadow="never">
+    <el-input v-model="deptName" placeholder="部门名称" clearable>
+      <template #prefix>
+        <i-ep-search />
+      </template>
+    </el-input>
+
+    <el-tree
+      ref="deptTreeRef"
+      class="mt-2"
+      :data="deptList"
+      :props="{ children: 'children', label: 'deptName', disabled: '' }"
+      :expand-on-click-node="false"
+      :filter-node-method="handleFilter"
+      highlight-current
+      default-expand-all
+      @node-click="handleNodeClick"
+    />
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { treeList } from "@/api/system/dept";
+
+const props = defineProps({
+  modelValue: {
+    type: [String],
+    default: undefined,
+  },
+});
+
+const deptList = ref<OptionType[]>(); // 部门列表
+const deptTreeRef = ref(ElTree); // 部门树
+const deptName = ref(); // 部门名称
+
+const emits = defineEmits(["node-click", "update:modelValue"]);
+
+const deptId = useVModel(props, "modelValue", emits);
+
+watchEffect?.(
+  () => {
+    deptTreeRef.value.filter(deptName.value);
+  },
+  {
+    flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
+  }
+);
+
+/** 部门筛选 */
+function handleFilter(value: string, data: any) {
+  if (!value) {
+    return true;
+  }
+  return data.deptName.indexOf(value) !== -1;
+}
+
+/** 部门树节点 Click */
+function handleNodeClick(data: { [key: string]: any }) {
+  deptId.value = data.id;
+  emits("node-click");
+}
+
+onBeforeMount?.(() => {
+  treeList().then((response) => {
+    if (response.data) {
+      deptId.value = response.data[0].id;
+      emits("node-click");
+    }
+    deptList.value = response.data;
+  });
+});
+</script>

+ 116 - 0
src/views/modules/person-manager/com/userCenter.vue

@@ -0,0 +1,116 @@
+<template>
+  <el-dialog
+    title="个人中心"
+    v-model="visible"
+    width="1200px"
+    :append-to-body="true"
+    :destroy-on-close="true"
+  >
+    <el-form
+      ref="passwordFormRef"
+      v-model:model="passwordForm"
+      status-icon
+      :rules="rules"
+      label-width="auto"
+      class="demo-ruleForm"
+    >
+      <el-form-item label="旧密码" prop="oldPassword">
+        <el-input
+          v-model="passwordForm.oldPassword"
+          type="password"
+          autocomplete="off"
+        />
+      </el-form-item>
+      <el-form-item label="新密码" prop="password">
+        <el-input
+          v-model="passwordForm.password"
+          type="password"
+          autocomplete="off"
+        />
+      </el-form-item>
+      <el-form-item label="确认密码" prop="confirmPassword">
+        <el-input type="password" v-model="passwordForm.confirmPassword" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="submitForm"> 修改密码 </el-button>
+        <el-button @click="resetForm">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {
+  getUserDetailApi,
+  updateHeadImg,
+  updateBaseInfo,
+} from "@/api/system/user/index";
+import { useUserStoreHook } from "@/store/modules/user";
+import SingleUpload from "@/components/Upload/SingleUpload.vue";
+
+const userStore = useUserStoreHook();
+const activeName = ref("first");
+const visible = ref(false);
+const userInfo = ref<any>({});
+const headUrl = ref("");
+
+const show = () => {
+  visible.value = true;
+};
+
+defineExpose({ show });
+
+const uploadHeadFinish = () => {
+  updateHeadImg(userStore.user.userId, { avatar: headUrl.value }).then(() => {
+    ElMessage.success("头像上传成功");
+  });
+};
+// =============== 修改密码相关
+const passwordFormRef = ref();
+const passwordForm = ref({
+  oldPassword: "",
+  password: "",
+  confirmPassword: "",
+});
+const validatePass = (rule: any, value: any, callback: any) => {
+  if (value === "") {
+    callback(new Error("请输入"));
+  } else if (value !== passwordForm.value.password) {
+    callback(new Error("两次密码不一致,请重新输入!"));
+  } else {
+    callback();
+  }
+};
+const rules = reactive({
+  oldPassword: [{ required: true, trigger: "blur" }],
+  password: [{ required: true, trigger: "blur" }],
+  confirmPassword: [
+    { required: true, validator: validatePass, trigger: "blur" },
+  ],
+});
+
+const submitForm = () => {
+  passwordFormRef.value.validate((valid: boolean) => {
+    if (valid) {
+      updateBaseInfo(userStore.user.userId, {
+        password: passwordForm.value.password,
+        oldPassword: passwordForm.value.oldPassword,
+      }).then(() => {
+        ElMessage.success("密码修改成功");
+        visible.value = false;
+        passwordFormRef.value.resetFields();
+      });
+    }
+  });
+};
+
+const resetForm = () => {
+  passwordFormRef.value.resetFields();
+};
+</script>
+
+<style scoped lang="scss">
+.lable {
+  font-weight: bolder;
+}
+</style>

+ 706 - 43
src/views/modules/person-manager/person-manager.vue

@@ -1,47 +1,710 @@
+<!-- 用户管理 -->
 <template>
-  <el-mention
-    v-model="value1"
-    whole
-    :options="options1"
-    style="width: 320px"
-    placeholder="Please input"
-  />
-  <el-divider />
-  <el-mention
-    v-model="value2"
-    :options="options2"
-    :prefix="['@', '#']"
-    whole
-    :check-is-whole="checkIsWhole"
-    style="width: 320px"
-    placeholder="input @ to mention people, # to mention tag"
-    @search="handleSearch"
-  />
-  <p style="color: red">{{ value1 }}||</p>
+  <div class="mainContentBox">
+    <el-row :gutter="20">
+      <!-- 部门树 -->
+      <el-col :lg="4" :xs="24" class="mb-[12px]">
+        <DeptTree v-model="queryParams.deptId" @node-click="handleQuery" />
+      </el-col>
+
+      <!-- 用户列表 -->
+      <el-col :lg="20" :xs="24">
+        <div class="search-container">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="关键字" prop="keywords">
+              <el-input
+                v-model="queryParams.keywords"
+                placeholder="用户名/昵称/手机号"
+                clearable
+                style="width: 200px"
+                @keyup.enter="handleQuery"
+              />
+            </el-form-item>
+
+            <el-form-item label="状态" prop="state">
+              <el-select
+                v-model="queryParams.state"
+                placeholder="全部"
+                clearable
+                class="!w-[100px]"
+              >
+                <el-option label="启用" value="0" />
+                <el-option label="禁用" value="1" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleQuery"
+                ><i-ep-search />搜索</el-button
+              >
+              <el-button @click="resetQuery">
+                <i-ep-refresh />
+                重置</el-button
+              >
+            </el-form-item>
+          </el-form>
+        </div>
+
+        <el-card shadow="never" class="table-container">
+          <template #header>
+            <div class="flex justify-between">
+              <div>
+                <el-button type="primary" @click="openDialog('user-form')"
+                  ><i-ep-plus />新增</el-button
+                >
+                <el-button
+                  type="danger"
+                  :disabled="removeIds.length === 0"
+                  @click="handleDelete()"
+                  ><i-ep-delete />删除</el-button
+                >
+              </div>
+              <!--              <div>-->
+              <!--                <el-dropdown split-button>-->
+              <!--                  导入-->
+              <!--                  <template #dropdown>-->
+              <!--                    <el-dropdown-menu>-->
+              <!--                      <el-dropdown-item @click="downloadTemplate">-->
+              <!--                        <i-ep-download />下载模板</el-dropdown-item-->
+              <!--                      >-->
+              <!--                      <el-dropdown-item @click="openDialog('user-import')">-->
+              <!--                        <i-ep-top />导入数据</el-dropdown-item-->
+              <!--                      >-->
+              <!--                    </el-dropdown-menu>-->
+              <!--                  </template>-->
+              <!--                </el-dropdown>-->
+              <!--                <el-button class="ml-3" @click="handleExport"-->
+              <!--                  ><template #icon><i-ep-download /></template>导出</el-button-->
+              <!--                >-->
+              <!--              </div>-->
+            </div>
+          </template>
+
+          <el-table
+            v-loading="loading"
+            :data="pageData"
+            @selection-change="handleSelectionChange"
+          >
+            <el-table-column type="selection" width="50" align="center" />
+            <el-table-column
+              label="用户名"
+              width="120"
+              align="center"
+              prop="userName"
+            />
+            <el-table-column
+              label="姓名"
+              width="120"
+              align="center"
+              prop="nickName"
+            />
+            <el-table-column
+              key="employeeCode"
+              label="员工编号"
+              align="center"
+              prop="employeeCode"
+            />
+            <el-table-column label="性别" width="100" align="center" prop="sex">
+              <template #default="scope">
+                <el-tag :type="scope.row.sex == 0 ? 'info' : 'success'">{{
+                  scope.row.sex == 0 ? "未知" : scope.row.sex == 1 ? "男" : "女"
+                }}</el-tag>
+              </template>
+            </el-table-column>
+
+            <el-table-column
+              label="部门"
+              width="120"
+              align="center"
+              overHidden="true"
+              prop="deptNames"
+            />
+            <el-table-column
+              label="手机号码"
+              align="center"
+              prop="phone"
+              width="120"
+            />
+
+            <el-table-column label="状态" align="center" prop="state">
+              <template #default="scope">
+                <el-tag :type="scope.row.state == 0 ? 'success' : 'info'">{{
+                  scope.row.state == 0 ? "启用" : "禁用"
+                }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column
+              label="创建时间"
+              align="center"
+              prop="created"
+              width="180"
+            />
+            <el-table-column label="操作" fixed="right" width="220">
+              <template #default="scope">
+                <el-button
+                  type="primary"
+                  size="small"
+                  link
+                  v-if="scope.row.id !== '1'"
+                  @click="resetPassword(scope.row)"
+                  ><i-ep-refresh-left />重置密码</el-button
+                >
+                <el-button
+                  type="primary"
+                  link
+                  size="small"
+                  v-if="scope.row.id !== '1'"
+                  @click="openDialog('user-form', scope.row)"
+                  ><i-ep-edit />编辑</el-button
+                >
+                <el-button
+                  type="primary"
+                  link
+                  size="small"
+                  v-if="scope.row.id !== '1'"
+                  @click="handleDelete(scope.row.id)"
+                  ><i-ep-delete />删除</el-button
+                >
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <pagination
+            v-if="total > 0"
+            v-model:total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="handleQuery"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 弹窗 -->
+    <el-dialog
+      v-model="dialog.visible"
+      :title="dialog.title"
+      :width="dialog.width"
+      append-to-body
+      @close="closeDialog"
+    >
+      <!-- 用户新增/编辑表单 -->
+      <el-form
+        v-if="dialog.type === 'user-form'"
+        ref="userFormRef"
+        :model="formData"
+        :rules="rules"
+        label-width="90px"
+      >
+        <el-row :gutter="22">
+          <el-col :span="11">
+            <el-form-item label="用户名称" prop="userName">
+              <el-input
+                v-model="formData.userName"
+                :disabled="!!formData.id"
+                placeholder="请输入用户名称"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="11">
+            <el-form-item label="用户昵称" prop="nickName">
+              <el-input
+                v-model="formData.nickName"
+                placeholder="请输入用户昵称"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="22">
+          <el-col :span="11">
+            <el-form-item label="员工编号" prop="employeeCode">
+              <el-input
+                v-model="formData.employeeCode"
+                placeholder="请输入员工编号"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="11">
+            <el-form-item label="性别" prop="sex">
+              <el-radio-group v-model="formData.sex">
+                <el-radio :value="0">未知</el-radio>
+                <el-radio :value="1">男</el-radio>
+                <el-radio :value="2">女</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="22">
+          <el-col :span="22">
+            <el-form-item label="所属部门" prop="deptIds">
+              <el-tree-select
+                v-model="formData.deptIds"
+                placeholder="请选择所属部门"
+                :data="deptList"
+                :multiple="true"
+                filterable
+                show-checkbox
+                load-key="deptName"
+                value-key="id"
+                :props="{
+                  children: 'children',
+                  label: 'deptName',
+                  value: 'id',
+                  disabled: '',
+                }"
+                check-strictly
+                :render-after-expand="false"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!--        <el-row :gutter="22">-->
+        <!--          <el-col :span="22">-->
+        <!--            <el-form-item label="角色" prop="roleIds">-->
+        <!--              <el-select-->
+        <!--                v-model="formData.roleIds"-->
+        <!--                multiple-->
+        <!--                placeholder="请选择"-->
+        <!--              >-->
+        <!--                <el-option-->
+        <!--                  v-for="item in roleList"-->
+        <!--                  :key="item.id"-->
+        <!--                  :label="item.roleName"-->
+        <!--                  :value="item.id"-->
+        <!--                />-->
+        <!--              </el-select>-->
+        <!--            </el-form-item>-->
+        <!--          </el-col>-->
+        <!--        </el-row>-->
+
+        <el-row :gutter="22">
+          <el-col :span="22">
+            <el-form-item label="岗位" prop="postIds">
+              <el-select
+                v-model="formData.postIds"
+                multiple
+                placeholder="请选择"
+              >
+                <el-option
+                  v-for="item in postList"
+                  :key="item.id"
+                  :label="item.postName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="22">
+          <el-col :span="11">
+            <el-form-item label="手机号码" prop="phone">
+              <el-input
+                v-model="formData.phone"
+                placeholder="请输入手机号码"
+                maxlength="11"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="11">
+            <el-form-item label="邮箱" prop="email">
+              <el-input
+                v-model="formData.email"
+                placeholder="请输入邮箱"
+                maxlength="50"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="状态" prop="state">
+          <el-radio-group v-model="formData.state">
+            <el-radio :value="0">启用</el-radio>
+            <el-radio :value="1">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+
+      <!-- 用户导入表单 -->
+      <el-form
+        v-else-if="dialog.type === 'user-import'"
+        :model="importData"
+        label-width="100px"
+      >
+        <el-form-item label="部门">
+          <el-tree-select
+            v-model="importData.deptId"
+            placeholder="请选择部门"
+            :data="deptList"
+            load-key="deptName"
+            value-key="id"
+            :props="{
+              children: 'children',
+              label: 'deptName',
+              value: 'id',
+              disabled: '',
+            }"
+            filterable
+            check-strictly
+          />
+        </el-form-item>
+
+        <el-form-item label="Excel文件">
+          <el-upload
+            ref="uploadRef"
+            action=""
+            drag
+            accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
+            :limit="1"
+            :auto-upload="false"
+            :file-list="importData.fileList"
+            :on-change="handleFileChange"
+            :on-exceed="handleFileExceed"
+          >
+            <el-icon class="el-icon--upload">
+              <i-ep-upload-filled />
+            </el-icon>
+            <div class="el-upload__text">
+              将文件拖到此处,或
+              <em>点击上传</em>
+            </div>
+            <template #tip>
+              <div>xls/xlsx files</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <!-- 弹窗底部操作按钮 -->
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSubmit">确 定</el-button>
+          <el-button @click="closeDialog">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
-<script setup lang="ts">
-import { ref } from "vue";
-import type { MentionOption } from "element-plus";
-
-const MOCK_DATA: Record<string, string[]> = {
-  "@": ["Fuphoenixes", "kooriookami", "Jeremy", "btea"],
-  "#": ["1.0", "2.0", "3.0"],
-};
-const value1 = ref("");
-const value2 = ref("");
-const options1 = ref<MentionOption[]>(
-  MOCK_DATA["@"].map((value) => ({ value }))
-);
-const options2 = ref<MentionOption[]>([]);
-
-const handleSearch = (_: string, prefix: string) => {
-  options2.value = (MOCK_DATA[prefix] || []).map((value) => ({
-    value,
-  }));
-};
-
-const checkIsWhole = (pattern: string, prefix: string) => {
-  return (MOCK_DATA[prefix] || []).includes(pattern);
-};
+<script setup>
+defineOptions({
+  name: "User",
+  inheritAttrs: false,
+});
+
+import DeptTree from "./com/dept-tree.vue";
+import {
+  getUserPage,
+  deleteUsers,
+  addUser,
+  updateUser,
+  updateUserPassword,
+  downloadTemplateApi,
+  exportUser,
+  getPostOptions,
+  importUser,
+} from "@/api/system/user";
+import { treeList } from "@/api/system/dept";
+import { getRoleOptions } from "@/api/system/role";
+
+import { genFileId } from "element-plus";
+
+const queryFormRef = ref(ElForm); // 查询表单
+const userFormRef = ref(ElForm); // 用户表单
+const uploadRef = ref(); // 上传组件
+
+const loading = ref(false); //  加载状态
+const removeIds = ref([]); // 删除用户ID集合 用于批量删除
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+});
+const dateTimeRange = ref("");
+const total = ref(0); // 数据总数
+const pageData = ref(); // 用户分页数据
+const deptList = ref(); // 部门下拉数据源
+const roleList = ref(); // 角色下拉数据源
+const postList = ref(); // 岗位下拉数据源
+
+// 弹窗对象
+const dialog = reactive({
+  visible: false,
+  type: "user-form",
+  width: 800,
+  title: "",
+});
+
+// 用户表单数据
+const formData = reactive({
+  state: 0,
+  sex: 0,
+  email: "",
+});
+
+// 用户导入数据
+const importData = reactive({
+  deptId: undefined,
+  file: undefined,
+  fileList: [],
+});
+
+// 校验规则
+const rules = reactive({
+  userName: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
+  nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+  employeeCode: [
+    { required: true, message: "员工编号不能为空", trigger: "blur" },
+  ],
+  deptIds: [{ required: true, message: "所属部门不能为空", trigger: "blur" }],
+  // roleIds: [{ required: true, message: "用户角色不能为空", trigger: "blur" }],
+  postIds: [{ required: true, message: "用户岗位不能为空", trigger: "blur" }],
+  email: [
+    {
+      pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
+      message: "请输入正确的邮箱地址",
+      trigger: "blur",
+    },
+  ],
+  phone: [
+    {
+      pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+      message: "请输入正确的手机号码",
+      trigger: "blur",
+    },
+  ],
+});
+
+/** 查询 */
+function handleQuery() {
+  loading.value = true;
+  queryParams.deptQuery = queryParams.deptId;
+  getUserPage(queryParams)
+    .then(({ data }) => {
+      pageData.value = data.records;
+      total.value = data.totalCount;
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+}
+
+/** 重置查询 */
+function resetQuery() {
+  queryFormRef.value.resetFields();
+  dateTimeRange.value = "";
+  queryParams.pageNo = 1;
+  //queryParams.deptId = undefined;
+  queryParams.startTime = undefined;
+  queryParams.endTime = undefined;
+  handleQuery();
+}
+
+/** 行选中 */
+function handleSelectionChange(selection) {
+  removeIds.value = selection.map((item) => item.id);
+}
+
+/** 重置密码 */
+function resetPassword(row) {
+  ElMessageBox.confirm("确认重置用户密码吗?", "警告", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(function () {
+    updateUserPassword(row).then(() => {
+      ElMessage.success("密码重置成功,新密码为系统初始密码");
+    });
+  });
+}
+
+/** 加载角色下拉数据源 */
+async function loadRoleOptions() {
+  getRoleOptions({}).then((response) => {
+    roleList.value = response.data;
+  });
+}
+/** 加载岗位下拉数据源 */
+async function loadPostOptions() {
+  getPostOptions({}).then((response) => {
+    postList.value = response.data;
+  });
+}
+/** 加载部门下拉数据源 */
+async function loadDeptOptions() {
+  treeList().then((response) => {
+    deptList.value = response.data;
+  });
+}
+
+/**
+ * 打开弹窗
+ *
+ * @param type 弹窗类型  用户表单:user-form | 用户导入:user-import
+ * @param id 用户ID
+ */
+async function openDialog(type, row) {
+  dialog.visible = true;
+  dialog.type = type;
+
+  if (dialog.type === "user-form") {
+    // 用户表单弹窗
+    await loadDeptOptions();
+    await loadPostOptions();
+    await loadRoleOptions();
+    if (row) {
+      dialog.title = "修改用户";
+      Object.assign(formData, row);
+    } else {
+      dialog.title = "新增用户";
+    }
+  } else if (dialog.type === "user-import") {
+    // 用户导入弹窗
+    dialog.title = "导入用户";
+    dialog.width = 600;
+    await loadDeptOptions();
+  }
+}
+
+/**
+ * 关闭弹窗
+ *
+ * @param type 弹窗类型  用户表单:user-form | 用户导入:user-import
+ */
+function closeDialog() {
+  dialog.visible = false;
+  if (dialog.type === "user-form") {
+    userFormRef.value.resetFields();
+    userFormRef.value.clearValidate();
+
+    formData.id = undefined;
+    formData.status = 1;
+  } else if (dialog.type === "user-import") {
+    importData.file = undefined;
+    importData.fileList = [];
+  }
+}
+
+/** 表单提交 */
+const handleSubmit = useThrottleFn(() => {
+  if (dialog.type === "user-form") {
+    userFormRef.value.validate((valid) => {
+      if (valid) {
+        const userId = formData.id;
+        loading.value = true;
+        if (userId) {
+          updateUser(userId, formData)
+            .then(() => {
+              ElMessage.success("修改用户成功");
+              closeDialog();
+              resetQuery();
+            })
+            .finally(() => (loading.value = false));
+        } else {
+          addUser(formData)
+            .then(() => {
+              ElMessage.success("新增用户成功");
+              closeDialog();
+              resetQuery();
+            })
+            .finally(() => (loading.value = false));
+        }
+      }
+    });
+  } else if (dialog.type === "user-import") {
+    if (!importData?.deptId) {
+      ElMessage.warning("请选择部门");
+      return false;
+    }
+    if (!importData?.file) {
+      ElMessage.warning("上传Excel文件不能为空");
+      return false;
+    }
+    importUser(importData?.deptId, importData?.file).then((response) => {
+      ElMessage.success(response.msg);
+      closeDialog();
+      resetQuery();
+    });
+  }
+}, 3000);
+
+/** 删除用户 */
+function handleDelete(id) {
+  ElMessageBox.confirm("确认删除用户?", "警告", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(function () {
+    deleteUsers(id).then(() => {
+      ElMessage.success("删除成功");
+      resetQuery();
+    });
+  });
+}
+
+/** 下载导入模板 */
+function downloadTemplate() {
+  downloadTemplateApi().then((response) => {
+    const fileData = response.data;
+    const fileName = decodeURI(
+      response.headers["content-disposition"].split(";")[1].split("=")[1]
+    );
+    const fileType =
+      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
+
+    const blob = new Blob([fileData], { type: fileType });
+    const downloadUrl = window.URL.createObjectURL(blob);
+
+    const downloadLink = document.createElement("a");
+    downloadLink.href = downloadUrl;
+    downloadLink.download = fileName;
+
+    document.body.appendChild(downloadLink);
+    downloadLink.click();
+
+    document.body.removeChild(downloadLink);
+    window.URL.revokeObjectURL(downloadUrl);
+  });
+}
+
+/** Excel文件 Change */
+function handleFileChange(file) {
+  importData.file = file.raw;
+}
+
+/** Excel文件 Exceed  */
+function handleFileExceed(files) {
+  uploadRef.value.clearFiles();
+  const file = files[0];
+  file.uid = genFileId();
+  uploadRef.value.handleStart(file);
+  importData.file = file;
+}
+
+/** 导出用户 */
+function handleExport() {
+  exportUser(queryParams).then((response) => {
+    const fileData = response.data;
+    const fileName = decodeURI(
+      response.headers["content-disposition"].split(";")[1].split("=")[1]
+    );
+    const fileType =
+      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
+
+    const blob = new Blob([fileData], { type: fileType });
+    const downloadUrl = window.URL.createObjectURL(blob);
+
+    const downloadLink = document.createElement("a");
+    downloadLink.href = downloadUrl;
+    downloadLink.download = fileName;
+
+    document.body.appendChild(downloadLink);
+    downloadLink.click();
+
+    document.body.removeChild(downloadLink);
+    window.URL.revokeObjectURL(downloadUrl);
+  });
+}
+
+onMounted?.(() => {
+  handleQuery();
+});
 </script>