5b97ea32ab
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态 - 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员 - PM(项目负责人)角色自动拥有全部项目权限且不可修改 - 系统管理员在项目中的成员身份不可被删除 - 管理界面全面美化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
6.1 KiB
Vue
197 lines
6.1 KiB
Vue
<template>
|
|
<div class="page ctms-page-shell page--flush">
|
|
<div class="unified-shell">
|
|
<section class="unified-section kickoff-table-section section--flush">
|
|
<el-table
|
|
:data="kickoffRows"
|
|
v-loading="loading"
|
|
style="width: 100%"
|
|
class="ctms-table kickoff-table"
|
|
:row-class-name="kickoffRowClass"
|
|
@row-click="onKickoffRowClick"
|
|
table-layout="fixed"
|
|
>
|
|
<el-table-column prop="site_name" :label="TEXT.common.fields.site" show-overflow-tooltip />
|
|
<el-table-column prop="kickoff_date" :label="TEXT.common.fields.kickoffDate">
|
|
<template #default="scope">{{ displayDate(scope.row.kickoff_date) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="TEXT.modules.startupMeetingAuth.ownerLabel" show-overflow-tooltip>
|
|
<template #default="scope">{{ contactLabel(scope.row) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="TEXT.common.fields.status">
|
|
<template #default="scope">
|
|
<el-tag :type="scope.row.status === 'COMPLETED' ? 'success' : 'info'">
|
|
{{ scope.row.status === 'COMPLETED' ? TEXT.modules.startupMeetingAuth.statusCompleted : TEXT.modules.startupMeetingAuth.statusPending }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<template #empty>
|
|
<div v-if="!loading" class="table-empty">{{ TEXT.modules.startupMeetingAuth.emptyKickoff }}</div>
|
|
</template>
|
|
</el-table>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { ElMessage } from "element-plus";
|
|
import { useAuthStore } from "../../store/auth";
|
|
import { useStudyStore } from "../../store/study";
|
|
import { createKickoff, listKickoffs } from "../../api/startup";
|
|
import { fetchSites } from "../../api/sites";
|
|
import { listMembers } from "../../api/members";
|
|
import { fetchUsers } from "../../api/users";
|
|
import { displayDate } from "../../utils/display";
|
|
import { TEXT } from "../../locales";
|
|
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
const study = useStudyStore();
|
|
const kickoffItems = ref<any[]>([]);
|
|
const sites = ref<any[]>([]);
|
|
const users = ref<any[]>([]);
|
|
const members = ref<any[]>([]);
|
|
const loading = ref(false);
|
|
const isAdmin = computed(() => auth.user?.is_admin);
|
|
|
|
const memberNameMap = computed(() => {
|
|
const map: Record<string, string> = {};
|
|
users.value.forEach((u: any) => {
|
|
if (u?.id) map[u.id] = u.full_name || u.username || u.id;
|
|
});
|
|
members.value.forEach((m: any) => {
|
|
const user = m?.user;
|
|
if (m?.user_id && !map[m.user_id]) {
|
|
map[m.user_id] = user?.full_name || user?.username || user?.email || m.user_id;
|
|
}
|
|
});
|
|
return map;
|
|
});
|
|
|
|
const siteActiveMap = computed(() => {
|
|
const map: Record<string, boolean> = {};
|
|
sites.value.forEach((site) => {
|
|
if (site?.id) map[site.id] = !!site.is_active;
|
|
});
|
|
return map;
|
|
});
|
|
|
|
const contactLabel = (row: any) => {
|
|
if (!row?.contact) return TEXT.common.fallback;
|
|
return String(row.contact)
|
|
.split(",")
|
|
.map((c) => c.trim())
|
|
.filter(Boolean)
|
|
.map((c) => memberNameMap.value[c] || TEXT.common.fallback)
|
|
.join("、") || TEXT.common.fallback;
|
|
};
|
|
|
|
const kickoffRows = computed(() => {
|
|
const kickoffMap = kickoffItems.value.reduce<Record<string, any>>((acc, item) => {
|
|
if (item.site_id) acc[item.site_id] = item;
|
|
return acc;
|
|
}, {});
|
|
return sites.value
|
|
.map((site) => {
|
|
const meeting = kickoffMap[site.id];
|
|
return {
|
|
site_id: site.id,
|
|
site_name: site.name || TEXT.common.fallback,
|
|
contact: site.contact,
|
|
kickoff_date: meeting?.kickoff_date || null,
|
|
meeting_id: meeting?.id,
|
|
status: meeting?.kickoff_date ? "COMPLETED" : "PENDING",
|
|
is_active: !!site.is_active,
|
|
};
|
|
})
|
|
.sort((a, b) => Number(a.is_active === false) - Number(b.is_active === false));
|
|
});
|
|
|
|
const loadKickoffs = async () => {
|
|
const studyId = study.currentStudy?.id;
|
|
if (!studyId) return;
|
|
loading.value = true;
|
|
try {
|
|
const requests = [
|
|
fetchSites(studyId, { limit: 500 }),
|
|
listKickoffs(studyId),
|
|
listMembers(studyId, { limit: 500 }),
|
|
];
|
|
if (isAdmin.value) {
|
|
requests.push(fetchUsers({ limit: 500 }));
|
|
}
|
|
const [sitesResp, kickoffResp, membersResp, usersResp] = await Promise.all(requests) as any[];
|
|
sites.value = Array.isArray(sitesResp.data) ? sitesResp.data : sitesResp.data.items || [];
|
|
kickoffItems.value = Array.isArray(kickoffResp.data) ? kickoffResp.data : kickoffResp.data.items || [];
|
|
members.value = Array.isArray(membersResp.data) ? membersResp.data : membersResp.data.items || [];
|
|
users.value = usersResp?.data?.items || [];
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const goKickoffDetail = (id: string) => router.push(`/startup/kickoff/${id}`);
|
|
const onKickoffRowClick = async (row: any) => {
|
|
const studyId = study.currentStudy?.id;
|
|
if (!studyId || !row?.site_id) return;
|
|
if (row.is_active === false && !row.meeting_id) {
|
|
ElMessage.warning("中心已停用");
|
|
return;
|
|
}
|
|
if (row.meeting_id) {
|
|
goKickoffDetail(row.meeting_id);
|
|
return;
|
|
}
|
|
try {
|
|
const { data } = await createKickoff(studyId, { site_id: row.site_id }) as any;
|
|
goKickoffDetail(data.id);
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.createFailed);
|
|
}
|
|
};
|
|
|
|
const kickoffRowClass = ({ row }: { row: any }) => (row?.is_active === false ? "row-inactive" : "");
|
|
onMounted(() => {
|
|
loadKickoffs();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
}
|
|
|
|
.kickoff-table :deep(.el-table__inner-wrapper::before) {
|
|
display: none;
|
|
}
|
|
|
|
.table-empty {
|
|
min-height: 220px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #8a97ab;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
.row-inactive td {
|
|
color: var(--ctms-text-secondary);
|
|
background-color: #fff7d6 !important;
|
|
}
|
|
|
|
.row-inactive .el-tag {
|
|
opacity: 0.6;
|
|
}
|
|
</style>
|