index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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>
  11. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  12. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  13. </el-form-item>
  14. </el-form>
  15. </el-card>
  16. </div>
  17. </transition>
  18. <el-card shadow="never">
  19. <template #header>
  20. <el-row :gutter="10" class="mb8">
  21. <el-col :span="1.5">
  22. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['business:tag:add']">新增</el-button>
  23. </el-col>
  24. <!-- <el-col :span="1.5">
  25. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['business:tag:edit']">修改</el-button>
  26. </el-col>
  27. <el-col :span="1.5">
  28. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['business:tag:remove']">删除</el-button>
  29. </el-col>
  30. <el-col :span="1.5">
  31. <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['business:tag:export']">导出</el-button>
  32. </el-col>-->
  33. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  34. </el-row>
  35. </template>
  36. <el-table v-loading="loading" border :data="tagList">
  37. <!-- <el-table-column type="selection" width="55" align="center" />-->
  38. <el-table-column label="编号" align="center" prop="id" v-if="true" />
  39. <el-table-column label="类目名称" align="center" prop="name" />
  40. <el-table-column label="图标" align="center" prop="iconUrl" width="120">
  41. <template #default="scope">
  42. <img v-if="scope.row.iconUrl" :src="scope.row.iconUrl" alt="" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px" />
  43. <span v-else>无</span>
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="排序" align="center" prop="sortWeight" />
  47. <el-table-column label="应用状态" align="center" prop="status">
  48. <template #default="scope">
  49. <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
  50. {{ scope.row.status === 1 ? '启用' : '禁用' }}
  51. </el-tag>
  52. </template>
  53. </el-table-column>
  54. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  55. <template #default="scope">
  56. <el-tooltip content="修改" placement="top">
  57. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['business:tag:edit']"></el-button>
  58. </el-tooltip>
  59. <el-tooltip content="删除" placement="top">
  60. <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['business:tag:remove']"></el-button>
  61. </el-tooltip>
  62. </template>
  63. </el-table-column>
  64. </el-table>
  65. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  66. </el-card>
  67. <!-- 添加或修改类目与标签合并对话框 -->
  68. <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
  69. <el-form ref="tagFormRef" :model="form" :rules="rules" label-width="80px">
  70. <el-form-item label="类目名称" prop="name">
  71. <el-input v-model="form.name" placeholder="请输入类目名称" />
  72. </el-form-item>
  73. <el-form-item label="图标" prop="iconUrl">
  74. <div class="upload-container">
  75. <el-upload
  76. class="upload-icon"
  77. action="#"
  78. :on-change="handleIconChange"
  79. :on-remove="handleIconRemove"
  80. :file-list="fileList"
  81. :auto-upload="false"
  82. :limit="1"
  83. accept="image/*"
  84. >
  85. <template #trigger>
  86. <el-button type="primary">点击选择图标</el-button>
  87. </template>
  88. <!-- 预览图区域 -->
  89. <template #default>
  90. <div class="preview-area" @click="handlePreviewClick">
  91. <img
  92. v-if="iconPreviewUrl || form.iconUrl"
  93. :src="iconPreviewUrl || form.iconUrl"
  94. alt="预览图"
  95. style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
  96. />
  97. <div v-else style="margin-top: 10px; color: #999">无</div>
  98. </div>
  99. </template>
  100. <template #tip>
  101. <div class="el-upload__tip">
  102. <span v-if="fileList.length > 0">当前已选文件:{{ fileList[0].name }}</span>
  103. </div>
  104. </template>
  105. </el-upload>
  106. </div>
  107. </el-form-item>
  108. <el-form-item label="排序" prop="sortWeight">
  109. <el-input v-model="form.sortWeight" placeholder="请输入排序" />
  110. </el-form-item>
  111. <el-form-item label="应用状态" prop="status">
  112. <el-radio-group v-model="form.status">
  113. <el-radio :label="1">启用</el-radio>
  114. <el-radio :label="0">禁用</el-radio>
  115. </el-radio-group>
  116. </el-form-item>
  117. </el-form>
  118. <template #footer>
  119. <div class="dialog-footer">
  120. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  121. <el-button @click="cancel">取 消</el-button>
  122. </div>
  123. </template>
  124. </el-dialog>
  125. <!-- 图片预览弹窗 -->
  126. <el-dialog v-model="dialogVisible" title="图片预览" width="50%">
  127. <img :src="previewSrc || iconPreviewUrl || form.iconUrl" alt="预览图片" style="max-width: 100%; max-height: 80vh" />
  128. </el-dialog>
  129. </div>
  130. </template>
  131. <script setup name="Tag" lang="ts">
  132. import { listTag, getTag, delTag, addTag, updateTag } from '@/api/system/business/tag';
  133. import { TagVO, TagQuery, TagForm } from '@/api/system/business/tag/types';
  134. import { uploadTournament } from '@/api/system/business/tournaments';
  135. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  136. const tagList = ref<TagVO[]>([]);
  137. const buttonLoading = ref(false);
  138. const loading = ref(true);
  139. const showSearch = ref(true);
  140. const ids = ref<Array<string | number>>([]);
  141. const single = ref(true);
  142. const multiple = ref(true);
  143. const total = ref(0);
  144. const queryFormRef = ref<ElFormInstance>();
  145. const tagFormRef = ref<ElFormInstance>();
  146. const dialog = reactive<DialogOption>({
  147. visible: false,
  148. title: ''
  149. });
  150. const initFormData: TagForm = {
  151. id: undefined,
  152. name: undefined,
  153. type: 'CATEGORY',
  154. iconUrl: undefined,
  155. sortWeight: undefined,
  156. status: 1,
  157. deletedAt: undefined
  158. };
  159. const data = reactive<PageData<TagForm, TagQuery>>({
  160. form: { ...initFormData },
  161. queryParams: {
  162. pageNum: 1,
  163. pageSize: 10,
  164. name: undefined,
  165. type: 'CATEGORY',
  166. iconUrl: undefined,
  167. sortWeight: undefined,
  168. status: undefined,
  169. deletedAt: undefined,
  170. params: {}
  171. },
  172. rules: {
  173. id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
  174. name: [
  175. { required: true, message: '名称,最多6字不能为空', trigger: 'blur' },
  176. { min: 2, max: 6, message: '名称长度必须在2到6个字符之间', trigger: 'blur' }
  177. ],
  178. type: [{ required: true, message: '类型:CATEGORY=类目,TAG=标签不能为空', trigger: 'change' }],
  179. sortWeight: [{ required: true, message: '排序权重,数值越小越靠前不能为空', trigger: 'blur' }],
  180. status: [{ required: true, message: '应用状态:1=启用,0=禁用不能为空', trigger: 'change' }]
  181. }
  182. });
  183. const { queryParams, form, rules } = toRefs(data);
  184. /** 查询类目与标签合并列表 */
  185. const getList = async () => {
  186. loading.value = true;
  187. const res = await listTag(queryParams.value);
  188. tagList.value = res.rows;
  189. total.value = res.total;
  190. loading.value = false;
  191. };
  192. /** 取消按钮 */
  193. const cancel = () => {
  194. reset();
  195. dialog.visible = false;
  196. };
  197. /** 表单重置 */
  198. const reset = () => {
  199. form.value = { ...initFormData };
  200. tagFormRef.value?.resetFields();
  201. // 清空上传相关状态
  202. fileList.value = [];
  203. iconPreviewUrl.value = '';
  204. form.value.iconUrl = '';
  205. };
  206. /** 搜索按钮操作 */
  207. const handleQuery = () => {
  208. queryParams.value.pageNum = 1;
  209. getList();
  210. };
  211. /** 重置按钮操作 */
  212. const resetQuery = () => {
  213. queryFormRef.value?.resetFields();
  214. handleQuery();
  215. };
  216. /** 新增按钮操作 */
  217. const handleAdd = () => {
  218. reset();
  219. dialog.visible = true;
  220. dialog.title = '添加类目';
  221. // 清空上传相关状态
  222. fileList.value = [];
  223. iconPreviewUrl.value = '';
  224. form.value.iconUrl = '';
  225. };
  226. /** 修改按钮操作 */
  227. const handleUpdate = async (row?: TagVO) => {
  228. reset();
  229. const _id = row?.id || ids.value[0];
  230. const res = await getTag(_id);
  231. Object.assign(form.value, res.data);
  232. // 设置预览图
  233. if (form.value.iconUrl) {
  234. iconPreviewUrl.value = form.value.iconUrl;
  235. }
  236. dialog.visible = true;
  237. dialog.title = '修改类目';
  238. };
  239. /** 提交按钮 */
  240. const submitForm = () => {
  241. tagFormRef.value?.validate(async (valid: boolean) => {
  242. if (valid) {
  243. buttonLoading.value = true;
  244. if (form.value.id) {
  245. await updateTag(form.value).finally(() => (buttonLoading.value = false));
  246. } else {
  247. await addTag(form.value).finally(() => (buttonLoading.value = false));
  248. }
  249. proxy?.$modal.msgSuccess('操作成功');
  250. dialog.visible = false;
  251. await getList();
  252. }
  253. });
  254. };
  255. /** 删除按钮操作 */
  256. const handleDelete = async (row?: TagVO) => {
  257. const _ids = row?.id || ids.value;
  258. await proxy?.$modal.confirm('是否确认删除编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  259. await delTag(_ids);
  260. proxy?.$modal.msgSuccess('删除成功');
  261. await getList();
  262. };
  263. /** 导出按钮操作 */
  264. const handleExport = () => {
  265. proxy?.download(
  266. 'business/tag/export',
  267. {
  268. ...queryParams.value
  269. },
  270. `tag_${new Date().getTime()}.xlsx`
  271. );
  272. };
  273. onMounted(() => {
  274. getList();
  275. });
  276. // 添加响应式变量(在其他ref声明附近添加)
  277. const iconPreviewUrl = ref('');
  278. const fileList = ref([]);
  279. const dialogVisible = ref(false);
  280. const previewSrc = ref('');
  281. /** 处理图标上传 */
  282. const handleIconChange = async (file) => {
  283. const index = fileList.value.findIndex((f) => f.uid === file.uid);
  284. fileList.value = [];
  285. if (file.raw) {
  286. iconPreviewUrl.value = URL.createObjectURL(file.raw);
  287. }
  288. if (index === -1) {
  289. fileList.value.push(file);
  290. }
  291. try {
  292. const rawFile = file.raw;
  293. const res = await uploadTournament(rawFile);
  294. if (res.code === 200) {
  295. fileList.value[index] = {
  296. ...file,
  297. status: 'success',
  298. response: res.data.url
  299. };
  300. form.value.iconUrl = fileList.value[index].response;
  301. iconPreviewUrl.value = fileList.value[index].response;
  302. proxy?.$modal.msgSuccess('上传成功');
  303. } else {
  304. throw new Error(res.msg);
  305. }
  306. } catch (error) {
  307. fileList.value[index] = {
  308. ...file,
  309. status: 'fail',
  310. error: '上传失败'
  311. };
  312. proxy?.$modal.msgError('上传失败,请重试');
  313. }
  314. };
  315. /** 处理图标删除 */
  316. const handleIconRemove = (file, updatedFileList) => {
  317. fileList.value = updatedFileList;
  318. iconPreviewUrl.value = '';
  319. form.value.iconUrl = '';
  320. };
  321. /** 点击预览图触发放大 */
  322. const handlePreviewClick = () => {
  323. const currentSrc = iconPreviewUrl.value || form.value.iconUrl;
  324. if (currentSrc) {
  325. previewSrc.value = currentSrc;
  326. dialogVisible.value = true;
  327. }
  328. };
  329. </script>