修复前后端显示异常问题(受试者、中心、负责人、日期等)

This commit is contained in:
Cheng Zhou
2025-12-18 22:09:28 +08:00
parent fb4950f8f7
commit 7137665410
48 changed files with 764 additions and 156 deletions
+28 -4
View File
@@ -14,9 +14,9 @@
</div>
</div>
<el-timeline>
<el-timeline-item v-for="c in comments" :key="c.id" :timestamp="c.created_at">
<el-timeline-item v-for="c in comments" :key="c.id" :timestamp="displayDateTime(c.created_at)">
<div class="comment-item">
<strong>{{ c.created_by }}</strong>
<strong>{{ displayUser(c.created_by, { users: userMap, members: memberMap }) }}</strong>
<p>{{ c.content }}</p>
</div>
</el-timeline-item>
@@ -25,10 +25,12 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { fetchComments, createComment } from "../api/comments";
import { useStudyStore } from "../store/study";
import { listMembers } from "../api/members";
import { displayDateTime, displayUser } from "../utils/display";
interface Props {
studyId: string;
@@ -43,6 +45,7 @@ const loading = ref(false);
const newComment = ref("");
const showInput = ref(false);
const study = useStudyStore();
const members = ref<any[]>([]);
const load = async () => {
if (!props.studyId) return;
@@ -57,6 +60,24 @@ const load = async () => {
}
};
const loadMembers = async () => {
if (!props.studyId) return;
try {
const { data } = await listMembers(props.studyId, { limit: 500 });
members.value = Array.isArray(data) ? data : data.items || [];
} catch {
members.value = [];
}
};
const memberMap = computed(() =>
members.value.reduce<Record<string, string>>((acc, cur) => {
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
return acc;
}, {})
);
const submit = async () => {
if (!newComment.value.trim()) {
ElMessage.warning("请输入评论");
@@ -77,7 +98,10 @@ const submit = async () => {
}
};
onMounted(() => load());
onMounted(() => {
loadMembers();
load();
});
</script>
<style scoped>
+4 -1
View File
@@ -12,7 +12,9 @@
<el-tag :type="scope.row.is_active ? 'success' : 'info'">{{ scope.row.is_active ? "是" : "否" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="180" />
<el-table-column prop="updated_at" label="更新时间" width="180">
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button type="text" size="small" @click="view(scope.row)">查看</el-button>
@@ -41,6 +43,7 @@ import { useRouter } from "vue-router";
import { updateFaqItem } from "../api/faqs";
import type { FaqItem, FaqCategory } from "../api/faqs";
import PermissionAction from "./PermissionAction.vue";
import { displayDateTime } from "../utils/display";
const props = defineProps<{
items: FaqItem[];
+20 -16
View File
@@ -11,7 +11,7 @@
</el-form-item>
<el-form-item label="中心" prop="site_id">
<el-select v-model="form.site_id" placeholder="请选择中心" filterable>
<el-option v-for="s in sites" :key="s.id" :label="s.name || s.id" :value="s.id" />
<el-option v-for="s in siteOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="owner_id">
@@ -54,7 +54,6 @@ interface Props {
milestone?: Record<string, any>;
sites?: any[];
members?: any[];
users?: any[];
}
const props = defineProps<Props>();
@@ -94,23 +93,28 @@ const submitting = ref(false);
const isEdit = computed(() => !!props.milestone);
const memberOptions = computed(() => {
const userMap = (props.users || []).reduce<Record<string, string>>((acc, cur: any) => {
if (cur?.id) acc[cur.id] = cur.username || cur.id;
return acc;
}, {});
const memberList = (props.members || []).filter((m: any) => m.is_active !== false);
if (memberList.length) {
return memberList.map((m: any) => ({
value: m.user_id,
label: userMap[m.user_id] || m.username || m.user_id,
}));
}
return (props.users || []).map((u: any) => ({
value: u.id,
label: u.username || u.id,
}));
const seen = new Set<string>();
return memberList
.map((m: any) => {
const user = m.user || {};
const label = user.display_name || user.username || m.username || "—";
return { value: m.user_id, label };
})
.filter((opt) => {
if (seen.has(opt.value)) return false;
seen.add(opt.value);
return true;
});
});
const siteOptions = computed(() =>
(props.sites || []).map((s: any) => ({
value: s.id,
label: s.name || "—",
}))
);
watch(
() => props.milestone,
(val) => {
+2 -4
View File
@@ -64,10 +64,9 @@ const onClose = () => {
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 = new Date().toISOString().slice(0, 10);
form.subject_no = "";
form.screening_date = "";
};
const onSubmit = async () => {
@@ -84,7 +83,6 @@ const onSubmit = async () => {
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) {
+18 -2
View File
@@ -13,7 +13,9 @@
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="assignee_id">
<UserSelect v-model="form.assignee_id" placeholder="选择负责人" />
<el-select v-model="form.assignee_id" placeholder="选择负责人" filterable clearable>
<el-option v-for="m in assigneeOptions" :key="m.value" :label="m.label" :value="m.value" />
</el-select>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择">
@@ -42,12 +44,12 @@ import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { createTask, updateTask } from "../api/tasks";
import { useStudyStore } from "../store/study";
import UserSelect from "./selectors/UserSelect.vue";
interface Props {
modelValue: boolean;
task?: Record<string, any>;
milestones: any[];
members?: any[];
}
const props = defineProps<Props>();
@@ -89,6 +91,20 @@ const rules: FormRules = {
const submitting = ref(false);
const isEdit = computed(() => !!props.task);
const assigneeOptions = computed(() => {
const seen = new Set<string>();
return (props.members || [])
.filter((m: any) => m.is_active !== false)
.map((m: any) => {
const user = m.user || {};
return { value: m.user_id, label: user.display_name || user.username || m.username || "—" };
})
.filter((opt) => {
if (seen.has(opt.value)) return false;
seen.add(opt.value);
return true;
});
});
watch(
() => props.task,
+17 -3
View File
@@ -1,14 +1,24 @@
<template>
<el-table :data="verifications" v-loading="loading" style="width: 100%">
<el-table-column prop="subject_id" label="受试者ID" />
<el-table-column prop="subject_id" label="受试者">
<template #default="scope">
{{ subjectMap[scope.row.subject_id] || scope.row.subject_no || scope.row.subject_id || "-" }}
</template>
</el-table-column>
<el-table-column prop="level" label="类型" width="100" />
<el-table-column label="完成度" width="180">
<template #default="scope">
<el-progress :percentage="scope.row.percent || 0" :stroke-width="12" />
</template>
</el-table-column>
<el-table-column prop="last_verified_at" label="最近核查" width="140" />
<el-table-column prop="verifier_id" label="核查人" width="160" />
<el-table-column prop="last_verified_at" label="最近核查" width="140">
<template #default="scope">{{ displayDate(scope.row.last_verified_at) }}</template>
</el-table-column>
<el-table-column prop="verifier_id" label="核查人" width="160">
<template #default="scope">
{{ verifierMap[scope.row.verifier_id] || "-" }}
</template>
</el-table-column>
<el-table-column v-if="canEdit" label="操作" width="120">
<template #default="scope">
<el-button type="primary" link size="small" @click="$emit('edit', scope.row)">更新进度</el-button>
@@ -18,10 +28,14 @@
</template>
<script setup lang="ts">
import { displayDate } from "../utils/display";
defineProps<{
verifications: any[];
canEdit: boolean;
loading: boolean;
subjectMap: Record<string, string>;
verifierMap: Record<string, string>;
}>();
defineEmits(["edit"]);
</script>
@@ -12,14 +12,18 @@
<el-table :data="attachments" v-loading="loading" style="width: 100%">
<el-table-column prop="filename" label="文件名" min-width="200" />
<el-table-column label="大小" width="120">
<template #default="scope">{{ formatFileSize(scope.row.file_size) }}</template>
<template #default="scope">{{ formatFileSize(scope.row.file_size ?? scope.row.size) }}</template>
</el-table-column>
<el-table-column label="上传人" width="180">
<template #default="scope">
{{ uploaderLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="uploaded_at" label="上传时间" width="180" />
<el-table-column prop="uploaded_at" label="上传时间" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.uploaded_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button link type="primary" size="small" @click="download(scope.row)">下载</el-button>
@@ -46,8 +50,8 @@ import AttachmentUploader from "./AttachmentUploader.vue";
import { formatFileSize } from "./attachmentUtils";
import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { fetchUsers } from "../../api/users";
import { listMembers } from "../../api/members";
import { displayDateTime, displayUser } from "../../utils/display";
const props = defineProps<{
studyId: string;
@@ -59,7 +63,6 @@ const attachments = ref<any[]>([]);
const loading = ref(false);
const auth = useAuthStore();
const study = useStudyStore();
const users = ref<any[]>([]);
const members = ref<any[]>([]);
const load = async () => {
@@ -75,15 +78,6 @@ const load = async () => {
}
};
const loadUsers = async () => {
try {
const { data } = await fetchUsers({ limit: 500 });
users.value = (data as any).items || data || [];
} catch {
users.value = [];
}
};
const loadMembers = async () => {
if (!props.studyId) return;
try {
@@ -95,15 +89,15 @@ const loadMembers = async () => {
};
const uploaderLabel = (row: any) => {
const userMap = users.value.reduce<Record<string, string>>((acc, cur) => {
if (cur?.id) acc[cur.id] = cur.username || cur.id;
return acc;
}, {});
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
if (cur?.user_id) acc[cur.user_id] = cur.username || cur.user_id;
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
return acc;
}, {});
return userMap[row.uploaded_by] || memberMap[row.uploaded_by] || row.uploaded_by || "-";
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
return row.uploaded_by.display_name || row.uploaded_by.username || row.uploaded_by.id || "—";
}
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
};
const download = (row: any) => {
@@ -116,7 +110,9 @@ const canDelete = (row: any) => {
const userId = auth.user?.id;
const role = auth.user?.role;
const projectRole = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
return userId === row.uploaded_by || role === "ADMIN" || projectRole === "PM";
const ownerId =
row.uploaded_by_id || (row.uploaded_by && typeof row.uploaded_by === "object" ? row.uploaded_by.id : row.uploaded_by);
return userId === ownerId || role === "ADMIN" || projectRole === "PM";
};
const remove = async (row: any) => {
@@ -132,7 +128,7 @@ const remove = async (row: any) => {
};
onMounted(async () => {
await Promise.all([loadUsers(), loadMembers()]);
await Promise.all([loadMembers()]);
load();
});
</script>