index.vue 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605
  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="microsoft-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 width="100" height="30" viewBox="0 0 100 30">
  404. <rect x="0" y="0" width="2" height="30" fill="black" />
  405. <rect x="4" y="0" width="1" height="30" fill="black" />
  406. <rect x="7" y="0" width="3" height="30" fill="black" />
  407. <rect x="12" y="0" width="1" height="30" fill="black" />
  408. <rect x="15" y="0" width="2" height="30" fill="black" />
  409. <rect x="19" y="0" width="1" height="30" fill="black" />
  410. <rect x="22" y="0" width="3" height="30" fill="black" />
  411. <rect x="27" y="0" width="2" height="30" fill="black" />
  412. <rect x="31" y="0" width="1" height="30" fill="black" />
  413. <rect x="34" y="0" width="2" height="30" fill="black" />
  414. <rect x="37" y="0" width="2" height="30" fill="black" />
  415. <rect x="42" y="0" width="2" height="30" fill="black" />
  416. <rect x="46" y="0" width="2" height="30" fill="black" />
  417. <rect x="49" y="0" width="2" height="30" fill="black" />
  418. <rect x="50" y="0" width="2" height="30" fill="black" />
  419. <rect x="52" y="0" width="2" height="30" fill="black" />
  420. <rect x="54" y="0" width="2" height="30" fill="black" />
  421. <rect x="58" y="0" width="2" height="30" fill="black" />
  422. <rect x="60" y="0" width="2" height="30" fill="black" />
  423. </svg>
  424. </div>
  425. <!-- 赛事名称预览 -->
  426. <div
  427. class="event-name-preview"
  428. :style="{
  429. fontSize: Math.round(bibForm.fontSize * 0.8) + 'px',
  430. color: bibForm.fontColorHex,
  431. fontFamily: bibForm.fontName
  432. }"
  433. >
  434. 赛事名称
  435. </div>
  436. <!-- 示例数字 1234 -->
  437. <div
  438. class="draggable-element number-element"
  439. :style="{
  440. left: '50%',
  441. top: '50%',
  442. transform: 'translate(-50%, -50%)',
  443. fontSize: bibForm.fontSize + 'px',
  444. color: bibForm.fontColorHex,
  445. fontFamily: bibForm.fontName
  446. }"
  447. >
  448. 1234
  449. </div>
  450. </div>
  451. </div>
  452. </el-col>
  453. </el-row>
  454. </div>
  455. <template #footer>
  456. <div class="dialog-footer">
  457. <el-button @click="handleCloseBibDialog">取 消</el-button>
  458. <el-button type="primary" @click="handleGenerateBibFile" :loading="bibDialog.loading">生成参赛证</el-button>
  459. </div>
  460. </template>
  461. </el-dialog>
  462. </div>
  463. </template>
  464. <script setup name="GameEvent" lang="ts">
  465. import {
  466. listGameEvent,
  467. changeEventDefault,
  468. delGameEvent,
  469. addGameEvent,
  470. updateGameEvent,
  471. generateNumberTable,
  472. generateBib,
  473. type GenerateBibBo
  474. } from '@/api/system/gameEvent';
  475. import { GameEventVO, GameEventQuery, GameEventForm } from '@/api/system/gameEvent/types';
  476. import { getEventMdByEventAndType, editEventMd } from '@/api/system/eventMd';
  477. import { EventMdVO, EventMdForm } from '@/api/system/eventMd/types';
  478. import { useRouter } from 'vue-router';
  479. import { ref, nextTick } from 'vue';
  480. import RefereeForm from '@/views/system/gameEvent/RefereeForm.vue';
  481. import RankingBoard from './RankingBoard.vue';
  482. import Editor from '@/components/Editor/index.vue';
  483. import { useTagsViewStore } from '@/store/modules/tagsView';
  484. import { globalHeaders } from '@/utils/request';
  485. import { useGameEventStore } from '@/store/modules/gameEvent';
  486. const router = useRouter();
  487. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  488. const { game_event_type, game_event_status, sys_yes_no, game_event_purpose } = toRefs<any>(
  489. proxy?.useDict('game_event_type', 'game_event_status', 'sys_yes_no', 'game_event_purpose')
  490. );
  491. // 定义 RefereeForm 组件的类型
  492. interface RefereeFormInstance {
  493. openDialog: (eventId: string) => void;
  494. }
  495. const refereeFormRef = ref<(InstanceType<typeof RefereeForm> & RefereeFormInstance) | null>(null);
  496. const gameEventList = ref<GameEventVO[]>([]);
  497. const buttonLoading = ref(false);
  498. const loading = ref(true);
  499. const showSearch = ref(true);
  500. const ids = ref<Array<string | number>>([]);
  501. const single = ref(true);
  502. const multiple = ref(true);
  503. const total = ref(0);
  504. // 列显隐数据
  505. const columns = ref<FieldOption[]>([
  506. { key: 0, label: '赛事id', visible: false },
  507. { key: 1, label: '赛事编号', visible: true },
  508. { key: 2, label: '赛事名称', visible: true },
  509. { key: 3, label: '赛事类型', visible: true },
  510. { key: 4, label: '举办地点', visible: true },
  511. { key: 5, label: '用途', visible: true },
  512. { key: 6, label: '开始时间', visible: true },
  513. { key: 7, label: '结束时间', visible: true },
  514. { key: 8, label: '赛事链接', visible: true },
  515. { key: 9, label: '裁判码', visible: true },
  516. { key: 10, label: '举办单位', visible: true },
  517. { key: 11, label: '是否默认赛事', visible: true },
  518. { key: 12, label: '创建时间', visible: true },
  519. { key: 13, label: '更新时间', visible: true },
  520. { key: 14, label: '状态', visible: true },
  521. { key: 15, label: '备注', visible: true }
  522. ]);
  523. const queryFormRef = ref<ElFormInstance>();
  524. const gameEventFormRef = ref<ElFormInstance>();
  525. const dialog = reactive<DialogOption>({
  526. visible: false,
  527. title: ''
  528. });
  529. const uploadRef = ref<ElUploadInstance>();
  530. /*** 用户导入参数 */
  531. const upload = reactive<ImportOption>({
  532. // 是否显示弹出层(用户导入)
  533. open: false,
  534. // 弹出层标题(用户导入)
  535. title: '',
  536. // 是否禁用上传
  537. isUploading: false,
  538. // 是否更新已经存在的用户数据
  539. updateSupport: 0,
  540. // 设置上传的请求头部
  541. headers: globalHeaders(),
  542. // 上传的地址
  543. url: import.meta.env.VITE_APP_BASE_API + '/system/enroll/importData'
  544. });
  545. const initFormData: GameEventForm = {
  546. eventId: undefined,
  547. eventCode: undefined,
  548. eventName: undefined,
  549. eventType: undefined,
  550. location: undefined,
  551. purpose: undefined,
  552. startTime: undefined,
  553. endTime: undefined,
  554. eventUrl: undefined,
  555. refereeUrl: undefined,
  556. registerUrl: undefined,
  557. unit: undefined,
  558. isDefault: undefined,
  559. status: undefined,
  560. remark: undefined
  561. };
  562. const data = reactive<PageData<GameEventForm, GameEventQuery>>({
  563. form: { ...initFormData },
  564. queryParams: {
  565. pageNum: 1,
  566. pageSize: 10,
  567. eventCode: undefined,
  568. eventName: undefined,
  569. eventType: undefined,
  570. purpose: undefined,
  571. startTime: undefined,
  572. isDefault: undefined,
  573. status: undefined,
  574. params: {},
  575. orderByColumn: '',
  576. isAsc: ''
  577. },
  578. rules: {
  579. eventCode: [{ required: true, message: '赛事编号不能为空', trigger: 'blur' }],
  580. eventName: [{ required: true, message: '赛事名称不能为空', trigger: 'blur' }],
  581. eventType: [{ required: true, message: '赛事类型不能为空', trigger: 'change' }]
  582. }
  583. });
  584. const { queryParams, form, rules } = toRefs(data);
  585. // 使用gameEvent store
  586. const gameEventStore = useGameEventStore();
  587. /** 查询赛事基本信息列表 */
  588. const getList = async () => {
  589. loading.value = true;
  590. const res = await listGameEvent(queryParams.value);
  591. gameEventList.value = res.rows;
  592. total.value = res.total;
  593. loading.value = false;
  594. };
  595. /** 取消按钮 */
  596. const cancel = () => {
  597. reset();
  598. dialog.visible = false;
  599. };
  600. /** 表单重置 */
  601. const reset = () => {
  602. form.value = { ...initFormData };
  603. gameEventFormRef.value?.resetFields();
  604. };
  605. /** 搜索按钮操作 */
  606. const handleQuery = () => {
  607. queryParams.value.pageNum = 1;
  608. getList();
  609. };
  610. /** 重置按钮操作 */
  611. const resetQuery = () => {
  612. queryFormRef.value?.resetFields();
  613. handleQuery();
  614. };
  615. /** 多选框选中数据 */
  616. const handleSelectionChange = (selection: GameEventVO[]) => {
  617. ids.value = selection.map((item) => item.eventId);
  618. single.value = selection.length != 1;
  619. multiple.value = !selection.length;
  620. };
  621. /** 新增按钮操作 */
  622. const handleAdd = () => {
  623. router.push('/system/gameEvent/add');
  624. };
  625. /** 修改按钮操作 */
  626. const handleUpdate = async (row?: GameEventVO) => {
  627. const _eventId = row?.eventId || ids.value[0];
  628. router.push(`/system/gameEvent/edit/${_eventId}`);
  629. };
  630. /** 提交按钮 */
  631. const submitForm = () => {
  632. gameEventFormRef.value?.validate(async (valid: boolean) => {
  633. if (valid) {
  634. buttonLoading.value = true;
  635. if (form.value.eventId) {
  636. await updateGameEvent(form.value).finally(() => (buttonLoading.value = false));
  637. } else {
  638. await addGameEvent(form.value).finally(() => (buttonLoading.value = false));
  639. }
  640. proxy?.$modal.msgSuccess('操作成功');
  641. dialog.visible = false;
  642. await getList();
  643. }
  644. });
  645. };
  646. /** 删除按钮操作 */
  647. const handleDelete = async (row?: GameEventVO) => {
  648. const _ids = row?.eventId || ids.value;
  649. if (!_ids || (Array.isArray(_ids) && _ids.length === 0)) {
  650. proxy?.$modal.msgError('请选择要删除的赛事');
  651. return;
  652. }
  653. // 如果是单个删除,需要检查时间限制
  654. if (row) {
  655. const endTime = new Date(row.endTime);
  656. const currentTime = new Date();
  657. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  658. if (daysDiff < 100) {
  659. proxy?.$modal.msgError(`该赛事结束时间不足100天,无法删除。距离可删除时间还有 ${100 - daysDiff} 天。`);
  660. return;
  661. }
  662. await proxy?.$modal.confirm('是否确认删除赛事编号为"' + row.eventCode + '"的数据项?').finally(() => (loading.value = false));
  663. await delGameEvent(row.eventId);
  664. proxy?.$modal.msgSuccess('删除成功');
  665. } else {
  666. // 批量删除
  667. const selectedEvents = gameEventList.value.filter((event) => ids.value.includes(event.eventId));
  668. const invalidEvents = selectedEvents.filter((event) => {
  669. const endTime = new Date(event.endTime);
  670. const currentTime = new Date();
  671. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  672. return daysDiff < 100;
  673. });
  674. if (invalidEvents.length > 0) {
  675. const eventNames = invalidEvents.map((event) => event.eventName).join('、');
  676. proxy?.$modal.msgError(`以下赛事结束时间不足100天,无法删除:${eventNames}`);
  677. return;
  678. }
  679. const eventCodes = selectedEvents.map((event) => event.eventCode).join('、');
  680. await proxy?.$modal.confirm(`是否确认删除以下赛事:${eventCodes}?`).finally(() => (loading.value = false));
  681. await delGameEvent(ids.value);
  682. proxy?.$modal.msgSuccess(`成功删除 ${selectedEvents.length} 个赛事`);
  683. }
  684. await getList();
  685. };
  686. /** 判断是否可以删除赛事 */
  687. const canDelete = (row: GameEventVO): boolean => {
  688. const endTime = new Date(row.endTime);
  689. const currentTime = new Date();
  690. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  691. return daysDiff >= 100;
  692. };
  693. /** 获取删除按钮的提示信息 */
  694. const getDeleteTooltip = (row: GameEventVO): string => {
  695. if (canDelete(row)) {
  696. return '删除';
  697. } else {
  698. const endTime = new Date(row.endTime);
  699. const currentTime = new Date();
  700. const daysDiff = Math.floor((currentTime.getTime() - endTime.getTime()) / (1000 * 60 * 60 * 24));
  701. return `赛事结束时间不足100天,无法删除。距离可删除时间还有 ${100 - daysDiff} 天。`;
  702. }
  703. };
  704. /** 导出按钮操作 */
  705. const handleExport = () => {
  706. proxy?.download(
  707. 'system/gameEvent/export',
  708. {
  709. ...queryParams.value
  710. },
  711. `gameEvent_${new Date().getTime()}.xlsx`
  712. );
  713. };
  714. /* 下载模板 */
  715. const handleDownloadTemplate = (row: GameEventVO) => {
  716. proxy?.download(
  717. 'system/enroll/importTemplate',
  718. {
  719. eventId: row.eventId
  720. },
  721. `event_enroll_template_${new Date().getTime()}.xlsx`
  722. );
  723. };
  724. // 导入报名表逻辑
  725. const handleImportRegistration = (row) => {
  726. upload.url = import.meta.env.VITE_APP_BASE_API + `/system/enroll/importData/${row.eventId}`;
  727. upload.title = '报名表导入';
  728. upload.open = true;
  729. };
  730. /**文件上传中处理 */
  731. const handleFileUploadProgress = () => {
  732. upload.isUploading = true;
  733. };
  734. /** 文件上传成功处理 */
  735. const handleFileSuccess = (response: any, file: UploadFile) => {
  736. upload.open = false;
  737. upload.isUploading = false;
  738. uploadRef.value?.handleRemove(file);
  739. ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
  740. dangerouslyUseHTMLString: true
  741. });
  742. getList();
  743. };
  744. /** 提交上传文件 */
  745. function submitFileForm() {
  746. uploadRef.value?.submit();
  747. }
  748. // 添加参赛者操作
  749. const handleAddParticipant = (row: GameEventVO) => {
  750. // 跳转到新增或编辑参赛者信息页面,并传递 eventName 参数
  751. router.push({
  752. name: 'GameEventAthlete',
  753. params: { eventId: row.eventId, eventName: row.eventName }
  754. });
  755. };
  756. // 添加裁判按钮操作 1
  757. const handleAddReferee = async (row: GameEventVO) => {
  758. // 打开裁判表单对话框并传递 eventId
  759. refereeFormRef.value?.openDialog(String(row.eventId));
  760. };
  761. // 预览按钮点击事件
  762. const handlePreview = (row: GameEventVO) => {
  763. // 跳转到赛事详情页面
  764. router.push({
  765. name: 'GameEventDetail',
  766. params: { eventId: row.eventId }
  767. });
  768. };
  769. // 比赛成绩按钮点击事件
  770. const handleGameData = (row: GameEventVO) => {
  771. router.push({
  772. name: 'RankingBoardPage',
  773. params: { eventId: row.eventId }
  774. });
  775. };
  776. // 文章编写相关数据
  777. const articleDialog = reactive({
  778. visible: false,
  779. title: '',
  780. currentEventId: undefined as string | number | undefined
  781. });
  782. const activeTab = ref('competition-process');
  783. // 标签页与类型值的映射关系
  784. const tabTypeMapping: Record<string, number> = {
  785. 'competition-process': 1, // 竞赛流程
  786. 'competition-items': 2, // 竞赛项目
  787. 'activity-agenda': 3, // 活动议程
  788. 'project-introduction': 4, // 项目介绍
  789. 'competition-flow': 5, // 竞赛流程
  790. 'event-grouping': 6, // 赛事分组
  791. 'athlete-handbook': 7, // 运动员号码簿
  792. 'project-venue': 8, // 项目场地
  793. 'traffic-guide': 9 // 交通指示
  794. };
  795. const articleData = reactive({
  796. competitionProcess: { id: undefined, title: '', content: '', remark: '' },
  797. competitionItems: { id: undefined, title: '', content: '', remark: '' },
  798. activityAgenda: { id: undefined, title: '', content: '', remark: '' },
  799. projectIntroduction: { id: undefined, title: '', content: '', remark: '' },
  800. competitionFlow: { id: undefined, title: '', content: '', remark: '' },
  801. eventGrouping: { id: undefined, title: '', content: '', remark: '' },
  802. athleteHandbook: { id: undefined, title: '', content: '', remark: '' },
  803. projectVenue: { id: undefined, title: '', content: '', remark: '' },
  804. trafficGuide: { id: undefined, title: '', content: '', remark: '' }
  805. });
  806. // 打开排行榜组件并传递赛事ID
  807. // const openRankingBoard = (eventId: string) => {
  808. // currentEventId.value = eventId;
  809. // rankingBoardVisible.value = true;
  810. // };
  811. /** 状态修改 */
  812. const handleStatusChange = async (row: GameEventVO) => {
  813. const text = row.isDefault === '0' ? '启用' : '停用';
  814. try {
  815. await proxy?.$modal.confirm('确认要"' + text + '""' + row.eventName + '"为默认赛事吗?');
  816. await changeEventDefault(row.eventId, row.isDefault);
  817. await getList();
  818. // 更新全局默认赛事信息
  819. await gameEventStore.fetchDefaultEvent();
  820. // localStorage.setItem('defaultEventId', row.eventId);
  821. proxy?.$modal.msgSuccess(text + '成功');
  822. // 刷新当前标签页
  823. await useTagsViewStore().delOthersViews(router.currentRoute.value);
  824. } catch {
  825. return;
  826. } finally {
  827. row.isDefault = row.isDefault === '0' ? '1' : '0';
  828. }
  829. };
  830. /** 编写文章按钮操作 */
  831. const handleWriteArticle = async (row: GameEventVO) => {
  832. articleDialog.title = `编写文章 - ${row.eventName}`;
  833. articleDialog.currentEventId = row.eventId;
  834. articleDialog.visible = true;
  835. activeTab.value = 'competition-process';
  836. // 加载默认标签页(竞赛流程)的数据
  837. await loadTabData('competition-process');
  838. };
  839. /** 加载指定标签页的数据 */
  840. const loadTabData = async (tabName: string) => {
  841. const type = tabTypeMapping[tabName];
  842. if (articleDialog.currentEventId && type) {
  843. try {
  844. const response = await getEventMdByEventAndType(articleDialog.currentEventId, type);
  845. const eventMd = response.data;
  846. const dataKey = getDataKeyByTabName(tabName);
  847. if (dataKey && articleData[dataKey]) {
  848. if (eventMd) {
  849. articleData[dataKey].id = eventMd.id;
  850. articleData[dataKey].title = eventMd.title || '';
  851. articleData[dataKey].content = eventMd.content || '';
  852. articleData[dataKey].remark = eventMd.remark || '';
  853. } else {
  854. articleData[dataKey].id = undefined;
  855. articleData[dataKey].title = '';
  856. articleData[dataKey].content = '';
  857. articleData[dataKey].remark = '';
  858. }
  859. }
  860. } catch (error) {
  861. const dataKey = getDataKeyByTabName(tabName);
  862. if (dataKey && articleData[dataKey]) {
  863. articleData[dataKey].id = undefined;
  864. articleData[dataKey].title = '';
  865. articleData[dataKey].content = '';
  866. articleData[dataKey].remark = '';
  867. }
  868. }
  869. }
  870. };
  871. /** 标签页点击事件 */
  872. const handleTabClick = async (tab: any) => {
  873. const tabName = tab.props.name;
  874. await loadTabData(tabName);
  875. };
  876. /** 根据标签页名称获取数据键名 */
  877. const getDataKeyByTabName = (tabName: string): keyof typeof articleData | null => {
  878. const mapping: Record<string, keyof typeof articleData> = {
  879. 'competition-process': 'competitionProcess',
  880. 'competition-items': 'competitionItems',
  881. 'activity-agenda': 'activityAgenda',
  882. 'project-introduction': 'projectIntroduction',
  883. 'competition-flow': 'competitionFlow',
  884. 'event-grouping': 'eventGrouping',
  885. 'athlete-handbook': 'athleteHandbook',
  886. 'project-venue': 'projectVenue',
  887. 'traffic-guide': 'trafficGuide'
  888. };
  889. return mapping[tabName] || null;
  890. };
  891. /** 关闭文章编写对话框 */
  892. const handleCloseArticleDialog = () => {
  893. // 清空所有文章数据
  894. Object.keys(articleData).forEach((key) => {
  895. const dataKey = key as keyof typeof articleData;
  896. articleData[dataKey].id = undefined;
  897. articleData[dataKey].title = '';
  898. articleData[dataKey].content = '';
  899. articleData[dataKey].remark = '';
  900. });
  901. // 重置对话框状态
  902. articleDialog.visible = false;
  903. articleDialog.currentEventId = undefined;
  904. activeTab.value = 'competition-process';
  905. };
  906. /** 保存文章 */
  907. const handleSaveArticle = async () => {
  908. if (!articleDialog.currentEventId) {
  909. proxy?.$modal.msgError('赛事ID不能为空');
  910. return;
  911. }
  912. const currentTabName = activeTab.value;
  913. const type = tabTypeMapping[currentTabName];
  914. const dataKey = getDataKeyByTabName(currentTabName);
  915. if (!dataKey || !articleData[dataKey]) {
  916. proxy?.$modal.msgError('获取当前标签页数据失败');
  917. return;
  918. }
  919. const currentData = articleData[dataKey];
  920. if (!currentData.title?.trim()) {
  921. proxy?.$modal.msgError('标题不能为空');
  922. return;
  923. }
  924. try {
  925. const formData: EventMdForm = {
  926. id: currentData.id,
  927. eventId: articleDialog.currentEventId,
  928. title: currentData.title,
  929. content: currentData.content || '',
  930. type: type,
  931. remark: currentData.remark || ''
  932. };
  933. await editEventMd(formData);
  934. proxy?.$modal.msgSuccess('文章保存成功');
  935. // 重新加载当前标签页数据以获取最新的ID
  936. await loadTabData(currentTabName);
  937. } catch (error) {
  938. console.error('保存文章失败:', error);
  939. proxy?.$modal.msgError('保存文章失败');
  940. }
  941. };
  942. // 基于默认赛事的操作函数
  943. /** 下载模板(默认赛事) */
  944. const handleDownloadTemplateDefault = async () => {
  945. const defaultEvent = gameEventStore.defaultEventInfo;
  946. if (!defaultEvent) {
  947. proxy?.$modal.msgError('请先设置默认赛事');
  948. return;
  949. }
  950. handleDownloadTemplate(defaultEvent);
  951. };
  952. /** 导入报名信息(默认赛事) */
  953. const handleImportRegistrationDefault = async () => {
  954. const defaultEvent = gameEventStore.defaultEventInfo;
  955. if (!defaultEvent) {
  956. proxy?.$modal.msgError('请先设置默认赛事');
  957. return;
  958. }
  959. handleImportRegistration(defaultEvent);
  960. };
  961. /** 添加参赛者(默认赛事) */
  962. const handleAddParticipantDefault = async () => {
  963. const defaultEvent = gameEventStore.defaultEventInfo;
  964. if (!defaultEvent) {
  965. proxy?.$modal.msgError('请先设置默认赛事');
  966. return;
  967. }
  968. handleAddParticipant(defaultEvent);
  969. };
  970. /** 添加裁判(默认赛事) */
  971. const handleAddRefereeDefault = async () => {
  972. const defaultEvent = gameEventStore.defaultEventInfo;
  973. if (!defaultEvent) {
  974. proxy?.$modal.msgError('请先设置默认赛事');
  975. return;
  976. }
  977. handleAddReferee(defaultEvent);
  978. };
  979. /** 预览(默认赛事) */
  980. const handlePreviewDefault = async () => {
  981. const defaultEvent = gameEventStore.defaultEventInfo;
  982. if (!defaultEvent) {
  983. proxy?.$modal.msgError('请先设置默认赛事');
  984. return;
  985. }
  986. handlePreview(defaultEvent);
  987. };
  988. /** 比赛数据(默认赛事) */
  989. const handleGameDataDefault = async () => {
  990. const defaultEvent = gameEventStore.defaultEventInfo;
  991. if (!defaultEvent) {
  992. proxy?.$modal.msgError('请先设置默认赛事');
  993. return;
  994. }
  995. handleGameData(defaultEvent);
  996. };
  997. /** 编写文章(默认赛事) */
  998. const handleWriteArticleDefault = async () => {
  999. const defaultEvent = gameEventStore.defaultEventInfo;
  1000. if (!defaultEvent) {
  1001. proxy?.$modal.msgError('请先设置默认赛事');
  1002. return;
  1003. }
  1004. handleWriteArticle(defaultEvent);
  1005. };
  1006. const handleExportNumberTableDefault = async () => {
  1007. await proxy?.download('system/number/export', {}, `号码对照表_${new Date().getTime()}.xlsx`);
  1008. };
  1009. // 生成参赛证相关
  1010. const bibDialog = reactive({
  1011. visible: false,
  1012. loading: false
  1013. });
  1014. const bibForm = reactive({
  1015. logoX: 50,
  1016. logoY: 50,
  1017. qRCodeX: 100,
  1018. qRCodeY: 200,
  1019. fontName: 'simhei',
  1020. fontSize: 36,
  1021. fontColor: '#000000',
  1022. fontColorHex: '#000000'
  1023. });
  1024. const bgImageFile = ref<File | null>(null);
  1025. const logoImageFile = ref<File | null>(null);
  1026. const bgImageUrl = ref<string>('');
  1027. const logoImageUrl = ref<string>('');
  1028. const previewContainer = ref<HTMLElement>();
  1029. const bgImageDimensions = ref<{ width: number; height: number } | null>(null);
  1030. const bgUploadRef = ref<ElUploadInstance>();
  1031. const logoUploadRef = ref<ElUploadInstance>();
  1032. // 拖拽相关
  1033. const dragState = reactive({
  1034. isDragging: false,
  1035. dragTarget: '',
  1036. startX: 0,
  1037. startY: 0,
  1038. startLeft: 0,
  1039. startTop: 0
  1040. });
  1041. // 生成参赛证按钮处理
  1042. const handleGenerateBib = () => {
  1043. // 强制设置默认值,不使用条件判断(像素单位)
  1044. bibForm.logoX = bibForm.logoX || 50;
  1045. bibForm.logoY = bibForm.logoY || 50;
  1046. bibForm.qRCodeX = bibForm.qRCodeX || 100; // 修正:使用独立的默认值
  1047. bibForm.qRCodeY = bibForm.qRCodeY || 200; // 修正:使用独立的默认值
  1048. bibForm.fontName = bibForm.fontName || 'simhei';
  1049. bibForm.fontSize = bibForm.fontSize || 36;
  1050. bibForm.fontColor = bibForm.fontColor || '#000000';
  1051. bibForm.fontColorHex = bibForm.fontColorHex || '#000000';
  1052. bibDialog.visible = true;
  1053. };
  1054. // 关闭对话框
  1055. const handleCloseBibDialog = () => {
  1056. bibDialog.visible = false;
  1057. resetBibForm();
  1058. // 清除上传的文件
  1059. if (bgUploadRef.value) {
  1060. bgUploadRef.value.clearFiles();
  1061. }
  1062. if (logoUploadRef.value) {
  1063. logoUploadRef.value.clearFiles();
  1064. }
  1065. };
  1066. // 重置表单
  1067. const resetBibForm = () => {
  1068. // 设置默认值(像素单位)
  1069. bibForm.logoX = 50;
  1070. bibForm.logoY = 50;
  1071. bibForm.qRCodeX = 100;
  1072. bibForm.qRCodeY = 200;
  1073. bibForm.fontName = 'simhei';
  1074. bibForm.fontSize = 36;
  1075. bibForm.fontColor = '#000000';
  1076. bibForm.fontColorHex = '#000000';
  1077. bgImageFile.value = null;
  1078. logoImageFile.value = null;
  1079. bgImageUrl.value = '';
  1080. logoImageUrl.value = '';
  1081. // 清除上传的文件
  1082. if (bgUploadRef.value) {
  1083. bgUploadRef.value.clearFiles();
  1084. }
  1085. if (logoUploadRef.value) {
  1086. logoUploadRef.value.clearFiles();
  1087. }
  1088. };
  1089. // 背景图片改变处理
  1090. const handleBgImageChange = async (file: any) => {
  1091. if (file.raw) {
  1092. bgImageFile.value = file.raw;
  1093. const reader = new FileReader();
  1094. reader.onload = (e) => {
  1095. bgImageUrl.value = e.target?.result as string;
  1096. };
  1097. reader.readAsDataURL(file.raw);
  1098. // 获取背景图片的实际尺寸
  1099. try {
  1100. const dimensions = await getImageDimensions(file.raw);
  1101. bgImageDimensions.value = dimensions;
  1102. } catch (error) {
  1103. console.error('获取图片尺寸失败:', error);
  1104. }
  1105. }
  1106. };
  1107. // Logo图片改变处理
  1108. const handleLogoImageChange = (file: any) => {
  1109. if (file.raw) {
  1110. logoImageFile.value = file.raw;
  1111. const reader = new FileReader();
  1112. reader.onload = (e) => {
  1113. logoImageUrl.value = e.target?.result as string;
  1114. };
  1115. reader.readAsDataURL(file.raw);
  1116. }
  1117. };
  1118. // 字体颜色改变处理
  1119. const handleFontColorChange = (color: string) => {
  1120. bibForm.fontColor = color;
  1121. bibForm.fontColorHex = color;
  1122. };
  1123. // 开始拖拽
  1124. const startDrag = (event: MouseEvent, target: string) => {
  1125. event.preventDefault();
  1126. dragState.isDragging = true;
  1127. dragState.dragTarget = target;
  1128. dragState.startX = event.clientX;
  1129. dragState.startY = event.clientY;
  1130. if (target === 'logo') {
  1131. dragState.startLeft = bibForm.logoX;
  1132. dragState.startTop = bibForm.logoY;
  1133. } else if (target === 'barcode') {
  1134. dragState.startLeft = bibForm.qRCodeX;
  1135. dragState.startTop = bibForm.qRCodeY;
  1136. }
  1137. document.addEventListener('mousemove', handleDrag);
  1138. document.addEventListener('mouseup', stopDrag);
  1139. };
  1140. // 处理拖拽
  1141. const handleDrag = (event: MouseEvent) => {
  1142. if (!dragState.isDragging) return;
  1143. const deltaX = event.clientX - dragState.startX;
  1144. const deltaY = event.clientY - dragState.startY;
  1145. if (dragState.dragTarget === 'logo') {
  1146. bibForm.logoX = Math.max(0, dragState.startLeft + deltaX);
  1147. bibForm.logoY = Math.max(0, dragState.startTop + deltaY);
  1148. } else if (dragState.dragTarget === 'barcode') {
  1149. bibForm.qRCodeX = Math.max(0, dragState.startLeft + deltaX);
  1150. bibForm.qRCodeY = Math.max(0, dragState.startTop + deltaY);
  1151. }
  1152. };
  1153. // 停止拖拽
  1154. const stopDrag = () => {
  1155. dragState.isDragging = false;
  1156. dragState.dragTarget = '';
  1157. document.removeEventListener('mousemove', handleDrag);
  1158. document.removeEventListener('mouseup', stopDrag);
  1159. };
  1160. // 坐标转换函数:根据实际背景图片尺寸调整坐标
  1161. const convertCoordinatesWithScale = (x: number, y: number): { x: number; y: number } => {
  1162. // 获取预览容器尺寸,使用更精确的方法
  1163. const container = previewContainer.value;
  1164. const previewWidth = container?.clientWidth || container?.offsetWidth || 400;
  1165. const previewHeight = container?.clientHeight || container?.offsetHeight || 400;
  1166. // 使用实际背景图片尺寸,如果没有则使用默认A4尺寸
  1167. const actualWidth = bgImageDimensions.value?.width || 595;
  1168. const actualHeight = bgImageDimensions.value?.height || 842;
  1169. // 计算实际比例
  1170. const scaleX = actualWidth / previewWidth;
  1171. const scaleY = actualHeight / previewHeight;
  1172. // 微调系数,用于补偿微小偏差
  1173. const fineTuneX = 1.15; // 进一步缩小X坐标,让元素向左移动
  1174. const fineTuneY = 0.9; // 进一步放大Y坐标,让元素向下移动
  1175. // 根据实际效果图,需要调整坐标系统
  1176. // 实际效果显示Logo在左上角,二维码在左下角
  1177. const adjustedX = x * scaleX * fineTuneX;
  1178. const adjustedY = (previewHeight - y) * scaleY * fineTuneY;
  1179. // 添加额外的偏移量调整
  1180. const offsetX = 8; // 向右偏移8pt(进一步减少向右偏移)
  1181. const offsetY = 65; // 向下偏移65pt(进一步增加向下偏移)
  1182. // 根据元素类型进行特殊调整
  1183. let finalX = adjustedX + offsetX;
  1184. let finalY = adjustedY + offsetY;
  1185. // 如果是Logo,进行特殊调整
  1186. if (x < 100 && y < 100) {
  1187. // 假设Logo在左上角区域
  1188. finalX += 5; // Logo额外向右偏移
  1189. finalY -= 10; // Logo额外向下偏移
  1190. }
  1191. // 如果是二维码,进行特殊调整
  1192. if (x > 200 && y > 200) {
  1193. // 假设二维码在右下角区域
  1194. finalX -= 3; // 二维码额外向左偏移
  1195. finalY += 5; // 二维码额外向下偏移
  1196. }
  1197. return {
  1198. x: finalX,
  1199. y: finalY
  1200. };
  1201. };
  1202. // 新增:获取背景图片实际尺寸的函数
  1203. const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
  1204. return new Promise((resolve) => {
  1205. const img = new Image();
  1206. img.onload = () => {
  1207. resolve({ width: img.width, height: img.height });
  1208. };
  1209. img.src = URL.createObjectURL(file);
  1210. });
  1211. };
  1212. // 生成参赛证文件
  1213. const handleGenerateBibFile = async () => {
  1214. if (!bgImageFile.value) {
  1215. proxy?.$modal.msgError('请上传背景图片');
  1216. return;
  1217. }
  1218. bibDialog.loading = true;
  1219. try {
  1220. let qRCodeX = bibForm.qRCodeX;
  1221. let qRCodeY = bibForm.qRCodeY;
  1222. // 如果值为null或undefined,强制使用默认值
  1223. if (qRCodeX === null || qRCodeX === undefined || isNaN(qRCodeX)) {
  1224. qRCodeX = 100;
  1225. console.warn('qRCodeX值异常,使用默认值100px');
  1226. }
  1227. if (qRCodeY === null || qRCodeY === undefined || isNaN(qRCodeY)) {
  1228. qRCodeY = 200;
  1229. console.warn('qRCodeY值异常,使用默认值200px');
  1230. }
  1231. // 获取预览容器的高度用于坐标转换
  1232. const containerHeight = previewContainer.value?.clientHeight || 400;
  1233. // 检查背景图片尺寸
  1234. if (!bgImageDimensions.value) {
  1235. proxy?.$modal.msgWarning('正在获取背景图片尺寸,请稍后再试');
  1236. return;
  1237. }
  1238. // 等待一帧确保所有尺寸都已计算完成
  1239. await nextTick();
  1240. // Logo坐标(左上角)
  1241. const logoCoords = convertCoordinatesWithScale(bibForm.logoX || 50, bibForm.logoY || 50);
  1242. // 二维码坐标(左上角)
  1243. const qrCoords = convertCoordinatesWithScale(qRCodeX, qRCodeY);
  1244. const bibParams = {
  1245. logoX: logoCoords.x,
  1246. logoY: logoCoords.y,
  1247. qRCodeX: qrCoords.x,
  1248. qRCodeY: qrCoords.y,
  1249. fontName: bibForm.fontName || 'simhei',
  1250. fontSize: Math.round((bibForm.fontSize || 36) * 0.75), // 字体大小转换为PDF点并四舍五入为整数
  1251. fontColor: parseInt((bibForm.fontColor || '#000000').replace('#', ''), 16)
  1252. };
  1253. // 最后一次检查,确保二维码坐标不为null
  1254. if (bibParams.qRCodeX === null || bibParams.qRCodeY === undefined) {
  1255. bibParams.qRCodeX = 148.75; // 100px * 1.4875
  1256. console.error('最后一次修复:qRCodeX仍为null,设置为100px转换后的值');
  1257. }
  1258. if (bibParams.qRCodeY === null || bibParams.qRCodeY === undefined) {
  1259. bibParams.qRCodeY = 421; // 200px * 2.105
  1260. console.error('最后一次修复:qRCodeY仍为null,设置为200px转换后的值');
  1261. }
  1262. const response = await generateBib(bgImageFile.value, logoImageFile.value, bibParams);
  1263. // 处理文件下载 - response已经是blob数据,不需要再访问.data属性
  1264. const blob = new Blob([response as any], { type: 'application/zip' });
  1265. const url = window.URL.createObjectURL(blob);
  1266. const link = document.createElement('a');
  1267. link.href = url;
  1268. link.download = `参赛证_${new Date().getTime()}.zip`;
  1269. link.click();
  1270. window.URL.revokeObjectURL(url);
  1271. proxy?.$modal.msgSuccess('参赛证生成成功');
  1272. handleCloseBibDialog();
  1273. } catch (error) {
  1274. console.error('生成参赛证失败:', error);
  1275. proxy?.$modal.msgError('生成参赛证失败');
  1276. } finally {
  1277. bibDialog.loading = false;
  1278. }
  1279. };
  1280. onMounted(() => {
  1281. // 获取默认赛事信息
  1282. gameEventStore.fetchDefaultEvent();
  1283. getList();
  1284. });
  1285. // 监听路由变化,当从编辑页返回时检查是否需要刷新列表
  1286. onActivated(() => {
  1287. // 检查是否有需要刷新的标识
  1288. const needRefresh = sessionStorage.getItem('needRefreshGameEventList');
  1289. if (needRefresh === 'true') {
  1290. // 清除标识
  1291. sessionStorage.removeItem('needRefreshGameEventList');
  1292. // 刷新列表数据
  1293. getList();
  1294. }
  1295. });
  1296. </script>
  1297. <style scoped>
  1298. .operation-buttons {
  1299. display: flex;
  1300. flex-direction: column;
  1301. gap: 4px;
  1302. align-items: center;
  1303. min-width: 200px;
  1304. }
  1305. .button-row {
  1306. display: flex;
  1307. gap: 6px;
  1308. justify-content: center;
  1309. align-items: center;
  1310. flex-wrap: wrap;
  1311. }
  1312. .operation-buttons .el-button {
  1313. min-width: 28px;
  1314. min-height: 28px;
  1315. padding: 4px 6px;
  1316. border-radius: 4px;
  1317. transition: all 0.2s ease;
  1318. display: flex;
  1319. align-items: center;
  1320. justify-content: center;
  1321. }
  1322. /* 生成参赛证样式 */
  1323. .bib-generator {
  1324. padding: 20px;
  1325. }
  1326. .bib-generator .el-upload__tip {
  1327. color: #909399;
  1328. font-size: 12px;
  1329. margin-top: 8px;
  1330. text-align: center;
  1331. }
  1332. .preview-container {
  1333. border: 2px dashed #ddd;
  1334. border-radius: 8px;
  1335. min-height: 400px;
  1336. position: relative;
  1337. overflow: hidden;
  1338. }
  1339. .preview-canvas {
  1340. width: 100%;
  1341. height: 400px;
  1342. position: relative;
  1343. background-size: cover;
  1344. background-position: center;
  1345. background-repeat: no-repeat;
  1346. background-color: #f5f5f5;
  1347. }
  1348. .draggable-element {
  1349. position: absolute;
  1350. cursor: move;
  1351. user-select: none;
  1352. z-index: 10;
  1353. }
  1354. .draggable-element:hover {
  1355. opacity: 0.8;
  1356. }
  1357. .logo-element {
  1358. border: 2px dashed transparent;
  1359. }
  1360. .logo-element:hover {
  1361. border-color: #409eff;
  1362. }
  1363. .barcode-element {
  1364. border: 2px dashed transparent;
  1365. padding: 5px;
  1366. }
  1367. .barcode-element:hover {
  1368. border-color: #67c23a;
  1369. background-color: rgba(103, 194, 58, 0.1);
  1370. }
  1371. .number-element {
  1372. font-weight: bold;
  1373. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
  1374. border: 2px dashed transparent;
  1375. padding: 5px;
  1376. white-space: nowrap;
  1377. user-select: none;
  1378. pointer-events: none;
  1379. }
  1380. .event-name-preview {
  1381. position: absolute;
  1382. top: 20px;
  1383. left: 50%;
  1384. transform: translateX(-50%);
  1385. font-weight: bold;
  1386. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
  1387. user-select: none;
  1388. pointer-events: none;
  1389. z-index: 5;
  1390. }
  1391. .operation-buttons .el-button:hover {
  1392. transform: translateY(-1px);
  1393. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
  1394. }
  1395. .operation-buttons .el-button[disabled] {
  1396. opacity: 0.5;
  1397. cursor: not-allowed;
  1398. }
  1399. .operation-buttons .el-button[disabled]:hover {
  1400. transform: none;
  1401. box-shadow: none;
  1402. }
  1403. /* 为不同类型的按钮设置不同的颜色主题 */
  1404. .operation-buttons .el-button--primary {
  1405. color: #409eff;
  1406. }
  1407. .operation-buttons .el-button--danger {
  1408. color: #f56c6c;
  1409. }
  1410. .operation-buttons .el-button--warning {
  1411. color: #e6a23c;
  1412. }
  1413. .operation-buttons .el-button--info {
  1414. color: #909399;
  1415. }
  1416. .operation-buttons .el-button--success {
  1417. color: #67c23a;
  1418. }
  1419. /* 响应式设计 */
  1420. @media (max-width: 1400px) {
  1421. .operation-buttons {
  1422. min-width: 180px;
  1423. }
  1424. .button-row {
  1425. gap: 4px;
  1426. }
  1427. .operation-buttons .el-button {
  1428. min-width: 24px;
  1429. min-height: 24px;
  1430. padding: 3px 5px;
  1431. }
  1432. }
  1433. @media (max-width: 1200px) {
  1434. .operation-buttons {
  1435. min-width: 160px;
  1436. gap: 2px;
  1437. }
  1438. .button-row {
  1439. gap: 2px;
  1440. }
  1441. .operation-buttons .el-button {
  1442. min-width: 22px;
  1443. min-height: 22px;
  1444. padding: 2px 4px;
  1445. }
  1446. }
  1447. </style>