20 changed files with 2592 additions and 2 deletions
@ -0,0 +1,3 @@ |
|||
import LayoutSeparate from './src/LayoutSeparate'; |
|||
|
|||
export { LayoutSeparate }; |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
}; |
|||
}; |
|||
@ -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 }; |
|||
@ -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> |
|||
@ -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, |
|||
}; |
|||
}; |
|||
@ -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, |
|||
}, |
|||
}); |
|||
}; |
|||
@ -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,
|
|||
}, |
|||
}, |
|||
]; |
|||
}; |
|||
@ -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> |
|||
@ -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, |
|||
}; |
|||
}; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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', |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
@ -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: '账户解锁成功', |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
@ -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; |
|||
}, |
|||
}, |
|||
}); |
|||
@ -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…
Reference in new issue