index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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">
  7. <el-form-item label="裁判姓名" prop="name">
  8. <el-input v-model="queryParams.name" placeholder="请输入裁判姓名" clearable @keyup.enter="handleQuery" />
  9. </el-form-item>
  10. <el-form-item label="工作年限" prop="workYears">
  11. <el-input v-model="queryParams.workYears" placeholder="请输入工作年限" clearable @keyup.enter="handleQuery" />
  12. </el-form-item>
  13. <el-form-item>
  14. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  15. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  16. </el-form-item>
  17. </el-form>
  18. </el-card>
  19. </div>
  20. </transition>
  21. <el-card shadow="never">
  22. <template #header>
  23. <el-row :gutter="10" class="mb8">
  24. <el-col :span="1.5">
  25. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:judge:add']">新增</el-button>
  26. </el-col>
  27. <el-col :span="1.5">
  28. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:judge:edit']"
  29. >修改</el-button
  30. >
  31. </el-col>
  32. <el-col :span="1.5">
  33. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:judge:remove']"
  34. >删除</el-button
  35. >
  36. </el-col>
  37. <el-col :span="1.5">
  38. <el-button type="warning" plain icon="Upload" @click="openImportDialog" v-hasPermi="['physical:judge:export']">导入</el-button>
  39. </el-col>
  40. <el-col :span="1.5">
  41. <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:judge:export']">导出</el-button>
  42. </el-col>
  43. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  44. </el-row>
  45. </template>
  46. <el-table v-loading="loading" border :data="judgeList" @selection-change="handleSelectionChange">
  47. <el-table-column type="selection" width="55" align="center" />
  48. <el-table-column label="编号" align="center" prop="id" v-if="true" />
  49. <el-table-column label="裁判图片" align="center" width="90">
  50. <template #default="scope">
  51. <el-image
  52. v-if="scope.row.peopleIconUrl"
  53. :src="scope.row.peopleIconUrl"
  54. style="width: 40px; height: 40px; border-radius: 4px; cursor: zoom-in"
  55. :preview-src-list="[scope.row.peopleIconUrl]"
  56. :preview-teleported="true"
  57. fit="cover"
  58. />
  59. <span v-else></span>
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="裁判编号" align="center" prop="judgeNumber" />
  63. <el-table-column label="裁判姓名" align="center" prop="name" />
  64. <el-table-column label="工作年限" align="center" prop="workYears" />
  65. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  66. <template #default="scope">
  67. <el-tooltip content="修改" placement="top">
  68. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:judge:edit']"></el-button>
  69. </el-tooltip>
  70. <el-tooltip content="删除" placement="top">
  71. <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:judge:remove']"></el-button>
  72. </el-tooltip>
  73. </template>
  74. </el-table-column>
  75. </el-table>
  76. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  77. </el-card>
  78. <!-- 添加或修改裁判管理对话框 -->
  79. <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
  80. <el-form ref="judgeFormRef" :model="form" :rules="rules" label-width="80px">
  81. <el-form-item label="裁判姓名" prop="name">
  82. <el-input v-model="form.name" placeholder="请输入裁判姓名" />
  83. </el-form-item>
  84. <el-form-item label="裁判图片" prop="peopleIconUrl">
  85. <div class="upload-container">
  86. <el-upload
  87. class="upload-icon"
  88. action="#"
  89. :on-change="handleJudgeIconChange"
  90. :on-remove="handleJudgeIconRemove"
  91. :file-list="judgeFileList"
  92. :auto-upload="false"
  93. accept="image/*"
  94. >
  95. <template #trigger>
  96. <el-button type="primary">点击选择图片</el-button>
  97. </template>
  98. <!-- 预览图区域 -->
  99. <template #default>
  100. <div class="preview-area" @click="handleJudgePreviewClick">
  101. <!-- 只有当 judgeIconPreviewUrl 或 form.peopleIconUrl 存在时才显示 img -->
  102. <img
  103. v-if="judgeIconPreviewUrl || form.peopleIconUrl"
  104. :src="judgeIconPreviewUrl || form.peopleIconUrl"
  105. alt="预览图"
  106. style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
  107. />
  108. <!-- 可选:无图时显示提示文字 -->
  109. <div v-else style="margin-top: 10px; color: #999">暂无图片</div>
  110. </div>
  111. </template>
  112. <template #tip>
  113. <div class="el-upload__tip">
  114. <span v-if="judgeFileList.length > 0">当前已选文件:{{ judgeFileList[0].name }}</span>
  115. </div>
  116. </template>
  117. </el-upload>
  118. </div>
  119. </el-form-item>
  120. <el-form-item label="工作年限" prop="workYears">
  121. <el-input v-model="form.workYears" placeholder="请输入工作年限" />
  122. </el-form-item>
  123. <el-form-item label="机构" prop="workYears">
  124. <el-input v-model="form.organization" placeholder="请输入机构" />
  125. </el-form-item>
  126. <el-form-item label="职务选择" prop="positionId">
  127. <el-select v-model="form.positionId" placeholder="请选择职务">
  128. <el-option v-for="item in positionOptions" :key="item.id" :label="item.label" :value="item.id" />
  129. </el-select>
  130. </el-form-item>
  131. </el-form>
  132. <template #footer>
  133. <div class="dialog-footer">
  134. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  135. <el-button @click="cancel">取 消</el-button>
  136. </div>
  137. </template>
  138. </el-dialog>
  139. <!-- 裁判图片预览弹窗 -->
  140. <el-dialog v-model="judgeDialogVisible" title="图片预览" width="50%">
  141. <img :src="judgePreviewSrc" alt="预览图片" style="max-width: 100%; max-height: 80vh" />
  142. </el-dialog>
  143. <!-- 导入对话框 -->
  144. <el-dialog :title="importDialog.title" v-model="importDialog.visible" width="500px" append-to-body>
  145. <el-form ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
  146. <el-form-item label="选择文件" prop="file">
  147. <div class="upload-container">
  148. <el-upload
  149. class="upload-demo"
  150. action="#"
  151. :on-change="handleImportFileChange"
  152. :on-remove="handleImportFileRemove"
  153. :file-list="importFileList"
  154. :auto-upload="false"
  155. :limit="1"
  156. accept=".xlsx,.xls"
  157. >
  158. <!-- 触发上传按钮 -->
  159. <template #trigger>
  160. <el-button type="primary">点击选择文件</el-button>
  161. </template>
  162. <!-- 自定义文件列表 -->
  163. <template #file="{ file }">
  164. <div style="display: flex; align-items: center">
  165. <span class="el-upload__tip">{{ file.name }}</span>
  166. <el-button type="text" icon="Delete" @click.stop="() => handleImportFileRemove(file)" style="margin-left: auto"> 删除 </el-button>
  167. </div>
  168. </template>
  169. <!-- 提示信息 -->
  170. <template #tip>
  171. <div class="el-upload__tip">
  172. <el-link type="primary" @click="downloadTemplate"> <i class="el-icon-download"></i> 模板下载 </el-link>
  173. </div>
  174. </template>
  175. </el-upload>
  176. </div>
  177. </el-form-item>
  178. </el-form>
  179. <template #footer>
  180. <div class="dialog-footer">
  181. <el-button :loading="importLoading" type="primary" @click="submitImportForm">确 定</el-button>
  182. <el-button @click="closeImportDialog">取 消</el-button>
  183. </div>
  184. </template>
  185. </el-dialog>
  186. </div>
  187. </template>
  188. <script setup name="Judge" lang="ts">
  189. import { listJudge, getJudge, delJudge, addJudge, updateJudge, downloadImportTemplates, uploadJudgeFileImport } from '@/api/system/physical/judge';
  190. import { JudgeVO, JudgeQuery, JudgeForm } from '@/api/system/physical/judge/types';
  191. import { uploadTournament } from '@/api/system/business/tournaments';
  192. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  193. import { parseTime } from '@/utils/dateUtils';
  194. import { selectJudgePositionSelList } from '@/api/system/physical/judgePosition';
  195. const judgeList = ref<JudgeVO[]>([]);
  196. const buttonLoading = ref(false);
  197. const loading = ref(true);
  198. const showSearch = ref(true);
  199. const ids = ref<Array<string | number>>([]);
  200. const single = ref(true);
  201. const multiple = ref(true);
  202. const total = ref(0);
  203. const queryFormRef = ref<ElFormInstance>();
  204. const judgeFormRef = ref<ElFormInstance>();
  205. const dialog = reactive<DialogOption>({
  206. visible: false,
  207. title: ''
  208. });
  209. const initFormData: JudgeForm = {
  210. id: undefined,
  211. name: undefined,
  212. peopleIconUrl: undefined,
  213. workYears: undefined
  214. };
  215. const data = reactive<PageData<JudgeForm, JudgeQuery>>({
  216. form: { ...initFormData },
  217. queryParams: {
  218. pageNum: 1,
  219. pageSize: 10,
  220. name: undefined,
  221. peopleIconUrl: undefined,
  222. workYears: undefined,
  223. params: {}
  224. },
  225. rules: {
  226. id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
  227. name: [{ required: true, message: '裁判姓名不能为空', trigger: 'blur' }]
  228. }
  229. });
  230. const { queryParams, form, rules } = toRefs(data);
  231. /** 查询裁判管理列表 */
  232. const getList = async () => {
  233. loading.value = true;
  234. const res = await listJudge(queryParams.value);
  235. judgeList.value = res.rows;
  236. total.value = res.total;
  237. loading.value = false;
  238. };
  239. /** 取消按钮 */
  240. const cancel = () => {
  241. reset();
  242. dialog.visible = false;
  243. };
  244. /** 表单重置 */
  245. const reset = () => {
  246. form.value = { ...initFormData };
  247. judgeFileList.value = [];
  248. // 清除预览图和临时链接
  249. judgeIconPreviewUrl.value = '';
  250. judgeFormRef.value?.resetFields();
  251. };
  252. /** 搜索按钮操作 */
  253. const handleQuery = () => {
  254. queryParams.value.pageNum = 1;
  255. getList();
  256. };
  257. /** 重置按钮操作 */
  258. const resetQuery = () => {
  259. queryFormRef.value?.resetFields();
  260. handleQuery();
  261. };
  262. /** 多选框选中数据 */
  263. const handleSelectionChange = (selection: JudgeVO[]) => {
  264. ids.value = selection.map((item) => item.id);
  265. single.value = selection.length != 1;
  266. multiple.value = !selection.length;
  267. };
  268. /** 新增按钮操作 */
  269. const handleAdd = () => {
  270. reset();
  271. dialog.visible = true;
  272. dialog.title = '添加';
  273. };
  274. /** 修改按钮操作 */
  275. const handleUpdate = async (row?: JudgeVO) => {
  276. reset();
  277. const _id = row?.id || ids.value[0];
  278. const res = await getJudge(_id);
  279. Object.assign(form.value, res.data);
  280. // 如果有图片URL,创建虚拟文件对象以便显示
  281. if (res.data.peopleIconUrl) {
  282. judgeFileList.value = [
  283. {
  284. name: '已上传图片',
  285. url: res.data.peopleIconUrl,
  286. status: 'success'
  287. }
  288. ];
  289. judgeIconPreviewUrl.value = res.data.peopleIconUrl;
  290. } else {
  291. judgeFileList.value = [];
  292. judgeIconPreviewUrl.value = '';
  293. }
  294. dialog.visible = true;
  295. dialog.title = '修改';
  296. };
  297. /** 提交按钮 */
  298. const submitForm = () => {
  299. judgeFormRef.value?.validate(async (valid: boolean) => {
  300. if (valid) {
  301. buttonLoading.value = true;
  302. if (form.value.id) {
  303. await updateJudge(form.value).finally(() => (buttonLoading.value = false));
  304. } else {
  305. await addJudge(form.value).finally(() => (buttonLoading.value = false));
  306. }
  307. proxy?.$modal.msgSuccess('操作成功');
  308. dialog.visible = false;
  309. await getList();
  310. }
  311. });
  312. };
  313. /** 删除按钮操作 */
  314. const handleDelete = async (row?: JudgeVO) => {
  315. const _ids = row?.id || ids.value;
  316. await proxy?.$modal.confirm('是否确认删除编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  317. await delJudge(_ids);
  318. proxy?.$modal.msgSuccess('删除成功');
  319. await getList();
  320. };
  321. /** 导出按钮操作 */
  322. const handleExport = () => {
  323. proxy?.download(
  324. 'physical/judge/export',
  325. {
  326. ...queryParams.value
  327. },
  328. `裁判列表${parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xlsx`
  329. );
  330. };
  331. onMounted(() => {
  332. getList();
  333. loadPositionOptions();
  334. });
  335. // 裁判图片上传相关
  336. const judgeIconPreviewUrl = ref('');
  337. const judgeFileList = ref([]);
  338. // 预览弹窗相关
  339. const judgeDialogVisible = ref(false);
  340. const judgePreviewSrc = ref('');
  341. // 裁判图片上传处理函数
  342. const handleJudgeIconChange = async (file) => {
  343. const index = judgeFileList.value.findIndex((f) => f.uid === file.uid);
  344. judgeFileList.value = [];
  345. if (file.raw) {
  346. judgeIconPreviewUrl.value = URL.createObjectURL(file.raw);
  347. }
  348. if (index === -1) {
  349. // 如果文件不在列表中,则添加进去
  350. judgeFileList.value.push(file);
  351. }
  352. try {
  353. const rawFile = file.raw;
  354. // 这里需要根据你的上传API进行调整,假设有一个上传图片的API
  355. const res = await uploadTournament(rawFile); // 使用现有的上传API或替换为你的API
  356. if (res.code === 200) {
  357. // 更新文件状态为成功
  358. judgeFileList.value[index] = {
  359. ...file,
  360. status: 'success',
  361. response: res.data.url
  362. };
  363. form.value.peopleIconUrl = judgeFileList.value[index].response;
  364. judgeIconPreviewUrl.value = judgeFileList.value[index].response;
  365. ElMessage.success('上传成功');
  366. } else {
  367. throw new Error(res.msg);
  368. }
  369. } catch (error) {
  370. // 更新文件状态为失败
  371. judgeFileList.value[index] = {
  372. ...file,
  373. status: 'fail',
  374. error: '上传失败'
  375. };
  376. ElMessage.error('上传失败,请重试');
  377. }
  378. };
  379. // 删除裁判图片处理函数
  380. const handleJudgeIconRemove = (file, updatedFileList) => {
  381. judgeFileList.value = updatedFileList;
  382. // 清除预览图和临时链接
  383. judgeIconPreviewUrl.value = '';
  384. // 同时清空表单数据中的图片链接
  385. form.value.peopleIconUrl = undefined;
  386. };
  387. // 点击预览图触发放大
  388. const handleJudgePreviewClick = () => {
  389. const currentSrc = judgeIconPreviewUrl.value || form.value.peopleIconUrl;
  390. if (currentSrc) {
  391. judgePreviewSrc.value = currentSrc;
  392. judgeDialogVisible.value = true;
  393. }
  394. };
  395. // 打开预览弹窗
  396. const openJudgePreview = (src: string) => {
  397. judgePreviewSrc.value = src;
  398. judgeDialogVisible.value = true;
  399. };
  400. const downloadTemplate = async () => {
  401. try {
  402. const response = await downloadImportTemplates();
  403. // 创建 Blob 对象(直接使用 response)
  404. // 确保正确获取 blob 数据
  405. const blobData = new Blob([response.data || response], {
  406. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  407. });
  408. // 创建对象 URL
  409. const urlBlob = window.URL.createObjectURL(blobData);
  410. const link = document.createElement('a');
  411. link.href = urlBlob;
  412. link.setAttribute('download', '裁判模板.xlsx');
  413. document.body.appendChild(link);
  414. // 触发点击事件并稍作延迟,确保链接生效
  415. link.click();
  416. setTimeout(() => {
  417. link.remove();
  418. window.URL.revokeObjectURL(urlBlob);
  419. }, 0);
  420. } catch (error) {
  421. console.error('Download error:', error); // 输出具体的错误信息
  422. ElMessage.error('下载失败,请重试');
  423. }
  424. };
  425. // 导入相关数据
  426. const importDialog = reactive({
  427. visible: false,
  428. title: '导入裁判数据'
  429. });
  430. const importForm = ref({
  431. file: undefined
  432. });
  433. const importRules = {
  434. file: [{ required: true, message: '请选择要上传的文件', trigger: 'change' }]
  435. };
  436. const importFileList = ref([]);
  437. const importLoading = ref(false);
  438. const importFormRef = ref<ElFormInstance>();
  439. // 导入相关函数
  440. const openImportDialog = () => {
  441. importForm.value = { file: undefined };
  442. importFileList.value = [];
  443. importDialog.visible = true;
  444. };
  445. const closeImportDialog = () => {
  446. importDialog.visible = false;
  447. importFormRef.value?.clearValidate();
  448. };
  449. const handleImportFileChange = (file) => {
  450. const index = importFileList.value.findIndex((f) => f.uid === file.uid);
  451. // 如果文件不存在于列表中,则添加进去
  452. if (index === -1) {
  453. importFileList.value.push(file);
  454. // 同步到表单数据
  455. importForm.value.file = file.raw;
  456. }
  457. };
  458. // 删除文件处理函数
  459. const handleImportFileRemove = (file) => {
  460. const index = importFileList.value.findIndex((f) => f.uid === file.uid);
  461. if (index > -1) {
  462. const newFileList = [...importFileList.value];
  463. newFileList.splice(index, 1);
  464. importFileList.value = newFileList;
  465. }
  466. };
  467. const submitImportForm = () => {
  468. importFormRef.value?.validate(async (valid: boolean) => {
  469. if (valid) {
  470. if (importFileList.value.length === 0) {
  471. proxy?.$modal.msgError('请先选择文件');
  472. return;
  473. }
  474. importLoading.value = true;
  475. try {
  476. const formData = new FormData();
  477. formData.append('file', importFileList.value[0].raw);
  478. // 调用导入API(需要在API文件中添加importJudge函数)
  479. const res = await uploadJudgeFileImport(formData);
  480. if (res.code === 200) {
  481. proxy?.$modal.msgSuccess('导入成功');
  482. importDialog.visible = false;
  483. await getList(); // 重新加载数据
  484. } else {
  485. proxy?.$modal.msgError(res.msg || '导入失败');
  486. }
  487. } catch (error) {
  488. console.error('导入失败:', error);
  489. proxy?.$modal.msgError('导入失败,请重试');
  490. } finally {
  491. importLoading.value = false;
  492. }
  493. }
  494. });
  495. };
  496. const positionOptions = ref<{ id: number; label: string }[]>([]);
  497. const loadPositionOptions = async () => {
  498. try {
  499. const res = await selectJudgePositionSelList();
  500. if (res.code === 200) {
  501. // 使用 unknown 中间类型进行类型转换
  502. const data = res.data as unknown as { id: number; name: string }[];
  503. const list = [];
  504. for (let i = 0; i < data.length; i++) {
  505. const item = data[i];
  506. list.push({
  507. id: item.id,
  508. label: item.name
  509. });
  510. }
  511. positionOptions.value = list;
  512. } else {
  513. alert('加载失败:' + res.msg);
  514. }
  515. } catch (error) {
  516. console.error('请求出错:', error);
  517. }
  518. };
  519. </script>