Procházet zdrojové kódy

feat: 新增岗位和部门管理

lujialiang před 3 týdny
rodič
revize
8dcb263a3b

+ 40 - 0
src/locales/langs/zh-cn.ts

@@ -435,6 +435,46 @@ const local: App.I18n.Schema = {
           local: '本地图标'
         }
       }
+    },
+    admin: {
+      sysDept: {
+        parentId: '上级部门',
+        deptName: '部门名称',
+        orderNum: '显示排序',
+        leader: '负责人',
+        phone: '联系电话',
+        email: '邮箱',
+        status: '部门状态',
+        form: {
+          parentId: '请选择上级部门',
+          deptName: '请输入部门名称',
+          orderNum: '请输入显示排序',
+          leader: '请输入负责人',
+          phone: '请输入联系电话',
+          email: '请输入邮箱',
+          status: '请选择部门状态'
+        },
+        addDept: '新增部门',
+        editDept: '编辑部门'
+      },
+      sysPost: {
+        postId: '岗位id',
+        postName: '岗位名称',
+        postCode: '岗位编码',
+        sort: '岗位顺序',
+        status: '岗位状态',
+        remark: '备注',
+        form: {
+          postId: '请输入岗位id',
+          postName: '请输入岗位名称',
+          postCode: '请输入岗位编码',
+          sort: '请输入岗位顺序',
+          status: '请选择岗位状态',
+          remark: '请输入备注'
+        },
+        addTitle: '添加岗位',
+        editTitle: '编辑岗位'
+      }
     }
   },
   form: {

+ 2 - 0
src/router/elegant/imports.ts

@@ -22,7 +22,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   login: () => import("@/views/_builtin/login/index.vue"),
   about: () => import("@/views/about/index.vue"),
   "admin_sys-dept": () => import("@/views/admin/sys-dept/index.vue"),
+  "admin_sys-login-log": () => import("@/views/admin/sys-login-log/index.vue"),
   "admin_sys-menu": () => import("@/views/admin/sys-menu/index.vue"),
+  "admin_sys-post": () => import("@/views/admin/sys-post/index.vue"),
   "admin_sys-role": () => import("@/views/admin/sys-role/index.vue"),
   "admin_sys-user": () => import("@/views/admin/sys-user/index.vue"),
   "function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),

+ 18 - 0
src/router/elegant/routes.ts

@@ -68,6 +68,15 @@ export const generatedRoutes: GeneratedRoute[] = [
           i18nKey: 'route.admin_sys-dept'
         }
       },
+      {
+        name: 'admin_sys-login-log',
+        path: '/admin/sys-login-log',
+        component: 'view.admin_sys-login-log',
+        meta: {
+          title: 'admin_sys-login-log',
+          i18nKey: 'route.admin_sys-login-log'
+        }
+      },
       {
         name: 'admin_sys-menu',
         path: '/admin/sys-menu',
@@ -77,6 +86,15 @@ export const generatedRoutes: GeneratedRoute[] = [
           i18nKey: 'route.admin_sys-menu'
         }
       },
+      {
+        name: 'admin_sys-post',
+        path: '/admin/sys-post',
+        component: 'view.admin_sys-post',
+        meta: {
+          title: 'admin_sys-post',
+          i18nKey: 'route.admin_sys-post'
+        }
+      },
       {
         name: 'admin_sys-role',
         path: '/admin/sys-role',

+ 2 - 0
src/router/elegant/transform.ts

@@ -179,7 +179,9 @@ const routeMap: RouteMap = {
   "about": "/about",
   "admin": "/admin",
   "admin_sys-dept": "/admin/sys-dept",
+  "admin_sys-login-log": "/admin/sys-login-log",
   "admin_sys-menu": "/admin/sys-menu",
+  "admin_sys-post": "/admin/sys-post",
   "admin_sys-role": "/admin/sys-role",
   "admin_sys-user": "/admin/sys-user",
   "function": "/function",

+ 89 - 0
src/service/api/admin.ts

@@ -0,0 +1,89 @@
+import { baseRequest } from '../request';
+
+// 获取部门列表
+export function fetchDeptList(params?: object) {
+  return baseRequest({
+    url: '/api/v1/dept',
+    method: 'get',
+    params
+  });
+}
+
+// 新增部门
+export function fetchDeptAdd(data: object) {
+  return baseRequest({
+    url: '/api/v1/dept',
+    method: 'post',
+    data
+  });
+}
+
+// 查询部门详细
+export function fetchDeptDetail(deptId: string) {
+  return baseRequest({
+    url: `/api/v1/dept/${deptId}`,
+    method: 'get'
+  });
+}
+
+// 修改部门
+export function fetchDeptEdit(data: object, id: string) {
+  return baseRequest({
+    url: `/api/v1/dept/${id}`,
+    method: 'put',
+    data
+  });
+}
+
+// 删除部门
+export function fetchDeptDel(data: object) {
+  return baseRequest({
+    url: '/api/v1/dept',
+    method: 'delete',
+    data
+  });
+}
+
+// 查询岗位列表
+export function fetchPostList(params: object) {
+  return baseRequest({
+    url: '/api/v1/post',
+    method: 'get',
+    params
+  });
+}
+
+// 查询岗位详细
+export function fetchPostDetail(postId: string) {
+  return baseRequest({
+    url: `/api/v1/post/${postId}`,
+    method: 'get'
+  });
+}
+
+// 新增岗位
+export function fetchPostAdd(data: object) {
+  return baseRequest({
+    url: '/api/v1/post',
+    method: 'post',
+    data
+  });
+}
+
+// 修改岗位
+export function fetchPostEdit(data: object, id: string) {
+  return baseRequest({
+    url: `/api/v1/post/${id}`,
+    method: 'put',
+    data
+  });
+}
+
+// 删除岗位
+export function fetchPostDel(data: object) {
+  return baseRequest({
+    url: '/api/v1/post',
+    method: 'delete',
+    data
+  });
+}

+ 1 - 0
src/service/api/index.ts

@@ -1,3 +1,4 @@
 export * from './auth';
 export * from './route';
 export * from './system-manage';
+export * from './admin';

+ 0 - 9
src/service/api/system-manage.ts

@@ -202,12 +202,3 @@ export function changeUserStatus(data: any) {
     data
   });
 }
-
-// 获取部门列表
-export function fetchDeptList(params?: Api.SystemManage.UserSearchParams) {
-  return baseRequest<Api.SystemManage.UserList>({
-    url: '/api/v1/dept',
-    method: 'get',
-    params
-  });
-}

+ 1 - 0
src/store/modules/route/index.ts

@@ -251,6 +251,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
 
     if (!error) {
       const { routes, home } = transformToTargetData(data);
+      console.log('routes', routes);
 
       addAuthRoutes(routes);
 

+ 29 - 0
src/typings/api.d.ts

@@ -263,4 +263,33 @@ declare namespace Api {
       children?: MenuTree[];
     };
   }
+
+  namespace Admin {
+    type SysDept = Common.CommonRecord<{
+      deptId: number | null;
+      parentId: string | null;
+      deptName: string;
+      orderNum: number;
+      leader: string;
+      phone: string;
+      email: string;
+      status: string;
+    }>;
+    type SysPost = Common.CommonRecord<{
+      postId: string;
+      postCode: string;
+      postName: string;
+      sort: number;
+      status: string;
+      createdAt: string;
+      remark: string;
+    }>;
+
+    interface CodeImg {
+      code: number;
+      data: object;
+      requestId: string;
+      msg: string;
+    }
+  }
 }

+ 40 - 0
src/typings/app.d.ts

@@ -601,6 +601,46 @@ declare namespace App {
             };
           };
         };
+        admin: {
+          sysDept: {
+            parentId: string;
+            deptName: string;
+            orderNum: string;
+            leader: string;
+            phone: string;
+            email: string;
+            status: string;
+            form: {
+              parentId: string;
+              deptName: string;
+              orderNum: string;
+              leader: string;
+              phone: string;
+              email: string;
+              status: string;
+            };
+            addDept: string;
+            editDept: string;
+          };
+          sysPost: {
+            postId: string;
+            postName: string;
+            postCode: string;
+            sort: string;
+            status: string;
+            remark: string;
+            form: {
+              postId: string;
+              postName: string;
+              postCode: string;
+              sort: string;
+              status: string;
+              remark: string;
+            };
+            addTitle: string;
+            editTitle: string;
+          };
+        };
       };
       form: {
         required: string;

+ 4 - 0
src/typings/elegant-router.d.ts

@@ -35,7 +35,9 @@ declare module "@elegant-router/types" {
     "about": "/about";
     "admin": "/admin";
     "admin_sys-dept": "/admin/sys-dept";
+    "admin_sys-login-log": "/admin/sys-login-log";
     "admin_sys-menu": "/admin/sys-menu";
+    "admin_sys-post": "/admin/sys-post";
     "admin_sys-role": "/admin/sys-role";
     "admin_sys-user": "/admin/sys-user";
     "function": "/function";
@@ -143,7 +145,9 @@ declare module "@elegant-router/types" {
     | "login"
     | "about"
     | "admin_sys-dept"
+    | "admin_sys-login-log"
     | "admin_sys-menu"
+    | "admin_sys-post"
     | "admin_sys-role"
     | "admin_sys-user"
     | "function_hide-child_one"

+ 23 - 29
src/views/admin/sys-dept/index.vue

@@ -1,21 +1,17 @@
 <script setup lang="tsx">
 import { useTable, useTableOperate, useTableScroll } from '@/hooks/common/table';
 import { $t } from '@/locales';
-import { fetchDelteUser, fetchDeptList } from '@/service/api';
+import { fetchDelDept, fetchDeptList, getDicts } from '@/service/api';
 import { Button, Popconfirm } from 'ant-design-vue';
 import dayjs from 'dayjs';
 import { ref } from 'vue';
-import UserOperateDrawer from './modules/user-operate-drawer.vue';
-import UserResetPwdDialog from './modules/user-resetpwd-dialog.vue';
+import MOperateDrawer from './modules/m-operate-drawer.vue';
 import MSearch from './modules/m-search.vue';
 const { tableWrapperRef, scrollConfig } = useTableScroll();
 const apiParams = ref({
   pageIndex: 1,
   pageSize: 10
 });
-let transUserId: number | null = null
-let userName = ''
-let resetDialogVisible = ref(false)
 
 const {
   columns,
@@ -54,12 +50,12 @@ const {
       minWidth: 100,
       customRender: ({ record }) => {
         let statusMap = {
-          1: '停用',
-          2: '启用',
+          '1': '停用',
+          '2': '正常',
         }
         let typeMap = {
-          1: 'error',
-          2: 'success',
+          '1': 'error',
+          '2': 'success',
         }
         return <a-tag color={typeMap[record.status]}>{statusMap[record.status]}</a-tag>
       }
@@ -82,17 +78,14 @@ const {
       width: 180,
       customRender: ({ record }) => (
         <div class="flex-center gap-0.1rem">
-          <Button type="primary" ghost size="small" onClick={() => handleRowEdit(record.userId)} style="margin-right:5px">
+          <Button type="primary" ghost size="small" onClick={() => handleRowEdit(record)} style="margin-right:5px">
             {$t('common.edit')}
           </Button>
-          <Popconfirm title={$t('common.confirmDelete')} onConfirm={() => handleDelete(record.userId)}>
+          <Popconfirm title={$t('common.confirmDelete')} onConfirm={() => handleDelete(record.deptId)}>
             <Button danger size="small" style="margin-right:5px">
               {$t('common.delete')}
             </Button>
           </Popconfirm>
-          <Button size="small" onClick={() => handleResetPwd(record)}>
-            重置
-          </Button>
         </div>
       )
     }
@@ -103,10 +96,9 @@ const {
   operateType,
   editingData,
   handleAdd,
-  handleEdit,
   checkedRowKeys,
-  onDeleted
-  // closeDrawer
+  onDeleted,
+  openDrawer
 } = useTableOperate(data, getData);
 
 async function handleBatchDelete() {
@@ -118,7 +110,8 @@ async function handleDelete(id: any) {
   // request
   let ids = []
   typeof id === 'number' ? ids.push(id) : ids = [].concat(id)
-  const res = await fetchDelteUser({ ids })
+  console.log('handleDelete', ids)
+  const res = await fetchDelDept({ ids })
   if (res.code === 200) {
     onDeleted();
   } else {
@@ -126,17 +119,19 @@ async function handleDelete(id: any) {
   }
 }
 
-// 重置
-function handleResetPwd(row: any) {
-  resetDialogVisible.value = true
-  userName = row.username
-  transUserId = row.userId
+// 编辑
+function handleRowEdit(row: AntDesign.TableData) {
+  operateType.value = 'edit'
+  editingData.value = row
+  openDrawer()
 }
 
-// 编辑
-function handleRowEdit(id: number) {
-  handleEdit(id, 'userId');
+const deptStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  deptStatus.value = data || []
 }
+getDeptStatusOptions()
 </script>
 
 <template>
@@ -152,9 +147,8 @@ function handleRowEdit(id: number) {
         :loading="loading" row-key="userId" :pagination="mobilePagination" class="h-full"
         :defaultExpandAllRows="true" />
 
-      <UserOperateDrawer v-model:visible="drawerVisible" :operate-type="operateType" :row-data="editingData"
+      <MOperateDrawer v-model:visible="drawerVisible" :operate-type="operateType" :row-data="editingData"
         @submitted="getDataByPage" />
-      <UserResetPwdDialog v-model:dialogVisible="resetDialogVisible" :userId="transUserId" :user-name="userName" />
     </ACard>
   </div>
 </template>

+ 196 - 0
src/views/admin/sys-dept/modules/m-operate-drawer.vue

@@ -0,0 +1,196 @@
+<script setup lang="ts">
+import { useAntdForm, useFormRules } from '@/hooks/common/form';
+import { $t } from '@/locales';
+import { fetchDeptAdd, fetchDeptList, getDicts, fetchDeptDetail, fetchDeptEdit } from '@/service/api';
+import { computed, reactive, ref, watch } from 'vue';
+defineOptions({
+  name: 'DeptOperateDrawer'
+});
+
+interface Props {
+  /** the type of operation */
+  operateType: AntDesign.TableOperateType;
+  /** the edit row data */
+  rowData?: Api.Admin.SysDept | null;
+}
+
+const props = defineProps<Props>();
+
+interface Emits {
+  (e: 'submitted'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const visible = defineModel<boolean>('visible', {
+  default: false
+});
+
+const { formRef, validate, resetFields } = useAntdForm();
+const { defaultRequiredRule, formRules, createRequiredRule } = useFormRules();
+
+const title = computed(() => {
+  const titles: Record<AntDesign.TableOperateType, string> = {
+    add: $t('page.admin.sysDept.addDept'),
+    edit: $t('page.admin.sysDept.editDept')
+  };
+  return titles[props.operateType];
+});
+
+type Model = Pick<
+  Api.Admin.SysDept,
+  'parentId' | 'deptName' | 'orderNum' | 'leader' | 'phone' | 'email' | 'status'>;
+
+const model: Model = reactive(createDefaultModel());
+
+function createDefaultModel(): Model {
+  return {
+    parentId: null,
+    deptName: '',
+    orderNum: 10,
+    leader: '',
+    phone: '',
+    email: '',
+    status: '2',
+  };
+}
+
+type RuleKey = Extract<keyof Model, 'parentId' | 'deptName' | 'orderNum' | 'leader' | 'phone' | 'email' | 'status'>;
+
+const rules: Record<RuleKey, App.Global.FormRule> = {
+  parentId: createRequiredRule($t('page.admin.sysDept.form.parentId')),
+  deptName: defaultRequiredRule,
+  orderNum: defaultRequiredRule,
+  leader: defaultRequiredRule,
+  phone: formRules.phone,
+  email: formRules.email,
+  status: defaultRequiredRule
+};
+
+const deptList = ref<CommonType.Option<string>[]>([]);
+async function getDeptList() {
+  const { data } = await fetchDeptList()
+  const dept = { deptId: '0', value: '0', deptName: '主类目', label: '主类目', children: [], selectable: false }
+  dept.children = data
+  deptList.value = [dept]
+}
+
+const deptStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  deptStatus.value = data || []
+}
+
+async function getDeptDetails() {
+  let deptId = props.rowData?.deptId + ''
+  const { data } = await fetchDeptDetail(deptId)
+  data.status = data.status + ''
+  data.orderNum = data.sort
+  Object.assign(model, data);
+}
+
+async function handleInitModel() {
+  Object.assign(model, createDefaultModel());
+  console.log('handleInitModel', props.operateType, props.rowData)
+  if (props.operateType === 'edit' && props.rowData) {
+    getDeptDetails()
+  }
+}
+
+function closeDrawer() {
+  visible.value = false;
+}
+
+async function handleSubmit() {
+  await validate();
+  let fnMap = {
+    add: () => {
+      const { code, msg } = fetchDeptAdd(model)
+      if (code === 200) {
+        window.$message?.success(msg);
+        closeDrawer();
+        emit('submitted');
+      }
+    },
+    edit: () => {
+      let deptId = props.rowData?.deptId + ''
+      const { code, msg } = fetchDeptEdit(model, deptId)
+      if (code === 200) {
+        window.$message?.success(msg);
+        closeDrawer();
+        emit('submitted');
+      }
+      console.log('edit')
+    }
+  }
+  fnMap[props.operateType] && fnMap[props.operateType]()
+}
+
+watch(visible, () => {
+  if (visible.value) {
+    handleInitModel()
+    resetFields()
+    getDeptList()
+    getDeptStatusOptions()
+  }
+});
+</script>
+
+<template>
+  <AModal v-model:open="visible" :title="title" width="800px">
+    <AForm ref="formRef" :model="model" :rules="rules" :label-col="{ lg: 8, xs: 4 }" label-wrap class="pr-20px">
+      <ARow wrap>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.parentId')" name="parentId">
+            <ATreeSelect v-model:value="model.parentId" show-search :tree-data="deptList"
+              :fieldNames="{ value: 'deptId', children: 'children', label: 'deptName' }"
+              :placeholder="$t('page.admin.sysDept.form.parentId')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.deptName')" name="deptName">
+            <AInput v-model:value="model.deptName" :placeholder="$t('page.admin.sysDept.form.deptName')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.orderNum')" name="orderNum">
+            <AInputNumber v-model:value="model.orderNum" :min="0" :placeholder="$t('page.admin.sysDept.form.orderNum')"
+              allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.leader')" name="leader">
+            <AInput v-model:value="model.leader" :placeholder="$t('page.admin.sysDept.form.leader')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.phone')" name="phone">
+            <AInput v-model:value="model.phone" :placeholder="$t('page.admin.sysDept.form.phone')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.email')" name="email">
+            <AInput v-model:value="model.email" :placeholder="$t('page.admin.sysDept.form.email')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysDept.status')" name="status">
+            <ARadioGroup v-model:value="model.status">
+              <ARadio v-for="item in deptStatus" :key="item.value" :value="item.value">
+                {{ item.label }}
+              </ARadio>
+            </ARadioGroup>
+          </AFormItem>
+        </ACol>
+      </ARow>
+    </AForm>
+    <template #footer>
+      <ASpace :size="16">
+        <AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
+        <AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
+      </ASpace>
+    </template>
+  </AModal>
+</template>
+
+<style scoped></style>

+ 12 - 8
src/views/admin/sys-dept/modules/m-search.vue

@@ -1,10 +1,7 @@
-/* eslint-disable */
 <script setup lang="ts">
-import { enableStatusOptions } from '@/constants/business';
 import { useAntdForm, useFormRules } from '@/hooks/common/form';
 import { $t } from '@/locales';
-import { fetchGetdeptTreeList } from '@/service/api';
-import { translateOptions } from '@/utils/common';
+import { fetchGetdeptTreeList, getDicts } from '@/service/api';
 import { computed, onMounted, ref } from 'vue';
 defineOptions({
   name: 'MSearch'
@@ -19,9 +16,9 @@ const emit = defineEmits<Emits>();
 const departOptions = ref<CommonType.Option<string>[]>([]);
 const { formRef, validate, resetFields } = useAntdForm();
 
-const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
+const model = defineModel<Api.Admin.SysDept>('model', { required: true });
 
-type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'email' | 'phone'>;
+type RuleKey = Extract<keyof Api.Admin.SysDept, 'email' | 'phone'>;
 
 const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
   const { patternRules } = useFormRules(); // inside computed to make locale reactive
@@ -48,6 +45,13 @@ async function search() {
   emit('search');
 }
 
+const deptStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  deptStatus.value = data || []
+}
+getDeptStatusOptions()
+
 </script>
 
 <template>
@@ -64,7 +68,7 @@ async function search() {
       <ARow :gutter="[16, 16]" wrap>
         <ACol :span="24" :md="12" :lg="6">
           <AFormItem label="部门名称" name="deptName" class="m-0">
-            <AInput v-model:value.trim="model.username" placeholder="请输入部门名称" @keyup.enter.native="search" allowClear/>
+            <AInput v-model:value.trim="model.deptName" placeholder="请输入部门名称" @keyup.enter.native="search" allowClear/>
           </AFormItem>
         </ACol>
         
@@ -73,7 +77,7 @@ async function search() {
             <ASelect
               v-model:value="model.status"
               placeholder="请选择部门状态"
-              :options="translateOptions(enableStatusOptions)"
+              :options="deptStatus"
               @change="search"
               allowClear
             />

+ 220 - 0
src/views/admin/sys-login-log/index.vue

@@ -0,0 +1,220 @@
+<script setup lang="tsx">
+import { useTable, useTableOperate, useTableScroll } from '@/hooks/common/table';
+import { $t } from '@/locales';
+import { changeUserStatus, fetchDelteUser, fetchGetUserList } from '@/service/api';
+import { Button, Popconfirm } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { ref } from 'vue';
+import UserOperateDrawer from './modules/user-operate-drawer.vue';
+import UserResetPwdDialog from './modules/user-resetpwd-dialog.vue';
+import UserSearch from './modules/user-search.vue';
+const { tableWrapperRef, scrollConfig } = useTableScroll();
+const apiParams = ref({
+  pageIndex: 1,
+  pageSize: 10
+});
+let transUserId:number | null = null
+let userName = ''
+let resetDialogVisible = ref(false)
+
+const {
+  columns,
+  columnChecks,
+  data,
+  getData,
+  getDataByPage,
+  loading,
+  mobilePagination,
+  searchParams,
+  resetSearchParams
+} = useTable({
+  apiFn: fetchGetUserList,
+  apiParams:apiParams.value,
+  showTotal: true,
+  columns: () => [
+    {
+      key: 'userId',
+      title: '用户ID',
+      dataIndex: 'userId',
+      align: 'center',
+      width: 64
+    },
+    {
+      key: 'username',
+      dataIndex: 'username',
+      title: '登录名',
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'nickName',
+      title: '昵称',
+      align: 'center',
+      dataIndex: 'nickName',
+      minWidth: 100
+    },
+    {
+      key: 'deptName',
+      dataIndex: 'deptName',
+      title: '部门',
+      align: 'center',
+      minWidth: 120,
+      customRender: ({ record }) => {
+        return record.dept.deptName;
+      }
+    },
+    {
+      key: 'phone',
+      dataIndex: 'phone',
+      title: $t('page.manage.user.userPhone'),
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'status',
+      dataIndex: 'status',
+      title: $t('page.manage.user.userStatus'),
+      align: 'center',
+      minWidth: 100,
+      customRender: ({ record}) => {
+        let showStatus:boolean = record.status == '2' ? true : false;
+        const text:string = record.status == '2' ? '停用' : '启用';
+        return <Popconfirm title={`确定要${text}${record.username}的用户吗?`} onConfirm={() => handleChangeStatus(record, text)} >
+          <a-switch v-model:checked={showStatus} ></a-switch>
+        </Popconfirm>;
+      }
+    },
+    {
+      key: 'createdAt',
+      dataIndex: 'createdAt',
+      title: '创建时间',
+      align: 'center',
+      minWidth: 150,
+      customRender:({record})=>{
+        return dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')
+      }
+    },
+    {
+      key: 'operate',
+      title: $t('common.operate'),
+      align: 'center',
+      fixed: 'right',
+      width: 180,
+      customRender: ({ record }) => (
+        <div class="flex-center gap-0.1rem">
+          <Button type="primary" ghost size="small" onClick={() => handleRowEdit(record.userId)} style="margin-right:5px">
+            {$t('common.edit')}
+          </Button>
+          <Popconfirm title={$t('common.confirmDelete')} onConfirm={() => handleDelete(record.userId)}>
+            <Button danger size="small" style="margin-right:5px">
+              {$t('common.delete')}
+            </Button>
+          </Popconfirm>
+          <Button size="small" onClick={() => handleResetPwd(record)}>
+            重置
+          </Button>
+        </div>
+      )
+    }
+  ]
+});
+const {
+  drawerVisible,
+  operateType,
+  editingData,
+  handleAdd,
+  handleEdit,
+  checkedRowKeys,
+  rowSelection,
+  onDeleted
+  // closeDrawer
+} = useTableOperate(data, getData);
+
+async function handleBatchDelete() {
+  // request
+  handleDelete(checkedRowKeys.value)
+}
+async function handleChangeStatus(row:any, text:string){
+  const data = {
+    userId: row.userId,
+    status: text === '停用'? '1' : '2'
+  }
+  const res = await changeUserStatus(data)
+  if (res.code === 200) {
+    window.$message?.success('更新成功!');
+  } else {
+    window.$message?.error('更新失败!');
+  }
+  await getData()
+
+}
+// 删除
+async function handleDelete(id: any) {
+  // request
+  let ids = []
+  typeof id === 'number' ? ids.push(id) : ids = [].concat(id)
+  const res = await fetchDelteUser({ids})
+  if (res.code === 200) {
+    onDeleted();
+  } else {
+    window.$message?.error('删除失败!');
+  }
+}
+
+// 重置
+function handleResetPwd(row:any) {
+  resetDialogVisible.value = true
+  userName = row.username
+  transUserId = row.userId
+}
+
+// 编辑
+function handleRowEdit(id: number) {
+  handleEdit(id, 'userId');
+}
+</script>
+
+<template>
+  <div class="min-h-6.25rem flex-col-stretch gap-0.2rem overflow-hidden lt-sm:overflow-auto">
+    <UserSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
+    <ACard
+      :title="$t('page.manage.user.title')"
+      :bordered="false"
+      :body-style="{ flex: 1, overflow: 'hidden' }"
+      class="flex-col-stretch sm:flex-1-hidden card-wrapper"
+    >
+      <template #extra>
+        <TableHeaderOperation
+          v-model:columns="columnChecks"
+          :disabled-delete="checkedRowKeys.length === 0"
+          :loading="loading"
+          @add="handleAdd"
+          @delete="handleBatchDelete"
+          @refresh="getData"
+        />
+      </template>
+      <ATable
+        ref="tableWrapperRef"
+        :columns="columns"
+        :data-source="data"
+        size="small"
+        :row-selection="rowSelection"
+        :scroll="scrollConfig"
+        :loading="loading"
+        row-key="userId"
+        :pagination="mobilePagination"
+        class="h-full"
+      />
+
+      <UserOperateDrawer
+        v-model:visible="drawerVisible"
+        :operate-type="operateType"
+        :row-data="editingData"
+        @submitted="getDataByPage"
+      />
+      <UserResetPwdDialog v-model:dialogVisible="resetDialogVisible" :userId="transUserId" :user-name="userName"/>
+    </ACard>
+  </div>
+</template>
+
+<style scoped></style>

+ 0 - 0
src/views/admin/sys-dept/modules/user-operate-drawer.vue → src/views/admin/sys-login-log/modules/user-operate-drawer.vue


+ 0 - 0
src/views/admin/sys-dept/modules/user-resetpwd-dialog.vue → src/views/admin/sys-login-log/modules/user-resetpwd-dialog.vue


+ 137 - 0
src/views/admin/sys-login-log/modules/user-search.vue

@@ -0,0 +1,137 @@
+/* eslint-disable */
+<script setup lang="ts">
+import { enableStatusOptions } from '@/constants/business';
+import { useAntdForm, useFormRules } from '@/hooks/common/form';
+import { $t } from '@/locales';
+import { fetchGetdeptTreeList } from '@/service/api';
+import { translateOptions } from '@/utils/common';
+import { computed, onMounted, ref } from 'vue';
+defineOptions({
+  name: 'UserSearch'
+});
+
+interface Emits {
+  (e: 'reset'): void;
+  (e: 'search'): void;
+}
+
+const emit = defineEmits<Emits>();
+const departOptions = ref<CommonType.Option<string>[]>([]);
+const { formRef, validate, resetFields } = useAntdForm();
+
+const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
+
+type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'email' | 'phone'>;
+
+const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
+  const { patternRules } = useFormRules(); // inside computed to make locale reactive
+
+  return {
+    email: patternRules.email,
+    phone: patternRules.phone
+  };
+});
+onMounted(()=>{
+  getDeptTree()
+})
+async function getDeptTree(){
+  const { data } = await fetchGetdeptTreeList()
+  departOptions.value = data
+}
+async function reset() {
+  await resetFields();
+  emit('reset');
+}
+
+async function search() {
+  await validate();
+  emit('search');
+}
+
+</script>
+
+<template>
+  <ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
+    <AForm
+      ref="formRef"
+      :model="model"
+      :rules="rules"
+      :label-col="{
+        span: 5,
+        md: 7
+      }"
+    >
+      <ARow :gutter="[16, 16]" wrap>
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.userName')" name="username" class="m-0">
+            <AInput v-model:value.trim="model.username" :placeholder="$t('page.manage.user.form.userName')" @keyup.enter.native="search" allowClear/>
+          </AFormItem>
+        </ACol>
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.deptId')" name="roleId">
+            <ATreeSelect v-model:value="model.deptId" show-search  :tree-data="departOptions"
+            :fieldNames="{value: 'id', children: 'children', label: 'label'}" @change="search"
+              :placeholder="$t('page.manage.user.form.deptId')" allowClear/>
+          </AFormItem>
+        </ACol>
+
+        <!-- <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.userGender')" name="sex" class="m-0">
+            <ASelect
+              v-model:value="model.sex"
+              :placeholder="$t('page.manage.user.form.userGender')"
+              :options="translateOptions(userGenderOptions)"
+              clearable
+            />
+          </AFormItem>
+        </ACol> -->
+        <!-- <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.nickName')" name="nickName" class="m-0">
+            <AInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
+          </AFormItem>
+        </ACol> -->
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.userPhone')" name="phone" class="m-0">
+            <AInput v-model:value.trim="model.phone" :placeholder="$t('page.manage.user.form.userPhone')" @keyup.enter.native="search" allowClear/>
+          </AFormItem>
+        </ACol>
+        <!-- <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.userEmail')" name="email" class="m-0">
+            <AInput v-model:value="model.email" :placeholder="$t('page.manage.user.form.userEmail')" />
+          </AFormItem>
+        </ACol> -->
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem :label="$t('page.manage.user.userStatus')" name="status" class="m-0">
+            <ASelect
+              v-model:value="model.status"
+              :placeholder="$t('page.manage.user.form.userStatus')"
+              :options="translateOptions(enableStatusOptions)"
+              @change="search"
+              allowClear
+            />
+          </AFormItem>
+        </ACol>
+        <div class="flex-1">
+          <AFormItem class="m-0">
+            <div class="w-full flex-y-center justify-end gap-12px">
+              <AButton @click="reset">
+                <template #icon>
+                  <icon-ic-round-refresh class="align-sub text-icon" />
+                </template>
+                <span class="ml-8px">{{ $t('common.reset') }}</span>
+              </AButton>
+              <AButton type="primary" ghost @click="search">
+                <template #icon>
+                  <icon-ic-round-search class="align-sub text-icon" />
+                </template>
+                <span class="ml-8px">{{ $t('common.search') }}</span>
+              </AButton>
+            </div>
+          </AFormItem>
+        </div>
+      </ARow>
+    </AForm>
+  </ACard>
+</template>
+
+<style scoped></style>

+ 168 - 0
src/views/admin/sys-post/index.vue

@@ -0,0 +1,168 @@
+<script setup lang="tsx">
+import { useTable, useTableOperate, useTableScroll } from '@/hooks/common/table';
+import { $t } from '@/locales';
+import { fetchPostDel, fetchPostList, getDicts } from '@/service/api';
+import { Button, Popconfirm } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { ref } from 'vue';
+import MOperateDrawer from './modules/m-operate-drawer.vue';
+import MSearch from './modules/m-search.vue';
+const { tableWrapperRef, scrollConfig } = useTableScroll();
+const apiParams = ref({
+  pageIndex: 1,
+  pageSize: 10
+});
+
+const {
+  columns,
+  columnChecks,
+  data,
+  getData,
+  getDataByPage,
+  loading,
+  mobilePagination,
+  searchParams,
+  resetSearchParams
+} = useTable({
+  apiFn: fetchPostList,
+  apiParams: apiParams.value,
+  showTotal: true,
+  columns: () => [
+    {
+      key: 'postId',
+      dataIndex: 'postId',
+      title: '岗位编号',
+      align: 'center',
+      minWidth: 100
+    },
+    {
+      key: 'postCode',
+      dataIndex: 'postCode',
+      title: '岗位编码',
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'postName',
+      dataIndex: 'postName',
+      title: '岗位名称',
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'sort',
+      dataIndex: 'sort',
+      title: '岗位排序',
+      align: 'center',
+      minWidth: 120
+    },
+    {
+      key: 'status',
+      dataIndex: 'status',
+      title: '状态',
+      align: 'center',
+      minWidth: 100,
+      customRender: ({ record }) => {
+        let statusMap = {
+          '1': '停用',
+          '2': '正常',
+        }
+        let typeMap = {
+          '1': 'error',
+          '2': 'success',
+        }
+        return <a-tag color={typeMap[record.status]}>{statusMap[record.status]}</a-tag>
+      }
+    },
+    {
+      key: 'createdAt',
+      dataIndex: 'createdAt',
+      title: '创建时间',
+      align: 'center',
+      minWidth: 150,
+      customRender: ({ record }) => {
+        return dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')
+      }
+    },
+    {
+      key: 'operate',
+      title: $t('common.operate'),
+      align: 'center',
+      fixed: 'right',
+      width: 180,
+      customRender: ({ record }) => (
+        <div class="flex-center gap-0.1rem">
+          <Button type="primary" ghost size="small" onClick={() => handleRowEdit(record.postId)} style="margin-right:5px">
+            {$t('common.edit')}
+          </Button>
+          <Popconfirm title={$t('common.confirmDelete')} onConfirm={() => handleDelete(record.deptId)}>
+            <Button danger size="small" style="margin-right:5px">
+              {$t('common.delete')}
+            </Button>
+          </Popconfirm>
+        </div>
+      )
+    }
+  ]
+});
+const {
+  drawerVisible,
+  operateType,
+  editingData,
+  handleAdd,
+  checkedRowKeys,
+  onDeleted,
+  handleEdit
+} = useTableOperate(data, getData);
+
+async function handleBatchDelete() {
+  // request
+  handleDelete(checkedRowKeys.value)
+}
+// 删除
+async function handleDelete(id: any) {
+  // request
+  let ids = []
+  typeof id === 'number' ? ids.push(id) : ids = [].concat(id)
+  console.log('handleDelete', ids)
+  const res = await fetchPostDel({ ids })
+  if (res.code === 200) {
+    onDeleted();
+  } else {
+    window.$message?.error('删除失败!');
+  }
+}
+
+// 编辑
+function handleRowEdit(id: number) {
+  handleEdit(id, 'postId')
+}
+
+const deptStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  deptStatus.value = data || []
+}
+getDeptStatusOptions()
+</script>
+
+<template>
+  <div class="min-h-6.25rem flex-col-stretch gap-0.2rem overflow-hidden lt-sm:overflow-auto">
+    <MSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
+    <ACard title="部门列表" :bordered="false" :body-style="{ flex: 1, overflow: 'hidden' }"
+      class="flex-col-stretch sm:flex-1-hidden card-wrapper">
+      <template #extra>
+        <TableHeaderOperation v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
+          :loading="loading" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData" />
+      </template>
+      <ATable ref="tableWrapperRef" :columns="columns" :data-source="data" size="small" :scroll="scrollConfig"
+        :loading="loading" row-key="userId" :pagination="mobilePagination" class="h-full"
+        :defaultExpandAllRows="true" />
+
+      <MOperateDrawer v-model:visible="drawerVisible" :operate-type="operateType" :row-data="editingData"
+        @submitted="getDataByPage" />
+    </ACard>
+  </div>
+</template>
+
+<style scoped></style>

+ 172 - 0
src/views/admin/sys-post/modules/m-operate-drawer.vue

@@ -0,0 +1,172 @@
+<script setup lang="ts">
+import { useAntdForm, useFormRules } from '@/hooks/common/form';
+import { $t } from '@/locales';
+import { getDicts, fetchPostDetail, fetchPostAdd, fetchPostEdit } from '@/service/api';
+import { computed, reactive, ref, watch } from 'vue';
+defineOptions({
+  name: 'MOperateDrawer'
+});
+
+interface Props {
+  /** the type of operation */
+  operateType: AntDesign.TableOperateType;
+  /** the edit row data */
+  rowData?: Api.Admin.SysPost | null;
+}
+
+const props = defineProps<Props>();
+
+interface Emits {
+  (e: 'submitted'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const visible = defineModel<boolean>('visible', {
+  default: false
+});
+
+const { formRef, validate, resetFields } = useAntdForm();
+const { defaultRequiredRule, createRequiredRule } = useFormRules();
+
+const title = computed(() => {
+  const titles: Record<AntDesign.TableOperateType, string> = {
+    add: $t('page.admin.sysPost.addTitle'),
+    edit: $t('page.admin.sysPost.editTitle')
+  };
+  return titles[props.operateType];
+});
+
+type Model = Pick<
+  Api.Admin.SysPost,
+  'postId' | 'postName' | 'postCode' | 'sort' | 'status' | 'remark'>;
+
+const model: Model = reactive(createDefaultModel());
+
+function createDefaultModel(): Model {
+  return {
+    postId: '',
+    postName: '',
+    postCode: '',
+    sort: 0,
+    status: '1',
+    remark: '',
+  };
+}
+
+type RuleKey = Extract<keyof Model, 'postId' | 'postName' | 'postCode' | 'sort' | 'status' | 'remark'>;
+
+const rules: Record<RuleKey, App.Global.FormRule> = {
+  postId: defaultRequiredRule,
+  postName: createRequiredRule($t('page.admin.sysPost.form.postName')),
+  postCode: createRequiredRule($t('page.admin.sysPost.form.postName')),
+  sort: defaultRequiredRule,
+  status: defaultRequiredRule,
+  remark: defaultRequiredRule,
+};
+
+const deptStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  deptStatus.value = data || []
+}
+
+async function getDeptDetails() {
+  let postId = props.rowData?.postId + ''
+  const { data } = await fetchPostDetail(postId)
+  data.status = data.status + ''
+  data.orderNum = data.sort
+  Object.assign(model, data);
+}
+
+async function handleInitModel() {
+  Object.assign(model, createDefaultModel());
+  if (props.operateType === 'edit' && props.rowData) {
+    getDeptDetails()
+  }
+}
+
+function closeDrawer() {
+  visible.value = false;
+}
+
+async function handleSubmit() {
+  await validate();
+  let fnMap = {
+    add: () => {
+      const { code, msg } = fetchPostAdd(model)
+      if (code === 200) {
+        window.$message?.success(msg);
+        closeDrawer();
+        emit('submitted');
+      }
+    },
+    edit: () => {
+      let postId = props.rowData?.postId + ''
+      const { code, msg } = fetchPostEdit(model, postId)
+      if (code === 200) {
+        window.$message?.success(msg);
+        closeDrawer();
+        emit('submitted');
+      }
+      console.log('edit')
+    }
+  }
+  fnMap[props.operateType] && fnMap[props.operateType]()
+}
+
+watch(visible, () => {
+  if (visible.value) {
+    handleInitModel()
+    resetFields()
+    getDeptStatusOptions()
+  }
+});
+</script>
+
+<template>
+  <AModal v-model:open="visible" :title="title" width="800px">
+    <AForm ref="formRef" :model="model" :rules="rules" :label-col="{ lg: 8, xs: 4 }" label-wrap class="pr-20px">
+      <ARow wrap>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysPost.postName')" name="postName">
+            <AInput v-model:value="model.postName" :placeholder="$t('page.admin.sysPost.form.postName')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysPost.postCode')" name="postCode">
+            <AInput v-model:value="model.postCode" :placeholder="$t('page.admin.sysPost.form.postCode')" allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysPost.sort')" name="sort">
+            <AInputNumber v-model:value="model.sort" :min="0" :placeholder="$t('page.admin.sysPost.form.sort')"
+              allowClear />
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysPost.status')" name="status">
+            <ARadioGroup v-model:value="model.status">
+              <ARadio v-for="item in deptStatus" :key="item.value" :value="item.value">
+                {{ item.label }}
+              </ARadio>
+            </ARadioGroup>
+          </AFormItem>
+        </ACol>
+        <ACol :span="12" :md="12" :xs="24">
+          <AFormItem :label="$t('page.admin.sysPost.remark')">
+            <ATextarea v-model:value="model.remark" :placeholder="$t('page.admin.sysPost.form.remark')" allowClear />
+          </AFormItem>
+        </ACol>
+      </ARow>
+    </AForm>
+    <template #footer>
+      <ASpace :size="16">
+        <AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
+        <AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
+      </ASpace>
+    </template>
+  </AModal>
+</template>
+
+<style scoped></style>

+ 114 - 0
src/views/admin/sys-post/modules/m-search.vue

@@ -0,0 +1,114 @@
+<script setup lang="ts">
+import { useAntdForm, useFormRules } from '@/hooks/common/form';
+import { $t } from '@/locales';
+import { fetchGetdeptTreeList, getDicts } from '@/service/api';
+import { computed, onMounted, ref } from 'vue';
+defineOptions({
+  name: 'MSearch'
+});
+
+interface Emits {
+  (e: 'reset'): void;
+  (e: 'search'): void;
+}
+
+const emit = defineEmits<Emits>();
+const departOptions = ref<CommonType.Option<string>[]>([]);
+const { formRef, validate, resetFields } = useAntdForm();
+
+const model = defineModel<Api.Admin.SysPost>('model', { required: true });
+
+type RuleKey = Extract<keyof Api.Admin.SysPost, 'email' | 'phone'>;
+
+const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
+  const { patternRules } = useFormRules(); // inside computed to make locale reactive
+
+  return {
+    email: patternRules.email,
+    phone: patternRules.phone
+  };
+});
+onMounted(()=>{
+  getDeptTree()
+})
+async function getDeptTree(){
+  const { data } = await fetchGetdeptTreeList()
+  departOptions.value = data
+}
+async function reset() {
+  await resetFields();
+  emit('reset');
+}
+
+async function search() {
+  await validate();
+  emit('search');
+}
+
+const normalStatus = ref<CommonType.Option<string>[]>([]);
+async function getDeptStatusOptions() {
+  const { data } = await getDicts('sys_normal_disable')
+  normalStatus.value = data || []
+}
+getDeptStatusOptions()
+
+</script>
+
+<template>
+  <ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
+    <AForm
+      ref="formRef"
+      :model="model"
+      :rules="rules"
+      :label-col="{
+        span: 5,
+        md: 7
+      }"
+    >
+      <ARow :gutter="[16, 16]" wrap>
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem label="岗位编码" name="postCode" class="m-0">
+            <AInput v-model:value.trim="model.postCode" placeholder="请输入岗位编码" @keyup.enter.native="search" allowClear/>
+          </AFormItem>
+        </ACol>
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem label="岗位名称" name="postName" class="m-0">
+            <AInput v-model:value.trim="model.postName" placeholder="请输入岗位名称" @keyup.enter.native="search" allowClear/>
+          </AFormItem>
+        </ACol>
+        
+        <ACol :span="24" :md="12" :lg="6">
+          <AFormItem label="岗位状态" name="status" class="m-0">
+            <ASelect
+              v-model:value="model.status"
+              placeholder="请选择岗位状态"
+              :options="normalStatus"
+              @change="search"
+              allowClear
+            />
+          </AFormItem>
+        </ACol>
+        <div class="flex-1">
+          <AFormItem class="m-0">
+            <div class="w-full flex-y-center justify-end gap-12px">
+              <AButton @click="reset">
+                <template #icon>
+                  <icon-ic-round-refresh class="align-sub text-icon" />
+                </template>
+                <span class="ml-8px">{{ $t('common.reset') }}</span>
+              </AButton>
+              <AButton type="primary" ghost @click="search">
+                <template #icon>
+                  <icon-ic-round-search class="align-sub text-icon" />
+                </template>
+                <span class="ml-8px">{{ $t('common.search') }}</span>
+              </AButton>
+            </div>
+          </AFormItem>
+        </div>
+      </ARow>
+    </AForm>
+  </ACard>
+</template>
+
+<style scoped></style>