项目详情页--初步更新
This commit is contained in:
BIN
Binary file not shown.
@@ -14,7 +14,11 @@
|
|||||||
<el-table-column label="大小" width="120">
|
<el-table-column label="大小" width="120">
|
||||||
<template #default="scope">{{ formatFileSize(scope.row.file_size) }}</template>
|
<template #default="scope">{{ formatFileSize(scope.row.file_size) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="uploaded_by" label="上传人" width="160" />
|
<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" />
|
||||||
<el-table-column label="操作" width="180">
|
<el-table-column label="操作" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -42,6 +46,8 @@ import AttachmentUploader from "./AttachmentUploader.vue";
|
|||||||
import { formatFileSize } from "./attachmentUtils";
|
import { formatFileSize } from "./attachmentUtils";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { useStudyStore } from "../../store/study";
|
import { useStudyStore } from "../../store/study";
|
||||||
|
import { fetchUsers } from "../../api/users";
|
||||||
|
import { listMembers } from "../../api/members";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
studyId: string;
|
studyId: string;
|
||||||
@@ -53,6 +59,8 @@ const attachments = ref<any[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
|
const users = ref<any[]>([]);
|
||||||
|
const members = ref<any[]>([]);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
if (!props.studyId || !props.entityId) return;
|
if (!props.studyId || !props.entityId) return;
|
||||||
@@ -67,6 +75,37 @@ 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 {
|
||||||
|
const { data } = await listMembers(props.studyId, { limit: 500 });
|
||||||
|
members.value = Array.isArray(data) ? data : data.items || [];
|
||||||
|
} catch {
|
||||||
|
members.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return userMap[row.uploaded_by] || memberMap[row.uploaded_by] || row.uploaded_by || "-";
|
||||||
|
};
|
||||||
|
|
||||||
const download = (row: any) => {
|
const download = (row: any) => {
|
||||||
const token = localStorage.getItem("ctms_token");
|
const token = localStorage.getItem("ctms_token");
|
||||||
const url = token ? `/api/v1/attachments/${row.id}/download?token=${token}` : `/api/v1/attachments/${row.id}/download`;
|
const url = token ? `/api/v1/attachments/${row.id}/download?token=${token}` : `/api/v1/attachments/${row.id}/download`;
|
||||||
@@ -92,7 +131,10 @@ const remove = async (row: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadUsers(), loadMembers()]);
|
||||||
|
load();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import AdminUsers from "../views/admin/Users.vue";
|
|||||||
import AdminProjects from "../views/admin/Projects.vue";
|
import AdminProjects from "../views/admin/Projects.vue";
|
||||||
import ProjectMembers from "../views/admin/ProjectMembers.vue";
|
import ProjectMembers from "../views/admin/ProjectMembers.vue";
|
||||||
import AdminSites from "../views/admin/Sites.vue";
|
import AdminSites from "../views/admin/Sites.vue";
|
||||||
|
import ProjectDetail from "../views/admin/ProjectDetail.vue";
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -222,6 +223,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/projects/:projectId",
|
||||||
|
name: "ProjectDetail",
|
||||||
|
component: ProjectDetail,
|
||||||
|
meta: { title: "项目详情", requiresAdmin: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/studies",
|
path: "/studies",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<el-card class="mb-12">
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<h3>项目详情</h3>
|
||||||
|
<p class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
|
||||||
|
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
|
||||||
|
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="mb-12">
|
||||||
|
<h4 class="section-title">项目基本信息</h4>
|
||||||
|
<el-descriptions v-if="project" :column="2" border>
|
||||||
|
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ project.created_at || "—" }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-skeleton v-else :rows="4" animated />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card>
|
||||||
|
<h4 class="section-title">项目通用文件</h4>
|
||||||
|
<AttachmentList
|
||||||
|
v-if="project"
|
||||||
|
:study-id="project.id"
|
||||||
|
entity-type="project"
|
||||||
|
:entity-id="project.id"
|
||||||
|
:show-uploader="true"
|
||||||
|
/>
|
||||||
|
<el-skeleton v-else :rows="3" animated />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { fetchStudyDetail } from "../../api/studies";
|
||||||
|
import type { Study } from "../../types/api";
|
||||||
|
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||||
|
import { useStudyStore } from "../../store/study";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const studyStore = useStudyStore();
|
||||||
|
|
||||||
|
const project = ref<Study | null>(null);
|
||||||
|
|
||||||
|
const loadProject = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await fetchStudyDetail(route.params.projectId as string);
|
||||||
|
project.value = data as Study;
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.response?.data?.message || "项目加载失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = (status: string) =>
|
||||||
|
({
|
||||||
|
DRAFT: "草稿",
|
||||||
|
ACTIVE: "进行中",
|
||||||
|
CLOSED: "已关闭",
|
||||||
|
}[status] || status);
|
||||||
|
|
||||||
|
const enterProject = () => {
|
||||||
|
if (!project.value) return;
|
||||||
|
studyStore.setCurrentStudy({ ...project.value, role_in_study: project.value.role_in_study || "PM" } as Study);
|
||||||
|
router.push("/study/home");
|
||||||
|
};
|
||||||
|
|
||||||
|
const goMembers = () => {
|
||||||
|
if (!project.value) return;
|
||||||
|
router.push(`/admin/projects/${project.value.id}/members`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goSites = () => {
|
||||||
|
if (!project.value) return;
|
||||||
|
router.push(`/admin/projects/${project.value.id}/sites`);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadProject();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.mb-12 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,21 +16,29 @@
|
|||||||
<el-tag>{{ scope.row.role_in_study }}</el-tag>
|
<el-tag>{{ scope.row.role_in_study }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="120">
|
<el-table-column label="状态" width="160">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
<el-tooltip
|
||||||
<el-tag type="danger" v-else>已禁用</el-tag>
|
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||||
|
content="请前往【账号治理】启用该账号"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-tag type="danger">全局已禁用</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tag v-else :type="scope.row.is_active ? 'success' : 'danger'">
|
||||||
|
{{ scope.row.is_active ? "启用" : "已禁用" }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="added_at" label="加入时间" width="200" />
|
<el-table-column prop="added_at" label="加入时间" width="200" />
|
||||||
<el-table-column label="操作" width="320" fixed="right">
|
<el-table-column label="操作" width="360" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="scope.row.role_in_study"
|
v-model="scope.row.role_in_study"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 120px"
|
style="width: 120px"
|
||||||
@change="(val) => updateRole(scope.row.id, val)"
|
@change="(val) => updateRole(scope.row.id, val)"
|
||||||
:disabled="!scope.row.is_active"
|
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||||
>
|
>
|
||||||
<el-option label="PM" value="PM" />
|
<el-option label="PM" value="PM" />
|
||||||
<el-option label="CRA" value="CRA" />
|
<el-option label="CRA" value="CRA" />
|
||||||
@@ -38,11 +46,13 @@
|
|||||||
<el-option label="IMP" value="IMP" />
|
<el-option label="IMP" value="IMP" />
|
||||||
<el-option label="ADMIN" value="ADMIN" />
|
<el-option label="ADMIN" value="ADMIN" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">该账号已在系统级被禁用,无法在任何项目中使用</span>
|
||||||
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
|
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||||
@click="toggleActive(scope.row)"
|
@click="toggleActive(scope.row)"
|
||||||
>
|
>
|
||||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||||
@@ -151,9 +161,11 @@ const loadUsers = async () => {
|
|||||||
const memberRows = computed(() =>
|
const memberRows = computed(() =>
|
||||||
members.value.map((m) => {
|
members.value.map((m) => {
|
||||||
const user = users.value.find((u) => u.id === m.user_id);
|
const user = users.value.find((u) => u.id === m.user_id);
|
||||||
|
const effectiveStatus = user && user.is_active === false ? "DISABLED_GLOBAL" : m.is_active ? "ACTIVE" : "DISABLED";
|
||||||
return {
|
return {
|
||||||
...m,
|
...m,
|
||||||
username: user?.username || m.user_id,
|
username: user?.username || m.user_id,
|
||||||
|
effectiveStatus,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -388,4 +400,9 @@ onMounted(async () => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,8 +9,16 @@
|
|||||||
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="projects" v-loading="loading" stripe>
|
<el-table :data="projects" v-loading="loading" stripe>
|
||||||
<el-table-column prop="code" label="项目编号" width="140" />
|
<el-table-column prop="code" label="项目编号" width="140">
|
||||||
<el-table-column prop="name" label="项目名称" min-width="200" />
|
<template #default="scope">
|
||||||
|
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.code }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="项目名称" min-width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.name }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="sponsor" label="申办方" min-width="160" />
|
<el-table-column prop="sponsor" label="申办方" min-width="160" />
|
||||||
<el-table-column prop="status" label="状态" width="120">
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -84,6 +92,10 @@ const enterStudy = (row: Study) => {
|
|||||||
router.push("/study/home");
|
router.push("/study/home");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goDetail = (row: Study) => {
|
||||||
|
router.push(`/projects/${row.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const statusLabel = (status: string) =>
|
const statusLabel = (status: string) =>
|
||||||
({
|
({
|
||||||
DRAFT: "草稿",
|
DRAFT: "草稿",
|
||||||
|
|||||||
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.
Reference in New Issue
Block a user