统一状态流转为前端状态机
This commit is contained in:
@@ -6,18 +6,28 @@
|
||||
<h3>AE(不良事件)</h3>
|
||||
<p>{{ ae.term }}</p>
|
||||
</div>
|
||||
<PermissionAction action="ae.close">
|
||||
<el-button :type="ae.status === 'CLOSED' ? 'primary' : 'danger'" size="small" @click="toggleClose">
|
||||
{{ ae.status === "CLOSED" ? "重新打开" : "关闭" }}
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
<div class="action-buttons">
|
||||
<template v-for="action in availableActions" :key="action.key">
|
||||
<PermissionAction action="ae.close">
|
||||
<el-button
|
||||
size="small"
|
||||
:type="action.danger ? 'danger' : 'primary'"
|
||||
@click="onAction(action)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="受试者ID">{{ ae.subject_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="严重性">{{ ae.seriousness }}</el-descriptions-item>
|
||||
<el-descriptions-item label="严重程度">{{ ae.severity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发生日期">{{ ae.onset_date }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ statusLabel(ae.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="stateColor(aeState)">{{ stateLabel(aeState) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述">{{ ae.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
@@ -52,6 +62,8 @@ import CommentList from "../components/CommentList.vue";
|
||||
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
||||
import PermissionAction from "../components/PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { aeMachine, getAvailableActions, getStateColor, getStateLabel } from "../state-machine";
|
||||
import type { ActionConfig } from "../state-machine";
|
||||
|
||||
const route = useRoute();
|
||||
const study = useStudyStore();
|
||||
@@ -61,7 +73,10 @@ const ae = ref<any | null>(null);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
|
||||
const { can } = usePermission();
|
||||
const canClose = computed(() => can("ae.close"));
|
||||
const aeState = computed(() => (ae.value?.status as string) || "NEW");
|
||||
const availableActions = computed(() => getAvailableActions(aeMachine, aeState.value));
|
||||
const stateLabel = (v?: string | null) => getStateLabel(aeMachine, v);
|
||||
const stateColor = (v?: string | null) => getStateColor(aeMachine, v) || "info";
|
||||
|
||||
const enrichOverdue = (item: any) => {
|
||||
if (!item) return item;
|
||||
@@ -84,15 +99,14 @@ const load = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleClose = async () => {
|
||||
const onAction = async (action: ActionConfig) => {
|
||||
if (!study.currentStudy || !ae.value) return;
|
||||
const targetStatus = ae.value.status === "CLOSED" ? "FOLLOW_UP" : "CLOSED";
|
||||
await ElMessageBox.confirm(
|
||||
`确定将该 AE 状态设置为 ${targetStatus === "CLOSED" ? "已关闭" : "随访中"}?该操作不可撤销,请谨慎确认。`,
|
||||
"提示"
|
||||
);
|
||||
if (action.confirm) {
|
||||
const ok = await ElMessageBox.confirm(action.confirmText || "确认执行该操作?", "提示").catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
const resp = await updateAe(study.currentStudy.id, ae.value.id, { status: targetStatus });
|
||||
const resp = await updateAe(study.currentStudy.id, ae.value.id, { status: action.to });
|
||||
ElMessage.success("状态已更新");
|
||||
ae.value = enrichOverdue(resp.data);
|
||||
await load();
|
||||
@@ -101,13 +115,6 @@ const toggleClose = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (v: string) =>
|
||||
({
|
||||
NEW: "新增",
|
||||
FOLLOW_UP: "随访中",
|
||||
CLOSED: "已关闭",
|
||||
}[v] || v);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!auth.user && auth.token) {
|
||||
await auth.fetchMe().catch(() => {});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>{{ item?.title }}</h3>
|
||||
<el-tag :type="statusTag(item?.status)">{{ statusLabel(item?.status) }}</el-tag>
|
||||
<el-tag :type="stateColor(item?.status)">{{ stateLabel(item?.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="header-actions" v-if="item">
|
||||
<PermissionAction action="finance.create">
|
||||
@@ -53,6 +53,7 @@ import { fetchFinanceItem } from "../api/finance";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { financeMachine, getAvailableActions, getStateColor, getStateLabel } from "../state-machine";
|
||||
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
@@ -62,7 +63,11 @@ const item = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
const showEdit = ref(false);
|
||||
const { can } = usePermission();
|
||||
const canEditDraft = computed(() => item.value?.status === "DRAFT" && can("finance.create"));
|
||||
const canEditDraft = computed(() => {
|
||||
const actions = getAvailableActions(financeMachine, item.value?.status);
|
||||
const submitAllowed = actions.some((a) => a.key === "submit");
|
||||
return submitAllowed && can("finance.create");
|
||||
});
|
||||
|
||||
const loadItem = async () => {
|
||||
if (!study.currentStudy) return;
|
||||
@@ -78,30 +83,8 @@ const loadItem = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const statusTag = (status?: string) => {
|
||||
switch (status) {
|
||||
case "DRAFT":
|
||||
return "info";
|
||||
case "SUBMITTED":
|
||||
return "warning";
|
||||
case "APPROVED":
|
||||
return "success";
|
||||
case "REJECTED":
|
||||
return "danger";
|
||||
case "PAID":
|
||||
return "success";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
const statusLabel = (status?: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
SUBMITTED: "已提交",
|
||||
APPROVED: "已审批",
|
||||
REJECTED: "已驳回",
|
||||
PAID: "已支付",
|
||||
}[status || ""] || status || "");
|
||||
const stateLabel = (v?: string | null) => getStateLabel(financeMachine, v);
|
||||
const stateColor = (v?: string | null) => getStateColor(financeMachine, v) || "info";
|
||||
|
||||
const formatAmount = (num?: number | string) => {
|
||||
const n = Number(num);
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
<el-card class="mb-12">
|
||||
<h3>受试者 {{ subject.subject_no }}</h3>
|
||||
<p>中心:{{ subject.site_id }}</p>
|
||||
<p>状态:{{ subject.status }}</p>
|
||||
<p>
|
||||
状态:
|
||||
<el-tag :type="stateColor(subject.status)">{{ stateLabel(subject.status) }}</el-tag>
|
||||
</p>
|
||||
<div class="actions">
|
||||
<PermissionAction action="subject.enroll">
|
||||
<el-button size="small" type="primary" @click="setStatus('ENROLLED')">标记入组</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction action="subject.complete">
|
||||
<el-button size="small" type="success" @click="setStatus('COMPLETED')">标记完成</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction action="subject.drop">
|
||||
<el-button size="small" type="danger" @click="setStatus('DROPPED')">标记脱落</el-button>
|
||||
</PermissionAction>
|
||||
<template v-for="action in availableActions" :key="action.key">
|
||||
<PermissionAction :action="permissionMap[action.key]">
|
||||
<el-button
|
||||
size="small"
|
||||
:type="action.danger ? 'danger' : 'primary'"
|
||||
@click="onAction(action)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</template>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -35,6 +40,8 @@ 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 type { ActionConfig } from "../state-machine";
|
||||
|
||||
const route = useRoute();
|
||||
const study = useStudyStore();
|
||||
@@ -45,6 +52,14 @@ const visits = ref<any[]>([]);
|
||||
|
||||
const { can } = usePermission();
|
||||
const canVisitEdit = computed(() => can("subject.enroll"));
|
||||
const permissionMap: Record<string, string> = {
|
||||
enroll: "subject.enroll",
|
||||
complete: "subject.complete",
|
||||
drop: "subject.drop",
|
||||
};
|
||||
const availableActions = computed(() => getAvailableActions(subjectMachine, subject.value?.status));
|
||||
const stateLabel = (v?: string | null) => getStateLabel(subjectMachine, v);
|
||||
const stateColor = (v?: string | null) => getStateColor(subjectMachine, v) || "info";
|
||||
|
||||
const loadSubject = async () => {
|
||||
if (!study.currentStudy) return;
|
||||
@@ -68,13 +83,16 @@ const loadVisits = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const setStatus = async (status: string) => {
|
||||
await ElMessageBox.confirm(`确认将受试者状态设置为 ${status}?`, "提示");
|
||||
const onAction = async (action: ActionConfig) => {
|
||||
if (!study.currentStudy || !subject.value) return;
|
||||
const payload: Record<string, any> = { status };
|
||||
if (status == "ENROLLED") payload.enrollment_date = new Date().toISOString().slice(0, 10);
|
||||
if (status == "COMPLETED") payload.completion_date = new Date().toISOString().slice(0, 10);
|
||||
if (status == "DROPPED") payload.drop_reason = "Dropped";
|
||||
if (action.confirm) {
|
||||
const ok = await ElMessageBox.confirm(action.confirmText || "确认执行该操作?", "提示").catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
const payload: Record<string, any> = { status: action.to };
|
||||
if (action.to === "ENROLLED") payload.enrollment_date = new Date().toISOString().slice(0, 10);
|
||||
if (action.to === "COMPLETED") payload.completion_date = new Date().toISOString().slice(0, 10);
|
||||
if (action.to === "DROPPED") payload.drop_reason = "Dropped";
|
||||
try {
|
||||
await updateSubject(study.currentStudy.id, subject.value.id, payload);
|
||||
ElMessage.success("状态已更新");
|
||||
|
||||
Reference in New Issue
Block a user