index.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. <template>
  2. <div class="p-2 page-container">
  3. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  4. <div v-show="showSearch" class="mb-[10px]">
  5. <el-card shadow="hover">
  6. <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
  7. <el-row :gutter="20">
  8. <el-col :span="6">
  9. <el-form-item label="活动名称" prop="activityName">
  10. <el-input v-model="queryParams.activityName" placeholder="请输入活动名称" clearable @keyup.enter="handleQuery" />
  11. </el-form-item>
  12. </el-col>
  13. <el-col :span="6">
  14. <el-form-item label="活动类型" prop="activityType">
  15. <el-select v-model="queryParams.activityType" placeholder="请选择活动类型" clearable style="width: 100%">
  16. <el-option v-for="dict in stgl_activity_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  17. </el-select>
  18. </el-form-item>
  19. </el-col>
  20. <el-col :span="6">
  21. <el-form-item label="主办方" prop="organizerName">
  22. <el-input v-model="queryParams.organizerName" placeholder="请输入主办方名称" clearable @keyup.enter="handleQuery" />
  23. </el-form-item>
  24. </el-col>
  25. <el-col :span="6">
  26. <el-form-item label="报名方式" prop="registrationType">
  27. <el-select v-model="queryParams.registrationType" placeholder="请选择报名方式" clearable style="width: 100%">
  28. <el-option v-for="dict in stgl_registration_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  29. </el-select>
  30. </el-form-item>
  31. </el-col>
  32. </el-row>
  33. <el-row>
  34. <el-col :span="24" class="search-btn-container">
  35. <el-form-item>
  36. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  37. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  38. </el-form-item>
  39. </el-col>
  40. </el-row>
  41. </el-form>
  42. </el-card>
  43. </div>
  44. </transition>
  45. <el-card shadow="never" class="button-card table-card">
  46. <template #header>
  47. <el-row :gutter="10" class="mb8">
  48. <el-col :span="1.5">
  49. <span class="page-title">活动报名大厅</span>
  50. </el-col>
  51. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  52. </el-row>
  53. </template>
  54. <el-table v-loading="loading" border :data="activityList" @row-dblclick="handleView">
  55. <el-table-column label="活动名称" align="center" prop="activityName" min-width="150" show-overflow-tooltip />
  56. <el-table-column label="活动类型" align="center" prop="activityType" width="100">
  57. <template #default="scope">
  58. <dict-tag :options="stgl_activity_type" :value="scope.row.activityType" />
  59. </template>
  60. </el-table-column>
  61. <el-table-column label="主办方" align="center" prop="organizerName" min-width="150" show-overflow-tooltip />
  62. <el-table-column label="关联社团/队伍" align="center" min-width="150" show-overflow-tooltip>
  63. <template #default="scope">
  64. <span v-if="scope.row.relatedClubName">{{ scope.row.relatedClubName }}</span>
  65. <span v-else class="text-gray-400">未关联</span>
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="活动时间" align="center" width="160">
  69. <template #default="scope">
  70. <div v-if="scope.row.startTime && scope.row.endTime">
  71. <div>{{ formatDate(scope.row.startTime) }}</div>
  72. <div class="text-sm text-gray-500">{{ formatDate(scope.row.endTime) }}</div>
  73. </div>
  74. <span v-else class="text-gray-400">未设置</span>
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="报名时间" align="center" width="160">
  78. <template #default="scope">
  79. <div v-if="scope.row.registrationStart && scope.row.registrationEnd">
  80. <div>{{ formatDate(scope.row.registrationStart) }}</div>
  81. <div class="text-sm text-gray-500">{{ formatDate(scope.row.registrationEnd) }}</div>
  82. </div>
  83. <span v-else class="text-gray-400">未设置</span>
  84. </template>
  85. </el-table-column>
  86. <el-table-column label="报名方式" align="center" prop="registrationType" width="100">
  87. <template #default="scope">
  88. <dict-tag :options="stgl_registration_type" :value="scope.row.registrationType" />
  89. </template>
  90. </el-table-column>
  91. <el-table-column label="报名状态" align="center" prop="registrationStatus" width="100">
  92. <template #default="scope">
  93. <dict-tag :options="stgl_registration_status" :value="scope.row.registrationStatus" />
  94. </template>
  95. </el-table-column>
  96. <el-table-column label="报名人数" align="center" width="100">
  97. <template #default="scope">
  98. <el-tag type="info" size="small">{{ scope.row.registrationCount || 0 }}</el-tag>
  99. </template>
  100. </el-table-column>
  101. <el-table-column label="审核设置" align="center" width="100">
  102. <template #default="scope">
  103. <el-tag v-if="scope.row.needApproval === 0" type="success" size="small">无需审核</el-tag>
  104. <el-tag v-else type="warning" size="small">需要审核</el-tag>
  105. </template>
  106. </el-table-column>
  107. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
  108. <template #default="scope">
  109. <el-tooltip content="查看详情" placement="top">
  110. <el-button
  111. link
  112. type="primary"
  113. icon="View"
  114. size="default"
  115. style="font-size: 16px; margin-right: 8px; color: blue"
  116. @click="handleView(scope.row)"
  117. ></el-button>
  118. </el-tooltip>
  119. <el-tooltip content="活动报名" placement="top" v-if="canRegister(scope.row)">
  120. <el-button
  121. link
  122. type="success"
  123. icon="Plus"
  124. size="default"
  125. style="font-size: 16px; margin-right: 8px"
  126. @click="handleRegister(scope.row)"
  127. ></el-button>
  128. </el-tooltip>
  129. <el-tooltip content="已报名" placement="top" v-else-if="isRegistered(scope.row)">
  130. <el-button link type="warning" icon="Check" size="default" style="font-size: 16px" disabled></el-button>
  131. </el-tooltip>
  132. <el-tooltip content="报名已结束" placement="top" v-else>
  133. <el-button link type="info" icon="Clock" size="default" style="font-size: 16px" disabled></el-button>
  134. </el-tooltip>
  135. </template>
  136. </el-table-column>
  137. </el-table>
  138. <div class="pagination-container">
  139. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  140. </div>
  141. </el-card>
  142. <!-- 查看活动详细信息对话框 -->
  143. <el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="1200px" append-to-body>
  144. <ActivityDetailView :activity="viewForm" />
  145. <template #footer>
  146. <div class="dialog-footer">
  147. <el-button type="primary" @click="handleRegister(viewForm)" v-if="canRegister(viewForm)">立即报名</el-button>
  148. <el-button @click="viewDialog.visible = false">关 闭</el-button>
  149. </div>
  150. </template>
  151. </el-dialog>
  152. <!-- 活动报名对话框 -->
  153. <el-dialog title="活动报名" v-model="registerDialog.visible" width="1000px" append-to-body>
  154. <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="120px">
  155. <el-form-item label="活动名称">
  156. <el-input v-model="selectedActivity.activityName" disabled />
  157. </el-form-item>
  158. <!-- 审核设置提示 -->
  159. <el-form-item label="审核设置">
  160. <el-alert
  161. v-if="selectedActivity.needApproval === 0"
  162. title="无需审核"
  163. type="success"
  164. description="该活动设置为无需审核,报名后将自动通过"
  165. :closable="false"
  166. show-icon
  167. />
  168. <el-alert
  169. v-else
  170. title="需要审核"
  171. type="info"
  172. description="该活动设置为需要审核,报名后需要等待管理员审核通过"
  173. :closable="false"
  174. show-icon
  175. />
  176. </el-form-item>
  177. <el-form-item label="报名类型" prop="registrationType">
  178. <el-radio-group v-model="registerForm.registrationType" @change="handleRegistrationTypeChange">
  179. <el-radio v-if="isRegistrationTypeAllowed(1)" :value="1">个人报名</el-radio>
  180. <el-radio v-if="isRegistrationTypeAllowed(2)" :value="2">团队报名</el-radio>
  181. </el-radio-group>
  182. </el-form-item>
  183. <!-- 个人报名信息 -->
  184. <div v-if="registerForm.registrationType === 1">
  185. <el-row :gutter="20">
  186. <el-col :span="12">
  187. <el-form-item label="姓名" prop="studentName">
  188. <el-input v-model="registerForm.studentName" readonly />
  189. </el-form-item>
  190. </el-col>
  191. <el-col :span="12">
  192. <el-form-item label="学号" prop="studentNo">
  193. <el-input v-model="registerForm.studentNo" readonly />
  194. </el-form-item>
  195. </el-col>
  196. </el-row>
  197. <el-row :gutter="20">
  198. <el-col :span="12">
  199. <el-form-item label="性别" prop="gender">
  200. <el-select v-model="registerForm.gender" disabled style="width: 100%">
  201. <el-option label="男" value="0" />
  202. <el-option label="女" value="1" />
  203. </el-select>
  204. </el-form-item>
  205. </el-col>
  206. <el-col :span="12">
  207. <el-form-item label="联系电话" prop="phone">
  208. <el-input v-model="registerForm.phone" />
  209. </el-form-item>
  210. </el-col>
  211. </el-row>
  212. <el-row :gutter="20">
  213. <el-col :span="12">
  214. <el-form-item label="院系" prop="department">
  215. <el-input v-model="registerForm.department" readonly />
  216. </el-form-item>
  217. </el-col>
  218. <el-col :span="12">
  219. <el-form-item label="专业" prop="major">
  220. <el-input v-model="registerForm.major" readonly />
  221. </el-form-item>
  222. </el-col>
  223. </el-row>
  224. <el-row :gutter="20">
  225. <el-col :span="12">
  226. <el-form-item label="班级" prop="className">
  227. <el-input v-model="registerForm.className" readonly />
  228. </el-form-item>
  229. </el-col>
  230. <el-col :span="12">
  231. <el-form-item label="邮箱" prop="email">
  232. <el-input v-model="registerForm.email" />
  233. </el-form-item>
  234. </el-col>
  235. </el-row>
  236. </div>
  237. <!-- 团队报名信息 -->
  238. <div v-if="registerForm.registrationType === 2">
  239. <el-form-item label="团队名称" prop="teamName">
  240. <el-input v-model="registerForm.teamName" placeholder="请输入团队名称" />
  241. </el-form-item>
  242. <el-form-item label="团队描述" prop="teamDescription">
  243. <el-input v-model="registerForm.teamDescription" type="textarea" :rows="3" placeholder="请输入团队描述" />
  244. </el-form-item>
  245. <!-- 团队负责人信息 -->
  246. <el-divider content-position="left">团队负责人(当前用户)</el-divider>
  247. <el-row :gutter="20">
  248. <el-col :span="12">
  249. <el-form-item label="负责人姓名" prop="studentName">
  250. <el-input v-model="registerForm.studentName" readonly />
  251. </el-form-item>
  252. </el-col>
  253. <el-col :span="12">
  254. <el-form-item label="负责人学号" prop="studentNo">
  255. <el-input v-model="registerForm.studentNo" readonly />
  256. </el-form-item>
  257. </el-col>
  258. </el-row>
  259. <el-row :gutter="20">
  260. <el-col :span="12">
  261. <el-form-item label="性别" prop="gender">
  262. <el-select v-model="registerForm.gender" disabled style="width: 100%">
  263. <el-option label="男" value="0" />
  264. <el-option label="女" value="1" />
  265. </el-select>
  266. </el-form-item>
  267. </el-col>
  268. <el-col :span="12">
  269. <el-form-item label="联系电话" prop="phone">
  270. <el-input v-model="registerForm.phone" readonly />
  271. </el-form-item>
  272. </el-col>
  273. </el-row>
  274. <el-row :gutter="20">
  275. <el-col :span="12">
  276. <el-form-item label="院系" prop="department">
  277. <el-input v-model="registerForm.department" readonly />
  278. </el-form-item>
  279. </el-col>
  280. <el-col :span="12">
  281. <el-form-item label="专业" prop="major">
  282. <el-input v-model="registerForm.major" readonly />
  283. </el-form-item>
  284. </el-col>
  285. </el-row>
  286. <el-row :gutter="20">
  287. <el-col :span="12">
  288. <el-form-item label="班级" prop="className">
  289. <el-input v-model="registerForm.className" readonly />
  290. </el-form-item>
  291. </el-col>
  292. <el-col :span="12">
  293. <el-form-item label="邮箱" prop="email">
  294. <el-input v-model="registerForm.email" readonly />
  295. </el-form-item>
  296. </el-col>
  297. </el-row>
  298. <!-- 团队成员管理 -->
  299. <el-divider content-position="left" style="margin-left: 0px">团队成员</el-divider>
  300. <VicePresidentManager
  301. :model-value="registerForm.teamMembersNew"
  302. title="团队管理"
  303. addButtonText="添加团队成员"
  304. emptyTip="暂无团队成员,点击上方按钮添加"
  305. selectPlaceholder="选择团队成员"
  306. @change="handleTeamMembersNewChange"
  307. />
  308. </div>
  309. <el-form-item label="申请理由" prop="remark" style="margin-top: 24px">
  310. <el-input v-model="registerForm.remark" type="textarea" :rows="4" placeholder="请简要说明您的申请理由和个人优势(选填)" />
  311. </el-form-item>
  312. </el-form>
  313. <template #footer>
  314. <div class="dialog-footer">
  315. <el-button :loading="registerLoading" type="primary" @click="submitRegister">提交报名</el-button>
  316. <el-button @click="registerDialog.visible = false">取 消</el-button>
  317. </div>
  318. </template>
  319. </el-dialog>
  320. </div>
  321. </template>
  322. <script setup name="StglActivityRegisterHall" lang="ts">
  323. import { nextTick } from 'vue';
  324. import { CircleCheck, Clock } from '@element-plus/icons-vue';
  325. import {
  326. listRegistrableActivities,
  327. getActivityDetail,
  328. registerActivity,
  329. checkRegistration,
  330. getMyRegistrations
  331. } from '@/api/stgl/activityregisterhall';
  332. import { StglActivityVO, StglActivityQuery } from '@/api/stgl/activity/types';
  333. import { StglActivityRegistrationForm, TeamMemberInfo, StglActivityVo } from '@/api/stgl/activityregisterhall/types';
  334. import ActivityDetailView from '@/views/stgl/activity/components/ActivityDetailView.vue';
  335. import TeamMemberManager from '@/components/TeamMemberManager/index.vue';
  336. import VicePresidentManager from '@/components/VicePresidentManager/index.vue';
  337. import StudentSelector from '@/components/StudentSelector/index.vue';
  338. import { getPersonalInfo } from '@/api/xg/xsjcxx/studentMsg/studentMsg/index';
  339. import type { StudentMsgForm } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
  340. import { StudentVO } from '@/api/xg/student/types';
  341. import { VicePresidentVO } from '@/components/VicePresidentManager/index.vue';
  342. import { getUser } from '@/api/system/user';
  343. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  344. const { stgl_activity_type, stgl_registration_type, stgl_registration_status } = toRefs<any>(
  345. proxy?.useDict('stgl_activity_type', 'stgl_registration_type', 'stgl_registration_status')
  346. );
  347. // 修改类型定义,使用后端返回的StglActivityVo类型
  348. const activityList = ref<StglActivityVo[]>([]);
  349. const loading = ref(true);
  350. const showSearch = ref(true);
  351. const total = ref(0);
  352. const registerLoading = ref(false);
  353. const queryFormRef = ref<ElFormInstance>();
  354. const registerFormRef = ref<ElFormInstance>();
  355. const viewDialog = reactive<DialogOption>({
  356. visible: false,
  357. title: ''
  358. });
  359. const registerDialog = reactive<DialogOption>({
  360. visible: false,
  361. title: ''
  362. });
  363. const selectedActivity = ref<StglActivityVo>({} as StglActivityVo);
  364. const viewForm = ref<StglActivityVo>({} as StglActivityVo);
  365. // 用户报名状态缓存
  366. const userRegistrationStatus = ref<Record<number, boolean>>({});
  367. const data = reactive<PageData<StglActivityRegistrationForm, StglActivityQuery>>({
  368. form: {
  369. activityId: undefined,
  370. registrationType: undefined,
  371. studentName: undefined,
  372. studentNo: undefined
  373. } as StglActivityRegistrationForm,
  374. queryParams: {
  375. pageNum: 1,
  376. pageSize: 10,
  377. activityName: undefined,
  378. activityType: undefined,
  379. organizerName: undefined,
  380. registrationType: undefined,
  381. params: {}
  382. },
  383. rules: {}
  384. });
  385. const { queryParams } = toRefs(data);
  386. // 报名表单
  387. const registerForm = reactive<
  388. StglActivityRegistrationForm & {
  389. presidentStudent?: StudentVO | null;
  390. teamMembersNew?: VicePresidentVO[];
  391. }
  392. >({
  393. activityId: undefined,
  394. registrationType: undefined,
  395. studentName: undefined,
  396. studentNo: undefined,
  397. gender: undefined,
  398. phone: undefined,
  399. email: undefined,
  400. department: undefined,
  401. major: undefined,
  402. className: undefined,
  403. teamName: undefined,
  404. teamDescription: undefined,
  405. teamMembers: [], // 保留原有字段,用于兼容
  406. teamMembersNew: [], // 新增:使用VicePresidentVO结构的团队成员
  407. remark: undefined,
  408. presidentStudent: null // 新增:团队负责人学生对象
  409. });
  410. const registerRules = reactive({
  411. registrationType: [{ required: true, message: '请选择报名类型', trigger: 'change' }],
  412. presidentStudent: [{ required: true, message: '请选择团队负责人', trigger: 'change' }],
  413. studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
  414. phone: [
  415. { required: true, message: '联系电话不能为空', trigger: 'blur' },
  416. {
  417. pattern: /^1[3-9]\d{9}$/,
  418. message: '请输入正确的手机号码',
  419. trigger: 'blur'
  420. } as any
  421. ],
  422. teamName: [{ required: true, message: '团队名称不能为空', trigger: 'blur' }]
  423. } as any);
  424. /** 查询活动列表 */
  425. const getList = async () => {
  426. loading.value = true;
  427. try {
  428. const res = await listRegistrableActivities(queryParams.value);
  429. activityList.value = res.rows || [];
  430. total.value = res.total || 0;
  431. // 批量检查用户报名状态
  432. await checkUserRegistrationStatus();
  433. } catch (error) {
  434. console.error('查询活动列表失败:', error);
  435. proxy?.$modal.msgError('查询活动列表失败');
  436. // 发生错误时重置数据
  437. activityList.value = [];
  438. total.value = 0;
  439. } finally {
  440. loading.value = false;
  441. }
  442. };
  443. /** 批量检查用户报名状态 */
  444. const checkUserRegistrationStatus = async () => {
  445. // 防御性检查:确保activityList存在且为数组
  446. if (!activityList.value || !Array.isArray(activityList.value) || activityList.value.length === 0) {
  447. console.warn('活动列表为空或无效,跳过报名状态检查');
  448. return;
  449. }
  450. const promises = activityList.value.map(async (activity) => {
  451. try {
  452. // 确保activity和activity.id存在
  453. if (!activity || !activity.id) {
  454. console.warn('活动数据不完整,跳过报名状态检查:', activity);
  455. return;
  456. }
  457. const registered = await checkRegistration(activity.id as number);
  458. userRegistrationStatus.value[activity.id as number] = registered.data;
  459. } catch (error) {
  460. console.error(`检查活动${activity?.id}报名状态失败:`, error);
  461. if (activity?.id) {
  462. userRegistrationStatus.value[activity.id as number] = false;
  463. }
  464. }
  465. });
  466. await Promise.all(promises);
  467. };
  468. /** 搜索按钮操作 */
  469. const handleQuery = () => {
  470. queryParams.value.pageNum = 1;
  471. getList();
  472. };
  473. /** 重置按钮操作 */
  474. const resetQuery = () => {
  475. queryFormRef.value?.resetFields();
  476. handleQuery();
  477. };
  478. /** 查看按钮操作 */
  479. const handleView = async (row: StglActivityVo) => {
  480. const res = await getActivityDetail(row.id);
  481. viewForm.value = res.data;
  482. viewDialog.visible = true;
  483. viewDialog.title = '查看活动信息';
  484. };
  485. /** 报名按钮操作 */
  486. const handleRegister = async (row: StglActivityVo) => {
  487. selectedActivity.value = row;
  488. resetRegisterForm();
  489. registerDialog.visible = true;
  490. };
  491. /** 重置报名表单 */
  492. const resetRegisterForm = () => {
  493. Object.assign(registerForm, {
  494. activityId: selectedActivity.value.id,
  495. registrationType: undefined,
  496. studentName: undefined,
  497. studentNo: undefined,
  498. gender: undefined,
  499. phone: undefined,
  500. email: undefined,
  501. department: undefined,
  502. major: undefined,
  503. className: undefined,
  504. teamName: undefined,
  505. teamDescription: undefined,
  506. teamMembers: [],
  507. teamMembersNew: [],
  508. remark: undefined,
  509. presidentStudent: null
  510. });
  511. nextTick(() => {
  512. registerFormRef.value?.resetFields();
  513. });
  514. };
  515. /** 加载用户信息到报名表单 */
  516. const loadUserInfoToForm = async () => {
  517. try {
  518. const response = await getPersonalInfo();
  519. const studentInfo: StudentMsgForm = response.data;
  520. if (studentInfo) {
  521. registerForm.studentName = studentInfo.name || '';
  522. registerForm.studentNo = studentInfo.studentNo || '';
  523. registerForm.gender = studentInfo.gender || '';
  524. registerForm.phone = studentInfo.phone || '';
  525. registerForm.email = studentInfo.email || '';
  526. registerForm.department = studentInfo.collegeName || '';
  527. registerForm.major = studentInfo.majorName || '';
  528. registerForm.className = studentInfo.className || '';
  529. } else {
  530. proxy?.$modal.msgWarning('未能获取到用户信息,请手动填写报名表单');
  531. }
  532. } catch (error) {
  533. console.error('获取用户信息失败:', error);
  534. proxy?.$modal.msgWarning('获取用户信息失败,请联系管理员或手动填写');
  535. }
  536. };
  537. /** 报名类型变化处理 */
  538. const handleRegistrationTypeChange = async (value: number) => {
  539. if (value === 1) {
  540. // 个人报名,清空团队相关信息并自动填充当前用户信息
  541. registerForm.teamName = undefined;
  542. registerForm.teamDescription = undefined;
  543. registerForm.teamMembers = [];
  544. registerForm.teamMembersNew = [];
  545. registerForm.presidentStudent = null;
  546. // 自动填充当前用户信息
  547. await loadUserInfoToForm();
  548. } else if (value === 2) {
  549. // 团队报名,自动填充当前用户信息作为团队负责人
  550. await loadUserInfoToForm();
  551. // 设置团队负责人为学生对象(用于StudentSelector组件)
  552. // 使用类型断言避免类型错误,因为我们只需要这些字段用于显示
  553. registerForm.presidentStudent = {
  554. id: 0, // 临时ID,实际提交时不需要
  555. name: registerForm.studentName || '',
  556. studentNo: registerForm.studentNo || '',
  557. gender: registerForm.gender || '',
  558. phone: registerForm.phone || '',
  559. email: registerForm.email || '',
  560. collegeId: '', // 必需的字段
  561. collegeName: registerForm.department || '',
  562. majorId: '', // 必需的字段
  563. majorName: registerForm.major || '',
  564. classId: '', // 必需的字段
  565. className: registerForm.className || '',
  566. grade: '', // 必需的字段
  567. studentStatus: '', // 必需的字段
  568. enrollmentYear: 0 // 必需的字段
  569. } as StudentVO;
  570. }
  571. };
  572. /** 团队负责人选择变化处理 */
  573. const handlePresidentChange = async (student: StudentVO | null) => {
  574. if (student) {
  575. // 自动填充负责人信息
  576. registerForm.studentName = student.name || '';
  577. registerForm.studentNo = student.studentNo || '';
  578. registerForm.gender = student.gender || '';
  579. registerForm.phone = student.phone || '';
  580. registerForm.department = student.collegeName || '';
  581. registerForm.major = student.majorName || '';
  582. registerForm.className = student.className || '';
  583. // 获取用户邮箱
  584. try {
  585. const userRes = await getUser(student.id);
  586. registerForm.email = userRes.data?.user?.email || '';
  587. } catch (error) {
  588. console.error('获取用户邮箱失败:', error);
  589. registerForm.email = ''; // 设置为空,让用户手动填写或通过其他方式获取
  590. }
  591. } else {
  592. // 清空负责人信息
  593. registerForm.studentName = undefined;
  594. registerForm.studentNo = undefined;
  595. registerForm.gender = undefined;
  596. registerForm.phone = undefined;
  597. registerForm.email = undefined;
  598. registerForm.department = undefined;
  599. registerForm.major = undefined;
  600. registerForm.className = undefined;
  601. }
  602. };
  603. /** 新版团队成员变化处理 */
  604. const handleTeamMembersNewChange = (members: VicePresidentVO[]) => {
  605. registerForm.teamMembersNew = members;
  606. // 同步到原有字段,用于兼容后端
  607. registerForm.teamMembers = members.map((member) => ({
  608. userId: member.student?.id || 0,
  609. studentNo: member.student?.studentNo || '',
  610. studentName: member.name || member.student?.name || '',
  611. gender: member.student?.gender || '',
  612. phone: member.phone || member.student?.phone || '',
  613. email: '', // VicePresidentVO没有email字段,设置为空
  614. department: member.dept || member.student?.collegeName || '',
  615. major: member.major || member.student?.majorName || '',
  616. className: member.class || member.student?.className || '',
  617. isLeader: 0 // 团队成员,非负责人
  618. }));
  619. // 验证团队人数限制
  620. const totalMembers = members.length + 1; // 包含负责人
  621. if (selectedActivity.value.teamSizeMin && totalMembers < selectedActivity.value.teamSizeMin) {
  622. proxy?.$modal.msgWarning(`团队人数不能少于${selectedActivity.value.teamSizeMin}人`);
  623. }
  624. if (selectedActivity.value.teamSizeMax && totalMembers > selectedActivity.value.teamSizeMax) {
  625. proxy?.$modal.msgWarning(`团队人数不能超过${selectedActivity.value.teamSizeMax}人`);
  626. }
  627. };
  628. /** 团队成员变化处理(兼容原有方法) */
  629. const handleTeamMembersChange = (members: TeamMemberInfo[]) => {
  630. registerForm.teamMembers = members;
  631. // 验证团队人数限制
  632. const totalMembers = members.length + 1; // 包含负责人
  633. if (selectedActivity.value.teamSizeMin && totalMembers < selectedActivity.value.teamSizeMin) {
  634. proxy?.$modal.msgWarning(`团队人数不能少于${selectedActivity.value.teamSizeMin}人`);
  635. }
  636. if (selectedActivity.value.teamSizeMax && totalMembers > selectedActivity.value.teamSizeMax) {
  637. proxy?.$modal.msgWarning(`团队人数不能超过${selectedActivity.value.teamSizeMax}人`);
  638. }
  639. };
  640. /** 提交报名 */
  641. const submitRegister = () => {
  642. registerFormRef.value?.validate(async (valid: boolean) => {
  643. if (valid) {
  644. // 团队报名时验证人数限制
  645. if (registerForm.registrationType === 2) {
  646. const totalMembers = (registerForm.teamMembersNew?.length || 0) + 1;
  647. if (selectedActivity.value.teamSizeMin && totalMembers < selectedActivity.value.teamSizeMin) {
  648. proxy?.$modal.msgError(`团队人数不能少于${selectedActivity.value.teamSizeMin}人,当前${totalMembers}人`);
  649. return;
  650. }
  651. if (selectedActivity.value.teamSizeMax && totalMembers > selectedActivity.value.teamSizeMax) {
  652. proxy?.$modal.msgError(`团队人数不能超过${selectedActivity.value.teamSizeMax}人,当前${totalMembers}人`);
  653. return;
  654. }
  655. }
  656. registerLoading.value = true;
  657. try {
  658. // 根据报名类型准备不同的提交数据
  659. const submitData: any = {
  660. activityId: registerForm.activityId,
  661. registrationType: registerForm.registrationType,
  662. studentName: registerForm.studentName,
  663. studentNo: registerForm.studentNo,
  664. gender: registerForm.gender,
  665. phone: registerForm.phone,
  666. email: registerForm.email,
  667. department: registerForm.department,
  668. major: registerForm.major,
  669. className: registerForm.className,
  670. remark: registerForm.remark
  671. };
  672. // 根据活动的审核设置自动设置报名状态
  673. // 如果活动设置为不需要审核(needApproval = 0),则自动设置为通过状态(approvalStatus = 1)
  674. // 如果活动设置为需要审核(needApproval = 1),则保持默认的待审核状态(approvalStatus = 0)
  675. if (selectedActivity.value.needApproval === 0) {
  676. submitData.approvalStatus = 1; // 自动通过
  677. } else {
  678. submitData.approvalStatus = 0; // 待审核
  679. }
  680. // 只有团队报名时才发送团队相关字段
  681. if (registerForm.registrationType === 2) {
  682. submitData.teamName = registerForm.teamName;
  683. submitData.teamDescription = registerForm.teamDescription;
  684. // 将团队成员数组转换为JSON字符串(后端期望String类型)
  685. if (registerForm.teamMembers && Array.isArray(registerForm.teamMembers)) {
  686. submitData.teamMembers = JSON.stringify(registerForm.teamMembers);
  687. } else {
  688. submitData.teamMembers = JSON.stringify([]);
  689. }
  690. }
  691. // 个人报名时不发送任何团队相关字段
  692. await registerActivity(submitData);
  693. // 根据审核设置显示不同的成功消息
  694. if (selectedActivity.value.needApproval === 0) {
  695. proxy?.$modal.msgSuccess('报名成功!您的报名已自动通过审核');
  696. } else {
  697. proxy?.$modal.msgSuccess('报名成功,请等待审核');
  698. }
  699. registerDialog.visible = false;
  700. // 更新报名状态
  701. userRegistrationStatus.value[selectedActivity.value.id as number] = true;
  702. } catch (error) {
  703. console.error('报名失败:', error);
  704. const errorMsg = (error as any)?.response?.data?.msg || (error as any)?.message || '报名失败,请稍后重试';
  705. proxy?.$modal.msgError(errorMsg);
  706. } finally {
  707. registerLoading.value = false;
  708. }
  709. }
  710. });
  711. };
  712. /** 判断是否可以报名 */
  713. const canRegister = (activity: StglActivityVo): boolean => {
  714. // 报名状态为0(未开始)或1(报名中),且用户未报名
  715. return [0, 1].includes(activity.registrationStatus || 0) && !isRegistered(activity);
  716. };
  717. /** 判断用户是否已报名 */
  718. const isRegistered = (activity: StglActivityVo): boolean => {
  719. return userRegistrationStatus.value[activity.id as number] || false;
  720. };
  721. /** 判断报名类型是否允许 */
  722. const isRegistrationTypeAllowed = (type: number): boolean => {
  723. const activityType = Number(selectedActivity.value.registrationType);
  724. return activityType === 3 || activityType === type; // 3表示两者均可
  725. };
  726. /** 格式化日期 */
  727. const formatDate = (dateStr: string): string => {
  728. if (!dateStr) return '';
  729. const date = new Date(dateStr);
  730. return date.toLocaleString('zh-CN', {
  731. month: '2-digit',
  732. day: '2-digit',
  733. hour: '2-digit',
  734. minute: '2-digit'
  735. });
  736. };
  737. onMounted(() => {
  738. getList();
  739. });
  740. </script>
  741. <style scoped>
  742. /* 页面容器样式 */
  743. .page-container {
  744. padding: 8px;
  745. background-color: #f5f5f5;
  746. }
  747. /* 表格卡片布局 */
  748. .table-card {
  749. margin-top: 0;
  750. border-radius: 12px;
  751. border: 1px solid #e4e7ed;
  752. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  753. }
  754. .table-card :deep(.el-card__body) {
  755. padding: 0px 20px 20px 20px !important;
  756. }
  757. /* 移除按钮栏底部边框 */
  758. .button-card :deep(.el-card__header) {
  759. border-bottom: none !important;
  760. background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
  761. padding: 16px 20px;
  762. }
  763. /* 页面标题 */
  764. .page-title {
  765. font-size: 18px;
  766. font-weight: 600;
  767. color: #303133;
  768. }
  769. /* 分页容器样式 */
  770. .pagination-container {
  771. display: flex;
  772. justify-content: flex-end;
  773. padding: 16px 20px 0 0 !important;
  774. background-color: #fff;
  775. margin-top: 0px !important;
  776. }
  777. /* 搜索按钮容器样式 */
  778. .search-btn-container {
  779. display: flex;
  780. justify-content: flex-end;
  781. margin-top: 10px;
  782. .el-form-item {
  783. margin-bottom: 0 !important;
  784. }
  785. }
  786. </style>