用户管理
Showing
16 changed files
with
1147 additions
and
2 deletions
| ... | @@ -41,6 +41,8 @@ declare module '@vue/runtime-core' { | ... | @@ -41,6 +41,8 @@ declare module '@vue/runtime-core' { |
| 41 | PageHeader: typeof import('./src/components/PageHeader/index.vue')['default'] | 41 | PageHeader: typeof import('./src/components/PageHeader/index.vue')['default'] |
| 42 | PageMain: typeof import('./src/components/PageMain/index.vue')['default'] | 42 | PageMain: typeof import('./src/components/PageMain/index.vue')['default'] |
| 43 | PageNav: typeof import('./src/components/PageNav/index.vue')['default'] | 43 | PageNav: typeof import('./src/components/PageNav/index.vue')['default'] |
| 44 | PasswordStrengthContent: typeof import('./src/components/PasswordStrengthMeter/PasswordStrengthContent.vue')['default'] | ||
| 45 | PasswordStrengthMeter: typeof import('./src/components/PasswordStrengthMeter/index.vue')['default'] | ||
| 44 | PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default'] | 46 | PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default'] |
| 45 | Popover: typeof import('./src/components/Popover/index.vue')['default'] | 47 | Popover: typeof import('./src/components/Popover/index.vue')['default'] |
| 46 | RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default'] | 48 | RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default'] |
| ... | @@ -49,6 +51,7 @@ declare module '@vue/runtime-core' { | ... | @@ -49,6 +51,7 @@ declare module '@vue/runtime-core' { |
| 49 | Schedule: typeof import('./src/components/Schedule/index.vue')['default'] | 51 | Schedule: typeof import('./src/components/Schedule/index.vue')['default'] |
| 50 | SearchBar: typeof import('./src/components/SearchBar/index.vue')['default'] | 52 | SearchBar: typeof import('./src/components/SearchBar/index.vue')['default'] |
| 51 | SecondAndMinute: typeof import('./src/components/Schedule/component/secondAndMinute.vue')['default'] | 53 | SecondAndMinute: typeof import('./src/components/Schedule/component/secondAndMinute.vue')['default'] |
| 54 | SelectPersonnel: typeof import('./src/components/SelectPersonnel/src/SelectPersonnel.vue')['default'] | ||
| 52 | SplitPane: typeof import('./src/components/SplitPane/index.vue')['default'] | 55 | SplitPane: typeof import('./src/components/SplitPane/index.vue')['default'] |
| 53 | StepBar: typeof import('./src/components/StepBar/index.vue')['default'] | 56 | StepBar: typeof import('./src/components/StepBar/index.vue')['default'] |
| 54 | SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default'] | 57 | SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default'] |
| ... | @@ -59,6 +62,7 @@ declare module '@vue/runtime-core' { | ... | @@ -59,6 +62,7 @@ declare module '@vue/runtime-core' { |
| 59 | Table_search: typeof import('./src/components/Tools/table_search.vue')['default'] | 62 | Table_search: typeof import('./src/components/Tools/table_search.vue')['default'] |
| 60 | Table_tools: typeof import('./src/components/Tools/table_tools.vue')['default'] | 63 | Table_tools: typeof import('./src/components/Tools/table_tools.vue')['default'] |
| 61 | Table_v2: typeof import('./src/components/Table/table_v2.vue')['default'] | 64 | Table_v2: typeof import('./src/components/Table/table_v2.vue')['default'] |
| 65 | TableActions: typeof import('./src/components/TablePlus/src/components/TableActions.vue')['default'] | ||
| 62 | Tabs: typeof import('./src/components/Tabs/index.vue')['default'] | 66 | Tabs: typeof import('./src/components/Tabs/index.vue')['default'] |
| 63 | Toolbar: typeof import('./src/components/LineageGraph/toolbar.vue')['default'] | 67 | Toolbar: typeof import('./src/components/LineageGraph/toolbar.vue')['default'] |
| 64 | Topbar: typeof import('./src/components/LineageGraph/topbar.vue')['default'] | 68 | Topbar: typeof import('./src/components/LineageGraph/topbar.vue')['default'] | ... | ... |
| 1 | import request from "@/utils/request"; | 1 | import request from "@/utils/request"; |
| 2 | import { ElMessage } from "element-plus"; | ||
| 2 | 3 | ||
| 3 | /** 获取租户列表(分页) */ | 4 | /** 获取租户列表(分页) */ |
| 4 | export const getTenantSinglePage = (params) => request({ | 5 | export const getTenantSinglePage = (params) => request({ |
| ... | @@ -70,6 +71,17 @@ export const removeOrganisation = (guids) => request({ | ... | @@ -70,6 +71,17 @@ export const removeOrganisation = (guids) => request({ |
| 70 | 71 | ||
| 71 | 72 | ||
| 72 | /** | 73 | /** |
| 74 | * 获取部门列表 | ||
| 75 | * @param param | ||
| 76 | * @returns | ||
| 77 | */ | ||
| 78 | export const getOrganisationListApi = (params) => request({ | ||
| 79 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/singlePage`, | ||
| 80 | method: 'post', | ||
| 81 | data: params | ||
| 82 | }) | ||
| 83 | |||
| 84 | /** | ||
| 73 | * 修改部门关系 | 85 | * 修改部门关系 |
| 74 | * @param param | 86 | * @param param |
| 75 | * @returns | 87 | * @returns |
| ... | @@ -91,3 +103,117 @@ export const addOrganisation = (params) => request({ | ... | @@ -91,3 +103,117 @@ export const addOrganisation = (params) => request({ |
| 91 | data: params | 103 | data: params |
| 92 | }); | 104 | }); |
| 93 | 105 | ||
| 106 | /** | ||
| 107 | * 获取人员列表 | ||
| 108 | * @param param | ||
| 109 | * @returns | ||
| 110 | */ | ||
| 111 | export const getStaff = (params) => request({ | ||
| 112 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/singlePage`, | ||
| 113 | method: 'post', | ||
| 114 | data: params | ||
| 115 | }); | ||
| 116 | |||
| 117 | /** | ||
| 118 | * 删除人员 | ||
| 119 | * @param guids:string[] | ||
| 120 | * @returns | ||
| 121 | */ | ||
| 122 | export const removeStaff = (guids:string[]) => request({ | ||
| 123 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/removeListByGuids`, | ||
| 124 | method: 'delete', | ||
| 125 | data: guids | ||
| 126 | }) | ||
| 127 | |||
| 128 | /** | ||
| 129 | * !获取部门关系树(不带人员) | ||
| 130 | * @param tenantGuid | ||
| 131 | * @returns | ||
| 132 | */ | ||
| 133 | export const getOrganisationRelTreeListPromise = (tenantGuid:string):Promise<any> => { | ||
| 134 | let organisationTree:any[] = [] | ||
| 135 | let param = { | ||
| 136 | tenantGuid, | ||
| 137 | bizState: "Y" | ||
| 138 | } | ||
| 139 | return new Promise((resolve, reject) => { | ||
| 140 | getOrganisationRelTreeList(param).then((res: any) => { | ||
| 141 | if (res?.code === '00000') { | ||
| 142 | organisationTree = res.data || [] | ||
| 143 | } else { | ||
| 144 | res?.msg && ElMessage.error(res?.msg) | ||
| 145 | } | ||
| 146 | resolve(organisationTree) | ||
| 147 | }) | ||
| 148 | }) | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * 获取部门关系列表tree | ||
| 153 | * @param param | ||
| 154 | * @returns | ||
| 155 | */ | ||
| 156 | export const getOrganisationRelTreeList = (param:{tenantGuid:string}) => request({ | ||
| 157 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/tree-list`, | ||
| 158 | method: 'post', | ||
| 159 | data: param | ||
| 160 | }); | ||
| 161 | |||
| 162 | export const resetPwd2 = (userGuid) => request({ | ||
| 163 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/user/data/resetPwd?userGuid=${userGuid}`, | ||
| 164 | method: 'put' | ||
| 165 | }); | ||
| 166 | |||
| 167 | |||
| 168 | // 模板列表 | ||
| 169 | export const getPermissionTemplateList = (param) => request({ | ||
| 170 | url: `${import.meta.env.VITE_APP_AUTH_URL}/func-permission-template/page-list`, | ||
| 171 | method: 'post', | ||
| 172 | data: param | ||
| 173 | }); | ||
| 174 | |||
| 175 | export const getTemplateListPromise = (customParam?: {}): Promise<any> => { | ||
| 176 | let list = [] | ||
| 177 | let param = customParam ? Object.assign({}, { pageIndex: 1, pageSize: -1, bizState: 'Y' }, customParam) : { pageIndex: 1, pageSize: -1, bizState: 'Y' } | ||
| 178 | return new Promise((resolve, reject) => { | ||
| 179 | getPermissionTemplateList(param).then((res: any) => { | ||
| 180 | if (res?.code === '00000') { | ||
| 181 | list = res?.data?.records || [] | ||
| 182 | } else { | ||
| 183 | res?.msg && ElMessage.error(res?.msg) | ||
| 184 | } | ||
| 185 | resolve(list) | ||
| 186 | }) | ||
| 187 | }) | ||
| 188 | } | ||
| 189 | |||
| 190 | /** | ||
| 191 | * 获取人员详情 | ||
| 192 | * @param guid:string | ||
| 193 | * @returns | ||
| 194 | */ | ||
| 195 | export const getStaffDetail = (guid: string) => request({ | ||
| 196 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/getByGuid/${guid}`, | ||
| 197 | method: 'get' | ||
| 198 | }); | ||
| 199 | |||
| 200 | /** | ||
| 201 | * 获取组织人员树 | ||
| 202 | * @param guids | ||
| 203 | * @returns | ||
| 204 | */ | ||
| 205 | export const getOrganisationTree = (tenantGuid:string) => request({ | ||
| 206 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/organisation/get-tenant-organisation-staff-tree?tenantGuid=${tenantGuid}`, | ||
| 207 | method: 'get' | ||
| 208 | }); | ||
| 209 | |||
| 210 | /** | ||
| 211 | * 新增修改人员基本信息 | ||
| 212 | * @param param | ||
| 213 | * @returns | ||
| 214 | */ | ||
| 215 | export const addorUpdateStaff = (param) => request({ | ||
| 216 | url: `${import.meta.env.VITE_APP_PERSONAL_URL}/staff/save-or-update`, | ||
| 217 | method: 'put', | ||
| 218 | data: param | ||
| 219 | }); | ... | ... |
src/assets/icons/file-person.svg
0 → 100644
| 1 | <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 | ... | \ No newline at end of file |
| ... | @@ -34,6 +34,7 @@ import { | ... | @@ -34,6 +34,7 @@ import { |
| 34 | getPathUrl | 34 | getPathUrl |
| 35 | } from "@/api/modules/obsService"; | 35 | } from "@/api/modules/obsService"; |
| 36 | import { Editor, EditorExpose } from '@/components/Editor' | 36 | import { Editor, EditorExpose } from '@/components/Editor' |
| 37 | import PasswordStrengthMeter from "../PasswordStrengthMeter/index.vue"; | ||
| 37 | 38 | ||
| 38 | const userStore = useUserStore() | 39 | const userStore = useUserStore() |
| 39 | 40 | ||
| ... | @@ -51,6 +52,7 @@ const emits = defineEmits([ | ... | @@ -51,6 +52,7 @@ const emits = defineEmits([ |
| 51 | "uploadFileChange", | 52 | "uploadFileChange", |
| 52 | "scheduleChange", | 53 | "scheduleChange", |
| 53 | "inputAppendClick", | 54 | "inputAppendClick", |
| 55 | "inputClick", | ||
| 54 | "cascaderChange" | 56 | "cascaderChange" |
| 55 | ]); | 57 | ]); |
| 56 | const props = defineProps({ | 58 | const props = defineProps({ |
| ... | @@ -88,7 +90,6 @@ const formRules = computed(() => { | ... | @@ -88,7 +90,6 @@ const formRules = computed(() => { |
| 88 | return reactive<FormRules>(props.rules); | 90 | return reactive<FormRules>(props.rules); |
| 89 | }); | 91 | }); |
| 90 | const formItemList: any = computed(() => { | 92 | const formItemList: any = computed(() => { |
| 91 | const readonly = props.readonly ?? false; | ||
| 92 | let itemList = props.itemList ?? []; | 93 | let itemList = props.itemList ?? []; |
| 93 | // return setItemsDisabled(itemList, readonly); | 94 | // return setItemsDisabled(itemList, readonly); |
| 94 | return itemList; | 95 | return itemList; |
| ... | @@ -293,6 +294,7 @@ const handlerIptClick = (row) => { | ... | @@ -293,6 +294,7 @@ const handlerIptClick = (row) => { |
| 293 | ) { | 294 | ) { |
| 294 | row.autocompleteSetting.readonly = false; | 295 | row.autocompleteSetting.readonly = false; |
| 295 | } | 296 | } |
| 297 | emits("inputClick", row, formInline.value) | ||
| 296 | }; | 298 | }; |
| 297 | 299 | ||
| 298 | const toolBtnClick = (btn) => { | 300 | const toolBtnClick = (btn) => { |
| ... | @@ -614,6 +616,12 @@ const panelChange = (scope, row) => { | ... | @@ -614,6 +616,12 @@ const panelChange = (scope, row) => { |
| 614 | } | 616 | } |
| 615 | } | 617 | } |
| 616 | 618 | ||
| 619 | const handlePwdLenMeterChange = (val, item) => { | ||
| 620 | nextTick(() => { | ||
| 621 | ruleFormRef.value?.validateField([item.field]) | ||
| 622 | }) | ||
| 623 | } | ||
| 624 | |||
| 617 | </script> | 625 | </script> |
| 618 | <template> | 626 | <template> |
| 619 | <el-form ref="ruleFormRef" class="dialog-form-inline" :class="[props.col]" :inline="true" :model="formInline" @submit.prevent | 627 | <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) => { | ... | @@ -705,6 +713,20 @@ const panelChange = (scope, row) => { |
| 705 | {{ item.getName(data, node) }} | 713 | {{ item.getName(data, node) }} |
| 706 | </div> | 714 | </div> |
| 707 | </template> | 715 | </template> |
| 716 | <span v-else-if="item.customInfo" class="custom-tree-node"> | ||
| 717 | <span v-if="item.customInfo?.type == 'prefixIcon'" class="custom_icon"> | ||
| 718 | <el-icon v-if="data.nodeType == 2" style="margin-top: 3px;"> | ||
| 719 | <svg-icon name="file-person" /> | ||
| 720 | </el-icon> | ||
| 721 | <el-icon v-else-if="node.expanded" style="margin-top: 3px;"> | ||
| 722 | <svg-icon name="file-open" /> | ||
| 723 | </el-icon> | ||
| 724 | <el-icon v-else-if="!node.expanded" style="margin-top: 3px;"> | ||
| 725 | <svg-icon name="file-closed" /> | ||
| 726 | </el-icon> | ||
| 727 | </span> | ||
| 728 | <span style="margin-left: 4px;vertical-align: top;">{{ data[item.props.label] }}</span> | ||
| 729 | </span> | ||
| 708 | <span v-else>{{ data[item.props.label] }}</span> | 730 | <span v-else>{{ data[item.props.label] }}</span> |
| 709 | </template> | 731 | </template> |
| 710 | </el-tree-select> | 732 | </el-tree-select> |
| ... | @@ -1530,6 +1552,15 @@ const panelChange = (scope, row) => { | ... | @@ -1530,6 +1552,15 @@ const panelChange = (scope, row) => { |
| 1530 | v-model.trim="formInline[item.field]" type="password" :placeholder="item.placeholder" | 1552 | v-model.trim="formInline[item.field]" type="password" :placeholder="item.placeholder" |
| 1531 | :clearable="item.clearable" :maxlength="item.maxlength ?? ''" show-password :disabled="item.disabled" | 1553 | :clearable="item.clearable" :maxlength="item.maxlength ?? ''" show-password :disabled="item.disabled" |
| 1532 | :autocomplete="item.autocompleteSetting?.autocomplete || item.autocomplete" :readonly="item.autocompleteSetting?.readonly || item.readonly" @click="handlerIptClick(item)" /> | 1554 | :autocomplete="item.autocompleteSetting?.autocomplete || item.autocomplete" :readonly="item.autocompleteSetting?.readonly || item.readonly" @click="handlerIptClick(item)" /> |
| 1555 | <PasswordStrengthMeter | ||
| 1556 | :class="[item.col, { is_block: item.block }]" | ||
| 1557 | v-else-if="item.type == 'password-len-meter'" | ||
| 1558 | :placeholder="item.placeholder" | ||
| 1559 | :disabled="item.disabled" | ||
| 1560 | :store-key="'passwordLenMeter' + item.field" | ||
| 1561 | v-model.trim="formInline[item.field]" | ||
| 1562 | @change="(val) => handlePwdLenMeterChange(val, item)" | ||
| 1563 | ></PasswordStrengthMeter> | ||
| 1533 | <div class="input_def" v-else> | 1564 | <div class="input_def" v-else> |
| 1534 | <span v-if="item.beforeMsg" style="color: #212121;">{{ item.beforeMsg }}</span> | 1565 | <span v-if="item.beforeMsg" style="color: #212121;">{{ item.beforeMsg }}</span> |
| 1535 | <el-input :class="[item.col, { is_block: item.block }]" v-model.trim="formInline[item.field]" :style="item.width ? { width: item.width } : null" | 1566 | <el-input :class="[item.col, { is_block: item.block }]" v-model.trim="formInline[item.field]" :style="item.width ? { width: item.width } : null" | ... | ... |
| 1 | <!-- PasswordStrengthContent.vue --> | ||
| 2 | <template> | ||
| 3 | <div class="password-strength-content"> | ||
| 4 | <div class="strength-label">密码需满足以下要求:</div> | ||
| 5 | |||
| 6 | <div class="strength-bar"> | ||
| 7 | <div | ||
| 8 | class="strength-fill" | ||
| 9 | :class="levelClass" | ||
| 10 | :style="{ width: fillWidth }" | ||
| 11 | ></div> | ||
| 12 | </div> | ||
| 13 | <div class="strength-text" :class="levelClass"> | ||
| 14 | 密码强度:{{ strengthText }} | ||
| 15 | </div> | ||
| 16 | |||
| 17 | <div class="requirements"> | ||
| 18 | <div v-for="(req, key) in requirements" :key="key" class="requirement" :class="getReqClass(key)"> | ||
| 19 | <span class="requirement-icon">{{ getReqIcon(key) }}</span> | ||
| 20 | <span>{{ req.label }}</span> | ||
| 21 | </div> | ||
| 22 | </div> | ||
| 23 | </div> | ||
| 24 | </template> | ||
| 25 | |||
| 26 | <script setup lang="ts"> | ||
| 27 | import { computed } from 'vue'; | ||
| 28 | import { useStorage } from '@/hooks/useStorage' | ||
| 29 | |||
| 30 | |||
| 31 | const props = defineProps({ | ||
| 32 | password: { | ||
| 33 | type: String, | ||
| 34 | default: '' | ||
| 35 | }, | ||
| 36 | storeKey: { | ||
| 37 | type: String, | ||
| 38 | default: 'firstUnmetRequirement' | ||
| 39 | } | ||
| 40 | }); | ||
| 41 | |||
| 42 | const emits = defineEmits(['getCheckResult']); | ||
| 43 | |||
| 44 | |||
| 45 | const { setStorage } = useStorage() | ||
| 46 | const checkPasswordStrength = (pwd) => { | ||
| 47 | if (!pwd) return { score: 0, level: 'weak', checks: {} }; | ||
| 48 | const checks = { | ||
| 49 | length: pwd.length >= 8, | ||
| 50 | lower: /[a-z]/.test(pwd), | ||
| 51 | upper: /[A-Z]/.test(pwd), | ||
| 52 | number: /[0-9]/.test(pwd), | ||
| 53 | special: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(pwd) | ||
| 54 | }; | ||
| 55 | const score = Object.values(checks).filter(Boolean).length; | ||
| 56 | let level = 'weak'; | ||
| 57 | if (score >= 5) level = 'strong'; | ||
| 58 | else if (score >= 3) level = 'medium'; | ||
| 59 | console.log(checks,'checkPasswordStrength'); | ||
| 60 | findFirstUnmetRequirement(checks) | ||
| 61 | return { score, level, checks }; | ||
| 62 | }; | ||
| 63 | |||
| 64 | const result = computed(() => checkPasswordStrength(props.password)); | ||
| 65 | const fillWidth = computed(() => `${(result.value.score / 5) * 100}%`); | ||
| 66 | const strengthText = computed(() => ({ weak: '弱', medium: '中', strong: '强' })[result.value.level]); | ||
| 67 | const levelClass = computed(() => result.value.level); | ||
| 68 | |||
| 69 | const requirements = { | ||
| 70 | number: { label: '包含数字 (0-9)' }, | ||
| 71 | length: { label: '至少 8 个字符' }, | ||
| 72 | lower: { label: '包含小写字母 (a-z)' }, | ||
| 73 | upper: { label: '包含大写字母 (A-Z)' }, | ||
| 74 | special: { label: '包含特殊符号' } | ||
| 75 | }; | ||
| 76 | |||
| 77 | const getReqClass = (key) => (result.value.checks[key] ? 'met' : props.password.length >= 3 ? 'unmet' : 'pending'); | ||
| 78 | const getReqIcon = (key) => { | ||
| 79 | if (result.value.checks[key]) return '✓'; | ||
| 80 | if (props.password.length >= 3) return '✗'; | ||
| 81 | return '○'; | ||
| 82 | }; | ||
| 83 | |||
| 84 | /** | ||
| 85 | * 匹配未满足的规则 | ||
| 86 | * @param checks | ||
| 87 | */ | ||
| 88 | const findFirstUnmetRequirement = (checks) => { | ||
| 89 | if (!props.password) return null; | ||
| 90 | console.log(checks,'firstUnmetRequirement'); | ||
| 91 | for (const key in requirements) { | ||
| 92 | const isMet = checks[key]; | ||
| 93 | if (!isMet) { | ||
| 94 | let label = requirements[key].label; | ||
| 95 | setStorage(props.storeKey, label) | ||
| 96 | return label; // 返回未满足的第一条规则的 label | ||
| 97 | } | ||
| 98 | } | ||
| 99 | setStorage(props.storeKey, '') | ||
| 100 | return null; // 全部满足 | ||
| 101 | }; | ||
| 102 | |||
| 103 | |||
| 104 | // 暴露方法给父组件 | ||
| 105 | defineExpose({ | ||
| 106 | }); | ||
| 107 | </script> | ||
| 108 | |||
| 109 | <style scoped lang="scss"> | ||
| 110 | /* 同前,省略样式 */ | ||
| 111 | .password-strength-content { | ||
| 112 | font-size: 14px; | ||
| 113 | } | ||
| 114 | |||
| 115 | .strength-label { | ||
| 116 | margin-bottom: 12px; | ||
| 117 | color: #333; | ||
| 118 | font-weight: 500; | ||
| 119 | } | ||
| 120 | |||
| 121 | .strength-bar { | ||
| 122 | height: 6px; | ||
| 123 | border-radius: 3px; | ||
| 124 | background: #e9ecef; | ||
| 125 | overflow: hidden; | ||
| 126 | margin: 8px 0; | ||
| 127 | } | ||
| 128 | |||
| 129 | .strength-fill { | ||
| 130 | height: 100%; | ||
| 131 | border-radius: 3px; | ||
| 132 | transition: all 0.3s ease-out; | ||
| 133 | } | ||
| 134 | |||
| 135 | .strength-fill.weak { | ||
| 136 | background-color: #dc3545; | ||
| 137 | } | ||
| 138 | |||
| 139 | .strength-fill.medium { | ||
| 140 | background-color: #ffc107; | ||
| 141 | } | ||
| 142 | |||
| 143 | .strength-fill.strong { | ||
| 144 | background-color: #28a745; | ||
| 145 | } | ||
| 146 | |||
| 147 | .strength-text { | ||
| 148 | font-weight: 600; | ||
| 149 | font-size: 13px; | ||
| 150 | margin-bottom: 12px; | ||
| 151 | } | ||
| 152 | |||
| 153 | .strength-text.weak { | ||
| 154 | color: #dc3545; | ||
| 155 | } | ||
| 156 | |||
| 157 | .strength-text.medium { | ||
| 158 | color: #e0a800; | ||
| 159 | } | ||
| 160 | |||
| 161 | .strength-text.strong { | ||
| 162 | color: #28a745; | ||
| 163 | } | ||
| 164 | |||
| 165 | .requirements { | ||
| 166 | color: #555; | ||
| 167 | } | ||
| 168 | |||
| 169 | .requirement { | ||
| 170 | display: flex; | ||
| 171 | align-items: center; | ||
| 172 | margin: 4px 0; | ||
| 173 | } | ||
| 174 | |||
| 175 | .requirement-icon { | ||
| 176 | width: 16px; | ||
| 177 | text-align: center; | ||
| 178 | margin-right: 6px; | ||
| 179 | font-weight: bold; | ||
| 180 | } | ||
| 181 | |||
| 182 | .requirement.met .requirement-icon { | ||
| 183 | color: #28a745; | ||
| 184 | } | ||
| 185 | |||
| 186 | .requirement.unmet .requirement-icon { | ||
| 187 | color: #dc3545; | ||
| 188 | } | ||
| 189 | |||
| 190 | .requirement.pending .requirement-icon { | ||
| 191 | color: #adb5bd; | ||
| 192 | } | ||
| 193 | |||
| 194 | .requirement.met { | ||
| 195 | color: #28a745; | ||
| 196 | } | ||
| 197 | |||
| 198 | .requirement.unmet { | ||
| 199 | color: #dc3545; | ||
| 200 | } | ||
| 201 | |||
| 202 | .requirement.pending { | ||
| 203 | color: #6c757d; | ||
| 204 | } | ||
| 205 | </style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | <!-- PasswordStrengthPopover.vue --> | ||
| 2 | <template> | ||
| 3 | <div class="password-wrapper"> | ||
| 4 | <el-popover | ||
| 5 | v-model:visible="popoverVisible" | ||
| 6 | placement="top" | ||
| 7 | :width="300" | ||
| 8 | trigger="manual" | ||
| 9 | popper-class="password-strength-popper" | ||
| 10 | :disabled="!internalValue" | ||
| 11 | > | ||
| 12 | <template #reference> | ||
| 13 | <el-input | ||
| 14 | ref="inputRef" | ||
| 15 | :model-value="internalValue" | ||
| 16 | :placeholder="placeholder" | ||
| 17 | type="password" | ||
| 18 | clearable | ||
| 19 | showPassword | ||
| 20 | :disabled="disabled" | ||
| 21 | autocomplete="new-password" | ||
| 22 | @update:model-value="onInput" | ||
| 23 | @focus="onFocus" | ||
| 24 | @blur="onBlur" | ||
| 25 | @click="focusInput" | ||
| 26 | /> | ||
| 27 | </template> | ||
| 28 | |||
| 29 | <PasswordStrengthContent | ||
| 30 | ref="strengthRef" | ||
| 31 | :storeKey="storeKey" | ||
| 32 | :password="internalValue" | ||
| 33 | /> | ||
| 34 | </el-popover> | ||
| 35 | </div> | ||
| 36 | </template> | ||
| 37 | |||
| 38 | <script setup lang="ts"> | ||
| 39 | import { ref, watch, nextTick } from 'vue'; | ||
| 40 | import { ElInput } from 'element-plus'; | ||
| 41 | import PasswordStrengthContent from './PasswordStrengthContent.vue'; | ||
| 42 | |||
| 43 | const props = defineProps({ | ||
| 44 | modelValue: { | ||
| 45 | type: String, | ||
| 46 | default: '' | ||
| 47 | }, | ||
| 48 | placeholder: { | ||
| 49 | type: String, | ||
| 50 | default: '请输入密码' | ||
| 51 | }, | ||
| 52 | // 新增:用于表单校验的字段名(可选) | ||
| 53 | name: { | ||
| 54 | type: String, | ||
| 55 | default: 'password' | ||
| 56 | }, | ||
| 57 | storeKey: { | ||
| 58 | type: String, | ||
| 59 | default: 'firstUnmetRequirement' | ||
| 60 | }, | ||
| 61 | disabled: { | ||
| 62 | type: Boolean, | ||
| 63 | default: false | ||
| 64 | } | ||
| 65 | }); | ||
| 66 | |||
| 67 | const emit = defineEmits(['update:modelValue','change']); | ||
| 68 | |||
| 69 | const internalValue = ref(props.modelValue); | ||
| 70 | |||
| 71 | watch( | ||
| 72 | () => props.modelValue, | ||
| 73 | (newVal) => { | ||
| 74 | internalValue.value = newVal; | ||
| 75 | } | ||
| 76 | ); | ||
| 77 | |||
| 78 | const onInput = (val) => { | ||
| 79 | internalValue.value = val; | ||
| 80 | emit('update:modelValue', val); | ||
| 81 | emit('change', val); | ||
| 82 | }; | ||
| 83 | |||
| 84 | const popoverVisible = ref(false); | ||
| 85 | const inputRef = ref(null); | ||
| 86 | const strengthRef = ref(null); | ||
| 87 | |||
| 88 | const onFocus = () => { | ||
| 89 | if (internalValue.value) { | ||
| 90 | popoverVisible.value = true; | ||
| 91 | } | ||
| 92 | }; | ||
| 93 | |||
| 94 | const onBlur = () => { | ||
| 95 | setTimeout(() => { | ||
| 96 | const activeEl = document.activeElement; | ||
| 97 | const popoverEl = document.querySelector('.password-strength-popper'); | ||
| 98 | if (!popoverEl?.contains(activeEl)) { | ||
| 99 | popoverVisible.value = false; | ||
| 100 | } | ||
| 101 | }, 200); | ||
| 102 | }; | ||
| 103 | |||
| 104 | const focusInput = () => { | ||
| 105 | nextTick(() => { | ||
| 106 | inputRef.value?.focus(); | ||
| 107 | }); | ||
| 108 | }; | ||
| 109 | |||
| 110 | watch(internalValue, (newVal) => { | ||
| 111 | if (newVal) { | ||
| 112 | if (document.activeElement === inputRef.value?.$el?.querySelector('input')) { | ||
| 113 | popoverVisible.value = true; | ||
| 114 | } | ||
| 115 | } else { | ||
| 116 | popoverVisible.value = false; | ||
| 117 | } | ||
| 118 | }); | ||
| 119 | |||
| 120 | |||
| 121 | |||
| 122 | // 暴露给 el-form 使用 | ||
| 123 | defineExpose({ | ||
| 124 | }); | ||
| 125 | </script> | ||
| 126 | |||
| 127 | <style scoped lang="scss"> | ||
| 128 | .password-wrapper { | ||
| 129 | width: 100%; | ||
| 130 | // max-width: 400px; | ||
| 131 | } | ||
| 132 | .password-strength-popper { | ||
| 133 | padding: 14px !important; | ||
| 134 | background: #fff; | ||
| 135 | border: 1px solid #ddd; | ||
| 136 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 137 | border-radius: 8px; | ||
| 138 | } | ||
| 139 | </style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/hooks/useGetData.ts
0 → 100644
| 1 | import dayjs from '@/utils/dayjs' | ||
| 2 | import useUserStore from '@/store/modules/user' | ||
| 3 | |||
| 4 | import { getOrganisationRelTreeListPromise, getTemplateListPromise } from "@/api/modules/dataBasic" | ||
| 5 | |||
| 6 | const currentDate = dayjs(new Date()).format('YYYY-MM-DD') | ||
| 7 | |||
| 8 | const isArray = (val: any): val is Array<any> => { | ||
| 9 | return val && Array.isArray(val) | ||
| 10 | } | ||
| 11 | |||
| 12 | const isNonEmptyArray = (val: any): boolean => { | ||
| 13 | return isArray(val) && val.length > 0; | ||
| 14 | } | ||
| 15 | |||
| 16 | const useGetData = (param = {}) => { | ||
| 17 | const BasicInfo: any = ref({}) // 基础资料 | ||
| 18 | const gradeList = ref<any>([]) // 职级关系 | ||
| 19 | const platformGradeList = ref<any>([]) // 平台职级 | ||
| 20 | const postionList = ref<any>([]) // 职位 | ||
| 21 | const tenantRelList = ref<any>([]) // 公司关系 | ||
| 22 | const templateList = ref<any>([]) // 菜单模板列表 | ||
| 23 | const orgMap = ref<orgMapRes>() // 人员组织信息 | ||
| 24 | const organisationTree = ref<any>([]) // 组织树 | ||
| 25 | const personelTree = ref<any>([]) // 人员树 | ||
| 26 | const amoebaTree = ref<any>([]) // 阿米巴树 | ||
| 27 | const financeSubjectDict = ref<any>([]) // 关联财务科目字典 | ||
| 28 | const systemSideList = ref<{ // 系统列表 | ||
| 29 | systemName: string, | ||
| 30 | guid: string | ||
| 31 | }[]>([]) | ||
| 32 | const getFinanceSubject = async (customParam = {}) => { // 获取业务线 | ||
| 33 | return await budgetApi.getFinanceSubjectTreePromise((Object.assign({}, param, customParam))) | ||
| 34 | } | ||
| 35 | // !基础数据缓存机制 | ||
| 36 | async function getFinanceSubjectTree({ useCache = true, customParam = {} } = {}) { | ||
| 37 | if (isNonEmptyArray(BasicInfo.financeSubject) && useCache) { | ||
| 38 | return BasicInfo.financeSubject | ||
| 39 | } else { | ||
| 40 | let res = await getFinanceSubject(customParam) | ||
| 41 | BasicInfo.financeSubject = useCache ? res : [] | ||
| 42 | return res | ||
| 43 | } | ||
| 44 | } | ||
| 45 | async function getGradeTitleRelList({ useCache = true, customParam = {} } = {}) { // 获取职级关系列表 | ||
| 46 | if ( isNonEmptyArray(BasicInfo.gradeList) && useCache) { | ||
| 47 | gradeList.value = BasicInfo.gradeList | ||
| 48 | return gradeList.value | ||
| 49 | } else { | ||
| 50 | let res = await userApi.getGradeRelListPromise(customParam) | ||
| 51 | BasicInfo.gradeList = useCache ? res : [] | ||
| 52 | gradeList.value = res | ||
| 53 | return gradeList.value | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | async function getGradeList({ useCache = true, customParam = {} } = {}) { // 获取平台职级列表 | ||
| 58 | if ( isNonEmptyArray(BasicInfo.platformGradeList) && useCache) { | ||
| 59 | platformGradeList.value = BasicInfo.platformGradeList | ||
| 60 | return platformGradeList.value | ||
| 61 | } else { | ||
| 62 | let res = await userApi.getGradeListPromise(customParam) | ||
| 63 | BasicInfo.platformGradeList = useCache ? res : [] | ||
| 64 | platformGradeList.value = res | ||
| 65 | return platformGradeList.value | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | async function getPostionList({ useCache = true, customParam = {} } = {}) { // 获取职位 | ||
| 70 | if ( isNonEmptyArray(BasicInfo.postionList) && useCache) { | ||
| 71 | postionList.value = BasicInfo.postionList | ||
| 72 | return postionList.value | ||
| 73 | } else { | ||
| 74 | let res = await userApi.getPositionPromise(customParam) | ||
| 75 | BasicInfo.postionList = useCache ? res : [] | ||
| 76 | postionList.value = res | ||
| 77 | return postionList.value | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | async function getTenantRelList(useCache = true) { // 获取公司关系列表 | ||
| 82 | if (isNonEmptyArray(BasicInfo.tenantRelList) && useCache) { | ||
| 83 | tenantRelList.value = BasicInfo.tenantRelList | ||
| 84 | return tenantRelList.value | ||
| 85 | } else { | ||
| 86 | let res = await tenantApi.getTenantListPromise('Y') | ||
| 87 | BasicInfo.tenantRelList = useCache ? res : [] | ||
| 88 | tenantRelList.value = res | ||
| 89 | return tenantRelList.value | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | async function getPermissionTemplateList({ useCache = true, customParam = {} } = {}) { // 获取菜单模板 | ||
| 94 | if (isNonEmptyArray(templateList.value) && useCache) { | ||
| 95 | return templateList.value | ||
| 96 | } else { | ||
| 97 | let res = await getTemplateListPromise(customParam) | ||
| 98 | templateList.value = res || [] | ||
| 99 | return templateList.value | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | interface orgMapRes { | ||
| 104 | [key:string]:{ | ||
| 105 | guid:string, // staffGuid | ||
| 106 | userGuid:string, // userGuid | ||
| 107 | orgGuid:string, // 部门 | ||
| 108 | orgName:string, | ||
| 109 | orgGuidTop:string, // 一级部门guid | ||
| 110 | orgNameTop:number, | ||
| 111 | } | ||
| 112 | } | ||
| 113 | // 获取人员组织信息 | ||
| 114 | async function getOrgMap({ useCache = true, customParam = {} } = {}) { // 获取人员组织信息 | ||
| 115 | const userStore = useUserStore() | ||
| 116 | let staffGuid = userStore.userInfo.staffGuid | ||
| 117 | if (isObject(BasicInfo.orgMap) && BasicInfo.orgMap[staffGuid] && useCache) { | ||
| 118 | orgMap.value = BasicInfo.orgMap | ||
| 119 | return orgMap.value | ||
| 120 | } else { | ||
| 121 | const userStore = useUserStore() | ||
| 122 | let res = await staffApi.getOrgMap([userStore.userInfo.staffGuid]) | ||
| 123 | BasicInfo.orgMap = useCache ? res : null | ||
| 124 | orgMap.value = res | ||
| 125 | return orgMap.value | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | // 获取组织树 | ||
| 130 | async function getPersonelTree({ useCache = true, customParam = {} } = {}) { | ||
| 131 | if ( isNonEmptyArray(BasicInfo.personelTree) && useCache) { | ||
| 132 | personelTree.value = BasicInfo.personelTree | ||
| 133 | return personelTree.value | ||
| 134 | } else { | ||
| 135 | const userStore = useUserStore() | ||
| 136 | let res = await tenantApi.getOrganisationTreePromise(userStore.userInfo.tenantGuid) | ||
| 137 | mulTreeData(res) | ||
| 138 | BasicInfo.personelTree = useCache ? res : [] | ||
| 139 | personelTree.value = res | ||
| 140 | return personelTree.value | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | async function getOrganisationTree({ useCache = true, tenantGuid = '' } = {}) { // 获取组织树 | ||
| 145 | if ( isNonEmptyArray(BasicInfo.organisationTree) && useCache) { | ||
| 146 | organisationTree.value = BasicInfo.organisationTree | ||
| 147 | return organisationTree.value | ||
| 148 | } else { | ||
| 149 | let res = await getOrganisationRelTreeListPromise(tenantGuid) | ||
| 150 | // getOrgtreeData(res,false) | ||
| 151 | BasicInfo.organisationTree = useCache ? res : [] | ||
| 152 | organisationTree.value = res | ||
| 153 | return organisationTree.value | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | async function getAmoebaTree({ useCache = true, customParam = {} } = {}) { // 获取组织树 | ||
| 158 | if ( isNonEmptyArray(BasicInfo.amoebaTree) && useCache) { | ||
| 159 | amoebaTree.value = BasicInfo.amoebaTree | ||
| 160 | return amoebaTree.value | ||
| 161 | } else { | ||
| 162 | let res = await tenantApi.getAmoebaTreePromise2(currentDate) | ||
| 163 | getOrgtreeData(res,false) | ||
| 164 | BasicInfo.amoebaTree = useCache ? res : [] | ||
| 165 | amoebaTree.value = res | ||
| 166 | return amoebaTree.value | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | async function getFinanceDict({ useCache = true, customParam = {} } = {}) { // 获取财务科目关联字典 | ||
| 171 | if ( isNonEmptyArray(BasicInfo.financeSubjectDict) && useCache) { | ||
| 172 | financeSubjectDict.value = BasicInfo.financeSubjectDict | ||
| 173 | return financeSubjectDict.value | ||
| 174 | } else { | ||
| 175 | let res = await budgetApi.getSubjectDict(customParam) | ||
| 176 | getOrgtreeData(res,false) | ||
| 177 | BasicInfo.financeSubjectDict = useCache ? res : [] | ||
| 178 | financeSubjectDict.value = res | ||
| 179 | return financeSubjectDict.value | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | // 获取子系统列表 | ||
| 184 | async function getSystemSideList({ useCache = true, customParam = {} } = {}) { | ||
| 185 | if ( isNonEmptyArray(BasicInfo.systemSideList) && useCache) { | ||
| 186 | systemSideList.value = BasicInfo.systemSideList | ||
| 187 | return systemSideList.value | ||
| 188 | } else { | ||
| 189 | let res = await authApi.getSystemSideData() | ||
| 190 | BasicInfo.systemSideList = useCache ? res : [] | ||
| 191 | systemSideList.value = res | ||
| 192 | return systemSideList.value | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | return { | ||
| 197 | getPersonelTree, | ||
| 198 | getFinanceSubject, | ||
| 199 | getGradeTitleRelList, | ||
| 200 | getGradeList, | ||
| 201 | getPostionList, | ||
| 202 | getTenantRelList, | ||
| 203 | getFinanceSubjectTree, | ||
| 204 | getPermissionTemplateList, | ||
| 205 | getOrgMap, | ||
| 206 | getOrganisationTree, | ||
| 207 | getAmoebaTree, | ||
| 208 | getFinanceDict, | ||
| 209 | getSystemSideList | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | export default useGetData |
src/hooks/useStorage.ts
0 → 100644
| 1 | // 获取传入的值的类型 | ||
| 2 | const getValueType = (value: any) => { | ||
| 3 | const type = Object.prototype.toString.call(value) | ||
| 4 | return type.slice(8, -1) | ||
| 5 | } | ||
| 6 | |||
| 7 | export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => { | ||
| 8 | const setStorage = (key: string, value: any) => { | ||
| 9 | const valueType = getValueType(value) | ||
| 10 | window[type].setItem(key, JSON.stringify({ type: valueType, value })) | ||
| 11 | } | ||
| 12 | |||
| 13 | const getStorage = (key: string) => { | ||
| 14 | const value = window[type].getItem(key) | ||
| 15 | if (value) { | ||
| 16 | const { value: val } = JSON.parse(value) | ||
| 17 | return val | ||
| 18 | } else { | ||
| 19 | return value | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | const removeStorage = (key: string) => { | ||
| 24 | window[type].removeItem(key) | ||
| 25 | } | ||
| 26 | |||
| 27 | const clear = (excludes?: string[]) => { | ||
| 28 | // 获取排除项 | ||
| 29 | const keys = Object.keys(window[type]) | ||
| 30 | const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter'] | ||
| 31 | const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes | ||
| 32 | const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys | ||
| 33 | // 排除项不清除 | ||
| 34 | excludesKeys.forEach((key) => { | ||
| 35 | window[type].removeItem(key) | ||
| 36 | }) | ||
| 37 | // window[type].clear() | ||
| 38 | } | ||
| 39 | |||
| 40 | return { | ||
| 41 | setStorage, | ||
| 42 | getStorage, | ||
| 43 | removeStorage, | ||
| 44 | clear | ||
| 45 | } | ||
| 46 | } |
| ... | @@ -363,6 +363,19 @@ export const useValidator = () => { | ... | @@ -363,6 +363,19 @@ export const useValidator = () => { |
| 363 | } | 363 | } |
| 364 | } | 364 | } |
| 365 | 365 | ||
| 366 | const phone = (message?: string): FormItemRule => { | ||
| 367 | return { | ||
| 368 | validator: (_, val, callback) => { | ||
| 369 | if (val && !/^\d+(?:-\d+)?$/i.test(val)) { | ||
| 370 | callback(new Error(message || '电话格式不正确')); | ||
| 371 | } else { | ||
| 372 | callback(); | ||
| 373 | } | ||
| 374 | }, | ||
| 375 | trigger: 'blur' | ||
| 376 | }; | ||
| 377 | }; | ||
| 378 | |||
| 366 | const mobileNumber = (message?: string): FormItemRule => { | 379 | const mobileNumber = (message?: string): FormItemRule => { |
| 367 | return { | 380 | return { |
| 368 | validator: (_, val, callback) => { | 381 | validator: (_, val, callback) => { |
| ... | @@ -393,6 +406,7 @@ export const useValidator = () => { | ... | @@ -393,6 +406,7 @@ export const useValidator = () => { |
| 393 | validateDomainList, | 406 | validateDomainList, |
| 394 | minChineseCount, | 407 | minChineseCount, |
| 395 | email, | 408 | email, |
| 396 | mobileNumber | 409 | mobileNumber, |
| 410 | phone | ||
| 397 | } | 411 | } |
| 398 | } | 412 | } | ... | ... |
| ... | @@ -22,6 +22,46 @@ const routes: RouteRecordRaw[] = [ | ... | @@ -22,6 +22,46 @@ const routes: RouteRecordRaw[] = [ |
| 22 | }, | 22 | }, |
| 23 | }] | 23 | }] |
| 24 | }, | 24 | }, |
| 25 | { | ||
| 26 | path: '/data-basic/user-manage', | ||
| 27 | component: Layout, | ||
| 28 | meta: { | ||
| 29 | title: '用户管理', | ||
| 30 | icon: 'sidebar-videos', | ||
| 31 | }, | ||
| 32 | children: [ | ||
| 33 | { | ||
| 34 | path: '', | ||
| 35 | name: 'userManagement', | ||
| 36 | component: () => import('@/views/data_basic/userManagement.vue'), | ||
| 37 | meta: { | ||
| 38 | title: '用户管理', | ||
| 39 | sidebar: false, | ||
| 40 | breadcrumb: false, | ||
| 41 | cache: true | ||
| 42 | }, | ||
| 43 | }, | ||
| 44 | { | ||
| 45 | path: 'user-edit', | ||
| 46 | name: 'addUser', | ||
| 47 | component: () => import('@/views/data_basic/addUser.vue'), | ||
| 48 | meta: { | ||
| 49 | title: '新增用户', | ||
| 50 | sidebar: false, | ||
| 51 | breadcrumb: false, | ||
| 52 | cache: true, | ||
| 53 | reuse: true, | ||
| 54 | editPage: true, | ||
| 55 | activeMenu: '/data-basic/user-manage' | ||
| 56 | }, | ||
| 57 | beforeEnter: (to) => { | ||
| 58 | if (to.query.userName) { | ||
| 59 | to.meta.title = `编辑-${to.query.userName}`; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | }, | ||
| 63 | ], | ||
| 64 | }, | ||
| 25 | ] | 65 | ] |
| 26 | 66 | ||
| 27 | 67 | ... | ... |
src/store/modules/dataBasic.ts
0 → 100644
| 1 | const useDataBasicStore = defineStore( | ||
| 2 | // api标签分类guid | ||
| 3 | 'dataBasic', | ||
| 4 | |||
| 5 | () => { | ||
| 6 | |||
| 7 | const isUpdate = ref(false); | ||
| 8 | function setIsUpdate(update: boolean) { | ||
| 9 | isUpdate.value = update; | ||
| 10 | } | ||
| 11 | |||
| 12 | return { | ||
| 13 | isUpdate, | ||
| 14 | setIsUpdate, | ||
| 15 | } | ||
| 16 | }, | ||
| 17 | ) | ||
| 18 | |||
| 19 | export default useDataBasicStore | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -41,6 +41,8 @@ declare module '@vue/runtime-core' { | ... | @@ -41,6 +41,8 @@ declare module '@vue/runtime-core' { |
| 41 | PageHeader: typeof import('./../components/PageHeader/index.vue')['default'] | 41 | PageHeader: typeof import('./../components/PageHeader/index.vue')['default'] |
| 42 | PageMain: typeof import('./../components/PageMain/index.vue')['default'] | 42 | PageMain: typeof import('./../components/PageMain/index.vue')['default'] |
| 43 | PageNav: typeof import('./../components/PageNav/index.vue')['default'] | 43 | PageNav: typeof import('./../components/PageNav/index.vue')['default'] |
| 44 | PasswordStrengthContent: typeof import('./../components/PasswordStrengthMeter/PasswordStrengthContent.vue')['default'] | ||
| 45 | PasswordStrengthMeter: typeof import('./../components/PasswordStrengthMeter/index.vue')['default'] | ||
| 44 | PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default'] | 46 | PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default'] |
| 45 | Popover: typeof import('./../components/Popover/index.vue')['default'] | 47 | Popover: typeof import('./../components/Popover/index.vue')['default'] |
| 46 | RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default'] | 48 | RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default'] |
| ... | @@ -49,6 +51,7 @@ declare module '@vue/runtime-core' { | ... | @@ -49,6 +51,7 @@ declare module '@vue/runtime-core' { |
| 49 | Schedule: typeof import('./../components/Schedule/index.vue')['default'] | 51 | Schedule: typeof import('./../components/Schedule/index.vue')['default'] |
| 50 | SearchBar: typeof import('./../components/SearchBar/index.vue')['default'] | 52 | SearchBar: typeof import('./../components/SearchBar/index.vue')['default'] |
| 51 | SecondAndMinute: typeof import('./../components/Schedule/component/secondAndMinute.vue')['default'] | 53 | SecondAndMinute: typeof import('./../components/Schedule/component/secondAndMinute.vue')['default'] |
| 54 | SelectPersonnel: typeof import('./../components/SelectPersonnel/src/SelectPersonnel.vue')['default'] | ||
| 52 | SplitPane: typeof import('./../components/SplitPane/index.vue')['default'] | 55 | SplitPane: typeof import('./../components/SplitPane/index.vue')['default'] |
| 53 | StepBar: typeof import('./../components/StepBar/index.vue')['default'] | 56 | StepBar: typeof import('./../components/StepBar/index.vue')['default'] |
| 54 | SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] | 57 | SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] |
| ... | @@ -59,6 +62,7 @@ declare module '@vue/runtime-core' { | ... | @@ -59,6 +62,7 @@ declare module '@vue/runtime-core' { |
| 59 | Table_search: typeof import('./../components/Tools/table_search.vue')['default'] | 62 | Table_search: typeof import('./../components/Tools/table_search.vue')['default'] |
| 60 | Table_tools: typeof import('./../components/Tools/table_tools.vue')['default'] | 63 | Table_tools: typeof import('./../components/Tools/table_tools.vue')['default'] |
| 61 | Table_v2: typeof import('./../components/Table/table_v2.vue')['default'] | 64 | Table_v2: typeof import('./../components/Table/table_v2.vue')['default'] |
| 65 | TableActions: typeof import('./../components/TablePlus/src/components/TableActions.vue')['default'] | ||
| 62 | Tabs: typeof import('./../components/Tabs/index.vue')['default'] | 66 | Tabs: typeof import('./../components/Tabs/index.vue')['default'] |
| 63 | Toolbar: typeof import('./../components/LineageGraph/toolbar.vue')['default'] | 67 | Toolbar: typeof import('./../components/LineageGraph/toolbar.vue')['default'] |
| 64 | Topbar: typeof import('./../components/LineageGraph/topbar.vue')['default'] | 68 | Topbar: typeof import('./../components/LineageGraph/topbar.vue')['default'] | ... | ... |
| ... | @@ -1228,3 +1228,30 @@ export const getPublicIP = async (): Promise<string> => { | ... | @@ -1228,3 +1228,30 @@ export const getPublicIP = async (): Promise<string> => { |
| 1228 | } | 1228 | } |
| 1229 | } | 1229 | } |
| 1230 | } | 1230 | } |
| 1231 | |||
| 1232 | export const isArray = (val: any): val is Array<any> => { | ||
| 1233 | return val && Array.isArray(val) | ||
| 1234 | } | ||
| 1235 | export const isNonEmptyArray = (val: any): boolean => { | ||
| 1236 | return isArray(val) && val.length > 0; | ||
| 1237 | } | ||
| 1238 | |||
| 1239 | /** | ||
| 1240 | * 判断对象是否为空 | ||
| 1241 | * @param obj | ||
| 1242 | * @param keyList 需要判断的keys 不传则判断所有属性 | ||
| 1243 | * @returns | ||
| 1244 | */ | ||
| 1245 | export const isAllPropertiesEmpty = (obj,keyList?: string[]) => { | ||
| 1246 | const keys = keyList && keyList.length ? keyList : Object.keys(obj); | ||
| 1247 | // 遍历每个属性 | ||
| 1248 | for (let key of keys) { | ||
| 1249 | const value = obj[key]; | ||
| 1250 | if (value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0) || (typeof value === "object" && Object.keys(value).length === 0)) { | ||
| 1251 | // 如果属性没有值,则返回false | ||
| 1252 | return true; | ||
| 1253 | } | ||
| 1254 | } | ||
| 1255 | // 如果所有属性都有值,则返回true | ||
| 1256 | return false; | ||
| 1257 | } | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
src/views/data_basic/addUser.vue
0 → 100644
This diff is collapsed.
Click to expand it.
| 1 | <template> | ||
| 2 | <div class="entry-org-container"> | ||
| 3 | <el-table ref="tableRef" class="entry-org-table" :data="tableData" :height="tableHeight" | ||
| 4 | :highlight-current-row="true" stripe tooltip-effect="light" border> | ||
| 5 | <el-table-column label="序号" width="56" align="center" fixed="left" :formatter="formatIndex" /> | ||
| 6 | <el-table-column prop="organisationName" label="所属部门" :width="columnWidths.organisationName" align="left" | ||
| 7 | :show-overflow-tooltip="true"> | ||
| 8 | <template #header> | ||
| 9 | <span>所属部门</span> | ||
| 10 | <span style="color:red;margin-left: 2px;">*</span> | ||
| 11 | </template> | ||
| 12 | <template #default="scope"> | ||
| 13 | <el-tree-select v-if="!isDetail" v-model="scope.row.organisationGuid" placeholder="请选择" :data="organisationList" | ||
| 14 | @node-click="(node, nodeObj) => handleTreeSelectNodeChange(node, nodeObj, scope)" | ||
| 15 | :props="{ | ||
| 16 | value: 'guid', | ||
| 17 | label: 'name', | ||
| 18 | children: 'children' | ||
| 19 | }" clearable filterable :checkStrictly="true" :showAllLevels="false" class="input-width"> | ||
| 20 | </el-tree-select> | ||
| 21 | <!-- <el-input v-if="!isDetail" v-model="scope.row.organisationName" placeholder="请选择" readonly | ||
| 22 | @click="openOrgSelector(scope.$index)"></el-input> --> | ||
| 23 | <span v-else>{{ scope.row.organisationName || '--' }}</span> | ||
| 24 | </template> | ||
| 25 | </el-table-column> | ||
| 26 | <el-table-column prop="isLeader" label="是否部门负责人" :width="columnWidths.isLeader" align="left" | ||
| 27 | :show-overflow-tooltip="true"> | ||
| 28 | <template #header> | ||
| 29 | <span>是否部门负责人</span> | ||
| 30 | <span style="color:red;margin-left: 2px;">*</span> | ||
| 31 | </template> | ||
| 32 | <template #default="scope"> | ||
| 33 | <el-select v-if="!isDetail" v-model="scope.row.isLeader" placeholder="请选择" clearable filterable> | ||
| 34 | <el-option v-for="opt in yesNoOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> | ||
| 35 | </el-select> | ||
| 36 | <span v-else>{{ scope.row.isLeader == 'Y' ? '是' : '否' }}</span> | ||
| 37 | </template> | ||
| 38 | </el-table-column> | ||
| 39 | <el-table-column prop="isMainOrg" label="是否主责部门" :width="columnWidths.isMainOrg" align="left" | ||
| 40 | :show-overflow-tooltip="true"> | ||
| 41 | <template #header> | ||
| 42 | <span>是否主责部门</span> | ||
| 43 | <span style="color:red;margin-left: 2px;">*</span> | ||
| 44 | </template> | ||
| 45 | <template #default="scope"> | ||
| 46 | <el-select v-if="!isDetail" v-model="scope.row.isMainOrg" placeholder="请选择" clearable filterable | ||
| 47 | @change="handleMainOrgChange(scope.row)"> | ||
| 48 | <el-option v-for="opt in yesNoOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> | ||
| 49 | </el-select> | ||
| 50 | <span v-else>{{ scope.row.isMainOrg == 'Y' ? '是' : '否' }}</span> | ||
| 51 | </template> | ||
| 52 | </el-table-column> | ||
| 53 | <el-table-column v-if="!isDetail" label="操作" width="140" align="left" fixed="right"> | ||
| 54 | <template #default="scope"> | ||
| 55 | <span class="text_btn" @click="deleteItem(scope)" v-preReClick>删除</span> | ||
| 56 | </template> | ||
| 57 | </el-table-column> | ||
| 58 | </el-table> | ||
| 59 | </div> | ||
| 60 | <div v-if="!isDetail" class="add-btn-container"> | ||
| 61 | <el-button link @click="addNewRow" :icon="CirclePlus" v-preReClick> | ||
| 62 | 新增入职部门 | ||
| 63 | </el-button> | ||
| 64 | </div> | ||
| 65 | </template> | ||
| 66 | |||
| 67 | <script lang="ts" setup name="EntryOrgList"> | ||
| 68 | import { CirclePlus } from "@element-plus/icons-vue"; | ||
| 69 | import { | ||
| 70 | getOrganisationRelTreeList | ||
| 71 | } from '@/api/modules/dataBasic'; | ||
| 72 | import { isAllPropertiesEmpty } from '@/utils/common'; | ||
| 73 | import useUserStore from "@/store/modules/user"; | ||
| 74 | |||
| 75 | const userStore = useUserStore(); | ||
| 76 | const userData = JSON.parse(userStore.userData) | ||
| 77 | |||
| 78 | const { proxy } = getCurrentInstance() as any; | ||
| 79 | |||
| 80 | // Props 定义 | ||
| 81 | const props = defineProps({ | ||
| 82 | disabled: { | ||
| 83 | type: Boolean, | ||
| 84 | default: false | ||
| 85 | }, | ||
| 86 | organisationJson: { | ||
| 87 | type: Array as PropType<any[]>, | ||
| 88 | default: () => [] | ||
| 89 | }, | ||
| 90 | projectDate: { | ||
| 91 | type: Array as PropType<any[]>, | ||
| 92 | default: () => [] | ||
| 93 | }, | ||
| 94 | }); | ||
| 95 | |||
| 96 | // 响应式数据 | ||
| 97 | const tableRef = ref(); | ||
| 98 | const currentIndex = ref<number>(0); | ||
| 99 | const organisationList = ref<any[]>([]); | ||
| 100 | const yesNoOptions = ref<any[]>([]); | ||
| 101 | |||
| 102 | // 表格数据计算属性 | ||
| 103 | const tableData = computed(() => { | ||
| 104 | return props.organisationJson; | ||
| 105 | }); | ||
| 106 | |||
| 107 | // 详情状态计算属性 | ||
| 108 | const isDetail = computed((): boolean => { | ||
| 109 | return proxy.$route.query.isDetail || props.disabled; | ||
| 110 | }); | ||
| 111 | |||
| 112 | // 表格高度计算 | ||
| 113 | const tableHeight = computed(() => { | ||
| 114 | return 'auto'; | ||
| 115 | }); | ||
| 116 | |||
| 117 | // 列宽配置 | ||
| 118 | const columnWidths = computed(() => { | ||
| 119 | return { | ||
| 120 | organisationName: '180px', | ||
| 121 | isLeader: '140px', | ||
| 122 | isMainOrg: '140px' | ||
| 123 | }; | ||
| 124 | }); | ||
| 125 | |||
| 126 | // 初始化数据 | ||
| 127 | const initializeData = async () => { | ||
| 128 | try { | ||
| 129 | // 获取是否字典选项 | ||
| 130 | yesNoOptions.value = [{ | ||
| 131 | value: 'Y', | ||
| 132 | label: '是' | ||
| 133 | }, { | ||
| 134 | value: 'N', | ||
| 135 | label: '否' | ||
| 136 | }]; | ||
| 137 | |||
| 138 | // 获取组织列表 | ||
| 139 | await getOrganisationList(); | ||
| 140 | } catch (error) { | ||
| 141 | console.error('初始化数据失败:', error); | ||
| 142 | } | ||
| 143 | }; | ||
| 144 | |||
| 145 | // 获取组织列表 | ||
| 146 | const getOrganisationList = async () => { | ||
| 147 | try { | ||
| 148 | const res: any = await getOrganisationRelTreeList({ | ||
| 149 | tenantGuid: userData.tenantGuid | ||
| 150 | }); | ||
| 151 | organisationList.value = res?.data || []; | ||
| 152 | } catch (error) { | ||
| 153 | console.error('获取组织列表失败:', error); | ||
| 154 | } | ||
| 155 | }; | ||
| 156 | |||
| 157 | // 格式化序号 | ||
| 158 | const formatIndex = (row: any, column: any, cellValue: any, index: number) => { | ||
| 159 | return String(index + 1); | ||
| 160 | }; | ||
| 161 | |||
| 162 | // 处理主责部门变更 | ||
| 163 | const handleMainOrgChange = (row: any) => { | ||
| 164 | // 检查是否已经有主责部门 | ||
| 165 | const mainOrgCount = tableData.value.filter(item => item.isMainOrg === 'Y').length; | ||
| 166 | if (mainOrgCount > 1) { | ||
| 167 | row.isMainOrg = null; | ||
| 168 | proxy.$message.warning('只能选择一个主责部门'); | ||
| 169 | } | ||
| 170 | }; | ||
| 171 | |||
| 172 | // 添加新行 | ||
| 173 | const addNewRow = () => { | ||
| 174 | // 检查最后一行是否填写完整 | ||
| 175 | if (tableData.value.length > 0) { | ||
| 176 | const lastItem = tableData.value[tableData.value.length - 1]; | ||
| 177 | const isEmpty = isAllPropertiesEmpty(lastItem, ['organisationGuid', 'isLeader', 'isMainOrg']); | ||
| 178 | if (isEmpty) { | ||
| 179 | proxy.$message.warning('请填写完整'); | ||
| 180 | return; | ||
| 181 | } | ||
| 182 | } | ||
| 183 | unref(tableData).push({}); | ||
| 184 | }; | ||
| 185 | |||
| 186 | // 删除行 | ||
| 187 | const deleteItem = (scope: any) => { | ||
| 188 | if (isDetail.value) return; | ||
| 189 | |||
| 190 | proxy.$openMessageBox('确定删除吗?', () => { | ||
| 191 | unref(tableData).splice(scope.$index, 1); | ||
| 192 | proxy.$ElMessage.success('删除成功'); | ||
| 193 | }, () => { | ||
| 194 | proxy.$ElMessage.info('已取消删除'); | ||
| 195 | }); | ||
| 196 | }; | ||
| 197 | |||
| 198 | const currentTreeNode = ref({}); | ||
| 199 | const handleTreeSelectNodeChange = (node, nodeObj, scope) => { | ||
| 200 | currentTreeNode.value = node; | ||
| 201 | scope.row.organisationName = node.name; | ||
| 202 | } | ||
| 203 | |||
| 204 | // 验证数据 | ||
| 205 | const validateData = (): boolean => { | ||
| 206 | const guidSet = new Set(); | ||
| 207 | for (const item of tableData.value) { | ||
| 208 | if (!item.organisationGuid) { | ||
| 209 | proxy.$ElMessage.error('所属部门必填,请填写完整'); | ||
| 210 | return false; | ||
| 211 | } | ||
| 212 | if (!item.isLeader) { | ||
| 213 | proxy.$ElMessage.error('是否部门负责人必填,请填写完整'); | ||
| 214 | return false; | ||
| 215 | } | ||
| 216 | if (!item.isMainOrg) { | ||
| 217 | proxy.$ElMessage.error('是否主责部门必填,请填写完整'); | ||
| 218 | return false; | ||
| 219 | } | ||
| 220 | if (guidSet.has(item.organisationGuid)) { | ||
| 221 | proxy.$ElMessage.error('部门不能重复'); | ||
| 222 | return false; | ||
| 223 | } | ||
| 224 | guidSet.add(item.organisationGuid); | ||
| 225 | } | ||
| 226 | |||
| 227 | return true; | ||
| 228 | }; | ||
| 229 | |||
| 230 | // 暴露方法 | ||
| 231 | defineExpose({ | ||
| 232 | tableData, | ||
| 233 | validateData, | ||
| 234 | tableRef | ||
| 235 | }); | ||
| 236 | |||
| 237 | // 生命周期钩子 | ||
| 238 | onBeforeMount(() => { | ||
| 239 | initializeData(); | ||
| 240 | }); | ||
| 241 | |||
| 242 | onMounted(() => { | ||
| 243 | // 组件挂载后的逻辑 | ||
| 244 | }); | ||
| 245 | </script> | ||
| 246 | |||
| 247 | <style lang="scss" scoped> | ||
| 248 | .entry-org-container { | ||
| 249 | .entry-org-table { | ||
| 250 | :deep(.el-table__row) { | ||
| 251 | .el-input.is-disabled .el-input__inner { | ||
| 252 | background-color: #f5f7fa; | ||
| 253 | cursor: not-allowed; | ||
| 254 | } | ||
| 255 | } | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | |||
| 260 | .add-btn-container { | ||
| 261 | .el-button--default { | ||
| 262 | padding: 4px 0px; | ||
| 263 | margin-top: 4px; | ||
| 264 | } | ||
| 265 | |||
| 266 | :deep(.el-icon) { | ||
| 267 | width: 16px; | ||
| 268 | height: 16px; | ||
| 269 | |||
| 270 | svg { | ||
| 271 | width: 16px; | ||
| 272 | height: 16px; | ||
| 273 | } | ||
| 274 | } | ||
| 275 | } | ||
| 276 | </style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/data_basic/userManagement.vue
0 → 100644
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment