index.vue 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611
  1. <template>
  2. <div class="p-2">
  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" :inline="true" label-width="100px">
  7. <el-form-item label="赛事编号" prop="eventCode">
  8. <el-input v-model="queryParams.eventCode" placeholder="请输入赛事编号" clearable @keyup.enter="handleQuery" />
  9. </el-form-item>
  10. <el-form-item label="赛事名称" prop="eventName">
  11. <el-input v-model="queryParams.eventName" placeholder="请输入赛事名称" clearable @keyup.enter="handleQuery" />
  12. </el-form-item>
  13. <el-form-item label="赛事类型" prop="eventType">
  14. <el-select v-model="queryParams.eventType" placeholder="请选择赛事类型" clearable>
  15. <el-option v-for="dict in game_event_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  16. </el-select>
  17. </el-form-item>
  18. <el-form-item label="开始时间" prop="startTime">
  19. <el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择开始时间" />
  20. </el-form-item>
  21. <el-form-item label="是否默认赛事" prop="isDefault">
  22. <el-select v-model="queryParams.isDefault" placeholder="请选择是否默认赛事" clearable>
  23. <el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="状态" prop="status">
  27. <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
  28. <el-option v-for="dict in game_event_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  29. </el-select>
  30. </el-form-item>
  31. <el-form-item>
  32. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  33. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  34. </el-form-item>
  35. </el-form>
  36. </el-card>
  37. </div>
  38. </transition>
  39. <el-card shadow="never">
  40. <template #header>
  41. <el-row :gutter="10" class="mb8">
  42. <el-col :span="1.5">
  43. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:gameEvent:add']">新增 </el-button>
  44. </el-col>
  45. <el-col :span="1.5">
  46. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:gameEvent:edit']"
  47. >修改
  48. </el-button>
  49. </el-col>
  50. <el-col :span="1.5">
  51. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:gameEvent:remove']"
  52. >删除
  53. </el-button>
  54. </el-col>
  55. <el-col :span="1.5">
  56. <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:gameEvent:export']">导出 </el-button>
  57. </el-col>
  58. <!-- 新增的操作按钮,基于默认赛事 -->
  59. <el-col :span="1.5">
  60. <el-button type="warning" plain icon="Download" @click="handleDownloadTemplateDefault" v-hasPermi="['system:gameEvent:download']"
  61. >下载模板
  62. </el-button>
  63. </el-col>
  64. <el-col :span="1.5">
  65. <el-button type="info" plain icon="FolderOpened" @click="handleImportRegistrationDefault" v-hasPermi="['system:gameEvent:import']"
  66. >导入报名
  67. </el-button>
  68. </el-col>
  69. <el-col :span="1.5">
  70. <el-button type="success" plain icon="User" @click="handleAddParticipantDefault" v-hasPermi="['system:gameEvent:addParticipant']"
  71. >参赛者
  72. </el-button>
  73. </el-col>
  74. <el-col :span="1.5">
  75. <el-button type="primary" plain icon="Avatar" @click="handleAddRefereeDefault" v-hasPermi="['system:gameEvent:addReferee']"
  76. >裁判
  77. </el-button>
  78. </el-col>
  79. <el-col :span="1.5">
  80. <el-button type="info" plain icon="View" @click="handlePreviewDefault" v-hasPermi="['system:gameEvent:view']">预览 </el-button>
  81. </el-col>
  82. <el-col :span="1.5">
  83. <el-button type="warning" plain icon="DataAnalysis" @click="handleGameDataDefault" v-hasPermi="['system:gameEvent:gameData']"
  84. >排行榜
  85. </el-button>
  86. </el-col>
  87. <el-col :span="1.5">
  88. <el-button type="primary" plain icon="EditPen" @click="handleWriteArticleDefault" v-hasPermi="['system:gameEvent:writeArticle']"
  89. >编写文章
  90. </el-button>
  91. </el-col>
  92. <el-col :span="1.5">
  93. <el-button type="primary" plain icon="Download" @click="handleExportNumberTableDefault" v-hasPermi="['system:gameEvent:numberExport']"
  94. >导出号码对照表
  95. </el-button>
  96. </el-col>
  97. <el-col :span="1.5">
  98. <el-button type="success" plain icon="Postcard" @click="handleGenerateBib" v-hasPermi="['system:gameEvent:numberBib']"
  99. >生成参赛证
  100. </el-button>
  101. </el-col>
  102. <right-toolbar v-model:showSearch="showSearch" :columns="columns" @queryTable="getList"></right-toolbar>
  103. </el-row>
  104. </template>
  105. <el-table v-loading="loading" border :data="gameEventList" @selection-change="handleSelectionChange">
  106. <!-- 第一列:多选列 -->
  107. <el-table-column type="selection" width="55" align="center" />
  108. <!-- 第二列:操作列 -->
  109. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
  110. <template #default="scope">
  111. <el-tooltip content="修改" placement="top">
  112. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:gameEvent:edit']"> 修改 </el-button>
  113. </el-tooltip>
  114. </template>
  115. </el-table-column>
  116. <el-table-column label="赛事id" align="center" prop="eventId" v-if="columns[0].visible" />
  117. <el-table-column label="赛事编号" align="center" prop="eventCode" v-if="columns[1].visible" />
  118. <el-table-column label="赛事名称" align="center" prop="eventName" v-if="columns[2].visible" />
  119. <el-table-column label="赛事类型" align="center" prop="eventType" v-if="columns[3].visible">
  120. <template #default="scope">
  121. <dict-tag :options="game_event_type" :value="scope.row.eventType" />
  122. </template>
  123. </el-table-column>
  124. <el-table-column label="举办地点" align="center" prop="location" v-if="columns[4].visible" />
  125. <el-table-column label="用途" align="center" prop="purpose" v-if="columns[5].visible">
  126. <template #default="scope">
  127. <dict-tag :options="game_event_purpose" :value="scope.row.purpose" />
  128. </template>
  129. </el-table-column>
  130. <el-table-column label="开始时间" align="center" prop="startTime" width="180" v-if="columns[6].visible">
  131. <template #default="scope">
  132. <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
  133. </template>
  134. </el-table-column>
  135. <el-table-column label="结束时间" align="center" prop="endTime" width="180" v-if="columns[7].visible">
  136. <template #default="scope">
  137. <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
  138. </template>
  139. </el-table-column>
  140. <el-table-column label="赛事链接" align="center" prop="eventUrlUrl" width="100" v-if="columns[8].visible">
  141. <template #default="scope">
  142. <image-preview :src="scope.row.eventUrlUrl" :width="50" :height="50" />
  143. </template>
  144. </el-table-column>
  145. <el-table-column label="裁判码" align="center" prop="refereeUrlUrl" width="100" v-if="columns[9].visible">
  146. <template #default="scope">
  147. <image-preview :src="scope.row.refereeUrlUrl" :width="50" :height="50" />
  148. </template>
  149. </el-table-column>
  150. <el-table-column label="举办单位" align="center" prop="unit" v-if="columns[10].visible" />
  151. <el-table-column label="是否默认赛事" align="center" prop="isDefault" v-if="columns[11].visible">
  152. <template #default="scope">
  153. <el-switch v-model="scope.row.isDefault" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
  154. </template>
  155. </el-table-column>
  156. <el-table-column label="创建时间" align="center" prop="createTime" width="180" v-if="columns[12].visible">
  157. <template #default="scope">
  158. <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
  159. </template>
  160. </el-table-column>
  161. <el-table-column label="更新时间" align="center" prop="updateTime" width="180" v-if="columns[13].visible">
  162. <template #default="scope">
  163. <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
  164. </template>
  165. </el-table-column>
  166. <el-table-column label="状态" align="center" prop="status" v-if="columns[14].visible">
  167. <template #default="scope">
  168. <dict-tag :options="game_event_status" :value="scope.row.status" />
  169. </template>
  170. </el-table-column>
  171. <el-table-column label="备注" align="center" prop="remark" v-if="columns[15].visible" />
  172. </el-table>
  173. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  174. </el-card>
  175. <!-- 注册 RefereeForm 组件 -->
  176. <RefereeForm ref="refereeFormRef" />
  177. <!-- 排行榜对话框 -->
  178. <!-- <el-dialog :title="`赛事 ${currentEventId} 排行榜`" v-model="rankingBoardVisible" width="800px" append-to-body>
  179. <RankingBoard :eventId="currentEventId" v-if="rankingBoardVisible" />
  180. </el-dialog> -->
  181. <!-- 文章编写对话框 -->
  182. <el-dialog v-model="articleDialog.visible" :title="articleDialog.title" width="1200px" append-to-body>
  183. <el-tabs v-model="activeTab" @tab-click="handleTabClick">
  184. <el-tab-pane label="竞赛流程" name="competition-process">
  185. <div class="article-form">
  186. <el-form-item label="标题">
  187. <el-input v-model="articleData.competitionProcess.title" placeholder="请输入标题" />
  188. </el-form-item>
  189. <el-form-item label="内容">
  190. <Editor v-model="articleData.competitionProcess.content" :min-height="300" />
  191. </el-form-item>
  192. <el-form-item label="备注">
  193. <el-input v-model="articleData.competitionProcess.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  194. </el-form-item>
  195. </div>
  196. </el-tab-pane>
  197. <el-tab-pane label="竞赛项目" name="competition-items">
  198. <div class="article-form">
  199. <el-form-item label="标题">
  200. <el-input v-model="articleData.competitionItems.title" placeholder="请输入标题" />
  201. </el-form-item>
  202. <el-form-item label="内容">
  203. <Editor v-model="articleData.competitionItems.content" :min-height="300" />
  204. </el-form-item>
  205. <el-form-item label="备注">
  206. <el-input v-model="articleData.competitionItems.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  207. </el-form-item>
  208. </div>
  209. </el-tab-pane>
  210. <el-tab-pane label="活动议程" name="activity-agenda">
  211. <div class="article-form">
  212. <el-form-item label="标题">
  213. <el-input v-model="articleData.activityAgenda.title" placeholder="请输入标题" />
  214. </el-form-item>
  215. <el-form-item label="内容">
  216. <Editor v-model="articleData.activityAgenda.content" :min-height="300" />
  217. </el-form-item>
  218. <el-form-item label="备注">
  219. <el-input v-model="articleData.activityAgenda.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  220. </el-form-item>
  221. </div>
  222. </el-tab-pane>
  223. <el-tab-pane label="项目介绍" name="project-introduction">
  224. <div class="article-form">
  225. <el-form-item label="标题">
  226. <el-input v-model="articleData.projectIntroduction.title" placeholder="请输入标题" />
  227. </el-form-item>
  228. <el-form-item label="内容">
  229. <Editor v-model="articleData.projectIntroduction.content" :min-height="300" />
  230. </el-form-item>
  231. <el-form-item label="备注">
  232. <el-input v-model="articleData.projectIntroduction.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  233. </el-form-item>
  234. </div>
  235. </el-tab-pane>
  236. <el-tab-pane label="竞赛流程" name="competition-flow">
  237. <div class="article-form">
  238. <el-form-item label="标题">
  239. <el-input v-model="articleData.competitionFlow.title" placeholder="请输入标题" />
  240. </el-form-item>
  241. <el-form-item label="内容">
  242. <Editor v-model="articleData.competitionFlow.content" :min-height="300" />
  243. </el-form-item>
  244. <el-form-item label="备注">
  245. <el-input v-model="articleData.competitionFlow.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  246. </el-form-item>
  247. </div>
  248. </el-tab-pane>
  249. <el-tab-pane label="赛事分组" name="event-grouping">
  250. <div class="article-form">
  251. <el-form-item label="标题">
  252. <el-input v-model="articleData.eventGrouping.title" placeholder="请输入标题" />
  253. </el-form-item>
  254. <el-form-item label="内容">
  255. <Editor v-model="articleData.eventGrouping.content" :min-height="300" />
  256. </el-form-item>
  257. <el-form-item label="备注">
  258. <el-input v-model="articleData.eventGrouping.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  259. </el-form-item>
  260. </div>
  261. </el-tab-pane>
  262. <el-tab-pane label="运动员号码簿" name="athlete-handbook">
  263. <div class="article-form">
  264. <el-form-item label="标题">
  265. <el-input v-model="articleData.athleteHandbook.title" placeholder="请输入标题" />
  266. </el-form-item>
  267. <el-form-item label="内容">
  268. <Editor v-model="articleData.athleteHandbook.content" :min-height="300" />
  269. </el-form-item>
  270. <el-form-item label="备注">
  271. <el-input v-model="articleData.athleteHandbook.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  272. </el-form-item>
  273. </div>
  274. </el-tab-pane>
  275. <el-tab-pane label="项目场地" name="project-venue">
  276. <div class="article-form">
  277. <el-form-item label="标题">
  278. <el-input v-model="articleData.projectVenue.title" placeholder="请输入标题" />
  279. </el-form-item>
  280. <el-form-item label="内容">
  281. <Editor v-model="articleData.projectVenue.content" :min-height="300" />
  282. </el-form-item>
  283. <el-form-item label="备注">
  284. <el-input v-model="articleData.projectVenue.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  285. </el-form-item>
  286. </div>
  287. </el-tab-pane>
  288. <el-tab-pane label="交通指示" name="traffic-guide">
  289. <div class="article-form">
  290. <el-form-item label="标题">
  291. <el-input v-model="articleData.trafficGuide.title" placeholder="请输入标题" />
  292. </el-form-item>
  293. <el-form-item label="内容">
  294. <Editor v-model="articleData.trafficGuide.content" :min-height="300" />
  295. </el-form-item>
  296. <el-form-item label="备注">
  297. <el-input v-model="articleData.trafficGuide.remark" placeholder="请输入备注" type="textarea" :rows="3" />
  298. </el-form-item>
  299. </div>
  300. </el-tab-pane>
  301. </el-tabs>
  302. <template #footer>
  303. <div class="dialog-footer">
  304. <el-button @click="handleCloseArticleDialog">取 消</el-button>
  305. <el-button type="primary" @click="handleSaveArticle">保 存</el-button>
  306. </div>
  307. </template>
  308. </el-dialog>
  309. <!-- 用户导入对话框 -->
  310. <el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
  311. <el-upload
  312. ref="uploadRef"
  313. :limit="1"
  314. accept=".xlsx"
  315. :headers="upload.headers"
  316. :action="upload.url + '?updateSupport=' + upload.updateSupport"
  317. :disabled="upload.isUploading"
  318. :on-progress="handleFileUploadProgress"
  319. :on-success="handleFileSuccess"
  320. :auto-upload="false"
  321. drag
  322. >
  323. <el-icon class="el-icon--upload">
  324. <i-ep-upload-filled />
  325. </el-icon>
  326. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  327. <template #tip>
  328. <div class="text-center el-upload__tip">
  329. <span>仅允许导入.xlsx格式文件。</span>
  330. </div>
  331. </template>
  332. </el-upload>
  333. <template #footer>
  334. <div class="dialog-footer">
  335. <el-button type="primary" @click="submitFileForm">确 定</el-button>
  336. <el-button @click="upload.open = false">取 消</el-button>
  337. </div>
  338. </template>
  339. </el-dialog>
  340. <!-- 生成参赛证对话框 -->
  341. <el-dialog v-model="bibDialog.visible" title="生成参赛证" width="800px" append-to-body @close="handleCloseBibDialog">
  342. <div class="bib-generator">
  343. <el-row :gutter="20">
  344. <!-- 左侧配置面板 -->
  345. <el-col :span="12">
  346. <el-form :model="bibForm" label-width="100px">
  347. <el-form-item label="背景图片">
  348. <el-upload ref="bgUploadRef" :limit="1" :auto-upload="false" :on-change="handleBgImageChange" accept="image/*" drag>
  349. <el-icon class="el-icon--upload">
  350. <i-ep-upload-filled />
  351. </el-icon>
  352. <div class="el-upload__text">拖拽背景图片到此处,或<em>点击上传</em></div>
  353. <div class="el-upload__tip">建议尺寸:842×595px (横向A4比例)</div>
  354. </el-upload>
  355. </el-form-item>
  356. <el-form-item label="Logo图片">
  357. <el-upload ref="logoUploadRef" :limit="1" :auto-upload="false" :on-change="handleLogoImageChange" accept="image/*" drag>
  358. <el-icon class="el-icon--upload">
  359. <i-ep-upload-filled />
  360. </el-icon>
  361. <div class="el-upload__text">拖拽Logo图片到此处,或<em>点击上传</em></div>
  362. <div class="el-upload__tip">建议尺寸:80×80px</div>
  363. </el-upload>
  364. </el-form-item>
  365. <el-form-item label="字体设置">
  366. <div style="display: flex; gap: 15px; align-items: center">
  367. <el-select v-model="bibForm.fontName" placeholder="字体" style="width: 100px">
  368. <el-option label="黑体" value="simhei"></el-option>
  369. <el-option label="宋体" value="simsun"></el-option>
  370. <el-option label="微软雅黑" value="yahei"></el-option>
  371. </el-select>
  372. <el-input-number v-model="bibForm.fontSize" :min="38" :max="198" placeholder="字体大小" style="width: 140px"></el-input-number>
  373. <el-color-picker v-model="bibForm.fontColor" @change="handleFontColorChange"></el-color-picker>
  374. </div>
  375. </el-form-item>
  376. </el-form>
  377. </el-col>
  378. <!-- 右侧预览面板 -->
  379. <el-col :span="12">
  380. <div class="preview-container" ref="previewContainer">
  381. <div class="preview-canvas" :style="{ backgroundImage: bgImageUrl ? `url(${bgImageUrl})` : 'none' }">
  382. <!-- Logo元素 -->
  383. <div
  384. v-if="logoImageUrl"
  385. class="draggable-element logo-element"
  386. :style="{
  387. left: bibForm.logoX + 'px',
  388. top: bibForm.logoY + 'px'
  389. }"
  390. @mousedown="startDrag($event, 'logo')"
  391. >
  392. <img :src="logoImageUrl" alt="Logo" style="max-width: 80px; max-height: 80px" />
  393. </div>
  394. <!-- 示例条形码 -->
  395. <div
  396. class="draggable-element barcode-element"
  397. :style="{
  398. left: bibForm.qRCodeX + 'px',
  399. top: bibForm.qRCodeY + 'px'
  400. }"
  401. @mousedown="startDrag($event, 'barcode')"
  402. >
  403. <svg
  404. t="1755833734016"
  405. class="icon"
  406. viewBox="0 0 1024 1024"
  407. version="1.1"
  408. xmlns="http://www.w3.org/2000/svg"
  409. p-id="2399"
  410. width="32"
  411. height="32"
  412. >
  413. <path
  414. d="M540.9 866h59v59h-59v-59zM422.8 423.1V98.4H98.1v324.8h59v59h59v-59h206.7z m-265.7-59V157.4h206.7v206.7H157.1z m0 0"
  415. p-id="2400"
  416. ></path>
  417. <path
  418. d="M216.2 216.4h88.6V305h-88.6v-88.6zM600 98.4v324.8h324.8V98.4H600z m265.7 265.7H659V157.4h206.7v206.7z m0 0"
  419. p-id="2401"
  420. ></path>
  421. <path
  422. d="M718.1 216.4h88.6V305h-88.6v-88.6zM216.2 718.3h88.6v88.6h-88.6v-88.6zM98.1 482.2h59v59h-59v-59z m118.1 0h59.1v59h-59.1v-59z m0 0"
  423. p-id="2402"
  424. ></path>
  425. <path
  426. d="M275.2 600.2H98.1V925h324.8V600.2h-88.6v-59h-59v59z m88.6 59.1V866H157.1V659.3h206.7z m118.1-531.4h59v88.6h-59v-88.6z m0 147.6h59v59h-59v-59zM659 482.2H540.9v-88.6h-59v88.6H334.3v59H600v59h59v-118z m0 118h59.1v59H659v-59z m-177.1 0h59v88.6h-59v-88.6z m0 147.7h59V866h-59V747.9zM600 688.8h59V866h-59V688.8z m177.1-88.6h147.6v59H777.1v-59z m88.6-118h59v59h-59v-59z m-147.6 0h118.1v59H718.1v-59z m0 206.6h59v59h-59v-59z m147.6 59.1h-29.5v59h59v-59h29.5v-59h-59v59z m-147.6 59h59V866h-59v-59.1z m59 59.1h147.6v59H777.1v-59z m0 0"
  427. p-id="2403"
  428. ></path>
  429. </svg>
  430. </div>
  431. <!-- 赛事名称预览 -->
  432. <div
  433. class="event-name-preview"
  434. :style="{
  435. fontSize: 32 + 'px',
  436. color: 'black',
  437. fontFamily: '黑体'
  438. }"
  439. >
  440. 赛事名称
  441. </div>
  442. <!-- 示例数字 1234 -->
  443. <div
  444. class="draggable-element number-element"
  445. :style="{
  446. left: '50%',
  447. top: '50%',
  448. transform: 'translate(-50%, -50%)',
  449. fontSize: bibForm.fontSize + 'px',
  450. color: bibForm.fontColorHex,
  451. fontFamily: bibForm.fontName
  452. }"
  453. >
  454. 1234
  455. </div>
  456. </div>
  457. </div>
  458. </el-col>
  459. </el-row>
  460. </div>
  461. <template #footer>
  462. <div class="dialog-footer">
  463. <el-button @click="handleCloseBibDialog">取 消</el-button>
  464. <el-button type="primary" @click="handleGenerateBibFile" :loading="bibDialog.loading">生成参赛证</el-button>
  465. </div>
  466. </template>
  467. </el-dialog>
  468. </div>
  469. </template>
  470. <script setup name="GameEvent" lang="ts">
  471. import {
  472. listGameEvent,
  473. changeEventDefault,
  474. delGameEvent,
  475. addGameEvent,
  476. updateGameEvent,
  477. generateNumberTable,
  478. generateBib,
  479. type GenerateBibBo
  480. } from '@/api/system/gameEvent';
  481. import { GameEventVO, GameEventQuery, GameEventForm } from '@/api/system/gameEvent/types';
  482. import { getEventMdByEventAndType, editEventMd } from '@/api/system/eventMd';
  483. import { EventMdVO, EventMdForm } from '@/api/system/eventMd/types';
  484. import { useRouter } from 'vue-router';
  485. import { ref, nextTick } from 'vue';
  486. import RefereeForm from '@/views/system/gameEvent/RefereeForm.vue';
  487. import RankingBoard from './RankingBoard.vue';
  488. import Editor from '@/components/Editor/index.vue';
  489. import { useTagsViewStore } from '@/store/modules/tagsView';
  490. import { globalHeaders } from '@/utils/request';
  491. import { useGameEventStore } from '@/store/modules/gameEvent';
  492. const router = useRouter();
  493. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  494. const { game_event_type, game_event_status, sys_yes_no, game_event_purpose } = toRefs<any>(
  495. proxy?.useDict('game_event_type', 'game_event_status', 'sys_yes_no', 'game_event_purpose')
  496. );
  497. // 定义 RefereeForm 组件的类型
  498. interface RefereeFormInstance {
  499. openDialog: (eventId: string) => void;
  500. }
  501. const refereeFormRef = ref<(InstanceType<typeof RefereeForm> & RefereeFormInstance) | null>(null);
  502. const gameEventList = ref<GameEventVO[]>([]);
  503. const buttonLoading = ref(false);
  504. const loading = ref(true);
  505. const showSearch = ref(true);
  506. const ids = ref<Array<string | number>>([]);
  507. const single = ref(true);
  508. const multiple = ref(true);
  509. const total = ref(0);
  510. // 列显隐数据
  511. const columns = ref<FieldOption[]>([
  512. { key: 0, label: '赛事id', visible: false },
  513. { key: 1, label: '赛事编号', visible: true },
  514. { key: 2, label: '赛事名称', visible: true },
  515. { key: 3, label: '赛事类型', visible: true },
  516. { key: 4, label: '举办地点', visible: true },
  517. { key: 5, label: '用途', visible: true },
  518. { key: 6, label: '开始时间', visible: true },
  519. { key: 7, label: '结束时间', visible: true },
  520. { key: 8, label: '赛事链接', visible: true },
  521. { key: 9, label: '裁判码', visible: true },
  522. { key: 10, label: '举办单位', visible: true },
  523. { key: 11, label: '是否默认赛事', visible: true },
  524. { key: 12, label: '创建时间', visible: true },
  525. { key: 13, label: '更新时间', visible: true },
  526. { key: 14, label: '状态', visible: true },
  527. { key: 15, label: '备注', visible: true }
  528. ]);
  529. const queryFormRef = ref<ElFormInstance>();
  530. const gameEventFormRef = ref<ElFormInstance>();
  531. const dialog = reactive<DialogOption>({
  532. visible: false,
  533. title: ''
  534. });
  535. const uploadRef = ref<ElUploadInstance>();
  536. /*** 用户导入参数 */
  537. const upload = reactive<ImportOption>({
  538. // 是否显示弹出层(用户导入)
  539. open: false,
  540. // 弹出层标题(用户导入)
  541. title: '',
  542. // 是否禁用上传
  543. isUploading: false,
  544. // 是否更新已经存在的用户数据
  545. updateSupport: 0,
  546. // 设置上传的请求头部
  547. headers: globalHeaders(),
  548. // 上传的地址
  549. url: import.meta.env.VITE_APP_BASE_API + '/system/enroll/importData'
  550. });
  551. const initFormData: GameEventForm = {
  552. eventId: undefined,
  553. eventCode: undefined,
  554. eventName: undefined,
  555. eventType: undefined,
  556. location: undefined,
  557. purpose: undefined,
  558. startTime: undefined,
  559. endTime: undefined,
  560. eventUrl: undefined,
  561. refereeUrl: undefined,
  562. registerUrl: undefined,
  563. unit: undefined,
  564. isDefault: undefined,
  565. status: undefined,
  566. remark: undefined
  567. };
  568. const data = reactive<PageData<GameEventForm, GameEventQuery>>({
  569. form: { ...initFormData },
  570. queryParams: {
  571. pageNum: 1,
  572. pageSize: 10,
  573. eventCode: undefined,
  574. eventName: undefined,
  575. eventType: undefined,
  576. purpose: undefined,
  577. startTime: undefined,
  578. isDefault: undefined,
  579. status: undefined,
  580. params: {},
  581. orderByColumn: '',
  582. isAsc: ''
  583. },
  584. rules: {
  585. eventCode: [{ required: true, message: '赛事编号不能为空', trigger: 'blur' }],
  586. eventName: [{ required: true, message: '赛事名称不能为空', trigger: 'blur' }],
  587. eventType: [{ required: true, message: '赛事类型不能为空', trigger: 'change' }]
  588. }
  589. });
  590. const { queryParams, form, rules } = toRefs(data);
  591. // 使用gameEvent store
  592. const gameEventStore = useGameEventStore();
  593. /** 查询赛事基本信息列表 */
  594. const getList = async () => {
  595. loading.value = true;
  596. const res = await listGameEvent(queryParams.value);
  597. gameEventList.value = res.rows;
  598. total.value = res.total;
  599. loading.value = false;
  600. };
  601. /** 取消按钮 */
  602. const cancel = () => {
  603. reset();
  604. dialog.visible = false;
  605. };
  606. /** 表单重置 */
  607. const reset = () => {
  608. form.value = { ...initFormData };
  609. gameEventFormRef.value?.resetFields();
  610. };
  611. /** 搜索按钮操作 */
  612. const handleQuery = () => {
  613. queryParams.value.pageNum = 1;
  614. getList();
  615. };
  616. /** 重置按钮操作 */
  617. const resetQuery = () => {
  618. queryFormRef.value?.resetFields();
  619. handleQuery();
  620. };
  621. /** 多选框选中数据 */
  622. const handleSelectionChange = (selection: GameEventVO[]) => {
  623. ids.value = selection.map((item) => item.eventId);
  624. single.value = selection.length != 1;
  625. multiple.value = !selection.length;
  626. };
  627. /** 新增按钮操作 */
  628. const handleAdd = () => {
  629. router.push('/system/gameEvent/add');
  630. };
  631. /** 修改按钮操作 */
  632. const handleUpdate = async (row?: GameEventVO) => {
  633. const _eventId = row?.eventId || ids.value[0];
  634. router.push(`/system/gameEvent/edit/${_eventId}`);
  635. };
  636. /** 提交按钮 */
  637. const submitForm = () => {
  638. gameEventFormRef.value?.validate(async (valid: boolean) => {
  639. if (valid) {
  640. buttonLoading.value = true;
  641. if (form.value.eventId) {
  642. await updateGameEvent(form.value).finally(() => (buttonLoading.value = false));
  643. } else {
  644. await addGameEvent(form.value).finally(() => (buttonLoading.value = false));
  645. }
  646. proxy?.$modal.msgSuccess('操作成功');
  647. dialog.visible = false;
  648. await getList();
  649. }
  650. });
  651. };
  652. /** 删除按钮操作 */
  653. const handleDelete = async (row?: GameEventVO) => {
  654. const _ids = row?.eventId || ids.value;
  655. if (!_ids || (Array.isArray(_ids) && _ids.length === 0)) {
  656. proxy?.$modal.msgError('请选择要删除的赛事');
  657. return;
  658. }
  659. // 如果是单个删除,需要检查时间限制
  660. if (row) {
  661. const endTime = new Date(row.endTime);
  662. const currentTime = new Date();
  663. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  664. if (daysDiff < 100) {
  665. proxy?.$modal.msgError(`该赛事结束时间不足100天,无法删除。距离可删除时间还有 ${100 - daysDiff} 天。`);
  666. return;
  667. }
  668. await proxy?.$modal.confirm('是否确认删除赛事编号为"' + row.eventCode + '"的数据项?').finally(() => (loading.value = false));
  669. await delGameEvent(row.eventId);
  670. proxy?.$modal.msgSuccess('删除成功');
  671. } else {
  672. // 批量删除
  673. const selectedEvents = gameEventList.value.filter((event) => ids.value.includes(event.eventId));
  674. const invalidEvents = selectedEvents.filter((event) => {
  675. const endTime = new Date(event.endTime);
  676. const currentTime = new Date();
  677. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  678. return daysDiff < 100;
  679. });
  680. if (invalidEvents.length > 0) {
  681. const eventNames = invalidEvents.map((event) => event.eventName).join('、');
  682. proxy?.$modal.msgError(`以下赛事结束时间不足100天,无法删除:${eventNames}`);
  683. return;
  684. }
  685. const eventCodes = selectedEvents.map((event) => event.eventCode).join('、');
  686. await proxy?.$modal.confirm(`是否确认删除以下赛事:${eventCodes}?`).finally(() => (loading.value = false));
  687. await delGameEvent(ids.value);
  688. proxy?.$modal.msgSuccess(`成功删除 ${selectedEvents.length} 个赛事`);
  689. }
  690. await getList();
  691. };
  692. /** 判断是否可以删除赛事 */
  693. const canDelete = (row: GameEventVO): boolean => {
  694. const endTime = new Date(row.endTime);
  695. const currentTime = new Date();
  696. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  697. return daysDiff >= 100;
  698. };
  699. /** 获取删除按钮的提示信息 */
  700. const getDeleteTooltip = (row: GameEventVO): string => {
  701. if (canDelete(row)) {
  702. return '删除';
  703. } else {
  704. const endTime = new Date(row.endTime);
  705. const currentTime = new Date();
  706. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  707. return `赛事结束时间不足100天,无法删除。距离可删除时间还有 ${100 - daysDiff} 天。`;
  708. }
  709. };
  710. /** 导出按钮操作 */
  711. const handleExport = () => {
  712. proxy?.download(
  713. 'system/gameEvent/export',
  714. {
  715. ...queryParams.value
  716. },
  717. `gameEvent_${new Date().getTime()}.xlsx`
  718. );
  719. };
  720. /* 下载模板 */
  721. const handleDownloadTemplate = (row: GameEventVO) => {
  722. proxy?.download(
  723. 'system/enroll/importTemplate',
  724. {
  725. eventId: row.eventId
  726. },
  727. `event_enroll_template_${new Date().getTime()}.xlsx`
  728. );
  729. };
  730. // 导入报名表逻辑
  731. const handleImportRegistration = (row) => {
  732. upload.url = import.meta.env.VITE_APP_BASE_API + `/system/enroll/importData/${row.eventId}`;
  733. upload.title = '报名表导入';
  734. upload.open = true;
  735. };
  736. /**文件上传中处理 */
  737. const handleFileUploadProgress = () => {
  738. upload.isUploading = true;
  739. };
  740. /** 文件上传成功处理 */
  741. const handleFileSuccess = (response: any, file: UploadFile) => {
  742. upload.open = false;
  743. upload.isUploading = false;
  744. uploadRef.value?.handleRemove(file);
  745. ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
  746. dangerouslyUseHTMLString: true
  747. });
  748. getList();
  749. };
  750. /** 提交上传文件 */
  751. function submitFileForm() {
  752. uploadRef.value?.submit();
  753. }
  754. // 添加参赛者操作
  755. const handleAddParticipant = (row: GameEventVO) => {
  756. // 跳转到新增或编辑参赛者信息页面,并传递 eventName 参数
  757. router.push({
  758. name: 'GameEventAthlete',
  759. params: { eventId: row.eventId, eventName: row.eventName }
  760. });
  761. };
  762. // 添加裁判按钮操作 1
  763. const handleAddReferee = async (row: GameEventVO) => {
  764. // 打开裁判表单对话框并传递 eventId
  765. refereeFormRef.value?.openDialog(String(row.eventId));
  766. };
  767. // 预览按钮点击事件
  768. const handlePreview = (row: GameEventVO) => {
  769. // 跳转到赛事详情页面
  770. router.push({
  771. name: 'GameEventDetail',
  772. params: { eventId: row.eventId }
  773. });
  774. };
  775. // 比赛成绩按钮点击事件
  776. const handleGameData = (row: GameEventVO) => {
  777. router.push({
  778. name: 'RankingBoardPage',
  779. params: { eventId: row.eventId }
  780. });
  781. };
  782. // 文章编写相关数据
  783. const articleDialog = reactive({
  784. visible: false,
  785. title: '',
  786. currentEventId: undefined as string | number | undefined
  787. });
  788. const activeTab = ref('competition-process');
  789. // 标签页与类型值的映射关系
  790. const tabTypeMapping: Record<string, number> = {
  791. 'competition-process': 1, // 竞赛流程
  792. 'competition-items': 2, // 竞赛项目
  793. 'activity-agenda': 3, // 活动议程
  794. 'project-introduction': 4, // 项目介绍
  795. 'competition-flow': 5, // 竞赛流程
  796. 'event-grouping': 6, // 赛事分组
  797. 'athlete-handbook': 7, // 运动员号码簿
  798. 'project-venue': 8, // 项目场地
  799. 'traffic-guide': 9 // 交通指示
  800. };
  801. const articleData = reactive({
  802. competitionProcess: { id: undefined, title: '', content: '', remark: '' },
  803. competitionItems: { id: undefined, title: '', content: '', remark: '' },
  804. activityAgenda: { id: undefined, title: '', content: '', remark: '' },
  805. projectIntroduction: { id: undefined, title: '', content: '', remark: '' },
  806. competitionFlow: { id: undefined, title: '', content: '', remark: '' },
  807. eventGrouping: { id: undefined, title: '', content: '', remark: '' },
  808. athleteHandbook: { id: undefined, title: '', content: '', remark: '' },
  809. projectVenue: { id: undefined, title: '', content: '', remark: '' },
  810. trafficGuide: { id: undefined, title: '', content: '', remark: '' }
  811. });
  812. // 打开排行榜组件并传递赛事ID
  813. // const openRankingBoard = (eventId: string) => {
  814. // currentEventId.value = eventId;
  815. // rankingBoardVisible.value = true;
  816. // };
  817. /** 状态修改 */
  818. const handleStatusChange = async (row: GameEventVO) => {
  819. const text = row.isDefault === '0' ? '启用' : '停用';
  820. try {
  821. await proxy?.$modal.confirm('确认要"' + text + '""' + row.eventName + '"为默认赛事吗?');
  822. await changeEventDefault(row.eventId, row.isDefault);
  823. await getList();
  824. // 更新全局默认赛事信息
  825. await gameEventStore.fetchDefaultEvent();
  826. // localStorage.setItem('defaultEventId', row.eventId);
  827. proxy?.$modal.msgSuccess(text + '成功');
  828. // 刷新当前标签页
  829. await useTagsViewStore().delOthersViews(router.currentRoute.value);
  830. } catch {
  831. return;
  832. } finally {
  833. row.isDefault = row.isDefault === '0' ? '1' : '0';
  834. }
  835. };
  836. /** 编写文章按钮操作 */
  837. const handleWriteArticle = async (row: GameEventVO) => {
  838. articleDialog.title = `编写文章 - ${row.eventName}`;
  839. articleDialog.currentEventId = row.eventId;
  840. articleDialog.visible = true;
  841. activeTab.value = 'competition-process';
  842. // 加载默认标签页(竞赛流程)的数据
  843. await loadTabData('competition-process');
  844. };
  845. /** 加载指定标签页的数据 */
  846. const loadTabData = async (tabName: string) => {
  847. const type = tabTypeMapping[tabName];
  848. if (articleDialog.currentEventId && type) {
  849. try {
  850. const response = await getEventMdByEventAndType(articleDialog.currentEventId, type);
  851. const eventMd = response.data;
  852. const dataKey = getDataKeyByTabName(tabName);
  853. if (dataKey && articleData[dataKey]) {
  854. if (eventMd) {
  855. articleData[dataKey].id = eventMd.id;
  856. articleData[dataKey].title = eventMd.title || '';
  857. articleData[dataKey].content = eventMd.content || '';
  858. articleData[dataKey].remark = eventMd.remark || '';
  859. } else {
  860. articleData[dataKey].id = undefined;
  861. articleData[dataKey].title = '';
  862. articleData[dataKey].content = '';
  863. articleData[dataKey].remark = '';
  864. }
  865. }
  866. } catch (error) {
  867. const dataKey = getDataKeyByTabName(tabName);
  868. if (dataKey && articleData[dataKey]) {
  869. articleData[dataKey].id = undefined;
  870. articleData[dataKey].title = '';
  871. articleData[dataKey].content = '';
  872. articleData[dataKey].remark = '';
  873. }
  874. }
  875. }
  876. };
  877. /** 标签页点击事件 */
  878. const handleTabClick = async (tab: any) => {
  879. const tabName = tab.props.name;
  880. await loadTabData(tabName);
  881. };
  882. /** 根据标签页名称获取数据键名 */
  883. const getDataKeyByTabName = (tabName: string): keyof typeof articleData | null => {
  884. const mapping: Record<string, keyof typeof articleData> = {
  885. 'competition-process': 'competitionProcess',
  886. 'competition-items': 'competitionItems',
  887. 'activity-agenda': 'activityAgenda',
  888. 'project-introduction': 'projectIntroduction',
  889. 'competition-flow': 'competitionFlow',
  890. 'event-grouping': 'eventGrouping',
  891. 'athlete-handbook': 'athleteHandbook',
  892. 'project-venue': 'projectVenue',
  893. 'traffic-guide': 'trafficGuide'
  894. };
  895. return mapping[tabName] || null;
  896. };
  897. /** 关闭文章编写对话框 */
  898. const handleCloseArticleDialog = () => {
  899. // 清空所有文章数据
  900. Object.keys(articleData).forEach((key) => {
  901. const dataKey = key as keyof typeof articleData;
  902. articleData[dataKey].id = undefined;
  903. articleData[dataKey].title = '';
  904. articleData[dataKey].content = '';
  905. articleData[dataKey].remark = '';
  906. });
  907. // 重置对话框状态
  908. articleDialog.visible = false;
  909. articleDialog.currentEventId = undefined;
  910. activeTab.value = 'competition-process';
  911. };
  912. /** 保存文章 */
  913. const handleSaveArticle = async () => {
  914. if (!articleDialog.currentEventId) {
  915. proxy?.$modal.msgError('赛事ID不能为空');
  916. return;
  917. }
  918. const currentTabName = activeTab.value;
  919. const type = tabTypeMapping[currentTabName];
  920. const dataKey = getDataKeyByTabName(currentTabName);
  921. if (!dataKey || !articleData[dataKey]) {
  922. proxy?.$modal.msgError('获取当前标签页数据失败');
  923. return;
  924. }
  925. const currentData = articleData[dataKey];
  926. if (!currentData.title?.trim()) {
  927. proxy?.$modal.msgError('标题不能为空');
  928. return;
  929. }
  930. try {
  931. const formData: EventMdForm = {
  932. id: currentData.id,
  933. eventId: articleDialog.currentEventId,
  934. title: currentData.title,
  935. content: currentData.content || '',
  936. type: type,
  937. remark: currentData.remark || ''
  938. };
  939. await editEventMd(formData);
  940. proxy?.$modal.msgSuccess('文章保存成功');
  941. // 重新加载当前标签页数据以获取最新的ID
  942. await loadTabData(currentTabName);
  943. } catch (error) {
  944. console.error('保存文章失败:', error);
  945. proxy?.$modal.msgError('保存文章失败');
  946. }
  947. };
  948. // 基于默认赛事的操作函数
  949. /** 下载模板(默认赛事) */
  950. const handleDownloadTemplateDefault = async () => {
  951. const defaultEvent = gameEventStore.defaultEventInfo;
  952. if (!defaultEvent) {
  953. proxy?.$modal.msgError('请先设置默认赛事');
  954. return;
  955. }
  956. handleDownloadTemplate(defaultEvent);
  957. };
  958. /** 导入报名信息(默认赛事) */
  959. const handleImportRegistrationDefault = async () => {
  960. const defaultEvent = gameEventStore.defaultEventInfo;
  961. if (!defaultEvent) {
  962. proxy?.$modal.msgError('请先设置默认赛事');
  963. return;
  964. }
  965. handleImportRegistration(defaultEvent);
  966. };
  967. /** 添加参赛者(默认赛事) */
  968. const handleAddParticipantDefault = async () => {
  969. const defaultEvent = gameEventStore.defaultEventInfo;
  970. if (!defaultEvent) {
  971. proxy?.$modal.msgError('请先设置默认赛事');
  972. return;
  973. }
  974. handleAddParticipant(defaultEvent);
  975. };
  976. /** 添加裁判(默认赛事) */
  977. const handleAddRefereeDefault = async () => {
  978. const defaultEvent = gameEventStore.defaultEventInfo;
  979. if (!defaultEvent) {
  980. proxy?.$modal.msgError('请先设置默认赛事');
  981. return;
  982. }
  983. handleAddReferee(defaultEvent);
  984. };
  985. /** 预览(默认赛事) */
  986. const handlePreviewDefault = async () => {
  987. const defaultEvent = gameEventStore.defaultEventInfo;
  988. if (!defaultEvent) {
  989. proxy?.$modal.msgError('请先设置默认赛事');
  990. return;
  991. }
  992. handlePreview(defaultEvent);
  993. };
  994. /** 比赛数据(默认赛事) */
  995. const handleGameDataDefault = async () => {
  996. const defaultEvent = gameEventStore.defaultEventInfo;
  997. if (!defaultEvent) {
  998. proxy?.$modal.msgError('请先设置默认赛事');
  999. return;
  1000. }
  1001. handleGameData(defaultEvent);
  1002. };
  1003. /** 编写文章(默认赛事) */
  1004. const handleWriteArticleDefault = async () => {
  1005. const defaultEvent = gameEventStore.defaultEventInfo;
  1006. if (!defaultEvent) {
  1007. proxy?.$modal.msgError('请先设置默认赛事');
  1008. return;
  1009. }
  1010. handleWriteArticle(defaultEvent);
  1011. };
  1012. const handleExportNumberTableDefault = async () => {
  1013. await proxy?.download('system/number/export', {}, `号码对照表_${new Date().getTime()}.xlsx`);
  1014. };
  1015. // 生成参赛证相关
  1016. const bibDialog = reactive({
  1017. visible: false,
  1018. loading: false
  1019. });
  1020. const bibForm = reactive({
  1021. logoX: 50,
  1022. logoY: 50,
  1023. qRCodeX: 100,
  1024. qRCodeY: 200,
  1025. fontName: 'simhei',
  1026. fontSize: 36,
  1027. fontColor: '#000000',
  1028. fontColorHex: '#000000'
  1029. });
  1030. const bgImageFile = ref<File | null>(null);
  1031. const logoImageFile = ref<File | null>(null);
  1032. const bgImageUrl = ref<string>('');
  1033. const logoImageUrl = ref<string>('');
  1034. const previewContainer = ref<HTMLElement>();
  1035. const bgImageDimensions = ref<{ width: number; height: number } | null>(null);
  1036. const bgUploadRef = ref<ElUploadInstance>();
  1037. const logoUploadRef = ref<ElUploadInstance>();
  1038. // 拖拽相关
  1039. const dragState = reactive({
  1040. isDragging: false,
  1041. dragTarget: '',
  1042. startX: 0,
  1043. startY: 0,
  1044. startLeft: 0,
  1045. startTop: 0
  1046. });
  1047. // 生成参赛证按钮处理
  1048. const handleGenerateBib = () => {
  1049. // 强制设置默认值,不使用条件判断(像素单位)
  1050. bibForm.logoX = bibForm.logoX || 50;
  1051. bibForm.logoY = bibForm.logoY || 50;
  1052. bibForm.qRCodeX = bibForm.qRCodeX || 100; // 修正:使用独立的默认值
  1053. bibForm.qRCodeY = bibForm.qRCodeY || 200; // 修正:使用独立的默认值
  1054. bibForm.fontName = bibForm.fontName || 'simhei';
  1055. bibForm.fontSize = bibForm.fontSize || 36;
  1056. bibForm.fontColor = bibForm.fontColor || '#000000';
  1057. bibForm.fontColorHex = bibForm.fontColorHex || '#000000';
  1058. bibDialog.visible = true;
  1059. };
  1060. // 关闭对话框
  1061. const handleCloseBibDialog = () => {
  1062. bibDialog.visible = false;
  1063. resetBibForm();
  1064. // 清除上传的文件
  1065. if (bgUploadRef.value) {
  1066. bgUploadRef.value.clearFiles();
  1067. }
  1068. if (logoUploadRef.value) {
  1069. logoUploadRef.value.clearFiles();
  1070. }
  1071. };
  1072. // 重置表单
  1073. const resetBibForm = () => {
  1074. // 设置默认值(像素单位)
  1075. bibForm.logoX = 50;
  1076. bibForm.logoY = 50;
  1077. bibForm.qRCodeX = 100;
  1078. bibForm.qRCodeY = 200;
  1079. bibForm.fontName = 'simhei';
  1080. bibForm.fontSize = 36;
  1081. bibForm.fontColor = '#000000';
  1082. bibForm.fontColorHex = '#000000';
  1083. bgImageFile.value = null;
  1084. logoImageFile.value = null;
  1085. bgImageUrl.value = '';
  1086. logoImageUrl.value = '';
  1087. // 清除上传的文件
  1088. if (bgUploadRef.value) {
  1089. bgUploadRef.value.clearFiles();
  1090. }
  1091. if (logoUploadRef.value) {
  1092. logoUploadRef.value.clearFiles();
  1093. }
  1094. };
  1095. // 背景图片改变处理
  1096. const handleBgImageChange = async (file: any) => {
  1097. if (file.raw) {
  1098. bgImageFile.value = file.raw;
  1099. const reader = new FileReader();
  1100. reader.onload = (e) => {
  1101. bgImageUrl.value = e.target?.result as string;
  1102. };
  1103. reader.readAsDataURL(file.raw);
  1104. // 获取背景图片的实际尺寸
  1105. try {
  1106. const dimensions = await getImageDimensions(file.raw);
  1107. bgImageDimensions.value = dimensions;
  1108. } catch (error) {
  1109. console.error('获取图片尺寸失败:', error);
  1110. }
  1111. }
  1112. };
  1113. // Logo图片改变处理
  1114. const handleLogoImageChange = (file: any) => {
  1115. if (file.raw) {
  1116. logoImageFile.value = file.raw;
  1117. const reader = new FileReader();
  1118. reader.onload = (e) => {
  1119. logoImageUrl.value = e.target?.result as string;
  1120. };
  1121. reader.readAsDataURL(file.raw);
  1122. }
  1123. };
  1124. // 字体颜色改变处理
  1125. const handleFontColorChange = (color: string) => {
  1126. bibForm.fontColor = color;
  1127. bibForm.fontColorHex = color;
  1128. };
  1129. // 开始拖拽
  1130. const startDrag = (event: MouseEvent, target: string) => {
  1131. event.preventDefault();
  1132. dragState.isDragging = true;
  1133. dragState.dragTarget = target;
  1134. dragState.startX = event.clientX;
  1135. dragState.startY = event.clientY;
  1136. if (target === 'logo') {
  1137. dragState.startLeft = bibForm.logoX;
  1138. dragState.startTop = bibForm.logoY;
  1139. } else if (target === 'barcode') {
  1140. dragState.startLeft = bibForm.qRCodeX;
  1141. dragState.startTop = bibForm.qRCodeY;
  1142. }
  1143. document.addEventListener('mousemove', handleDrag);
  1144. document.addEventListener('mouseup', stopDrag);
  1145. };
  1146. // 处理拖拽
  1147. const handleDrag = (event: MouseEvent) => {
  1148. if (!dragState.isDragging) return;
  1149. const deltaX = event.clientX - dragState.startX;
  1150. const deltaY = event.clientY - dragState.startY;
  1151. if (dragState.dragTarget === 'logo') {
  1152. bibForm.logoX = Math.max(0, dragState.startLeft + deltaX);
  1153. bibForm.logoY = Math.max(0, dragState.startTop + deltaY);
  1154. } else if (dragState.dragTarget === 'barcode') {
  1155. bibForm.qRCodeX = Math.max(0, dragState.startLeft + deltaX);
  1156. bibForm.qRCodeY = Math.max(0, dragState.startTop + deltaY);
  1157. }
  1158. };
  1159. // 停止拖拽
  1160. const stopDrag = () => {
  1161. dragState.isDragging = false;
  1162. dragState.dragTarget = '';
  1163. document.removeEventListener('mousemove', handleDrag);
  1164. document.removeEventListener('mouseup', stopDrag);
  1165. };
  1166. // 坐标转换函数:根据实际背景图片尺寸调整坐标
  1167. const convertCoordinatesWithScale = (x: number, y: number): { x: number; y: number } => {
  1168. // 获取预览容器尺寸,使用更精确的方法
  1169. const container = previewContainer.value;
  1170. const previewWidth = container?.clientWidth || container?.offsetWidth || 400;
  1171. const previewHeight = container?.clientHeight || container?.offsetHeight || 400;
  1172. // 使用实际背景图片尺寸,如果没有则使用默认A4尺寸
  1173. const actualWidth = bgImageDimensions.value?.width || 595;
  1174. const actualHeight = bgImageDimensions.value?.height || 842;
  1175. // 计算实际比例
  1176. const scaleX = actualWidth / previewWidth;
  1177. const scaleY = actualHeight / previewHeight;
  1178. // 微调系数,用于补偿微小偏差
  1179. const fineTuneX = 1.15; // 进一步缩小X坐标,让元素向左移动
  1180. const fineTuneY = 0.9; // 进一步放大Y坐标,让元素向下移动
  1181. // 根据实际效果图,需要调整坐标系统
  1182. // 实际效果显示Logo在左上角,二维码在左下角
  1183. const adjustedX = x * scaleX * fineTuneX;
  1184. const adjustedY = (previewHeight - y) * scaleY * fineTuneY;
  1185. // 添加额外的偏移量调整
  1186. const offsetX = 8; // 向右偏移8pt(进一步减少向右偏移)
  1187. const offsetY = 65; // 向下偏移65pt(进一步增加向下偏移)
  1188. // 根据元素类型进行特殊调整
  1189. let finalX = adjustedX + offsetX;
  1190. let finalY = adjustedY + offsetY;
  1191. // 如果是Logo,进行特殊调整
  1192. if (x < 100 && y < 100) {
  1193. // 假设Logo在左上角区域
  1194. finalX += 5; // Logo额外向右偏移
  1195. finalY -= 10; // Logo额外向下偏移
  1196. }
  1197. // 如果是二维码,进行特殊调整
  1198. if (x > 200 && y > 200) {
  1199. // 假设二维码在右下角区域
  1200. finalX -= 3; // 二维码额外向左偏移
  1201. finalY += 5; // 二维码额外向下偏移
  1202. }
  1203. return {
  1204. x: finalX,
  1205. y: finalY
  1206. };
  1207. };
  1208. // 新增:获取背景图片实际尺寸的函数
  1209. const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
  1210. return new Promise((resolve) => {
  1211. const img = new Image();
  1212. img.onload = () => {
  1213. resolve({ width: img.width, height: img.height });
  1214. };
  1215. img.src = URL.createObjectURL(file);
  1216. });
  1217. };
  1218. // 生成参赛证文件
  1219. const handleGenerateBibFile = async () => {
  1220. if (!bgImageFile.value) {
  1221. proxy?.$modal.msgError('请上传背景图片');
  1222. return;
  1223. }
  1224. bibDialog.loading = true;
  1225. try {
  1226. let qRCodeX = bibForm.qRCodeX;
  1227. let qRCodeY = bibForm.qRCodeY;
  1228. // 如果值为null或undefined,强制使用默认值
  1229. if (qRCodeX === null || qRCodeX === undefined || isNaN(qRCodeX)) {
  1230. qRCodeX = 100;
  1231. console.warn('qRCodeX值异常,使用默认值100px');
  1232. }
  1233. if (qRCodeY === null || qRCodeY === undefined || isNaN(qRCodeY)) {
  1234. qRCodeY = 200;
  1235. console.warn('qRCodeY值异常,使用默认值200px');
  1236. }
  1237. // 获取预览容器的高度用于坐标转换
  1238. const containerHeight = previewContainer.value?.clientHeight || 400;
  1239. // 检查背景图片尺寸
  1240. if (!bgImageDimensions.value) {
  1241. proxy?.$modal.msgWarning('正在获取背景图片尺寸,请稍后再试');
  1242. return;
  1243. }
  1244. // 等待一帧确保所有尺寸都已计算完成
  1245. await nextTick();
  1246. // Logo坐标(左上角)
  1247. const logoCoords = convertCoordinatesWithScale(bibForm.logoX || 50, bibForm.logoY || 50);
  1248. // 二维码坐标(左上角)
  1249. const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
  1250. const bibParams = {
  1251. logoX: logoCoords.x,
  1252. logoY: logoCoords.y,
  1253. qRCodeX: qrCoords.x,
  1254. qRCodeY: qrCoords.y,
  1255. fontName: bibForm.fontName || 'simhei',
  1256. fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
  1257. fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
  1258. };
  1259. // 最后一次检查,确保二维码坐标不为null
  1260. if (bibParams.qRCodeX === null || bibParams.qRCodeY === undefined) {
  1261. bibParams.qRCodeX = 148.75; // 100px * 1.4875
  1262. console.error('最后一次修复:qRCodeX仍为null,设置为100px转换后的值');
  1263. }
  1264. if (bibParams.qRCodeY === null || bibParams.qRCodeY === undefined) {
  1265. bibParams.qRCodeY = 421; // 200px * 2.105
  1266. console.error('最后一次修复:qRCodeY仍为null,设置为200px转换后的值');
  1267. }
  1268. const response = await generateBib(bgImageFile.value, logoImageFile.value, bibParams);
  1269. // 处理文件下载 - response已经是blob数据,不需要再访问.data属性
  1270. const blob = new Blob([response as any], { type: 'application/zip' });
  1271. const url = window.URL.createObjectURL(blob);
  1272. const link = document.createElement('a');
  1273. link.href = url;
  1274. link.download = `参赛证_${new Date().getTime()}.zip`;
  1275. link.click();
  1276. window.URL.revokeObjectURL(url);
  1277. proxy?.$modal.msgSuccess('参赛证生成成功');
  1278. handleCloseBibDialog();
  1279. } catch (error) {
  1280. console.error('生成参赛证失败:', error);
  1281. proxy?.$modal.msgError('生成参赛证失败');
  1282. } finally {
  1283. bibDialog.loading = false;
  1284. }
  1285. };
  1286. onMounted(() => {
  1287. // 获取默认赛事信息
  1288. gameEventStore.fetchDefaultEvent();
  1289. getList();
  1290. });
  1291. // 监听路由变化,当从编辑页返回时检查是否需要刷新列表
  1292. onActivated(() => {
  1293. // 检查是否有需要刷新的标识
  1294. const needRefresh = sessionStorage.getItem('needRefreshGameEventList');
  1295. if (needRefresh === 'true') {
  1296. // 清除标识
  1297. sessionStorage.removeItem('needRefreshGameEventList');
  1298. // 刷新列表数据
  1299. getList();
  1300. }
  1301. });
  1302. </script>
  1303. <style scoped>
  1304. .operation-buttons {
  1305. display: flex;
  1306. flex-direction: column;
  1307. gap: 4px;
  1308. align-items: center;
  1309. min-width: 200px;
  1310. }
  1311. .button-row {
  1312. display: flex;
  1313. gap: 6px;
  1314. justify-content: center;
  1315. align-items: center;
  1316. flex-wrap: wrap;
  1317. }
  1318. .operation-buttons .el-button {
  1319. min-width: 28px;
  1320. min-height: 28px;
  1321. padding: 4px 6px;
  1322. border-radius: 4px;
  1323. transition: all 0.2s ease;
  1324. display: flex;
  1325. align-items: center;
  1326. justify-content: center;
  1327. }
  1328. /* 生成参赛证样式 */
  1329. .bib-generator {
  1330. padding: 20px;
  1331. }
  1332. .bib-generator .el-upload__tip {
  1333. color: #909399;
  1334. font-size: 12px;
  1335. margin-top: 8px;
  1336. text-align: center;
  1337. }
  1338. .preview-container {
  1339. border: 2px dashed #ddd;
  1340. border-radius: 8px;
  1341. min-height: 400px;
  1342. position: relative;
  1343. overflow: hidden;
  1344. }
  1345. .preview-canvas {
  1346. width: 100%;
  1347. height: 400px;
  1348. position: relative;
  1349. background-size: cover;
  1350. background-position: center;
  1351. background-repeat: no-repeat;
  1352. background-color: #f5f5f5;
  1353. }
  1354. .draggable-element {
  1355. position: absolute;
  1356. cursor: move;
  1357. user-select: none;
  1358. z-index: 10;
  1359. }
  1360. .draggable-element:hover {
  1361. opacity: 0.8;
  1362. }
  1363. .logo-element {
  1364. border: 2px dashed transparent;
  1365. }
  1366. .logo-element:hover {
  1367. border-color: #409eff;
  1368. }
  1369. .barcode-element {
  1370. border: 2px dashed transparent;
  1371. padding: 5px;
  1372. }
  1373. .barcode-element:hover {
  1374. border-color: #67c23a;
  1375. background-color: rgba(103, 194, 58, 0.1);
  1376. }
  1377. .number-element {
  1378. font-weight: bold;
  1379. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
  1380. border: 2px dashed transparent;
  1381. padding: 5px;
  1382. white-space: nowrap;
  1383. user-select: none;
  1384. pointer-events: none;
  1385. }
  1386. .event-name-preview {
  1387. position: absolute;
  1388. top: 20px;
  1389. left: 50%;
  1390. transform: translateX(-50%);
  1391. font-weight: bold;
  1392. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
  1393. user-select: none;
  1394. pointer-events: none;
  1395. z-index: 5;
  1396. }
  1397. .operation-buttons .el-button:hover {
  1398. transform: translateY(-1px);
  1399. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
  1400. }
  1401. .operation-buttons .el-button[disabled] {
  1402. opacity: 0.5;
  1403. cursor: not-allowed;
  1404. }
  1405. .operation-buttons .el-button[disabled]:hover {
  1406. transform: none;
  1407. box-shadow: none;
  1408. }
  1409. /* 为不同类型的按钮设置不同的颜色主题 */
  1410. .operation-buttons .el-button--primary {
  1411. color: #409eff;
  1412. }
  1413. .operation-buttons .el-button--danger {
  1414. color: #f56c6c;
  1415. }
  1416. .operation-buttons .el-button--warning {
  1417. color: #e6a23c;
  1418. }
  1419. .operation-buttons .el-button--info {
  1420. color: #909399;
  1421. }
  1422. .operation-buttons .el-button--success {
  1423. color: #67c23a;
  1424. }
  1425. /* 响应式设计 */
  1426. @media (max-width: 1400px) {
  1427. .operation-buttons {
  1428. min-width: 180px;
  1429. }
  1430. .button-row {
  1431. gap: 4px;
  1432. }
  1433. .operation-buttons .el-button {
  1434. min-width: 24px;
  1435. min-height: 24px;
  1436. padding: 3px 5px;
  1437. }
  1438. }
  1439. @media (max-width: 1200px) {
  1440. .operation-buttons {
  1441. min-width: 160px;
  1442. gap: 2px;
  1443. }
  1444. .button-row {
  1445. gap: 2px;
  1446. }
  1447. .operation-buttons .el-button {
  1448. min-width: 22px;
  1449. min-height: 22px;
  1450. padding: 2px 4px;
  1451. }
  1452. }
  1453. </style>