修复前后端显示异常问题(受试者、中心、负责人、日期等)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user