数据字典集中治理

This commit is contained in:
Cheng Zhou
2025-12-17 20:58:42 +08:00
parent c2328594b0
commit b0bffbc75e
16 changed files with 170 additions and 39 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CTMS</title>
<script type="module" crossorigin src="/assets/index-BA1yIlA_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DMvYZYMw.css">
<script type="module" crossorigin src="/assets/index-DWloMSfp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D1uShwvc.css">
</head>
<body>
<div id="app"></div>
+16
View File
@@ -0,0 +1,16 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
const seriousnessItems = [
{ value: "NON_SERIOUS", label: "非严重", order: 1 },
{ value: "SERIOUS", label: "严重", order: 2, color: "danger" },
];
const severityItems = [
{ value: "MILD", label: "轻度", order: 1 },
{ value: "MODERATE", label: "中度", order: 2 },
{ value: "SEVERE", label: "重度", order: 3, color: "danger" },
];
export const aeSeriousnessDict: Dict = createDict(seriousnessItems);
export const aeSeverityDict: Dict = createDict(severityItems);
+13
View File
@@ -0,0 +1,13 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
import { statusDict } from "./status.dict";
const categoryItems = [
{ value: "SITE_FEE", label: "中心费用", order: 1 },
{ value: "SUBJECT_FEE", label: "受试者费用", order: 2 },
{ value: "TRAVEL_FEE", label: "差旅费用", order: 3 },
{ value: "OTHER", label: "其他", order: 99 },
];
export const financeCategoryDict: Dict = createDict(categoryItems);
export const financeStatusDict = statusDict;
+12
View File
@@ -0,0 +1,12 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
const transactionItems = [
{ value: "RECEIPT", label: "接收", order: 1 },
{ value: "DISPENSE", label: "发药", order: 2 },
{ value: "RETURN", label: "回收", order: 3 },
{ value: "DESTROY", label: "销毁", order: 4 },
{ value: "TRANSFER", label: "转移", order: 5 },
];
export const impTransactionTypeDict: Dict = createDict(transactionItems);
+8
View File
@@ -0,0 +1,8 @@
export * from "./types";
export * from "./utils";
export * from "./status.dict";
export * from "./priority.dict";
export * from "./role.dict";
export * from "./ae.dict";
export * from "./finance.dict";
export * from "./imp.dict";
@@ -0,0 +1,10 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
const items = [
{ value: "LOW", label: "低", color: "info", order: 1 },
{ value: "MEDIUM", label: "中", color: "warning", order: 2 },
{ value: "HIGH", label: "高", color: "danger", order: 3 },
];
export const priorityDict: Dict = createDict(items);
+12
View File
@@ -0,0 +1,12 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
const items = [
{ value: "ADMIN", label: "管理员", order: 1 },
{ value: "PM", label: "项目负责人", order: 2 },
{ value: "CRA", label: "CRA", order: 3 },
{ value: "PV", label: "PV", order: 4 },
{ value: "IMP", label: "药品管理员", order: 5 },
];
export const roleDict: Dict = createDict(items);
+24
View File
@@ -0,0 +1,24 @@
import type { Dict } from "./types";
import { createDict } from "./utils";
const items = [
{ value: "TODO", label: "待处理", color: "info", order: 1 },
{ value: "DOING", label: "进行中", color: "warning", order: 2 },
{ value: "DONE", label: "已完成", color: "success", order: 3 },
{ value: "BLOCKED", label: "阻塞", color: "danger", order: 4 },
{ value: "DRAFT", label: "草稿", color: "info", order: 10 },
{ value: "SUBMITTED", label: "已提交", color: "warning", order: 11 },
{ value: "APPROVED", label: "已审批", color: "success", order: 12 },
{ value: "REJECTED", label: "已驳回", color: "danger", order: 13 },
{ value: "PAID", label: "已支付", color: "success", order: 14 },
{ value: "CLOSED", label: "已关闭", color: "success", order: 20 },
{ value: "OPEN", label: "开放", color: "info", order: 19 },
{ value: "FOLLOW_UP", label: "随访中", color: "warning", order: 18 },
{ value: "OVERDUE", label: "已逾期", color: "danger", order: 30 },
{ value: "SCREENING", label: "筛选中", color: "info", order: 40 },
{ value: "ENROLLED", label: "已入组", color: "success", order: 41 },
{ value: "COMPLETED", label: "已完成", color: "success", order: 42 },
{ value: "DROPPED", label: "已脱落", color: "danger", order: 43 },
];
export const statusDict: Dict = createDict(items);
+13
View File
@@ -0,0 +1,13 @@
export interface DictItem {
value: string;
label: string;
color?: string;
order?: number;
description?: string;
}
export interface Dict {
items: DictItem[];
get: (value?: string | null) => DictItem | undefined;
list: () => DictItem[];
}
+15
View File
@@ -0,0 +1,15 @@
import type { Dict, DictItem } from "./types";
export const createDict = (items: DictItem[]): Dict => {
const normalized = items.map((item, idx) => ({ order: idx, ...item }));
const get = (value?: string | null) => normalized.find((i) => i.value === value);
const list = () =>
[...normalized].sort((a, b) => (a.order || 0) - (b.order || 0));
return { items: normalized, get, list };
};
export const getDictItem = (dict: Dict, value?: string | null) => dict.get(value);
export const getDictLabel = (dict: Dict, value?: string | null) => dict.get(value)?.label || value || "";
export const getDictColor = (dict: Dict, value?: string | null) => dict.get(value)?.color;
export const getDictOptions = (dict: Dict) =>
dict.list().map((i) => ({ label: i.label, value: i.value, disabled: false }));
+4 -3
View File
@@ -62,8 +62,9 @@ import CommentList from "../components/CommentList.vue";
import AttachmentList from "../components/attachments/AttachmentList.vue";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import { aeMachine, getAvailableActions, getStateColor, getStateLabel } from "../state-machine";
import { aeMachine, getAvailableActions } from "../state-machine";
import type { ActionConfig } from "../state-machine";
import { statusDict, getDictColor, getDictLabel } from "../dictionaries";
const route = useRoute();
const study = useStudyStore();
@@ -75,8 +76,8 @@ const studyId = computed(() => study.currentStudy?.id || "");
const { can } = usePermission();
const aeState = computed(() => (ae.value?.status as string) || "NEW");
const availableActions = computed(() => getAvailableActions(aeMachine, aeState.value));
const stateLabel = (v?: string | null) => getStateLabel(aeMachine, v);
const stateColor = (v?: string | null) => getStateColor(aeMachine, v) || "info";
const stateLabel = (v?: string | null) => getDictLabel(statusDict, v || "");
const stateColor = (v?: string | null) => getDictColor(statusDict, v || "") || "info";
const enrichOverdue = (item: any) => {
if (!item) return item;
+4 -3
View File
@@ -53,7 +53,8 @@ import { fetchFinanceItem } from "../api/finance";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import { usePermission } from "../utils/permission";
import { financeMachine, getAvailableActions, getStateColor, getStateLabel } from "../state-machine";
import { financeMachine, getAvailableActions } from "../state-machine";
import { financeStatusDict, getDictColor, getDictLabel } from "../dictionaries";
const study = useStudyStore();
const auth = useAuthStore();
@@ -83,8 +84,8 @@ const loadItem = async () => {
}
};
const stateLabel = (v?: string | null) => getStateLabel(financeMachine, v);
const stateColor = (v?: string | null) => getStateColor(financeMachine, v) || "info";
const stateLabel = (v?: string | null) => getDictLabel(financeStatusDict, v || "");
const stateColor = (v?: string | null) => getDictColor(financeStatusDict, v || "") || "info";
const formatAmount = (num?: number | string) => {
const n = Number(num);
+3 -2
View File
@@ -42,6 +42,7 @@ import { usePermission } from "../utils/permission";
import VisitTable from "../components/VisitTable.vue";
import { getAvailableActions, getStateColor, getStateLabel, subjectMachine } from "../state-machine";
import type { ActionConfig } from "../state-machine";
import { statusDict, getDictLabel, getDictColor } from "../dictionaries";
const route = useRoute();
const study = useStudyStore();
@@ -58,8 +59,8 @@ const permissionMap: Record<string, string> = {
drop: "subject.drop",
};
const availableActions = computed(() => getAvailableActions(subjectMachine, subject.value?.status));
const stateLabel = (v?: string | null) => getStateLabel(subjectMachine, v);
const stateColor = (v?: string | null) => getStateColor(subjectMachine, v) || "info";
const stateLabel = (v?: string | null) => getDictLabel(statusDict, v || "");
const stateColor = (v?: string | null) => getDictColor(statusDict, v || "") || "info";
const loadSubject = async () => {
if (!study.currentStudy) return;
+13 -8
View File
@@ -3,7 +3,7 @@
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadTasks">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
<el-option v-for="opt in statusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<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" />
@@ -25,12 +25,14 @@
</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="priority" label="优先级" width="100">
<template #default="scope">
<el-tag :type="priorityColor(scope.row.priority)">{{ priorityLabel(scope.row.priority) }}</el-tag>
</template>
</el-table-column>
<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>
<el-tag :type="statusColor(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="due_date" label="截止日期" width="140" />
@@ -66,6 +68,7 @@ import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import TaskForm from "../components/TaskForm.vue";
import { getDictColor, getDictLabel, getDictOptions, statusDict, priorityDict } from "../dictionaries";
const study = useStudyStore();
const auth = useAuthStore();
@@ -77,7 +80,7 @@ const total = ref(0);
const page = ref(1);
const pageSize = 10;
const statuses = ["TODO", "DOING", "DONE", "BLOCKED"];
const statusOptions = getDictOptions(statusDict).filter((opt) => ["TODO", "DOING", "DONE", "BLOCKED"].includes(opt.value));
const filters = ref({
status: "",
@@ -179,8 +182,10 @@ const loadSavedFilters = () => {
}
};
const statusLabel = (v: string) =>
({ TODO: "待处理", DOING: "进行中", DONE: "已完成", BLOCKED: "阻塞" }[v] || v);
const statusLabel = (v: string) => getDictLabel(statusDict, v);
const statusColor = (v: string) => getDictColor(statusDict, v) || "info";
const priorityLabel = (v: string) => getDictLabel(priorityDict, v);
const priorityColor = (v: string) => getDictColor(priorityDict, v) || "info";
const milestoneName = (id: string) => milestoneMap.value[id] || id || "-";