fix:完善组织管理逻辑
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
||||
Comment: typeof import('./src/components/comment/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']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
|
||||
39
src/components/autoTooltip/index.vue
Normal file
39
src/components/autoTooltip/index.vue
Normal 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>
|
||||
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<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
|
||||
v-for="(item, index) in visibleItems"
|
||||
:key="item.id"
|
||||
:key="item[itemMap.id]"
|
||||
class="tab-item"
|
||||
:class="{ active: modelValue === item.id }"
|
||||
@click="$emit('update:modelValue', item.id)"
|
||||
:class="{ active: modelValue === item[itemMap.id] }"
|
||||
@click="$emit('update:modelValue', item[itemMap.id])"
|
||||
>
|
||||
<span class="tab-text">{{ item.label }}</span>
|
||||
<span class="tab-text">{{ item[itemMap.label] }}</span>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
@@ -17,7 +23,7 @@
|
||||
@command="handleCommand"
|
||||
class="more-dropdown"
|
||||
>
|
||||
<div class="tab-item more-trigger">
|
||||
<div class="tab-item more-trigger" :class="{ 'is-more-active': isHiddenActive }">
|
||||
<span>更多</span>
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
@@ -25,11 +31,12 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in hiddenItems"
|
||||
:key="item.id"
|
||||
:command="item.id"
|
||||
|
||||
:key="item[itemMap.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-menu>
|
||||
</template>
|
||||
@@ -42,53 +49,94 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number],
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 32,
|
||||
},
|
||||
items: { type: Array, default: () => [] },
|
||||
itemMap: { type: Object, default: () => ({ id: 'id', label: 'label' }) },
|
||||
height: { type: Number, default: 32 },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const containerRef = ref(null);
|
||||
const ghostRef = ref(null);
|
||||
const splitIndex = ref(props.items.length);
|
||||
const itemWidths = ref([]); // 缓存所有项的宽度
|
||||
let timer = null;
|
||||
|
||||
const activeBarStyle = ref({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
opacity: 0,
|
||||
});
|
||||
const activeBarStyle = ref({ width: "0px", left: "0px", opacity: 0 });
|
||||
|
||||
const visibleItems = computed(() => props.items.slice(0, splitIndex.value));
|
||||
const hiddenItems = computed(() => props.items.slice(splitIndex.value));
|
||||
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 () => {
|
||||
await nextTick();
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// 1. 检查激活项是否在可见区域
|
||||
const activeIndex = visibleItems.value.findIndex((item) => item.id === props.modelValue);
|
||||
const activeIndex = visibleItems.value.findIndex(item => item[props.itemMap.id] === props.modelValue);
|
||||
|
||||
if (activeIndex >= 0) {
|
||||
// 激活项在可见区域:计算位置并显示下划线
|
||||
const tabItems = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
|
||||
const tabItems = containerRef.value.querySelectorAll(".tabs-wrapper > .tab-item:not(.more-trigger)");
|
||||
const activeElement = tabItems[activeIndex];
|
||||
|
||||
if (activeElement) {
|
||||
const rect = activeElement.getBoundingClientRect();
|
||||
const containerRect = containerRef.value.getBoundingClientRect();
|
||||
|
||||
activeBarStyle.value = {
|
||||
width: `${rect.width * 0.6}px`,
|
||||
left: `${rect.left - containerRect.left + rect.width * 0.2}px`,
|
||||
@@ -97,66 +145,44 @@ const updateActiveBar = async () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果在隐藏区域或未找到,将下划线宽度设为 0
|
||||
activeBarStyle.value = {
|
||||
...activeBarStyle.value,
|
||||
width: "0px",
|
||||
opacity: 0,
|
||||
};
|
||||
activeBarStyle.value.opacity = 0;
|
||||
};
|
||||
|
||||
const calculateLayout = () => {
|
||||
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(() => {
|
||||
const handleResize = () => {
|
||||
calculateLayout();
|
||||
nextTick(() => {
|
||||
updateActiveBar();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
let resizeObserver = null;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
measureWidths();
|
||||
calculateLayout();
|
||||
updateActiveBar();
|
||||
resizeObserver = new ResizeObserver(() => debouncedCalc());
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
// 尽量不要在 Resize 里用太长的 debounce,
|
||||
// 否则你会感觉 Tab 是“跳”出来的,而不是“滑”出来的
|
||||
handleResize();
|
||||
});
|
||||
|
||||
if (containerRef.value) resizeObserver.observe(containerRef.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
if (timer) clearTimeout(timer);
|
||||
resizeObserver?.disconnect();
|
||||
clearTimeout(timer);
|
||||
});
|
||||
|
||||
const handleCommand = (id) => emit("update:modelValue", id);
|
||||
|
||||
watch(() => props.modelValue, () => updateActiveBar());
|
||||
watch(() => props.items, async () => {
|
||||
splitIndex.value = props.items.length;
|
||||
await nextTick();
|
||||
measureWidths();
|
||||
calculateLayout();
|
||||
updateActiveBar();
|
||||
}, { deep: true });
|
||||
@@ -165,7 +191,17 @@ watch(() => props.items, async () => {
|
||||
<style scoped lang="scss">
|
||||
.tabs-outer-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
// 关键:测量层不可见且不占位,但必须渲染以获取宽度
|
||||
.ghost-wrapper {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
display: flex;
|
||||
@@ -173,6 +209,7 @@ watch(() => props.items, async () => {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%; // 确保占满父级
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
@@ -183,37 +220,36 @@ watch(() => props.items, async () => {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.is-more-active {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.more-trigger {
|
||||
margin-left: auto;
|
||||
border-left: 1px solid #f0f2f5;
|
||||
|
||||
// 选中更多中的数据时,仅文字和图标变蓝
|
||||
&.is-more-active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.active-bar {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active-item-overflow-tabs {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -38,8 +38,8 @@
|
||||
>
|
||||
<div class="info-label">{{ base.name }}</div>
|
||||
<div class="info-value">
|
||||
<el-button link type="primary" v-if="base.slotName === 'link'">{{base.value}} ></el-button>
|
||||
<span v-else>{{ base.value }}</span>
|
||||
<el-button link type="primary" v-if="base.slotName === 'link'" @click="onOpenWeixin">已开启 ></el-button>
|
||||
<span v-else>{{ info[base.value] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,20 +78,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
info:{
|
||||
Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const turnOn = ref(true);
|
||||
const systemList = [
|
||||
{
|
||||
name: "生效时间",
|
||||
value: "2024-01-01",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "过期时间",
|
||||
value: "永久生效",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "最后更新",
|
||||
value: "2025-12-29 16:06",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "更新人",
|
||||
@@ -103,20 +108,20 @@ const systemList = [
|
||||
const baseList = [
|
||||
{
|
||||
name: "部门/公司名称",
|
||||
value: "广州分公司",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "上级单位",
|
||||
value: "集团1",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "同步企微",
|
||||
value: "已开启",
|
||||
value: "",
|
||||
slotName: "link",
|
||||
},
|
||||
{
|
||||
name: "部门负责人",
|
||||
value: "赵康, 李思奇, 董峥",
|
||||
value: "",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -125,6 +130,14 @@ const baseList = [
|
||||
const onButtonCheck = () =>{
|
||||
turnOn.value = !turnOn.value;
|
||||
}
|
||||
|
||||
// 开启微信
|
||||
|
||||
const onOpenWeixin = () =>{
|
||||
console.log('onOpenWeixin')
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -20,12 +20,29 @@
|
||||
</template>
|
||||
|
||||
<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-input v-model="form.name" placeholder="如:名匠视界" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-col :span="12" v-if="!isSyncConfig">
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="form.domain" placeholder="example.com">
|
||||
<template #prefix
|
||||
@@ -43,9 +60,6 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应用 ID">
|
||||
<el-input v-model="form.appId" placeholder="1000001">
|
||||
@@ -55,7 +69,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-col :span="isSyncConfig ? 24 : 12">
|
||||
<el-form-item label="应用密钥">
|
||||
<el-input
|
||||
v-model="form.appSecret"
|
||||
@@ -64,8 +78,7 @@
|
||||
show-password
|
||||
>
|
||||
<template #prefix
|
||||
><el-icon><Key /></el-icon
|
||||
></template>
|
||||
><el-icon><Key /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -82,8 +95,13 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="org-ganization-footer">
|
||||
<el-button @click="dialogVisible = false" round>取消</el-button>
|
||||
<el-button type="primary" class="btn-confirm" round @click="handleSubmit"
|
||||
<el-button @click="dialogVisible = false" round size="large">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="btn-confirm"
|
||||
round
|
||||
size="large"
|
||||
@click="handleSubmit"
|
||||
>确认创建</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -95,8 +113,8 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: "AddOrgan" });
|
||||
|
||||
|
||||
const dialogVisible = defineModel("visible")
|
||||
const dialogVisible = defineModel("visible");
|
||||
const isSyncConfig = ref(true);
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
@@ -172,7 +190,42 @@ const handleSubmit = () => {};
|
||||
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>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
<div class="organization-tabs">
|
||||
<stageBreadcrumbs title="组织管理">
|
||||
<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 #action>
|
||||
<el-button type="primary" :icon="'Plus'" plain @click="onAddGroup">新增集团</el-button>
|
||||
<el-button type="primary" :icon="'Plus'" plain @click="onAddGroup"
|
||||
>新增集团</el-button
|
||||
>
|
||||
</template>
|
||||
</stageBreadcrumbs>
|
||||
</div>
|
||||
@@ -18,7 +20,7 @@
|
||||
<div class="mj-panel-title org-tree-head">组织架构</div>
|
||||
<div class="org-tree-search">
|
||||
<el-input
|
||||
v-model="search"
|
||||
v-model="filterText"
|
||||
:prefix-icon="'Search'"
|
||||
placeholder="搜索部门或公司"
|
||||
/>
|
||||
@@ -26,7 +28,12 @@
|
||||
</div>
|
||||
<div class="org-tree-list">
|
||||
<el-tree
|
||||
:data="data"
|
||||
v-loading="treeLoading"
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
lazy
|
||||
:load="loadNode"
|
||||
:filter-node-method="filterNode"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
@@ -39,10 +46,11 @@
|
||||
:hover-color="'#67c23a'"
|
||||
:color="getIconColor(node)"
|
||||
/>
|
||||
<span>{{ node.label }}</span>
|
||||
|
||||
<AutoTooltip :content="node.label" class="tree-node-label" />
|
||||
</div>
|
||||
<div class="org-tree-item-right">
|
||||
<el-icon :size="15">
|
||||
<el-icon :size="15" @click.stop="onOrgTreeDelete(node,data)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
@@ -62,10 +70,10 @@
|
||||
<div class="mj-organization-card organization-info">
|
||||
<el-tabs v-model="activeName" class="organization-info-tabs">
|
||||
<el-tab-pane label="基础信息" name="baseInfo">
|
||||
<OrganizationDetail />
|
||||
<OrganizationDetail :info="detailInfo" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="动态日志" name="auditLogs">
|
||||
<AuditLogs />
|
||||
<AuditLogs :info="detailInfo"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
@@ -80,31 +88,62 @@ import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
|
||||
import OverflowTabs from "@/components/overflowTabs/index.vue";
|
||||
import AuditLogs from "./auditLogs.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";
|
||||
|
||||
defineOptions({ name: "Organization" });
|
||||
|
||||
const addValue = ref("");
|
||||
const search = ref("");
|
||||
const activeName = ref("baseInfo");
|
||||
|
||||
const showAddOrgan = ref(false);
|
||||
|
||||
// 集团Tabs切换
|
||||
const activeTab = ref(1);
|
||||
const tabList = ref([
|
||||
{ id: 1, label: '集团1' },
|
||||
{ id: 2, label: '集团2' },
|
||||
{ id: 3, label: '集团3' },
|
||||
{ id: 4, label: '集团4' },
|
||||
{ id: 5, label: '集团5' },
|
||||
{ id: 6, label: '集团6' },
|
||||
]);
|
||||
import AutoTooltip from "@/components/autoTooltip/index.vue";
|
||||
import { debounce } from 'lodash-es'
|
||||
import {
|
||||
getEnterprise,
|
||||
addEnterprise,
|
||||
enableEnterprise,
|
||||
disableEnterprise,
|
||||
getEnterpriseOrg,
|
||||
getEnterpriseUser,
|
||||
getEnterpriseDetail,
|
||||
getEnterpriseOrgDetail
|
||||
} from "@/api/stage/organization";
|
||||
interface Tree {
|
||||
label: string;
|
||||
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 = () => {
|
||||
@@ -113,92 +152,113 @@ const onAddGroup = () => {
|
||||
// 获取图标组件
|
||||
const getIconComponent = (node: any) => {
|
||||
if (node.level === 1) {
|
||||
return 'building'; // 一级节点使用
|
||||
return "building"; // 一级节点使用
|
||||
} else if (node.level === 2) {
|
||||
return `flag`; // 二级节点使用其他图标
|
||||
} else {
|
||||
return 'flag'; // 更深层级的默认图标
|
||||
return "flag"; // 更深层级的默认图标
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图标颜色(可选)
|
||||
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) => {
|
||||
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 = {
|
||||
children: "children",
|
||||
label: "label",
|
||||
};
|
||||
onMounted(()=>{
|
||||
Promise.all([getEnterpriseOrgDetail(),getEnterpriseList()]);
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "sass:math";
|
||||
.mj-organization {
|
||||
height: 100%;
|
||||
.organization-tabs {
|
||||
:deep(.stage-breadcrumbs) {
|
||||
// border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -210,6 +270,7 @@ const defaultProps = {
|
||||
box-shadow: 0 0 6px #e9e8e8;
|
||||
}
|
||||
.organization-content {
|
||||
height: calc(100% - 60px);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.org-tree {
|
||||
@@ -230,14 +291,15 @@ const defaultProps = {
|
||||
overflow: auto;
|
||||
padding: math.div($mj-padding-standard, 2) $mj-padding-standard;
|
||||
.org-tree-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
.org-tree-item-right {
|
||||
color:#A5ADB8;
|
||||
color: #a5adb8;
|
||||
&:hover {
|
||||
color:#2065FC;
|
||||
color: #2065fc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,9 +307,20 @@ const defaultProps = {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.org-tree-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
:deep(.el-icon) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.tree-node-label{
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,44 @@ const addDynamicRoutes = async () => {
|
||||
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||
let allRoutes:any[] = [];
|
||||
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 = [
|
||||
{
|
||||
code: "stage",
|
||||
|
||||
Reference in New Issue
Block a user