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) {
|
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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user