Selaa lähdekoodia

feat(system): 新增部门管理功能

- 添加部门管理相关的 API 接口和类型定义
- 实现部门管理的表单和表格组件
- 添加部门管理的业务逻辑和数据处理
- 优化部门管理的用户体验和界面设计
zhangtao 4 päivää sitten
vanhempi
commit
1b5e9e6e57

+ 47 - 12
src/components/zt/ApiSelect/api-select.vue

@@ -1,15 +1,27 @@
 <script setup lang="ts">
-import { computed, ref, unref, watch, watchEffect } from 'vue';
+import { computed, onMounted, ref, unref } from 'vue';
+import { useDebounceFn } from '@vueuse/core';
+import { get } from '@/utils/zt/lodashChunk';
+import { isFunction } from '@/utils/zt/is';
 import { basicApiSelectProps } from './props';
 import type { ApiSelectProps } from './type';
+
+const debouncedFn = useDebounceFn(() => {
+  fetchApi();
+}, 1000);
+
 const props = withDefaults(defineProps<ApiSelectProps>(), {
   immediate: true,
-  resultFeild: 'data'
+  resultFeild: 'data',
+  clearable: true
 });
+
+const fetchLoading = ref(false);
 const basicProps = ref(basicApiSelectProps);
-const options = ref([]);
+const options = ref<Recordable[]>([]);
 const modelVlaue = defineModel<Array<string | number> | string | number | null>();
 const isFirstReq = ref(false);
+const searchText = ref('');
 const bindValue = computed(() => {
   return {
     ...props,
@@ -18,25 +30,32 @@ const bindValue = computed(() => {
 });
 
 async function fetchApi() {
-  const res = await props.api();
-  options.value = res[props.resultFeild];
+  const api = props.api;
+  if (!api || !isFunction(api)) return;
+  const params = {
+    ...props.params
+  };
+  if (unref(bindValue).filterFeild && unref(searchText)) {
+    params[String(unref(bindValue).filterFeild)] = unref(searchText);
+    console.log(params, 'apiselect请求参数');
+  }
+  const res = await api(params);
+  options.value = get(res, props.resultFeild);
+  fetchLoading.value = false;
 }
-watchEffect(() => {
+onMounted(() => {
   if (unref(bindValue).immediate) {
     fetchApi();
   }
 });
-watch(
-  () => modelVlaue.value,
-  () => {
-    console.log(modelVlaue.value);
-  }
-);
 function handleFocus() {
   if (!unref(bindValue).immediate && !unref(isFirstReq)) {
     fetchApi();
     isFirstReq.value = true;
   }
+  if (!unref(options).length && unref(bindValue).immediate) {
+    fetchApi();
+  }
 }
 function handleScroll(e: Event) {
   const currentTarget = e.currentTarget as HTMLElement;
@@ -44,6 +63,18 @@ function handleScroll(e: Event) {
     // console.log('到底了');
   }
 }
+function handleSearch(value: string) {
+  fetchLoading.value = true;
+  searchText.value = value;
+  debouncedFn();
+}
+function handleClear() {
+  searchText.value = '';
+  fetchApi();
+}
+function handleBlur() {
+  searchText.value = '';
+}
 </script>
 
 <template>
@@ -53,8 +84,12 @@ function handleScroll(e: Event) {
     :options="options"
     :label-field="labelFeild"
     :value-field="valueFeild"
+    :loading="fetchLoading"
     @focus="handleFocus"
     @scroll="handleScroll"
+    @search="handleSearch"
+    @clear="handleClear"
+    @blur="handleBlur"
   ></NSelect>
 </template>
 

+ 3 - 1
src/components/zt/ApiSelect/props.ts

@@ -4,5 +4,7 @@ export const basicApiSelectProps: ApiSelectProps = {
   labelFeild: '',
   valueFeild: '',
   immediate: true,
-  resultFeild: 'data'
+  resultFeild: 'data',
+  params: {},
+  filterFeild: ''
 };

+ 8 - 0
src/components/zt/ApiSelect/type/index.ts

@@ -22,4 +22,12 @@ export interface ApiSelectProps extends /* @vue-ignore */ SelectProps {
    * 请求结果字段
    */
   resultFeild?: string;
+  /**
+   * 请求参数
+   */
+  params?: Recordable;
+  /**
+   * 过滤字段(请求筛选字段)
+   */
+  filterFeild?: string;
 }

+ 61 - 0
src/components/zt/ApiTreeSelect/api-tree-select.vue

@@ -0,0 +1,61 @@
+<script setup lang="ts">
+import { computed, ref, unref, watch, watchEffect } from 'vue';
+import { basicApiSelectProps } from './props';
+import type { ApiTreeSelectProps } from './type';
+const props = withDefaults(defineProps<ApiTreeSelectProps>(), {
+  immediate: true,
+  resultFeild: 'data'
+});
+const basicProps = ref(basicApiSelectProps);
+const options = ref([]);
+const modelVlaue = defineModel<Array<string | number> | string | number | null>();
+const isFirstReq = ref(false);
+const bindValue = computed(() => {
+  return {
+    ...props,
+    ...basicProps
+  };
+});
+
+async function fetchApi() {
+  const res = await props.api();
+  options.value = res[props.resultFeild];
+}
+watchEffect(() => {
+  if (unref(bindValue).immediate) {
+    fetchApi();
+  }
+});
+watch(
+  () => modelVlaue.value,
+  () => {
+    console.log(modelVlaue.value);
+  }
+);
+function handleFocus() {
+  if (!unref(bindValue).immediate && !unref(isFirstReq)) {
+    fetchApi();
+    isFirstReq.value = true;
+  }
+}
+function handleScroll(e: Event) {
+  const currentTarget = e.currentTarget as HTMLElement;
+  if (currentTarget.scrollTop + currentTarget.offsetHeight >= currentTarget.scrollHeight) {
+    // console.log('到底了');
+  }
+}
+</script>
+
+<template>
+  <NTreeSelect
+    v-bind="bindValue"
+    v-model:value="modelVlaue"
+    :options="options"
+    :label-field="labelFeild"
+    :key-field="valueFeild"
+    @focus="handleFocus"
+    @scroll="handleScroll"
+  ></NTreeSelect>
+</template>
+
+<style scoped></style>

+ 3 - 0
src/components/zt/ApiTreeSelect/index.ts

@@ -0,0 +1,3 @@
+import ApiTreeSelect from './api-tree-select.vue';
+
+export { ApiTreeSelect };

+ 8 - 0
src/components/zt/ApiTreeSelect/props.ts

@@ -0,0 +1,8 @@
+import type { ApiTreeSelectProps } from './type';
+export const basicApiSelectProps: ApiTreeSelectProps = {
+  api: () => Promise.resolve({}),
+  labelFeild: '',
+  valueFeild: '',
+  immediate: true,
+  resultFeild: 'data'
+};

+ 25 - 0
src/components/zt/ApiTreeSelect/type/index.ts

@@ -0,0 +1,25 @@
+import type { TreeSelectProps } from 'naive-ui';
+export interface ApiTreeSelectProps extends /* @vue-ignore */ TreeSelectProps {
+  /**
+   * 请求方法
+   * @param params
+   * @returns
+   */
+  api: (params?: any) => Promise<any>;
+  /**
+   * 绑定的展示字段
+   */
+  labelFeild: string;
+  /**
+   * 绑定的值字段
+   */
+  valueFeild: string;
+  /**
+   * 是否立即请求
+   */
+  immediate?: boolean;
+  /**
+   * 请求结果字段
+   */
+  resultFeild?: string;
+}

+ 2 - 1
src/components/zt/Form/helper.ts

@@ -15,7 +15,8 @@ export function createPlaceholderMessage(component: keyof ComponentMap, label: s
       'NDatePicker',
       'NTimePicker',
       'NCheckboxGroup',
-      'ApiSelect'
+      'ApiSelect',
+      'ApiTreeSelect'
     ].includes(component)
   )
     return `请选择${label}`;

+ 6 - 2
src/components/zt/Form/types/form.ts

@@ -48,6 +48,8 @@ import type { GridItemProps, GridProps } from 'naive-ui/lib/grid';
 import type { ButtonProps } from 'naive-ui/lib/button';
 import { ApiSelect } from '../../ApiSelect';
 import type { ApiSelectProps } from '../../ApiSelect/type';
+import type { ApiTreeSelectProps } from '../../ApiTreeSelect/type';
+import { ApiTreeSelect } from '../../ApiTreeSelect';
 // componentMap.ts
 export interface FormProps {
   model?: Recordable;
@@ -121,7 +123,8 @@ export type FormSchema =
   | FormSchemaWithType<'NQrCode', QrCodeProps>
   | FormSchemaWithType<'NCalendar', CalendarProps>
   | FormSchemaWithType<'NDynamicTags', DynamicTagsProps>
-  | FormSchemaWithType<'ApiSelect', ApiSelectProps>;
+  | FormSchemaWithType<'ApiSelect', ApiSelectProps>
+  | FormSchemaWithType<'ApiTreeSelect', ApiTreeSelectProps>;
 
 export interface RenderCallbackParams {
   schema: FormSchema;
@@ -201,7 +204,8 @@ export const componentMap = {
   NCalendar,
   NDynamicTags,
   NMention,
-  ApiSelect
+  ApiSelect,
+  ApiTreeSelect
 } as const;
 
 export type ComponentMap = typeof componentMap;

+ 14 - 6
src/components/zt/Modal/basic-modal.vue

@@ -14,6 +14,7 @@ export default defineComponent({
     const propsRef = ref<Partial<typeof basicModalProps> | null>(null);
     const isModal = ref(false);
     const subLoading = ref(false);
+    const loading = ref(false);
     const getProps = computed((): modalProps => {
       return { ...props, ...unref(propsRef) } as unknown as modalProps;
     });
@@ -53,12 +54,16 @@ export default defineComponent({
       subLoading.value = true;
       emit('ok');
     }
+    function setModalLoading(status: boolean) {
+      loading.value = status;
+    }
 
     const modalMethods: ModalMethods = {
       setModalProps,
       openModal,
       closeModal,
-      setSubLoading
+      setSubLoading,
+      setModalLoading
     };
 
     const instance = getCurrentInstance();
@@ -74,7 +79,8 @@ export default defineComponent({
       getBindValue,
       isModal,
       closeModal,
-      subLoading
+      subLoading,
+      loading
     };
   }
 });
@@ -91,12 +97,14 @@ export default defineComponent({
       <div id="basic-modal-bar" class="w-full cursor-move">{{ getBindValue.title }}</div>
     </template>
     <template #default>
-      <NScrollbar class="pr-20px" :style="{ height: getBindValue.height + 'px' }">
-        <slot name="default"></slot>
-      </NScrollbar>
+      <NSpin :show="loading">
+        <NScrollbar class="pr-20px" :style="{ height: getBindValue.height + 'px' }">
+          <slot name="default"></slot>
+        </NScrollbar>
+      </NSpin>
     </template>
     <template v-if="!$slots.action" #action>
-      <NSpace justify="end">
+      <NSpace v-if="!loading" justify="end">
         <NButton @click="closeModal">取消</NButton>
         <NButton type="primary" :loading="subLoading" @click="handleSubmit">{{ getBindValue.subBtuText }}</NButton>
       </NSpace>

+ 3 - 0
src/components/zt/Modal/hooks/useModal.ts

@@ -35,6 +35,9 @@ export function useModal(props: modalProps): UseModalReturnType {
   };
 
   const methods: ModalMethods = {
+    setModalLoading(status) {
+      getInstance()?.setModalLoading(status);
+    },
     setModalProps: (newProps: modalProps): void => {
       getInstance()?.setModalProps(newProps);
     },

+ 1 - 0
src/components/zt/Modal/types/index.ts

@@ -7,6 +7,7 @@ export interface ModalMethods {
   openModal: (record?: Recordable, isView?: boolean) => void;
   closeModal: () => void;
   setSubLoading: (status: boolean) => void;
+  setModalLoading: (status: boolean) => void;
 }
 
 /**

+ 5 - 2
src/components/zt/ModalForm/basic-model-form.vue

@@ -32,7 +32,9 @@ export default defineComponent({
         ...unref(modelPropsRef)
       };
     });
-    const [registerModal, { openModal, closeModal, setSubLoading, setModalProps }] = useModal(unref(modalComputRef));
+    const [registerModal, { openModal, closeModal, setSubLoading, setModalProps, setModalLoading }] = useModal(
+      unref(modalComputRef)
+    );
     const [
       registerForm,
       {
@@ -64,7 +66,8 @@ export default defineComponent({
       getFieldsValue,
       restoreValidation,
       setSubLoading,
-      updateSchema
+      updateSchema,
+      setModalLoading
     };
 
     onMounted(() => {

+ 5 - 2
src/components/zt/ModalForm/hooks/useModalForm.ts

@@ -55,8 +55,6 @@ export function useModalFrom(props: modalFormProps): UseModalFormReturnType {
     // 表单相关方法
     updateSchema: async (formSchema: Partial<FormSchema> | Partial<FormSchema>[]) => {
       const form = await getForm();
-      console.log(form, 'form');
-
       nextTick(async () => {
         await form.updateSchema(formSchema);
       });
@@ -103,6 +101,11 @@ export function useModalFrom(props: modalFormProps): UseModalFormReturnType {
     },
 
     // 模态框相关方法
+    setModalLoading: async status => {
+      console.log(status, 'setModalLoading', getModalInstance());
+
+      getModalInstance()?.setModalLoading(status);
+    },
     setModalProps: (newProps: modalProps): void => {
       getModalInstance()?.setModalProps(newProps);
     },

+ 8 - 3
src/components/zt/Table/hooks/useTable.ts

@@ -1,7 +1,7 @@
 import { onUnmounted, ref, unref, watch } from 'vue';
-import { getDynamicProps } from '@/utils/zt';
+import type { tableProp, ztTableProps } from '../types';
 import type { FormProps } from '../../Form/types/form';
-export function useTable(tableProps: FormProps): UseTableReturnType {
+export function useTable(tableProps: ztTableProps): UseTableReturnType {
   const tableRef = ref<Nullable<TableMethods>>(null);
   function register(instance: TableMethods) {
     onUnmounted(() => {
@@ -15,7 +15,8 @@ export function useTable(tableProps: FormProps): UseTableReturnType {
       () => tableProps,
       () => {
         if (tableProps) {
-          instance.setSearchProps(getDynamicProps(tableProps) as FormProps);
+          instance.setSearchProps(tableProps.searchFormConfig);
+          instance.setTableConfig(tableProps.tableConfig);
         }
       },
       {
@@ -34,6 +35,9 @@ export function useTable(tableProps: FormProps): UseTableReturnType {
     },
     setTableLoading(loading) {
       tableRef.value?.setTableLoading(loading);
+    },
+    setTableConfig(config) {
+      tableRef.value?.setTableConfig(config);
     }
   };
   return [register, methods];
@@ -43,6 +47,7 @@ export interface TableMethods {
   refresh: () => Promise<any>;
   setSearchProps: (props: FormProps) => void;
   setTableLoading: (loading: boolean) => void;
+  setTableConfig: (config: tableProp) => void;
 }
 
 export type RegisterFn = (TableInstance: TableMethods) => void;

+ 10 - 4
src/components/zt/Table/props.ts

@@ -1,6 +1,14 @@
 import type { FormProps } from '../Form/types/form';
 import type { tableProp } from './types';
 export const basicProps = {
+  api: {
+    type: Function as PropType<tableProp['api']>,
+    default: () => Promise.resolve()
+  },
+  columns: {
+    type: Array as PropType<tableProp['columns']>,
+    default: () => []
+  },
   searchFormConfig: {
     type: Object as PropType<FormProps>,
     default: {
@@ -18,11 +26,9 @@ export const basicProps = {
     default: {
       opWdith: 130,
       title: '',
-      showDeleteButton: true,
+      showDeleteButton: false,
       showAddButton: true,
-      keyField: 'id',
-      api: () => Promise.resolve(),
-      columns: () => []
+      keyField: 'id'
     }
   }
 };

+ 9 - 5
src/components/zt/Table/types/index.ts

@@ -1,4 +1,4 @@
-import type { InternalRowData } from 'naive-ui/es/data-table/src/interface';
+import type { DataTableProps } from 'naive-ui';
 import type { FormProps } from '../../Form/types/form';
 
 export interface SearchProps {
@@ -8,20 +8,19 @@ export interface SearchProps {
   searchFormConfig?: FormProps;
 }
 
-export interface tableProp {
+export interface tableProp extends DataTableProps {
   opWdith?: number;
   keyField: string;
 
-  api: (params?: any) => Promise<any>;
+  api?: (params?: any) => Promise<any>;
 
-  columns: NaiveUI.TableColumn<InternalRowData>[];
   /**
    * 表格标题
    */
   title: string;
 
   /**
-   * 是否显示删除按钮
+   * 是否显示批量删除按钮
    */
   showDeleteButton?: boolean;
   /**
@@ -30,3 +29,8 @@ export interface tableProp {
    */
   showAddButton?: boolean;
 }
+
+export interface ztTableProps {
+  searchFormConfig: FormProps;
+  tableConfig: tableProp;
+}

+ 15 - 6
src/components/zt/Table/z-table.vue

@@ -32,11 +32,15 @@ export default defineComponent({
     });
     const getTableProps = computed(() => {
       return {
-        ...propsData,
         ...propsData.tableConfig,
         ...unref(tableProps)
       };
     });
+    const NTableProps = computed(() => {
+      return {
+        ...unref(tableProps)
+      };
+    });
     const [registerSearchForm, { getFieldsValue: getSeachForm, setFormProps: setSearchProps }] = useForm(
       getFormSearch.value
     );
@@ -46,7 +50,7 @@ export default defineComponent({
     });
     const getForm = ref();
     const { columns, columnChecks, data, getData, loading, mobilePagination } = useNaivePaginatedTable({
-      api: () => getTableProps.value.api({ ...searchPage, ...getForm.value }),
+      api: () => propsData.api({ ...searchPage, ...getForm.value }),
       transform: response => defaultTransform(response),
       onPaginationParamsChange: params => {
         searchPage.current = Number(params.page);
@@ -56,7 +60,7 @@ export default defineComponent({
         pageSizes: [10, 20, 50, 100, 150, 200]
       },
       columns: () => [
-        ...propsData.tableConfig.columns,
+        ...propsData.columns,
         {
           key: 'operate',
           title: '操作',
@@ -68,11 +72,14 @@ export default defineComponent({
       ]
     });
     const { checkedRowKeys } = useTableOperate(data, propsData.tableConfig.keyField, getData);
-
+    function setTableConfig(config: tableProp) {
+      tableProps.value = config;
+    }
     const TableMethod: TableMethods = {
       refresh: getData,
       setSearchProps,
-      setTableLoading
+      setTableLoading,
+      setTableConfig
     };
     function setTableLoading(flage: boolean) {
       loading.value = flage;
@@ -109,7 +116,8 @@ export default defineComponent({
       handleAdd,
       getData,
       handleReset,
-      handleSearch
+      handleSearch,
+      NTableProps
     };
   }
 });
@@ -141,6 +149,7 @@ export default defineComponent({
       </TableHeaderOperation>
     </template>
     <NDataTable
+      v-bind="NTableProps"
       v-model:checked-row-keys="checkedRowKeys"
       :columns="columns"
       :data="data"

+ 49 - 0
src/service/api/system-department.ts

@@ -0,0 +1,49 @@
+import { request } from '../request';
+/**
+ * 获取部门列表
+ */
+export function fetchGetDepartmentList(data: Recordable) {
+  return request<Api.SystemManage.DepartmentModel[]>({
+    url: '/admin/dept/list',
+    method: 'GET',
+    params: data
+  });
+}
+
+/**
+ *  修改部门
+ * @param data
+ * @returns
+ */
+export function fetchEditDepartment(data: Api.SystemManage.DepartmentModel) {
+  return request({
+    url: '/admin/dept',
+    method: 'PUT',
+    data
+  });
+}
+
+/**
+ * 新增部门
+ * @param data
+ * @returns
+ */
+export function fetchAddDepartment(data: Api.SystemManage.DepartmentModel) {
+  return request({
+    url: '/admin/dept',
+    method: 'POST',
+    data
+  });
+}
+
+/**
+ * 删除部门
+ * @param data
+ * @returns
+ */
+export function fetchDeleteDepartment(deptId: number) {
+  return request({
+    url: `/admin/dept/${deptId}`,
+    method: 'DELETE'
+  });
+}

+ 56 - 0
src/typings/api/system-manage.d.ts

@@ -5,6 +5,62 @@ declare namespace Api {
    * backend api module: "systemManage"
    */
   namespace SystemManage {
+    type DepartmentModel = {
+      /**
+       * 子部门
+       */
+      children?: DepartmentModel[];
+      /**
+       * 创建时间
+       */
+      createTime?: string;
+      /**
+       * 部门ID
+       */
+      deptId?: number;
+      /**
+       * 部门名称
+       */
+      deptName?: string;
+      /**
+       * 部门描述
+       */
+      description?: string;
+      /**
+       * 部门层级
+       */
+      level?: number;
+      /**
+       * 父级部门ID
+       */
+      parentId?: number;
+      /**
+       * 排序
+       */
+      seq?: number;
+      /**
+       * 状态 1:正常 0:禁用
+       */
+      status?: number;
+      /**
+       * 更新时间
+       */
+      updateTime?: string;
+      /**
+       * 部门负责人(用户ID)
+       */
+      leaderUserId?: number;
+      leaderUserName?: number;
+      /**
+       * 邮箱
+       */
+      email?: string;
+      /**
+       * 联系电话(加密存储)
+       */
+      phone?: string;
+      [property: string]: any;
+    };
     type UserModel = {
       userId?: number;
       username: string;

+ 1 - 0
src/typings/components.d.ts

@@ -9,6 +9,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     ApiSelect: typeof import('./../components/zt/ApiSelect/api-select.vue')['default']
+    ApiTreeSelect: typeof import('./../components/zt/ApiTreeSelect/api-tree-select.vue')['default']
     AppProvider: typeof import('./../components/common/app-provider.vue')['default']
     'Basic-ModelForm': typeof import('../components/zt/ModalForm/basic-model-form.vue')['default']
     BasicForm: typeof import('./../components/zt/Form/basic-form.vue')['default']

+ 123 - 0
src/views/manage/department/department.data.ts

@@ -0,0 +1,123 @@
+import { h } from 'vue';
+import { fetchGetDepartmentList } from '@/service/api/system-department';
+import { fetchGetUserList } from '@/service/api';
+import type { FormSchema } from '@/components/zt/Form/types/form';
+import { ApiSelect } from '@/components/zt/ApiSelect';
+
+export const formSchems: FormSchema[] = [
+  {
+    field: 'deptId',
+    show: false,
+    component: 'NInput'
+  },
+  {
+    field: 'parentId',
+    label: '上级部门',
+    component: 'ApiTreeSelect',
+    required: true,
+    componentProps: {
+      api: fetchGetDepartmentList,
+      labelFeild: 'deptName',
+      valueFeild: 'deptId',
+      immediate: true,
+      resultFeild: 'data'
+    }
+  },
+  {
+    field: 'deptName',
+    label: '部门名称',
+    component: 'NInput',
+    required: true
+  },
+  {
+    field: 'seq',
+    label: '显示排序',
+    component: 'NInputNumber',
+    required: true,
+    componentProps: {
+      min: 1
+    }
+  },
+  {
+    field: 'leaderUserId',
+    label: '负责人',
+    component: 'ApiSelect',
+    render({ model, field }) {
+      return h(ApiSelect, {
+        api: fetchGetUserList,
+        labelFeild: 'username',
+        valueFeild: 'userId',
+        resultFeild: 'data.records',
+        filterable: true,
+        remote: true,
+        filterFeild: 'username',
+        value: model[field],
+        onUpdateValue: (value, option: Recordable) => {
+          model[field] = value;
+          model.phone = option.mobile;
+          model.email = option.email;
+        }
+      });
+    }
+  },
+  {
+    field: 'phone',
+    label: '联系电话',
+    component: 'NInput',
+    ifShow({ model }) {
+      return model.leaderUserId;
+    }
+  },
+  {
+    field: 'email',
+    label: '邮箱',
+    component: 'NInput',
+    ifShow({ model }) {
+      return model.leaderUserId;
+    }
+  },
+  {
+    field: 'status',
+    label: '部门状态',
+    component: 'NRadioGroup',
+    required: true,
+    componentProps: {
+      options: [
+        {
+          label: '正常',
+          value: 1
+        },
+        {
+          label: '停用',
+          value: 0
+        }
+      ]
+    },
+    defaultValue: 1
+  }
+];
+
+export const searchSchems: FormSchema[] = [
+  {
+    field: 'deptName',
+    label: '部门名称',
+    component: 'NInput'
+  },
+  {
+    field: 'status',
+    label: '部门状态',
+    component: 'NSelect',
+    componentProps: {
+      options: [
+        {
+          label: '启用',
+          value: 1
+        },
+        {
+          label: '禁用',
+          value: 0
+        }
+      ]
+    }
+  }
+];

+ 233 - 3
src/views/manage/department/index.vue

@@ -1,8 +1,238 @@
-<script setup lang="ts"></script>
+<script setup lang="tsx">
+import { nextTick, ref } from 'vue';
+import { NButton, NPopconfirm, NTag } from 'naive-ui';
+import type { InternalRowData } from 'naive-ui/es/data-table/src/interface';
+import { enableStatusRecord } from '@/constants/business';
+import {
+  fetchAddDepartment,
+  fetchDeleteDepartment,
+  fetchEditDepartment,
+  fetchGetDepartmentList
+} from '@/service/api/system-department';
+import { useAppStore } from '@/store/modules/app';
+import { $t } from '@/locales';
+import { useForm } from '@/components/zt/Form/hooks/useForm';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+import { formSchems, searchSchems } from './department.data';
+const appStore = useAppStore();
+const checkedRowKeys = ref([]);
+
+const wrapperRef = ref<HTMLElement | null>(null);
+const deptData = ref<Api.SystemManage.DepartmentModel[]>([]);
+const loading = ref(false);
+
+const [registerForm, { setFieldsValue, validate, getFieldsValue, updateSchema }] = useForm({
+  schemas: formSchems,
+  showSubmitButton: false,
+  showAdvancedButton: false,
+  showResetButton: false,
+  labelWidth: 120,
+  showActionButtonGroup: false,
+  layout: 'horizontal',
+  gridProps: {
+    cols: 1,
+    itemResponsive: true
+  },
+  collapsedRows: 1
+});
+const [registerSearchForm, { getFieldsValue: getSeachForm }] = useForm({
+  labelWidth: 120,
+  layout: 'horizontal',
+  gridProps: {
+    cols: '1 xl:4 s:1 l:3',
+    itemResponsive: true
+  },
+  collapsedRows: 1,
+  schemas: searchSchems
+});
+
+const colums: NaiveUI.TableColumn<InternalRowData>[] = [
+  {
+    key: 'deptId',
+    title: '部门ID',
+    align: 'center'
+  },
+  {
+    key: 'deptName',
+    title: '部门名称',
+    align: 'center',
+    minWidth: 120
+  },
+  {
+    key: 'leaderUserName',
+    title: '部门负责人',
+    align: 'center'
+  },
+  {
+    key: 'phone',
+    title: '负责人电话',
+    align: 'center'
+  },
+  {
+    key: 'seq',
+    title: '排序',
+    align: 'center'
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    width: 60,
+    render: (row: InternalRowData) => {
+      const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
+        1: 'success',
+        0: 'warning'
+      };
+      const status = row.status as Api.Common.EnableStatus;
+      const label = $t(enableStatusRecord[status]);
+      return <NTag type={tagMap[status]}>{label}</NTag>;
+    }
+  },
+  {
+    key: 'createTime',
+    title: '创建时间',
+    align: 'center',
+    width: 160
+  },
+  {
+    key: 'operate',
+    title: $t('common.operate'),
+    align: 'center',
+    width: 130,
+    fixed: 'right',
+    render: (row: any) => (
+      <div class="flex-center justify-end gap-8px">
+        <NButton type="primary" ghost size="small" onClick={() => handleEdit(row)}>
+          {$t('common.edit')}
+        </NButton>
+        {row.deptId != 1 && (
+          <NPopconfirm onPositiveClick={() => handleDelete(row)}>
+            {{
+              default: () => $t('common.confirmDelete'),
+              trigger: () => (
+                <NButton type="error" ghost size="small">
+                  {$t('common.delete')}
+                </NButton>
+              )
+            }}
+          </NPopconfirm>
+        )}
+      </div>
+    )
+  }
+];
+const [registerModal, { openModal, setModalProps, setSubLoading, closeModal }] = useModal({
+  title: '部门',
+  width: 800,
+  isShowHeaderText: false
+});
+// const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, 'id', getData);
+function handleAdd() {
+  setModalProps({
+    title: '新增部门'
+  });
+  openModal();
+}
+
+async function handleBatchDelete() {
+  // request
+  console.log(checkedRowKeys.value);
+}
+
+async function handleDelete(row: Recordable) {
+  // request
+  loading.value = true;
+  await fetchDeleteDepartment(row.deptId);
+  getDepartment({});
+  loading.value = false;
+}
+
+function handleEdit(item: Api.SystemManage.DepartmentModel) {
+  setModalProps({
+    title: '编辑部门'
+  });
+  openModal(item);
+  nextTick(async () => {
+    await setFieldsValue(item);
+    if (item.level == 1 && item.deptId == 1) {
+      updateSchema([{ field: 'parentId', componentProps: { disabled: true } }]);
+    }
+  });
+}
+async function getDepartment(seachForm: Recordable) {
+  console.log(seachForm);
+
+  loading.value = true;
+  const { data } = await fetchGetDepartmentList(seachForm as Recordable);
+  deptData.value = data as Api.SystemManage.DepartmentModel[];
+  loading.value = false;
+}
+
+// init
+getDepartment({});
+
+async function handleSubmit() {
+  setSubLoading(false);
+  await validate();
+  setSubLoading(true);
+  const form = getFieldsValue();
+  const { error } = form.deptId ? await fetchEditDepartment(form) : await fetchAddDepartment(form);
+  if (!error) {
+    closeModal();
+    getDepartment({});
+  }
+}
+</script>
 
 <template>
-  <div>
-    <LookForward></LookForward>
+  <div ref="wrapperRef" class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
+    <NCard :bordered="false" size="small">
+      <NCollapse display-directive="show" :default-expanded-names="['dept-search']">
+        <NCollapseItem title="搜索" name="dept-search">
+          <BasicForm
+            @register-form="registerSearchForm"
+            @submit="getDepartment(getSeachForm())"
+            @reset="getDepartment({})"
+          />
+        </NCollapseItem>
+      </NCollapse>
+    </NCard>
+    <NCard title="部门列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
+      <template #header-extra>
+        <TableHeaderOperation
+          :disabled-delete="checkedRowKeys.length === 0"
+          :loading="loading"
+          is-add
+          @add="handleAdd"
+          @delete="handleBatchDelete"
+          @refresh="getDepartment({})"
+        />
+      </template>
+      <NDataTable
+        v-model:checked-row-keys="checkedRowKeys"
+        :columns="colums"
+        :data="deptData"
+        size="small"
+        :flex-height="!appStore.isMobile"
+        :scroll-x="1088"
+        :loading="loading"
+        :row-key="row => row.deptId"
+        remote
+        class="sm:h-full"
+      />
+      <BasicModal @register-modal="registerModal" @ok="handleSubmit">
+        <BasicForm @register-form="registerForm">
+          <template #parentId="{ model, field }">
+            <NTreeSelect
+              v-model:value="model[field]"
+              :options="deptData"
+              label-field="deptName"
+              key-field="deptId"
+            ></NTreeSelect>
+          </template>
+        </BasicForm>
+      </BasicModal>
+    </NCard>
   </div>
 </template>
 

+ 2 - 1
src/views/manage/menu/modules/shared.ts

@@ -133,7 +133,8 @@ export const formSchems: FormSchema[] = [
     component: 'NInput',
     required: true,
     componentProps: {
-      disabled: true
+      disabled: true,
+      placeholder: '输入路由名称自动生成'
     }
   },
 

+ 155 - 138
src/views/manage/user/index.vue

@@ -10,6 +10,7 @@ import {
   fetchGetRoleAllList,
   fetchGetUserList
 } from '@/service/api';
+import { fetchGetDepartmentList } from '@/service/api/system-department';
 import { useTable } from '@/components/zt/Table/hooks/useTable';
 import { $t } from '@/locales';
 import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
@@ -67,18 +68,26 @@ const columns: NaiveUI.TableColumn<InternalRowData>[] = [
     }
   }
 ];
+
 const [registerTable, { refresh, setTableLoading }] = useTable({
-  schemas: [
-    {
-      field: 'username',
-      label: '用户名',
-      component: 'NInput'
-    }
-  ],
-  inline: false,
-  size: 'small',
-  labelPlacement: 'left',
-  isFull: false
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'username',
+        label: '用户名',
+        component: 'NInput'
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'userId',
+    title: '用户列表',
+    showAddButton: true
+  }
 });
 
 async function handleDelete(row: Recordable) {
@@ -86,133 +95,149 @@ async function handleDelete(row: Recordable) {
   await fetchDeleteUser(row.userId);
   refresh();
 }
-const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue, updateSchema }] = useModalFrom({
-  modalConfig: {
-    title: '用户 ',
-    width: 800
-  },
-  formConfig: {
-    schemas: [
-      {
-        field: 'userId',
-        label: '',
-        component: 'NInput',
-        show: false
-      },
-      {
-        field: 'username',
-        label: '用户名',
-        component: 'NInput',
-        required: true,
-        rules: [
-          {
-            required: true,
-            message: '请输入用户名',
-            trigger: ['blur', 'input'],
-            validator: (rule, value) => {
-              return new Promise<void>((resolve, reject) => {
-                if (value.length < 2) {
-                  console.log(rule);
+const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue, updateSchema, setModalLoading }] =
+  useModalFrom({
+    modalConfig: {
+      title: '用户 ',
+      width: 800,
+      isShowHeaderText: true
+    },
+    formConfig: {
+      schemas: [
+        {
+          field: 'userId',
+          label: '',
+          component: 'NInput',
+          show: false
+        },
+        {
+          field: 'username',
+          label: '用户名',
+          component: 'NInput',
+          required: true,
+          rules: [
+            {
+              required: true,
+              message: '请输入用户名',
+              trigger: ['blur', 'input'],
+              validator: (rule, value) => {
+                return new Promise<void>((resolve, reject) => {
+                  if (value.length < 2) {
+                    console.log(rule);
 
-                  reject(new Error('用户名不能低于2位'));
-                }
-                if (value.length > 20) {
-                  reject(new Error('用户名不能高于20位'));
-                }
-                resolve();
-              });
+                    reject(new Error('用户名不能低于2位'));
+                  }
+                  if (value.length > 20) {
+                    reject(new Error('用户名不能高于20位'));
+                  }
+                  resolve();
+                });
+              }
             }
+          ]
+        },
+        {
+          field: 'deptIds',
+          label: '归属部门',
+          component: 'ApiTreeSelect',
+          required: true,
+          componentProps: {
+            api: fetchGetDepartmentList,
+            labelFeild: 'deptName',
+            valueFeild: 'deptId',
+            immediate: true,
+            resultFeild: 'data',
+            multiple: true
           }
-        ]
-      },
-      {
-        field: 'password',
-        label: '密码',
-        component: 'NInput',
-        required: true,
-        componentProps: {
-          type: 'password',
-          showPasswordOn: 'click'
-        }
-      },
-      {
-        field: 'comfirmPassword',
-        label: '确认密码',
-        component: 'NInput',
-        required: true,
-        componentProps: {
-          type: 'password',
-          showPasswordOn: 'click'
-        }
-      },
-      {
-        field: 'email',
-        label: '邮箱',
-        component: 'NInput',
-        required: true,
-        rules: [
-          {
-            pattern: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
-            type: 'email',
-            message: '请输入正确的邮箱地址',
-            trigger: ['blur', 'input']
+        },
+        {
+          field: 'password',
+          label: '密码',
+          component: 'NInput',
+          required: true,
+          componentProps: {
+            type: 'password',
+            showPasswordOn: 'click'
           }
-        ]
-      },
-      {
-        field: 'mobile',
-        label: '手机号',
-        component: 'NInput',
-        required: true,
-        componentProps: {
-          maxlength: 11
         },
-        rules: [
-          {
-            pattern: /^1[3456789]\d{9}$/,
-            message: '请输入正确的手机号',
-            trigger: ['blur', 'input']
+        {
+          field: 'comfirmPassword',
+          label: '确认密码',
+          component: 'NInput',
+          required: true,
+          componentProps: {
+            type: 'password',
+            showPasswordOn: 'click'
           }
-        ]
-      },
-      {
-        field: 'roleIdList',
-        label: '角色',
-        component: 'ApiSelect',
-        required: true,
-        componentProps: {
-          api: () => fetchGetRoleAllList(),
-          labelFeild: 'roleName',
-          valueFeild: 'roleId',
-          multiple: true
-        }
-      },
-      {
-        field: 'status',
-        label: '状态',
-        component: 'NRadioGroup',
-        required: true,
-        componentProps: {
-          options: [
+        },
+        {
+          field: 'email',
+          label: '邮箱',
+          component: 'NInput',
+          required: true,
+          rules: [
             {
-              label: '启用',
-              value: 1
-            },
+              pattern: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
+              type: 'email',
+              message: '请输入正确的邮箱地址',
+              trigger: ['blur', 'input']
+            }
+          ]
+        },
+        {
+          field: 'mobile',
+          label: '手机号',
+          component: 'NInput',
+          required: true,
+          componentProps: {
+            maxlength: 11
+          },
+          rules: [
             {
-              label: '禁用',
-              value: 0
+              pattern: /^1[3456789]\d{9}$/,
+              message: '请输入正确的手机号',
+              trigger: ['blur', 'input']
             }
           ]
         },
-        defaultValue: 1
+        {
+          field: 'roleIdList',
+          label: '角色',
+          component: 'ApiSelect',
+          required: true,
+          componentProps: {
+            api: () => fetchGetRoleAllList(),
+            labelFeild: 'roleName',
+            valueFeild: 'roleId',
+            multiple: true
+          }
+        },
+        {
+          field: 'status',
+          label: '状态',
+          component: 'NRadioGroup',
+          required: true,
+          componentProps: {
+            options: [
+              {
+                label: '启用',
+                value: 1
+              },
+              {
+                label: '禁用',
+                value: 0
+              }
+            ]
+          },
+          defaultValue: 1
+        }
+      ],
+      labelWidth: 120,
+      gridProps: {
+        cols: '1'
       }
-    ],
-    labelWidth: 120,
-    gridProps: {
-      cols: '1'
     }
-  }
-});
+  });
 async function handleSubmit() {
   const form = await getFieldsValue();
   if (form.userId) {
@@ -225,8 +250,10 @@ async function handleSubmit() {
 }
 
 async function edit(row: Recordable) {
-  const res = await fetchDetaileUser(row.userId);
+  setModalLoading(true);
   openModal(row);
+  const res = await fetchDetaileUser(row.userId);
+  setModalLoading(false);
   setFieldsValue({ ...res.data, userId: row.userId });
   updateSchema([
     { field: 'password', required: false },
@@ -237,19 +264,9 @@ async function edit(row: Recordable) {
 
 <template>
   <LayoutTable>
-    <ZTable
-      :table-config="{
-        api: fetchGetUserList,
-        columns: columns,
-        keyField: 'userId',
-        title: '用户列表',
-        showAddButton: true
-      }"
-      @register="registerTable"
-      @add="openModal"
-    >
+    <ZTable :columns="columns" :api="fetchGetUserList" @register="registerTable" @add="openModal">
       <template #op="{ row }">
-        <NButton v-if="row.userId != 1" size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
         <NPopconfirm v-if="row.userId != 1" @positive-click="handleDelete(row)">
           <template #trigger>
             <NButton size="small" type="error" ghost>删除</NButton>