-
+
+
+
@@ -33,6 +58,7 @@
import { onMounted, ref, computed } from "vue";
import { useRoute } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
+import { Calendar } from "@element-plus/icons-vue";
import { fetchSubjects, updateSubject } from "../api/subjects";
import { fetchVisits } from "../api/visits";
import { fetchSites } from "../api/sites";
@@ -41,7 +67,7 @@ import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import VisitTable from "../components/VisitTable.vue";
-import { getAvailableActions, getStateColor, getStateLabel, subjectMachine } from "../state-machine";
+import { getAvailableActions, subjectMachine } from "../state-machine";
import type { ActionConfig } from "../state-machine";
import { statusDict, getDictLabel, getDictColor } from "../dictionaries";
import { evaluateAction } from "../guards/actionGuard";
@@ -62,7 +88,16 @@ const permissionMap: Record
= {
complete: "subject.complete",
drop: "subject.drop",
};
-const availableActions = computed(() => getAvailableActions(subjectMachine, subject.value?.status));
+
+// 增加 primary 属性判断以便视觉区分
+const availableActions = computed(() => {
+ const actions = getAvailableActions(subjectMachine, subject.value?.status);
+ return actions.map((a: any) => ({
+ ...a,
+ primary: a.key === 'enroll' || a.key === 'complete'
+ }));
+});
+
const stateLabel = (v?: string | null) => getDictLabel(statusDict, v || "");
const stateColor = (v?: string | null) => getDictColor(statusDict, v || "") || "info";
@@ -71,7 +106,8 @@ const loadSubject = async () => {
try {
const subjectId = route.params.subjectId as string;
const { data } = await fetchSubjects(study.currentStudy.id, { skip: 0, limit: 1000 });
- const list = data.items || data || [];
+ // 兼容處理 data 結構,解決類型檢查問題
+ const list = (data as any).items || (Array.isArray(data) ? data : []);
subject.value = list.find((s: any) => s.id === subjectId) || null;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "受试者加载失败");
@@ -82,7 +118,7 @@ const loadSites = async () => {
if (!study.currentStudy) return;
try {
const { data } = await fetchSites(study.currentStudy.id, { limit: 500 });
- sites.value = (data as any).items || data || [];
+ sites.value = (data as any).items || (Array.isArray(data) ? data : []);
} catch {
sites.value = [];
}
@@ -98,7 +134,7 @@ const loadVisits = async () => {
if (!study.currentStudy || !route.params.subjectId) return;
try {
const { data } = await fetchVisits(study.currentStudy.id, route.params.subjectId as string);
- visits.value = data.items || data;
+ visits.value = (data as any).items || (Array.isArray(data) ? data : []);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "访视加载失败");
}
@@ -168,15 +204,115 @@ onMounted(async () => {
diff --git a/frontend/src/views/Subjects.vue b/frontend/src/views/Subjects.vue
index 8b0debee..0078f397 100644
--- a/frontend/src/views/Subjects.vue
+++ b/frontend/src/views/Subjects.vue
@@ -1,49 +1,97 @@
-
-
-
-
-
-
-
-
-
-
-
新增受试者
+
+
+
+
+
+
+
+ 所属中心
+
+
+
+
+
+ 当前状态
+
+
+
+
+
+
+ 重置筛选
+
-
-
-
-
+
+
+
+
+ {{ scope.row.subject_no }}
+
+
+
{{ siteName(scope.row.site_id) }}
- {{ statusLabel(scope.row.status) }}
+
+ {{ statusLabel(scope.row.status) }}
+
+
+
+ 详细资料
+
+
+
+
+
+
-
+
+
@@ -54,6 +102,7 @@
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
+import { Plus, Document } from "@element-plus/icons-vue";
import { fetchSubjects } from "../api/subjects";
import { fetchSites } from "../api/sites";
import { useStudyStore } from "../store/study";
@@ -116,6 +165,11 @@ const loadSubjects = async () => {
}
};
+const resetFilters = () => {
+ filters.value = { site_id: "", status: "" };
+ loadSubjects();
+};
+
const onPageChange = (p: number) => {
page.value = p;
loadSubjects();
@@ -151,26 +205,102 @@ const statusLabel = (v: string) =>
DROPPED: "已脱落",
}[v] || v);
+const statusTagType = (v: string) => {
+ const map: Record = {
+ SCREENING: "info",
+ ENROLLED: "warning",
+ COMPLETED: "success",
+ DROPPED: "danger",
+ };
+ return map[v] || "info";
+};
+
const siteName = (id: string) => siteMap.value[id] || id || "-";