refactor: 项目结构优化调整
This commit is contained in:
@@ -3,16 +3,28 @@
|
||||
<div class="ai-assistant">
|
||||
<!-- AI 助手图标按钮 -->
|
||||
<el-button
|
||||
v-if="!dialogVisible"
|
||||
v-if="!dialogVisible && !fabCollapsed"
|
||||
class="ai-fab-button"
|
||||
type="primary"
|
||||
circle
|
||||
size="large"
|
||||
:style="fabStyle"
|
||||
@contextmenu.prevent="fabCollapsed = true"
|
||||
@click="handleOpen"
|
||||
>
|
||||
<div class="i-svg:ai ai-icon" />
|
||||
</el-button>
|
||||
|
||||
<!-- 收缩态:贴边小标签,避免遮挡表单控件 -->
|
||||
<div
|
||||
v-if="!dialogVisible && fabCollapsed"
|
||||
class="ai-fab-tab"
|
||||
:style="fabStyle"
|
||||
@click="fabCollapsed = false"
|
||||
>
|
||||
AI
|
||||
</div>
|
||||
|
||||
<!-- AI 对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -107,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { onBeforeUnmount, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
@@ -151,6 +163,111 @@ const command = ref("");
|
||||
const loading = ref(false);
|
||||
const response = ref<AiResponse | null>(null);
|
||||
|
||||
const fabCollapsed = useStorage<boolean>("vea:ui:ai_assistant_fab_collapsed", false);
|
||||
|
||||
const fabRight = ref(30);
|
||||
const fabBottom = ref(80);
|
||||
const fabStyle = computed(() => ({
|
||||
right: `${fabRight.value}px`,
|
||||
bottom: `${fabBottom.value}px`,
|
||||
}));
|
||||
|
||||
const isElementVisible = (el: Element) => {
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.display === "none" || style.visibility === "hidden") {
|
||||
return false;
|
||||
}
|
||||
return (el as HTMLElement).getClientRects().length > 0;
|
||||
};
|
||||
|
||||
const getActiveRightDrawerWidth = (): number => {
|
||||
const drawers = Array.from(document.querySelectorAll(".el-drawer"));
|
||||
for (let i = drawers.length - 1; i >= 0; i--) {
|
||||
const drawer = drawers[i] as HTMLElement;
|
||||
if (!isElementVisible(drawer)) {
|
||||
continue;
|
||||
}
|
||||
const rect = drawer.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.right >= window.innerWidth - 1) {
|
||||
return rect.width;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const updateFabPosition = () => {
|
||||
const safeMargin = 24;
|
||||
const drawerWidth = getActiveRightDrawerWidth() || 0;
|
||||
const baseRight = drawerWidth + 30;
|
||||
|
||||
// base position
|
||||
const nextRight = baseRight;
|
||||
let nextBottom = 80;
|
||||
|
||||
// Avoid Element Plus popper overlays (select dropdown, icon picker, date picker, etc.)
|
||||
// If the FAB would overlap any visible popper, push it upward.
|
||||
const fabSize = fabCollapsed.value ? 42 : 60;
|
||||
const computeFabRect = (rightPx: number, bottomPx: number) => {
|
||||
const right = window.innerWidth - rightPx;
|
||||
const left = right - fabSize;
|
||||
const bottom = window.innerHeight - bottomPx;
|
||||
const top = bottom - fabSize;
|
||||
return { left, right, top, bottom };
|
||||
};
|
||||
|
||||
const intersects = (
|
||||
a: { left: number; right: number; top: number; bottom: number },
|
||||
b: DOMRect
|
||||
) => {
|
||||
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
|
||||
};
|
||||
|
||||
const poppers = Array.from(document.querySelectorAll(".el-popper"));
|
||||
for (const popper of poppers) {
|
||||
if (!isElementVisible(popper)) {
|
||||
continue;
|
||||
}
|
||||
const rect = (popper as HTMLElement).getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateFabRect = computeFabRect(nextRight, nextBottom);
|
||||
if (intersects(candidateFabRect, rect)) {
|
||||
const requiredBottom = Math.ceil(window.innerHeight - rect.top + safeMargin);
|
||||
nextBottom = Math.max(nextBottom, requiredBottom);
|
||||
}
|
||||
}
|
||||
|
||||
// clamp so the button doesn't get pushed off-screen
|
||||
const maxBottom = window.innerHeight - fabSize - safeMargin;
|
||||
nextBottom = Math.min(nextBottom, Math.max(0, maxBottom));
|
||||
|
||||
fabRight.value = nextRight + (drawerWidth > 0 ? safeMargin : 0);
|
||||
fabBottom.value = nextBottom;
|
||||
};
|
||||
|
||||
watch(
|
||||
fabCollapsed,
|
||||
() => {
|
||||
updateFabPosition();
|
||||
},
|
||||
{ flush: "post" }
|
||||
);
|
||||
|
||||
let domObserver: MutationObserver | null = null;
|
||||
let rafId: number | null = null;
|
||||
|
||||
const scheduleUpdateFabPosition = () => {
|
||||
if (rafId != null) {
|
||||
return;
|
||||
}
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
rafId = null;
|
||||
updateFabPosition();
|
||||
});
|
||||
};
|
||||
|
||||
// 快捷命令示例
|
||||
const examples = [
|
||||
"修改test用户的姓名为测试人员",
|
||||
@@ -550,7 +667,32 @@ const executeAction = async (action: AiAction) => {
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onMounted(() => {
|
||||
updateFabPosition();
|
||||
window.addEventListener("resize", updateFabPosition);
|
||||
|
||||
domObserver = new MutationObserver(() => {
|
||||
scheduleUpdateFabPosition();
|
||||
});
|
||||
domObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style"],
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updateFabPosition);
|
||||
if (domObserver) {
|
||||
domObserver.disconnect();
|
||||
domObserver = null;
|
||||
}
|
||||
if (rafId != null) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
|
||||
if (navigationTimer) {
|
||||
clearTimeout(navigationTimer);
|
||||
navigationTimer = null;
|
||||
@@ -566,8 +708,6 @@ onBeforeUnmount(() => {
|
||||
.ai-assistant {
|
||||
.ai-fab-button {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
bottom: 80px;
|
||||
z-index: 9999;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
@@ -584,6 +724,24 @@ onBeforeUnmount(() => {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-fab-tab {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 4px 12px rgba(2, 119, 252, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assistant-dialog {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleTenantSwitch">
|
||||
<div class="tenant-select">
|
||||
<el-icon class="tenant-select__icon"><OfficeBuilding /></el-icon>
|
||||
<span class="tenant-select__name">{{ currentTenantName }}</span>
|
||||
<el-icon class="tenant-select__arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:command="tenant.id"
|
||||
:class="{ 'is-active': tenant.id === currentTenantId }"
|
||||
>
|
||||
<div class="tenant-item">
|
||||
<span class="tenant-item__name">{{ tenant.name }}</span>
|
||||
<el-icon v-if="tenant.id === currentTenantId" class="tenant-item__check">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { OfficeBuilding, ArrowDown, Check } from "@element-plus/icons-vue";
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
// 当前租户名称
|
||||
const currentTenantName = computed(() => {
|
||||
if (tenantStore.currentTenant?.name) {
|
||||
return tenantStore.currentTenant.name;
|
||||
}
|
||||
// 如果当前租户信息不存在,尝试从租户列表中查找
|
||||
if (tenantStore.currentTenantId) {
|
||||
const tenant = tenantStore.tenantList.find((t) => t.id === tenantStore.currentTenantId);
|
||||
if (tenant) {
|
||||
return tenant.name;
|
||||
}
|
||||
}
|
||||
return "未选择租户";
|
||||
});
|
||||
|
||||
// 当前租户ID
|
||||
const currentTenantId = computed(() => tenantStore.currentTenantId);
|
||||
|
||||
// 租户列表
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*/
|
||||
async function handleTenantSwitch(tenantId: number) {
|
||||
if (tenantId === currentTenantId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tenantStore.switchTenant(tenantId);
|
||||
ElMessage.success("切换租户成功");
|
||||
// 刷新页面以重新加载菜单和权限
|
||||
window.location.reload();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:获取租户列表
|
||||
onMounted(() => {
|
||||
tenantStore.fetchTenantList().catch((error) => {
|
||||
console.error("获取租户列表失败:", error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&__icon {
|
||||
margin-right: 6px;
|
||||
font-size: 18px;
|
||||
color: inherit;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&__name {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
|
||||
.tenant-select__icon,
|
||||
.tenant-select__name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tenant-select__arrow {
|
||||
color: var(--el-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__check {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
</style>
|
||||
@@ -1,187 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择租户"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<div v-if="loading" class="tenant-dialog-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载租户列表...</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tenantList.length === 0" class="tenant-dialog-empty">
|
||||
<el-empty description="暂无可用租户" />
|
||||
</div>
|
||||
|
||||
<el-radio-group v-else v-model="selectedTenantId" class="tenant-radio-group">
|
||||
<el-radio
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:label="tenant.id"
|
||||
class="tenant-radio-item"
|
||||
>
|
||||
<div class="tenant-radio-content">
|
||||
<div class="tenant-radio-content__name">{{ tenant.name }}</div>
|
||||
<div v-if="tenant.code" class="tenant-radio-content__code">{{ tenant.code }}</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="switching" @click="handleConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
confirm: [];
|
||||
}>();
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const switching = ref(false);
|
||||
const selectedTenantId = ref<number | null>(null);
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
// 监听对话框打开,加载租户列表
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadTenantList();
|
||||
// 默认选择当前租户或第一个租户
|
||||
selectedTenantId.value = tenantStore.currentTenantId || tenantList.value[0]?.id || null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载租户列表
|
||||
*/
|
||||
async function loadTenantList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await tenantStore.fetchTenantList();
|
||||
// 如果列表为空,自动关闭对话框
|
||||
if (tenantList.value.length === 0) {
|
||||
visible.value = false;
|
||||
ElMessage.warning("您暂无可用租户");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "获取租户列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
async function handleConfirm() {
|
||||
if (!selectedTenantId.value) {
|
||||
ElMessage.warning("请选择租户");
|
||||
return;
|
||||
}
|
||||
|
||||
switching.value = true;
|
||||
try {
|
||||
await tenantStore.switchTenant(selectedTenantId.value);
|
||||
ElMessage.success("切换租户成功");
|
||||
visible.value = false;
|
||||
emit("confirm");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
} finally {
|
||||
switching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
// 如果用户没有租户,不允许取消
|
||||
if (tenantList.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-dialog-loading {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.tenant-dialog-empty {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tenant-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tenant-radio-item {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-radio__input.is-checked) {
|
||||
.el-radio__inner {
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-radio-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__code {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<el-dropdown v-if="showTenantSelector" trigger="click" @command="handleSwitchTenant">
|
||||
<div class="tenant-selector">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span class="tenant-name">{{ currentTenantName }}</span>
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:command="tenant.id"
|
||||
:disabled="tenant.id === currentTenantId"
|
||||
>
|
||||
<div class="tenant-item">
|
||||
<span>{{ tenant.name }}</span>
|
||||
<el-icon v-if="tenant.id === currentTenantId" class="check-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { OfficeBuilding, ArrowDown, Check } from "@element-plus/icons-vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
/**
|
||||
* 租户切换器组件
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前租户名称
|
||||
* - 下拉列表展示所有可访问的租户
|
||||
* - 点击切换租户
|
||||
* - 切换后刷新页面以重新加载数据
|
||||
*
|
||||
* 使用条件:
|
||||
* - 需要在 .env 中设置 VITE_APP_TENANT_ENABLED=true
|
||||
* - 后端需要启用多租户功能
|
||||
* - 用户至少属于一个租户
|
||||
*/
|
||||
|
||||
// 多租户开关
|
||||
const TENANT_ENABLED = import.meta.env.VITE_APP_TENANT_ENABLED === "true";
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
// 当前租户ID
|
||||
const currentTenantId = computed(() => tenantStore.currentTenantId);
|
||||
|
||||
// 当前租户名称
|
||||
const currentTenantName = computed(() => {
|
||||
return tenantStore.currentTenant?.name || "未选择租户";
|
||||
});
|
||||
|
||||
// 租户列表
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
// 是否显示租户切换器(多租户开关启用 且 有租户列表)
|
||||
const showTenantSelector = computed(() => {
|
||||
return TENANT_ENABLED && tenantList.value.length > 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*/
|
||||
const handleSwitchTenant = async (tenantId: number) => {
|
||||
if (tenantId === currentTenantId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tenantStore.switchTenant(tenantId);
|
||||
ElMessage.success("切换租户成功");
|
||||
|
||||
// 刷新页面以重新加载数据(确保所有数据都基于新租户)
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || "切换租户失败");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tenant-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.tenant-name {
|
||||
margin: 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 150px;
|
||||
|
||||
.check-icon {
|
||||
margin-left: 10px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,33 @@
|
||||
<template>
|
||||
<el-select
|
||||
<el-dropdown
|
||||
v-if="tenantList.length > 0"
|
||||
v-model="currentTenantIdRef"
|
||||
placeholder="选择租户"
|
||||
style="width: 180px"
|
||||
@change="onChange"
|
||||
class="tenant-switcher"
|
||||
trigger="click"
|
||||
:hide-on-click="true"
|
||||
@command="onCommand"
|
||||
>
|
||||
<el-option v-for="item in tenantList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<div class="tenant-switcher__trigger">
|
||||
<span class="tenant-switcher__label">{{ currentTenantName }}</span>
|
||||
<el-icon class="tenant-switcher__icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="tenant-switcher__menu">
|
||||
<el-dropdown-item
|
||||
v-for="item in tenantList"
|
||||
:key="item.id"
|
||||
:command="item.id"
|
||||
:class="{ 'is-active': item.id === currentTenantIdRef }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -29,7 +45,62 @@ const currentTenantIdRef = computed<number | null>({
|
||||
},
|
||||
});
|
||||
|
||||
function onChange(tenantId: number) {
|
||||
const currentTenantName = computed(() => {
|
||||
const currentId = currentTenantIdRef.value;
|
||||
const fromList = tenantList.value.find((t) => t.id === currentId)?.name;
|
||||
return fromList || tenantStore.currentTenant?.name || "切换租户";
|
||||
});
|
||||
|
||||
function onCommand(tenantId: number) {
|
||||
if (tenantId === currentTenantIdRef.value) {
|
||||
return;
|
||||
}
|
||||
emit("change", tenantId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tenant-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
&__label {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tenant-switcher__trigger {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user