Files
ctms/frontend/src/views/ia/StartupMeetingAuth.vue
T
Cheng Zhou 5b97ea32ab 移除用户系统级角色,权限完全迁移至项目级管理
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态
- 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员
- PM(项目负责人)角色自动拥有全部项目权限且不可修改
- 系统管理员在项目中的成员身份不可被删除
- 管理界面全面美化

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:52:17 +08:00

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>