Преглед изворни кода

feat(系统模块): 添加用户管理页面

shizhongming пре 2 година
родитељ
комит
8e00be6f79

+ 3 - 0
src/components/LayoutSeparate/index.ts

@@ -0,0 +1,3 @@
+import LayoutSeparate from './src/LayoutSeparate';
+
+export { LayoutSeparate };

+ 89 - 0
src/components/LayoutSeparate/src/LayoutSeparate.less

@@ -0,0 +1,89 @@
+@line_high_light: 2px solid rgb(24, 144, 255) !important;
+
+.smart-layout-separate {
+  display: flex;
+
+  .first-container {
+    position: relative;
+  }
+  //
+  .second-container {
+    position: relative;
+  }
+  // 左右布局
+  &.left-right-layout {
+    flex-direction: row;
+    // 分割线样式
+    .drag-line {
+      z-index: 9999;
+      top: 0;
+      right: -5px;
+      bottom: 0;
+      width: 10px;
+      height: 100%;
+
+      .ant-divider-vertical {
+        width: 2px;
+        height: 100%;
+        margin: 0 4px;
+      }
+    }
+
+    .draggable {
+      cursor: col-resize;
+    }
+    // 第二个div样式
+    .second-outer {
+      margin-left: 5px;
+    }
+
+    .first-outer {
+      margin-right: 5px;
+    }
+  }
+  // 上下布局
+  &.top-bottom-layout {
+    flex-direction: column;
+    // 分割线样式
+    .drag-line {
+      z-index: 9999;
+      right: 0;
+      bottom: -5px;
+      left: 0;
+      height: 10px;
+
+      .ant-divider-horizontal {
+        height: 2px;
+        margin: 4px 0;
+      }
+    }
+
+    .draggable {
+      cursor: row-resize;
+    }
+
+    .first-outer {
+      margin-bottom: 5px;
+    }
+
+    .second-outer {
+      margin-top: 5px;
+    }
+  }
+  // 分割线样式
+  .drag-line {
+    position: absolute;
+    z-index: 1;
+
+    &.high-light {
+      //  线高亮
+      .ant-divider-horizontal {
+        border-top: @line_high_light;
+      }
+
+      .ant-divider-vertical {
+        border-left: @line_high_light;
+      }
+    }
+  }
+}

+ 294 - 0
src/components/LayoutSeparate/src/LayoutSeparate.tsx

@@ -0,0 +1,294 @@
+import { defineComponent, computed, toRefs, ref, watch } from 'vue';
+import type { PropType, Ref, StyleValue } from 'vue';
+
+import { Divider } from 'ant-design-vue';
+import { isFinite, endsWith, replace, parseInt, toNumber } from 'lodash-es';
+
+import './LayoutSeparate.less';
+
+enum Layout {
+  'LEFT_RIGHT_LAYOUT' = 'leftRight',
+  'TOP_BOTTOM_LAYOUT' = 'topBottom',
+}
+
+/**
+ * 支持分隔拖拽的layout
+ * @author shizhongming
+ */
+export default defineComponent({
+  name: 'LayoutSeparate',
+  props: {
+    //布局,默认左右布局
+    layout: {
+      type: String as PropType<string>,
+      default: Layout.LEFT_RIGHT_LAYOUT,
+      validator(value: string) {
+        // @ts-ignore
+        return [Layout.LEFT_RIGHT_LAYOUT, Layout.TOP_BOTTOM_LAYOUT].includes(value);
+      },
+    },
+    // 是否可拖拽
+    draggable: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+    // 尺寸,如果是number类型,按照百分比分隔
+    firstSize: {
+      type: [Number, String] as PropType<number | string>,
+      default: 50,
+      validator(value: string | number) {
+        if (!isFinite(value)) {
+          // @ts-ignore
+          return endsWith(value, '%') || endsWith(value, 'px');
+        }
+        return true;
+      },
+    },
+    showLine: {
+      type: Boolean as PropType<boolean>,
+      default: true,
+    },
+    lineStyle: {
+      type: [Object, String] as PropType<StyleValue | string>,
+      default: () => {
+        return {
+          'border-left': '1px solid rgba(0,0,0,0.2)',
+        };
+      },
+    },
+    highLineStyle: {
+      type: [Object, String] as PropType<StyleValue | string>,
+      default: () => {
+        return {
+          'border-left': '2px solid rgb(24, 144, 255)',
+        };
+      },
+    },
+  },
+  setup(props, { slots }) {
+    const { layout, draggable, firstSize, showLine } = toRefs(props);
+    // 是否是左右布局
+    const isLeftRight = computed(() => layout.value === Layout.LEFT_RIGHT_LAYOUT);
+    // 拖拽是否初始化
+    const dividerRef = ref<HTMLElement | null>(null);
+    const dragVue = useDrag(isLeftRight, draggable);
+
+    /**
+     * 外层DIV class 计算属性
+     */
+    const computedContainerClassList = computed(() => {
+      // 外层DIV容器class
+      const containerClassList = ['smart-layout-separate'];
+      if (isLeftRight.value) {
+        containerClassList.push('left-right-layout');
+      } else {
+        containerClassList.push('top-bottom-layout');
+      }
+      return containerClassList;
+    });
+    /**
+     * 分割线class
+     */
+    const computedLineClass = computed(() => {
+      // 分割线样式
+      const dividerClassList: Array<string> = ['drag-line'];
+      // 添加高亮设置
+      if (dragVue.isMouseDown && dragVue.isMouseDown.value === true) {
+        dividerClassList.push('high-light');
+      }
+      if (draggable.value) {
+        dividerClassList.push('draggable');
+      }
+      return dividerClassList;
+    });
+
+    /**
+     * layout样式计算属性
+     */
+    const layoutStyle = computed(() => {
+      const { xLength, yLength } = dragVue;
+      const firstStyle: StyleValue = {};
+      const firstSizeValue = firstSize.value;
+      let firstValue = '';
+      let secondValue = '';
+      const addValue = isLeftRight.value ? xLength.value : yLength.value;
+      if (isFinite(firstSizeValue) || isFinite(toNumber(firstSizeValue))) {
+        // 按照百分比处理
+        firstValue = toNumber(firstSizeValue) + addValue + 'px';
+        secondValue = `calc(100% - ${firstValue})`;
+      } else {
+        // @ts-ignore
+        if (endsWith(firstSizeValue, '%')) {
+          // @ts-ignore
+          const firstSize = parseInt(replace(firstSizeValue, '%'));
+          firstValue = `calc(${firstSize}% ${addValue > 0 ? '+' : '-'} ${Math.abs(addValue)}px)`;
+          secondValue = `calc(${100 - firstSize}% ${addValue < 0 ? '+' : '-'} ${Math.abs(
+            addValue,
+          )}px)`;
+          // @ts-ignore
+        } else if (endsWith(firstSizeValue, 'px')) {
+          // @ts-ignore
+          const firstSize = parseInt(replace(firstSizeValue, 'px'));
+          firstValue = firstSize + addValue + 'px';
+          secondValue = `calc(100% - ${firstValue})`;
+        }
+      }
+      const secondStyle: any = {};
+      if (isLeftRight.value) {
+        firstStyle.width = firstValue;
+        secondStyle.width = secondValue;
+      } else {
+        firstStyle.height = firstValue;
+        secondStyle.height = secondValue;
+      }
+      return {
+        firstStyle,
+        secondStyle,
+      };
+    });
+
+    const computedFirstContainerClass = computed(() => {
+      const classList = ['full-height'];
+      if (showLine.value) {
+        classList.push('first-outer');
+      }
+      return classList;
+    });
+
+    const computedSecondContainerClass = computed(() => {
+      const classList: string[] = [];
+      if (showLine.value) {
+        classList.push('second-outer');
+      }
+      return classList;
+    });
+
+    return () => {
+      const { onLineMouseDown, dragLineStyle } = dragVue;
+      const { first, second } = slots;
+      return (
+        <div class={computedContainerClassList.value}>
+          {/* 第一块区域,左或者上 */}
+          <div class="first-container" style={layoutStyle.value.firstStyle}>
+            <div class={computedFirstContainerClass.value}>{first ? first() : ''}</div>
+            {/* 分割线 */}
+            {props.showLine ? (
+              <div
+                ref={dividerRef}
+                style={dragLineStyle && dragLineStyle.value}
+                onMousedown={(e) => draggable.value && onLineMouseDown && onLineMouseDown(e)}
+                class={computedLineClass.value}
+              >
+                <Divider
+                  style={props.lineStyle}
+                  type={isLeftRight.value ? 'vertical' : 'horizontal'}
+                />
+              </div>
+            ) : (
+              ''
+            )}
+          </div>
+          {/* 第二块区域,右或者下 */}
+          <div class={computedSecondContainerClass.value} style={layoutStyle.value.secondStyle}>
+            {second ? second() : ''}
+          </div>
+        </div>
+      );
+    };
+  },
+});
+
+/**
+ * 支持拖拽
+ * @param isLeftRight 是否是左右布局
+ * @param draggable
+ */
+const useDrag = (isLeftRight: Ref<boolean>, draggable: Ref<boolean>) => {
+  // 鼠标是否按下
+  const isMouseDown = ref(false);
+  // 鼠标拖动时 lined XY坐标
+  const lineDownX = ref(0);
+  const lineDownY = ref(0);
+  // 初始化状态的 x y位置
+  let initX = -1;
+  let initY = -1;
+  let lineDefaultX = -1;
+  let lineDefaultY = -1;
+  // 分割线样式
+  const dragLineStyle = ref<StyleValue>();
+  // 左右方向移动距离
+  const xLength = ref(0);
+  // Y方向移动距离
+  const yLength = ref(0);
+
+  /**
+   * 重置函数
+   */
+  const reset = () => {
+    initX = -1;
+    initY = -1;
+    lineDefaultX = -1;
+    lineDefaultY = -1;
+    lineDownX.value = 0;
+    lineDownY.value = 0;
+    xLength.value = 0;
+    yLength.value = 0;
+  };
+
+  watch([isLeftRight, draggable], reset);
+
+  /**
+   * 监控 x y 变化改变line样式
+   */
+  watch([lineDownX, lineDownY], () => {
+    if (lineDefaultX === -1 || lineDefaultY === -1) {
+      return;
+    }
+    if (isLeftRight.value) {
+      dragLineStyle.value = {
+        right: lineDefaultX - lineDownX.value - 5 + 'px',
+      };
+    } else {
+      dragLineStyle.value = {
+        bottom: lineDefaultY - lineDownY.value - 5 + 'px',
+      };
+    }
+  });
+  /**
+   * 分割线鼠标点击事件
+   */
+  const onLineMouseDown = (downE: MouseEvent) => {
+    if (initX === -1) {
+      initX = downE.clientX;
+    }
+    if (initY === -1) {
+      initY = downE.clientY;
+    }
+
+    lineDefaultX = downE.clientX;
+    lineDefaultY = downE.clientY;
+
+    isMouseDown.value = true;
+    document.onmousemove = (e) => {
+      lineDownX.value = e.clientX;
+      lineDownY.value = e.clientY;
+    };
+    document.onmouseup = (e) => {
+      xLength.value = e.clientX - initX;
+      yLength.value = e.clientY - initY;
+
+      dragLineStyle.value = {};
+      isMouseDown.value = false;
+      // 移除响应事件
+      document.onmousemove = null;
+      document.onmouseup = null;
+    };
+  };
+  return {
+    onLineMouseDown,
+    isMouseDown,
+    dragLineStyle,
+    xLength,
+    yLength,
+  };
+};

+ 1 - 0
src/components/SmartTable/index.ts

@@ -15,3 +15,4 @@ export * from './src/types/SmartTableButton';
 export * from './src/types/SmartTableColumnType';
 export * from './src/types/SmartTableAuthType';
 export * from './src/types/SmartTableToolbarConfigType';
+export * from './src/utils/TableCommon';

+ 2 - 1
src/components/SmartTable/src/hooks/useTableToolbarConfig.ts

@@ -348,7 +348,8 @@ const getDefaultUseYnButtonConfig = (t: Function, useYn: boolean): SmartTableBut
     name: useYn ? t('common.button.use') : t('common.button.noUse'),
     code: useYn ? 'useYnTrue' : 'useYnFalse',
     props: {
-      color: useYn ? '' : 'error',
+      type: 'primary',
+      danger: !useYn,
       preIcon: useYn ? 'ant-design:check-outlined' : 'ant-design:close-outlined',
     },
     buttonRender: {

+ 114 - 0
src/components/SmartTable/src/utils/TableCommon.tsx

@@ -0,0 +1,114 @@
+import { Tag } from 'ant-design-vue';
+import { useI18n } from '@/hooks/web/useI18n';
+import { SmartColumn } from '@/components/SmartTable';
+
+/**
+ * Boolean列配置
+ * @param t
+ * @param title
+ * @param field
+ */
+export const tableBooleanColumn = (t: Function, title: string, field: string) => {
+  const createSlot = ({ row }: any) => {
+    const value = row[field];
+    if (value === true) {
+      return <Tag color="#108ee9">{t('common.form.yes')}</Tag>;
+    }
+    return <Tag color="#f50">{t('common.form.no')}</Tag>;
+  };
+  /**
+   * 创建列信息
+   */
+  const createColumn = () => {
+    return {
+      title: title,
+      field: field,
+      width: 100,
+      slots: {
+        default: createSlot,
+      },
+    };
+  };
+  return {
+    createColumn,
+  };
+};
+
+const tableDeleteYn = (t: Function) => {
+  const createSlot = ({ row }: any) => {
+    const value = row.deleteYn;
+    if (value === false) {
+      return <Tag color="#108ee9">{t('common.form.no')}</Tag>;
+    }
+    return <Tag color="#f50">{t('common.form.yes')}</Tag>;
+  };
+  /**
+   * 创建列信息
+   */
+  const createColumn = () => {
+    return {
+      title: '{common.table.deleteYn}',
+      field: 'deleteYn',
+      width: 100,
+      slots: {
+        default: createSlot,
+      },
+    };
+  };
+  return {
+    createColumn,
+  };
+};
+
+const tableUseYn = (t: Function) => {
+  const createSlot = ({ row }: any) => {
+    const useYn = row.useYn;
+    if (useYn === true) {
+      return <Tag color="#108ee9">{t('common.form.use')}</Tag>;
+    }
+    return <Tag color="#f50">{t('common.form.noUse')}</Tag>;
+  };
+  /**
+   * 创建列信息
+   */
+  const createColumn = () => {
+    return {
+      title: '{common.table.useYn}',
+      field: 'useYn',
+      width: 100,
+      slots: {
+        default: createSlot,
+      },
+    };
+  };
+
+  return {
+    createColumn,
+  };
+};
+
+const tableUseYnClass = (): SmartColumn => {
+  const { t } = useI18n();
+
+  return {
+    title: '{common.table.useYn}',
+    field: 'useYn',
+    width: 100,
+    formatter: ({ row }) => {
+      const useYn = row.useYn as boolean | null;
+      if (useYn === null) {
+        return '';
+      }
+      return useYn ? t('common.form.use') : t('common.form.noUse');
+    },
+    dynamicClass: ({ row }) => {
+      const useYn = row.useYn as boolean | null;
+      if (useYn === null) {
+        return '';
+      }
+      return useYn ? 'text-color--success-bold' : 'text-color--danger-bold';
+    },
+  };
+};
+
+export { tableUseYn, tableDeleteYn, tableUseYnClass };

+ 29 - 1
src/hooks/web/useMessage.tsx

@@ -4,6 +4,8 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des
 import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification';
 import { useI18n } from './useI18n';
 import { isString } from '@/utils/is';
+import { Result } from '#/axios';
+import { useSystemExceptionStore } from '@/store/modules/exception';
 
 export interface NotifyApi {
   info(config: NotificationArgsProps): void;
@@ -111,6 +113,7 @@ export function useMessage() {
     createWarningModal,
     successMessage,
     warnMessage,
+    errorMessage,
   };
 }
 
@@ -122,7 +125,7 @@ export interface MessageOptions {
  * 成功
  * @param options
  */
-const successMessage = (options: MessageOptions) => {
+const successMessage = (options: MessageOptions | string) => {
   if (isString(options)) {
     return Message.warning(options);
   }
@@ -135,3 +138,28 @@ const warnMessage = (options: MessageOptions | string) => {
   }
   return Message.warning(options.message);
 };
+
+/**
+ * 500错误弹窗
+ * @param e
+ */
+const createError500Modal = (e: Result) => {
+  const { exceptionNo } = e;
+  const systemExceptionStore = useSystemExceptionStore();
+  systemExceptionStore.handleShowExceptionModal(exceptionNo!);
+};
+
+const errorMessage = (e: Result | string | Error) => {
+  if (isString(e)) {
+    return Message.error(e);
+  }
+  console.error(e);
+  const code = (e as any).code;
+  switch (code) {
+    case 500: {
+      return createError500Modal(e as Result);
+    }
+    default:
+      return Message.error(e.message);
+  }
+};

+ 236 - 0
src/modules/system/components/SysDept/SysDeptTree.vue

@@ -0,0 +1,236 @@
+<template>
+  <div>
+    <div v-if="showSearch" class="search-container">
+      <a-input-search
+        v-model:value="searchValue"
+        :placeholder="$t('system.views.dept.search.deptName')"
+      />
+    </div>
+    <Spin :spinning="loading">
+      <Tree
+        v-bind="getAttrs"
+        :expanded-keys="expandedKeys"
+        :auto-expand-parent="autoExpandParent"
+        @expand="onExpand"
+        :field-names="fieldNames"
+        :tree-data="computedTreeData"
+      >
+        <template #title="{ deptName }">
+          <span v-if="!showSearch">
+            {{ deptName }}
+          </span>
+          <span v-else-if="deptName.indexOf(searchValue) > -1">
+            {{ deptName.substr(0, deptName.indexOf(searchValue)) }}
+            <span style="color: #f50">{{ searchValue }}</span>
+            {{ deptName.substr(deptName.indexOf(searchValue) + searchValue.length) }}
+          </span>
+          <span v-else>{{ deptName }}</span>
+        </template>
+      </Tree>
+    </Spin>
+  </div>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, onMounted, reactive, ref, toRefs, unref, watch } from 'vue';
+
+  import { errorMessage } from '@/utils/message/SystemNotice';
+  import TreeUtils from '@/utils/TreeUtils';
+  import { propTypes } from '@/utils/propTypes';
+  import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+  import { Spin, Tree } from 'ant-design-vue';
+
+  const getParentKey = (key: number, treeData: Array<any>): number => {
+    let parentKey;
+    for (let i = 0; i < treeData.length; i++) {
+      const node = treeData[i];
+      if (node.children) {
+        if (node.children.some((item: any) => item.deptId === key)) {
+          parentKey = node.deptId;
+        } else {
+          const secondParentKey = getParentKey(key, node.children);
+          if (secondParentKey) {
+            parentKey = secondParentKey;
+          }
+        }
+      }
+    }
+    return parentKey;
+  };
+
+  export default defineComponent({
+    name: 'SysDeptTree',
+    components: {
+      Spin,
+      Tree,
+    },
+    props: {
+      // 是否支持搜索
+      showSearch: propTypes.bool.def(true),
+      // 是否异步加载
+      async: propTypes.bool,
+    },
+    setup(props, { attrs }) {
+      const { async: asyncRef } = toRefs(props);
+      const searchValue = ref<string>('');
+
+      const dataList = ref<Array<any>>([]);
+      const autoExpandParent = ref(false);
+      const expandedKeys = ref<Array<number>>([]);
+      const loading = ref(false);
+
+      const getAttrs = computed(() => {
+        const result: any = {
+          ...attrs,
+        };
+        if (unref(asyncRef)) {
+          result.loadData = handleAsyncLoadData;
+        }
+        return result;
+      });
+
+      /**
+       * 树形数据计算属性
+       */
+      const computedTreeData = computed(() => {
+        const async = unref(asyncRef);
+        if (async) {
+          return unref(dataList);
+        }
+        return (
+          TreeUtils.convertList2Tree(
+            dataList.value,
+            (item) => item.deptId,
+            (item) => item.parentId,
+            0,
+          ) || []
+        );
+      });
+
+      const onExpand = (keys: Array<number>) => {
+        expandedKeys.value = keys;
+        autoExpandParent.value = false;
+      };
+
+      /**
+       * 所有数据
+       */
+      const getAllDataList = computed(() => {
+        const result: any[] = [];
+        if (unref(asyncRef)) {
+          recursionAddChildren(unref(dataList), result);
+        } else {
+          result.push(...unref(dataList));
+        }
+        return result;
+      });
+
+      const recursionAddChildren = (list: any[], allData: any[]) => {
+        list.forEach((item) => {
+          allData.push(item);
+          if (item.children && item.children.length > 0) {
+            recursionAddChildren(item.children, allData);
+          }
+        });
+      };
+
+      watch(searchValue, (value) => {
+        const allData = unref(getAllDataList);
+        expandedKeys.value = allData
+          .map(({ deptName, deptId }: any) => {
+            if (deptName.indexOf(value) > -1) {
+              return getParentKey(deptId, computedTreeData.value);
+            }
+            return null;
+          })
+          .filter((item, i, self) => item && self.indexOf(item) === i) as Array<number>;
+        autoExpandParent.value = true;
+      });
+
+      const handleAsyncLoadData = async (treeNode) => {
+        const dataRef = treeNode.dataRef;
+        dataRef.children = await loadData(dataRef.deptId);
+        dataList.value = [...unref(dataList)];
+      };
+
+      const reload = () => loadData();
+
+      /**
+       * 加载数据函数
+       */
+      const loadData = async (parentId?: number | null) => {
+        const parameter: Recordable = {
+          sortName: 'seq',
+          sortOrder: 'asc',
+        };
+        if (parentId !== undefined && parentId !== null) {
+          parameter.parameter = {
+            'parentId@=': parentId,
+          };
+        }
+        try {
+          loading.value = true;
+          const result = (await defHttp.post({
+            service: ApiServiceEnum.SMART_SYSTEM,
+            url: 'sys/dept/list',
+            data: parameter,
+          })) as any[];
+
+          result.forEach((item) => {
+            if (item.hasChild !== true) {
+              item.isLeaf = true;
+            }
+          });
+          if (unref(asyncRef)) {
+            if (parentId === 0) {
+              dataList.value = result;
+            } else {
+              return result;
+            }
+          } else {
+            dataList.value = result;
+          }
+        } catch (e) {
+          errorMessage(e);
+        } finally {
+          loading.value = false;
+        }
+      };
+
+      /**
+       * 加载数据
+       */
+      onMounted(() => {
+        let parentId: number | undefined = undefined;
+        if (unref(asyncRef)) {
+          parentId = 0;
+        }
+        loadData(parentId);
+      });
+
+      return {
+        computedTreeData,
+        autoExpandParent,
+        onExpand,
+        loadData,
+        loading,
+        expandedKeys,
+        fieldNames: reactive({
+          children: 'children',
+          title: 'deptName',
+          key: 'deptId',
+        }),
+        getAttrs,
+        handleAsyncLoadData,
+        searchValue,
+        reload,
+      };
+    },
+  });
+</script>
+
+<style scoped lang="less">
+  .search-container {
+    margin-bottom: 10px;
+  }
+</style>

+ 120 - 0
src/modules/system/hooks/SysDictHooks.ts

@@ -0,0 +1,120 @@
+import { computed, onMounted, ref, unref, watch } from 'vue';
+import type { Ref } from 'vue';
+
+import { errorMessage } from '@/utils/message/SystemNotice';
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+export interface SysDictModel {
+  dictItemCode: string;
+  dictItemName: string;
+}
+
+/**
+ * 加载字典项hook
+ * @param dictCodeRef
+ * @param immediate
+ */
+export const useLoadDictItem = (dictCodeRef: Ref<string> | string, immediate = true) => {
+  const dictData = ref<Array<SysDictModel>>([]);
+
+  const getDictItemMap = computed(() => {
+    const result: { [index: string]: string } = {};
+    unref(dictData).forEach((item) => {
+      result[item.dictItemCode] = item.dictItemName;
+    });
+    return result;
+  });
+
+  /**
+   * 加载数据函数
+   */
+  const loadDictData = async () => {
+    const dictCode = unref(dictCodeRef);
+    if (!dictCode || dictCode === '') {
+      dictData.value = [];
+    } else {
+      try {
+        dictData.value = await defHttp.post({
+          service: ApiServiceEnum.SMART_SYSTEM,
+          url: 'sys/dict/listItemByCode',
+          data: dictCode,
+        });
+      } catch (e) {
+        errorMessage(e);
+      }
+    }
+  };
+
+  watch(
+    () => unref(dictCodeRef),
+    () => {
+      loadDictData();
+    },
+  );
+
+  onMounted(() => {
+    if (immediate) {
+      loadDictData();
+    }
+  });
+
+  return {
+    dictData,
+    loadDictData,
+    getDictItemMap: getDictItemMap,
+  };
+};
+
+/**
+ * 批量加载字典数据
+ * @param dictCodeList
+ * @param immediate
+ */
+export const useBatchLoadDictItem = (dictCodeList: string[], immediate = true) => {
+  const dictMapRef = ref<Recordable<any[]>>({});
+
+  const computedDictMap = computed(() => {
+    const result: Recordable<Recordable> = {};
+
+    Object.keys(unref(dictMapRef)).forEach((key) => {
+      const list = unref(dictMapRef)[key];
+      const itemMap: Recordable = {};
+      list.forEach((item) => {
+        itemMap[item.dictItemCode] = item.dictItemName;
+      });
+      result[key] = itemMap;
+    });
+
+    return result;
+  });
+
+  const loadDictData = async () => {
+    const result =
+      (await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: 'sys/dict/batchListItemByCode',
+        data: dictCodeList,
+      })) || {};
+    const dictMap: Recordable<any[]> = {};
+    for (const key in result) {
+      dictMap[key] = result[key].map((item) => {
+        return {
+          ...item,
+          label: `${item.dictItemCode}-${item.dictItemName}`,
+          value: item.dictItemCode,
+        };
+      });
+    }
+    dictMapRef.value = dictMap;
+  };
+
+  if (immediate) {
+    loadDictData();
+  }
+
+  return {
+    computedDictMap,
+    loadDictData,
+    dictMapRef,
+  };
+};

+ 138 - 0
src/modules/system/views/user/UserListView.api.ts

@@ -0,0 +1,138 @@
+import type { VxeGridPropTypes } from 'vxe-table';
+
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+import TreeUtils from '@/utils/TreeUtils';
+
+enum Api {
+  list = 'sys/user/list',
+  delete = 'sys/user/batchDeleteById',
+  saveUpdateWithDataScope = 'sys/user/saveUpdateWithDataScope',
+  getByIdWithDataScope = 'sys/user/getByIdWithDataScope',
+  getById = 'sys/user/getById',
+  setUseYn = 'sys/user/setUseYn',
+  createAccount = 'sys/user/createAccount',
+  saveAccountSetting = 'sys/user/saveAccountSetting',
+  deptTreeList = 'sys/dept/list',
+  unlockUserAccount = 'sys/user/unlockUserAccount',
+  resetPassword = 'sys/user/resetPassword',
+}
+
+export const listApi = (ajaxParameter) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.list,
+    data: ajaxParameter,
+  });
+};
+
+export const deleteApi = (params: VxeGridPropTypes.ProxyAjaxDeleteParams) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.delete,
+    data: params.body.removeRecords.map((item) => item.userId),
+  });
+};
+
+export const saveUpdateWithDataScopeApi = async ({
+  body,
+}: VxeGridPropTypes.ProxyAjaxSaveParams) => {
+  const saveList = [...body.insertRecords, ...body.updateRecords];
+  if (saveList.length === 0) {
+    return false;
+  }
+  const model = saveList[0];
+  if (model.userType === 'SYSTEM_USER') {
+    model.deptId = null;
+  }
+  return await defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.saveUpdateWithDataScope,
+    data: saveList[0],
+  });
+};
+
+export const getByIdWithDataScopeApi = async (params) => {
+  const result = await defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getByIdWithDataScope,
+    data: params.userId,
+  });
+  return {
+    ...result,
+    dataScopeList: result.dataScopeList || [],
+  };
+};
+
+export const setUseYnApi = (userList: any[], useYn: boolean) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.setUseYn,
+    data: {
+      idList: userList.map((item) => item.userId),
+      useYn,
+    },
+  });
+};
+
+export const createAccountApi = (userList: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.createAccount,
+    data: userList.map((item) => item.userId),
+  });
+};
+
+export const saveAccountSettingApi = (data) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.saveAccountSetting,
+    data: data,
+  });
+};
+
+export const getByIdApi = (id: string | null) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getById,
+    data: id,
+  });
+};
+
+export const getDeptTreeListApi = async () => {
+  const data = await defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.deptTreeList,
+    data: {
+      sortName: 'seq',
+      propertyList: ['deptId', 'deptName', 'parentId'],
+    },
+  });
+  return (
+    TreeUtils.convertList2Tree(
+      data,
+      (item) => item.deptId,
+      (item) => item.parentId,
+      0,
+    ) || []
+  );
+};
+
+export const unlockUserAccountApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.unlockUserAccount,
+    data: {
+      id,
+    },
+  });
+};
+
+export const resetPassword = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.resetPassword,
+    data: {
+      id,
+    },
+  });
+};

+ 297 - 0
src/modules/system/views/user/UserListView.config.ts

@@ -0,0 +1,297 @@
+import type { Ref } from 'vue';
+import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
+import type { FormSchema } from '@/components/Form';
+
+import { unref } from 'vue';
+import { tableUseYnClass } from '@/components/SmartTable';
+import { DATA_SCOPE, SYS_USER_TYPE } from '@/modules/system/constants/SystemConstants';
+
+import { getDeptTreeListApi } from './UserListView.api';
+
+export const getTableColumns = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      title: '{system.views.user.table.username}',
+      field: 'username',
+      width: 120,
+      fixed: 'left',
+    },
+    {
+      title: '{system.views.user.table.fullName}',
+      field: 'fullName',
+      width: 120,
+      fixed: 'left',
+    },
+    {
+      title: '{system.views.user.table.userType}',
+      field: 'userType',
+      width: 120,
+      slots: {
+        default: 'table-userType',
+      },
+    },
+    {
+      title: '{system.views.user.table.accountStatus}',
+      field: 'account',
+      width: 100,
+      slots: {
+        default: 'table-accountStatus',
+      },
+    },
+    {
+      title: '{system.views.user.table.email}',
+      field: 'email',
+      minWidth: 160,
+    },
+    {
+      title: '{system.views.user.table.mobile}',
+      field: 'mobile',
+      minWidth: 140,
+    },
+    {
+      ...tableUseYnClass(),
+      sortable: true,
+    },
+    // {
+    //   ...tableDeleteYn(t).createColumn(),
+    //   sortable: true,
+    // },
+    {
+      title: '{common.table.seq}',
+      field: 'seq',
+      width: 100,
+      sortable: true,
+    },
+    {
+      title: '{common.table.createTime}',
+      field: 'createTime',
+      width: 165,
+      sortable: true,
+    },
+    {
+      title: '{common.table.createUser}',
+      field: 'createUserId',
+      width: 120,
+      formatter: ({ row }: any) => {
+        if (row.createUser) {
+          return row.createUser.fullName;
+        }
+        return '';
+      },
+    },
+    {
+      title: '{common.table.updateTime}',
+      field: 'updateTime',
+      width: 165,
+      sortable: true,
+    },
+    {
+      title: '{common.table.updateUser}',
+      field: 'updateUserId',
+      width: 120,
+      formatter: ({ row }: any) => {
+        if (row.updateUser) {
+          return row.updateUser.fullName;
+        }
+        return '';
+      },
+    },
+    {
+      title: '{common.table.operation}',
+      field: 'operation',
+      width: 180,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+export const getAddEditFormSchemas = (t: Function, userTypeRef: Ref<any[]>): FormSchema[] => {
+  return [
+    {
+      label: '',
+      field: 'userId',
+      component: 'Input',
+      show: false,
+    },
+    {
+      label: t('system.views.user.table.username'),
+      field: 'username',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('system.views.user.table.fullName'),
+      field: 'fullName',
+      component: 'Input',
+      required: true,
+    },
+    {
+      label: t('system.views.user.table.email'),
+      field: 'email',
+      component: 'Input',
+    },
+    {
+      label: t('system.views.user.table.userType'),
+      field: 'userType',
+      component: 'Select',
+      required: true,
+      componentProps: () => {
+        return {
+          options: unref(userTypeRef).map((item) => {
+            return {
+              label: item.dictItemName,
+              value: item.dictItemCode,
+            };
+          }),
+        };
+      },
+    },
+    {
+      label: t('system.views.user.table.mobile'),
+      field: 'mobile',
+      component: 'Input',
+    },
+    {
+      label: t('common.table.seq'),
+      field: 'seq',
+      component: 'Input',
+      required: true,
+      defaultValue: 1,
+      componentProps: {
+        style: {
+          width: '100%',
+        },
+      },
+    },
+    {
+      label: t('system.views.user.form.dept'),
+      field: 'deptId',
+      component: 'ApiTreeSelect',
+      dynamicDisabled: ({ model }) => {
+        return model.userType === SYS_USER_TYPE;
+      },
+      componentProps: {
+        showSearch: true,
+        api: getDeptTreeListApi,
+        allowClear: true,
+        fieldNames: {
+          children: 'children',
+          label: 'deptName',
+          value: 'deptId',
+        },
+        placeholder: t('system.views.user.validate.selectDept'),
+      },
+    },
+    {
+      label: t('system.views.user.form.dataScope'),
+      field: 'dataScopeList',
+      component: 'Select',
+      dynamicDisabled: ({ model }) => {
+        return model.userType === SYS_USER_TYPE;
+      },
+      dynamicRules: ({ model }) => {
+        const { userType, deptId } = model;
+        const required = userType !== SYS_USER_TYPE && deptId !== undefined && deptId !== null;
+        return [
+          {
+            required,
+            message: t('system.views.user.validate.selectDataScope'),
+          },
+        ];
+      },
+      defaultValue: [],
+      componentProps: {
+        mode: 'multiple',
+        options: DATA_SCOPE.map((item) => {
+          return {
+            label: t(item.value),
+            value: item.key,
+          };
+        }),
+      },
+    },
+  ];
+};
+
+export const getSearchSchemas = (t: Function): SmartSearchFormSchema[] => {
+  return [
+    {
+      label: t('system.views.user.table.username'),
+      field: 'username',
+      component: 'Input',
+      colProps: {
+        // span: 3,
+      },
+      searchSymbol: 'like',
+      componentProps: {
+        placeholder: t('system.views.user.table.username'),
+      },
+    },
+    {
+      label: t('system.views.user.table.fullName'),
+      field: 'fullName',
+      component: 'Input',
+      colProps: {
+        // span: 3,
+      },
+      searchSymbol: 'like',
+      componentProps: {
+        placeholder: t('system.views.user.table.fullName'),
+      },
+    },
+    {
+      label: t('system.views.user.table.email'),
+      field: 'email',
+      component: 'Input',
+      searchSymbol: 'like',
+      componentProps: {
+        placeholder: t('system.views.user.table.email'),
+      },
+    },
+    {
+      label: t('common.table.useYn'),
+      field: 'useYn',
+      component: 'Select',
+      defaultValue: 1,
+      searchSymbol: '=',
+      componentProps: {
+        style: {
+          width: '100px',
+        },
+        options: [
+          {
+            label: 'Y',
+            value: 1,
+          },
+          {
+            label: 'N',
+            value: 0,
+          },
+        ],
+      },
+    },
+    {
+      label: t('system.views.user.table.userType'),
+      field: 'userType',
+      componentProps: {
+        style: {
+          width: '100px',
+        },
+      },
+      searchSymbol: '=',
+      slot: 'search-userType',
+      colProps: {
+        // span: 5,
+      },
+    },
+  ];
+};

+ 444 - 0
src/modules/system/views/user/UserListView.vue

@@ -0,0 +1,444 @@
+<template>
+  <div class="full-height page-container">
+    <LayoutSeparate :show-line="false" first-size="280px" class="full-height">
+      <template #first>
+        <div class="full-height dept-container">
+          <SysDeptTree async show-search @select="handleDeptSelected" />
+        </div>
+      </template>
+      <template #second>
+        <SmartTable @register="registerTable" :size="getTableSize">
+          <template #table-operation="{ row }">
+            <SmartVxeTableAction :actions="getTableActions(row)" />
+          </template>
+          <template #table-userType="{ row }">
+            <span>
+              {{ getUserTypeMap[row.userType] }}
+            </span>
+          </template>
+          <template #search-userType="{ model, size }">
+            <a-select style="width: 100px" :size="size" v-model:value="model.userType" allowClear>
+              <a-select-option
+                v-for="item in userTypeListRef"
+                :key="'userType_' + item.dictItemCode"
+                :value="item.dictItemCode"
+              >
+                {{ item.dictItemName }}
+              </a-select-option>
+            </a-select>
+          </template>
+          <template #table-accountStatus="{ row }">
+            <a-tooltip :title="getLockedMessage(row.userAccount?.accountStatus)">
+              <span
+                :style="{
+                  color: getAccountData(row.userAccount?.accountStatus).color,
+                  fontWeight: 'bold',
+                }"
+              >
+                {{ getAccountData(row.userAccount?.accountStatus).label }}
+              </span>
+            </a-tooltip>
+          </template>
+        </SmartTable>
+      </template>
+    </LayoutSeparate>
+    <UserAccountUpdateModal @register="registerAccountModal" />
+    <UserSetRole @register="registerSetRoleModal" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { computed, ref, unref } from 'vue';
+  import { useI18n } from '@/hooks/web/useI18n';
+
+  import { useLoadDictItem } from '@/modules/system/hooks/SysDictHooks';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { hasPermission } from '@/utils/auth';
+  import { useModal } from '@/components/Modal';
+
+  import { LayoutSeparate } from '@/components/LayoutSeparate';
+  import SysDeptTree from '@/modules/system/components/SysDept/SysDeptTree.vue';
+  import { useMessage } from '@/hooks/web/useMessage';
+
+  import {
+    SmartTable,
+    useSmartTable,
+    SmartVxeTableAction,
+    ActionItem,
+  } from '@/components/SmartTable';
+  import UserAccountUpdateModal from './account/UserAccountUpdateModal.vue';
+  import UserSetRole from './components/UserSetRole.vue';
+
+  import { getAddEditFormSchemas, getSearchSchemas, getTableColumns } from './UserListView.config';
+  import {
+    listApi,
+    deleteApi,
+    saveUpdateWithDataScopeApi,
+    getByIdWithDataScopeApi,
+    setUseYnApi,
+    createAccountApi,
+    unlockUserAccountApi,
+    resetPassword,
+  } from './UserListView.api';
+  import { SYS_USER_TYPE, SystemPermissions } from '@/modules/system/constants/SystemConstants';
+
+  const { t } = useI18n();
+  const { warnMessage, errorMessage, createConfirm, successMessage } = useMessage();
+  const { getTableSize } = useSizeSetting();
+
+  const { dictData: userTypeListRef } = useLoadDictItem(ref('SYSTEM_USER_TYPE'));
+  const getUserTypeMap = computed(() => {
+    const result: { [index: string]: string } = {};
+    result[SYS_USER_TYPE] = '系统用户';
+    for (let userType of unref(userTypeListRef)) {
+      result[userType.dictItemCode] = userType.dictItemName;
+    }
+    return result;
+  });
+
+  const accountLockedMessage = {
+    LOGIN_FAIL_LOCKED: '多次登录失败锁定',
+    LONG_TIME_LOCKED: '超出指定时间未登录锁定',
+    LONG_TIME_PASSWORD_MODIFY_LOCKED: '超出指定时间未修改密码锁定',
+  };
+
+  const [registerSetRoleModal, { openModal: openSetRoleModal }] = useModal();
+
+  const getLockedMessage = (status: string | null | undefined) => {
+    if (!status || status === 'NORMAL') {
+      return '正常';
+    }
+    return accountLockedMessage[status];
+  };
+
+  /**
+   * 账户状态
+   */
+  const accountStatusMap = {
+    empty: {
+      label: '未创建',
+      color: '#A9A9A9',
+    },
+    NORMAL: {
+      label: '正常',
+      color: '#228B22',
+    },
+    LOCKED: {
+      label: '锁定',
+      color: 'red',
+    },
+  };
+
+  const getAccountData = (status: string | null | undefined) => {
+    if (status === undefined || status === null) {
+      return accountStatusMap.empty;
+    }
+    if (status === 'NORMAL') {
+      return accountStatusMap.NORMAL;
+    }
+    return accountStatusMap.LOCKED;
+  };
+
+  /**
+   * 权限处理
+   */
+  const permissions = SystemPermissions.user;
+  const hasPermissionUpdateSystemUser = hasPermission('sys:systemUser:update');
+  const hasSystemUserUpdate = (type: string) => {
+    return hasPermissionUpdateSystemUser || type !== SYS_USER_TYPE;
+  };
+
+  /**
+   * 选中组织架构操作
+   * @param selectedKeys
+   */
+  const currentDeptId = ref<number | null>(null);
+  const handleDeptSelected = (selectedKeys: Array<number>) => {
+    if (selectedKeys.length > 0) {
+      currentDeptId.value = selectedKeys[0];
+    } else {
+      currentDeptId.value = null;
+    }
+    // 重新加载数据
+    query();
+  };
+
+  /**
+   * 账户弹窗
+   */
+  const [registerAccountModal, { openModal }] = useModal();
+
+  /**
+   * table行按钮
+   */
+  const getTableActions = (row): ActionItem[] => {
+    return [
+      {
+        label: t('common.button.edit'),
+        onClick: () => editByRowModal(row),
+        disabled: !hasPermission(permissions.update) || !hasSystemUserUpdate(row.userType),
+      },
+      {
+        label: t('system.views.user.button.showAccount'),
+        disabled: !hasPermission('sys:account:query') || !hasSystemUserUpdate(row.userType),
+        onClick: () => openModal(true, row),
+      },
+      {
+        label: t('system.views.user.button.unlockUserAccount'),
+        auth: permissions.unlockUserAccount,
+        disabled:
+          !hasPermission(permissions.unlockUserAccount) ||
+          (row.userAccount && row.userAccount.accountStatus === 'NORMAL'),
+        onClick: () => handleUnlockUserAccount(row.userId),
+      },
+    ];
+  };
+
+  const handleUnlockUserAccount = (id: number) => {
+    createConfirm({
+      iconType: 'warning',
+      content: t('system.views.user.message.confirmUnlockUserAccount'),
+      onOk: async () => {
+        await unlockUserAccountApi(id);
+        successMessage(t('system.views.user.message.unlockUserAccountSuccess'));
+        await query();
+      },
+    });
+  };
+
+  /**
+   * 用户操作验证
+   * @param userList
+   */
+  const validateOperateUser = (userList: Array<any>) => {
+    if (userList.length === 0) {
+      warnMessage({
+        message: t('system.views.user.validate.selectUser'),
+      });
+      return false;
+    }
+    if (!hasPermissionUpdateSystemUser) {
+      // 如果没有修改系统用户的权限,判断用户中是否有系统用户
+      const hasSysUser = userList.some(({ userType }: any) => userType === SYS_USER_TYPE);
+      if (hasSysUser) {
+        errorMessage(t('system.views.user.validate.noSysUserUpdatePermission'));
+        return false;
+      }
+    }
+    return true;
+  };
+
+  /**
+   * 创建账户
+   */
+  const handleCreateAccount = () => {
+    const userList = getCheckboxRecords(false);
+    if (userList.length === 0) {
+      warnMessage({
+        message: t('system.views.user.validate.selectUser'),
+      });
+      return false;
+    }
+    if (!hasPermissionUpdateSystemUser) {
+      // 如果没有修改系统用户的权限,判断用户中是否有系统用户
+      const hasSysUser = userList.some(({ userType }: any) => userType === SYS_USER_TYPE);
+      if (hasSysUser) {
+        errorMessage(t('system.views.user.validate.noSysUserUpdatePermission'));
+        return false;
+      }
+    }
+    // 判断是否有停用用户
+    const hasNoUse = userList.some((item) => item.useYn === false);
+    if (hasNoUse) {
+      warnMessage(t('system.views.user.message.noUseUserNotCreateAccount'));
+      return false;
+    }
+    createConfirm({
+      iconType: 'warning',
+      title: t('system.views.user.validate.createAccountConfirm'),
+      onOk: () => createAccountApi(userList),
+    });
+  };
+
+  const [
+    registerTable,
+    { editByRowModal, getCheckboxRecords, query, deleteByCheckbox, showAddModal },
+  ] = useSmartTable({
+    columns: getTableColumns(),
+    stripe: true,
+    height: 'auto',
+    border: true,
+    align: 'left',
+    rowConfig: {
+      isHover: true,
+    },
+    pagerConfig: true,
+    useSearchForm: true,
+    sortConfig: {
+      remote: true,
+      defaultSort: {
+        field: 'seq',
+        order: 'asc',
+      },
+    },
+    searchFormConfig: {
+      layout: 'inline',
+      schemas: getSearchSchemas(t),
+      colon: true,
+      searchWithSymbol: true,
+      actionColOptions: {
+        span: undefined,
+      },
+      compact: true,
+    },
+    addEditConfig: {
+      modalConfig: {
+        width: '700px',
+      },
+      formConfig: {
+        colon: true,
+        schemas: getAddEditFormSchemas(t, userTypeListRef),
+        labelCol: {
+          span: 4,
+        },
+        wrapperCol: {
+          span: 19,
+        },
+        baseColProps: {
+          span: 24,
+        },
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: ({ ajaxParameter }) => {
+          const parameter = {
+            ...ajaxParameter,
+          };
+          const deptId = unref(currentDeptId);
+          if (deptId) {
+            parameter.deptIdList = [deptId];
+          }
+          return listApi(parameter);
+        },
+        delete: deleteApi,
+        save: saveUpdateWithDataScopeApi,
+        getById: getByIdWithDataScopeApi,
+        useYn: setUseYnApi,
+      },
+    },
+    toolbarConfig: {
+      refresh: true,
+      resizable: true,
+      buttons: [
+        {
+          code: 'ModalAdd',
+          auth: permissions.add,
+          props: {
+            onClick: () => {
+              showAddModal({ deptId: unref(currentDeptId) });
+            },
+          },
+        },
+        {
+          name: t('system.views.user.button.createAccount'),
+          customRender: 'ant',
+          auth: permissions.createAccount,
+          props: {
+            onClick: () => handleCreateAccount(),
+            type: 'primary',
+          },
+        },
+        {
+          code: 'delete',
+          props: {
+            onClick: () => {
+              const userList = getCheckboxRecords(false);
+              // 验证用户
+              const result = validateOperateUser(userList);
+              if (!result) {
+                return false;
+              }
+              // 验证是否包含系统用户
+              const sysUserValidate = userList.some((item: any) => item.userType === SYS_USER_TYPE);
+              if (sysUserValidate) {
+                errorMessage(t('system.views.user.validate.sysUserNoDelete'));
+                return false;
+              }
+              // 执行删除操作
+              deleteByCheckbox();
+            },
+          },
+        },
+        {
+          code: 'useYnTrue',
+        },
+        {
+          code: 'useYnFalse',
+        },
+        {
+          name: t('system.views.user.button.resetPassword'),
+          auth: permissions.unlockPassword,
+          customRender: 'ant',
+          props: {
+            type: 'primary',
+            preIcon: 'ant-design:unlock-outlined',
+            onClick: () => {
+              const selectRows = getCheckboxRecords(false);
+              if (selectRows.length !== 1) {
+                warnMessage('请选择一条数据');
+                return;
+              }
+              createConfirm({
+                iconType: 'warning',
+                title: '重置密码',
+                content: '确定要重置密码吗?',
+                onOk: async () => {
+                  const newPassword = await resetPassword(selectRows[0].userId);
+                  createConfirm({
+                    iconType: 'warning',
+                    title: '请保存密码',
+                    content: newPassword,
+                  });
+                },
+              });
+            },
+          },
+        },
+        {
+          name: t('system.views.user.button.setRole'),
+          auth: permissions.setRole,
+          customRender: 'ant',
+          props: {
+            type: 'primary',
+            preIcon: 'ant-design:team-outlined',
+            onClick: () => {
+              const selectRows = getCheckboxRecords(false);
+              if (selectRows.length !== 1) {
+                warnMessage('请选择一条数据');
+                return;
+              }
+              openSetRoleModal(true, { userId: selectRows[0].userId });
+            },
+          },
+        },
+      ],
+    },
+  });
+</script>
+
+<style scoped lang="less">
+  .page-container {
+    :deep(.smart-search-container) {
+      .ant-col {
+        //padding: 0 5px;
+      }
+    }
+  }
+
+  .dept-container {
+    margin-right: 5px;
+    padding: 10px;
+    background: white;
+  }
+</style>

+ 87 - 0
src/modules/system/views/user/account/UserAccountUpdateHooks.ts

@@ -0,0 +1,87 @@
+import { ref } from 'vue';
+
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+import { useMessage } from '@/hooks/web/useMessage';
+import { useLoading } from '@/components/Loading';
+
+const { errorMessage, successMessage } = useMessage();
+
+/**
+ * 显示账户hook
+ */
+export const useShowAccount = (t: Function) => {
+  const modalVisible = ref(false);
+  const userId = ref<number>();
+  const dataLoading = ref(false);
+  const userData = ref<any>({});
+  const accountData = ref<any>({});
+  const saveLoading = ref(false);
+  /**
+   * 显示账户信息
+   * @param id 用户ID
+   */
+  const show = (id: number) => {
+    userId.value = id;
+    userData.value = {};
+    accountData.value = {};
+    handleLoadUserAccount();
+  };
+
+  /**
+   * 加载账户信息
+   */
+  const handleLoadUserAccount = async () => {
+    const [openFullLoading, closeFullLoading] = useLoading({});
+    openFullLoading();
+    try {
+      const result = await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: '/sys/user/getById',
+        data: userId.value,
+      });
+      if (result) {
+        userData.value = result;
+        if (result.userAccount) {
+          accountData.value = result.userAccount;
+          modalVisible.value = true;
+        } else {
+          errorMessage(t('system.views.user.message.noAccount'));
+        }
+      } else {
+        userData.value = {};
+        accountData.value = {};
+      }
+    } finally {
+      closeFullLoading();
+    }
+  };
+
+  /**
+   * 执行保存操作
+   */
+  const handleSave = async () => {
+    try {
+      saveLoading.value = true;
+      await defHttp.post({
+        service: ApiServiceEnum.SMART_SYSTEM,
+        url: '/sys/user/saveAccountSetting',
+        data: accountData.value,
+      });
+      modalVisible.value = false;
+      successMessage(t('common.message.editSuccess'));
+    } finally {
+      saveLoading.value = false;
+    }
+  };
+
+  return {
+    modalVisible,
+    show,
+    userId,
+    dataLoading,
+    userData,
+    accountData,
+    handleSave,
+    saveLoading,
+  };
+};

+ 228 - 0
src/modules/system/views/user/account/UserAccountUpdateModal.vue

@@ -0,0 +1,228 @@
+<template>
+  <BasicModal
+    @register="registerModal"
+    @ok="handleSave"
+    width="1200px"
+    :mask-closable="false"
+    :ok-text="$t('common.button.save')"
+    :title="$t('system.views.user.account.title')"
+  >
+    <Descriptions :title="$t('system.views.user.account.title')" bordered>
+      <DescriptionsItem :label="$t('system.views.user.table.username')">
+        {{ userData.username }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.table.fullName')">
+        {{ userData.fullName }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.table.userType')">
+        {{ userData.userType }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.user.table.mobile')">
+        {{ userData.mobile }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.table.email')" :span="2">
+        {{ userData.email }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.user.account.createTime')">
+        {{ accountData.createTime }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.account.accountStatus')">
+        {{ accountData.accountStatus }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.account.initialPasswordYn')">
+        <ATag v-if="accountData.initialPasswordYn" color="#f50">
+          {{ $t('common.form.yes') }}
+        </ATag>
+        <ATag v-else color="#108ee9">{{ $t('common.form.no') }}</ATag>
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.user.account.loginFailTime')">
+        {{ accountData.loginFailTime }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.account.lockTime')">
+        {{ accountData.lockTime }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.user.account.passwordModifyTime')">
+        {{ accountData.passwordModifyTime }}
+      </DescriptionsItem>
+    </Descriptions>
+    <Divider />
+    <section class="account-setting">
+      <div class="title">{{ $t('system.views.user.account.accountSet') }}</div>
+      <BasicForm @register="registerForm" />
+    </section>
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  import { computed, ref, unref } from 'vue';
+
+  import { BasicModal, useModalInner } from '@/components/Modal';
+  import { useForm, BasicForm } from '@/components/Form';
+  import { hasPermission } from '@/utils/auth';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { message, Descriptions, Divider, DescriptionsItem } from 'ant-design-vue';
+
+  import { saveAccountSettingApi, getByIdApi } from '../UserListView.api';
+
+  const { t } = useI18n();
+
+  const userData = ref<Recordable>({});
+  const accountData = ref<Recordable>({});
+
+  /**
+   * 是否有编辑权限
+   */
+  const computedHasEditPermission = computed(() => hasPermission('sys:account:update'));
+
+  const [registerModal, { changeOkLoading, closeModal, changeLoading }] = useModalInner(
+    async (user) => {
+      userData.value = {};
+      accountData.value = {};
+      changeLoading(true);
+      try {
+        const result = await getByIdApi(user.userId);
+        if (result) {
+          userData.value = result;
+          if (result.userAccount) {
+            accountData.value = result.userAccount;
+            setFieldsValue({
+              ...result.userAccount,
+            });
+          } else {
+            message.error(t('system.views.user.message.noAccount'));
+          }
+        }
+      } finally {
+        changeLoading(false);
+      }
+    },
+  );
+
+  const handleSave = async () => {
+    const data = await validate();
+    try {
+      changeOkLoading(true);
+      await saveAccountSettingApi(data);
+      closeModal();
+    } finally {
+      changeOkLoading(false);
+    }
+  };
+
+  const [registerForm, { setFieldsValue, validate }] = useForm({
+    showActionButtonGroup: false,
+    colon: true,
+    baseColProps: {
+      span: 12,
+    },
+    rowProps: {
+      gutter: 18,
+    },
+    schemas: [
+      {
+        label: '',
+        field: 'userId',
+        component: 'Input',
+        show: false,
+      },
+      {
+        label: t('system.views.user.account.maxConnections'),
+        field: 'maxConnections',
+        component: 'InputNumber',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.maxDaysSinceLogin'),
+        field: 'maxDaysSinceLogin',
+        component: 'InputNumber',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.passwordLifeDays'),
+        field: 'passwordLifeDays',
+        component: 'InputNumber',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.maxConnectionsPolicy'),
+        field: 'maxConnectionsPolicy',
+        component: 'Select',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          options: [
+            {
+              label: t('system.views.user.account.loginNotAllow'),
+              value: 'LOGIN_NOT_ALLOW',
+            },
+            {
+              label: t('system.views.user.account.firstUserLogout'),
+              value: 'FIRST_USER_LOGOUT',
+            },
+          ],
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.loginFailTimeLimit'),
+        field: 'loginFailTimeLimit',
+        component: 'InputNumber',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.passwordErrorUnlockSecond'),
+        field: 'passwordErrorUnlockSecond',
+        component: 'InputNumber',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          style: {
+            width: '100%',
+          },
+        },
+      },
+      {
+        label: t('system.views.user.account.ipWhiteList'),
+        field: 'ipWhiteList',
+        component: 'InputTextArea',
+        componentProps: {
+          disabled: !unref(computedHasEditPermission),
+          rows: 4,
+        },
+      },
+    ],
+  });
+</script>
+
+<style lang="less" scoped>
+  .account-setting {
+    .title {
+      margin-bottom: 5px;
+      font-size: 16px;
+      font-weight: 700;
+    }
+  }
+</style>

+ 215 - 0
src/modules/system/views/user/account/UserAccountUpdateModalOld.vue

@@ -0,0 +1,215 @@
+<template>
+  <a-modal
+    v-model:visible="modalVisible"
+    :title="$t('system.views.user.account.title')"
+    :ok-button-props="{ disabled: !computedHasEditPermission, loading: saveLoading }"
+    :ok-text="$t('common.button.save')"
+    width="1200px"
+    :mask-closable="false"
+    @ok="handleSave"
+  >
+    <a-spin :spinning="dataLoading">
+      <a-descriptions :title="$t('system.views.user.account.title')" bordered>
+        <a-descriptions-item :label="$t('system.views.user.table.username')">
+          {{ userData.username }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.table.fullName')">
+          {{ userData.fullName }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.table.userType')">
+          {{ userData.userType }}
+        </a-descriptions-item>
+
+        <a-descriptions-item :label="$t('system.views.user.table.mobile')">
+          {{ userData.mobile }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.table.email')" :span="2">
+          {{ userData.email }}
+        </a-descriptions-item>
+
+        <a-descriptions-item :label="$t('system.views.user.account.createTime')">
+          {{ formatTime(accountData.createTime) }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.account.accountStatus')">
+          {{ accountData.accountStatus }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.account.initialPasswordYn')">
+          <ATag v-if="accountData.initialPasswordYn" color="#f50">
+            {{ $t('common.form.yes') }}
+          </ATag>
+          <ATag v-else color="#108ee9">{{ $t('common.form.no') }}</ATag>
+        </a-descriptions-item>
+
+        <a-descriptions-item :label="$t('system.views.user.account.loginFailTime')">
+          {{ accountData.loginFailTime }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.account.lockTime')">
+          {{ formatTime(accountData.lockTime) }}
+        </a-descriptions-item>
+        <a-descriptions-item :label="$t('system.views.user.account.passwordModifyTime')">
+          {{ formatTime(accountData.passwordModifyTime) }}
+        </a-descriptions-item>
+      </a-descriptions>
+      <a-divider />
+      <section class="account-setting">
+        <div class="title">{{ $t('system.views.user.account.accountSet') }}</div>
+        <a-form :model="accountData">
+          <a-row :gutter="18">
+            <a-col :span="12">
+              <a-form-item
+                name="maxConnections"
+                :label="$t('system.views.user.account.maxConnections')"
+              >
+                <a-input-number
+                  v-model:value="accountData.maxConnections"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                />
+              </a-form-item>
+            </a-col>
+            <a-col :span="12">
+              <a-form-item
+                name="maxDaysSinceLogin"
+                :label="$t('system.views.user.account.maxDaysSinceLogin')"
+              >
+                <a-input-number
+                  v-model:value="accountData.maxDaysSinceLogin"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                />
+              </a-form-item>
+            </a-col>
+          </a-row>
+          <a-row :gutter="18">
+            <a-col :span="12">
+              <a-form-item
+                name="passwordLifeDays"
+                :label="$t('system.views.user.account.passwordLifeDays')"
+              >
+                <a-input-number
+                  v-model:value="accountData.passwordLifeDays"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                />
+              </a-form-item>
+            </a-col>
+            <a-col :span="12">
+              <a-form-item
+                name="maxConnectionsPolicy"
+                :label="$t('system.views.user.account.maxConnectionsPolicy')"
+              >
+                <a-select
+                  v-model:value="accountData.maxConnectionsPolicy"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                >
+                  <a-select-option value="LOGIN_NOT_ALLOW">
+                    {{ $t('system.views.user.account.loginNotAllow') }}
+                  </a-select-option>
+                  <a-select-option value="FIRST_USER_LOGOUT">
+                    {{ $t('system.views.user.account.firstUserLogout') }}
+                  </a-select-option>
+                </a-select>
+              </a-form-item>
+            </a-col>
+          </a-row>
+          <a-row :gutter="18">
+            <a-col :span="12">
+              <a-form-item
+                name="loginFailTimeLimit"
+                :label="$t('system.views.user.account.loginFailTimeLimit')"
+              >
+                <a-input-number
+                  v-model:value="accountData.loginFailTimeLimit"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                />
+              </a-form-item>
+            </a-col>
+            <a-col :span="12">
+              <a-form-item
+                name="passwordErrorUnlockSecond"
+                :label="$t('system.views.user.account.passwordErrorUnlockSecond')"
+              >
+                <a-input-number
+                  v-model:value="accountData.passwordErrorUnlockSecond"
+                  :disabled="!computedHasEditPermission"
+                  style="width: 100%"
+                />
+              </a-form-item>
+            </a-col>
+          </a-row>
+          <a-form-item name="ipWhiteList" :label="$t('system.views.user.account.ipWhiteList')">
+            <a-textarea
+              v-model:value="accountData.ipWhiteList"
+              :disabled="!computedHasEditPermission"
+              :placeholder="$t('system.views.user.account.ipWhiteListPlaceholder')"
+              :rows="4"
+            />
+          </a-form-item>
+        </a-form>
+      </section>
+    </a-spin>
+  </a-modal>
+</template>
+
+<script lang="ts">
+  import { defineComponent, computed } from 'vue';
+  import { useI18n } from 'vue-i18n';
+
+  import dayjs from 'dayjs';
+
+  import { useShowAccount } from './UserAccountUpdateHooks';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { hasPermission } from '@/utils/auth';
+
+  /**
+   * 用户账户更新弹窗
+   */
+  export default defineComponent({
+    name: 'UserAccountUpdateModal',
+    setup() {
+      const { t } = useI18n();
+      const { modalVisible, dataLoading, userData, accountData, show, handleSave, saveLoading } =
+        useShowAccount(t);
+
+      /**
+       * 格式化时间
+       */
+      const formatTime = (timeValue: any) => {
+        if (timeValue) {
+          return dayjs(timeValue).format('YYYY-MM-DD HH:mm:ss');
+        }
+        return '';
+      };
+
+      /**
+       * 是否有编辑权限
+       */
+      const computedHasEditPermission = computed(() => hasPermission('sys:account:update'));
+
+      return {
+        modalVisible,
+        dataLoading,
+        userData,
+        accountData,
+        show,
+        formatTime,
+        ...useSizeSetting(),
+        computedHasEditPermission,
+        handleSave,
+        saveLoading,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .account-setting {
+    .title {
+      margin-bottom: 5px;
+      font-size: 16px;
+      font-weight: 700;
+    }
+  }
+</style>

+ 76 - 0
src/modules/system/views/user/components/UserSetRole.vue

@@ -0,0 +1,76 @@
+<template>
+  <BasicModal @register="registerModal" :title="t('system.views.user.button.setRole')">
+    <SmartTable @register="registerTable" />
+  </BasicModal>
+</template>
+
+<script setup lang="ts">
+  import { BasicModal, useModalInner } from '@/components/Modal';
+  import { SmartTable, useSmartTable } from '@/components/SmartTable';
+  import { ref } from 'vue';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+  const { t } = useI18n();
+
+  const currentUserId = ref<number | null>(null);
+
+  const [registerModal] = useModalInner(async (userId: number) => {
+    currentUserId.value = userId;
+    await query();
+  });
+
+  // const setSelectRole = () => {
+  //   const userId = unref(currentUserId)!;
+  // };
+
+  const [registerTable, { query }] = useSmartTable({
+    border: true,
+    size: 'small',
+    checkboxConfig: {
+      rowTrigger: 'multiple',
+    },
+    proxyConfig: {
+      autoLoad: false,
+      ajax: {
+        query: ({ ajaxParameter }) => {
+          return defHttp.post({
+            service: ApiServiceEnum.SMART_SYSTEM,
+            url: 'sys/role/list',
+            data: {
+              sortName: 'seq',
+              ...ajaxParameter,
+              parameter: {
+                ...ajaxParameter?.parameter,
+                'useYn@=': true,
+              },
+            },
+          });
+        },
+      },
+    },
+    columns: [
+      {
+        type: 'checkbox',
+        width: 50,
+      },
+      {
+        title: '#',
+        type: 'seq',
+        width: 50,
+      },
+      {
+        title: '角色编码',
+        field: 'roleCode',
+        width: 160,
+      },
+      {
+        title: '角色名称',
+        field: 'roleName',
+        minWidth: 160,
+      },
+    ],
+  });
+</script>
+
+<style scoped lang="less"></style>

+ 78 - 0
src/modules/system/views/user/lang/en_US.ts

@@ -0,0 +1,78 @@
+export default {
+  system: {
+    views: {
+      user: {
+        title: {
+          dataAll: 'All permissions',
+          dataDept: 'Dept permission',
+          dataDeptAll: 'Dept and children permission',
+          dataPersonal: 'Personal data permission',
+        },
+        form: {
+          systemUser: 'System user',
+          businessUser: 'Business user',
+          dept: 'Department',
+          dataScope: 'Data scope',
+        },
+        table: {
+          username: 'Username',
+          fullName: 'Full name',
+          email: 'Email',
+          mobile: 'Mobile',
+          userType: 'User type',
+        },
+        button: {
+          createAccount: 'Create account',
+          showAccount: 'Account info',
+          unlockUserAccount: 'Unlock account',
+          resetPassword: 'Rest password',
+          setRole: 'Set role',
+        },
+        account: {
+          title: 'Account info',
+          accountSet: 'Account setting',
+          createTime: 'Account create Time',
+          accountStatus: 'Status',
+          passwordModifyTime: 'Password modify time',
+          lockTime: 'Locked time',
+          loginFailTime: 'Login fail number',
+          initialPasswordYn: 'Initial Password',
+
+          ipWhiteList: 'IP white list',
+          maxConnections: 'Maximum access connections',
+          maxDaysSinceLogin: 'Maximum days since login',
+          passwordLifeDays: 'Password life days',
+          maxConnectionsPolicy: 'Maximum connections policy',
+          loginFailTimeLimit: 'Login fail limit',
+          passwordErrorUnlockSecond: 'Password error unlock',
+
+          loginNotAllow: 'Login not allowed',
+          firstUserLogout: 'Earliest login user logout',
+
+          ipWhiteListPlaceholder: 'IP white list, multiple IPS separated by commas',
+        },
+        validate: {
+          username: 'Please enter username',
+          fullName: 'Please enter full name',
+          email: 'Please enter email',
+          mobile: 'Please enter mobile',
+          selectUser: 'Please select user',
+          sysUserNoDelete: 'System user cannot delete!',
+          setUserUseYn: 'Are you sure you want to {msg} user?',
+          createAccountConfirm: 'Are you sure you want to create account?',
+          noSysUserUpdatePermission: 'Operation failed, no permission to modify system user!',
+          selectDept: 'Please select dept',
+          selectDataScope: 'Please select data scope',
+          selectUserType: 'Please select user type',
+        },
+        message: {
+          deleteUserNotCreateAccount: 'Deleted user cannot create account',
+          noUseUserNotCreateAccount: 'Disabled user cannot create account',
+          createAccountSuccess: 'Account created successfully',
+          deleteValidate: 'Are you sure you want to {msg} this user?',
+          noAccount: 'The user has not created an account',
+        },
+      },
+    },
+  },
+};

+ 81 - 0
src/modules/system/views/user/lang/zh_CN.ts

@@ -0,0 +1,81 @@
+export default {
+  system: {
+    views: {
+      user: {
+        title: {
+          dataAll: '所有数据权限',
+          dataDept: '部门数据权限',
+          dataDeptAll: '部门及下级部门数据权限',
+          dataPersonal: '个人数据权限',
+        },
+        form: {
+          systemUser: '系统用户',
+          businessUser: '业务用户',
+          dept: '所属部门',
+          dataScope: '数据权限范围',
+        },
+        table: {
+          username: '用户名',
+          fullName: '姓名',
+          email: '邮箱',
+          mobile: '手机号',
+          userType: '用户类型',
+          accountStatus: '账户状态',
+        },
+        button: {
+          createAccount: '创建账户',
+          showAccount: '账户信息',
+          unlockUserAccount: '解锁账户',
+          resetPassword: '重置密码',
+          setRole: '设置角色',
+        },
+        account: {
+          title: '账户信息',
+          accountSet: '账户设置',
+          createTime: '账户创建时间',
+          accountStatus: '账户状态',
+          passwordModifyTime: '密码修改时间',
+          lockTime: '账户锁定时间',
+          loginFailTime: '登录失败次数',
+          initialPasswordYn: '是否初始化密码',
+
+          ipWhiteList: 'IP白名单',
+          maxConnections: '最大访问连接数,0:不限制',
+          maxDaysSinceLogin: '指定天数未登录账户锁定,0:永不锁定',
+          passwordLifeDays: '密码必须修改的天数,0:不限制',
+          maxConnectionsPolicy: '超出最大连接数执行策略',
+          loginFailTimeLimit: '登录失败锁定次数,0永不锁定',
+          passwordErrorUnlockSecond: '多次输入密码错误锁定后,指定秒后自动解锁,0:永不自动解锁',
+
+          loginNotAllow: '不允许登录',
+          firstUserLogout: '最早登录用户登出',
+
+          ipWhiteListPlaceholder: 'IP白名单,多个IP以逗号分隔',
+        },
+        validate: {
+          username: '请输入用户名',
+          fullName: '请输入姓名',
+          email: '请输入邮箱',
+          mobile: '请输入手机号',
+          selectUser: '请选择用户',
+          sysUserNoDelete: '系统用户不能删除!',
+          setUserUseYn: '确定要{useYn}用户吗?',
+          createAccountConfirm: '确定要创建账户吗?',
+          noSysUserUpdatePermission: '操作失败,无修改系统用户权限!',
+          selectDept: '请选择部门',
+          selectDataScope: '请选择数据权限',
+          selectUserType: '请选择用户类型',
+        },
+        message: {
+          deleteUserNotCreateAccount: '已删除用户不能创建账户',
+          noUseUserNotCreateAccount: '未启用用户不能创建账户',
+          createAccountSuccess: '创建账户成功',
+          deleteValidate: '确定要{msg}该用户吗?',
+          noAccount: '该用户未创建账户',
+          confirmUnlockUserAccount: '确定要解锁账户吗?',
+          unlockUserAccountSuccess: '账户解锁成功',
+        },
+      },
+    },
+  },
+};

+ 33 - 0
src/store/modules/exception.ts

@@ -0,0 +1,33 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 系统异常store
+ */
+export const useSystemExceptionStore = defineStore('systemExceptionStore', {
+  state: () => {
+    return {
+      modalVisible: false,
+      noList: [] as Array<number>,
+    };
+  },
+  actions: {
+    /**
+     * 显示异常弹窗
+     * @param exceptionNo
+     */
+    handleShowExceptionModal(exceptionNo: number) {
+      if (this.modalVisible === false) {
+        this.noList = [];
+      }
+      this.modalVisible = true;
+      this.noList.push(exceptionNo);
+    },
+    /**
+     * 隐藏弹窗
+     */
+    handleHideExceptionModal() {
+      this.noList = [];
+      this.modalVisible = false;
+    },
+  },
+});

+ 27 - 0
src/utils/message/SystemNotice.ts

@@ -0,0 +1,27 @@
+import type { ModalOptionsEx, MessageOptions } from '@/hooks/web/useMessage';
+import { ModalOptionsPartial, useMessage } from '@/hooks/web/useMessage';
+
+export const errorMessage = (e: any) => {
+  const { errorMessage } = useMessage();
+  errorMessage(e);
+};
+
+export const successMessage = (options: MessageOptions | string) => {
+  const { successMessage } = useMessage();
+  successMessage(options);
+};
+
+export const createConfirm = (options: ModalOptionsEx) => {
+  const { createConfirm } = useMessage();
+  return createConfirm(options);
+};
+
+export const warnMessage = (options: MessageOptions | string) => {
+  const { warnMessage } = useMessage();
+  return warnMessage(options);
+};
+
+export const createWaringModal = (options: ModalOptionsPartial) => {
+  const { createWarningModal } = useMessage();
+  createWarningModal(options);
+};