From e92cd56bb56b111c1afe9922191f9897215ff6b0 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期五, 28 八月 2026 17:55:27 +0800
Subject: [PATCH] 提交数据库和服务器部署说明文档
---
frontend-admin/src/layouts/MainLayout.vue | 547 ++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 480 insertions(+), 67 deletions(-)
diff --git a/frontend-admin/src/layouts/MainLayout.vue b/frontend-admin/src/layouts/MainLayout.vue
index 839f2a9..5aa619b 100644
--- a/frontend-admin/src/layouts/MainLayout.vue
+++ b/frontend-admin/src/layouts/MainLayout.vue
@@ -1,22 +1,23 @@
<template>
<el-container class="layout">
- <el-aside width="248px" class="side-wrap">
+ <el-aside width="15.98vw" class="side-wrap">
<div class="brand-block">
- <div class="brand-icon"></div>
- <div class="brand-name">运维运营管理平台</div>
+ <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="#2f63d4"
+ background-color="#245ECC"
text-color="#dce8ff"
- active-text-color="#2f63d4"
+ 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>
+ <span>首页统计</span>
</el-menu-item>
<el-sub-menu
@@ -32,12 +33,13 @@
>
<template #title>
<el-icon><Setting /></el-icon>
- <span>系统</span>
+ <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
@@ -59,7 +61,7 @@
>
<template #title>
<el-icon><Grid /></el-icon>
- <span>区域</span>
+ <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>
@@ -71,17 +73,16 @@
</el-menu-item>
<el-sub-menu
- v-if="auth.hasAnyPermission(['appointments:page', 'approvals:page', 'exports:page'])"
+ 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-menu-item v-if="auth.hasPermission('exports:page')" index="/exports">导出任务</el-menu-item>
- </el-sub-menu>
+ <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'])"
@@ -99,22 +100,23 @@
<el-container class="main-shell">
<div class="top-wrap">
<el-header class="header">
- <div class="header-left">
- <div class="header-title">
- <span class="header-dot"></span>
- <span>运维运营管理平台</span>
- </div>
- </div>
+ <div class="header-spacer"></div>
<div class="header-right">
- <el-button class="menu-trigger" text>
- <el-icon><Operation /></el-icon>
- </el-button>
- <div class="user-box">
- <el-avatar v-if="auth.avatarUrl" :size="28" :src="auth.avatarUrl" class="hdr-av" />
- <el-avatar v-else :size="28" class="hdr-av">{{ nameInitial }}</el-avatar>
- <span class="user-name">{{ auth.displayName }}</span>
- </div>
- <el-button type="primary" link @click="logout">退出</el-button>
+
+ <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>
@@ -125,6 +127,7 @@
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>
@@ -137,6 +140,15 @@
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>
@@ -145,14 +157,89 @@
</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, ref, watch } from "vue";
+import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
-import { Document, Grid, House, Operation, Setting, User, UserFilled, Wallet } from "@element-plus/icons-vue";
+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;
@@ -160,20 +247,68 @@
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 defaultOpeneds: string[] = [];
-const tabs = ref<LayoutTab[]>([{ path: "/dashboard", title: "首页", closable: false }]);
+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": "首页",
+ "/dashboard": "首页统计",
"/appointments": "预约记录",
- "/approvals": "待办日志",
+ "/approvals": "操作日志",
+ "/exports": "操作日志",
"/system/approval-settings": "审核配置",
"/system/booking-settings": "预约配置",
"/system/visit-reasons": "来访事由配置",
"/system/content-settings": "内容设置",
+ "/system/sensitive-words": "敏感词库",
"/staff/depts": "部门管理",
"/staff/employees": "员工管理",
"/staff/groups": "人员分组",
@@ -183,11 +318,7 @@
"/region/pass-areas": "通行区域管理",
"/region/local-units": "属地单位管理",
};
-const nameInitial = computed(() => {
- const n = auth.displayName;
- return n && n.length ? n.slice(0, 1) : "?";
-});
-
+const hiddenTabRouteNames = new Set(["staff-group-detail", "role-detail"]);
function resolveRouteTitle() {
if (typeof route.meta.title === "string" && route.meta.title) {
return route.meta.title;
@@ -199,6 +330,9 @@
}
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);
@@ -220,6 +354,7 @@
}
function closeTab(path: string) {
+ hideTabContextMenu();
const idx = tabs.value.findIndex((item) => item.path === path);
if (idx < 0) {
return;
@@ -233,6 +368,59 @@
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,
() => {
@@ -240,6 +428,17 @@
},
{ 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 {
@@ -249,6 +448,171 @@
}
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>
@@ -264,7 +628,7 @@
}
.side-wrap {
- background: linear-gradient(180deg, #2f63d4 0%, #2d5fce 100%);
+ background: #245ECC;
color: #fff;
padding: 0 10px 12px;
box-sizing: border-box;
@@ -274,18 +638,18 @@
display: flex;
align-items: center;
gap: 10px;
- height: 44px;
+ height: 59px;
margin: 0 -10px 10px;
padding: 0 16px;
- background: rgba(18, 55, 138, 0.28);
+ background: #245ECC;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.brand-icon {
- width: 18px;
- height: 18px;
+ width: 22px;
+ height: 22px;
border-radius: 50%;
- background: rgba(255, 255, 255, 0.4);
+ object-fit: cover;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.08);
}
@@ -319,13 +683,13 @@
.side-menu :deep(.el-menu-item.is-active) {
background: #ffffff !important;
- color: #2f63d4 !important;
+ color: #245ECC !important;
}
.side-menu :deep(.el-sub-menu .el-menu-item) {
min-width: 0;
padding-left: 48px !important;
- font-size: 13px;
+ font-size: 14px;
}
.side-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
@@ -357,34 +721,23 @@
display: flex;
align-items: center;
justify-content: space-between;
- height: 44px;
- width: 100%;
+ height: 60px;
+ width: 84vw;
padding: 0 12px 0 14px;
background: #ffffff;
border-bottom: 1px solid #e7edf8;
box-sizing: border-box;
}
-.header-left,
+.header-spacer,
.header-right,
-.user-box,
-.header-title {
+.user-box {
display: flex;
align-items: center;
}
-.header-title {
- gap: 10px;
- color: #3d5d9e;
- font-size: 14px;
- font-weight: 600;
-}
-
-.header-dot {
- width: 18px;
- height: 18px;
- border-radius: 50%;
- background: linear-gradient(135deg, #7ca9ff, #4f7dff);
+.header-spacer {
+ flex: 1 1 auto;
}
.header-right {
@@ -395,8 +748,20 @@
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 {
@@ -404,12 +769,32 @@
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: 36px;
+ min-height: 46px;
padding: 0 10px;
background: #edf3ff;
box-sizing: border-box;
@@ -452,6 +837,34 @@
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;
--
Gitblit v1.7.1