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="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
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!