From 8e00be6f79b2f5bf4289a926ddffe039f96e179d Mon Sep 17 00:00:00 2001 From: shizhongming Date: Wed, 7 Feb 2024 23:49:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=B3=BB=E7=BB=9F=E6=A8=A1=E5=9D=97):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LayoutSeparate/index.ts | 3 + .../LayoutSeparate/src/LayoutSeparate.less | 89 ++++ .../LayoutSeparate/src/LayoutSeparate.tsx | 294 ++++++++++++ src/components/SmartTable/index.ts | 1 + .../src/hooks/useTableToolbarConfig.ts | 3 +- .../SmartTable/src/utils/TableCommon.tsx | 114 +++++ src/hooks/web/useMessage.tsx | 30 +- .../system/components/SysDept/SysDeptTree.vue | 236 ++++++++++ src/modules/system/hooks/SysDictHooks.ts | 120 +++++ .../system/views/user/UserListView.api.ts | 138 ++++++ .../system/views/user/UserListView.config.ts | 297 ++++++++++++ .../system/views/user/UserListView.vue | 444 ++++++++++++++++++ .../user/account/UserAccountUpdateHooks.ts | 87 ++++ .../user/account/UserAccountUpdateModal.vue | 228 +++++++++ .../account/UserAccountUpdateModalOld.vue | 215 +++++++++ .../views/user/components/UserSetRole.vue | 76 +++ src/modules/system/views/user/lang/en_US.ts | 78 +++ src/modules/system/views/user/lang/zh_CN.ts | 81 ++++ src/store/modules/exception.ts | 33 ++ src/utils/message/SystemNotice.ts | 27 ++ 20 files changed, 2592 insertions(+), 2 deletions(-) create mode 100644 src/components/LayoutSeparate/index.ts create mode 100644 src/components/LayoutSeparate/src/LayoutSeparate.less create mode 100644 src/components/LayoutSeparate/src/LayoutSeparate.tsx create mode 100644 src/components/SmartTable/src/utils/TableCommon.tsx create mode 100644 src/modules/system/components/SysDept/SysDeptTree.vue create mode 100644 src/modules/system/hooks/SysDictHooks.ts create mode 100644 src/modules/system/views/user/UserListView.api.ts create mode 100644 src/modules/system/views/user/UserListView.config.ts create mode 100644 src/modules/system/views/user/UserListView.vue create mode 100644 src/modules/system/views/user/account/UserAccountUpdateHooks.ts create mode 100644 src/modules/system/views/user/account/UserAccountUpdateModal.vue create mode 100644 src/modules/system/views/user/account/UserAccountUpdateModalOld.vue create mode 100644 src/modules/system/views/user/components/UserSetRole.vue create mode 100644 src/modules/system/views/user/lang/en_US.ts create mode 100644 src/modules/system/views/user/lang/zh_CN.ts create mode 100644 src/store/modules/exception.ts create mode 100644 src/utils/message/SystemNotice.ts diff --git a/src/components/LayoutSeparate/index.ts b/src/components/LayoutSeparate/index.ts new file mode 100644 index 000000000..6ce66e1a5 --- /dev/null +++ b/src/components/LayoutSeparate/index.ts @@ -0,0 +1,3 @@ +import LayoutSeparate from './src/LayoutSeparate'; + +export { LayoutSeparate }; diff --git a/src/components/LayoutSeparate/src/LayoutSeparate.less b/src/components/LayoutSeparate/src/LayoutSeparate.less new file mode 100644 index 000000000..cd19ddd23 --- /dev/null +++ b/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; + } + } + } +} diff --git a/src/components/LayoutSeparate/src/LayoutSeparate.tsx b/src/components/LayoutSeparate/src/LayoutSeparate.tsx new file mode 100644 index 000000000..360721835 --- /dev/null +++ b/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, + 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, + default: false, + }, + // 尺寸,如果是number类型,按照百分比分隔 + firstSize: { + type: [Number, String] as PropType, + default: 50, + validator(value: string | number) { + if (!isFinite(value)) { + // @ts-ignore + return endsWith(value, '%') || endsWith(value, 'px'); + } + return true; + }, + }, + showLine: { + type: Boolean as PropType, + default: true, + }, + lineStyle: { + type: [Object, String] as PropType, + default: () => { + return { + 'border-left': '1px solid rgba(0,0,0,0.2)', + }; + }, + }, + highLineStyle: { + type: [Object, String] as PropType, + 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(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 = ['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 ( +
+ {/* 第一块区域,左或者上 */} +
+
{first ? first() : ''}
+ {/* 分割线 */} + {props.showLine ? ( +
draggable.value && onLineMouseDown && onLineMouseDown(e)} + class={computedLineClass.value} + > + +
+ ) : ( + '' + )} +
+ {/* 第二块区域,右或者下 */} +
+ {second ? second() : ''} +
+
+ ); + }; + }, +}); + +/** + * 支持拖拽 + * @param isLeftRight 是否是左右布局 + * @param draggable + */ +const useDrag = (isLeftRight: Ref, draggable: Ref) => { + // 鼠标是否按下 + 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(); + // 左右方向移动距离 + 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, + }; +}; diff --git a/src/components/SmartTable/index.ts b/src/components/SmartTable/index.ts index b7be5c1dc..5b4adc37c 100644 --- a/src/components/SmartTable/index.ts +++ b/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'; diff --git a/src/components/SmartTable/src/hooks/useTableToolbarConfig.ts b/src/components/SmartTable/src/hooks/useTableToolbarConfig.ts index 5a1ad8dbe..104b354d0 100644 --- a/src/components/SmartTable/src/hooks/useTableToolbarConfig.ts +++ b/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: { diff --git a/src/components/SmartTable/src/utils/TableCommon.tsx b/src/components/SmartTable/src/utils/TableCommon.tsx new file mode 100644 index 000000000..5acb3aebe --- /dev/null +++ b/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 {t('common.form.yes')}; + } + return {t('common.form.no')}; + }; + /** + * 创建列信息 + */ + 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 {t('common.form.no')}; + } + return {t('common.form.yes')}; + }; + /** + * 创建列信息 + */ + 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 {t('common.form.use')}; + } + return {t('common.form.noUse')}; + }; + /** + * 创建列信息 + */ + 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 }; diff --git a/src/hooks/web/useMessage.tsx b/src/hooks/web/useMessage.tsx index bc5c6dd93..b5e1bdf23 100644 --- a/src/hooks/web/useMessage.tsx +++ b/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); + } +}; diff --git a/src/modules/system/components/SysDept/SysDeptTree.vue b/src/modules/system/components/SysDept/SysDeptTree.vue new file mode 100644 index 000000000..c6017fc89 --- /dev/null +++ b/src/modules/system/components/SysDept/SysDeptTree.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/modules/system/hooks/SysDictHooks.ts b/src/modules/system/hooks/SysDictHooks.ts new file mode 100644 index 000000000..3d301facc --- /dev/null +++ b/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, immediate = true) => { + const dictData = ref>([]); + + 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>({}); + + const computedDictMap = computed(() => { + const result: 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 = {}; + 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, + }; +}; diff --git a/src/modules/system/views/user/UserListView.api.ts b/src/modules/system/views/user/UserListView.api.ts new file mode 100644 index 000000000..29c662bd8 --- /dev/null +++ b/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, + }, + }); +}; diff --git a/src/modules/system/views/user/UserListView.config.ts b/src/modules/system/views/user/UserListView.config.ts new file mode 100644 index 000000000..c8ff2a4fa --- /dev/null +++ b/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): 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, + }, + }, + ]; +}; diff --git a/src/modules/system/views/user/UserListView.vue b/src/modules/system/views/user/UserListView.vue new file mode 100644 index 000000000..cc0130ec1 --- /dev/null +++ b/src/modules/system/views/user/UserListView.vue @@ -0,0 +1,444 @@ + + + + + diff --git a/src/modules/system/views/user/account/UserAccountUpdateHooks.ts b/src/modules/system/views/user/account/UserAccountUpdateHooks.ts new file mode 100644 index 000000000..42d2f4453 --- /dev/null +++ b/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(); + const dataLoading = ref(false); + const userData = ref({}); + const accountData = ref({}); + 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, + }; +}; diff --git a/src/modules/system/views/user/account/UserAccountUpdateModal.vue b/src/modules/system/views/user/account/UserAccountUpdateModal.vue new file mode 100644 index 000000000..d77794be0 --- /dev/null +++ b/src/modules/system/views/user/account/UserAccountUpdateModal.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/modules/system/views/user/account/UserAccountUpdateModalOld.vue b/src/modules/system/views/user/account/UserAccountUpdateModalOld.vue new file mode 100644 index 000000000..6e92b8eca --- /dev/null +++ b/src/modules/system/views/user/account/UserAccountUpdateModalOld.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/modules/system/views/user/components/UserSetRole.vue b/src/modules/system/views/user/components/UserSetRole.vue new file mode 100644 index 000000000..07d79fc7c --- /dev/null +++ b/src/modules/system/views/user/components/UserSetRole.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/modules/system/views/user/lang/en_US.ts b/src/modules/system/views/user/lang/en_US.ts new file mode 100644 index 000000000..c741cf560 --- /dev/null +++ b/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', + }, + }, + }, + }, +}; diff --git a/src/modules/system/views/user/lang/zh_CN.ts b/src/modules/system/views/user/lang/zh_CN.ts new file mode 100644 index 000000000..9d731d566 --- /dev/null +++ b/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: '账户解锁成功', + }, + }, + }, + }, +}; diff --git a/src/store/modules/exception.ts b/src/store/modules/exception.ts new file mode 100644 index 000000000..e486e4190 --- /dev/null +++ b/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, + }; + }, + 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; + }, + }, +}); diff --git a/src/utils/message/SystemNotice.ts b/src/utils/message/SystemNotice.ts new file mode 100644 index 000000000..dba4712d1 --- /dev/null +++ b/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); +};