detail.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <div class="p-4">
  3. <!-- 顶部信息区域 -->
  4. <el-card shadow="hover" class="mb-4">
  5. <div class="flex justify-between items-center">
  6. <div class="text-left">
  7. <h2 class="text-xl font-bold text-gray-800 mb-2">{{ projectName }} - {{ groupInfo.groupName }}</h2>
  8. <div class="grid grid-cols-3 gap-4 text-sm text-gray-600">
  9. <div><span class="font-medium"></span>{{ groupInfo.personNum }}人</div>
  10. <div><span class="font-medium">组数:</span>{{ groupInfo.includeGroupNum }}组</div>
  11. <div><span class="font-medium">道数:</span>{{ groupInfo.trackNum }}道</div>
  12. <div><span class="font-medium">开始时间:</span>{{ groupInfo.beginTime }}</div>
  13. <div><span class="font-medium">预计结束时间:</span>{{ groupInfo.endTime }}</div>
  14. <div><span class="font-medium">场地数量:</span>{{ groupInfo.fieldNum }}个</div>
  15. </div>
  16. </div>
  17. <div class="text-right">
  18. <el-button type="primary" @click="generateGroups" :loading="generating">重新生成分组</el-button>
  19. <el-button @click="goBack">返回</el-button>
  20. </div>
  21. </div>
  22. </el-card>
  23. <!-- 分组详情表格 -->
  24. <el-card shadow="hover">
  25. <template #header>
  26. <div class="flex justify-between items-center">
  27. <span class="font-medium">分组详情</span>
  28. <div class="text-sm text-gray-500">
  29. 共 {{ groupInfo.includeGroupNum }} 组,{{ groupInfo.trackNum }} 条道
  30. </div>
  31. </div>
  32. </template>
  33. <div class="overflow-x-auto">
  34. <table class="w-full border-collapse border border-gray-300">
  35. <!-- 表头 -->
  36. <thead>
  37. <tr class="bg-gray-100">
  38. <th class="border border-gray-300 px-4 py-2 text-center font-medium">组别</th>
  39. <th
  40. v-for="track in groupInfo.trackNum"
  41. :key="track"
  42. class="border border-gray-300 px-4 py-2 text-center font-medium"
  43. >
  44. 第{{ getTrackName(track) }}道
  45. </th>
  46. </tr>
  47. </thead>
  48. <!-- 分组内容 -->
  49. <tbody>
  50. <tr
  51. v-for="groupIndex in groupInfo.includeGroupNum"
  52. :key="groupIndex"
  53. class="hover:bg-gray-50"
  54. >
  55. <td class="border border-gray-300 px-4 py-3 text-center font-medium bg-blue-50">
  56. 第{{ groupIndex }}组
  57. </td>
  58. <td
  59. v-for="track in groupInfo.trackNum"
  60. :key="track"
  61. class="border border-gray-300 px-4 py-3 text-center"
  62. >
  63. <div v-if="getAthleteByGroupAndTrack(groupIndex, track)" class="space-y-1">
  64. <div class="font-medium text-blue-600">{{ getAthleteByGroupAndTrack(groupIndex, track)?.athleteCode }}</div>
  65. <div class="text-sm">{{ getAthleteByGroupAndTrack(groupIndex, track)?.name }}</div>
  66. <div class="text-xs text-gray-500">{{ getTeamName(getAthleteByGroupAndTrack(groupIndex, track)?.teamId) }}</div>
  67. </div>
  68. <div v-else class="text-gray-400 text-sm">-</div>
  69. </td>
  70. </tr>
  71. </tbody>
  72. </table>
  73. </div>
  74. </el-card>
  75. <!-- 统计信息 -->
  76. <el-card shadow="hover" class="mt-4">
  77. <template #header>
  78. <span class="font-medium">分组统计</span>
  79. </template>
  80. <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
  81. <div class="text-center p-3 bg-blue-50 rounded-lg">
  82. <div class="text-2xl font-bold text-blue-600">{{ totalAthletes }}</div>
  83. <div class="text-sm text-gray-600">符合条件的运动员数量</div>
  84. </div>
  85. <div class="text-center p-3 bg-green-50 rounded-lg">
  86. <div class="text-2xl font-bold text-green-600">{{ groupInfo.includeGroupNum }}</div>
  87. <div class="text-sm text-gray-600">分组数</div>
  88. </div>
  89. <div class="text-center p-3 bg-purple-50 rounded-lg">
  90. <div class="text-2xl font-bold text-purple-600">{{ groupInfo.trackNum }}</div>
  91. <div class="text-sm text-gray-600">道数</div>
  92. </div>
  93. <!-- <div class="text-center p-3 bg-orange-50 rounded-lg">
  94. <div class="text-2xl font-bold text-orange-600">{{ groupInfo.personNum }}</div>
  95. <div class="text-sm text-gray-600">{{ groupInfo.groupName }}人数</div>
  96. </div> -->
  97. <div class="text-center p-3 bg-orange-50 rounded-lg">
  98. <div class="text-2xl font-bold text-orange-600">{{ roundType }}</div>
  99. <div class="text-sm text-gray-600">录取人数</div>
  100. </div>
  101. </div>
  102. </el-card>
  103. <!-- 调试信息 -->
  104. <!-- <el-card shadow="hover" class="mt-4">
  105. <template #header>
  106. <span class="font-medium">调试信息</span>
  107. </template>
  108. <div class="space-y-2 text-sm">
  109. <div><strong>项目ID:</strong> {{ groupInfo.projectId }}</div>
  110. <div><strong>性别要求:</strong> {{ groupInfo.memberGender === '0' ? '不分男女' : groupInfo.memberGender === '1' ? '男' : '女' }}</div>
  111. <div><strong>运动员总数:</strong> {{ athletes.length }}</div>
  112. <div><strong>符合条件的运动员数:</strong> {{ totalAthletes }}</div>
  113. <div><strong>分组结果大小:</strong> {{ groupResult.size }}</div>
  114. <div><strong>分组结果:</strong></div>
  115. <pre class="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">{{ JSON.stringify(Array.from(groupResult.entries()), null, 2) }}</pre>
  116. </div>
  117. </el-card> -->
  118. </div>
  119. </template>
  120. <script setup name="GameEventGroupDetail" lang="ts">
  121. import { ref, onMounted, computed, getCurrentInstance } from 'vue';
  122. import { useRoute, useRouter } from 'vue-router';
  123. import { getGameEventGroup } from '@/api/system/gameEventGroup';
  124. import { listGameAthlete } from '@/api/system/gameAthlete';
  125. import { listGameTeam } from '@/api/system/gameTeam';
  126. import { listGameEventProject } from '@/api/system/gameEventProject';
  127. import { GameEventGroupVO } from '@/api/system/gameEventGroup/types';
  128. import { GameAthleteVO } from '@/api/system/gameAthlete/types';
  129. import { GameTeamVO } from '@/api/system/gameTeam/types';
  130. import { GameEventProjectVO } from '@/api/system/gameEventProject/types';
  131. import type { ComponentInternalInstance } from 'vue';
  132. const route = useRoute();
  133. const router = useRouter();
  134. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  135. // 分组信息
  136. const groupInfo = ref<GameEventGroupVO>({} as GameEventGroupVO);
  137. // 运动员列表
  138. const athletes = ref<GameAthleteVO[]>([]);
  139. // 队伍列表
  140. const teams = ref<GameTeamVO[]>([]);
  141. // 项目列表
  142. const projects = ref<GameEventProjectVO[]>([]);
  143. // 分组结果
  144. const groupResult = ref<Map<string, GameAthleteVO>>(new Map());
  145. // 加载状态
  146. const loading = ref(false);
  147. // 生成分组状态
  148. const generating = ref(false);
  149. const roundType = ref();
  150. // 项目名称(需要从项目信息中获取)
  151. const projectName = computed(() => {
  152. if (!groupInfo.value.projectId) return '';
  153. const project = projects.value.find(p => p.projectId === groupInfo.value.projectId);
  154. roundType.value = project?.roundType || 0;
  155. return project?.projectName || '';
  156. });
  157. // 计算总运动员数
  158. const totalAthletes = computed(() => {
  159. return athletes.value.filter(athlete => {
  160. // 检查运动员是否参与该项目
  161. if (!athlete.projectList) return false;
  162. // 处理项目列表
  163. let projectIds: string[] = [];
  164. if (Array.isArray(athlete.projectList)) {
  165. projectIds = athlete.projectList.map(p => p.toString());
  166. } else if (typeof athlete.projectList === 'string') {
  167. try {
  168. projectIds = JSON.parse(athlete.projectList);
  169. } catch (e) {
  170. // 如果不是JSON格式,可能是逗号分隔的字符串
  171. projectIds = (athlete.projectList as string).split(',').map(p => p.trim());
  172. }
  173. }
  174. const targetProjectId = groupInfo.value.projectId?.toString();
  175. const hasProject = projectIds.includes(targetProjectId);
  176. if (!hasProject) {
  177. return false;
  178. }
  179. // 检查性别是否匹配
  180. if (groupInfo.value.memberGender && groupInfo.value.memberGender !== '0') {
  181. // 使用字典来匹配性别,而不是硬编码的字符串
  182. if (athlete.gender?.toString() !== groupInfo.value.memberGender?.toString()) {
  183. return false;
  184. }
  185. }
  186. return true;
  187. }).length;
  188. });
  189. // 调试:打印运动员数据
  190. const debugAthletes = computed(() => {
  191. return athletes.value.map(athlete => ({
  192. id: athlete.athleteId,
  193. name: athlete.name,
  194. teamId: athlete.teamId,
  195. projectList: athlete.projectList,
  196. gender: athlete.gender
  197. }));
  198. });
  199. // 获取道次名称
  200. const getTrackName = (track: number) => {
  201. const trackNames = ['一', '二', '三', '四', '五', '六', '七', '八','九','十'];
  202. return trackNames[track - 1] || track;
  203. };
  204. // 根据组别和道次获取运动员
  205. const getAthleteByGroupAndTrack = (groupIndex: number, track: number) => {
  206. const key = `${groupIndex}-${track}`;
  207. return groupResult.value.get(key);
  208. };
  209. // 根据队伍ID获取队伍名称
  210. const getTeamName = (teamId: string | number | undefined) => {
  211. if (!teamId) return '';
  212. const team = teams.value.find(t => t.teamId === teamId);
  213. return team?.teamName || '';
  214. };
  215. // 获取分组信息
  216. const getGroupInfo = async () => {
  217. try {
  218. loading.value = true;
  219. const groupId = route.query.id;
  220. if (!groupId || Array.isArray(groupId)) {
  221. proxy?.$modal.msgError('分组ID不能为空');
  222. return;
  223. }
  224. const res = await getGameEventGroup(groupId);
  225. groupInfo.value = res.data;
  226. // 获取运动员、队伍和项目信息
  227. await Promise.all([
  228. getAthletes(),
  229. getTeams(),
  230. getProjects()
  231. ]);
  232. // 生成分组
  233. generateGroups();
  234. } catch (error) {
  235. console.error('获取分组信息失败:', error);
  236. proxy?.$modal.msgError('获取分组信息失败');
  237. } finally {
  238. loading.value = false;
  239. }
  240. };
  241. // 获取运动员列表
  242. const getAthletes = async () => {
  243. try {
  244. const res = await listGameAthlete({
  245. pageNum: 1,
  246. pageSize: 1000,
  247. eventId: groupInfo.value.eventId,
  248. orderByColumn: '',
  249. isAsc: ''
  250. });
  251. athletes.value = res.rows;
  252. } catch (error) {
  253. console.error('获取运动员列表失败:', error);
  254. }
  255. };
  256. // 获取队伍列表
  257. const getTeams = async () => {
  258. try {
  259. const res = await listGameTeam({
  260. pageNum: 1,
  261. pageSize: 1000,
  262. eventId: groupInfo.value.eventId,
  263. orderByColumn: '',
  264. isAsc: ''
  265. });
  266. teams.value = res.rows;
  267. } catch (error) {
  268. console.error('获取队伍列表失败:', error);
  269. }
  270. };
  271. // 获取项目列表
  272. const getProjects = async () => {
  273. try {
  274. const res = await listGameEventProject({
  275. pageNum: 1,
  276. pageSize: 1000,
  277. orderByColumn: '',
  278. isAsc: ''
  279. });
  280. projects.value = res.rows;
  281. } catch (error) {
  282. console.error('获取项目列表失败:', error);
  283. }
  284. };
  285. // 生成分组
  286. const generateGroups = async () => {
  287. try {
  288. generating.value = true;
  289. // 清空之前的分组结果
  290. groupResult.value.clear();
  291. // 筛选符合条件的运动员
  292. const eligibleAthletes = athletes.value.filter(athlete => {
  293. // 检查是否参与该项目
  294. if (!athlete.projectList) {
  295. return false;
  296. }
  297. // 处理项目列表
  298. let projectIds: string[] = [];
  299. if (Array.isArray(athlete.projectList)) {
  300. projectIds = athlete.projectList.map(p => p.toString());
  301. } else if (typeof athlete.projectList === 'string') {
  302. try {
  303. projectIds = JSON.parse(athlete.projectList);
  304. } catch (e) {
  305. // 如果不是JSON格式,可能是逗号分隔的字符串
  306. projectIds = (athlete.projectList as string).split(',').map(p => p.trim());
  307. }
  308. }
  309. const targetProjectId = groupInfo.value.projectId?.toString();
  310. const hasProject = projectIds.includes(targetProjectId);
  311. if (!hasProject) {
  312. return false;
  313. }
  314. // 检查性别是否匹配
  315. if (groupInfo.value.memberGender && groupInfo.value.memberGender !== '0') {
  316. // 使用字典来匹配性别,而不是硬编码的字符串
  317. if (athlete.gender?.toString() !== groupInfo.value.memberGender?.toString()) {
  318. return false;
  319. }
  320. }
  321. return true;
  322. });
  323. if (eligibleAthletes.length === 0) {
  324. proxy?.$modal.msgWarning('没有找到符合条件的运动员');
  325. return;
  326. }
  327. // 随机打乱运动员顺序
  328. const shuffledAthletes = [...eligibleAthletes].sort(() => Math.random() - 0.5);
  329. // 记录已分配的运动员ID,避免重复分配
  330. const assignedAthleteIds = new Set();
  331. // 按组别和道次分配运动员
  332. for (let groupIndex = 1; groupIndex <= groupInfo.value.includeGroupNum; groupIndex++) {
  333. for (let track = 1; track <= groupInfo.value.trackNum; track++) {
  334. // 寻找可用的运动员
  335. let selectedAthlete = null;
  336. let athleteIndex = 0;
  337. while (athleteIndex < shuffledAthletes.length && !selectedAthlete) {
  338. const candidateAthlete = shuffledAthletes[athleteIndex];
  339. // 检查运动员是否已经被分配
  340. if (assignedAthleteIds.has(candidateAthlete.athleteId)) {
  341. athleteIndex++;
  342. continue;
  343. }
  344. // 检查同一组中是否已有同一队伍的运动员
  345. const hasSameTeamInGroup = Array.from(groupResult.value.entries())
  346. .some(([key, existingAthlete]) => {
  347. const [existingGroup] = key.split('-');
  348. return existingGroup === groupIndex.toString() &&
  349. existingAthlete.teamId === candidateAthlete.teamId;
  350. });
  351. if (!hasSameTeamInGroup) {
  352. selectedAthlete = candidateAthlete;
  353. // 标记运动员为已分配
  354. assignedAthleteIds.add(candidateAthlete.athleteId);
  355. }
  356. athleteIndex++;
  357. }
  358. // 如果找到了合适的运动员,分配到当前组和道次
  359. if (selectedAthlete) {
  360. const key = `${groupIndex}-${track}`;
  361. groupResult.value.set(key, selectedAthlete);
  362. }
  363. }
  364. }
  365. proxy?.$modal.msgSuccess('分组生成成功');
  366. } catch (error) {
  367. console.error('生成分组失败:', error);
  368. proxy?.$modal.msgError('生成分组失败');
  369. } finally {
  370. generating.value = false;
  371. }
  372. };
  373. // 返回上一页
  374. const goBack = () => {
  375. router.go(-1);
  376. };
  377. onMounted(() => {
  378. getGroupInfo();
  379. });
  380. </script>
  381. <style scoped>
  382. .overflow-x-auto {
  383. overflow-x: auto;
  384. }
  385. /* 响应式表格 */
  386. @media (max-width: 768px) {
  387. .grid {
  388. grid-template-columns: repeat(1, 1fr);
  389. }
  390. .overflow-x-auto {
  391. font-size: 12px;
  392. }
  393. .px-4 {
  394. padding-left: 8px;
  395. padding-right: 8px;
  396. }
  397. .py-3 {
  398. padding-top: 6px;
  399. padding-bottom: 6px;
  400. }
  401. }
  402. </style>