前端UI界面美化-2

This commit is contained in:
Cheng Zhou
2025-12-22 08:54:34 +08:00
parent 4f510871bf
commit 03fddba406
8 changed files with 873 additions and 257 deletions
+199 -78
View File
@@ -1,71 +1,90 @@
<template>
<div class="page">
<el-card class="mb-12">
<div class="header">
<div>
<h2>我的工作台</h2>
<div class="sub">基于当前角色为你聚合最需要处理的事项</div>
</div>
<el-tag type="info">{{ roleLabel }}</el-tag>
<div class="workbench-container">
<!-- 头部欢迎区域 -->
<div class="workbench-header">
<div class="header-content">
<h1 class="welcome-title">我的工作台</h1>
<p class="welcome-subtitle">欢迎回来这是基于您的角色 <strong>{{ roleLabel }}</strong> 聚合的待办事项</p>
</div>
<div v-if="!currentStudyId" class="empty-tip">
请选择一个项目后查看工作台内容
<div class="header-actions">
<el-tag effect="dark" class="role-badge">{{ roleLabel }}</el-tag>
</div>
</el-card>
</div>
<!-- 提示未选择项目 -->
<el-alert
v-if="!currentStudyId"
title="请先在顶部导航或项目列表中选择一个项目,以开启完整的工作台功能。"
type="warning"
show-icon
:closable="false"
class="study-alert"
/>
<template v-if="currentStudyId">
<el-row :gutter="12">
<el-col :span="12">
<!-- 核心看板层 -->
<div class="stats-row">
<div class="stats-col">
<SectionCard
title="今日待办"
:items="todayList"
:loading="loading"
empty-text="暂无需要你处理的事项"
empty-text="今天暂时没有需要紧急处理的业务事项"
@more="goMore(todayMorePath)"
/>
</el-col>
<el-col :span="12">
</div>
<div class="stats-col">
<SectionCard
title="已逾期"
title="已逾期事项"
:items="overdueList"
:loading="loading"
empty-text="暂无需要你处理的事项"
empty-text="太棒了目前没有任何逾期待处理的事项"
@more="goMore(overdueMorePath)"
/>
</el-col>
</el-row>
</div>
</div>
<el-row :gutter="12" class="mt-12">
<el-col :span="12">
<!-- 次级看板与快捷入口 -->
<div class="secondary-row">
<div class="action-col">
<SectionCard
title="需要我处理的事项"
:items="actionList"
:loading="loading"
empty-text="暂无需要处理的事项"
empty-text="暂无需要处理的事项"
@more="goMore(actionMorePath)"
/>
</el-col>
<el-col :span="12">
<el-card shadow="never" class="section-card">
<div class="section-header">
<span>快捷入口</span>
<el-button type="primary" link @click="goMore('/study/home')">进入项目</el-button>
</div>
<div class="quick-list">
<el-button v-for="qa in quickActions" :key="qa.path" type="primary" plain size="small" @click="goMore(qa.path)">
{{ qa.label }}
</div>
<div class="quick-col">
<div class="quick-entry-card">
<div class="entry-header">
<span class="entry-title">快捷入口</span>
<el-button type="primary" link @click="goMore('/study/home')" class="entry-more">
进入项目 <el-icon class="el-icon--right"><Right /></el-icon>
</el-button>
</div>
<div v-if="!quickActions.length" class="empty">{{ emptyText }}</div>
</el-card>
</el-col>
</el-row>
<div class="quick-grid">
<div
v-for="qa in quickActions"
:key="qa.path"
class="quick-item"
@click="goMore(qa.path)"
>
<div class="quick-icon-box">
<el-icon><component :is="qa.icon" /></el-icon>
</div>
<span class="quick-label">{{ qa.label }}</span>
</div>
</div>
<div v-if="!quickActions.length" class="empty-quick">{{ emptyText }}</div>
</div>
</div>
</div>
<el-row v-if="showPersonalTodo" class="mt-12">
<el-col :span="24">
<PersonalTodo :storage-key="todoStorageKey" />
</el-col>
</el-row>
<!-- 个人备忘层 -->
<div v-if="showPersonalTodo" class="todo-row">
<PersonalTodo :storage-key="todoStorageKey" />
</div>
</template>
</div>
</template>
@@ -79,6 +98,7 @@ import { useStudyStore } from "../../store/study";
import { fetchAes } from "../../api/aes";
import { fetchFinanceItems } from "../../api/finance";
import { fetchImpTransactions } from "../../api/impTransactions";
import { Right, Timer, UserFilled, Warning, Money, Collection, Management } from "@element-plus/icons-vue";
interface WorkItem {
title: string;
@@ -112,18 +132,18 @@ const quickActions = computed(() => {
if (!currentStudyId.value) return [];
if (role.value === "CRA")
return [
{ label: "伦理启动", path: "/study/milestones" },
{ label: "AE 列表", path: "/study/aes" },
{ label: "伦理启动", path: "/study/milestones", icon: Timer },
{ label: "AE 列表", path: "/study/aes", icon: Warning },
];
if (role.value === "PM")
return [
{ label: "伦理启动", path: "/study/milestones" },
{ label: "费用审核", path: "/study/finance" },
{ label: "公告维护", path: "/study/faq" },
{ label: "伦理启动", path: "/study/milestones", icon: Timer },
{ label: "费用审核", path: "/study/finance", icon: Money },
{ label: "公告管理", path: "/study/faq", icon: Collection },
];
if (role.value === "PV") return [{ label: "AE 列表", path: "/study/aes" }];
if (role.value === "IMP") return [{ label: "药品台账", path: "/study/imp/transactions" }];
return [{ label: "伦理启动", path: "/study/milestones" }];
if (role.value === "PV") return [{ label: "AE 列表", path: "/study/aes", icon: Warning }];
if (role.value === "IMP") return [{ label: "药品台账", path: "/study/imp/transactions", icon: Management }];
return [{ label: "伦理启动", path: "/study/milestones", icon: Timer }];
});
const todayMorePath = computed(() => (role.value === "IMP" ? "/study/imp/transactions" : "/study/aes"));
@@ -176,7 +196,8 @@ const loadData = async () => {
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 impTx = impRes.status === "fulfilled" ? (impRes.value.data.items || impRes.value.data || []) : [];
const impData = impRes.status === "fulfilled" ? (impRes.value as any).data : [];
const impTx = Array.isArray(impData) ? impData : (impData?.items || []);
buildSections(aes, finances, impTx);
} catch (e: any) {
@@ -193,7 +214,6 @@ const buildSections = (aes: any[], finances: any[], impTx: any[]) => {
const openAes = aes.filter((a) => a.status !== "CLOSED");
const financePending = finances.filter((f) => f.status !== "PAID");
const overdueAes = openAes.filter((a) => isOverdue(a.expected_resolution_date || a.updated_at));
if (role.value === "CRA") {
@@ -231,43 +251,144 @@ onMounted(async () => {
</script>
<style scoped>
.page {
padding: 16px;
}
.header {
.workbench-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column;
gap: 24px;
}
.sub {
color: #666;
.workbench-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.welcome-title {
font-size: 28px;
font-weight: 700;
color: var(--ctms-text-main);
margin: 0;
}
.welcome-subtitle {
margin: 8px 0 0;
font-size: 15px;
color: var(--ctms-text-secondary);
}
.role-badge {
background-color: var(--ctms-primary);
border: none;
font-weight: 600;
padding: 0 12px;
height: 28px;
line-height: 28px;
}
.stats-row, .secondary-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.todo-row {
margin-top: 8px;
}
.study-alert {
border: none;
border-left: 4px solid var(--ctms-warning);
background-color: #fffbe6;
}
/* 快捷入口卡片样式 */
.quick-entry-card {
background-color: #fff;
border: 1px solid var(--ctms-border-color);
border-radius: 8px;
height: 100%;
display: flex;
flex-direction: column;
}
.entry-header {
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--ctms-border-color);
background-color: #fafafa;
}
.entry-title {
font-size: 16px;
font-weight: 600;
color: var(--ctms-text-main);
}
.entry-more {
font-size: 13px;
}
.empty-tip {
padding: 12px 0;
color: #888;
.quick-grid {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 16px;
}
.section-card {
min-height: 220px;
}
.section-header {
.quick-item {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
gap: 10px;
cursor: pointer;
transition: all 0.3s ease;
padding: 12px;
border-radius: 8px;
}
.quick-list {
.quick-item:hover {
background-color: var(--ctms-primary-light);
transform: translateY(-2px);
}
.quick-icon-box {
width: 40px;
height: 40px;
background-color: #fff;
border: 1px solid var(--ctms-border-color);
border-radius: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--ctms-primary);
box-shadow: var(--ctms-shadow-sm);
transition: all 0.3s ease;
}
.empty {
color: #888;
.quick-item:hover .quick-icon-box {
border-color: var(--ctms-primary);
background-color: var(--ctms-primary);
color: #fff;
}
.mt-12 {
margin-top: 12px;
.quick-label {
font-size: 13px;
font-weight: 500;
color: var(--ctms-text-regular);
}
.mb-12 {
margin-bottom: 12px;
.empty-quick {
padding: 40px;
text-align: center;
color: var(--ctms-text-disabled);
}
@media (max-width: 992px) {
.stats-row, .secondary-row {
grid-template-columns: 1fr;
}
}
</style>