简单汉化

This commit is contained in:
Cheng Zhou
2025-12-17 19:23:34 +08:00
parent 1e6cae6b6e
commit 4f50237f56
41 changed files with 2123 additions and 92 deletions
+1722
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"axios": "^1.6.8",
"date-fns": "^3.6.0",
"element-plus": "^2.4.4",
"pinia": "^2.1.7",
"vue": "^3.4.15",
+11 -3
View File
@@ -12,13 +12,13 @@
</el-form-item>
<el-form-item label="严重性" prop="seriousness">
<el-select v-model="form.seriousness" placeholder="请选择">
<el-option label="SERIOUS" value="SERIOUS" />
<el-option label="NON_SERIOUS" value="NON_SERIOUS" />
<el-option label="SERIOUS(重)" value="SERIOUS" />
<el-option label="NON_SERIOUS(非重)" value="NON_SERIOUS" />
</el-select>
</el-form-item>
<el-form-item label="严重程度" prop="severity">
<el-select v-model="form.severity" placeholder="请选择">
<el-option v-for="s in severities" :key="s" :label="s" :value="s" />
<el-option v-for="s in severities" :key="s" :label="severityLabel(s)" :value="s" />
</el-select>
</el-form-item>
<el-form-item label="描述" prop="description">
@@ -57,6 +57,14 @@ const submitting = ref(false);
const study = useStudyStore();
const severities = ["G1", "G2", "G3", "G4", "G5"];
const severityLabel = (v: string) =>
({
G1: "G1(轻)",
G2: "G2(中)",
G3: "G3(重)",
G4: "G4(危重)",
G5: "G5(致死)",
}[v] || v);
const form = reactive({
subject_id: "",
+7 -7
View File
@@ -9,17 +9,17 @@
</el-form-item>
<el-form-item label="类别" prop="category">
<el-select v-model="form.category" placeholder="请选择">
<el-option label="MISSING" value="MISSING" />
<el-option label="INCONSISTENT" value="INCONSISTENT" />
<el-option label="OUT_OF_RANGE" value="OUT_OF_RANGE" />
<el-option label="OTHER" value="OTHER" />
<el-option label="缺失(MISSING)" value="MISSING" />
<el-option label="不一致(INCONSISTENT)" value="INCONSISTENT" />
<el-option label="超范围(OUT_OF_RANGE)" value="OUT_OF_RANGE" />
<el-option label="其他(OTHER)" value="OTHER" />
</el-select>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择">
<el-option label="LOW" value="LOW" />
<el-option label="MEDIUM" value="MEDIUM" />
<el-option label="HIGH" value="HIGH" />
<el-option label="" value="LOW" />
<el-option label="" value="MEDIUM" />
<el-option label="" value="HIGH" />
</el-select>
</el-form-item>
<el-form-item label="指派给" prop="assigned_to">
+8 -1
View File
@@ -3,7 +3,7 @@
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="类别" prop="category">
<el-select v-model="form.category" placeholder="请选择">
<el-option v-for="c in categories" :key="c" :label="c" :value="c" />
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
@@ -51,6 +51,13 @@ const formRef = ref<FormInstance>();
const submitting = ref(false);
const categories = ["SITE_FEE", "SUBJECT_STIPEND", "TRAVEL", "OTHER"];
const categoryLabel = (v: string) =>
({
SITE_FEE: "中心费用",
SUBJECT_STIPEND: "受试者补贴",
TRAVEL: "差旅",
OTHER: "其他",
}[v] || v);
const form = reactive({
category: "SITE_FEE",
@@ -75,7 +75,7 @@ const change = async (status: string, reject_reason?: string) => {
};
const reject = async () => {
const reason = await ElMessageBox.prompt("请输入驳回原因", "驳回", {
const reason = await ElMessageBox.prompt("请输入驳回原因", "驳回审批", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).catch(() => null);
+12 -5
View File
@@ -16,11 +16,13 @@
<el-date-picker v-model="form.manufacture_date" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option label="ACTIVE" value="ACTIVE" />
<el-option label="QUARANTINED" value="QUARANTINED" />
<el-option label="EXPIRED" value="EXPIRED" />
<el-option label="CLOSED" value="CLOSED" />
<el-select v-model="form.status" placeholder="请选择状态">
<el-option
v-for="s in statusOptions"
:key="s"
:label="`${statusLabel(s)}(${s})`"
:value="s"
/>
</el-select>
</el-form-item>
</el-form>
@@ -44,6 +46,8 @@ const study = useStudyStore();
const formRef = ref<FormInstance>();
const submitting = ref(false);
const statusOptions = ["ACTIVE", "QUARANTINED", "EXPIRED", "CLOSED"];
const form = reactive({
product_id: "",
batch_no: "",
@@ -115,4 +119,7 @@ const onSubmit = async () => {
submitting.value = false;
}
};
const statusLabel = (v: string) =>
({ ACTIVE: "在库", QUARANTINED: "隔离", EXPIRED: "过期", CLOSED: "关闭" }[v] || v);
</script>
+20 -5
View File
@@ -10,8 +10,13 @@
</el-select>
</el-form-item>
<el-form-item label="交易类型" prop="tx_type">
<el-select v-model="form.tx_type">
<el-option v-for="t in txTypes" :key="t" :label="t" :value="t" />
<el-select v-model="form.tx_type" placeholder="请选择类型">
<el-option
v-for="t in txTypes"
:key="t"
:label="`${txTypeLabel(t)}(${t})`"
:value="t"
/>
</el-select>
</el-form-item>
<el-form-item label="数量" prop="quantity">
@@ -21,7 +26,7 @@
<SubjectSelect
v-model="form.subject_id"
:study-id="study.currentStudy?.id || null"
placeholder="DISPENSE 时必填"
placeholder="发放时必填"
/>
</el-form-item>
<el-form-item label="日期" prop="tx_date">
@@ -70,7 +75,7 @@ const form = reactive({
});
const rules: FormRules = {
site_id: [{ required: true, message: "请输入中心ID", trigger: "blur" }],
site_id: [{ required: true, message: "请选择中心", trigger: "change" }],
batch_id: [{ required: true, message: "请选择批次", trigger: "change" }],
tx_type: [{ required: true, message: "请选择类型", trigger: "change" }],
quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
@@ -92,7 +97,7 @@ const onSubmit = async () => {
if (!study.currentStudy || !formRef.value) return;
await formRef.value.validate();
if (form.tx_type === "DISPENSE" && !form.subject_id) {
ElMessage.error("DISPENSE 时受试者必填");
ElMessage.error("发放时受试者必填");
return;
}
submitting.value = true;
@@ -112,4 +117,14 @@ const onSubmit = async () => {
submitting.value = false;
}
};
const txTypeLabel = (t: string) =>
({
RECEIPT: "入库",
DISPENSE: "发放",
RETURN: "回收",
DESTROY: "销毁",
TRANSFER_IN: "调入",
TRANSFER_OUT: "调出",
}[t] || t);
</script>
+17 -2
View File
@@ -9,12 +9,12 @@
</el-form-item>
<el-form-item label="类别" prop="category">
<el-select v-model="form.category" placeholder="请选择">
<el-option v-for="c in categories" :key="c" :label="c" :value="c" />
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="等级" prop="level">
<el-select v-model="form.level" placeholder="请选择">
<el-option v-for="l in levels" :key="l" :label="l" :value="l" />
<el-option v-for="l in levels" :key="l" :label="levelLabel(l)" :value="l" />
</el-select>
</el-form-item>
<el-form-item label="截止日期" prop="due_date">
@@ -132,4 +132,19 @@ const onSubmit = async () => {
}
});
};
const categoryLabel = (c: string) =>
({
RISK: "风险",
ISSUE: "问题",
PROTOCOL_DEVIATION: "方案偏差",
}[c] || c);
const levelLabel = (l: string) =>
({
LOW: "低",
MEDIUM: "中",
HIGH: "高",
CRITICAL: "关键",
}[l] || l);
</script>
+6 -6
View File
@@ -16,14 +16,14 @@
<el-aside width="200px" class="aside" v-if="study.currentStudy">
<el-menu router :default-active="$route.path">
<el-menu-item index="/home">首页</el-menu-item>
<el-menu-item index="/study/home">项目首页</el-menu-item>
<el-menu-item index="/study/milestones">里程碑</el-menu-item>
<el-menu-item index="/study/home">项目概览</el-menu-item>
<el-menu-item index="/study/milestones">伦理与启动管理</el-menu-item>
<el-menu-item index="/study/tasks">任务</el-menu-item>
<el-menu-item index="/study/subjects">受试者</el-menu-item>
<el-menu-item index="/study/aes">AE</el-menu-item>
<el-menu-item index="/study/issues">风险/问题</el-menu-item>
<el-menu-item index="/study/aes">不良事件</el-menu-item>
<el-menu-item index="/study/issues">风险问题</el-menu-item>
<el-menu-item index="/study/data-queries">数据问题</el-menu-item>
<el-menu-item index="/study/verifications">SDV/SDR</el-menu-item>
<el-menu-item index="/study/verifications">数据核查</el-menu-item>
<el-sub-menu index="imp">
<template #title>药品</template>
<el-menu-item index="/study/imp/inventory">库存</el-menu-item>
@@ -32,7 +32,7 @@
<el-menu-item index="/study/imp/transactions">交易台账</el-menu-item>
</el-sub-menu>
<el-menu-item index="/study/finance">费用</el-menu-item>
<el-menu-item index="/study/faq">FAQ</el-menu-item>
<el-menu-item index="/study/faq">知识库</el-menu-item>
</el-menu>
</el-aside>
<el-main>
+24 -2
View File
@@ -14,6 +14,7 @@
</el-form-item>
</el-form>
<template #footer>
<el-checkbox v-model="keepCreating" class="mr-8">提交后继续新建</el-checkbox>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
</template>
@@ -22,6 +23,7 @@
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { format } from "date-fns";
import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { createSubject } from "../api/subjects";
@@ -49,6 +51,7 @@ const form = reactive({
subject_no: "",
screening_date: "",
});
const keepCreating = ref(false);
const rules: FormRules = {
site_id: [{ required: true, message: "请选择中心", trigger: "change" }],
@@ -59,6 +62,15 @@ const onClose = () => {
visible.value = false;
};
const resetForm = () => {
const studyId = study.currentStudy?.id;
const recentSite = studyId ? localStorage.getItem(`recent_site_${studyId}`) : null;
const recentSubject = studyId ? localStorage.getItem(`recent_subject_${studyId}`) : null;
form.site_id = recentSite || "";
form.subject_no = recentSubject || "";
form.screening_date = format(new Date(), "yyyy-MM-dd");
};
const onSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
@@ -69,10 +81,18 @@ const onSubmit = async () => {
}
submitting.value = true;
try {
await createSubject(study.currentStudy.id, { ...form });
const { data } = await createSubject(study.currentStudy.id, { ...form });
ElMessage.success("提交成功");
if (study.currentStudy?.id) {
localStorage.setItem(`recent_site_${study.currentStudy.id}`, form.site_id || "");
if (data?.id) localStorage.setItem(`recent_subject_${study.currentStudy.id}`, data.id);
}
emit("success");
onClose();
if (keepCreating.value) {
resetForm();
} else {
onClose();
}
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "提交失败");
} finally {
@@ -80,4 +100,6 @@ const onSubmit = async () => {
}
});
};
resetForm();
</script>
+10 -2
View File
@@ -17,7 +17,7 @@
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择">
<el-option v-for="p in priorities" :key="p" :label="p" :value="p" />
<el-option v-for="p in priorities" :key="p" :label="priorityLabel(p)" :value="p" />
</el-select>
</el-form-item>
<el-form-item label="截止日期" prop="due_date">
@@ -25,7 +25,7 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
</el-form-item>
</el-form>
@@ -62,6 +62,14 @@ const visible = computed({
const priorities = ["LOW", "MEDIUM", "HIGH"];
const statuses = ["TODO", "DOING", "DONE", "BLOCKED"];
const priorityLabel = (v: string) => ({ LOW: "低", MEDIUM: "中", HIGH: "高" }[v] || v);
const statusLabel = (v: string) =>
({
TODO: "待处理",
DOING: "进行中",
DONE: "已完成",
BLOCKED: "阻塞",
}[v] || v);
const form = reactive({
title: "",
+10 -3
View File
@@ -3,7 +3,7 @@
<el-card class="mb-12">
<div class="header">
<div>
<h3>AE 详情</h3>
<h3>AE不良事件</h3>
<p>{{ ae.term }}</p>
</div>
<PermissionAction action="ae.close">
@@ -17,7 +17,7 @@
<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="状态">{{ ae.status }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusLabel(ae.status) }}</el-descriptions-item>
<el-descriptions-item label="描述">{{ ae.description }}</el-descriptions-item>
</el-descriptions>
</el-card>
@@ -88,7 +88,7 @@ const toggleClose = async () => {
if (!study.currentStudy || !ae.value) return;
const targetStatus = ae.value.status === "CLOSED" ? "FOLLOW_UP" : "CLOSED";
await ElMessageBox.confirm(
`将该 AE 状态设置为 ${targetStatus === "CLOSED" ? "CLOSED" : "FOLLOW_UP"}`,
`将该 AE 状态设置为 ${targetStatus === "CLOSED" ? "已关闭" : "随访中"}?该操作不可撤销,请谨慎确认。`,
"提示"
);
try {
@@ -101,6 +101,13 @@ const toggleClose = async () => {
}
};
const statusLabel = (v: string) =>
({
NEW: "新增",
FOLLOW_UP: "随访中",
CLOSED: "已关闭",
}[v] || v);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+23 -6
View File
@@ -3,15 +3,14 @@
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadAes">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<el-select v-model="filters.seriousness" placeholder="严重性" clearable @change="loadAes">
<el-option label="SERIOUS" value="SERIOUS" />
<el-option label="NON_SERIOUS" value="NON_SERIOUS" />
<el-option v-for="s in seriousnessOptions" :key="s" :label="seriousnessLabel(s)" :value="s" />
</el-select>
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadAes" />
<div class="spacer" />
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建 AE</el-button>
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建不良事件</el-button>
</div>
</el-card>
@@ -23,12 +22,16 @@
{{ subjectMap[scope.row.subject_id] || scope.row.subject_id || "-" }}
</template>
</el-table-column>
<el-table-column prop="seriousness" label="严重性" width="140" />
<el-table-column prop="seriousness" label="严重性" width="140">
<template #default="scope">
{{ seriousnessLabel(scope.row.seriousness) }}
</template>
</el-table-column>
<el-table-column prop="onset_date" label="发生日期" width="140" />
<el-table-column prop="report_due_date" label="报告截止" width="140" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ scope.row.status }}</el-tag>
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
</el-table>
@@ -74,6 +77,7 @@ const subjectMap = computed(() =>
);
const statuses = ["NEW", "FOLLOW_UP", "CLOSED"];
const seriousnessOptions = ["SERIOUS", "NON_SERIOUS"];
const filters = ref({
status: "",
@@ -138,6 +142,19 @@ const goDetail = (row: any) => {
router.push(`/study/aes/${row.id}`);
};
const statusLabel = (v: string) =>
({
NEW: "新增",
FOLLOW_UP: "随访中",
CLOSED: "已关闭",
}[v] || v);
const seriousnessLabel = (v: string) =>
({
SERIOUS: "严重",
NON_SERIOUS: "非严重",
}[v] || v);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+39 -6
View File
@@ -3,12 +3,12 @@
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadQueries">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadQueries" />
<el-input v-model="filters.assigned_to" placeholder="指派人ID" style="width: 180px" @change="loadQueries" />
<el-input v-model="filters.assigned_to" placeholder="负责人" style="width: 180px" @change="loadQueries" />
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="showForm = true">新建 Query</el-button>
<el-button type="primary" v-if="canEdit" @click="showForm = true">新建数据问题</el-button>
</div>
</el-card>
@@ -20,12 +20,20 @@
{{ subjectMap[scope.row.subject_id] || scope.row.subject_id || "-" }}
</template>
</el-table-column>
<el-table-column prop="category" label="类别" width="140" />
<el-table-column prop="priority" label="优先级" width="120" />
<el-table-column prop="category" label="类别" width="140">
<template #default="scope">
{{ categoryLabel(scope.row.category) }}
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="120">
<template #default="scope">
{{ priorityLabel(scope.row.priority) }}
</template>
</el-table-column>
<el-table-column prop="due_date" label="截止日期" width="140" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ scope.row.status }}</el-tag>
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
</el-table>
@@ -71,6 +79,8 @@ const subjectMap = computed(() =>
);
const statuses = ["OPEN", "IN_PROGRESS", "ANSWERED", "CLOSED"];
const categories = ["MISSING", "INCONSISTENT", "OUT_OF_RANGE", "OTHER"];
const priorities = ["LOW", "MEDIUM", "HIGH"];
const filters = ref({
status: "",
@@ -124,6 +134,29 @@ const goDetail = (row: any) => {
router.push(`/study/data-queries/${row.id}`);
};
const statusLabel = (v: string) =>
({
OPEN: "待处理",
IN_PROGRESS: "处理中",
ANSWERED: "已回复",
CLOSED: "已关闭",
}[v] || v);
const categoryLabel = (v: string) =>
({
MISSING: "缺失",
INCONSISTENT: "不一致",
OUT_OF_RANGE: "超范围",
OTHER: "其他",
}[v] || v);
const priorityLabel = (v: string) =>
({
LOW: "低",
MEDIUM: "中",
HIGH: "高",
}[v] || v);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+24 -4
View File
@@ -5,10 +5,10 @@
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadList">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<el-select v-model="filters.category" placeholder="类别" clearable @change="loadList">
<el-option v-for="c in categories" :key="c" :label="c" :value="c" />
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
</el-select>
<el-date-picker
v-model="filters.date_from"
@@ -34,7 +34,11 @@
<el-card>
<el-table :data="items" v-loading="loading" @row-click="goDetail" style="width: 100%">
<el-table-column prop="title" label="标题" min-width="160" />
<el-table-column prop="category" label="类别" width="140" />
<el-table-column prop="category" label="类别" width="140">
<template #default="scope">
{{ categoryLabel(scope.row.category) }}
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="140" align="right">
<template #default="scope">
{{ formatAmount(scope.row.amount) }}
@@ -43,7 +47,7 @@
<el-table-column prop="occur_date" label="发生日期" width="140" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="statusTag(scope.row.status)">{{ scope.row.status }}</el-tag>
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="submitted_at" label="提交时间" width="160" />
@@ -123,6 +127,22 @@ const statusTag = (status?: string) => {
return "info";
}
};
const statusLabel = (status?: string) =>
({
DRAFT: "草稿",
SUBMITTED: "已提交",
APPROVED: "已审批",
REJECTED: "已驳回",
PAID: "已支付",
}[status || ""] || status || "");
const categoryLabel = (c?: string) =>
({
SITE_FEE: "中心费用",
SUBJECT_STIPEND: "受试者补偿",
TRAVEL: "差旅",
OTHER: "其他",
}[c || ""] || c || "");
const loadSummary = async () => {
if (!study.currentStudy) return;
+9 -1
View File
@@ -4,7 +4,7 @@
<div class="header">
<div>
<h3>{{ item?.title }}</h3>
<el-tag :type="statusTag(item?.status)">{{ item?.status }}</el-tag>
<el-tag :type="statusTag(item?.status)">{{ statusLabel(item?.status) }}</el-tag>
</div>
<div class="header-actions" v-if="item">
<PermissionAction action="finance.create">
@@ -94,6 +94,14 @@ const statusTag = (status?: string) => {
return "info";
}
};
const statusLabel = (status?: string) =>
({
DRAFT: "草稿",
SUBMITTED: "已提交",
APPROVED: "已审批",
REJECTED: "已驳回",
PAID: "已支付",
}[status || ""] || status || "");
const formatAmount = (num?: number | string) => {
const n = Number(num);
+11 -3
View File
@@ -6,10 +6,10 @@
<el-option v-for="p in products" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
<el-select v-model="filters.status" placeholder="状态" clearable @change="load">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="openForm()">批次</el-button>
<el-button type="primary" v-if="canEdit" @click="openForm()">批次</el-button>
</div>
</el-card>
<el-card>
@@ -23,7 +23,7 @@
<el-table-column prop="expiry_date" label="失效日期" width="140" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag>{{ scope.row.status }}</el-tag>
<el-tag>{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" v-if="canEdit">
@@ -129,6 +129,14 @@ onMounted(async () => {
await loadProducts();
load();
});
const statusLabel = (v: string) =>
({
ACTIVE: "在库",
QUARANTINED: "隔离",
EXPIRED: "过期",
CLOSED: "关闭",
}[v] || v);
</script>
<style scoped>
+1 -1
View File
@@ -14,7 +14,7 @@
<el-table :data="rows" v-loading="loading" style="width: 100%">
<el-table-column prop="site_id" label="中心" />
<el-table-column prop="batch_id" label="批次" />
<el-table-column prop="quantity_on_hand" label="结余" width="120" align="right" />
<el-table-column prop="quantity_on_hand" label="当前结余" width="120" align="right" />
<el-table-column prop="updated_at" label="更新时间" width="180" />
</el-table>
</el-card>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="page">
<div class="toolbar">
<el-button type="primary" v-if="canEdit" @click="openForm()">新建产品</el-button>
<el-button type="primary" v-if="canEdit" @click="openForm()">新建 IMP 产品</el-button>
</div>
<el-card>
<el-table :data="products" v-loading="loading" style="width: 100%">
+18 -4
View File
@@ -4,8 +4,8 @@
<div class="filters">
<el-input v-model="filters.site_id" placeholder="中心ID" clearable @change="load" style="width: 160px" />
<el-input v-model="filters.batch_id" placeholder="批次ID" clearable @change="load" style="width: 160px" />
<el-select v-model="filters.tx_type" placeholder="类型" clearable @change="load" style="width: 160px">
<el-option v-for="t in txTypes" :key="t" :label="t" :value="t" />
<el-select v-model="filters.tx_type" placeholder="交易类型" clearable @change="load" style="width: 160px">
<el-option v-for="t in txTypes" :key="t" :label="txTypeLabel(t)" :value="t" />
</el-select>
<el-date-picker
v-model="filters.date_from"
@@ -23,14 +23,18 @@
/>
<div class="spacer" />
<PermissionAction action="imp.transaction">
<el-button type="primary" @click="showForm = true">交易</el-button>
<el-button type="primary" @click="showForm = true">交易</el-button>
</PermissionAction>
</div>
</el-card>
<el-card>
<el-table :data="txs" v-loading="loading" style="width: 100%">
<el-table-column prop="tx_date" label="日期" width="120" />
<el-table-column prop="tx_type" label="类型" width="140" />
<el-table-column prop="tx_type" label="类型" width="140">
<template #default="scope">
{{ txTypeLabel(scope.row.tx_type) }}
</template>
</el-table-column>
<el-table-column prop="site_id" label="中心" width="180" />
<el-table-column prop="batch_id" label="批次" width="180" />
<el-table-column prop="subject_id" label="受试者" width="180" />
@@ -115,6 +119,16 @@ const onPageChange = (p: number) => {
load();
};
const txTypeLabel = (t: string) =>
({
RECEIPT: "入库",
DISPENSE: "发放",
RETURN: "回收",
DESTROY: "销毁",
TRANSFER_IN: "调入",
TRANSFER_OUT: "调出",
}[t] || t);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+10 -3
View File
@@ -3,7 +3,7 @@
<el-card class="mb-12">
<div class="header">
<div>
<h3>Issue 详情</h3>
<h3>风险与问题</h3>
<p>{{ issue.title }}</p>
</div>
<PermissionAction action="issue.close">
@@ -14,7 +14,7 @@
<el-descriptions-item label="类别">{{ issue.category }}</el-descriptions-item>
<el-descriptions-item label="等级">{{ issue.level }}</el-descriptions-item>
<el-descriptions-item label="截止日期">{{ issue.due_date }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ issue.status }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusLabel(issue.status) }}</el-descriptions-item>
<el-descriptions-item label="描述">{{ issue.description }}</el-descriptions-item>
</el-descriptions>
</el-card>
@@ -70,7 +70,7 @@ const load = async () => {
};
const closeIssue = async () => {
await ElMessageBox.confirm("确认将 Issue 关闭?", "提示");
await ElMessageBox.confirm("确定要关闭该风险/问题吗?该操作不可撤销,请谨慎确认。", "提示");
if (!study.currentStudy || !issue.value) return;
try {
await updateIssue(study.currentStudy.id, issue.value.id, { status: "CLOSED" });
@@ -81,6 +81,13 @@ const closeIssue = async () => {
}
};
const statusLabel = (v: string) =>
({
OPEN: "开放",
MITIGATING: "处理中",
CLOSED: "已关闭",
}[v] || v);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+37 -7
View File
@@ -3,29 +3,37 @@
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadIssues">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<el-select v-model="filters.level" placeholder="等级" clearable @change="loadIssues">
<el-option v-for="l in levels" :key="l" :label="l" :value="l" />
<el-option v-for="l in levels" :key="l" :label="levelLabel(l)" :value="l" />
</el-select>
<el-select v-model="filters.category" placeholder="类别" clearable @change="loadIssues">
<el-option v-for="c in categories" :key="c" :label="c" :value="c" />
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
</el-select>
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadIssues" />
<div class="spacer" />
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建 Issue</el-button>
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建风险/问题</el-button>
</div>
</el-card>
<el-card>
<el-table :data="issues" v-loading="loading" style="width: 100%" @row-click="goDetail">
<el-table-column prop="title" label="标题" />
<el-table-column prop="level" label="等级" width="120" />
<el-table-column prop="category" label="类别" width="140" />
<el-table-column prop="level" label="等级" width="120">
<template #default="scope">
{{ levelLabel(scope.row.level) }}
</template>
</el-table-column>
<el-table-column prop="category" label="类别" width="140">
<template #default="scope">
{{ categoryLabel(scope.row.category) }}
</template>
</el-table-column>
<el-table-column prop="due_date" label="截止日期" width="140" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ scope.row.status }}</el-tag>
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
</el-table>
@@ -120,6 +128,28 @@ const goDetail = (row: any) => {
router.push(`/study/issues/${row.id}`);
};
const statusLabel = (v: string) =>
({
OPEN: "开放",
MITIGATING: "处理中",
CLOSED: "已关闭",
}[v] || v);
const levelLabel = (v: string) =>
({
LOW: "低",
MEDIUM: "中",
HIGH: "高",
CRITICAL: "关键",
}[v] || v);
const categoryLabel = (v: string) =>
({
RISK: "风险",
ISSUE: "问题",
PROTOCOL_DEVIATION: "方案偏差",
}[v] || v);
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
+31 -4
View File
@@ -2,9 +2,9 @@
<div class="page">
<el-card class="mb-12">
<div class="actions">
<h3>里程碑</h3>
<h3>伦理与启动管理</h3>
<PermissionAction action="milestone.create">
<el-button type="primary" @click="openCreate">增里程碑</el-button>
<el-button type="primary" @click="openCreate">建节点</el-button>
</PermissionAction>
</div>
</el-card>
@@ -12,10 +12,18 @@
<el-card>
<el-table :data="milestones" v-loading="loading" style="width: 100%">
<el-table-column prop="name" label="名称" />
<el-table-column prop="type" label="类型" width="160" />
<el-table-column label="类型" width="160">
<template #default="scope">
{{ typeLabel(scope.row.type) }}
</template>
</el-table-column>
<el-table-column prop="planned_date" label="计划日期" width="140" />
<el-table-column prop="actual_date" label="实际日期" width="140" />
<el-table-column prop="status" label="状态" width="140" />
<el-table-column label="状态" width="140">
<template #default="scope">
{{ statusLabel(scope.row.status) }}
</template>
</el-table-column>
<el-table-column prop="owner_id" label="负责人" width="160" />
<el-table-column label="操作" width="120">
<template #default="scope">
@@ -74,6 +82,25 @@ const openEdit = (row: any) => {
onMounted(() => {
loadMilestones();
});
const typeLabel = (v: string) =>
({
ETHICS_SUBMISSION: "伦理递交",
ETHICS_APPROVAL: "伦理批件",
SIV: "启动会",
FPI: "首例入组",
LPI: "末例入组",
DBL: "揭盲",
CSR: "总结报告",
}[v] || v);
const statusLabel = (v: string) =>
({
NOT_STARTED: "未开始",
IN_PROGRESS: "进行中",
DONE: "已完成",
BLOCKED: "阻塞",
}[v] || v);
</script>
<style scoped>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="study-home" v-if="study.currentStudy">
<div class="header">
<div>
<h2>项目首页 / Dashboard</h2>
<h2>项目概览</h2>
<p class="subtitle">
{{ study.currentStudy.name }}{{ study.currentStudy.code }}
</p>
+14 -2
View File
@@ -13,7 +13,7 @@
<el-option v-for="s in sites" :key="s.id" :label="s.name || s.id" :value="s.id" />
</el-select>
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadSubjects">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
</el-select>
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="showForm = true">新增受试者</el-button>
@@ -24,7 +24,11 @@
<el-table :data="subjects" v-loading="loading" style="width: 100%" @row-click="goDetail">
<el-table-column prop="subject_no" label="受试者编号" />
<el-table-column prop="site_id" label="中心ID" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag>{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="screening_date" label="筛选日期" width="140" />
<el-table-column prop="enrollment_date" label="入组日期" width="140" />
</el-table>
@@ -128,6 +132,14 @@ onMounted(async () => {
await loadSites();
loadSubjects();
});
const statusLabel = (v: string) =>
({
SCREENING: "筛选中",
ENROLLED: "已入组",
COMPLETED: "已完成",
DROPPED: "已脱落",
}[v] || v);
</script>
<style scoped>
+48 -5
View File
@@ -8,10 +8,10 @@
<el-select v-model="filters.milestone_id" placeholder="里程碑" clearable @change="loadTasks">
<el-option v-for="m in milestones" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
<el-input v-model="filters.assignee_id" placeholder="负责人ID" style="width: 180px" @change="loadTasks" />
<el-input v-model="filters.assignee_id" placeholder="负责人" style="width: 180px" @change="loadTasks" />
<div class="spacer" />
<PermissionAction action="task.create">
<el-button type="primary" @click="openCreate">任务</el-button>
<el-button type="primary" @click="openCreate">任务</el-button>
</PermissionAction>
</div>
</el-card>
@@ -19,10 +19,20 @@
<el-card>
<el-table :data="tasks" v-loading="loading" style="width: 100%">
<el-table-column prop="title" label="标题" />
<el-table-column prop="milestone_id" label="里程碑" />
<el-table-column prop="milestone_id" label="节点/里程碑">
<template #default="scope">
{{ milestoneName(scope.row.milestone_id) }}
</template>
</el-table-column>
<el-table-column prop="assignee_id" label="负责人" width="160" />
<el-table-column prop="priority" label="优先级" width="100" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 'DONE' ? 'success' : scope.row.status === 'BLOCKED' ? 'danger' : 'info'">
{{ statusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="due_date" label="截止日期" width="140" />
<el-table-column label="操作" width="120">
<template #default="scope">
@@ -47,7 +57,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { onMounted, ref, computed } from "vue";
import { ElMessage } from "element-plus";
import { fetchTasks } from "../api/tasks";
import { fetchMilestones } from "../api/milestones";
@@ -79,6 +89,12 @@ const showForm = ref(false);
const editingTask = ref<any | null>(null);
const { can } = usePermission();
const milestoneMap = computed(() =>
milestones.value.reduce<Record<string, string>>((acc, cur) => {
acc[cur.id] = cur.name || cur.id;
return acc;
}, {})
);
const loadMilestones = async () => {
if (!study.currentStudy) return;
@@ -142,8 +158,35 @@ const ensureUser = async () => {
}
};
const filterKey = computed(() => (study.currentStudy ? `tasks_filter_${study.currentStudy.id}` : null));
const saveFilters = () => {
if (!filterKey.value) return;
localStorage.setItem(filterKey.value, JSON.stringify({ ...filters.value, page: page.value }));
};
const loadSavedFilters = () => {
if (!filterKey.value) return;
const raw = localStorage.getItem(filterKey.value);
if (raw) {
try {
const parsed = JSON.parse(raw);
filters.value.status = parsed.status || "";
filters.value.milestone_id = parsed.milestone_id || "";
filters.value.assignee_id = parsed.assignee_id || "";
page.value = parsed.page || 1;
} catch {
/* ignore */
}
}
};
const statusLabel = (v: string) =>
({ TODO: "待处理", DOING: "进行中", DONE: "已完成", BLOCKED: "阻塞" }[v] || v);
const milestoneName = (id: string) => milestoneMap.value[id] || id || "-";
onMounted(async () => {
await ensureUser();
loadSavedFilters();
loadMilestones();
loadTasks();
});
+7 -7
View File
@@ -2,9 +2,9 @@
<div class="page">
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.level" placeholder="类型" clearable @change="load">
<el-option label="SDV" value="SDV" />
<el-option label="SDR" value="SDR" />
<el-select v-model="filters.level" placeholder="核查层级" clearable @change="load">
<el-option label="SDV 核查" value="SDV" />
<el-option label="SDR 核查" value="SDR" />
</el-select>
<SiteSelect
v-model="filters.site_id"
@@ -14,13 +14,13 @@
@change="load"
/>
<div class="spacer" />
<el-button v-if="canEdit" type="primary" @click="openCreate">新增进度</el-button>
<el-button v-if="canEdit" type="primary" @click="openCreate">新增核查进度</el-button>
</div>
</el-card>
<VerificationTable :verifications="verifications" :can-edit="canEdit" :loading="loading" @edit="openEdit" />
<el-dialog :title="editForm.id ? '更新进度' : '新增进度'" v-model="showEdit" width="520px">
<el-dialog :title="editForm.id ? '更新核查进度' : '新增核查进度'" v-model="showEdit" width="520px">
<el-form :model="editForm" label-width="120px">
<el-form-item label="受试者">
<SubjectSelect
@@ -34,8 +34,8 @@
</el-form-item>
<el-form-item label="类型">
<el-select v-model="editForm.level" placeholder="请选择">
<el-option label="SDV" value="SDV" />
<el-option label="SDR" value="SDR" />
<el-option label="SDV 核查" value="SDV" />
<el-option label="SDR 核查" value="SDR" />
</el-select>
</el-form-item>
<el-form-item label="完成度">