Browse Source

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

shizhongming 3 years ago
parent
commit
8e00be6f79
  1. 3
      src/components/LayoutSeparate/index.ts
  2. 89
      src/components/LayoutSeparate/src/LayoutSeparate.less
  3. 294
      src/components/LayoutSeparate/src/LayoutSeparate.tsx
  4. 1
      src/components/SmartTable/index.ts
  5. 3
      src/components/SmartTable/src/hooks/useTableToolbarConfig.ts
  6. 114
      src/components/SmartTable/src/utils/TableCommon.tsx
  7. 30
      src/hooks/web/useMessage.tsx
  8. 236
      src/modules/system/components/SysDept/SysDeptTree.vue
  9. 120
      src/modules/system/hooks/SysDictHooks.ts
  10. 138
      src/modules/system/views/user/UserListView.api.ts
  11. 297
      src/modules/system/views/user/UserListView.config.ts
  12. 444
      src/modules/system/views/user/UserListView.vue
  13. 87
      src/modules/system/views/user/account/UserAccountUpdateHooks.ts
  14. 228
      src/modules/system/views/user/account/UserAccountUpdateModal.vue
  15. 215
      src/modules/system/views/user/account/UserAccountUpdateModalOld.vue
  16. 76
      src/modules/system/views/user/components/UserSetRole.vue
  17. 78
      src/modules/system/views/user/lang/en_US.ts
  18. 81
      src/modules/system/views/user/lang/zh_CN.ts
  19. 33
      src/store/modules/exception.ts
  20. 27
      src/utils/message/SystemNotice.ts

3
src/components/LayoutSeparate/index.ts

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

89
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
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
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';

3
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
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 };

30
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
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
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
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
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
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
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
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
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
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
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
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
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
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);
};
Loading…
Cancel
Save