启动与授权-初步优化

This commit is contained in:
Cheng Zhou
2026-01-15 11:25:13 +08:00
parent 4fc5f0ee9b
commit 05c1f9579a
28 changed files with 1082 additions and 300 deletions
+91 -103
View File
@@ -8,133 +8,126 @@
</div>
<el-card>
<el-tabs v-model="activeTab">
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.kickoffTab" name="kickoff">
<div class="tab-actions">
<el-button type="primary" @click="goNewKickoff">{{ TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</el-button>
</div>
<el-table :data="kickoffItems" v-loading="loadingKickoff" style="width: 100%">
<el-table-column prop="kickoff_date" :label="TEXT.common.fields.kickoffDate" width="160">
<template #default="scope">{{ displayDate(scope.row.kickoff_date) }}</template>
</el-table-column>
<el-table-column prop="attendees" :label="TEXT.common.fields.attendees" min-width="200">
<template #default="scope">
{{ (scope.row.attendees || []).join("、") || TEXT.common.fallback }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="200">
<template #default="scope">
<el-button link type="primary" size="small" @click="goKickoffDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
<el-button link type="primary" size="small" @click="goKickoffEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loadingKickoff && kickoffItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
</el-tab-pane>
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.trainingTab" name="training">
<div class="tab-actions">
<el-button type="primary" @click="goNewTraining">{{ TEXT.modules.startupMeetingAuth.trainingNewTitle }}</el-button>
</div>
<el-table :data="trainingItems" v-loading="loadingTraining" style="width: 100%">
<el-table-column prop="name" :label="TEXT.common.fields.name" min-width="140" />
<el-table-column prop="role" :label="TEXT.common.labels.role" min-width="120" />
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="140" />
<el-table-column :label="TEXT.common.fields.trained" width="120">
<template #default="scope">
<el-checkbox v-model="scope.row.trained" @change="toggleTraining(scope.row)" />
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.authorized" width="120">
<template #default="scope">
<el-checkbox v-model="scope.row.authorized" @change="toggleTraining(scope.row)" />
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="200">
<template #default="scope">
<el-button link type="primary" size="small" @click="goTrainingDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
<el-button link type="primary" size="small" @click="goTrainingEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loadingTraining && trainingItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyTraining" />
</el-tab-pane>
</el-tabs>
<el-table :data="kickoffRows" v-loading="loading" style="width: 100%" class="ctms-table" @row-click="onKickoffRowClick">
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="180" />
<el-table-column prop="kickoff_date" :label="TEXT.common.fields.kickoffDate" width="160">
<template #default="scope">{{ displayDate(scope.row.kickoff_date) }}</template>
</el-table-column>
<el-table-column :label="TEXT.modules.startupMeetingAuth.ownerLabel" min-width="160">
<template #default="scope">{{ contactLabel(scope.row) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.status" width="140">
<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>
</el-table>
<StateEmpty v-if="!loading && kickoffRows.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
</el-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useStudyStore } from "../../store/study";
import { listKickoffs, listTrainingAuthorizations, updateTrainingAuthorization } from "../../api/startup";
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 StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
const router = useRouter();
const study = useStudyStore();
const activeTab = ref("kickoff");
const kickoffItems = ref<any[]>([]);
const trainingItems = ref<any[]>([]);
const loadingKickoff = ref(false);
const loadingTraining = ref(false);
const sites = ref<any[]>([]);
const users = ref<any[]>([]);
const members = ref<any[]>([]);
const loading = ref(false);
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) => {
if (m?.user_id && !map[m.user_id]) map[m.user_id] = m.full_name || m.username || m.user_id;
});
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",
};
});
});
const loadKickoffs = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
loadingKickoff.value = true;
loading.value = true;
try {
const { data } = await listKickoffs(studyId);
kickoffItems.value = Array.isArray(data) ? data : data.items || [];
const [sitesResp, kickoffResp, membersResp, usersResp] = await Promise.all([
fetchSites(studyId, { limit: 500 }),
listKickoffs(studyId),
listMembers(studyId, { limit: 500 }),
fetchUsers({ limit: 500 }),
]);
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 {
loadingKickoff.value = false;
loading.value = false;
}
};
const loadTraining = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
loadingTraining.value = true;
try {
const { data } = await listTrainingAuthorizations(studyId);
trainingItems.value = Array.isArray(data) ? data : data.items || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loadingTraining.value = false;
}
};
const toggleTraining = async (row: any) => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
try {
await updateTrainingAuthorization(studyId, row.id, {
trained: row.trained,
authorized: row.authorized,
});
ElMessage.success(TEXT.common.messages.saveSuccess);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
loadTraining();
}
};
const goNewKickoff = () => router.push("/startup/kickoff/new");
const goKickoffDetail = (id: string) => router.push(`/startup/kickoff/${id}`);
const goKickoffEdit = (id: string) => router.push(`/startup/kickoff/${id}/edit`);
const goNewTraining = () => router.push("/startup/training/new");
const goTrainingDetail = (id: string) => router.push(`/startup/training/${id}`);
const goTrainingEdit = (id: string) => router.push(`/startup/training/${id}/edit`);
const onKickoffRowClick = async (row: any) => {
const studyId = study.currentStudy?.id;
if (!studyId || !row?.site_id) return;
if (row.meeting_id) {
goKickoffDetail(row.meeting_id);
return;
}
try {
const { data } = await createKickoff(studyId, { site_id: row.site_id });
goKickoffDetail(data.id);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.createFailed);
}
};
onMounted(() => {
loadKickoffs();
loadTraining();
});
</script>
@@ -163,9 +156,4 @@ onMounted(() => {
color: var(--ctms-text-secondary);
}
.tab-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
</style>