未知(继上次中断)
This commit is contained in:
@@ -4,9 +4,6 @@
|
||||
<div class="workbench-header">
|
||||
<div class="header-content">
|
||||
<h1 class="welcome-title">{{ TEXT.modules.workbench.title }}</h1>
|
||||
<p class="welcome-subtitle">
|
||||
{{ TEXT.modules.workbench.subtitle.replace("{role}", roleLabel) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-tag effect="dark" class="role-badge">{{ roleLabel }}</el-tag>
|
||||
@@ -24,6 +21,9 @@
|
||||
/>
|
||||
|
||||
<template v-if="currentStudyId">
|
||||
<!-- 汇总框 -->
|
||||
<CenterSummary :data="centerSummaryData" :loading="loading" />
|
||||
|
||||
<!-- 核心看板层 -->
|
||||
<div class="stats-row">
|
||||
<div class="stats-col">
|
||||
@@ -36,13 +36,41 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<SectionCard
|
||||
:title="TEXT.modules.workbench.overdueTitle"
|
||||
:items="overdueList"
|
||||
:loading="loading"
|
||||
:empty-text="TEXT.modules.workbench.overdueEmpty"
|
||||
@more="goMore(overdueMorePath)"
|
||||
/>
|
||||
<div class="notifications-card">
|
||||
<div class="notifications-header">
|
||||
<div class="notifications-title-group">
|
||||
<span class="notifications-title">{{ TEXT.modules.projectOverview.notificationsTitle }}</span>
|
||||
<span class="notifications-subtitle">{{ TEXT.modules.projectOverview.notificationsSubtitle }}</span>
|
||||
</div>
|
||||
<el-button v-if="notifications.length" type="primary" link @click="goMore('/documents')" class="notifications-more">
|
||||
{{ TEXT.common.actions.more }} <el-icon class="el-icon--right"><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-skeleton v-if="loading" :rows="3" animated />
|
||||
<StateEmpty v-else-if="!notifications.length" :description="TEXT.modules.projectOverview.notificationsEmpty" />
|
||||
<div v-else class="notification-list">
|
||||
<div
|
||||
class="notification-item"
|
||||
v-for="item in notifications.slice(0, 5)"
|
||||
:key="item.id"
|
||||
@click="openDocument(item.document_id)"
|
||||
>
|
||||
<div class="notification-main">
|
||||
<div class="notification-title">
|
||||
<span class="notification-doc">{{ item.document_title }}</span>
|
||||
<span class="notification-version">V{{ item.version_no }}</span>
|
||||
</div>
|
||||
<div class="notification-meta">
|
||||
<span class="notification-time">{{ formatDate(item.effective_at || item.created_at) }}</span>
|
||||
<span v-if="item.change_summary" class="notification-summary">{{ item.change_summary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-update-badge">
|
||||
<span class="update-text">更新</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,11 +78,11 @@
|
||||
<div class="secondary-row">
|
||||
<div class="action-col">
|
||||
<SectionCard
|
||||
:title="TEXT.modules.workbench.actionTitle"
|
||||
:items="actionList"
|
||||
:title="TEXT.modules.workbench.overdueTitle"
|
||||
:items="overdueList"
|
||||
:loading="loading"
|
||||
:empty-text="TEXT.modules.workbench.actionEmpty"
|
||||
@more="goMore(actionMorePath)"
|
||||
:empty-text="TEXT.modules.workbench.overdueEmpty"
|
||||
@more="goMore(overdueMorePath)"
|
||||
/>
|
||||
</div>
|
||||
<div class="quick-col">
|
||||
@@ -83,10 +111,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人备忘层 -->
|
||||
<div v-if="showPersonalTodo" class="todo-row">
|
||||
<PersonalTodo :storage-key="todoStorageKey" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -98,10 +122,14 @@ import { ElMessage } from "element-plus";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { fetchAes } from "../../api/aes";
|
||||
import { listFinanceContracts } from "../../api/financeContracts";
|
||||
import { Right, Timer, UserFilled, Warning, Money, Collection } from "@element-plus/icons-vue";
|
||||
import { fetchLostVisits, fetchCenterSummary } from "../../api/dashboard";
|
||||
import { listNotifications } from "../../api/notifications";
|
||||
import { Right, Timer, Warning, Money, Collection, RefreshRight, ArrowRight } from "@element-plus/icons-vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { displayEnum } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
import type { NotificationItem } from "../../types/notifications";
|
||||
import type { VisitLostItem } from "../../types/visits";
|
||||
|
||||
interface WorkItem {
|
||||
title: string;
|
||||
@@ -112,8 +140,7 @@ interface WorkItem {
|
||||
}
|
||||
|
||||
const SectionCard = defineAsyncComponent(() => import("./components/SectionCard.vue"));
|
||||
const PersonalTodo = defineAsyncComponent(() => import("./components/PersonalTodo.vue"));
|
||||
|
||||
const CenterSummary = defineAsyncComponent(() => import("./components/CenterSummary.vue"));
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const router = useRouter();
|
||||
@@ -121,15 +148,14 @@ const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const todayList = ref<WorkItem[]>([]);
|
||||
const overdueList = ref<WorkItem[]>([]);
|
||||
const actionList = ref<WorkItem[]>([]);
|
||||
const notifications = ref<NotificationItem[]>([]);
|
||||
const lostVisits = ref<VisitLostItem[]>([]);
|
||||
const centerSummaryData = ref<any[]>([]);
|
||||
|
||||
const currentStudyId = computed(() => study.currentStudy?.id || "");
|
||||
const todayStr = computed(() => new Date().toISOString().slice(0, 10));
|
||||
const roleLabel = computed(() => displayEnum(TEXT.enums.userRole, auth.user?.role));
|
||||
const emptyText = TEXT.modules.workbench.quickEmpty;
|
||||
const showPersonalTodo = computed(() => role.value !== "ADMIN");
|
||||
const todoStorageKey = computed(() => `workbench_todos_${auth.user?.id || "guest"}`);
|
||||
|
||||
const role = computed(() => auth.user?.role || "");
|
||||
const quickActions = computed(() => {
|
||||
if (!currentStudyId.value) return [];
|
||||
@@ -150,10 +176,6 @@ const quickActions = computed(() => {
|
||||
|
||||
const todayMorePath = computed(() => "/subjects");
|
||||
const overdueMorePath = computed(() => "/subjects");
|
||||
const actionMorePath = computed(() => {
|
||||
if (role.value === "PM" || role.value === "ADMIN") return "/finance/contracts";
|
||||
return "/subjects";
|
||||
});
|
||||
|
||||
const isOverdue = (d?: string | null) => {
|
||||
if (!d) return false;
|
||||
@@ -162,18 +184,18 @@ const isOverdue = (d?: string | null) => {
|
||||
|
||||
const toAeItem = (ae: any): WorkItem => ({
|
||||
title: ae.term || TEXT.modules.workbench.aeTitleFallback,
|
||||
subtitle: study.currentStudy?.name || "",
|
||||
subtitle: "",
|
||||
status: ae.status,
|
||||
overdue: isOverdue(ae.expected_resolution_date || ae.updated_at),
|
||||
path: ae.subject_id ? `/subjects/${ae.subject_id}` : "/subjects",
|
||||
});
|
||||
|
||||
const toContractItem = (fi: any): WorkItem => ({
|
||||
title: fi.contract_no || TEXT.modules.workbench.contractTitleFallback,
|
||||
subtitle: study.currentStudy?.name || "",
|
||||
status: fi.site_name,
|
||||
overdue: false,
|
||||
path: `/finance/contracts/${fi.id}`,
|
||||
const toLostVisitItem = (visit: VisitLostItem): WorkItem => ({
|
||||
title: `${visit.subject_no} ${visit.visit_code}`,
|
||||
subtitle: "",
|
||||
status: isNewLostVisit(visit.updated_at) ? TEXT.modules.workbench.newLostVisit : TEXT.enums.visitStatus.LOST,
|
||||
overdue: true,
|
||||
path: `/subjects/${visit.subject_id}`,
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -182,13 +204,21 @@ const loadData = async () => {
|
||||
try {
|
||||
const studyId = currentStudyId.value;
|
||||
const aesReq = fetchAes(studyId, { status: "NEW", limit: 50 });
|
||||
|
||||
const [aesRes, financeRes] = await Promise.allSettled([aesReq, listFinanceContracts(studyId, { limit: 50 })]);
|
||||
const overdueReq = fetchAes(studyId, { overdue: true, limit: 50 });
|
||||
const [aesRes, overdueRes, noticeRes, lostRes, centerRes] = await Promise.allSettled([
|
||||
aesReq,
|
||||
overdueReq,
|
||||
listNotifications(studyId, { limit: 5 }),
|
||||
fetchLostVisits(studyId, { limit: 20 }),
|
||||
fetchCenterSummary(studyId),
|
||||
]);
|
||||
const aes = aesRes.status === "fulfilled" ? (aesRes.value.data.items || aesRes.value.data || []) : [];
|
||||
const finances =
|
||||
financeRes.status === "fulfilled" ? (financeRes.value.data.items || financeRes.value.data || []) : [];
|
||||
const overdueAes = overdueRes.status === "fulfilled" ? (overdueRes.value.data.items || overdueRes.value.data || []) : [];
|
||||
notifications.value = noticeRes.status === "fulfilled" ? noticeRes.value.data || [] : [];
|
||||
lostVisits.value = lostRes.status === "fulfilled" ? lostRes.value.data || [] : [];
|
||||
centerSummaryData.value = centerRes.status === "fulfilled" ? centerRes.value.data || [] : [];
|
||||
|
||||
buildSections(aes, finances);
|
||||
buildSections(aes, overdueAes, lostVisits.value);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.workbench.loadFailed);
|
||||
} finally {
|
||||
@@ -196,29 +226,23 @@ const loadData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildSections = (aes: any[], finances: any[]) => {
|
||||
const buildSections = (aes: any[], overdueAes: any[], lost: VisitLostItem[]) => {
|
||||
todayList.value = [];
|
||||
overdueList.value = [];
|
||||
actionList.value = [];
|
||||
|
||||
const openAes = aes.filter((a) => a.status !== "CLOSED");
|
||||
const financePending = finances;
|
||||
const overdueAes = openAes.filter((a) => isOverdue(a.expected_resolution_date || a.updated_at));
|
||||
const lostVisitItems = lost.map(toLostVisitItem);
|
||||
|
||||
if (role.value === "CRA") {
|
||||
todayList.value = openAes.slice(0, 5).map(toAeItem);
|
||||
overdueList.value = overdueAes.slice(0, 5).map(toAeItem);
|
||||
actionList.value = openAes.slice(0, 5).map(toAeItem);
|
||||
overdueList.value = [...lostVisitItems, ...overdueAes.map(toAeItem)].slice(0, 5);
|
||||
} else if (role.value === "PM" || role.value === "ADMIN") {
|
||||
todayList.value = openAes.slice(0, 5).map(toAeItem);
|
||||
overdueList.value = overdueAes.slice(0, 5).map(toAeItem);
|
||||
const financeTodo = financePending.slice(0, 3).map(toContractItem);
|
||||
actionList.value = [...financeTodo, ...openAes.slice(0, 2).map(toAeItem)].slice(0, 5);
|
||||
overdueList.value = [...lostVisitItems, ...overdueAes.map(toAeItem)].slice(0, 5);
|
||||
} else if (role.value === "PV") {
|
||||
const aeOpen = aes.filter((a) => a.status !== "CLOSED");
|
||||
todayList.value = aeOpen.slice(0, 5).map(toAeItem);
|
||||
overdueList.value = aeOpen.filter((a) => isOverdue(a.updated_at)).slice(0, 5).map(toAeItem);
|
||||
actionList.value = aeOpen.slice(0, 5).map(toAeItem);
|
||||
overdueList.value = [...lostVisitItems, ...overdueAes.map(toAeItem)].slice(0, 5);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,6 +251,23 @@ const goMore = (path: string) => {
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return TEXT.common.fallback;
|
||||
return value.replace("T", " ").replace("Z", "").split(".")[0];
|
||||
};
|
||||
|
||||
const isNewLostVisit = (value?: string | null) => {
|
||||
if (!value) return false;
|
||||
const updatedAt = new Date(value);
|
||||
if (Number.isNaN(updatedAt.getTime())) return false;
|
||||
const now = new Date();
|
||||
return now.getTime() - updatedAt.getTime() <= 24 * 60 * 60 * 1000;
|
||||
};
|
||||
|
||||
const openDocument = (documentId: string) => {
|
||||
router.push(`/documents/${documentId}`);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!auth.user && auth.token) {
|
||||
await auth.fetchMe().catch(() => {});
|
||||
@@ -239,141 +280,458 @@ onMounted(async () => {
|
||||
.workbench-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 28px;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部区域 - 添加渐变背景 */
|
||||
.workbench-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
margin: -8px -8px 0 -8px;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workbench-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 15px;
|
||||
color: var(--ctms-text-secondary);
|
||||
margin: 10px 0 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
background-color: var(--ctms-primary);
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-row, .secondary-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.todo-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.study-alert {
|
||||
border: none;
|
||||
border-left: 4px solid var(--ctms-warning);
|
||||
background-color: #fffbe6;
|
||||
background: linear-gradient(90deg, #fffbe6 0%, #ffffff 100%);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
}
|
||||
|
||||
/* 快捷入口卡片样式 */
|
||||
/* 快捷入口卡片样式 - 大幅优化 */
|
||||
.quick-entry-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-entry-card:hover {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
padding: 16px 20px;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--ctms-border-color);
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.08);
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.03) 0%, rgba(118, 75, 162, 0.02) 100%);
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.entry-more {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
padding: 16px 12px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.quick-item:hover {
|
||||
background-color: var(--ctms-primary-light);
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: 0 12px 28px rgba(102, 126, 234, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.quick-icon-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
border-radius: 10px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: var(--ctms-primary);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
transition: all 0.3s ease;
|
||||
font-size: 26px;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-icon-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 16px;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
.quick-item:hover .quick-icon-box {
|
||||
border-color: var(--ctms-primary);
|
||||
background-color: var(--ctms-primary);
|
||||
color: #fff;
|
||||
background: #ffffff;
|
||||
color: #667eea;
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
box-shadow: 0 12px 32px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-regular);
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-item:hover .quick-label {
|
||||
color: #ffffff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 通知卡片 - 增强样式 */
|
||||
.notifications-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fef7ff 100%);
|
||||
border: 1px solid rgba(118, 75, 162, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notifications-card:hover {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.notifications-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid rgba(118, 75, 162, 0.08);
|
||||
}
|
||||
|
||||
.notifications-title-group {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notifications-more {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notifications-more:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.notifications-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.notifications-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(102, 126, 234, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
|
||||
border-color: rgba(102, 126, 234, 0.2);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.notification-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notification-version {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.notification-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.notification-summary {
|
||||
max-width: 360px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 锯齿状更新徽章 - 扁平版 */
|
||||
.notification-update-badge {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-update-badge::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #ff4757 0%, #ff6b81 100%);
|
||||
clip-path: polygon(
|
||||
10% 0%,
|
||||
20% 8%,
|
||||
30% 0%,
|
||||
40% 8%,
|
||||
50% 0%,
|
||||
60% 8%,
|
||||
70% 0%,
|
||||
80% 8%,
|
||||
90% 0%,
|
||||
100% 15%,
|
||||
95% 50%,
|
||||
100% 85%,
|
||||
90% 100%,
|
||||
80% 92%,
|
||||
70% 100%,
|
||||
60% 92%,
|
||||
50% 100%,
|
||||
40% 92%,
|
||||
30% 100%,
|
||||
20% 92%,
|
||||
10% 100%,
|
||||
0% 85%,
|
||||
5% 50%,
|
||||
0% 15%
|
||||
);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 透明内层 - 镂空效果 */
|
||||
.notification-update-badge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
background: #ffffff;
|
||||
clip-path: polygon(
|
||||
10% 0%,
|
||||
20% 10%,
|
||||
30% 0%,
|
||||
40% 10%,
|
||||
50% 0%,
|
||||
60% 10%,
|
||||
70% 0%,
|
||||
80% 10%,
|
||||
90% 0%,
|
||||
100% 18%,
|
||||
94% 50%,
|
||||
100% 82%,
|
||||
90% 100%,
|
||||
80% 90%,
|
||||
70% 100%,
|
||||
60% 90%,
|
||||
50% 100%,
|
||||
40% 90%,
|
||||
30% 100%,
|
||||
20% 90%,
|
||||
10% 100%,
|
||||
0% 82%,
|
||||
6% 50%,
|
||||
0% 18%
|
||||
);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.update-text {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #1a1a2e;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.notification-item:hover .notification-update-badge {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.notification-item:hover .notification-update-badge::before {
|
||||
box-shadow: 0 4px 16px rgba(255, 71, 87, 0.4);
|
||||
}
|
||||
|
||||
.empty-quick {
|
||||
padding: 40px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
color: var(--ctms-text-disabled);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.stats-row, .secondary-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workbench-header {
|
||||
padding: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user