fix:完善组织管理逻辑

This commit is contained in:
liangdong
2026-01-07 18:59:34 +08:00
parent 9a8a2e3064
commit 90297a14ed
7 changed files with 484 additions and 231 deletions

2
components.d.ts vendored
View File

@@ -11,6 +11,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
CardItem: typeof import('./src/components/cardItem/index.vue')['default'] CardItem: typeof import('./src/components/cardItem/index.vue')['default']
Comment: typeof import('./src/components/comment/index.vue')['default'] Comment: typeof import('./src/components/comment/index.vue')['default']
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default'] CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
@@ -56,6 +57,7 @@ declare module 'vue' {
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']

View File

@@ -0,0 +1,39 @@
<template>
<el-tooltip
:content="content"
:disabled="!isOverflow"
placement="top"
v-bind="$attrs"
>
<div
class="ellipsis-content"
@mouseenter="checkOverflow"
>
<slot>{{ content }}</slot>
</div>
</el-tooltip>
</template>
<script setup>
import { ref } from 'vue';
defineOptions({ name: 'AutoEllipsis' })
defineProps({
content: String
});
const isOverflow = ref(false);
const checkOverflow = (event) => {
const el = event.currentTarget;
isOverflow.value = el.scrollWidth > el.clientWidth;
};
</script>
<style scoped>
.ellipsis-content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
</style>

View File

@@ -1,14 +1,20 @@
<template> <template>
<div class="tabs-outer-container" ref="containerRef" :style="{ height: height + 'px' }"> <div class="tabs-outer-container" ref="containerRef" :style="{ height: height + 'px' }">
<div class="ghost-wrapper" ref="ghostRef">
<div v-for="item in items" :key="'ghost' + item[itemMap.id]" class="tab-item">
<span class="tab-text">{{ item[itemMap.label] }}</span>
</div>
</div>
<div class="tabs-wrapper"> <div class="tabs-wrapper">
<div <div
v-for="(item, index) in visibleItems" v-for="(item, index) in visibleItems"
:key="item.id" :key="item[itemMap.id]"
class="tab-item" class="tab-item"
:class="{ active: modelValue === item.id }" :class="{ active: modelValue === item[itemMap.id] }"
@click="$emit('update:modelValue', item.id)" @click="$emit('update:modelValue', item[itemMap.id])"
> >
<span class="tab-text">{{ item.label }}</span> <span class="tab-text">{{ item[itemMap.label] }}</span>
</div> </div>
<el-dropdown <el-dropdown
@@ -17,7 +23,7 @@
@command="handleCommand" @command="handleCommand"
class="more-dropdown" class="more-dropdown"
> >
<div class="tab-item more-trigger"> <div class="tab-item more-trigger" :class="{ 'is-more-active': isHiddenActive }">
<span>更多</span> <span>更多</span>
<el-icon class="el-icon--right"><ArrowDown /></el-icon> <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</div> </div>
@@ -25,11 +31,12 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
v-for="item in hiddenItems" v-for="item in hiddenItems"
:key="item.id" :key="item[itemMap.id]"
:command="item.id" :command="item[itemMap.id]"
> >
<span :class="{ 'is-active-item-overflow-tabs': modelValue === item.id }">{{ item.label }}</span> <span :class="{ 'is-active-item-overflow-tabs': modelValue === item[itemMap.id] }">
{{ item[itemMap.label] }}
</span>
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -42,53 +49,94 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed, nextTick, watch } from "vue"; import { ref, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
import { ArrowDown } from "@element-plus/icons-vue";
const props = defineProps({ const props = defineProps({
modelValue: [String, Number], modelValue: [String, Number],
items: { items: { type: Array, default: () => [] },
type: Array, itemMap: { type: Object, default: () => ({ id: 'id', label: 'label' }) },
default: () => [], height: { type: Number, default: 32 },
},
height: {
type: Number,
default: 32,
},
}); });
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const containerRef = ref(null); const containerRef = ref(null);
const ghostRef = ref(null);
const splitIndex = ref(props.items.length); const splitIndex = ref(props.items.length);
const itemWidths = ref([]); // 缓存所有项的宽度
let timer = null; let timer = null;
const activeBarStyle = ref({ const activeBarStyle = ref({ width: "0px", left: "0px", opacity: 0 });
width: "0px",
left: "0px",
opacity: 0,
});
const visibleItems = computed(() => props.items.slice(0, splitIndex.value)); const visibleItems = computed(() => props.items.slice(0, splitIndex.value));
const hiddenItems = computed(() => props.items.slice(splitIndex.value)); const hiddenItems = computed(() => props.items.slice(splitIndex.value));
const isHiddenActive = computed(() => { const isHiddenActive = computed(() => {
return hiddenItems.value.some((item) => item.id === props.modelValue); return hiddenItems.value.some((item) => item[props.itemMap.id] === props.modelValue);
}); });
// 获取所有 Tab 的初始宽度
const measureWidths = () => {
if (!ghostRef.value) return;
const nodes = ghostRef.value.querySelectorAll(".tab-item");
if (nodes.length === 0) return;
const widths = Array.from(nodes).map(node => node.getBoundingClientRect().width);
// 只有拿到有效宽度才更新,防止在某些极端情况下宽度全为 0 导致计算错误
if (widths.some(w => w > 0)) {
itemWidths.value = widths;
}
};
const calculateLayout = () => {
if (!containerRef.value) return;
// 如果没有宽度数据,先测一遍
if (itemWidths.value.length === 0) {
measureWidths();
}
const containerWidth = containerRef.value.offsetWidth;
// 如果容器本身没宽度(比如在隐藏的弹窗里),直接返回
if (containerWidth <= 0) return;
const moreBtnWidth = 80;
let currentWidth = 0;
let newSplitIndex = props.items.length;
for (let i = 0; i < itemWidths.value.length; i++) {
const w = itemWidths.value[i];
// 加上 20px 的 padding 补偿 (对应你 CSS 里的 padding: 0 20px)
// 最好在 measureWidths 阶段就包含 padding或者在这里统一加
const fullWidth = w;
if (currentWidth + fullWidth > containerWidth) {
newSplitIndex = i;
// 预留更多按钮位置
while (newSplitIndex > 0 && currentWidth + moreBtnWidth > containerWidth) {
newSplitIndex--;
currentWidth -= itemWidths.value[newSplitIndex];
}
break;
}
currentWidth += fullWidth;
}
splitIndex.value = newSplitIndex;
};
const updateActiveBar = async () => { const updateActiveBar = async () => {
await nextTick(); await nextTick();
if (!containerRef.value) return; if (!containerRef.value) return;
// 1. 检查激活项是否在可见区域 const activeIndex = visibleItems.value.findIndex(item => item[props.itemMap.id] === props.modelValue);
const activeIndex = visibleItems.value.findIndex((item) => item.id === props.modelValue);
if (activeIndex >= 0) { if (activeIndex >= 0) {
// 激活项在可见区域:计算位置并显示下划线 const tabItems = containerRef.value.querySelectorAll(".tabs-wrapper > .tab-item:not(.more-trigger)");
const tabItems = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
const activeElement = tabItems[activeIndex]; const activeElement = tabItems[activeIndex];
if (activeElement) { if (activeElement) {
const rect = activeElement.getBoundingClientRect(); const rect = activeElement.getBoundingClientRect();
const containerRect = containerRef.value.getBoundingClientRect(); const containerRect = containerRef.value.getBoundingClientRect();
activeBarStyle.value = { activeBarStyle.value = {
width: `${rect.width * 0.6}px`, width: `${rect.width * 0.6}px`,
left: `${rect.left - containerRect.left + rect.width * 0.2}px`, left: `${rect.left - containerRect.left + rect.width * 0.2}px`,
@@ -97,66 +145,44 @@ const updateActiveBar = async () => {
return; return;
} }
} }
activeBarStyle.value.opacity = 0;
// 2. 如果在隐藏区域或未找到,将下划线宽度设为 0
activeBarStyle.value = {
...activeBarStyle.value,
width: "0px",
opacity: 0,
};
}; };
const calculateLayout = () => { const handleResize = () => {
if (!containerRef.value) return;
const containerWidth = Math.floor(containerRef.value.getBoundingClientRect().width);
const itemsNodes = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
const moreBtnWidth = 90;
let currentWidth = 0;
let newSplitIndex = props.items.length;
for (let i = 0; i < itemsNodes.length; i++) {
currentWidth += Math.ceil(itemsNodes[i].getBoundingClientRect().width) + 20;
if (currentWidth + moreBtnWidth > containerWidth) {
newSplitIndex = i;
break;
}
}
if (splitIndex.value !== newSplitIndex) {
splitIndex.value = newSplitIndex;
}
};
const debouncedCalc = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
calculateLayout(); calculateLayout();
nextTick(() => {
updateActiveBar(); updateActiveBar();
}, 100); });
}; };
let resizeObserver = null; let resizeObserver = null;
onMounted(async () => { onMounted(async () => {
await nextTick(); await nextTick();
measureWidths();
calculateLayout(); calculateLayout();
updateActiveBar(); updateActiveBar();
resizeObserver = new ResizeObserver(() => debouncedCalc());
resizeObserver = new ResizeObserver(() => {
// 尽量不要在 Resize 里用太长的 debounce
// 否则你会感觉 Tab 是“跳”出来的,而不是“滑”出来的
handleResize();
});
if (containerRef.value) resizeObserver.observe(containerRef.value); if (containerRef.value) resizeObserver.observe(containerRef.value);
}); });
onUnmounted(() => { onUnmounted(() => {
if (resizeObserver) resizeObserver.disconnect(); resizeObserver?.disconnect();
if (timer) clearTimeout(timer); clearTimeout(timer);
}); });
const handleCommand = (id) => emit("update:modelValue", id); const handleCommand = (id) => emit("update:modelValue", id);
watch(() => props.modelValue, () => updateActiveBar()); watch(() => props.modelValue, () => updateActiveBar());
watch(() => props.items, async () => { watch(() => props.items, async () => {
splitIndex.value = props.items.length;
await nextTick(); await nextTick();
measureWidths();
calculateLayout(); calculateLayout();
updateActiveBar(); updateActiveBar();
}, { deep: true }); }, { deep: true });
@@ -165,7 +191,17 @@ watch(() => props.items, async () => {
<style scoped lang="scss"> <style scoped lang="scss">
.tabs-outer-container { .tabs-outer-container {
width: 100%; width: 100%;
overflow: hidden; position: relative;
// 关键:测量层不可见且不占位,但必须渲染以获取宽度
.ghost-wrapper {
position: absolute;
top: -9999px;
left: -9999px;
visibility: hidden;
display: flex;
white-space: nowrap;
}
.tabs-wrapper { .tabs-wrapper {
display: flex; display: flex;
@@ -173,6 +209,7 @@ watch(() => props.items, async () => {
height: 100%; height: 100%;
position: relative; position: relative;
user-select: none; user-select: none;
width: 100%; // 确保占满父级
} }
.tab-item { .tab-item {
@@ -183,37 +220,36 @@ watch(() => props.items, async () => {
cursor: pointer; cursor: pointer;
color: #606266; color: #606266;
font-size: 14px; font-size: 14px;
position: relative;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
transition: color 0.2s ease;
&.active { &.active {
color: #409eff; color: #409eff;
font-weight: 600; font-weight: 600;
} }
&.is-more-active {
color: #409eff;
}
} }
.more-trigger { .more-trigger {
margin-left: auto; margin-left: auto;
border-left: 1px solid #f0f2f5; border-left: 1px solid #f0f2f5;
// 选中更多中的数据时,仅文字和图标变蓝
&.is-more-active {
color: #409eff;
font-weight: 600;
}
} }
.active-bar { .active-bar {
position: absolute; position: absolute;
height: 2px; height: 2px;
background-color: #409eff; background-color: #409eff;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s;
bottom: 0; bottom: 0;
pointer-events: none; pointer-events: none;
} }
} }
.is-active-item-overflow-tabs {
color: #409eff;
font-weight: 600;
}
</style> </style>

View File

@@ -38,8 +38,8 @@
> >
<div class="info-label">{{ base.name }}</div> <div class="info-label">{{ base.name }}</div>
<div class="info-value"> <div class="info-value">
<el-button link type="primary" v-if="base.slotName === 'link'">{{base.value}} &gt;</el-button> <el-button link type="primary" v-if="base.slotName === 'link'" @click="onOpenWeixin">已开启 &gt;</el-button>
<span v-else>{{ base.value }}</span> <span v-else>{{ info[base.value] }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -78,20 +78,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps({
info:{
Object,
default: () => ({})
}
})
const turnOn = ref(true); const turnOn = ref(true);
const systemList = [ const systemList = [
{ {
name: "生效时间", name: "生效时间",
value: "2024-01-01", value: "",
}, },
{ {
name: "过期时间", name: "过期时间",
value: "永久生效", value: "",
}, },
{ {
name: "最后更新", name: "最后更新",
value: "2025-12-29 16:06", value: "",
}, },
{ {
name: "更新人", name: "更新人",
@@ -103,20 +108,20 @@ const systemList = [
const baseList = [ const baseList = [
{ {
name: "部门/公司名称", name: "部门/公司名称",
value: "广州分公司", value: "",
}, },
{ {
name: "上级单位", name: "上级单位",
value: "集团1", value: "",
}, },
{ {
name: "同步企微", name: "同步企微",
value: "已开启", value: "",
slotName: "link", slotName: "link",
}, },
{ {
name: "部门负责人", name: "部门负责人",
value: "赵康, 李思奇, 董峥", value: "",
}, },
]; ];
@@ -125,6 +130,14 @@ const baseList = [
const onButtonCheck = () =>{ const onButtonCheck = () =>{
turnOn.value = !turnOn.value; turnOn.value = !turnOn.value;
} }
// 开启微信
const onOpenWeixin = () =>{
console.log('onOpenWeixin')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -20,12 +20,29 @@
</template> </template>
<el-form :model="form" layout="vertical" label-position="top"> <el-form :model="form" layout="vertical" label-position="top">
<!-- 编辑的时候展示 同步按钮 -->
<template v-if="isSyncConfig">
<el-form-item>
<div class="sync-card">
<div class="sync-card__content">
<h3 class="sync-card__title">开启企微同步</h3>
<p class="sync-card__desc">开启后将定期拉取企微通讯录数据</p>
</div>
<div class="sync-card__action">
<el-switch v-model="form.syncEnabled" size="large" />
</div>
</div>
</el-form-item>
</template>
<template v-else>
<el-form-item label="企业名称"> <el-form-item label="企业名称">
<el-input v-model="form.name" placeholder="如:名匠视界" /> <el-input v-model="form.name" placeholder="如:名匠视界" />
</el-form-item> </el-form-item>
</template>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12" v-if="!isSyncConfig">
<el-form-item label="域名"> <el-form-item label="域名">
<el-input v-model="form.domain" placeholder="example.com"> <el-input v-model="form.domain" placeholder="example.com">
<template #prefix <template #prefix
@@ -43,9 +60,6 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="应用 ID"> <el-form-item label="应用 ID">
<el-input v-model="form.appId" placeholder="1000001"> <el-input v-model="form.appId" placeholder="1000001">
@@ -55,7 +69,7 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="isSyncConfig ? 24 : 12">
<el-form-item label="应用密钥"> <el-form-item label="应用密钥">
<el-input <el-input
v-model="form.appSecret" v-model="form.appSecret"
@@ -64,8 +78,7 @@
show-password show-password
> >
<template #prefix <template #prefix
><el-icon><Key /></el-icon ><el-icon><Key /></el-icon></template>
></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -82,8 +95,13 @@
<template #footer> <template #footer>
<div class="org-ganization-footer"> <div class="org-ganization-footer">
<el-button @click="dialogVisible = false" round>取消</el-button> <el-button @click="dialogVisible = false" round size="large">取消</el-button>
<el-button type="primary" class="btn-confirm" round @click="handleSubmit" <el-button
type="primary"
class="btn-confirm"
round
size="large"
@click="handleSubmit"
>确认创建</el-button >确认创建</el-button
> >
</div> </div>
@@ -95,8 +113,8 @@
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ name: "AddOrgan" }); defineOptions({ name: "AddOrgan" });
const dialogVisible = defineModel("visible");
const dialogVisible = defineModel("visible") const isSyncConfig = ref(true);
const form = reactive({ const form = reactive({
name: "", name: "",
@@ -164,15 +182,50 @@ const handleSubmit = () => {};
} }
} }
.org-ganization-footer{ .org-ganization-footer {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.el-button{ .el-button {
width: 50%; width: 50%;
} }
} }
}
.sync-card {
display: flex;
align-items: center;
padding: 18px 20px;
background-color: #f8faff;
border-radius: 16px;
border: 1px solid #edf2f9;
flex: 1;
&__content {
display: flex;
flex-direction: column;
gap: 8px; // 标题和描述的间距
flex: 1;
line-height: 1;
}
&__title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1d2129; // 深色标题
}
&__desc {
margin: 0;
font-size: 14px;
color: #86909c; // 灰色描述
}
&__action {
margin-left: 24px;
flex-shrink: 0;
}
} }
} }
</style> </style>

View File

@@ -4,10 +4,12 @@
<div class="organization-tabs"> <div class="organization-tabs">
<stageBreadcrumbs title="组织管理"> <stageBreadcrumbs title="组织管理">
<template #content> <template #content>
<OverflowTabs v-model="activeTab" :items="tabList" :height="60"/> <OverflowTabs :itemMap="{id:'id',label:'name'}" v-model="activeTab" :items="tabList" :height="60" />
</template> </template>
<template #action> <template #action>
<el-button type="primary" :icon="'Plus'" plain @click="onAddGroup">新增集团</el-button> <el-button type="primary" :icon="'Plus'" plain @click="onAddGroup"
>新增集团</el-button
>
</template> </template>
</stageBreadcrumbs> </stageBreadcrumbs>
</div> </div>
@@ -18,7 +20,7 @@
<div class="mj-panel-title org-tree-head">组织架构</div> <div class="mj-panel-title org-tree-head">组织架构</div>
<div class="org-tree-search"> <div class="org-tree-search">
<el-input <el-input
v-model="search" v-model="filterText"
:prefix-icon="'Search'" :prefix-icon="'Search'"
placeholder="搜索部门或公司" placeholder="搜索部门或公司"
/> />
@@ -26,7 +28,12 @@
</div> </div>
<div class="org-tree-list"> <div class="org-tree-list">
<el-tree <el-tree
:data="data" v-loading="treeLoading"
ref="treeRef"
:data="treeData"
lazy
:load="loadNode"
:filter-node-method="filterNode"
:props="defaultProps" :props="defaultProps"
@node-click="handleNodeClick" @node-click="handleNodeClick"
> >
@@ -39,10 +46,11 @@
:hover-color="'#67c23a'" :hover-color="'#67c23a'"
:color="getIconColor(node)" :color="getIconColor(node)"
/> />
<span>{{ node.label }}</span>
<AutoTooltip :content="node.label" class="tree-node-label" />
</div> </div>
<div class="org-tree-item-right"> <div class="org-tree-item-right">
<el-icon :size="15"> <el-icon :size="15" @click.stop="onOrgTreeDelete(node,data)">
<Delete /> <Delete />
</el-icon> </el-icon>
</div> </div>
@@ -62,17 +70,17 @@
<div class="mj-organization-card organization-info"> <div class="mj-organization-card organization-info">
<el-tabs v-model="activeName" class="organization-info-tabs"> <el-tabs v-model="activeName" class="organization-info-tabs">
<el-tab-pane label="基础信息" name="baseInfo"> <el-tab-pane label="基础信息" name="baseInfo">
<OrganizationDetail /> <OrganizationDetail :info="detailInfo" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="动态日志" name="auditLogs"> <el-tab-pane label="动态日志" name="auditLogs">
<AuditLogs /> <AuditLogs :info="detailInfo"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</div> </div>
<!-- 添加集团--> <!-- 添加集团-->
<addOrgan v-model:visible="showAddOrgan"/> <addOrgan v-model:visible="showAddOrgan" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -80,31 +88,62 @@ import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
import OverflowTabs from "@/components/overflowTabs/index.vue"; import OverflowTabs from "@/components/overflowTabs/index.vue";
import AuditLogs from "./auditLogs.vue"; import AuditLogs from "./auditLogs.vue";
import OrganizationDetail from "./organizationDetail.vue"; import OrganizationDetail from "./organizationDetail.vue";
import DynamicSvgIcon from '@/components/dynamicSvgIcon/index.vue'; import DynamicSvgIcon from "@/components/dynamicSvgIcon/index.vue";
import addOrgan from "./addOrgan.vue"; import addOrgan from "./addOrgan.vue";
import AutoTooltip from "@/components/autoTooltip/index.vue";
defineOptions({ name: "Organization" }); import { debounce } from 'lodash-es'
import {
const addValue = ref(""); getEnterprise,
const search = ref(""); addEnterprise,
const activeName = ref("baseInfo"); enableEnterprise,
disableEnterprise,
const showAddOrgan = ref(false); getEnterpriseOrg,
getEnterpriseUser,
// 集团Tabs切换 getEnterpriseDetail,
const activeTab = ref(1); getEnterpriseOrgDetail
const tabList = ref([ } from "@/api/stage/organization";
{ id: 1, label: '集团1' },
{ id: 2, label: '集团2' },
{ id: 3, label: '集团3' },
{ id: 4, label: '集团4' },
{ id: 5, label: '集团5' },
{ id: 6, label: '集团6' },
]);
interface Tree { interface Tree {
label: string; label: string;
children?: Tree[]; children?: Tree[];
} }
defineOptions({ name: "Organization" });
const addValue = ref("");
const activeName = ref("baseInfo");
const showAddOrgan = ref(false);
const treeData = ref([]);
// 集团Tabs切换
const activeTab = ref('');
const tabList = ref([]);
const filterText = ref('');
const treeRef = ref(null);
const treeLoading = ref(false);
const detailInfo = reactive<Record<string, any>>({});
const defaultProps = {
children: "children",
label: "name",
isLeaf:'leaf'
};
// 加载子机构
const loadNode = async (node, resolve, reject) =>{
try {
const response = await getEnterpriseOrg(node.id);
resolve(response);
} catch (error) {
console.log('search error:',error);
}
}
watch(filterText, (val) => {
treeRef.value!.filter(val)
})
// 过滤节点
const filterNode = (value: string, data:Record<string, any>) => {
if (!value) return true
return data.name.includes(value)
}
// 添加集团 // 添加集团
const onAddGroup = () => { const onAddGroup = () => {
@@ -113,93 +152,114 @@ const onAddGroup = () => {
// 获取图标组件 // 获取图标组件
const getIconComponent = (node: any) => { const getIconComponent = (node: any) => {
if (node.level === 1) { if (node.level === 1) {
return 'building'; // 一级节点使用 return "building"; // 一级节点使用
} else if (node.level === 2) { } else if (node.level === 2) {
return `flag`; // 二级节点使用其他图标 return `flag`; // 二级节点使用其他图标
} else { } else {
return 'flag'; // 更深层级的默认图标 return "flag"; // 更深层级的默认图标
} }
}; };
// 获取图标颜色(可选) // 获取图标颜色(可选)
const getIconColor = (node: any) => { const getIconColor = (node: any) => {
return '#9EADC2'; return "#9EADC2";
}; };
// 获取企业列表
const getEnterpriseList = async () => {
try {
const queryParams = { name: "", domain: "" };
const res = await getEnterprise(queryParams);
activeTab.value = res[0]?.id || "";
getOrgTree(activeTab.value);
tabList.value = res;
} catch (error) {
console.log('getEnterpriseList error:',error);
}
};
// 获取组织架构数
const getOrgTree = async (id:number|string) => {
treeLoading.value = true;
try {
const response = await getEnterpriseOrg(id);
treeData.value = response;
} catch (error) {
console.log('get org error:',error);
treeData.value = [];
} finally {
treeLoading.value = false;
}
};
// 获取机构-企业详情数据
const apiMap = {
1: getEnterpriseOrg, //机构详情
2: getEnterpriseUser //员工详情
}
const orgDetail = async (data:Record<string,any>) =>{
const { kind,id } = data
try {
const response = await apiMap[kind](id);
Object.assign(detailInfo, response);
} catch (error) {
console.log('orgDetail error:',error);
}
}
// 获取企业详情
const getEnterpriseOrgDetail = async () => {
try {
const response = await getEnterpriseDetail();
console.log('获取当前的企业详情数据信息:',response);
} catch (error) {
console.log('getEnterpriseDetail error:',error);
}
}
// 删除部门-人员-组织
const onOrgTreeDelete = async (node,data) => {
ElMessageBox.confirm("确定要删除吗?", "提示", { type: "warning" })
.then(async () => {
try {
// await disableEnterprise(data.id);
treeRef.value && treeRef.value.remove(node);
} catch (error) {
console.log('fetch error',error);
}
})
}
// 请求机构
const debouncedGetOrgTree = debounce((id) => {
if(!id) return;
getOrgTree(id);
}, 300);
// 监听切换
watch(activeTab, (val) => {
debouncedGetOrgTree(val);
});
// 点击树节点获取数据
const handleNodeClick = (data: Tree) => { const handleNodeClick = (data: Tree) => {
console.log(data); orgDetail(data);
}; };
const data: Tree[] = [
{
label: "Level one 1",
children: [
{
label: "Level two 1-1",
children: [
{
label: "Level three 1-1-1",
},
],
},
],
},
{
label: "Level one 2",
children: [
{
label: "Level two 2-1",
children: [
{
label: "Level three 2-1-1",
},
],
},
{
label: "Level two 2-2",
children: [
{
label: "Level three 2-2-1",
},
],
},
],
},
{
label: "Level one 3",
children: [
{
label: "Level two 3-1",
children: [
{
label: "Level three 3-1-1",
},
],
},
{
label: "Level two 3-2",
children: [
{
label: "Level three 3-2-1",
},
],
},
],
},
];
const defaultProps = { onMounted(()=>{
children: "children", Promise.all([getEnterpriseOrgDetail(),getEnterpriseList()]);
label: "label", })
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "sass:math"; @use "sass:math";
.mj-organization { .mj-organization {
.organization-tabs{ height: 100%;
:deep(.stage-breadcrumbs){ .organization-tabs {
// border-bottom: none; :deep(.stage-breadcrumbs) {
padding:0; padding: 0;
} }
} }
.mj-organization-card { .mj-organization-card {
@@ -210,6 +270,7 @@ const defaultProps = {
box-shadow: 0 0 6px #e9e8e8; box-shadow: 0 0 6px #e9e8e8;
} }
.organization-content { .organization-content {
height: calc(100% - 60px);
display: flex; display: flex;
gap: 16px; gap: 16px;
.org-tree { .org-tree {
@@ -229,25 +290,37 @@ const defaultProps = {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: math.div($mj-padding-standard, 2) $mj-padding-standard; padding: math.div($mj-padding-standard, 2) $mj-padding-standard;
.org-tree-item{ .org-tree-item {
flex: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.org-tree-item-right{ min-width: 0;
color:#A5ADB8; width: 100%;
&:hover{ .org-tree-item-right {
color:#2065FC; color: #a5adb8;
&:hover {
color: #2065fc;
} }
} }
:deep(.el-icon){ :deep(.el-icon) {
vertical-align: middle; vertical-align: middle;
} }
.org-tree-item-left{ .org-tree-item-left {
:deep(.el-icon){ display: flex;
align-items: center;
flex: 1;
min-width: 0;
:deep(.el-icon) {
margin-right: 3px; margin-right: 3px;
} }
.tree-node-label{
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
} }
} }
} }

View File

@@ -143,7 +143,44 @@ const addDynamicRoutes = async () => {
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单) // 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
let allRoutes:any[] = []; let allRoutes:any[] = [];
if (userStore.isBackendUser) { if (userStore.isBackendUser) {
const backendResponse = await getRouteMenus(); // const backendResponse = await getRouteMenus();
const backendResponse = [
{
"name": "字典管理",
"code": "dict",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "组织管理",
"code": "origanization",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "人员管理",
"code": "personnel",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "权限管理",
"code": "permission",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "流程管理",
"code": "flow",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
}
];
allRoutes = [ allRoutes = [
{ {
code: "stage", code: "stage",