7731293f by lihua

用户管理

1 parent b5d70ee4
......@@ -41,6 +41,8 @@ declare module '@vue/runtime-core' {
PageHeader: typeof import('./src/components/PageHeader/index.vue')['default']
PageMain: typeof import('./src/components/PageMain/index.vue')['default']
PageNav: typeof import('./src/components/PageNav/index.vue')['default']
PasswordStrengthContent: typeof import('./src/components/PasswordStrengthMeter/PasswordStrengthContent.vue')['default']
PasswordStrengthMeter: typeof import('./src/components/PasswordStrengthMeter/index.vue')['default']
PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default']
Popover: typeof import('./src/components/Popover/index.vue')['default']
RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default']
......@@ -49,6 +51,7 @@ declare module '@vue/runtime-core' {
Schedule: typeof import('./src/components/Schedule/index.vue')['default']
SearchBar: typeof import('./src/components/SearchBar/index.vue')['default']
SecondAndMinute: typeof import('./src/components/Schedule/component/secondAndMinute.vue')['default']
SelectPersonnel: typeof import('./src/components/SelectPersonnel/src/SelectPersonnel.vue')['default']
SplitPane: typeof import('./src/components/SplitPane/index.vue')['default']
StepBar: typeof import('./src/components/StepBar/index.vue')['default']
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
......@@ -59,6 +62,7 @@ declare module '@vue/runtime-core' {
Table_search: typeof import('./src/components/Tools/table_search.vue')['default']
Table_tools: typeof import('./src/components/Tools/table_tools.vue')['default']
Table_v2: typeof import('./src/components/Table/table_v2.vue')['default']
TableActions: typeof import('./src/components/TablePlus/src/components/TableActions.vue')['default']
Tabs: typeof import('./src/components/Tabs/index.vue')['default']
Toolbar: typeof import('./src/components/LineageGraph/toolbar.vue')['default']
Topbar: typeof import('./src/components/LineageGraph/topbar.vue')['default']
......
import request from "@/utils/request";
import { ElMessage } from "element-plus";
/** 获取租户列表(分页) */
export const getTenantSinglePage = (params) => request({
......@@ -70,6 +71,17 @@ export const removeOrganisation = (guids) => request({
/**
* 获取部门列表
* @param param
* @returns
*/
export const getOrganisationListApi = (params) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/singlePage`,
method: 'post',
data: params
})
/**
* 修改部门关系
* @param param
* @returns
......@@ -91,3 +103,117 @@ export const addOrganisation = (params) => request({
data: params
});
/**
* 获取人员列表
* @param param
* @returns
*/
export const getStaff = (params) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/singlePage`,
method: 'post',
data: params
});
/**
* 删除人员
* @param guids:string[]
* @returns
*/
export const removeStaff = (guids:string[]) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/removeListByGuids`,
method: 'delete',
data: guids
})
/**
* !获取部门关系树(不带人员)
* @param tenantGuid
* @returns
*/
export const getOrganisationRelTreeListPromise = (tenantGuid:string):Promise<any> => {
let organisationTree:any[] = []
let param = {
tenantGuid,
bizState: "Y"
}
return new Promise((resolve, reject) => {
getOrganisationRelTreeList(param).then((res: any) => {
if (res?.code === '00000') {
organisationTree = res.data || []
} else {
res?.msg && ElMessage.error(res?.msg)
}
resolve(organisationTree)
})
})
}
/**
* 获取部门关系列表tree
* @param param
* @returns
*/
export const getOrganisationRelTreeList = (param:{tenantGuid:string}) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/tree-list`,
method: 'post',
data: param
});
export const resetPwd2 = (userGuid) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/user/data/resetPwd?userGuid=${userGuid}`,
method: 'put'
});
// 模板列表
export const getPermissionTemplateList = (param) => request({
url: `${import.meta.env.VITE_APP_AUTH_URL}/func-permission-template/page-list`,
method: 'post',
data: param
});
export const getTemplateListPromise = (customParam?: {}): Promise<any> => {
let list = []
let param = customParam ? Object.assign({}, { pageIndex: 1, pageSize: -1, bizState: 'Y' }, customParam) : { pageIndex: 1, pageSize: -1, bizState: 'Y' }
return new Promise((resolve, reject) => {
getPermissionTemplateList(param).then((res: any) => {
if (res?.code === '00000') {
list = res?.data?.records || []
} else {
res?.msg && ElMessage.error(res?.msg)
}
resolve(list)
})
})
}
/**
* 获取人员详情
* @param guid:string
* @returns
*/
export const getStaffDetail = (guid: string) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/getByGuid/${guid}`,
method: 'get'
});
/**
* 获取组织人员树
* @param guids
* @returns
*/
export const getOrganisationTree = (tenantGuid:string) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/get-tenant-organisation-staff-tree?tenantGuid=${tenantGuid}`,
method: 'get'
});
/**
* 新增修改人员基本信息
* @param param
* @returns
*/
export const addorUpdateStaff = (param) => request({
url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/save-or-update`,
method: 'put',
data: param
});
......
<svg t="1772248555140" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1764" width="200" height="200"><path d="M438.851435 3.158997c100.42378-18.02947 311.950052 41.873717 329.942951 160.363538V302.089797s46.66451 9.179304 38.472621 81.553179c-8.191889 72.264162-62.243727 92.341604-99.838645 127.705696-37.66806 35.364091-30.719583 91.756469-92.085608 120.574363 0 0-1.645692 1.023986-4.607937 2.669678v69.228775s200.774418 61.658592 261.66502 78.261795C943.969151 805.817818 1005.700885 899.329692 1005.700885 1024H18.285714c0-124.670308 61.731734-218.182182 136.592432-241.953288 21.393995-6.765622 48.419914-16.09121 76.140681-25.89222l16.639774-5.961062c63.889419-22.856833 126.096574-46.408513 126.096575-46.408513l-0.036571-69.594484c-2.559965-1.462837-3.91309-2.303969-3.91309-2.303969-61.402595-28.817895-52.771855-85.210272-92.085607-120.574363a328.187546 328.187546 0 0 0-20.479722-16.712916l-10.569-8.009034c-31.670427-23.91739-62.682578-48.749053-68.789923-102.983746-8.191889-72.300733 16.749487-81.553179 35.985797-81.553179V179.065181C222.826938 79.628816 339.232216 14.057135 438.888006 3.158997z" fill="#B2B2B2" p-id="1765"></path></svg>
\ No newline at end of file
......@@ -34,6 +34,7 @@ import {
getPathUrl
} from "@/api/modules/obsService";
import { Editor, EditorExpose } from '@/components/Editor'
import PasswordStrengthMeter from "../PasswordStrengthMeter/index.vue";
const userStore = useUserStore()
......@@ -51,6 +52,7 @@ const emits = defineEmits([
"uploadFileChange",
"scheduleChange",
"inputAppendClick",
"inputClick",
"cascaderChange"
]);
const props = defineProps({
......@@ -88,7 +90,6 @@ const formRules = computed(() => {
return reactive<FormRules>(props.rules);
});
const formItemList: any = computed(() => {
const readonly = props.readonly ?? false;
let itemList = props.itemList ?? [];
// return setItemsDisabled(itemList, readonly);
return itemList;
......@@ -293,6 +294,7 @@ const handlerIptClick = (row) => {
) {
row.autocompleteSetting.readonly = false;
}
emits("inputClick", row, formInline.value)
};
const toolBtnClick = (btn) => {
......@@ -614,6 +616,12 @@ const panelChange = (scope, row) => {
}
}
const handlePwdLenMeterChange = (val, item) => {
nextTick(() => {
ruleFormRef.value?.validateField([item.field])
})
}
</script>
<template>
<el-form ref="ruleFormRef" class="dialog-form-inline" :class="[props.col]" :inline="true" :model="formInline" @submit.prevent
......@@ -705,6 +713,20 @@ const panelChange = (scope, row) => {
{{ item.getName(data, node) }}
</div>
</template>
<span v-else-if="item.customInfo" class="custom-tree-node">
<span v-if="item.customInfo?.type == 'prefixIcon'" class="custom_icon">
<el-icon v-if="data.nodeType == 2" style="margin-top: 3px;">
<svg-icon name="file-person" />
</el-icon>
<el-icon v-else-if="node.expanded" style="margin-top: 3px;">
<svg-icon name="file-open" />
</el-icon>
<el-icon v-else-if="!node.expanded" style="margin-top: 3px;">
<svg-icon name="file-closed" />
</el-icon>
</span>
<span style="margin-left: 4px;vertical-align: top;">{{ data[item.props.label] }}</span>
</span>
<span v-else>{{ data[item.props.label] }}</span>
</template>
</el-tree-select>
......@@ -1530,6 +1552,15 @@ const panelChange = (scope, row) => {
v-model.trim="formInline[item.field]" type="password" :placeholder="item.placeholder"
:clearable="item.clearable" :maxlength="item.maxlength ?? ''" show-password :disabled="item.disabled"
:autocomplete="item.autocompleteSetting?.autocomplete || item.autocomplete" :readonly="item.autocompleteSetting?.readonly || item.readonly" @click="handlerIptClick(item)" />
<PasswordStrengthMeter
:class="[item.col, { is_block: item.block }]"
v-else-if="item.type == 'password-len-meter'"
:placeholder="item.placeholder"
:disabled="item.disabled"
:store-key="'passwordLenMeter' + item.field"
v-model.trim="formInline[item.field]"
@change="(val) => handlePwdLenMeterChange(val, item)"
></PasswordStrengthMeter>
<div class="input_def" v-else>
<span v-if="item.beforeMsg" style="color: #212121;">{{ item.beforeMsg }}</span>
<el-input :class="[item.col, { is_block: item.block }]" v-model.trim="formInline[item.field]" :style="item.width ? { width: item.width } : null"
......
<!-- PasswordStrengthContent.vue -->
<template>
<div class="password-strength-content">
<div class="strength-label">密码需满足以下要求:</div>
<div class="strength-bar">
<div
class="strength-fill"
:class="levelClass"
:style="{ width: fillWidth }"
></div>
</div>
<div class="strength-text" :class="levelClass">
密码强度:{{ strengthText }}
</div>
<div class="requirements">
<div v-for="(req, key) in requirements" :key="key" class="requirement" :class="getReqClass(key)">
<span class="requirement-icon">{{ getReqIcon(key) }}</span>
<span>{{ req.label }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useStorage } from '@/hooks/useStorage'
const props = defineProps({
password: {
type: String,
default: ''
},
storeKey: {
type: String,
default: 'firstUnmetRequirement'
}
});
const emits = defineEmits(['getCheckResult']);
const { setStorage } = useStorage()
const checkPasswordStrength = (pwd) => {
if (!pwd) return { score: 0, level: 'weak', checks: {} };
const checks = {
length: pwd.length >= 8,
lower: /[a-z]/.test(pwd),
upper: /[A-Z]/.test(pwd),
number: /[0-9]/.test(pwd),
special: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(pwd)
};
const score = Object.values(checks).filter(Boolean).length;
let level = 'weak';
if (score >= 5) level = 'strong';
else if (score >= 3) level = 'medium';
console.log(checks,'checkPasswordStrength');
findFirstUnmetRequirement(checks)
return { score, level, checks };
};
const result = computed(() => checkPasswordStrength(props.password));
const fillWidth = computed(() => `${(result.value.score / 5) * 100}%`);
const strengthText = computed(() => ({ weak: '弱', medium: '中', strong: '强' })[result.value.level]);
const levelClass = computed(() => result.value.level);
const requirements = {
number: { label: '包含数字 (0-9)' },
length: { label: '至少 8 个字符' },
lower: { label: '包含小写字母 (a-z)' },
upper: { label: '包含大写字母 (A-Z)' },
special: { label: '包含特殊符号' }
};
const getReqClass = (key) => (result.value.checks[key] ? 'met' : props.password.length >= 3 ? 'unmet' : 'pending');
const getReqIcon = (key) => {
if (result.value.checks[key]) return '✓';
if (props.password.length >= 3) return '✗';
return '○';
};
/**
* 匹配未满足的规则
* @param checks
*/
const findFirstUnmetRequirement = (checks) => {
if (!props.password) return null;
console.log(checks,'firstUnmetRequirement');
for (const key in requirements) {
const isMet = checks[key];
if (!isMet) {
let label = requirements[key].label;
setStorage(props.storeKey, label)
return label; // 返回未满足的第一条规则的 label
}
}
setStorage(props.storeKey, '')
return null; // 全部满足
};
// 暴露方法给父组件
defineExpose({
});
</script>
<style scoped lang="scss">
/* 同前,省略样式 */
.password-strength-content {
font-size: 14px;
}
.strength-label {
margin-bottom: 12px;
color: #333;
font-weight: 500;
}
.strength-bar {
height: 6px;
border-radius: 3px;
background: #e9ecef;
overflow: hidden;
margin: 8px 0;
}
.strength-fill {
height: 100%;
border-radius: 3px;
transition: all 0.3s ease-out;
}
.strength-fill.weak {
background-color: #dc3545;
}
.strength-fill.medium {
background-color: #ffc107;
}
.strength-fill.strong {
background-color: #28a745;
}
.strength-text {
font-weight: 600;
font-size: 13px;
margin-bottom: 12px;
}
.strength-text.weak {
color: #dc3545;
}
.strength-text.medium {
color: #e0a800;
}
.strength-text.strong {
color: #28a745;
}
.requirements {
color: #555;
}
.requirement {
display: flex;
align-items: center;
margin: 4px 0;
}
.requirement-icon {
width: 16px;
text-align: center;
margin-right: 6px;
font-weight: bold;
}
.requirement.met .requirement-icon {
color: #28a745;
}
.requirement.unmet .requirement-icon {
color: #dc3545;
}
.requirement.pending .requirement-icon {
color: #adb5bd;
}
.requirement.met {
color: #28a745;
}
.requirement.unmet {
color: #dc3545;
}
.requirement.pending {
color: #6c757d;
}
</style>
\ No newline at end of file
<!-- PasswordStrengthPopover.vue -->
<template>
<div class="password-wrapper">
<el-popover
v-model:visible="popoverVisible"
placement="top"
:width="300"
trigger="manual"
popper-class="password-strength-popper"
:disabled="!internalValue"
>
<template #reference>
<el-input
ref="inputRef"
:model-value="internalValue"
:placeholder="placeholder"
type="password"
clearable
showPassword
:disabled="disabled"
autocomplete="new-password"
@update:model-value="onInput"
@focus="onFocus"
@blur="onBlur"
@click="focusInput"
/>
</template>
<PasswordStrengthContent
ref="strengthRef"
:storeKey="storeKey"
:password="internalValue"
/>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import { ElInput } from 'element-plus';
import PasswordStrengthContent from './PasswordStrengthContent.vue';
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入密码'
},
// 新增:用于表单校验的字段名(可选)
name: {
type: String,
default: 'password'
},
storeKey: {
type: String,
default: 'firstUnmetRequirement'
},
disabled: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue','change']);
const internalValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newVal) => {
internalValue.value = newVal;
}
);
const onInput = (val) => {
internalValue.value = val;
emit('update:modelValue', val);
emit('change', val);
};
const popoverVisible = ref(false);
const inputRef = ref(null);
const strengthRef = ref(null);
const onFocus = () => {
if (internalValue.value) {
popoverVisible.value = true;
}
};
const onBlur = () => {
setTimeout(() => {
const activeEl = document.activeElement;
const popoverEl = document.querySelector('.password-strength-popper');
if (!popoverEl?.contains(activeEl)) {
popoverVisible.value = false;
}
}, 200);
};
const focusInput = () => {
nextTick(() => {
inputRef.value?.focus();
});
};
watch(internalValue, (newVal) => {
if (newVal) {
if (document.activeElement === inputRef.value?.$el?.querySelector('input')) {
popoverVisible.value = true;
}
} else {
popoverVisible.value = false;
}
});
// 暴露给 el-form 使用
defineExpose({
});
</script>
<style scoped lang="scss">
.password-wrapper {
width: 100%;
// max-width: 400px;
}
.password-strength-popper {
padding: 14px !important;
background: #fff;
border: 1px solid #ddd;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 8px;
}
</style>
\ No newline at end of file
import dayjs from '@/utils/dayjs'
import useUserStore from '@/store/modules/user'
import { getOrganisationRelTreeListPromise, getTemplateListPromise } from "@/api/modules/dataBasic"
const currentDate = dayjs(new Date()).format('YYYY-MM-DD')
const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
const isNonEmptyArray = (val: any): boolean => {
return isArray(val) && val.length > 0;
}
const useGetData = (param = {}) => {
const BasicInfo: any = ref({}) // 基础资料
const gradeList = ref<any>([]) // 职级关系
const platformGradeList = ref<any>([]) // 平台职级
const postionList = ref<any>([]) // 职位
const tenantRelList = ref<any>([]) // 公司关系
const templateList = ref<any>([]) // 菜单模板列表
const orgMap = ref<orgMapRes>() // 人员组织信息
const organisationTree = ref<any>([]) // 组织树
const personelTree = ref<any>([]) // 人员树
const amoebaTree = ref<any>([]) // 阿米巴树
const financeSubjectDict = ref<any>([]) // 关联财务科目字典
const systemSideList = ref<{ // 系统列表
systemName: string,
guid: string
}[]>([])
const getFinanceSubject = async (customParam = {}) => { // 获取业务线
return await budgetApi.getFinanceSubjectTreePromise((Object.assign({}, param, customParam)))
}
// !基础数据缓存机制
async function getFinanceSubjectTree({ useCache = true, customParam = {} } = {}) {
if (isNonEmptyArray(BasicInfo.financeSubject) && useCache) {
return BasicInfo.financeSubject
} else {
let res = await getFinanceSubject(customParam)
BasicInfo.financeSubject = useCache ? res : []
return res
}
}
async function getGradeTitleRelList({ useCache = true, customParam = {} } = {}) { // 获取职级关系列表
if ( isNonEmptyArray(BasicInfo.gradeList) && useCache) {
gradeList.value = BasicInfo.gradeList
return gradeList.value
} else {
let res = await userApi.getGradeRelListPromise(customParam)
BasicInfo.gradeList = useCache ? res : []
gradeList.value = res
return gradeList.value
}
}
async function getGradeList({ useCache = true, customParam = {} } = {}) { // 获取平台职级列表
if ( isNonEmptyArray(BasicInfo.platformGradeList) && useCache) {
platformGradeList.value = BasicInfo.platformGradeList
return platformGradeList.value
} else {
let res = await userApi.getGradeListPromise(customParam)
BasicInfo.platformGradeList = useCache ? res : []
platformGradeList.value = res
return platformGradeList.value
}
}
async function getPostionList({ useCache = true, customParam = {} } = {}) { // 获取职位
if ( isNonEmptyArray(BasicInfo.postionList) && useCache) {
postionList.value = BasicInfo.postionList
return postionList.value
} else {
let res = await userApi.getPositionPromise(customParam)
BasicInfo.postionList = useCache ? res : []
postionList.value = res
return postionList.value
}
}
async function getTenantRelList(useCache = true) { // 获取公司关系列表
if (isNonEmptyArray(BasicInfo.tenantRelList) && useCache) {
tenantRelList.value = BasicInfo.tenantRelList
return tenantRelList.value
} else {
let res = await tenantApi.getTenantListPromise('Y')
BasicInfo.tenantRelList = useCache ? res : []
tenantRelList.value = res
return tenantRelList.value
}
}
async function getPermissionTemplateList({ useCache = true, customParam = {} } = {}) { // 获取菜单模板
if (isNonEmptyArray(templateList.value) && useCache) {
return templateList.value
} else {
let res = await getTemplateListPromise(customParam)
templateList.value = res || []
return templateList.value
}
}
interface orgMapRes {
[key:string]:{
guid:string, // staffGuid
userGuid:string, // userGuid
orgGuid:string, // 部门
orgName:string,
orgGuidTop:string, // 一级部门guid
orgNameTop:number,
}
}
// 获取人员组织信息
async function getOrgMap({ useCache = true, customParam = {} } = {}) { // 获取人员组织信息
const userStore = useUserStore()
let staffGuid = userStore.userInfo.staffGuid
if (isObject(BasicInfo.orgMap) && BasicInfo.orgMap[staffGuid] && useCache) {
orgMap.value = BasicInfo.orgMap
return orgMap.value
} else {
const userStore = useUserStore()
let res = await staffApi.getOrgMap([userStore.userInfo.staffGuid])
BasicInfo.orgMap = useCache ? res : null
orgMap.value = res
return orgMap.value
}
}
// 获取组织树
async function getPersonelTree({ useCache = true, customParam = {} } = {}) {
if ( isNonEmptyArray(BasicInfo.personelTree) && useCache) {
personelTree.value = BasicInfo.personelTree
return personelTree.value
} else {
const userStore = useUserStore()
let res = await tenantApi.getOrganisationTreePromise(userStore.userInfo.tenantGuid)
mulTreeData(res)
BasicInfo.personelTree = useCache ? res : []
personelTree.value = res
return personelTree.value
}
}
async function getOrganisationTree({ useCache = true, tenantGuid = '' } = {}) { // 获取组织树
if ( isNonEmptyArray(BasicInfo.organisationTree) && useCache) {
organisationTree.value = BasicInfo.organisationTree
return organisationTree.value
} else {
let res = await getOrganisationRelTreeListPromise(tenantGuid)
// getOrgtreeData(res,false)
BasicInfo.organisationTree = useCache ? res : []
organisationTree.value = res
return organisationTree.value
}
}
async function getAmoebaTree({ useCache = true, customParam = {} } = {}) { // 获取组织树
if ( isNonEmptyArray(BasicInfo.amoebaTree) && useCache) {
amoebaTree.value = BasicInfo.amoebaTree
return amoebaTree.value
} else {
let res = await tenantApi.getAmoebaTreePromise2(currentDate)
getOrgtreeData(res,false)
BasicInfo.amoebaTree = useCache ? res : []
amoebaTree.value = res
return amoebaTree.value
}
}
async function getFinanceDict({ useCache = true, customParam = {} } = {}) { // 获取财务科目关联字典
if ( isNonEmptyArray(BasicInfo.financeSubjectDict) && useCache) {
financeSubjectDict.value = BasicInfo.financeSubjectDict
return financeSubjectDict.value
} else {
let res = await budgetApi.getSubjectDict(customParam)
getOrgtreeData(res,false)
BasicInfo.financeSubjectDict = useCache ? res : []
financeSubjectDict.value = res
return financeSubjectDict.value
}
}
// 获取子系统列表
async function getSystemSideList({ useCache = true, customParam = {} } = {}) {
if ( isNonEmptyArray(BasicInfo.systemSideList) && useCache) {
systemSideList.value = BasicInfo.systemSideList
return systemSideList.value
} else {
let res = await authApi.getSystemSideData()
BasicInfo.systemSideList = useCache ? res : []
systemSideList.value = res
return systemSideList.value
}
}
return {
getPersonelTree,
getFinanceSubject,
getGradeTitleRelList,
getGradeList,
getPostionList,
getTenantRelList,
getFinanceSubjectTree,
getPermissionTemplateList,
getOrgMap,
getOrganisationTree,
getAmoebaTree,
getFinanceDict,
getSystemSideList
}
}
export default useGetData
// 获取传入的值的类型
const getValueType = (value: any) => {
const type = Object.prototype.toString.call(value)
return type.slice(8, -1)
}
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
const setStorage = (key: string, value: any) => {
const valueType = getValueType(value)
window[type].setItem(key, JSON.stringify({ type: valueType, value }))
}
const getStorage = (key: string) => {
const value = window[type].getItem(key)
if (value) {
const { value: val } = JSON.parse(value)
return val
} else {
return value
}
}
const removeStorage = (key: string) => {
window[type].removeItem(key)
}
const clear = (excludes?: string[]) => {
// 获取排除项
const keys = Object.keys(window[type])
const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter']
const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes
const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys
// 排除项不清除
excludesKeys.forEach((key) => {
window[type].removeItem(key)
})
// window[type].clear()
}
return {
setStorage,
getStorage,
removeStorage,
clear
}
}
......@@ -363,6 +363,19 @@ export const useValidator = () => {
}
}
const phone = (message?: string): FormItemRule => {
return {
validator: (_, val, callback) => {
if (val && !/^\d+(?:-\d+)?$/i.test(val)) {
callback(new Error(message || '电话格式不正确'));
} else {
callback();
}
},
trigger: 'blur'
};
};
const mobileNumber = (message?: string): FormItemRule => {
return {
validator: (_, val, callback) => {
......@@ -393,6 +406,7 @@ export const useValidator = () => {
validateDomainList,
minChineseCount,
email,
mobileNumber
mobileNumber,
phone
}
}
......
......@@ -22,6 +22,46 @@ const routes: RouteRecordRaw[] = [
},
}]
},
{
path: '/data-basic/user-manage',
component: Layout,
meta: {
title: '用户管理',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'userManagement',
component: () => import('@/views/data_basic/userManagement.vue'),
meta: {
title: '用户管理',
sidebar: false,
breadcrumb: false,
cache: true
},
},
{
path: 'user-edit',
name: 'addUser',
component: () => import('@/views/data_basic/addUser.vue'),
meta: {
title: '新增用户',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true,
editPage: true,
activeMenu: '/data-basic/user-manage'
},
beforeEnter: (to) => {
if (to.query.userName) {
to.meta.title = `编辑-${to.query.userName}`;
}
}
},
],
},
]
......
const useDataBasicStore = defineStore(
// api标签分类guid
'dataBasic',
() => {
const isUpdate = ref(false);
function setIsUpdate(update: boolean) {
isUpdate.value = update;
}
return {
isUpdate,
setIsUpdate,
}
},
)
export default useDataBasicStore
\ No newline at end of file
......@@ -41,6 +41,8 @@ declare module '@vue/runtime-core' {
PageHeader: typeof import('./../components/PageHeader/index.vue')['default']
PageMain: typeof import('./../components/PageMain/index.vue')['default']
PageNav: typeof import('./../components/PageNav/index.vue')['default']
PasswordStrengthContent: typeof import('./../components/PasswordStrengthMeter/PasswordStrengthContent.vue')['default']
PasswordStrengthMeter: typeof import('./../components/PasswordStrengthMeter/index.vue')['default']
PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default']
Popover: typeof import('./../components/Popover/index.vue')['default']
RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default']
......@@ -49,6 +51,7 @@ declare module '@vue/runtime-core' {
Schedule: typeof import('./../components/Schedule/index.vue')['default']
SearchBar: typeof import('./../components/SearchBar/index.vue')['default']
SecondAndMinute: typeof import('./../components/Schedule/component/secondAndMinute.vue')['default']
SelectPersonnel: typeof import('./../components/SelectPersonnel/src/SelectPersonnel.vue')['default']
SplitPane: typeof import('./../components/SplitPane/index.vue')['default']
StepBar: typeof import('./../components/StepBar/index.vue')['default']
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
......@@ -59,6 +62,7 @@ declare module '@vue/runtime-core' {
Table_search: typeof import('./../components/Tools/table_search.vue')['default']
Table_tools: typeof import('./../components/Tools/table_tools.vue')['default']
Table_v2: typeof import('./../components/Table/table_v2.vue')['default']
TableActions: typeof import('./../components/TablePlus/src/components/TableActions.vue')['default']
Tabs: typeof import('./../components/Tabs/index.vue')['default']
Toolbar: typeof import('./../components/LineageGraph/toolbar.vue')['default']
Topbar: typeof import('./../components/LineageGraph/topbar.vue')['default']
......
......@@ -1228,3 +1228,30 @@ export const getPublicIP = async (): Promise<string> => {
}
}
}
export const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
export const isNonEmptyArray = (val: any): boolean => {
return isArray(val) && val.length > 0;
}
/**
* 判断对象是否为空
* @param obj
* @param keyList 需要判断的keys 不传则判断所有属性
* @returns
*/
export const isAllPropertiesEmpty = (obj,keyList?: string[]) => {
const keys = keyList && keyList.length ? keyList : Object.keys(obj);
// 遍历每个属性
for (let key of keys) {
const value = obj[key];
if (value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0) || (typeof value === "object" && Object.keys(value).length === 0)) {
// 如果属性没有值,则返回false
return true;
}
}
// 如果所有属性都有值,则返回true
return false;
}
\ No newline at end of file
......
<template>
<div class="container_wrap full" v-loading="fullscreenLoading">
<div class="content_main panel">
<ContentWrap title="账户信息" expandSwicth style="margin-top: 15px">
<Form ref="formItem1" :itemList="schemaParam1" :rules="schemaParamRules" formId="model-select-edit"
col="col3" />
</ContentWrap>
<ContentWrap title="入职部门" expandSwicth style="margin-top: 15px">
<EntryOrgList ref="entryOrgListRef" :organisationJson="organisationInfo"></EntryOrgList>
</ContentWrap>
</div>
<div class="submit_btn_wrap">
<template v-for="(item, index) in schemaBtns" :key="index">
<ElButton v-if="item.visible" :type="item.type" @click="() => { if (item.event) item.event(item.param) }">
{{ item.label }}
</ElButton>
</template>
</div>
</div>
</template>
<script setup lang="ts" name="addUser">
import EntryOrgList from './components/EntryOrgList.vue'
import { unref, ref, computed, onBeforeMount, onMounted, onUnmounted } from 'vue'
import { useValidator } from '@/hooks/useValidator'
import useGetData from '@/hooks/useGetData';
import useUserStore from "@/store/modules/user";
import { getStaffDetail, getOrganisationTree, addorUpdateStaff } from '@/api/modules/dataBasic'
import useDataBasicStore from '@/store/modules/dataBasic';
const userStore = useUserStore();
const userData = JSON.parse(userStore.userData)
const dataBasicStore = useDataBasicStore();
// 全局状态和工具
const { getPermissionTemplateList } = useGetData()
const { proxy } = getCurrentInstance() as any;
const { required, phone, minChineseCount } = useValidator()
const route = useRoute()
const router = useRouter()
const fullPath = route.fullPath;
// 加载状态
const fullscreenLoading = ref(false)
// 响应式数据
const formItem1 = ref() // 账户信息表单引用
const password = ref('')
const detailData = ref<any>({}) // 员工详情数据
const organisationInfo = ref<any[]>([]) // 入职部门信息
const templateList = ref<any[]>([])
// 提交状态计算属性
const isDetail = computed((): boolean => route.query.isDetail === 'isDetail')
const isEdit = computed((): boolean => route.query.isEdit === 'isEdit')
const tenantGuid = computed((): string => userData.tenantGuid)
// 业务GUID计算属性
const bizGuid = computed((): string => route.query.guid as string)
/** 选择人员上级的列表 */
const schemaSatffTreeList: any = ref([]);
// 表单配置
const schemaParam1 = ref([
{
label: '员工编号',
type: 'input',
placeholder: '请输入',
field: 'staffNo',
maxlength: 50,
default: '',
required: false,
clearable: true,
visible: true,
},
{
label: '人员姓名',
type: 'input',
placeholder: '请输入',
field: 'staffName',
maxlength: 50,
default: '',
required: true,
clearable: true,
visible: true,
},
{
field: 'mobileNo',
label: '手机号',
type: 'input',
placeholder: '请输入',
maxlength: 50,
default: '',
required: true,
clearable: true,
visible: true,
},
{
label: '密码',
type: 'password-len-meter',
placeholder: '请输入',
field: 'password',
default: '',
clearable: true,
required: true,
autocompleteSetting: {
autocomplete: "one-time-code",
},
},
{
label: "功能权限",
type: "select",
placeholder: "请选择",
field: "funcPermissionTemplateJson",
options: templateList.value,
props: {
label: 'templateName',
value: 'guid'
},
default: [],
multiple: true,
collapse: true,
tagsTooltip: true,
filterable: true,
clearable: true,
required: true,
},
{
label: '账号状态',
type: 'switch',
field: 'bizState',
default: 'Y',
placeholder: '请选择',
activeValue: 'Y',
inactiveValue: 'S',
switchWidth: 32,
required: true,
},
{
label: '直接上级',
type: 'tree-select',
placeholder: '请选择',
field: 'leaderGuid',
nodeKey: 'value',
options: schemaSatffTreeList.value,
checkStrictly: false,//只能选择叶子节点。
lazy: false,
multiple: false,
props: {
label: "name",
value: "guid",
children: 'children'
},
customInfo: {
type: 'prefixIcon'
},
filterable: true,
clearable: true,
default: '',
required: false,
},
// {
// field: 'leaderName',
// label: '直接上级:',
// type: 'input',
// placeholder: '请选择',
// required: false,
// clearable: true,
// // componentProps: {
// // readonly: true,
// // on: {
// // click: (val: any) => {
// // schemaSatff.value.visible = true
// // operate.value = '直接上级'
// // }
// // },
// // formItemProps: {}
// // }
// },
// {
// field: 'leaderGuid',
// label: '直接上级:',
// type: 'input',
// visible: false
// }
])
const schemaParamRules = ref({
staffName: [required(), minChineseCount()],
mobileNo: [required(), phone()],
funcPermissionTemplateJson: [{ type: 'array', required: true, trigger: 'change', message: "请选择功能权限" }],
password: [{
required: true,
message: "该项为必填项",
trigger: "blur",
},
{
validator: (rule: any, value: any, callback: any) => {
let getStorage = (key) => {
const value = window['sessionStorage'].getItem(key);
if (value) {
const { value: val } = JSON.parse(value);
return val;
} else {
return value;
}
};
if (!value) {
callback();
// callback(new Error('该项为必填项'));
} else if (getStorage('passwordLenMeter' + 'pwd')) {
callback(new Error(getStorage('passwordLenMeter' + 'pwd')));
} else {
callback();
}
},
trigger: ['change', 'blur']
}],
});
// 页面按钮配置
const schemaBtns = computed(() => {
return <any>[
{ visible: true, type: "", label: "关闭", value: "cancel", event: cancel, param: "" },
// { type: "primary", visible: !unref(isEdit) && !unref(isDetail), label: "提交并添加下一个", value: "cancel", event: submitForm, param: "submitNext" },
{ type: "primary", visible: !unref(isDetail), label: "提交", value: "cancel", event: submitHandle, param: "submit" }
]
})
const entryOrgListRef = ref();
const submitHandle = async () => {
formItem1.value.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
let resultForm1 = formItem1.value.formInline;
let orgOne = organisationInfo.value.find(item => item.isMainOrg == 'Y')
if (!orgOne) {
proxy.$message.warning('请选择主责部门')
return
}
let result = entryOrgListRef.value.validateData();
if (!result) {
return;
}
let submitParam: any = {
staffName: resultForm1.staffName,
leaderGuid: resultForm1.leaderGuid,
staffMain: {
...resultForm1,
staffNo: resultForm1.staffNo || null,
userGuid: detailData.value.userGuid,
guid: unref(bizGuid) || '',
tenantGuid: tenantGuid.value,
},
organisationInfo: organisationInfo.value
}
let message = '提交成功'
let param = {
guid: unref(bizGuid) || '',
...submitParam
}
addorUpdateStaff(param).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage({
type: "success",
message: message,
});
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: "userManagement",
});
dataBasicStore.setIsUpdate(true);
} else {
res?.msg && proxy.$ElMessage({
type: "error",
message: res.msg,
});
}
})
} else {
}
});
}
const cancel = () => {
if (isDetail.value) {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'userManagement'
});
return;
}
proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'userManagement'
});
}, () => {
proxy.$ElMessage.info("已取消");
});
}
// 获取详情
async function getDetail() {
if (!unref(bizGuid)) return
fullscreenLoading.value = true
try {
let res: any = await getStaffDetail(unref(bizGuid))
if (res?.code == proxy.$passCode) {
detailData.value = res.data || {}
setDetail()
}
} catch (error) {
console.error('获取详情失败:', error)
proxy.$ElMessage.error('获取详情失败')
} finally {
fullscreenLoading.value = false
}
}
function setDetail() {
let funcPermissionTemplateJson = detailData.value.funcPermissionTemplateJson
let formData = {
funcPermissionTemplateJson,
mobileNo: detailData.value.mobileNo,
staffName: detailData.value.staffName,
staffNo: detailData.value.staffNo,
bizState: detailData.value.bizState,
leaderGuid: detailData.value.leaderGuid,
leaderName: detailData.value.leaderName,
}
nextTick(() => {
formItem1.value.setValue(formData)
organisationInfo.value = detailData.value.staffOrgRelRSVOList || []
})
}
// 初始化权限模板列表
const initPermissionTemplates = async () => {
try {
const res = await getPermissionTemplateList({
useCache: false,
customParam: {
tenantGuid: tenantGuid.value
}
})
templateList.value = res || [];
let item = schemaParam1.value.find(item => item.field === 'funcPermissionTemplateJson');
item && (item.options = templateList.value);
} catch (error) {
console.error('获取权限模板失败:', error)
}
}
// 生命周期钩子
onBeforeMount(() => {
initPermissionTemplates()
if (unref(bizGuid)) {
getDetail()
}
// 组件挂载前的初始化逻辑
getOrganisationTree(tenantGuid.value).then((res: any) => {
if (res?.code == proxy.$passCode) {
schemaSatffTreeList.value = res.data || [];
let item = schemaParam1.value.at(-1);
item && (item.options = schemaSatffTreeList.value);
} else {
proxy.$ElMessage({
type: 'error',
message: res?.msg || '获取组织人员树列表失败',
})
}
})
})
onMounted(() => {
})
onUnmounted(() => {
// 组件销毁时的清理工作
})
</script>
<style lang="scss" scoped>
.add-bank-item {
height: 40px;
width: 100%;
background-color: #FAFAFA;
border: 1px solid rgba(229, 229, 229, 1);
border-radius: 2px;
text-align: center;
line-height: 40px;
color: #999999;
padding-bottom: 10px;
margin-top: 5px;
}
.add-bank-item:hover {
background: #EBF6F7;
border: 1px solid rgba(79, 161, 164, 1);
}
.container_wrap {
overflow: hidden;
.content_main {
height: calc(100% - 45px);
overflow: hidden auto;
&.panel {
padding: 0 16px 16px;
}
}
}
.submit_btn_wrap {
height: 44px;
margin: 0 -8px;
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid #d9d9d9;
}
</style>
\ No newline at end of file
<template>
<div class="entry-org-container">
<el-table ref="tableRef" class="entry-org-table" :data="tableData" :height="tableHeight"
:highlight-current-row="true" stripe tooltip-effect="light" border>
<el-table-column label="序号" width="56" align="center" fixed="left" :formatter="formatIndex" />
<el-table-column prop="organisationName" label="所属部门" :width="columnWidths.organisationName" align="left"
:show-overflow-tooltip="true">
<template #header>
<span>所属部门</span>
<span style="color:red;margin-left: 2px;">*</span>
</template>
<template #default="scope">
<el-tree-select v-if="!isDetail" v-model="scope.row.organisationGuid" placeholder="请选择" :data="organisationList"
@node-click="(node, nodeObj) => handleTreeSelectNodeChange(node, nodeObj, scope)"
:props="{
value: 'guid',
label: 'name',
children: 'children'
}" clearable filterable :checkStrictly="true" :showAllLevels="false" class="input-width">
</el-tree-select>
<!-- <el-input v-if="!isDetail" v-model="scope.row.organisationName" placeholder="请选择" readonly
@click="openOrgSelector(scope.$index)"></el-input> -->
<span v-else>{{ scope.row.organisationName || '--' }}</span>
</template>
</el-table-column>
<el-table-column prop="isLeader" label="是否部门负责人" :width="columnWidths.isLeader" align="left"
:show-overflow-tooltip="true">
<template #header>
<span>是否部门负责人</span>
<span style="color:red;margin-left: 2px;">*</span>
</template>
<template #default="scope">
<el-select v-if="!isDetail" v-model="scope.row.isLeader" placeholder="请选择" clearable filterable>
<el-option v-for="opt in yesNoOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<span v-else>{{ scope.row.isLeader == 'Y' ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column prop="isMainOrg" label="是否主责部门" :width="columnWidths.isMainOrg" align="left"
:show-overflow-tooltip="true">
<template #header>
<span>是否主责部门</span>
<span style="color:red;margin-left: 2px;">*</span>
</template>
<template #default="scope">
<el-select v-if="!isDetail" v-model="scope.row.isMainOrg" placeholder="请选择" clearable filterable
@change="handleMainOrgChange(scope.row)">
<el-option v-for="opt in yesNoOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<span v-else>{{ scope.row.isMainOrg == 'Y' ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column v-if="!isDetail" label="操作" width="140" align="left" fixed="right">
<template #default="scope">
<span class="text_btn" @click="deleteItem(scope)" v-preReClick>删除</span>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="!isDetail" class="add-btn-container">
<el-button link @click="addNewRow" :icon="CirclePlus" v-preReClick>
新增入职部门
</el-button>
</div>
</template>
<script lang="ts" setup name="EntryOrgList">
import { CirclePlus } from "@element-plus/icons-vue";
import {
getOrganisationRelTreeList
} from '@/api/modules/dataBasic';
import { isAllPropertiesEmpty } from '@/utils/common';
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const userData = JSON.parse(userStore.userData)
const { proxy } = getCurrentInstance() as any;
// Props 定义
const props = defineProps({
disabled: {
type: Boolean,
default: false
},
organisationJson: {
type: Array as PropType<any[]>,
default: () => []
},
projectDate: {
type: Array as PropType<any[]>,
default: () => []
},
});
// 响应式数据
const tableRef = ref();
const currentIndex = ref<number>(0);
const organisationList = ref<any[]>([]);
const yesNoOptions = ref<any[]>([]);
// 表格数据计算属性
const tableData = computed(() => {
return props.organisationJson;
});
// 详情状态计算属性
const isDetail = computed((): boolean => {
return proxy.$route.query.isDetail || props.disabled;
});
// 表格高度计算
const tableHeight = computed(() => {
return 'auto';
});
// 列宽配置
const columnWidths = computed(() => {
return {
organisationName: '180px',
isLeader: '140px',
isMainOrg: '140px'
};
});
// 初始化数据
const initializeData = async () => {
try {
// 获取是否字典选项
yesNoOptions.value = [{
value: 'Y',
label: '是'
}, {
value: 'N',
label: '否'
}];
// 获取组织列表
await getOrganisationList();
} catch (error) {
console.error('初始化数据失败:', error);
}
};
// 获取组织列表
const getOrganisationList = async () => {
try {
const res: any = await getOrganisationRelTreeList({
tenantGuid: userData.tenantGuid
});
organisationList.value = res?.data || [];
} catch (error) {
console.error('获取组织列表失败:', error);
}
};
// 格式化序号
const formatIndex = (row: any, column: any, cellValue: any, index: number) => {
return String(index + 1);
};
// 处理主责部门变更
const handleMainOrgChange = (row: any) => {
// 检查是否已经有主责部门
const mainOrgCount = tableData.value.filter(item => item.isMainOrg === 'Y').length;
if (mainOrgCount > 1) {
row.isMainOrg = null;
proxy.$message.warning('只能选择一个主责部门');
}
};
// 添加新行
const addNewRow = () => {
// 检查最后一行是否填写完整
if (tableData.value.length > 0) {
const lastItem = tableData.value[tableData.value.length - 1];
const isEmpty = isAllPropertiesEmpty(lastItem, ['organisationGuid', 'isLeader', 'isMainOrg']);
if (isEmpty) {
proxy.$message.warning('请填写完整');
return;
}
}
unref(tableData).push({});
};
// 删除行
const deleteItem = (scope: any) => {
if (isDetail.value) return;
proxy.$openMessageBox('确定删除吗?', () => {
unref(tableData).splice(scope.$index, 1);
proxy.$ElMessage.success('删除成功');
}, () => {
proxy.$ElMessage.info('已取消删除');
});
};
const currentTreeNode = ref({});
const handleTreeSelectNodeChange = (node, nodeObj, scope) => {
currentTreeNode.value = node;
scope.row.organisationName = node.name;
}
// 验证数据
const validateData = (): boolean => {
const guidSet = new Set();
for (const item of tableData.value) {
if (!item.organisationGuid) {
proxy.$ElMessage.error('所属部门必填,请填写完整');
return false;
}
if (!item.isLeader) {
proxy.$ElMessage.error('是否部门负责人必填,请填写完整');
return false;
}
if (!item.isMainOrg) {
proxy.$ElMessage.error('是否主责部门必填,请填写完整');
return false;
}
if (guidSet.has(item.organisationGuid)) {
proxy.$ElMessage.error('部门不能重复');
return false;
}
guidSet.add(item.organisationGuid);
}
return true;
};
// 暴露方法
defineExpose({
tableData,
validateData,
tableRef
});
// 生命周期钩子
onBeforeMount(() => {
initializeData();
});
onMounted(() => {
// 组件挂载后的逻辑
});
</script>
<style lang="scss" scoped>
.entry-org-container {
.entry-org-table {
:deep(.el-table__row) {
.el-input.is-disabled .el-input__inner {
background-color: #f5f7fa;
cursor: not-allowed;
}
}
}
}
.add-btn-container {
.el-button--default {
padding: 4px 0px;
margin-top: 4px;
}
:deep(.el-icon) {
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
}
}
</style>
\ No newline at end of file
<route lang="yaml">
# 用户管理
name: userManagement
</route>
<template>
<div class="container_wrap full flex">
<!-- 侧边tree组件 start -->
<div class="aside_wrap">
<div class="aside_title">{{ '企业组织列表' }}</div>
<Tree :treeInfo="treeInfo" @nodeClick="nodeClick" />
</div>
<!-- 侧边tree组件 end -->
<!-- 列表区域 start -->
<div class="main_wrap">
<!-- 头部搜索和工具栏 -->
<div class="table_tool_wrap">
<div class="aside_title">
{{ title }}
</div>
<TableTools :searchItems="searchItemList" :searchId="'user-manage-search'" @search="toSearch" :init="false" />
<div class="tools_btns">
<el-button type="primary" @click="handleCreate">新增用户</el-button>
</div>
</div>
<!-- 表格展示 -->
<div class="table_panel_wrap">
<Table :tableInfo="tableInfo" @tablePageChange="tablePageChange" />
</div>
</div>
<!-- 列表区域 end -->
</div>
</template>
<script lang="ts" setup name="userManagement">
import { ref, computed } from "vue";
import TableTools from "@/components/Tools/table_tools.vue";
import { TableColumnWidth } from "@/utils/enum";
import { commonPageConfig } from '@/components/PageNav/index';
import useGetData from '@/hooks/useGetData'
import {
getStaff,
removeStaff,
resetPwd2,
} from '@/api/modules/dataBasic';
import Moment from 'moment';
import useUserStore from "@/store/modules/user";
import useDataBasicStore from "@/store/modules/dataBasic";
const userStore = useUserStore();
const userData = JSON.parse(userStore.userData)
const dataBasicStore = useDataBasicStore();
const { getOrganisationTree } = useGetData()
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance() as any;
const tenantGuid = computed(() => userData.tenantGuid)
const tenantName = computed(() => userData.tenantName)
// 搜索配置
const searchItemList = ref([
{
type: "input",
label: "",
field: "staffName",
default: "",
placeholder: "姓名",
maxlength: 50,
clearable: true,
},
{
type: "input",
label: "",
field: "mobileNo",
default: "",
placeholder: "手机号",
maxlength: 50,
clearable: true,
},
{
type: "select",
label: "",
field: "bizState",
default: "",
options: [{
value: 'Y',
label: '启用'
}, {
value: 'S',
label: '停用'
}],
placeholder: "启用状态",
clearable: true,
}
]);
// 分页配置
const page = ref({
...commonPageConfig,
staffName: '',
mobileNo: '',
bizState: ''
});
const currentRow: any = ref({});
// 表格配置
const tableInfo = ref({
id: 'user-table',
rowKey: 'guid',
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "姓名", field: "staffName", width: 120 },
{ label: "手机号", field: "mobileNo", width: 140 },
{ label: "员工编号", field: "staffNo", width: 140 },
{
label: "所属部门",
field: "organisationNameList",
width: 180,
getName: (scope) => {
const cellValue = scope.row.organisationNameList;
return Array.isArray(cellValue) && cellValue.length > 0 ? cellValue.join('、') : '-';
}
},
{
label: "权限模板",
field: "funcPermissionTemplateJson",
width: 180,
getName: (scope) => {
const cellValue = scope.row.funcPermissionTemplateJson;
return Array.isArray(cellValue) && cellValue.length > 0 ? cellValue.map(item => item.name).join('、') : '-';
}
},
{
label: "状态",
field: "bizState",
width: TableColumnWidth.STATE,
align: 'center',
type: "tag",
getName: (scope) => {
return scope.row.bizState === 'Y' ? '启用' : '停用';
}
},
{ label: "创建人", field: "createUserName", width: 140 },
{
label: "创建时间",
field: "createTime",
width: TableColumnWidth.DATETIME,
getName: (scope) => {
return scope.row.createTime ? Moment(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') : '--';
}
},
],
data: [],
page: {
type: "normal",
rows: 0,
...page.value,
},
loading: false,
actionInfo: {
label: "操作",
type: "btn",
width: 210,
fixed: 'right',
btns: (scope) => {
return [
{ label: "详情", value: "detail", click: toDetail },
{ label: "编辑", value: "edit", click: toEdit },
{ label: "重置密码", value: "resetPwd", click: resetPassword },
{ label: "删除", value: "delete", click: toDelete }
];
}
}
})
const title = computed(() => {
return currentRow.value.name ? `${currentRow.value.name}(${tableInfo.value.data.length}人)` : ''
})
const organisationGuidList = ref<any[]>([])
// 搜索方法
const toSearch = (val: any = {}, clear: boolean = false) => {
if (clear) {
searchItemList.value.map(item => (item.default = ""));
page.value.staffName = '';
page.value.mobileNo = '';
page.value.bizState = '';
} else {
page.value.staffName = val.staffName || '';
page.value.mobileNo = val.mobileNo || '';
page.value.bizState = val.bizState || '';
}
getTableData();
};
// 获取表格数据
const getTableData = () => {
tableInfo.value.loading = true;
const params = {
pageIndex: page.value.curr,
pageSize: page.value.limit,
staffName: page.value.staffName,
mobileNo: page.value.mobileNo,
bizState: page.value.bizState,
organisationGuidList: organisationGuidList.value,
tenantGuid: unref(tenantGuid),
};
getStaff(params).then((res: any) => {
if (res?.code == proxy.$passCode) {
const data = res.data || {};
tableInfo.value.data = data.records ?? [];
tableInfo.value.page.limit = data.pageSize
tableInfo.value.page.curr = data.pageIndex
tableInfo.value.page.rows = data.totalRows
// 总数需要通过其他接口获取或者在返回数据中包含
} else {
proxy.$ElMessage({
type: 'error',
message: res?.msg || '获取数据失败',
})
}
tableInfo.value.loading = false;
}).catch((err) => {
tableInfo.value.loading = false;
});
}
// 分页变化
const tablePageChange = (info) => {
page.value.curr = Number(info.curr);
page.value.limit = Number(info.limit);
tableInfo.value.page.curr = page.value.curr;
tableInfo.value.page.limit = page.value.limit;
getTableData();
};
// 操作方法
const toDetail = (scope) => {
router.push({
name: 'addUser',
query: {
guid: scope.row.guid,
isDetail: 'isDetail',
userName: scope.row.staffName
}
})
}
const toEdit = (scope) => {
router.push({
name: 'addUser',
query: {
guid: scope.row.guid,
isEdit: 'isEdit',
userName: scope.row.staffName
}
})
}
const resetPassword = (scope) => {
proxy.$openMessageBox("确定是重置密码?", "warning", () => {
resetPwd2([scope.row.userGuid]).then((res: any) => {
if (res?.code === proxy.$passCode) {
proxy.$ElMessage({
type: 'success',
message: '重置成功,密码为:YzU&+66w'
})
} else {
proxy.$ElMessage({
type: 'error',
message: res?.msg || '重置密码失败',
})
}
})
})
}
const toDelete = (scope) => {
proxy.$openMessageBox("数据删除后不可恢复,确定是否删除?", "warning", () => {
removeStaff([scope.row.guid]).then((res: any) => {
if (res?.code === proxy.$passCode) {
proxy.$ElMessage({
type: 'success',
message: '删除成功'
})
getTableData();
} else {
proxy.$ElMessage({
type: 'error',
message: res?.msg || '删除失败',
})
}
})
})
}
// 新增用户
const handleCreate = () => {
router.push({
name: 'addUser',
query: {}
})
}
const expandedKey: any = ref([]);
const currentNodeKey = ref('');
// 获取公司部门树
const getTenantList = async () => {
let orgTree = await getOrganisationTree({
useCache: false,
tenantGuid: tenantGuid.value
})
let tree = [{
guid: tenantGuid.value,
name: tenantName.value,
id: tenantGuid.value,
label: tenantName.value,
nodeType: 'tenant',
children: orgTree
}]
treeInfo.value.data = tree
currentRow.value = tree[0] || {}
expandedKey.value = expandedKey.value.length == 0 ? [tree[0].guid] : expandedKey.value
currentNodeKey.value = currentNodeKey.value == '' ? tree[0].guid : currentNodeKey.value
treeInfo.value.currentNodeKey = tree[0]?.guid;
nextTick(() => {
treeInfo.value.currentNodeKey = currentNodeKey.value
treeInfo.value.expandedKey = expandedKey.value
})
toSearch({})
}
// 树节点点击
const nodeClick = (node) => {
currentRow.value = node
let children = node.children || []
let guids = children.map(item => item.guid)
guids.push(node.guid)
organisationGuidList.value = node.$treeNodeId == 1 ? [] : guids
page.value.curr = 1;
toSearch({})
}
const treeInfo = ref<any>({
filter: true,
defaultExpandAll: true,
queryValue: "",
currentNodeKey: '',
expandOnNodeClick: false,
queryPlaceholder: "输入关键字搜索",
props: {
label: "name",
value: "guid",
},
nodeKey: 'guid',
expandedKey: [],
data: [],
});
onActivated(() => {
if (dataBasicStore.isUpdate) {//如果是首次加载,则不需要调用
page.value.curr = 1;
toSearch({})
dataBasicStore.setIsUpdate(false);
}
})
onMounted(() => {
getTenantList()
});
</script>
<style lang="scss" scoped>
.container_wrap {
.aside_wrap {
width: 200px;
}
.main_wrap {
width: calc(100% - 200px);
.table_tool_wrap {
width: 100%;
// height: 84px !important;
// padding: 0 8px;
:deep(.tools_search) {
padding: 0px;
}
.tools_btns {
padding: 0px 0 8px;
}
}
.table_panel_wrap {
width: 100%;
height: calc(100% - 112px);
// padding: 0px 8px 8px;
}
}
.tree_panel {
height: calc(100% - 36px);
padding-top: 0;
:deep(.el-tree) {
margin: 0;
overflow: hidden auto;
}
}
}
.aside_title {
font-size: 14px;
color: var(--el-color-regular);
height: 36px;
line-height: 36px;
font-weight: 600;
}
</style>
\ No newline at end of file
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!