简单汉化

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
+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="完成度">