Răsfoiți Sursa

```
feat(tournaments): 增加比赛背景图功能并优化相关交互- 在比赛管理页面新增“比赛背景”图片上传与预览功能- 支持比赛背景图的上传、删除及预览弹窗展示
- 添加比赛状态筛选条件,便于快速查找不同状态的比赛- 调整表单重置逻辑,确保时间范围和昵称等字段正确清空
-修复部分字典引用缺失问题,完善类型定义
- 注释掉产品管理中的“关联道具”查询项,暂不使用
- 将 API 用户搜索框标签由“登录名称”改为“用户昵称”,提升语义准确性
```

fugui001 2 luni în urmă
părinte
comite
ca4b56a5f2

+ 2 - 0
src/api/system/business/tournaments/types.ts

@@ -76,6 +76,7 @@ export interface TournamentsVO {
    */
   blindStructureId?: number;
   competitionIcon?: string;
+  competitionBg?: string;
   blindStructuresName?: string;
   itemsName?: string;
   itemsPrizeList?: ItemsPrize[];
@@ -163,6 +164,7 @@ export interface TournamentsForm extends BaseEntity {
   robotCount?: number;
   delayCardNum?: string;
   delayCardTime?: string;
+  competitionBg?: string;
 }
 
 export interface ItemsPrize {

+ 6 - 2
src/views/system/business/apiUsers/index.vue

@@ -4,8 +4,8 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="登录名称" prop="loginName">
-              <el-input v-model="queryParams.loginName" placeholder="请输入登录名称" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="用户昵称" prop="loginName">
+              <el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
 
             <el-form-item label="手机号" prop="phone">
@@ -463,6 +463,10 @@ const handleQuery = () => {
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
+  // 重置时间范围选择器
+  data.queryParams.registerTimeRange = '';
+  data.queryParams.loginTimeRange = '';
+  data.queryParams.nickName = '';
   handleQuery();
 };
 

+ 2 - 2
src/views/system/business/products/index.vue

@@ -13,9 +13,9 @@
             <el-form-item label="说明" prop="description">
               <el-input v-model="queryParams.description" placeholder="请输入说明" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="关联道具" prop="relatedItemId">
+<!--            <el-form-item label="关联道具" prop="relatedItemId">
               <el-input v-model="queryParams.relatedItemId" placeholder="请输入关联道具" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
+            </el-form-item>-->
             <el-form-item label="道具值" prop="quantity">
               <el-input v-model="queryParams.quantity" placeholder="请输入道具值" clearable @keyup.enter="handleQuery" />
             </el-form-item>

+ 145 - 2
src/views/system/business/tournaments/index.vue

@@ -12,6 +12,12 @@
               <el-input v-model="queryParams.name" placeholder="请输入比赛名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
 
+            <el-form-item label="比赛状态" prop="gameType">
+              <el-select aria-required="true" v-model="queryParams.status" placeholder="请选择" :disabled="dialog.mode === 'view'">
+                <el-option v-for="dict in tournaments_status" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
+              </el-select>
+            </el-form-item>
+
             <el-form-item label="开始时间" prop="startTime">
               <el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择比赛开始时间" />
             </el-form-item>
@@ -84,6 +90,21 @@
             <span v-else></span>
           </template>
         </el-table-column>
+
+        <el-table-column label="比赛背景" align="center" width="90">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.competitionBg"
+              :src="scope.row.competitionBg"
+              style="width: 40px; height: 40px; border-radius: 4px; cursor: zoom-in"
+              :preview-src-list="[scope.row.competitionBg]"
+              :preview-teleported="true"
+              fit="cover"
+            />
+            <span v-else></span>
+          </template>
+        </el-table-column>
+
         <el-table-column label="开始时间" align="center" prop="startTime" width="150" sortable="custom"> </el-table-column>
         <el-table-column label="结束时间" align="center" prop="endTime" width="150"> </el-table-column>
         <el-table-column label="报名人数" align="center" prop="signNum" width="80"> </el-table-column>
@@ -265,6 +286,48 @@
           </div>
         </el-form-item>
 
+        <!-- 比赛图标 -->
+        <el-form-item label="比赛背景" prop="icon">
+          <div class="upload-container">
+            <el-upload
+              class="upload-icon"
+              action="#"
+              :on-change="handleIconChange2"
+              :on-remove="handleIconRemove2"
+              :file-list="fileList2"
+              :auto-upload="false"
+              :limit="1"
+              accept="image/*"
+              :disabled="dialog.mode === 'view'"
+            >
+              <template #trigger>
+                <el-button type="primary" :disabled="dialog.mode === 'view'">点击选择背景</el-button>
+              </template>
+
+              <!-- 预览图区域 -->
+              <template #default>
+                <div class="preview-area" @click="handlePreviewClick2">
+                  <!-- 只有当 iconPreviewUrl 或 competitionIcon 存在时才显示 img -->
+                  <img
+                    v-if="iconPreviewUrl2 || competitionBg"
+                    :src="iconPreviewUrl2 || competitionBg"
+                    alt="预览图"
+                    style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
+                  />
+                  <!-- 可选:无图时显示提示文字 -->
+                  <div v-else style="margin-top: 10px; color: #999"></div>
+                </div>
+              </template>
+
+              <template #tip>
+                <div class="el-upload__tip">
+                  <span v-if="fileList2.length > 0">当前已选文件:{{ fileList2[0].name }}</span>
+                </div>
+              </template>
+            </el-upload>
+          </div>
+        </el-form-item>
+
         <!-- 开始时间 -->
         <el-form-item label="开始时间" prop="startTime">
           <el-date-picker
@@ -454,6 +517,10 @@
       <img :src="previewSrc || iconPreviewUrl || competitionIcon" alt="预览图片" style="max-width: 100%; max-height: 80vh" />
     </el-dialog>
 
+    <el-dialog v-model="dialogVisible2" title="图片预览" width="50%">
+      <img :src="previewSrc2 || iconPreviewUrl2 || competitionBg" alt="预览图片" style="max-width: 100%; max-height: 80vh" />
+    </el-dialog>
+
     <el-dialog
       :title="`${tournamentInfo.name} ${tournamentInfo.tournamentsBiId}`"
       v-model="auditDialog.visible"
@@ -567,7 +634,9 @@ import { ref } from 'vue';
 import LevelsIndex from '@/views/system/business/levels/index.vue';
 import { ClaimsVO } from '@/api/system/business/claims/types';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { tournaments_type, tournaments_time } = toRefs<any>(proxy?.useDict('tournaments_type', 'tournaments_time'));
+const { tournaments_type, tournaments_time, tournaments_status } = toRefs<any>(
+  proxy?.useDict('tournaments_type', 'tournaments_time', 'tournaments_status')
+);
 
 const tournamentsList = ref<TournamentsVO[]>([]);
 const buttonLoading = ref(false);
@@ -603,11 +672,17 @@ const assignForm = reactive({
 const dialogVisible = ref(false);
 const previewSrc = ref('');
 
+const dialogVisible2 = ref(false);
+const previewSrc2 = ref('');
+
 function openPreview(src: string) {
   previewSrc.value = src;
   dialogVisible.value = true;
 }
-
+function openPreview2(src: string) {
+  previewSrc2.value = src;
+  dialogVisible2.value = true;
+}
 // 控制 Dialog 是否显示
 const levelsDialogVisible = ref(false);
 // 传递给子组件的参数
@@ -740,6 +815,12 @@ const removeReward = (index: number) => {
 const iconPreviewUrl = ref('');
 const competitionIcon = ref('');
 const fileList = ref([]);
+
+//预览图标需要
+const iconPreviewUrl2 = ref('');
+const competitionBg = ref('');
+const fileList2 = ref([]);
+
 import { parseTime } from '@/utils/dateUtils';
 // 单选控制变量(用于绑定 el-radio)
 const selectedRadio = ref<number | null>(null);
@@ -913,6 +994,9 @@ const cancel = () => {
   // 清除预览图和临时链接
   iconPreviewUrl.value = '';
   competitionIcon.value = ''; // 如果需要清除后台加载的图标,可以在这里设置为空字符串
+
+  iconPreviewUrl2.value = '';
+  competitionBg.value = '';
   dialog.visible = false;
 };
 
@@ -955,6 +1039,8 @@ const handleAdd = () => {
   // 清除预览图和临时链接
   iconPreviewUrl.value = '';
   competitionIcon.value = ''; // 如果需要清除后台加载的图标,可以在这里设置为空字符串
+  iconPreviewUrl2.value = '';
+  competitionBg.value = '';
   dialog.visible = true;
   dialog.title = '创建比赛';
   dialog.mode = 'add'; // 设置模式
@@ -975,6 +1061,7 @@ const handleUpdate = async (row?: TournamentsVO, mode: 'edit' | 'view' = 'edit')
   form.value.signTime = String(res.data.signTime);
   form.value.gameType = String(res.data.gameType); // 转为字符串
   competitionIcon.value = res.data.competitionIcon;
+  competitionBg.value = res.data.competitionBg;
   // 处理奖励表单数据
   const prizeItems = res.data.itemsPrizeList || [];
   if (prizeItems.length > 0) {
@@ -1240,6 +1327,46 @@ const handleIconChange = async (file) => {
   }
 };
 
+const handleIconChange2 = async (file) => {
+  const index = fileList2.value.findIndex((f) => f.uid === file.uid);
+  fileList2.value = [];
+
+  if (file.raw) {
+    iconPreviewUrl2.value = URL.createObjectURL(file.raw);
+  }
+
+  if (index === -1) {
+    // 如果文件不在列表中,则添加进去
+    fileList2.value.push(file);
+  }
+
+  try {
+    const rawFile = file.raw;
+    const res = await uploadTournament(rawFile);
+    if (res.code === 200) {
+      // 更新文件状态为成功
+      fileList2.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      data.form.competitionBg = fileList2.value[index].response;
+      iconPreviewUrl2.value = fileList2.value[index].response;
+      ElMessage.success('上传成功');
+    } else {
+      throw new Error(res.msg);
+    }
+  } catch (error) {
+    // 更新文件状态为失败
+    fileList2.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('上传失败,请重试');
+  }
+};
+
 const handleViewLevels = () => {
   const blindStructureId = data.form.blindStructureId;
   if (blindStructureId === null || blindStructureId === undefined) {
@@ -1258,6 +1385,14 @@ const handlePreviewClick = () => {
     dialogVisible.value = true;
   }
 };
+// 点击预览图触发放大
+const handlePreviewClick2 = () => {
+  const currentSrc = iconPreviewUrl2.value || competitionBg.value;
+  if (currentSrc) {
+    dialogVisible2.value = true;
+  }
+};
+
 // 删除文件处理函数
 const handleIconRemove = (file, updatedFileList) => {
   fileList.value = updatedFileList;
@@ -1266,6 +1401,14 @@ const handleIconRemove = (file, updatedFileList) => {
   competitionIcon.value = ''; // 如果需要清除后台加载的图标,可以在这里设置为空字符串
 };
 
+// 删除文件处理函数
+const handleIconRemove2 = (file, updatedFileList) => {
+  fileList2.value = updatedFileList;
+  // 清除预览图和临时链接
+  iconPreviewUrl2.value = '';
+  competitionBg.value = ''; // 如果需要清除后台加载的图标,可以在这里设置为空字符串
+};
+
 // 排序字段和顺序
 const sortData = ref({
   prop: 'startTime',