<template>
|
<el-container class="layout">
|
<el-aside width="15.98vw" class="side-wrap">
|
<div class="brand-block">
|
<img :src="siteLogo" alt="logo" class="brand-icon" />
|
<div class="brand-name">运维运营管理平台</div>
|
</div>
|
<el-menu
|
:default-active="active"
|
:default-openeds="defaultOpeneds"
|
unique-opened
|
router
|
class="side-menu"
|
background-color="#245ECC"
|
text-color="#dce8ff"
|
active-text-color="#245ECC"
|
>
|
<el-menu-item v-if="auth.hasPermission('dashboard:page')" index="/dashboard" class="home-item">
|
<el-icon><House /></el-icon>
|
<span>首页统计</span>
|
</el-menu-item>
|
|
<el-sub-menu
|
v-if="
|
auth.hasAnyPermission([
|
'config:approval-settings:page',
|
'config:booking-settings:page',
|
'config:visit-reasons:page',
|
'config:content-settings:page',
|
])
|
"
|
index="system"
|
>
|
<template #title>
|
<el-icon><Setting /></el-icon>
|
<span>系统管理</span>
|
</template>
|
<el-menu-item v-if="auth.hasPermission('config:approval-settings:page')" index="/system/approval-settings">审核配置</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('config:booking-settings:page')" index="/system/booking-settings">预约管理</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('config:visit-reasons:page')" index="/system/visit-reasons">来访事由配置</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('config:content-settings:page')" index="/system/content-settings">内容设置</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('config:sensitive-words:page')" index="/system/sensitive-words">敏感词库</el-menu-item>
|
</el-sub-menu>
|
|
<el-sub-menu
|
v-if="auth.hasAnyPermission(['sys:depts:page', 'sys:staff:page', 'sys:staff-groups:page'])"
|
index="staff"
|
>
|
<template #title>
|
<el-icon><User /></el-icon>
|
<span>员工管理</span>
|
</template>
|
<el-menu-item v-if="auth.hasPermission('sys:depts:page')" index="/staff/depts">部门</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('sys:staff:page')" index="/staff/employees">员工管理</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('sys:staff-groups:page')" index="/staff/groups">人员分组</el-menu-item>
|
</el-sub-menu>
|
|
<el-sub-menu
|
v-if="auth.hasAnyPermission(['sys:pass-areas:page', 'sys:local-units:page'])"
|
index="region"
|
>
|
<template #title>
|
<el-icon><Grid /></el-icon>
|
<span>区域管理</span>
|
</template>
|
<el-menu-item v-if="auth.hasPermission('sys:pass-areas:page')" index="/region/pass-areas">通行区域管理</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('sys:local-units:page')" index="/region/local-units">属地单位管理</el-menu-item>
|
</el-sub-menu>
|
|
<el-menu-item v-if="auth.hasPermission('sys:visitor-users:page')" index="/visitor-info" class="group-link">
|
<el-icon><UserFilled /></el-icon>
|
<span>访客信息</span>
|
</el-menu-item>
|
|
<el-sub-menu
|
v-if="auth.hasAnyPermission(['appointments:page', 'approvals:page'])"
|
index="logs"
|
>
|
<template #title>
|
<el-icon><Document /></el-icon>
|
<span>记录与日志</span>
|
</template>
|
<el-menu-item v-if="auth.hasPermission('appointments:page')" index="/appointments">预约记录</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('approvals:page')" index="/approvals">操作日志</el-menu-item>
|
</el-sub-menu>
|
|
<el-sub-menu
|
v-if="auth.hasAnyPermission(['sys:users:page', 'sys:roles:page'])"
|
index="account"
|
>
|
<template #title>
|
<el-icon><Wallet /></el-icon>
|
<span>账号管理</span>
|
</template>
|
<el-menu-item v-if="auth.hasPermission('sys:users:page')" index="/users">平台管理员</el-menu-item>
|
<el-menu-item v-if="auth.hasPermission('sys:roles:page')" index="/roles">角色</el-menu-item>
|
</el-sub-menu>
|
</el-menu>
|
</el-aside>
|
<el-container class="main-shell">
|
<div class="top-wrap">
|
<el-header class="header">
|
<div class="header-spacer"></div>
|
<div class="header-right">
|
|
<el-dropdown trigger="click" @command="handleUserCommand">
|
<div class="user-box">
|
<el-avatar v-if="auth.avatarUrl" :size="30" :src="auth.avatarUrl" class="hdr-av" />
|
<el-avatar v-else :size="30" class="hdr-av">{{ nameInitial }}</el-avatar>
|
<span class="user-name">{{ auth.displayName }}</span>
|
</div>
|
<template #dropdown>
|
<el-dropdown-menu>
|
<el-dropdown-item command="profile">账号设置</el-dropdown-item>
|
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
</el-dropdown-menu>
|
</template>
|
</el-dropdown>
|
</div>
|
</el-header>
|
|
<div class="tabs-bar">
|
<div
|
v-for="tab in tabs"
|
:key="tab.path"
|
class="route-tab"
|
:class="{ active: tab.path === active }"
|
@click="goTab(tab.path)"
|
@contextmenu.prevent="openTabContextMenu($event, tab.path)"
|
>
|
<el-icon class="tab-home" v-if="tab.path === '/dashboard'"><House /></el-icon>
|
<span>{{ tab.title }}</span>
|
<button
|
v-if="tab.closable"
|
type="button"
|
class="tab-close"
|
@click.stop="closeTab(tab.path)"
|
>
|
x
|
</button>
|
</div>
|
<div
|
v-if="tabContextMenu.visible"
|
class="tab-context-menu"
|
:style="{ left: `${tabContextMenu.x}px`, top: `${tabContextMenu.y}px` }"
|
>
|
<button type="button" class="tab-context-item" @click="closeLeftTabs">关闭左边</button>
|
<button type="button" class="tab-context-item" @click="closeRightTabs">关闭右边</button>
|
<button type="button" class="tab-context-item" @click="closeOtherTabs">关闭其他</button>
|
</div>
|
</div>
|
</div>
|
|
<el-main class="main-area">
|
<router-view />
|
</el-main>
|
</el-container>
|
</el-container>
|
|
<el-dialog v-model="profileDialogVisible" title="账号设置" width="520px" destroy-on-close>
|
<el-form v-loading="profileLoading" :model="profileForm" label-width="100px">
|
<el-form-item label="登录名">
|
<el-input v-model="profileForm.username" disabled />
|
</el-form-item>
|
<el-form-item label="姓名">
|
<el-input v-model="profileForm.realName" maxlength="64" />
|
</el-form-item>
|
<el-form-item label="手机">
|
<el-input v-model="profileForm.mobile" maxlength="32" />
|
</el-form-item>
|
<el-form-item label="头像">
|
<div class="avatar-upload-row">
|
<el-avatar v-if="profileForm.avatarUrl" :size="64" :src="profileForm.avatarUrl" />
|
<el-avatar v-else :size="64">{{ (profileForm.realName || profileForm.username || "?").slice(0, 1) }}</el-avatar>
|
<div class="avatar-upload-actions">
|
<el-upload
|
:show-file-list="false"
|
accept=".jpg,.jpeg,.png,.webp,.gif"
|
:http-request="uploadProfileAvatar"
|
>
|
<el-button :loading="profileAvatarUploading">上传头像</el-button>
|
</el-upload>
|
<el-button v-if="profileForm.avatarUrl" link type="danger" @click="profileForm.avatarUrl = ''">清除</el-button>
|
<div class="avatar-upload-tip">支持 jpg、png、webp、gif,大小不超过 5MB</div>
|
</div>
|
</div>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="profileDialogVisible = false">取消</el-button>
|
<el-button type="primary" :loading="profileSaving" @click="submitProfile">保存</el-button>
|
</template>
|
</el-dialog>
|
|
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="560px" destroy-on-close @closed="resetPasswordForm">
|
<el-form :model="passwordForm" label-width="120px">
|
<el-form-item label="原密码" required>
|
<el-input
|
v-model="passwordForm.oldPassword"
|
type="password"
|
show-password
|
autocomplete="current-password"
|
placeholder="请输入原密码"
|
/>
|
</el-form-item>
|
<el-form-item label="新密码" required>
|
<el-input
|
v-model="passwordForm.newPassword"
|
type="password"
|
show-password
|
autocomplete="new-password"
|
placeholder="请输入至少12位含大小写字母、特殊字符的密码"
|
/>
|
</el-form-item>
|
<el-form-item label="确认新密码" required>
|
<el-input
|
v-model="passwordForm.newPasswordConfirm"
|
type="password"
|
show-password
|
autocomplete="new-password"
|
placeholder="请输入至少12位含大小写字母、特殊字符的密码"
|
/>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
<el-button type="primary" :loading="passwordSaving" @click="submitPassword">确定</el-button>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup lang="ts">
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
import { useRoute, useRouter } from "vue-router";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import type { UploadRequestOptions } from "element-plus";
|
import { Document, Grid, House, Setting, User, UserFilled, Wallet } from "@element-plus/icons-vue";
|
import { useAuthStore } from "../stores/auth";
|
import http from "../api/http";
|
import menuFoldIcon from "../static/img/MenuFold.png";
|
import siteLogo from "../static/logo.jpg";
|
|
type LayoutTab = {
|
path: string;
|
title: string;
|
closable: boolean;
|
};
|
|
type CurrentUserDetail = {
|
id: number;
|
uuid: string;
|
username: string;
|
realName?: string;
|
mobile?: string;
|
avatarUrl?: string;
|
};
|
|
const route = useRoute();
|
const router = useRouter();
|
const auth = useAuthStore();
|
const active = computed(() => route.path);
|
const nameInitial = computed(() => {
|
const n = auth.displayName;
|
return n && n.length ? n.slice(0, 1) : "?";
|
});
|
const defaultOpeneds = computed(() => {
|
const path = route.path;
|
if (path.startsWith("/system/")) return ["system"];
|
if (path.startsWith("/staff/")) return ["staff"];
|
if (path.startsWith("/region/")) return ["region"];
|
if (path === "/appointments" || path === "/approvals" || path === "/exports") return ["logs"];
|
if (path === "/users" || path.startsWith("/roles")) return ["account"];
|
return [];
|
});
|
const tabs = ref<LayoutTab[]>([{ path: "/dashboard", title: "首页统计", closable: false }]);
|
const profileDialogVisible = ref(false);
|
const passwordDialogVisible = ref(false);
|
const profileLoading = ref(false);
|
const profileSaving = ref(false);
|
const passwordSaving = ref(false);
|
const profileAvatarUploading = ref(false);
|
const tabContextMenu = reactive({
|
visible: false,
|
x: 0,
|
y: 0,
|
path: "",
|
});
|
const profileForm = reactive({
|
id: 0,
|
uuid: "",
|
username: "",
|
realName: "",
|
mobile: "",
|
avatarUrl: "",
|
});
|
const passwordForm = reactive({
|
oldPassword: "",
|
newPassword: "",
|
newPasswordConfirm: "",
|
});
|
const routeTitleMap: Record<string, string> = {
|
"/dashboard": "首页统计",
|
"/appointments": "预约记录",
|
"/approvals": "操作日志",
|
"/exports": "操作日志",
|
"/system/approval-settings": "审核配置",
|
"/system/booking-settings": "预约配置",
|
"/system/visit-reasons": "来访事由配置",
|
"/system/content-settings": "内容设置",
|
"/system/sensitive-words": "敏感词库",
|
"/staff/depts": "部门管理",
|
"/staff/employees": "员工管理",
|
"/staff/groups": "人员分组",
|
"/visitor-info": "访客信息",
|
"/users": "平台管理员",
|
"/roles": "角色",
|
"/region/pass-areas": "通行区域管理",
|
"/region/local-units": "属地单位管理",
|
};
|
const hiddenTabRouteNames = new Set(["staff-group-detail", "role-detail"]);
|
function resolveRouteTitle() {
|
if (typeof route.meta.title === "string" && route.meta.title) {
|
return route.meta.title;
|
}
|
if (routeTitleMap[route.path]) {
|
return routeTitleMap[route.path];
|
}
|
return "页面";
|
}
|
|
function ensureTab() {
|
if (typeof route.name === "string" && hiddenTabRouteNames.has(route.name)) {
|
return;
|
}
|
const path = route.path;
|
const title = resolveRouteTitle();
|
const idx = tabs.value.findIndex((item) => item.path === path);
|
if (idx >= 0) {
|
tabs.value[idx] = { ...tabs.value[idx], title };
|
return;
|
}
|
tabs.value.push({
|
path,
|
title,
|
closable: path !== "/dashboard",
|
});
|
}
|
|
function goTab(path: string) {
|
if (path !== route.path) {
|
router.push(path);
|
}
|
}
|
|
function closeTab(path: string) {
|
hideTabContextMenu();
|
const idx = tabs.value.findIndex((item) => item.path === path);
|
if (idx < 0) {
|
return;
|
}
|
const isCurrent = route.path === path;
|
tabs.value.splice(idx, 1);
|
if (!isCurrent) {
|
return;
|
}
|
const fallback = tabs.value[idx] || tabs.value[idx - 1] || tabs.value[0];
|
router.push(fallback?.path || "/dashboard");
|
}
|
|
function openTabContextMenu(event: MouseEvent, path: string) {
|
tabContextMenu.visible = true;
|
tabContextMenu.x = event.clientX;
|
tabContextMenu.y = event.clientY;
|
tabContextMenu.path = path;
|
}
|
|
function hideTabContextMenu() {
|
tabContextMenu.visible = false;
|
tabContextMenu.path = "";
|
}
|
|
function closeTabsByFilter(keep: (tab: LayoutTab, index: number) => boolean) {
|
const currentPath = route.path;
|
tabs.value = tabs.value.filter((tab, index) => !tab.closable || keep(tab, index));
|
if (!tabs.value.some((tab) => tab.path === currentPath)) {
|
const fallback = tabs.value[tabs.value.length - 1] || tabs.value[0];
|
router.push(fallback?.path || "/dashboard");
|
}
|
hideTabContextMenu();
|
}
|
|
function closeLeftTabs() {
|
const targetIndex = tabs.value.findIndex((tab) => tab.path === tabContextMenu.path);
|
if (targetIndex < 0) {
|
hideTabContextMenu();
|
return;
|
}
|
closeTabsByFilter((tab, index) => index >= targetIndex || !tab.closable);
|
}
|
|
function closeRightTabs() {
|
const targetIndex = tabs.value.findIndex((tab) => tab.path === tabContextMenu.path);
|
if (targetIndex < 0) {
|
hideTabContextMenu();
|
return;
|
}
|
closeTabsByFilter((tab, index) => index <= targetIndex || !tab.closable);
|
}
|
|
function closeOtherTabs() {
|
const targetPath = tabContextMenu.path;
|
if (!targetPath) {
|
hideTabContextMenu();
|
return;
|
}
|
closeTabsByFilter((tab) => tab.path === targetPath || !tab.closable);
|
}
|
|
function handleGlobalClick() {
|
hideTabContextMenu();
|
}
|
|
watch(
|
() => route.path,
|
() => {
|
ensureTab();
|
},
|
{ immediate: true }
|
);
|
|
onMounted(() => {
|
window.addEventListener("click", handleGlobalClick);
|
if (auth.token && auth.userId == null) {
|
void loadCurrentUser(false);
|
}
|
});
|
|
onBeforeUnmount(() => {
|
window.removeEventListener("click", handleGlobalClick);
|
});
|
|
async function logout() {
|
try {
|
await http.post("/auth/logout");
|
} catch {
|
/* ignore */
|
}
|
auth.clear();
|
router.push("/login");
|
}
|
|
async function confirmLogout() {
|
try {
|
await ElMessageBox.confirm("确认退出当前账号吗?", "退出确认", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
});
|
} catch {
|
return;
|
}
|
await logout();
|
}
|
|
async function loadCurrentUser(showError = true) {
|
profileLoading.value = true;
|
try {
|
const { data } = await http.get("/auth/me");
|
const detail = (data.result || {}) as CurrentUserDetail;
|
profileForm.id = detail.id || 0;
|
profileForm.uuid = detail.uuid || "";
|
profileForm.username = detail.username || "";
|
profileForm.realName = detail.realName || "";
|
profileForm.mobile = detail.mobile || "";
|
profileForm.avatarUrl = detail.avatarUrl || "";
|
syncAuthProfile(detail);
|
return detail;
|
} catch (e: unknown) {
|
if (showError) {
|
ElMessage.error(e instanceof Error ? e.message : "加载账号信息失败");
|
}
|
return null;
|
} finally {
|
profileLoading.value = false;
|
}
|
}
|
|
function syncAuthProfile(detail: CurrentUserDetail) {
|
if (!auth.token) {
|
return;
|
}
|
auth.setSession(
|
auth.token,
|
detail.realName?.trim() || detail.username || auth.displayName || "",
|
detail.avatarUrl || null,
|
auth.permissions,
|
detail.id,
|
);
|
}
|
|
async function openProfileDialog() {
|
profileDialogVisible.value = true;
|
await loadCurrentUser();
|
}
|
|
function openPasswordDialog() {
|
resetPasswordForm();
|
passwordDialogVisible.value = true;
|
}
|
|
function handleUserCommand(command: string | number | object) {
|
const value = String(command);
|
if (value === "profile") {
|
void openProfileDialog();
|
return;
|
}
|
if (value === "password") {
|
openPasswordDialog();
|
return;
|
}
|
if (value === "logout") {
|
void confirmLogout();
|
}
|
}
|
|
async function submitProfile() {
|
profileSaving.value = true;
|
try {
|
const { data } = await http.put("/auth/me", {
|
realName: profileForm.realName.trim() || undefined,
|
mobile: profileForm.mobile.trim() || undefined,
|
avatarUrl: profileForm.avatarUrl.trim() || undefined,
|
});
|
const detail = (data.result || {}) as CurrentUserDetail;
|
profileForm.id = detail.id || profileForm.id;
|
profileForm.uuid = detail.uuid || profileForm.uuid;
|
profileForm.username = detail.username || profileForm.username;
|
profileForm.realName = detail.realName || "";
|
profileForm.mobile = detail.mobile || "";
|
profileForm.avatarUrl = detail.avatarUrl || "";
|
syncAuthProfile({
|
...detail,
|
username: detail.username || profileForm.username,
|
id: detail.id || profileForm.id,
|
});
|
profileDialogVisible.value = false;
|
ElMessage.success("账号信息已保存");
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "保存失败");
|
} finally {
|
profileSaving.value = false;
|
}
|
}
|
|
function resetPasswordForm() {
|
passwordForm.oldPassword = "";
|
passwordForm.newPassword = "";
|
passwordForm.newPasswordConfirm = "";
|
}
|
|
function isStrongPassword(value: string) {
|
return (
|
value.length >= 12
|
&& value.length <= 64
|
&& /[a-z]/.test(value)
|
&& /[A-Z]/.test(value)
|
&& /[^A-Za-z0-9]/.test(value)
|
);
|
}
|
|
async function submitPassword() {
|
if (!passwordForm.oldPassword.trim()) {
|
ElMessage.warning("请输入原密码");
|
return;
|
}
|
if (!isStrongPassword(passwordForm.newPassword)) {
|
ElMessage.warning("请输入至少12位含大小写字母、特殊字符的密码");
|
return;
|
}
|
if (passwordForm.newPassword !== passwordForm.newPasswordConfirm) {
|
ElMessage.warning("两次输入的新密码不一致");
|
return;
|
}
|
passwordSaving.value = true;
|
try {
|
await http.post("/auth/me/password", {
|
oldPassword: passwordForm.oldPassword,
|
newPassword: passwordForm.newPassword,
|
newPasswordConfirm: passwordForm.newPasswordConfirm,
|
});
|
passwordDialogVisible.value = false;
|
ElMessage.success("密码已修改");
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "修改失败");
|
} finally {
|
passwordSaving.value = false;
|
}
|
}
|
|
async function uploadProfileAvatar(opt: UploadRequestOptions) {
|
const fd = new FormData();
|
fd.append("file", opt.file as File);
|
profileAvatarUploading.value = true;
|
try {
|
const { data } = await http.post("/common/upload/image", fd, {
|
headers: { "Content-Type": "multipart/form-data" },
|
});
|
profileForm.avatarUrl = String(data.result?.url || "");
|
ElMessage.success("头像上传成功");
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "头像上传失败");
|
} finally {
|
profileAvatarUploading.value = false;
|
}
|
}
|
</script>
|
|
<style scoped>
|
.layout {
|
height: 100%;
|
}
|
|
.main-shell {
|
display: flex;
|
flex-direction: column;
|
min-width: 0;
|
}
|
|
.side-wrap {
|
background: #245ECC;
|
color: #fff;
|
padding: 0 10px 12px;
|
box-sizing: border-box;
|
}
|
|
.brand-block {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
height: 59px;
|
margin: 0 -10px 10px;
|
padding: 0 16px;
|
background: #245ECC;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
}
|
|
.brand-icon {
|
width: 22px;
|
height: 22px;
|
border-radius: 50%;
|
object-fit: cover;
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.08);
|
}
|
|
.brand-name {
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.side-menu {
|
border-right: 0;
|
background: transparent;
|
}
|
|
.side-menu :deep(.el-menu) {
|
border-right: 0;
|
background: transparent;
|
}
|
|
.side-menu :deep(.el-sub-menu__title),
|
.side-menu :deep(.el-menu-item) {
|
height: 42px;
|
line-height: 42px;
|
border-radius: 8px;
|
margin: 4px 8px;
|
}
|
|
.side-menu :deep(.el-sub-menu__title:hover),
|
.side-menu :deep(.el-menu-item:hover) {
|
background: rgba(255, 255, 255, 0.08) !important;
|
}
|
|
.side-menu :deep(.el-menu-item.is-active) {
|
background: #ffffff !important;
|
color: #245ECC !important;
|
}
|
|
.side-menu :deep(.el-sub-menu .el-menu-item) {
|
min-width: 0;
|
padding-left: 48px !important;
|
font-size: 14px;
|
}
|
|
.side-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
color: #ffffff !important;
|
}
|
|
.home-item {
|
margin-bottom: 8px !important;
|
font-weight: 600;
|
}
|
|
.group-link {
|
color: #dce8ff !important;
|
}
|
|
.top-wrap {
|
display: flex;
|
flex-direction: column;
|
width: 100%;
|
background: #eef3ff;
|
border-bottom: 1px solid #dbe5fb;
|
}
|
|
.hdr-av {
|
flex-shrink: 0;
|
}
|
|
.header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 60px;
|
width: 84vw;
|
padding: 0 12px 0 14px;
|
background: #ffffff;
|
border-bottom: 1px solid #e7edf8;
|
box-sizing: border-box;
|
}
|
|
.header-spacer,
|
.header-right,
|
.user-box {
|
display: flex;
|
align-items: center;
|
}
|
|
.header-spacer {
|
flex: 1 1 auto;
|
}
|
|
.header-right {
|
gap: 10px;
|
}
|
|
.menu-trigger {
|
color: #5e6f95;
|
}
|
|
.menu-trigger-icon {
|
display: block;
|
width: 18px;
|
height: 18px;
|
object-fit: contain;
|
}
|
|
.user-box {
|
gap: 8px;
|
}
|
|
.logout-trigger {
|
color: #5e6f95;
|
padding: 0;
|
}
|
|
.user-name {
|
color: #47587c;
|
font-size: 13px;
|
}
|
|
.avatar-upload-row {
|
display: flex;
|
align-items: center;
|
gap: 14px;
|
}
|
|
.avatar-upload-actions {
|
display: flex;
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 8px;
|
}
|
|
.avatar-upload-tip {
|
color: #8a97b3;
|
font-size: 12px;
|
line-height: 1.4;
|
}
|
|
.tabs-bar {
|
position: relative;
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
width: 100%;
|
min-height: 46px;
|
padding: 0 10px;
|
background: #edf3ff;
|
box-sizing: border-box;
|
overflow-x: auto;
|
overflow-y: hidden;
|
}
|
|
.route-tab {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
flex: 0 0 auto;
|
height: 28px;
|
padding: 0 12px;
|
border-radius: 6px 6px 0 0;
|
color: #7b8cae;
|
font-size: 13px;
|
cursor: pointer;
|
user-select: none;
|
border: 1px solid transparent;
|
border-bottom: 0;
|
}
|
|
.route-tab.active {
|
background: #ffffff;
|
color: #4f74d8;
|
border-color: #d8e3fb;
|
}
|
|
.tab-home {
|
font-size: 12px;
|
}
|
|
.tab-close {
|
border: 0;
|
padding: 0;
|
background: transparent;
|
color: inherit;
|
font-size: 12px;
|
cursor: pointer;
|
}
|
|
.tab-context-menu {
|
position: fixed;
|
z-index: 1200;
|
min-width: 120px;
|
padding: 6px 0;
|
border: 1px solid #d8e3fb;
|
border-radius: 8px;
|
background: #ffffff;
|
box-shadow: 0 8px 24px rgba(52, 84, 148, 0.16);
|
}
|
|
.tab-context-item {
|
display: block;
|
width: 100%;
|
border: 0;
|
padding: 8px 14px;
|
background: transparent;
|
color: #4e5f82;
|
font-size: 13px;
|
text-align: left;
|
cursor: pointer;
|
}
|
|
.tab-context-item:hover {
|
background: #f3f7ff;
|
color: #4f74d8;
|
}
|
|
.main-area {
|
background: linear-gradient(180deg, #f3f7ff 0%, #eef3fd 100%);
|
padding: 14px;
|
}
|
|
.side-menu :deep(.el-icon) {
|
margin-right: 8px;
|
}
|
</style>
|