简单汉化

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>
+23 -1
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");
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="完成度">
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.