Files
ctms/frontend/src/views/DesktopProjectEntry.vue
T

1100 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="desktop-entry" :class="{ 'page-leaving': isTransitioning }">
<div class="entry-background-grid" aria-hidden="true"></div>
<!-- 左侧分屏总控与身份中心 -->
<aside class="entry-sidebar" data-tauri-drag-region>
<div class="sidebar-top">
<div class="sidebar-brand">
<div class="brand-mark"><span>CTMS</span></div>
<div class="brand-text">
<span class="brand-kicker">Desktop Workbench</span>
<h1>工作台总控</h1>
</div>
</div>
<!-- 身份卡片 -->
<div class="sidebar-account">
<div class="avatar-wrapper">
<span class="account-avatar">{{ userInitial }}</span>
<span class="status-dot-active"></span>
</div>
<div class="account-details">
<small>当前操作员</small>
<strong>{{ userDisplayName }}</strong>
<span class="account-email">{{ auth.user?.email || "Operator" }}</span>
</div>
<button class="logout-btn" type="button" :disabled="loggingOut" @click="logout">
<el-icon><SwitchButton /></el-icon>
<span>{{ loggingOut ? "正在退出" : "注销账号" }}</span>
</button>
</div>
</div>
<!-- 管理后台控制卡片 - 常驻左侧底部 -->
<div v-if="isAdmin" class="sidebar-admin">
<button class="admin-action-card" type="button" @click="enterAdmin">
<span class="admin-glow-layer" aria-hidden="true"></span>
<div class="admin-card-head">
<div class="admin-icon-box">
<el-icon><Monitor /></el-icon>
</div>
<div class="admin-text-box">
<span class="admin-kicker">ADMIN SYSTEM</span>
<strong>管理后台</strong>
</div>
</div>
<p class="admin-desc">人员账号管理项目授权审计及系统配置</p>
<div class="admin-foot">
<span>进入控制台</span>
<el-icon><ArrowRight /></el-icon>
</div>
</button>
</div>
</aside>
<main class="entry-main">
<div class="project-header">
<div class="header-title-block">
<span class="section-eyebrow">Project Workspace</span>
<h2>选择目标项目</h2>
</div>
<div class="header-actions-block">
<span v-if="!loading && projects.length" class="project-pill-badge">
<span class="badge-dot"></span>
<span>{{ projects.length }} 个可进入项目</span>
</span>
</div>
</div>
<!-- 骨架屏加载 -->
<div v-if="loading" class="project-cards-grid">
<div v-for="index in 6" :key="index" class="skeleton-project-card">
<div class="skeleton-head">
<div class="skeleton-title-stack">
<div class="skeleton-bar title-bar"></div>
<div class="skeleton-bar subtitle-bar"></div>
</div>
</div>
<div class="skeleton-bar full-bar"></div>
<div class="skeleton-bar medium-bar"></div>
</div>
</div>
<!-- 项目卡片网格 -->
<div v-else-if="projects.length" class="project-cards-grid">
<button
v-for="project in projects"
:key="project.id"
class="modern-project-card"
type="button"
@click="enterProject(project)"
>
<div class="project-glow-spot" aria-hidden="true"></div>
<div class="card-top-info">
<div class="project-title-stack">
<small>{{ project.code || project.protocol_no || "PROJ-TEMP" }}</small>
<strong>{{ project.name }}</strong>
</div>
<span class="status-pill-badge" :class="`status-${(project.status || 'unknown').toLowerCase()}`">
{{ statusLabel(project.status) }}
</span>
</div>
<div class="card-meta-list">
<div v-if="project.protocol_no" class="meta-item">
<span class="meta-label">方案编号</span>
<span class="meta-val">{{ project.protocol_no }}</span>
</div>
<div v-if="project.sponsor" class="meta-item">
<span class="meta-label">申办方</span>
<span class="meta-val">{{ project.sponsor }}</span>
</div>
<div v-if="project.role_in_study && !isAdmin" class="meta-item">
<span class="meta-label">分配角色</span>
<span class="meta-val">{{ project.role_in_study }}</span>
</div>
</div>
<div class="card-action-bar">
<span>进入项目工作空间</span>
<div class="action-circle-arrow">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</button>
</div>
<!-- 空状态 -->
<div v-else class="empty-project-workspace">
<div class="empty-icon-capsule"><span>CTMS</span></div>
<h3>未检测到可用项目</h3>
<p>您的账号尚未关联至任何研究项目请联系系统管理员进行项目授权与分配</p>
</div>
</main>
<!-- 科技感转场遮罩 -->
<div class="transition-overlay" :class="{ active: isTransitioning }" aria-hidden="true">
<div class="overlay-glow"></div>
<div class="overlay-spinner">
<span class="spinner-brand">CTMS</span>
<div class="spinner-ring"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { ArrowRight, Monitor, SwitchButton, Refresh } from "@element-plus/icons-vue";
import { fetchStudies } from "../api/studies";
import { TEXT } from "../locales";
import { forceLogout, LOGOUT_REASON_MANUAL } from "../session/sessionManager";
import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import type { Study } from "../types/api";
import { findFirstAccessibleProjectPath } from "../utils/projectRoutePermissions";
import { isSystemAdmin } from "../utils/roles";
const auth = useAuthStore();
const studyStore = useStudyStore();
const router = useRouter();
const projects = ref<Study[]>([]);
const loading = ref(false);
const loggingOut = ref(false);
const isTransitioning = ref(false);
const isAdmin = computed(() => isSystemAdmin(auth.user));
const userDisplayName = computed(
() => auth.user?.full_name || auth.user?.username || auth.user?.email || TEXT.common.labels.userFallback,
);
const userInitial = computed(() => userDisplayName.value.charAt(0).toUpperCase() || TEXT.common.labels.userInitialFallback);
const entrySubtitle = computed(() =>
isAdmin.value ? "进入管理后台,或选择一个项目开始桌面工作。" : "请选择本次要进入的项目。",
);
const statusLabel = (status: string | undefined) =>
status ? TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status : "未知";
const loadProjects = async () => {
loading.value = true;
try {
const { data } = await fetchStudies();
projects.value = Array.isArray(data) ? data : (data as any).items || [];
} catch (error: any) {
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
} finally {
loading.value = false;
}
};
const enterAdmin = () => {
if (!isAdmin.value) return;
isTransitioning.value = true;
window.setTimeout(() => {
studyStore.clearCurrentStudy();
router.push("/admin/users");
}, 300);
};
const enterProject = async (project: Study) => {
studyStore.setCurrentStudy(project);
await studyStore.loadCurrentStudyPermissions().catch(() => {});
const role = studyStore.currentStudyRole || project.role_in_study || "";
const landingPath = findFirstAccessibleProjectPath(studyStore.currentPermissions, role, isAdmin.value);
if (!landingPath) {
studyStore.clearCurrentStudy();
ElMessage.warning("当前账号暂无该项目可访问模块,请联系管理员确认权限。");
return;
}
isTransitioning.value = true;
window.setTimeout(() => {
router.push(landingPath);
}, 300);
};
const logout = async () => {
if (loggingOut.value) return;
loggingOut.value = true;
try {
await forceLogout(LOGOUT_REASON_MANUAL);
} finally {
window.setTimeout(() => {
loggingOut.value = false;
}, 300);
}
};
onMounted(() => {
loadProjects();
});
</script>
<style scoped>
/* =====================================================
* 全局双分屏容器与背景网格
* ===================================================== */
.desktop-entry {
display: grid;
grid-template-columns: 340px minmax(0, 1fr);
width: 100vw;
height: 100vh;
height: 100dvh;
overflow: hidden;
background: #f8fafc;
color: #0f172a;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
transition: opacity 300ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
}
.desktop-entry.page-leaving {
opacity: 0;
transform: scale(0.985);
}
/* 极其隐约的背景网格点,仅渲染在右侧内容区 */
.entry-background-grid {
position: fixed;
inset: 340px 0 0 0;
z-index: 1;
pointer-events: none;
opacity: 0.28;
background-image: radial-gradient(rgba(148, 163, 184, 0.15) 1px, transparent 1px);
background-size: 20px 20px;
}
/* =====================================================
* 左侧分屏:总控中心 (Aside Sidebar)
* ===================================================== */
.entry-sidebar {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px 28px;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
border-right: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 10px 0 40px rgba(15, 23, 42, 0.15);
}
/* 顶部品牌与Logo */
.sidebar-brand {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 48px;
}
.brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: #ffffff;
font-size: 13.5px;
font-weight: 900;
letter-spacing: 0.05em;
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.25);
}
.brand-text h1 {
margin: 1px 0 0;
color: #ffffff;
font-size: 17.5px;
font-weight: 800;
letter-spacing: -0.02em;
}
.brand-kicker {
display: block;
color: rgba(147, 197, 253, 0.65);
font-size: 9.5px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* 个人信息卡片(暗色拟物化) */
.sidebar-account {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 24px 20px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.avatar-wrapper {
position: relative;
margin-bottom: 14px;
}
.account-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 58px;
height: 58px;
border-radius: 14px;
background: linear-gradient(135deg, #3b82f6 0%, #1e3a8a 100%);
color: #ffffff;
font-size: 21px;
font-weight: 800;
box-shadow: 0 8px 24px rgba(29, 78, 216, 0.3);
}
.status-dot-active {
position: absolute;
bottom: -2px;
right: -2px;
width: 12px;
height: 12px;
border: 2.5px solid #1e293b;
border-radius: 50%;
background: #10b981;
}
.account-details {
display: flex;
flex-direction: column;
width: 100%;
gap: 2px;
margin-bottom: 20px;
}
.account-details small {
color: rgba(255, 255, 255, 0.4);
font-size: 9.5px;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.account-details strong {
color: #ffffff;
font-size: 15px;
font-weight: 700;
}
.account-email {
color: #94a3b8;
font-size: 11.5px;
word-break: break-all;
opacity: 0.8;
}
.logout-btn {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
height: 36px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
background: transparent;
color: rgba(255, 255, 255, 0.7);
font-size: 12.5px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.logout-btn:hover {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.2);
color: #f87171;
}
/* 常驻管理后台卡片(精致深黑流光) */
.sidebar-admin {
width: 100%;
}
.admin-action-card {
appearance: none;
position: relative;
display: block;
width: 100%;
overflow: hidden;
padding: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
background: linear-gradient(135deg, #090d16 0%, #111827 100%);
color: #ffffff;
cursor: pointer;
text-align: left;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.admin-glow-layer {
position: absolute;
top: -40px;
right: -40px;
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(59, 130, 246, 0.15);
filter: blur(15px);
pointer-events: none;
}
.admin-action-card:hover {
transform: translateY(-2px);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 16px 30px rgba(59, 130, 246, 0.12);
}
.admin-card-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.admin-icon-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
font-size: 16px;
}
.admin-text-box {
display: flex;
flex-direction: column;
}
.admin-kicker {
color: rgba(59, 130, 246, 0.7);
font-size: 8.5px;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.admin-text-box strong {
font-size: 13.5px;
font-weight: 700;
}
.admin-desc {
margin: 0 0 12px;
color: #64748b;
font-size: 11px;
line-height: 1.4;
}
.admin-foot {
display: flex;
align-items: center;
justify-content: space-between;
color: #3b82f6;
font-size: 11.5px;
font-weight: 700;
}
.admin-foot .el-icon {
font-size: 12px;
transition: transform 0.2s ease;
}
.admin-action-card:hover .admin-foot .el-icon {
transform: translateX(2px);
}
/* =====================================================
* 右侧分屏:主内容与项目中心 (Main Workspace)
* ===================================================== */
.entry-main {
position: relative;
z-index: 5;
display: flex;
flex-direction: column;
padding: 48px 48px 48px 40px;
overflow-y: auto;
}
.project-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 32px;
}
.header-title-block h2 {
margin: 2px 0 0;
color: #0f172a;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
}
.header-title-block p {
margin: 6px 0 0;
color: #475569;
font-size: 13px;
line-height: 1.55;
max-width: 600px;
}
.section-eyebrow {
display: inline-flex;
align-items: center;
color: #3b82f6;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.section-eyebrow::before {
display: inline-block;
width: 10px;
height: 2px;
margin-right: 5px;
border-radius: 999px;
background: #3b82f6;
content: "";
}
.header-actions-block {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.project-pill-badge {
display: inline-flex;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 10px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.08);
color: #1d4ed8;
font-size: 11.5px;
font-weight: 700;
}
.badge-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #3b82f6;
}
.btn-refresh-work {
min-width: 76px;
height: 28px;
border-radius: 8px;
font-weight: 700;
font-size: 12px;
}
/* =====================================================
* 项目卡片网格布局(现代三列式宽屏)
* ===================================================== */
.project-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.modern-project-card {
appearance: none;
position: relative;
display: flex;
flex-direction: column;
min-height: 168px;
padding: 20px;
border: 1px solid rgba(226, 232, 240, 0.85);
border-radius: 8px;
background: #ffffff;
color: inherit;
cursor: pointer;
text-align: left;
box-shadow:
0 4px 15px rgba(15, 23, 42, 0.01),
0 1px 2px rgba(15, 23, 42, 0.02);
transition: all 0.28s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
}
/* 顶部流影光斑,Hover 时移动点亮 */
.project-glow-spot {
position: absolute;
top: -40px;
left: -40px;
width: 100px;
height: 100px;
border-radius: 50%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
.modern-project-card:hover {
transform: translateY(-2px);
border-color: rgba(59, 130, 246, 0.35);
box-shadow:
0 12px 30px rgba(15, 23, 42, 0.04),
0 0 0 1px rgba(59, 130, 246, 0.04);
}
.modern-project-card:hover .project-glow-spot {
opacity: 1;
transform: translate(20px, 20px);
}
.card-top-info {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.project-title-stack {
display: flex;
flex-direction: column;
min-width: 0;
}
.project-title-stack small {
color: #64748b;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.01em;
}
.project-title-stack strong {
color: #0f172a;
font-size: 14.5px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 1px;
}
/* 扁平状态标签 */
.status-pill-badge {
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
padding: 0 8px;
border-radius: 4px;
background: #f1f5f9;
color: #475569;
font-size: 10px;
font-weight: 700;
}
.status-active, .status-running {
background: #ecfdf5;
color: #047857;
}
.status-draft {
background: #f8fafc;
color: #64748b;
}
.status-locked,
.status-closed {
background: #fffbeb;
color: #b45309;
}
/* 核心元数据区 */
.card-meta-list {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 14px;
font-size: 11.5px;
color: #475569;
}
.meta-item {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
}
.meta-label {
color: #64748b;
font-weight: 600;
flex-shrink: 0;
}
.meta-val {
color: #0f172a;
font-weight: 550;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 底部功能栏 */
.card-action-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 10px;
border-top: 1px solid rgba(241, 245, 249, 0.85);
color: #1e3a8a;
font-size: 11px;
font-weight: 800;
}
.action-circle-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(59, 130, 246, 0.06);
color: #2563eb;
transition: all 0.2s ease;
}
.modern-project-card:hover .action-circle-arrow {
background: #2563eb;
color: #ffffff;
transform: translateX(2px);
}
/* 空状态 */
.empty-project-workspace {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 240px;
padding: 40px;
border: 1px dashed #cbd5e1;
border-radius: 8px;
background: #ffffff;
color: #64748b;
text-align: center;
}
.empty-icon-capsule {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 8px;
background: #eff6ff;
color: #2563eb;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.05em;
margin-bottom: 12px;
}
.empty-project-workspace h3 {
margin: 0 0 6px;
color: #0f172a;
font-size: 14.5px;
font-weight: 700;
}
.empty-project-workspace p {
margin: 0;
font-size: 12px;
line-height: 1.5;
max-width: 320px;
}
/* 骨架屏 */
.skeleton-project-card {
height: 168px;
padding: 20px;
border: 1px solid rgba(226, 232, 240, 0.85);
border-radius: 8px;
background: #ffffff;
display: flex;
flex-direction: column;
gap: 14px;
}
.skeleton-head {
display: flex;
align-items: center;
gap: 10px;
}
.skeleton-avatar {
width: 42px;
height: 42px;
border-radius: 8px;
background: linear-gradient(90deg, #f1f5f9 0%, #e2e8f0 50%, #f1f5f9 100%);
background-size: 200% 100%;
animation: pulseSk 1.5s infinite;
}
.skeleton-title-stack {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}
.skeleton-bar {
height: 10px;
border-radius: 4px;
background: linear-gradient(90deg, #f1f5f9 0%, #e2e8f0 50%, #f1f5f9 100%);
background-size: 200% 100%;
animation: pulseSk 1.5s infinite;
}
.title-bar {
width: 60%;
height: 12px;
}
.subtitle-bar {
width: 35%;
}
.full-bar {
width: 100%;
}
.medium-bar {
width: 75%;
}
@keyframes pulseSk {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* =====================================================
* 深色主题适配 (Dark Mode)
* ===================================================== */
:global([data-ctms-theme="dark"] .desktop-entry) {
background: #020617;
color: #f1f5f9;
}
:global([data-ctms-theme="dark"] .entry-background-grid) {
opacity: 0.08;
background-image: radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px);
}
:global([data-ctms-theme="dark"] .entry-sidebar) {
background: linear-gradient(180deg, #090d16 0%, #030712 100%);
border-right-color: rgba(255, 255, 255, 0.02);
box-shadow: 10px 0 40px rgba(0, 0, 0, 0.4);
}
:global([data-ctms-theme="dark"] .sidebar-account) {
background: rgba(255, 255, 255, 0.01);
border-color: rgba(255, 255, 255, 0.02);
}
:global([data-ctms-theme="dark"] .logout-btn) {
border-color: rgba(255, 255, 255, 0.05);
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .logout-btn:hover) {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
:global([data-ctms-theme="dark"] .admin-action-card) {
background: linear-gradient(135deg, #030712 0%, #090d16 100%);
border-color: rgba(255, 255, 255, 0.04);
}
:global([data-ctms-theme="dark"] .admin-action-card:hover) {
border-color: rgba(59, 130, 246, 0.4);
box-shadow: 0 12px 25px rgba(59, 130, 246, 0.15);
}
:global([data-ctms-theme="dark"] .header-title-block h2),
:global([data-ctms-theme="dark"] .project-title-stack strong),
:global([data-ctms-theme="dark"] .empty-project-workspace h3) {
color: #f8fafc;
}
:global([data-ctms-theme="dark"] .header-title-block p),
:global([data-ctms-theme="dark"] .project-title-stack small),
:global([data-ctms-theme="dark"] .meta-label) {
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .meta-val) {
color: #e2e8f0;
}
:global([data-ctms-theme="dark"] .project-pill-badge) {
background: rgba(59, 130, 246, 0.12);
color: #60a5fa;
}
:global([data-ctms-theme="dark"] .modern-project-card) {
background: rgba(15, 23, 42, 0.55);
border-color: rgba(51, 65, 85, 0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
:global([data-ctms-theme="dark"] .modern-project-card:hover) {
background: rgba(30, 41, 59, 0.45);
border-color: rgba(59, 130, 246, 0.45);
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.25);
}
:global([data-ctms-theme="dark"] .card-action-bar) {
border-top-color: rgba(51, 65, 85, 0.45);
color: #93c5fd;
}
:global([data-ctms-theme="dark"] .action-circle-arrow) {
background: rgba(255, 255, 255, 0.05);
color: #93c5fd;
}
:global([data-ctms-theme="dark"] .modern-project-card:hover .action-circle-arrow) {
background: #2563eb;
color: #ffffff;
}
:global([data-ctms-theme="dark"] .status-pill-badge) {
background: #1e293b;
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .status-active, [data-ctms-theme="dark"] .status-running) {
background: rgba(16, 185, 129, 0.12);
color: #34d399;
}
:global([data-ctms-theme="dark"] .status-locked, [data-ctms-theme="dark"] .status-closed) {
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
}
:global([data-ctms-theme="dark"] .empty-project-workspace) {
background: rgba(15, 23, 42, 0.35);
border-color: rgba(51, 65, 85, 0.6);
}
:global([data-ctms-theme="dark"] .empty-icon-capsule) {
background: #1e293b;
color: #60a5fa;
}
/* =====================================================
* 科技感转场遮罩动效
* ===================================================== */
.transition-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 50% 50%, #0f172a 0%, #020617 100%);
opacity: 0;
pointer-events: none;
transition: opacity 320ms cubic-bezier(0.16, 1, 0.3, 1);
}
.transition-overlay.active {
opacity: 1;
pointer-events: auto;
}
.overlay-glow {
position: absolute;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.12) 0%, transparent 70%);
filter: blur(40px);
animation: glowFloat 5s ease-in-out infinite;
}
.overlay-spinner {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner-brand {
color: #ffffff;
font-size: 15px;
font-weight: 900;
letter-spacing: 0.1em;
text-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
animation: pulseBrand 1.4s ease-in-out infinite alternate;
}
.spinner-ring {
position: absolute;
width: 96px;
height: 96px;
border: 2px solid transparent;
border-top-color: #3b82f6;
border-bottom-color: rgba(59, 130, 246, 0.15);
border-radius: 50%;
animation: rotateRing 0.75s linear infinite;
}
@keyframes rotateRing {
to { transform: rotate(360deg); }
}
@keyframes pulseBrand {
from { opacity: 0.5; transform: scale(0.96); }
to { opacity: 1; transform: scale(1.04); }
}
@keyframes glowFloat {
0%, 100% { transform: scale(1) translate(0, 0); }
50% { transform: scale(1.08) translate(15px, -15px); }
}
</style>