|
|
|
@@ -1,130 +1,107 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref, watch } from "vue";
|
|
|
|
|
import { computed } from "vue";
|
|
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
|
import { useAuthStore } from "@/stores/auth";
|
|
|
|
|
import { useBackendStatus } from "@/composables/useBackendStatus";
|
|
|
|
|
import { useUiPreferences } from "@/composables/useUiPreferences";
|
|
|
|
|
import { useViewport } from "@/composables/useViewport";
|
|
|
|
|
import {
|
|
|
|
|
Bell,
|
|
|
|
|
Collection,
|
|
|
|
|
DataAnalysis,
|
|
|
|
|
Document,
|
|
|
|
|
FolderOpened,
|
|
|
|
|
Fold,
|
|
|
|
|
House,
|
|
|
|
|
Menu,
|
|
|
|
|
Moon,
|
|
|
|
|
RefreshRight,
|
|
|
|
|
Setting,
|
|
|
|
|
Sunny,
|
|
|
|
|
Tickets,
|
|
|
|
|
Upload,
|
|
|
|
|
VideoCamera
|
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
import { Bell, Collection, DataAnalysis, Menu, Moon, Setting, Sunny, Tickets, VideoCamera } from "@element-plus/icons-vue";
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
const { isMobile } = useViewport();
|
|
|
|
|
const { backendUnavailable, backendMessage, backendLastChangedAt } = useBackendStatus();
|
|
|
|
|
const { resolvedTheme, sidebarCollapsed, cycleThemeMode, toggleSidebarCollapsed } = useUiPreferences();
|
|
|
|
|
|
|
|
|
|
const mobileNavVisible = ref(false);
|
|
|
|
|
|
|
|
|
|
const navigationGroups = [
|
|
|
|
|
{
|
|
|
|
|
key: "overview",
|
|
|
|
|
title: "总览",
|
|
|
|
|
key: "workspace",
|
|
|
|
|
title: "工作区",
|
|
|
|
|
items: [
|
|
|
|
|
{ index: "/", label: "仪表盘", icon: DataAnalysis },
|
|
|
|
|
{ index: "/", label: "运行中心", shortLabel: "运行", icon: DataAnalysis, names: ["dashboard"] },
|
|
|
|
|
{ index: "/live-rooms", label: "直播与录制", shortLabel: "录制", icon: VideoCamera, names: ["live-rooms", "record-tasks", "record-task-detail", "record-session-detail", "recovery"] },
|
|
|
|
|
{ index: "/transcode-tasks", label: "处理与归档", shortLabel: "归档", icon: Collection, names: ["transcode-tasks", "upload-tasks", "media-browser"] },
|
|
|
|
|
{ index: "/daily-reviews", label: "分析与排障", shortLabel: "分析", icon: Tickets, names: ["daily-reviews", "logs"] }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "monitor",
|
|
|
|
|
title: "监控录制",
|
|
|
|
|
key: "manage",
|
|
|
|
|
title: "管理",
|
|
|
|
|
items: [
|
|
|
|
|
{ index: "/live-rooms", label: "直播间", icon: House },
|
|
|
|
|
{ index: "/record-tasks", label: "录制任务", icon: VideoCamera },
|
|
|
|
|
{ index: "/recovery", label: "恢复中心", icon: RefreshRight }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "media",
|
|
|
|
|
title: "媒资归档",
|
|
|
|
|
items: [
|
|
|
|
|
{ index: "/transcode-tasks", label: "转码任务", icon: Collection },
|
|
|
|
|
{ index: "/upload-tasks", label: "上传任务", icon: Upload },
|
|
|
|
|
{ index: "/media-browser", label: "文件库", icon: FolderOpened }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "analysis",
|
|
|
|
|
title: "分析",
|
|
|
|
|
items: [
|
|
|
|
|
{ index: "/daily-reviews", label: "回顾日报", icon: Document },
|
|
|
|
|
{ index: "/logs", label: "系统日志", icon: Tickets }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "system",
|
|
|
|
|
title: "系统",
|
|
|
|
|
items: [
|
|
|
|
|
{ index: "/settings", label: "系统设置", icon: Setting }
|
|
|
|
|
{ index: "/settings", label: "系统设置", shortLabel: "设置", icon: Setting, names: ["settings"] }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const bottomNavigation = computed(() => navigationGroups.flatMap((group) => group.items));
|
|
|
|
|
|
|
|
|
|
const workspaceTabs = computed(() => {
|
|
|
|
|
const name = String(route.name ?? "");
|
|
|
|
|
if (["live-rooms", "record-tasks", "record-task-detail", "record-session-detail", "recovery"].includes(name)) {
|
|
|
|
|
return [
|
|
|
|
|
{ name: "live-rooms", label: "直播间" },
|
|
|
|
|
{ name: "record-tasks", label: "录制会话" },
|
|
|
|
|
{ name: "recovery", label: "恢复中心" }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
if (["transcode-tasks", "upload-tasks", "media-browser"].includes(name)) {
|
|
|
|
|
return [
|
|
|
|
|
{ name: "transcode-tasks", label: "转码队列" },
|
|
|
|
|
{ name: "upload-tasks", label: "上传队列" },
|
|
|
|
|
{ name: "media-browser", label: "文件库" }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
if (["daily-reviews", "logs"].includes(name)) {
|
|
|
|
|
return [
|
|
|
|
|
{ name: "daily-reviews", label: "回顾日报" },
|
|
|
|
|
{ name: "logs", label: "系统日志" }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const userDisplayName = computed(() => authStore.user?.displayName || authStore.user?.username || "管理员");
|
|
|
|
|
const userAvatarText = computed(() => userDisplayName.value.trim().slice(0, 1).toUpperCase() || "录");
|
|
|
|
|
|
|
|
|
|
function isNavItemActive(index: string) {
|
|
|
|
|
return route.path === index || route.path.startsWith(`${index}/`);
|
|
|
|
|
function isNavItemActive(item: { names: string[] }) {
|
|
|
|
|
return item.names.includes(String(route.name ?? ""));
|
|
|
|
|
}
|
|
|
|
|
function navigate(index: string) { void router.push(index); }
|
|
|
|
|
|
|
|
|
|
function isWorkspaceTabActive(name: string) {
|
|
|
|
|
const current = String(route.name ?? "");
|
|
|
|
|
if (name === "record-tasks") return ["record-tasks", "record-task-detail", "record-session-detail"].includes(current);
|
|
|
|
|
return current === name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pageBreadcrumb = computed(() => {
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
dashboard: "总览 / 仪表盘",
|
|
|
|
|
"live-rooms": "监控录制 / 直播间",
|
|
|
|
|
"record-tasks": "监控录制 / 录制任务",
|
|
|
|
|
"transcode-tasks": "媒资归档 / 转码任务",
|
|
|
|
|
"upload-tasks": "媒资归档 / 上传任务",
|
|
|
|
|
"media-browser": "媒资归档 / 文件库",
|
|
|
|
|
"record-task-detail": "监控录制 / 录制任务 / 分片详情",
|
|
|
|
|
"record-session-detail": "监控录制 / 录制任务 / 会话详情",
|
|
|
|
|
"daily-reviews": "分析 / 回顾日报",
|
|
|
|
|
logs: "分析 / 系统日志",
|
|
|
|
|
recovery: "监控录制 / 恢复中心",
|
|
|
|
|
dashboard: "工作台 / 运行中心",
|
|
|
|
|
"live-rooms": "直播与录制 / 直播间",
|
|
|
|
|
"record-tasks": "直播与录制 / 录制会话",
|
|
|
|
|
"transcode-tasks": "处理与归档 / 转码队列",
|
|
|
|
|
"upload-tasks": "处理与归档 / 上传队列",
|
|
|
|
|
"media-browser": "处理与归档 / 文件库",
|
|
|
|
|
"record-task-detail": "直播与录制 / 分片详情",
|
|
|
|
|
"record-session-detail": "直播与录制 / 会话详情",
|
|
|
|
|
"daily-reviews": "分析与排障 / 回顾日报",
|
|
|
|
|
logs: "分析与排障 / 系统日志",
|
|
|
|
|
recovery: "直播与录制 / 恢复中心",
|
|
|
|
|
settings: "系统 / 系统设置"
|
|
|
|
|
};
|
|
|
|
|
return m[String(route.name)] || "控制台";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function handleLogout() {
|
|
|
|
|
mobileNavVisible.value = false;
|
|
|
|
|
await authStore.logout();
|
|
|
|
|
await router.push({ name: "login" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openMobileNav() { mobileNavVisible.value = true; }
|
|
|
|
|
function closeMobileNav() { mobileNavVisible.value = false; }
|
|
|
|
|
|
|
|
|
|
watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-shell">
|
|
|
|
|
<!-- mobile scrim -->
|
|
|
|
|
<div
|
|
|
|
|
class="scrim"
|
|
|
|
|
:class="{ open: mobileNavVisible }"
|
|
|
|
|
@click="closeMobileNav"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- sidebar -->
|
|
|
|
|
<aside class="app-sidebar" :class="{ collapsed: sidebarCollapsed, 'mobile-open': mobileNavVisible }">
|
|
|
|
|
<aside class="app-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
|
|
|
|
<div class="app-brand">
|
|
|
|
|
<div class="app-brand__mark">
|
|
|
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
|
|
|
@@ -151,7 +128,7 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="app-nav-icon-item"
|
|
|
|
|
:class="{ 'is-active': isNavItemActive(item.index) }"
|
|
|
|
|
:class="{ 'is-active': isNavItemActive(item) }"
|
|
|
|
|
@click="navigate(item.index)"
|
|
|
|
|
>
|
|
|
|
|
<el-icon :size="18"><component :is="item.icon" /></el-icon>
|
|
|
|
@@ -163,7 +140,7 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
v-for="item in group.items" :key="item.index"
|
|
|
|
|
type="button"
|
|
|
|
|
class="app-nav-item"
|
|
|
|
|
:class="{ 'is-active': isNavItemActive(item.index) }"
|
|
|
|
|
:class="{ 'is-active': isNavItemActive(item) }"
|
|
|
|
|
@click="navigate(item.index)"
|
|
|
|
|
>
|
|
|
|
|
<el-icon :size="18"><component :is="item.icon" /></el-icon>
|
|
|
|
@@ -186,9 +163,10 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
<!-- main -->
|
|
|
|
|
<div class="app-main-col">
|
|
|
|
|
<header class="app-topbar">
|
|
|
|
|
<button class="app-topbar__menu-btn" @click="isMobile ? openMobileNav() : toggleSidebarCollapsed()">
|
|
|
|
|
<button class="app-topbar__menu-btn" @click="toggleSidebarCollapsed">
|
|
|
|
|
<el-icon :size="20"><Menu /></el-icon>
|
|
|
|
|
</button>
|
|
|
|
|
<div class="app-mobile-brand" aria-label="Live Recorder">LR</div>
|
|
|
|
|
<div class="app-breadcrumb">{{ pageBreadcrumb }}</div>
|
|
|
|
|
|
|
|
|
|
<div class="app-topbar__spacer" />
|
|
|
|
@@ -201,8 +179,18 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<el-dropdown trigger="click">
|
|
|
|
|
<button type="button" class="app-user-btn">
|
|
|
|
|
<span class="app-user-btn__avatar">{{ userAvatarText }}</span>
|
|
|
|
|
<button type="button" class="app-user-btn" aria-label="管理员菜单" title="管理员菜单">
|
|
|
|
|
<svg
|
|
|
|
|
class="app-user-btn__avatar"
|
|
|
|
|
viewBox="0 0 36 36"
|
|
|
|
|
width="36"
|
|
|
|
|
height="36"
|
|
|
|
|
preserveAspectRatio="xMidYMid meet"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
>
|
|
|
|
|
<circle class="app-user-btn__avatar-circle" cx="18" cy="18" r="17.5" />
|
|
|
|
|
<text class="app-user-btn__avatar-text" x="18" y="18">{{ userAvatarText }}</text>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="app-user-btn__name desktop-only">{{ userDisplayName }}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<template #dropdown>
|
|
|
|
@@ -215,14 +203,49 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main class="app-main">
|
|
|
|
|
<nav v-if="workspaceTabs.length" class="workspace-tabs" aria-label="工作区页面切换">
|
|
|
|
|
<button
|
|
|
|
|
v-for="tab in workspaceTabs"
|
|
|
|
|
:key="tab.name"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ 'is-active': isWorkspaceTabActive(tab.name) }"
|
|
|
|
|
@click="router.push({ name: tab.name })"
|
|
|
|
|
>
|
|
|
|
|
{{ tab.label }}
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
<el-alert
|
|
|
|
|
v-if="backendUnavailable && route.name !== 'record-tasks'"
|
|
|
|
|
class="backend-alert"
|
|
|
|
|
type="error" :closable="false" show-icon
|
|
|
|
|
title="后端服务暂时不可用" :description="backendMessage"
|
|
|
|
|
/>
|
|
|
|
|
<router-view />
|
|
|
|
|
<router-view v-slot="{ Component, route: viewRoute }">
|
|
|
|
|
<transition name="page-swap" mode="out-in">
|
|
|
|
|
<component
|
|
|
|
|
:is="Component"
|
|
|
|
|
:key="String(viewRoute.name ?? viewRoute.path)"
|
|
|
|
|
class="app-route-view"
|
|
|
|
|
/>
|
|
|
|
|
</transition>
|
|
|
|
|
</router-view>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<nav class="app-bottom-nav" aria-label="移动端主导航">
|
|
|
|
|
<button
|
|
|
|
|
v-for="item in bottomNavigation"
|
|
|
|
|
:key="item.index"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ 'is-active': isNavItemActive(item) }"
|
|
|
|
|
:aria-current="isNavItemActive(item) ? 'page' : undefined"
|
|
|
|
|
@click="navigate(item.index)"
|
|
|
|
|
>
|
|
|
|
|
<span class="app-bottom-nav__icon" aria-hidden="true">
|
|
|
|
|
<el-icon :size="19"><component :is="item.icon" /></el-icon>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="app-bottom-nav__label">{{ item.shortLabel }}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
@@ -230,9 +253,6 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* ==================== Shell ==================== */
|
|
|
|
|
.app-shell { display: flex; width: 100%; height: 100dvh; min-height: 0; overflow: hidden; }
|
|
|
|
|
.scrim { position: fixed; inset: 0; background: rgba(15,23,42,.5); z-index: 39; opacity: 0; pointer-events: none; transition: opacity .2s; }
|
|
|
|
|
.scrim.open { opacity: 1; pointer-events: auto; }
|
|
|
|
|
|
|
|
|
|
/* ==================== Sidebar ==================== */
|
|
|
|
|
.app-sidebar {
|
|
|
|
|
position: fixed; inset: 0 auto 0 0; z-index: 30;
|
|
|
|
@@ -242,45 +262,44 @@ watch(() => route.fullPath, () => { mobileNavVisible.value = false; });
|
|
|
|
|
}
|
|
|
|
|
.app-sidebar.collapsed { width: var(--sidebar-w-collapsed); }
|
|
|
|
|
|
|
|
|
|
.app-brand { display: flex; align-items: center; gap: 11px; height: var(--topbar-h); padding: 0 18px; flex-shrink: 0; border-bottom: 1px solid var(--side-border); }
|
|
|
|
|
.app-brand__mark { width: 36px; height: 36px; flex-shrink: 0; border-radius: 8px; display: grid; place-items: center; color: #fff; background: var(--accent); box-shadow: 0 2px 8px rgba(26,86,219,.25); }
|
|
|
|
|
html[data-theme="dark"] .app-brand__mark { background: #1a2438; box-shadow: 0 2px 8px rgba(0,0,0,.5); }
|
|
|
|
|
.app-brand__name { font-weight: 800; font-size: 16px; letter-spacing: -.02em; color: var(--text-primary); white-space: nowrap; }
|
|
|
|
|
html[data-theme="dark"] .app-brand__name { color: #eaf0fb; }
|
|
|
|
|
.app-brand__sub { font-size: 11px; color: var(--side-title); white-space: nowrap; margin-top: 1px; }
|
|
|
|
|
.app-brand { display: flex; align-items: center; gap: 11px; height: var(--topbar-h); padding: 0 17px; flex-shrink: 0; border-bottom: 1px solid var(--side-border); }
|
|
|
|
|
.app-brand__mark { width: 34px; height: 34px; flex-shrink: 0; border-radius: 10px; display: grid; place-items: center; color: #fff; background: var(--action); box-shadow: 0 8px 22px color-mix(in srgb, var(--action) 26%, transparent); }
|
|
|
|
|
html[data-theme="dark"] .app-brand__mark { background: var(--action); box-shadow: 0 8px 22px rgba(0,0,0,.28); }
|
|
|
|
|
.app-brand__name { font-weight: 780; font-size: 14px; letter-spacing: -.01em; color: var(--text-primary); white-space: nowrap; }
|
|
|
|
|
.app-brand__sub { font-size: 10px; color: var(--side-title); white-space: nowrap; margin-top: 1px; }
|
|
|
|
|
.collapsed .app-brand__copy { display: none; }
|
|
|
|
|
|
|
|
|
|
.app-nav { flex: 1; overflow-y: auto; padding: 14px 12px 8px; }
|
|
|
|
|
.app-nav-group { margin-bottom: 16px; }
|
|
|
|
|
.app-nav-group__title { padding: 6px 10px; font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--side-title); }
|
|
|
|
|
.app-nav { flex: 1; overflow-y: auto; padding: 14px 10px 8px; }
|
|
|
|
|
.app-nav-group { margin-bottom: 10px; }
|
|
|
|
|
.app-nav-group__title { padding: 8px 10px 7px; font-size: 10px; font-weight: 750; letter-spacing: .12em; color: var(--side-title); }
|
|
|
|
|
.collapsed .app-nav-group__title { font-size: 0; padding: 6px 0; text-align: center; }
|
|
|
|
|
.collapsed .app-nav-group__title::after { content: "•"; font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
.app-nav-item {
|
|
|
|
|
display: flex; align-items: center; gap: 11px; width: 100%;
|
|
|
|
|
padding: 9px 11px; margin-bottom: 2px; border: 0; border-radius: 6px;
|
|
|
|
|
background: transparent; color: var(--side-item); font-weight: 600; font-size: 14px;
|
|
|
|
|
min-height: 43px; padding: 0 11px; margin-bottom: 4px; border: 0; border-radius: 9px;
|
|
|
|
|
background: transparent; color: var(--side-item); font-weight: 650; font-size: 13px;
|
|
|
|
|
text-align: left; cursor: pointer; transition: background .15s, color .15s;
|
|
|
|
|
}
|
|
|
|
|
.app-nav-item:hover { background: var(--side-item-hover-bg); color: var(--side-item-hover); }
|
|
|
|
|
.app-nav-item.is-active { background: var(--side-active-bg); color: var(--side-active); }
|
|
|
|
|
.app-nav-item.is-active { background: var(--side-active-bg); color: var(--side-active); box-shadow: inset 1px 0 var(--side-active); }
|
|
|
|
|
|
|
|
|
|
.app-nav-icon-list { display: grid; gap: 8px; }
|
|
|
|
|
.app-nav-icon-item {
|
|
|
|
|
display: grid; place-items: center; min-height: 46px; border: 0; border-radius: 6px;
|
|
|
|
|
display: grid; place-items: center; min-height: 43px; border: 0; border-radius: 9px;
|
|
|
|
|
background: transparent; color: var(--side-item); cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.app-nav-icon-item:hover { background: var(--side-item-hover-bg); color: var(--side-item-hover); }
|
|
|
|
|
.app-nav-icon-item.is-active { background: var(--side-active-bg); color: var(--side-active); }
|
|
|
|
|
.app-nav-icon-item.is-active { background: var(--side-active-bg); color: var(--side-active); box-shadow: inset 1px 0 var(--side-active); }
|
|
|
|
|
|
|
|
|
|
.app-sidebar__foot { padding: 12px; border-top: 1px solid var(--side-border); }
|
|
|
|
|
.app-health { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: rgba(22,163,74,.10); border: 1px solid rgba(22,163,74,.20); }
|
|
|
|
|
.app-health { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 10px; border-radius: 10px; background: var(--success-soft); border: 1px solid color-mix(in srgb, var(--success) 22%, var(--side-border)); }
|
|
|
|
|
html[data-theme="dark"] .app-health { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.22); }
|
|
|
|
|
.app-health.is-danger { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
|
|
|
|
|
.app-health__dot { width: 8px; height: 8px; border-radius: 99px; background: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.18); }
|
|
|
|
|
html[data-theme="dark"] .app-health__dot { background: #34d399; box-shadow: 0 0 0 3px rgba(52,211,153,.22); }
|
|
|
|
|
.app-health.is-danger .app-health__dot { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
|
|
|
|
|
.app-health__txt { font-size: 12.5px; font-weight: 600; color: #15803d; }
|
|
|
|
|
.app-health__txt { font-size: 11px; font-weight: 700; color: var(--success); }
|
|
|
|
|
html[data-theme="dark"] .app-health__txt { color: #6ee7b7; }
|
|
|
|
|
.app-health.is-danger .app-health__txt { color: var(--danger); }
|
|
|
|
|
.collapsed .app-health__txt { display: none; }
|
|
|
|
@@ -298,41 +317,78 @@ html[data-theme="dark"] .app-health__txt { color: #6ee7b7; }
|
|
|
|
|
.app-topbar {
|
|
|
|
|
position: relative; z-index: 20; flex: 0 0 var(--topbar-h);
|
|
|
|
|
height: var(--topbar-h); display: flex; align-items: center; gap: 12px;
|
|
|
|
|
padding: 0 24px;
|
|
|
|
|
background: color-mix(in srgb, var(--surface) 70%, transparent);
|
|
|
|
|
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
|
|
|
border-bottom: 1px solid rgba(15,23,42,.07);
|
|
|
|
|
padding: 0 22px;
|
|
|
|
|
background: color-mix(in srgb, var(--surface) 96%, transparent);
|
|
|
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
|
|
|
}
|
|
|
|
|
html[data-theme="dark"] .app-topbar { border-bottom-color: rgba(255,255,255,.06); }
|
|
|
|
|
.app-topbar__menu-btn { width: 36px; height: 36px; flex-shrink: 0; display: grid; place-items: center; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-secondary); cursor: pointer; }
|
|
|
|
|
.app-topbar__menu-btn:hover { background: var(--surface-hover); color: var(--text-primary); }
|
|
|
|
|
.app-mobile-brand { display: none; width: 27px; height: 27px; place-items: center; border-radius: 8px; color: #fff; background: var(--action); font-size: 10px; font-weight: 800; }
|
|
|
|
|
.app-breadcrumb { font-size: 13.5px; color: var(--text-muted); min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
|
|
|
.app-topbar__spacer { flex: 1; }
|
|
|
|
|
.app-topbar__icon-btn { width: 36px; height: 36px; flex-shrink: 0; display: grid; place-items: center; border: 1px solid transparent; border-radius: var(--radius-sm); background: transparent; color: var(--text-secondary); cursor: pointer; }
|
|
|
|
|
.app-topbar__icon-btn:hover { background: var(--surface-hover); color: var(--text-primary); }
|
|
|
|
|
.app-user-btn { display: flex; align-items: center; gap: 9px; padding: 4px 10px 4px 4px; border: 1px solid var(--border-subtle); border-radius: 99px; background: var(--surface); cursor: pointer; }
|
|
|
|
|
.app-user-btn:hover { background: var(--surface-hover); }
|
|
|
|
|
.app-user-btn__avatar { width: 30px; height: 30px; flex-shrink: 0; border-radius: 99px; display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 13px; background: var(--accent); }
|
|
|
|
|
.app-user-btn__avatar { display: block; width: 30px; min-width: 30px; height: 30px; min-height: 30px; flex: 0 0 30px; overflow: visible; }
|
|
|
|
|
.app-user-btn__avatar-circle { fill: var(--purple); stroke: color-mix(in srgb, var(--purple) 76%, var(--border-subtle)); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
|
|
|
|
.app-user-btn__avatar-text { fill: #fff; font-family: var(--font-sans); font-size: 13px; font-weight: 750; text-anchor: middle; dominant-baseline: central; }
|
|
|
|
|
.app-user-btn__name { font-size: 13px; font-weight: 700; }
|
|
|
|
|
|
|
|
|
|
/* ==================== Main ==================== */
|
|
|
|
|
.app-main {
|
|
|
|
|
flex: 1; min-width: 0; min-height: 0; padding: 24px 28px 0;
|
|
|
|
|
overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable;
|
|
|
|
|
background:
|
|
|
|
|
radial-gradient(ellipse at 88% -12%, var(--ambient-blue), transparent 34%),
|
|
|
|
|
radial-gradient(ellipse at 98% 8%, var(--ambient-violet), transparent 28%),
|
|
|
|
|
radial-gradient(ellipse at 76% -18%, var(--ambient-warm), transparent 25%),
|
|
|
|
|
var(--bg-base);
|
|
|
|
|
}
|
|
|
|
|
.backend-alert { width: min(100%, var(--page-max)); margin: 0 auto 18px; border-radius: var(--radius-md); }
|
|
|
|
|
.workspace-tabs { display: flex; align-items: center; gap: 4px; width: min(100%, var(--page-max)); margin: 0 auto 16px; padding: 4px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--surface); }
|
|
|
|
|
.workspace-tabs button { min-height: 33px; padding: 0 13px; border: 0; border-radius: 7px; color: var(--text-muted); background: transparent; font-size: 11px; font-weight: 700; }
|
|
|
|
|
.workspace-tabs button:hover { color: var(--text-primary); background: var(--surface-muted); }
|
|
|
|
|
.workspace-tabs button.is-active { color: var(--accent); background: var(--accent-soft); }
|
|
|
|
|
.app-bottom-nav { display: none; height: calc(65px + env(safe-area-inset-bottom)); flex: 0 0 calc(65px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); border-top: 1px solid var(--border-subtle); background: var(--surface); }
|
|
|
|
|
.app-bottom-nav button { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; padding: 4px 0 3px; border: 0; color: var(--text-muted); background: transparent; font-size: 9px; transition: color .2s ease; }
|
|
|
|
|
.app-bottom-nav__icon { display: grid; width: 44px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: 99px; background: transparent; transform: scale(.84); transition: background-color .18s ease, transform .24s cubic-bezier(.2,.8,.2,1); }
|
|
|
|
|
.app-bottom-nav__icon :deep(.el-icon) { transition: transform .24s cubic-bezier(.2,.8,.2,1); }
|
|
|
|
|
.app-bottom-nav__label { max-width: 100%; overflow: hidden; line-height: 14px; text-overflow: ellipsis; white-space: nowrap; transition: color .2s ease, font-weight .2s ease; }
|
|
|
|
|
.app-bottom-nav button.is-active { color: var(--accent); }
|
|
|
|
|
.app-bottom-nav button.is-active .app-bottom-nav__icon { background: var(--accent-soft); transform: scale(1); }
|
|
|
|
|
.app-bottom-nav button.is-active .app-bottom-nav__icon :deep(.el-icon) { transform: scale(1.05); }
|
|
|
|
|
.app-bottom-nav button.is-active .app-bottom-nav__label { font-weight: 750; }
|
|
|
|
|
.app-bottom-nav button:active .app-bottom-nav__icon { transform: scale(.9); transition-duration: .08s; }
|
|
|
|
|
|
|
|
|
|
.page-swap-enter-active { transition: opacity .2s ease, transform .24s cubic-bezier(.2,.8,.2,1); }
|
|
|
|
|
.page-swap-leave-active { transition: opacity .12s ease, transform .12s ease; }
|
|
|
|
|
.page-swap-enter-from { opacity: 0; transform: translateY(7px); }
|
|
|
|
|
.page-swap-leave-to { opacity: 0; transform: translateY(-3px); }
|
|
|
|
|
|
|
|
|
|
/* ==================== Responsive ==================== */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.app-sidebar { transform: translateX(-100%); transition: transform .24s cubic-bezier(.4,0,.2,1); }
|
|
|
|
|
.app-sidebar.mobile-open { transform: translateX(0); width: 280px; z-index: 50; }
|
|
|
|
|
.app-sidebar:not(.mobile-open) .app-brand { display: none; }
|
|
|
|
|
.app-sidebar:not(.mobile-open) .app-sidebar__foot { display: none; }
|
|
|
|
|
.app-sidebar { display: none; }
|
|
|
|
|
.app-main-col { margin-left: 0 !important; }
|
|
|
|
|
.app-main { padding: 12px 12px 0; }
|
|
|
|
|
.app-main {
|
|
|
|
|
padding: 12px 12px 0;
|
|
|
|
|
background:
|
|
|
|
|
radial-gradient(240px 170px at 108% -5%, color-mix(in srgb, var(--ambient-blue) 58%, transparent), transparent 74%),
|
|
|
|
|
radial-gradient(180px 130px at 96% -12%, color-mix(in srgb, var(--ambient-violet) 46%, transparent), transparent 76%),
|
|
|
|
|
var(--bg-base);
|
|
|
|
|
}
|
|
|
|
|
.app-topbar { padding: 0 10px; gap: 6px; }
|
|
|
|
|
.app-topbar__menu-btn { display: grid; }
|
|
|
|
|
.app-topbar__menu-btn { display: none; }
|
|
|
|
|
.app-mobile-brand { display: grid; }
|
|
|
|
|
.app-breadcrumb { font-size: 11.5px; max-width: 130px; }
|
|
|
|
|
.app-topbar :deep(.el-dropdown) { display: block; width: 36px; min-width: 36px; height: 36px; flex: 0 0 36px; line-height: 0; }
|
|
|
|
|
.app-user-btn { display: grid; width: 36px; min-width: 36px; height: 36px; min-height: 36px; padding: 0; flex: 0 0 36px; place-items: center; border: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
|
|
|
|
.app-user-btn:hover { background: transparent; }
|
|
|
|
|
.app-user-btn__avatar { width: 36px; min-width: 36px; height: 36px; min-height: 36px; flex-basis: 36px; filter: drop-shadow(0 3px 5px color-mix(in srgb, var(--purple) 24%, transparent)); }
|
|
|
|
|
.app-user-btn__name { display: none; }
|
|
|
|
|
.workspace-tabs { width: 100%; overflow-x: auto; margin-bottom: 12px; }
|
|
|
|
|
.workspace-tabs button { flex: 1; min-width: max-content; }
|
|
|
|
|
.app-bottom-nav { display: flex; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|