1100 lines
26 KiB
Vue
1100 lines
26 KiB
Vue
<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>
|