feat: 清理示例数据并统一空态体验
This commit is contained in:
@@ -84,8 +84,10 @@
|
||||
<el-table-column :label="TEXT.common.fields.remark" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="table-empty">{{ TEXT.modules.feeContracts.paymentEmpty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="detail.payments.length === 0" :description="TEXT.modules.feeContracts.paymentEmpty" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card unified-shell" shadow="never">
|
||||
@@ -116,7 +118,6 @@ import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import StateError from "../../components/StateError.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import FeeAttachmentPanel from "../../components/fees/FeeAttachmentPanel.vue";
|
||||
@@ -307,4 +308,14 @@ onMounted(async () => {
|
||||
color: var(--ctms-text-placeholder);
|
||||
font-size: 13px;
|
||||
}
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,8 +164,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="table-empty">{{ TEXT.modules.feeContracts.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="sortedContracts.length === 0" :description="TEXT.modules.feeContracts.empty" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,7 +183,6 @@ import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import StateError from "../../components/StateError.vue";
|
||||
import KpiCard from "../../components/KpiCard.vue";
|
||||
@@ -444,6 +445,16 @@ onMounted(async () => {
|
||||
font-family: var(--el-font-family);
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -163,8 +163,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="table-empty">{{ TEXT.modules.feeSpecials.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="sortedExpenses.length === 0" :description="TEXT.modules.feeSpecials.empty" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +182,6 @@ import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import StateError from "../../components/StateError.vue";
|
||||
import KpiCard from "../../components/KpiCard.vue";
|
||||
@@ -424,6 +425,16 @@ onMounted(async () => {
|
||||
color: var(--ctms-text-placeholder);
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -102,8 +102,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.drugShipments.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.drugShipments.empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +120,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listDrugShipments, deleteDrugShipment } from "../../api/drugShipments";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -312,6 +313,16 @@ onMounted(async () => {
|
||||
.text-muted {
|
||||
color: var(--ctms-text-placeholder);
|
||||
}
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -67,8 +67,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.financeContracts.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeContracts.empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +84,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listFinanceContracts, deleteFinanceContract } from "../../api/financeContracts";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayDateTime } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -223,6 +224,16 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -77,8 +77,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.financeSpecials.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeSpecials.empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +94,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listFinanceSpecials, deleteFinanceSpecial } from "../../api/financeSpecials";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayDateTime, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -235,6 +236,16 @@ onMounted(async () => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -57,8 +57,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.knowledgeNotes.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.knowledgeNotes.empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +74,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listKnowledgeNotes, deleteKnowledgeNote } from "../../api/knowledgeNotes";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -199,6 +200,16 @@ onMounted(async () => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
<el-button link type="danger" @click="removeRow(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">暂无设备数据</div>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<StateEmpty v-if="!loading && rows.length === 0" description="暂无设备数据" />
|
||||
</section>
|
||||
</div>
|
||||
<StateEmpty v-else :description="TEXT.common.empty.selectProject" />
|
||||
@@ -598,4 +599,14 @@ watch(
|
||||
.equipment-table :deep(td.el-table__cell .cell) {
|
||||
text-align: center;
|
||||
}
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -102,17 +102,15 @@
|
||||
<el-table-column prop="remark" :label="TEXT.modules.projectMilestones.columns.remark" min-width="100">
|
||||
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.projectMilestones.columns.operations" width="84" fixed="right">
|
||||
<el-table-column :label="TEXT.modules.projectMilestones.columns.operations" width="84">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openEditor(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.projectMilestones.emptyDescription }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<StateEmpty
|
||||
v-if="!loading && rows.length === 0"
|
||||
:description="TEXT.modules.projectMilestones.emptyDescription"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<StateEmpty v-else :description="TEXT.common.empty.selectProject" />
|
||||
@@ -665,4 +663,15 @@ watch(
|
||||
.ctms-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<div class="unified-shell ctms-table-card">
|
||||
<section class="unified-section">
|
||||
<div class="overview-meta-inline">
|
||||
<el-tag v-if="usingDemo" effect="plain" type="info" class="demo-tag">示例数据</el-tag>
|
||||
<div class="updated-at">更新:{{ updatedAtLabel }}</div>
|
||||
<el-button size="small" @click="loadOverview">刷新</el-button>
|
||||
</div>
|
||||
@@ -20,11 +19,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<StateLoading v-if="loading" :rows="6" />
|
||||
<StateEmpty
|
||||
v-else-if="centers.length === 0"
|
||||
title="暂无中心进度"
|
||||
description="当前项目未配置中心或暂无进度数据"
|
||||
/>
|
||||
<div v-else-if="centers.length === 0" class="overview-empty-panel">
|
||||
<div class="overview-empty-content">
|
||||
<div class="overview-empty-title">中心整体进度暂未生成</div>
|
||||
<div class="overview-empty-desc">当前项目未配置中心,或尚未形成可展示的中心进度数据。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="progress-list">
|
||||
<CenterProgressRow
|
||||
v-for="(center, index) in centers"
|
||||
@@ -72,17 +72,13 @@ import StateLoading from "../../components/StateLoading.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";
|
||||
import { overviewMock } from "./project-overview/overview.mock";
|
||||
|
||||
const study = useStudyStore();
|
||||
const loading = ref(false);
|
||||
const usingDemo = ref(false);
|
||||
const overview = ref<ProjectOverviewViewModel | null>(null);
|
||||
const chartMode = ref<"center" | "month">("center");
|
||||
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 buildFallbackCentersFromSites = (siteList: any[]): CenterOverview[] =>
|
||||
@@ -128,29 +124,35 @@ const chartEmptyText = computed(() =>
|
||||
chartMode.value === "center" ? "暂无中心入组数据" : "暂无月度入组数据"
|
||||
);
|
||||
|
||||
const buildOverviewFromSites = (studyId: string, siteList: any[]): ProjectOverviewViewModel => {
|
||||
const centers = buildFallbackCentersFromSites(siteList);
|
||||
const totalTarget = centers.reduce((sum, center) => sum + center.enrollment_target, 0);
|
||||
return {
|
||||
study_id: studyId,
|
||||
updated_at: new Date().toISOString(),
|
||||
centers,
|
||||
months: [],
|
||||
summary: {
|
||||
total_actual: 0,
|
||||
total_target: totalTarget,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const loadOverview = async () => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
loading.value = true;
|
||||
let siteList: any[] = [];
|
||||
try {
|
||||
const sitesResp = await fetchSites(studyId, { limit: 500 });
|
||||
const siteList = Array.isArray(sitesResp.data) ? sitesResp.data : (sitesResp.data as any).items || [];
|
||||
siteList = Array.isArray(sitesResp.data) ? sitesResp.data : (sitesResp.data as any).items || [];
|
||||
const siteActiveMap = new Map<string, boolean>(siteList.map((s: any) => [s.id, !!s.is_active]));
|
||||
|
||||
if (preferApi) {
|
||||
const { data } = await fetchProjectOverview(studyId);
|
||||
overview.value = adaptProjectOverview(data);
|
||||
usingDemo.value = false;
|
||||
updatedAtLabel.value = overview.value.updated_at
|
||||
? displayDateTime(overview.value.updated_at)
|
||||
: displayDateTime(new Date());
|
||||
} else {
|
||||
overview.value = adaptProjectOverview(overviewMock);
|
||||
usingDemo.value = true;
|
||||
updatedAtLabel.value = overview.value.updated_at
|
||||
? displayDateTime(overview.value.updated_at)
|
||||
: displayDateTime(new Date());
|
||||
}
|
||||
const { data } = await fetchProjectOverview(studyId);
|
||||
overview.value = adaptProjectOverview(data);
|
||||
updatedAtLabel.value = overview.value.updated_at
|
||||
? displayDateTime(overview.value.updated_at)
|
||||
: displayDateTime(new Date());
|
||||
|
||||
if (overview.value && overview.value.centers.length === 0 && siteList.length > 0) {
|
||||
overview.value.centers = buildFallbackCentersFromSites(siteList);
|
||||
@@ -166,11 +168,8 @@ const loadOverview = async () => {
|
||||
overview.value.centers.sort((a, b) => Number(b.is_active) - Number(a.is_active));
|
||||
}
|
||||
} catch {
|
||||
overview.value = adaptProjectOverview(overviewMock);
|
||||
usingDemo.value = true;
|
||||
updatedAtLabel.value = overview.value.updated_at
|
||||
? displayDateTime(overview.value.updated_at)
|
||||
: displayDateTime(new Date());
|
||||
overview.value = buildOverviewFromSites(studyId, siteList);
|
||||
updatedAtLabel.value = displayDateTime(overview.value.updated_at);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -178,7 +177,6 @@ const loadOverview = async () => {
|
||||
|
||||
const reset = () => {
|
||||
overview.value = null;
|
||||
usingDemo.value = false;
|
||||
updatedAtLabel.value = displayDateTime(new Date());
|
||||
};
|
||||
|
||||
@@ -208,11 +206,6 @@ watch(
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.demo-tag {
|
||||
border-color: var(--ctms-border-color);
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.updated-at {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -292,6 +285,37 @@ watch(
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.overview-empty-panel {
|
||||
min-height: 168px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 6px;
|
||||
padding: 24px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(246, 249, 253, 0.96), rgba(251, 253, 255, 0.98));
|
||||
}
|
||||
|
||||
.overview-empty-content {
|
||||
max-width: 520px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview-empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
color: #243a5a;
|
||||
}
|
||||
|
||||
.overview-empty-desc {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #8a97ab;
|
||||
}
|
||||
|
||||
.mode-switch :deep(.el-radio-button__inner) {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<StateEmpty v-if="!loading" description="暂无监查访视问题" />
|
||||
<div v-if="!loading" class="issue-table-empty">暂无监查访视问题</div>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
@@ -824,6 +824,17 @@ watch(
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.issue-table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.issue-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="risk-table-empty">{{ TEXT.modules.riskIssuePd.emptyDescription }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && filteredItems.length === 0" :description="TEXT.modules.riskIssuePd.emptyDescription" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +69,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listStudySubjectPds } from "../../api/subjectPds";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -190,4 +191,15 @@ onMounted(load);
|
||||
.risk-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.risk-table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -79,8 +79,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="risk-table-empty">{{ TEXT.modules.riskIssueSae.emptyDescription }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && filteredItems.length === 0" :description="TEXT.modules.riskIssueSae.emptyDescription" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +98,6 @@ import { fetchAes } from "../../api/aes";
|
||||
import { fetchSubjects } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -247,4 +248,15 @@ onMounted(load);
|
||||
.risk-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.risk-table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,8 +58,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingFeasibility" class="table-empty">{{ TEXT.modules.startupFeasibilityEthics.emptyFeasibility }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingFeasibility && sortedFeasibilityItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyFeasibility" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.ethicsTab" name="ethics">
|
||||
@@ -121,8 +123,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingEthics" class="table-empty">{{ TEXT.modules.startupFeasibilityEthics.emptyEthics }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingEthics && sortedEthicsItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyEthics" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -140,7 +144,6 @@ import { listFeasibility, listEthics, deleteFeasibility, deleteEthics } from "..
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import type { Site } from "../../types/api";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import dayjs from "dayjs";
|
||||
import type { StageStatus } from "./project-overview/overview.adapter";
|
||||
import { TEXT } from "../../locales";
|
||||
@@ -323,6 +326,16 @@ onMounted(() => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.startupMeetingAuth.emptyKickoff }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && kickoffRows.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +44,6 @@ import { fetchSites } from "../../api/sites";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -170,6 +171,16 @@ onMounted(() => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.subjectManagement.empty }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<div class="pagination-wrap" v-if="filteredItems.length > 0">
|
||||
<el-pagination
|
||||
@@ -116,7 +119,6 @@
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.subjectManagement.empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +133,6 @@ import { useStudyStore } from "../../store/study";
|
||||
import { deleteSubject, fetchSubjects } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -364,6 +365,16 @@ onMounted(async () => {
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="enrollment-chart">
|
||||
<StateLoading v-if="loading" :rows="5" />
|
||||
<StateEmpty v-else-if="items.length === 0" :description="emptyText" />
|
||||
<div v-else-if="items.length === 0" class="chart-empty-shell">
|
||||
<div class="chart-empty-body">
|
||||
<div class="chart-empty-title">暂无入组进度数据</div>
|
||||
<div class="chart-empty-desc">{{ emptyText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="chart-body">
|
||||
<div class="chart-scroll">
|
||||
<div class="chart-plot">
|
||||
@@ -94,7 +99,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import StateLoading from "../../../components/StateLoading.vue";
|
||||
import StateEmpty from "../../../components/StateEmpty.vue";
|
||||
|
||||
export type ChartMode = "center" | "month";
|
||||
|
||||
@@ -206,6 +210,37 @@ const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(va
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-empty-shell {
|
||||
min-height: 176px;
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(246, 249, 253, 0.96), rgba(251, 253, 255, 0.98));
|
||||
}
|
||||
|
||||
.chart-empty-body {
|
||||
min-height: 118px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #243a5a;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.chart-empty-desc {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #8a97ab;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import type { ProjectOverviewResponse } from "./overview.adapter";
|
||||
|
||||
// demo / overview only
|
||||
|
||||
export const overviewMock: ProjectOverviewResponse = {
|
||||
study_id: "demo-overview",
|
||||
updated_at: "2025-06-18",
|
||||
centers: [
|
||||
{
|
||||
center_id: "center-001",
|
||||
center_name: "中心01",
|
||||
institution_initiation_status: "COMPLETED",
|
||||
institution_initiation_completed_at: "2024-12-20",
|
||||
ethics_status: "COMPLETED",
|
||||
ethics_completed_at: "2025-01-12",
|
||||
contract_sign_status: "COMPLETED",
|
||||
contract_sign_completed_at: "2025-01-20",
|
||||
startup_status: "COMPLETED",
|
||||
startup_completed_at: "2025-02-03",
|
||||
enrollment_status: "IN_PROGRESS",
|
||||
inspection_status: "NOT_STARTED",
|
||||
closeout_status: "NOT_STARTED",
|
||||
enrollment_target: 80,
|
||||
enrollment_actual: 52,
|
||||
},
|
||||
{
|
||||
center_id: "center-002",
|
||||
center_name: "中心02",
|
||||
institution_initiation_status: "COMPLETED",
|
||||
institution_initiation_completed_at: "2025-01-05",
|
||||
ethics_status: "COMPLETED",
|
||||
ethics_completed_at: "2025-02-08",
|
||||
contract_sign_status: "IN_PROGRESS",
|
||||
startup_status: "IN_PROGRESS",
|
||||
enrollment_status: "NOT_STARTED",
|
||||
inspection_status: "NOT_STARTED",
|
||||
closeout_status: "NOT_STARTED",
|
||||
enrollment_target: 60,
|
||||
enrollment_actual: 0,
|
||||
},
|
||||
{
|
||||
center_id: "center-003",
|
||||
center_name: "中心03",
|
||||
institution_initiation_status: "COMPLETED",
|
||||
institution_initiation_completed_at: "2024-12-10",
|
||||
ethics_status: "COMPLETED",
|
||||
ethics_completed_at: "2024-12-28",
|
||||
contract_sign_status: "COMPLETED",
|
||||
contract_sign_completed_at: "2025-01-05",
|
||||
startup_status: "COMPLETED",
|
||||
startup_completed_at: "2025-01-20",
|
||||
enrollment_status: "COMPLETED",
|
||||
enrollment_completed_at: "2025-04-05",
|
||||
inspection_status: "IN_PROGRESS",
|
||||
closeout_status: "NOT_STARTED",
|
||||
enrollment_target: 70,
|
||||
enrollment_actual: 70,
|
||||
},
|
||||
{
|
||||
center_id: "center-004",
|
||||
center_name: "中心04",
|
||||
institution_initiation_status: "COMPLETED",
|
||||
institution_initiation_completed_at: "2024-12-22",
|
||||
ethics_status: "COMPLETED",
|
||||
ethics_completed_at: "2025-01-15",
|
||||
contract_sign_status: "COMPLETED",
|
||||
contract_sign_completed_at: "2025-01-25",
|
||||
startup_status: "BLOCKED",
|
||||
enrollment_status: "NOT_STARTED",
|
||||
inspection_status: "NOT_STARTED",
|
||||
closeout_status: "NOT_STARTED",
|
||||
enrollment_target: 50,
|
||||
enrollment_actual: 0,
|
||||
},
|
||||
{
|
||||
center_id: "center-005",
|
||||
center_name: "中心05",
|
||||
institution_initiation_status: "IN_PROGRESS",
|
||||
ethics_status: "IN_PROGRESS",
|
||||
contract_sign_status: "NOT_STARTED",
|
||||
startup_status: "NOT_STARTED",
|
||||
enrollment_status: "NOT_STARTED",
|
||||
inspection_status: "NOT_STARTED",
|
||||
closeout_status: "NOT_STARTED",
|
||||
enrollment_target: 40,
|
||||
enrollment_actual: 0,
|
||||
},
|
||||
],
|
||||
enrollment_by_month: [
|
||||
{ month: "2025-01", count: 12 },
|
||||
{ month: "2025-02", count: 18 },
|
||||
{ month: "2025-03", count: 24 },
|
||||
{ month: "2025-04", count: 28 },
|
||||
{ month: "2025-05", count: 32 },
|
||||
{ month: "2025-06", count: 30 },
|
||||
],
|
||||
};
|
||||
@@ -111,8 +111,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingHistory" class="table-empty">{{ TEXT.modules.subjectDetail.emptyHistory }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingHistory && historyItems.length === 0" :description="TEXT.modules.subjectDetail.emptyHistory" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.visits" name="visits">
|
||||
@@ -192,8 +194,10 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingVisits" class="table-empty">{{ TEXT.modules.subjectDetail.emptyVisits }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingVisits && visitItems.length === 0" :description="TEXT.modules.subjectDetail.emptyVisits" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.ae" name="ae">
|
||||
@@ -248,8 +252,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingAes" class="table-empty">{{ TEXT.modules.subjectDetail.emptyAe }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingAes && aeItems.length === 0" :description="TEXT.modules.subjectDetail.emptyAe" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.pd" name="pd">
|
||||
@@ -292,8 +298,10 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div v-if="!loadingPds" class="table-empty">{{ TEXT.modules.subjectDetail.emptyPd }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingPds && pdItems.length === 0" :description="TEXT.modules.subjectDetail.emptyPd" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -427,7 +435,6 @@ import { fetchVisits, createVisit, updateVisit, deleteVisit } from "../../api/vi
|
||||
import { fetchAes, createAe, updateAe, deleteAe } from "../../api/aes";
|
||||
import { listSubjectPds, createSubjectPd, updateSubjectPd, deleteSubjectPd } from "../../api/subjectPds";
|
||||
import { displayDate, displayEnum, displayText } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -1161,4 +1168,14 @@ onMounted(async () => {
|
||||
color: #ffffff;
|
||||
background-color: #8b5cf6;
|
||||
}
|
||||
.table-empty {
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97ab;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user