feat(ui): elevate project overview for executive readout
This commit is contained in:
@@ -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) {
|
||||
console.error("UI contract check failed. Missing:", missing.join(", "));
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-icon" v-if="icon">
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -188,6 +188,21 @@
|
||||
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) {
|
||||
.unified-action-bar,
|
||||
.unified-section {
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page ctms-page-shell">
|
||||
<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">
|
||||
<div class="overview-meta-inline">
|
||||
<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 StateEmpty from "../../components/StateEmpty.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import KpiCard from "../../components/KpiCard.vue";
|
||||
import CenterProgressRow from "./project-overview/CenterProgressRow.vue";
|
||||
import EnrollmentBarChart, { type EnrollmentBarItem } from "./project-overview/EnrollmentBarChart.vue";
|
||||
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 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[] =>
|
||||
siteList.map((site: any) => ({
|
||||
@@ -208,6 +252,12 @@ watch(
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.demo-tag {
|
||||
border-color: var(--ctms-border-color);
|
||||
color: var(--ctms-text-secondary);
|
||||
|
||||
Reference in New Issue
Block a user