feat(ui): elevate project overview for executive readout

This commit is contained in:
Cheng Zhou
2026-03-04 16:34:25 +08:00
parent 1b920520d6
commit 90d724a5fe
4 changed files with 82 additions and 4 deletions
+13
View File
@@ -54,6 +54,19 @@ for (const file of pageContractChecks) {
} }
} }
const overview = readFileSync("src/views/ia/ProjectOverview.vue", "utf8");
const requiredOverviewClasses = [
"ctms-page-shell",
"kpi",
"unified-section"
];
for (const className of requiredOverviewClasses) {
if (!overview.includes(className)) {
missing.push(`src/views/ia/ProjectOverview.vue:${className}`);
}
}
if (missing.length) { if (missing.length) {
console.error("UI contract check failed. Missing:", missing.join(", ")); console.error("UI contract check failed. Missing:", missing.join(", "));
process.exit(1); process.exit(1);
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="kpi-card" :class="[`type-${type}`]"> <article class="kpi-card kpi-enterprise" :class="[`type-${type}`]">
<!-- 顶部标签区域 --> <!-- 顶部标签区域 -->
<div class="kpi-badge"> <div class="kpi-badge">
<div class="kpi-badge-icon" v-if="icon"> <div class="kpi-badge-icon" v-if="icon">
@@ -30,7 +30,7 @@
</template> </template>
</div> </div>
</div> </div>
</div> </article>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
+15
View File
@@ -188,6 +188,21 @@
color: var(--unified-muted-color); color: var(--unified-muted-color);
} }
.unified-shell .kpi-enterprise {
min-height: 132px;
border-radius: 14px;
padding: 16px;
}
.unified-shell .kpi-enterprise .kpi-title {
font-size: 16px;
line-height: 1.25;
}
.unified-shell .kpi-enterprise .kpi-value {
font-size: 34px;
}
@media (max-width: 960px) { @media (max-width: 960px) {
.unified-action-bar, .unified-action-bar,
.unified-section { .unified-section {
+52 -2
View File
@@ -1,7 +1,21 @@
<template> <template>
<div class="page"> <div class="page ctms-page-shell">
<div v-if="study.currentStudy" class="page-body"> <div v-if="study.currentStudy" class="page-body">
<div class="unified-shell"> <div class="unified-shell ctms-table-card">
<section class="unified-section">
<div class="kpi-grid">
<KpiCard
v-for="item in kpiItems"
:key="item.key"
class="kpi"
:title="item.title"
:value="item.value"
:subtext="item.subtext"
:type="item.type"
/>
</div>
</section>
<section class="unified-section"> <section class="unified-section">
<div class="overview-meta-inline"> <div class="overview-meta-inline">
<el-tag v-if="usingDemo" effect="plain" type="info" class="demo-tag">示例数据</el-tag> <el-tag v-if="usingDemo" effect="plain" type="info" class="demo-tag">示例数据</el-tag>
@@ -69,6 +83,7 @@ import { fetchProjectOverview } from "../../api/overview";
import { fetchSites } from "../../api/sites"; import { fetchSites } from "../../api/sites";
import StateEmpty from "../../components/StateEmpty.vue"; import StateEmpty from "../../components/StateEmpty.vue";
import StateLoading from "../../components/StateLoading.vue"; import StateLoading from "../../components/StateLoading.vue";
import KpiCard from "../../components/KpiCard.vue";
import CenterProgressRow from "./project-overview/CenterProgressRow.vue"; import CenterProgressRow from "./project-overview/CenterProgressRow.vue";
import EnrollmentBarChart, { type EnrollmentBarItem } from "./project-overview/EnrollmentBarChart.vue"; import EnrollmentBarChart, { type EnrollmentBarItem } from "./project-overview/EnrollmentBarChart.vue";
import { adaptProjectOverview, type CenterOverview, type ProjectOverviewViewModel } from "./project-overview/overview.adapter"; import { adaptProjectOverview, type CenterOverview, type ProjectOverviewViewModel } from "./project-overview/overview.adapter";
@@ -84,6 +99,35 @@ const updatedAtLabel = ref(displayDateTime(new Date()));
const preferApi = String(import.meta.env.VITE_USE_OVERVIEW_API || "").toLowerCase() === "true"; const preferApi = String(import.meta.env.VITE_USE_OVERVIEW_API || "").toLowerCase() === "true";
const centers = computed(() => overview.value?.centers || []); const centers = computed(() => overview.value?.centers || []);
const kpiItems = computed(() => {
const summary = overview.value?.summary;
const totalTarget = summary?.total_target ?? 0;
const totalActual = summary?.total_actual ?? 0;
const activeCenters = centers.value.filter((item) => item.is_active !== false).length;
return [
{
key: "target",
title: "总入组目标",
value: totalTarget,
subtext: "项目整体计划",
type: "primary" as const,
},
{
key: "actual",
title: "当前入组",
value: totalActual,
subtext: `达成率 ${totalTarget > 0 ? Math.round((totalActual / totalTarget) * 100) : 0}%`,
type: "success" as const,
},
{
key: "centers",
title: "活跃中心",
value: activeCenters,
subtext: `中心总数 ${centers.value.length}`,
type: "info" as const,
},
];
});
const buildFallbackCentersFromSites = (siteList: any[]): CenterOverview[] => const buildFallbackCentersFromSites = (siteList: any[]): CenterOverview[] =>
siteList.map((site: any) => ({ siteList.map((site: any) => ({
@@ -208,6 +252,12 @@ watch(
gap: 0; gap: 0;
} }
.kpi-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.demo-tag { .demo-tag {
border-color: var(--ctms-border-color); border-color: var(--ctms-border-color);
color: var(--ctms-text-secondary); color: var(--ctms-text-secondary);