Explorar o código

feat(tournaments): 新增比赛创建和编辑功能

- 添加比赛名称、图标、开始时间等基本信息输入
- 实现报名时间、报名条件、盲注表等高级设置
- 增加奖励内容配置,支持多个奖励等级
- 集成物品列表、盲注等级等数据接口
-优化表单验证和数据提交逻辑
fugui001 hai 5 meses
pai
achega
ef941044f5

+ 11 - 0
src/api/system/business/items/index.ts

@@ -61,3 +61,14 @@ export const delItems = (id: string | number | Array<string | number>) => {
     method: 'delete'
     method: 'delete'
   });
   });
 };
 };
+
+/**
+ * 获取报名条件信息
+ * @param id
+ */
+export const selectItemsSelList = (): AxiosPromise<ItemsVO> => {
+  return request({
+    url: '/business/items/selectItemsSelList',
+    method: 'get'
+  });
+};

+ 11 - 0
src/api/system/business/levels/index.ts

@@ -61,3 +61,14 @@ export const delLevels = (id: string | number | Array<string | number>) => {
     method: 'delete'
     method: 'delete'
   });
   });
 };
 };
+
+/**
+ * 查询【请填写功能名称】详细
+ * @param id
+ */
+export const selectBlindLevelsById = (id: number): AxiosPromise<LevelsVO[]> => {
+  return request({
+    url: '/business/levels/selectBlindLevelsById/' + id,
+    method: 'get'
+  });
+};

+ 8 - 0
src/api/system/business/structures/index.ts

@@ -1,6 +1,7 @@
 import request from '@/utils/request';
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import { AxiosPromise } from 'axios';
 import { StructuresVO, StructuresForm, StructuresQuery } from '@/api/system/business/structures/types';
 import { StructuresVO, StructuresForm, StructuresQuery } from '@/api/system/business/structures/types';
+import { ItemsVO } from '@/api/system/business/items/types';
 
 
 /**
 /**
  * 查询【盲注结构】列表
  * 查询【盲注结构】列表
@@ -94,3 +95,10 @@ export const importData = async (file: File) => {
 
 
   return res;
   return res;
 };
 };
+
+export const selectBlingStructuresInfo = (): AxiosPromise<StructuresVO> => {
+  return request({
+    url: '/business/structures/selectBlingStructuresInfo',
+    method: 'get'
+  });
+};

+ 19 - 0
src/api/system/business/tournaments/index.ts

@@ -87,3 +87,22 @@ export const assignTournamentBlindStructures = (data: { tournamentId: number; bl
     data: data
     data: data
   });
   });
 };
 };
+
+/**
+ * 上传 比赛图标
+ * @param file 文件
+ */
+export const uploadTournament = async (file: File) => {
+  const formData = new FormData();
+  formData.append('file', file); // 后端接收的参数名是 "file"
+  const res = await request({
+    url: '/business/tournaments/uploadTournament',
+    method: 'POST',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+
+  return res;
+};

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

@@ -103,6 +103,34 @@ export interface TournamentsForm extends BaseEntity {
 
 
   /***/
   /***/
   updatedAt?: string;
   updatedAt?: string;
+
+  /**
+   * 报名时间
+   */
+  signTime?: number;
+
+  /**
+   * 道具ID
+   */
+  itemsId?: number;
+
+  /**
+   * 道具数量
+   */
+  itemsNum?: number;
+
+  /**
+   * 盲注表ID
+   */
+  blindStructureId?: number;
+  competitionIcon?: string;
+  itemsPrizeList?: ItemsPrize[];
+}
+
+export interface ItemsPrize {
+  ranking: number;
+  itemId: number;
+  quantity: number;
 }
 }
 
 
 export interface TournamentsQuery extends PageQuery {
 export interface TournamentsQuery extends PageQuery {

+ 334 - 22
src/views/system/business/tournaments/index.vue

@@ -101,7 +101,7 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
     </el-card>
     <!-- 添加或修改【请填写功能名称】对话框 -->
     <!-- 添加或修改【请填写功能名称】对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+    <!--    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="tournamentsFormRef" :model="form" :rules="rules" label-width="80px">
       <el-form ref="tournamentsFormRef" :model="form" :rules="rules" label-width="80px">
         <el-form-item label="赛事名称" prop="name">
         <el-form-item label="赛事名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入赛事名称" />
           <el-input v-model="form.name" placeholder="请输入赛事名称" />
@@ -129,29 +129,126 @@
           <el-button @click="cancel">取 消</el-button>
           <el-button @click="cancel">取 消</el-button>
         </div>
         </div>
       </template>
       </template>
-    </el-dialog>
+    </el-dialog>-->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body @close="cancel">
+      <el-form ref="tournamentsFormRef" :model="form" :rules="rules" label-width="120px">
+        <!-- 赛事名称 -->
+        <el-form-item label="比赛名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入比赛名称" />
+        </el-form-item>
+
+        <!-- 比赛图标 -->
+        <!--        <el-form-item label="比赛图标">
+          <el-button type="primary">点击选择图标</el-button>
+        </el-form-item>-->
+        <el-form-item label="比赛图标" prop="icon">
+          <div class="upload-container">
+            <el-upload
+              class="upload-icon"
+              action="#"
+              :on-change="handleIconChange"
+              :file-list="fileList"
+              :auto-upload="false"
+              :limit="1"
+              accept="image/*"
+            >
+              <template #trigger>
+                <el-button type="primary">点击选择图标</el-button>
+              </template>
+
+              <template #tip>
+                <div class="el-upload__tip">
+                  <!--                  请选择 PNG/JPG/JPEG 格式图片-->
+                  <span v-if="fileList.length > 0">当前已选文件:{{ fileList[0].name }} </span>
+                </div>
+              </template>
+            </el-upload>
+          </div>
+        </el-form-item>
+
+        <!-- 比赛开始时间 -->
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择开始时间">
+          </el-date-picker>
+        </el-form-item>
+
+        <!-- 报名时间 -->
+        <el-form-item label="报名时间">
+          <el-select v-model="form.signTime" placeholder="请选择">
+            <el-option label="比赛前5分钟" :value="5" />
+            <el-option label="比赛前10分钟" :value="10" />
+          </el-select>
+        </el-form-item>
+        <!-- 报名条件 -->
+
+        <el-form-item label="报名条件">
+          <div style="display: flex; align-items: center; gap: 10px">
+            <el-select v-model="form.itemsId" placeholder="请选择道具类型">
+              <el-option v-for="item in itemOptions" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+            <el-input v-model="form.itemsNum" :min="1" placeholder="数量" />
+          </div>
+        </el-form-item>
+
+        <!-- 盲注表 -->
+        <el-form-item label="盲注表">
+          <div style="display: flex; align-items: center">
+            <el-select v-model="form.blindStructureId" placeholder="选项" style="width: 200px" @change="handleBlindStructureChange">
+              <el-option v-for="item in itemOptionsStructures" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+
+            <el-button type="primary">上传新盲注</el-button>
+            <el-button type="primary" @click="handleViewLevels">预览</el-button>
+          </div>
+        </el-form-item>
+
+        <!-- 报名截止等级 itemOptionsStructuresLevel-->
+        <el-form-item label="报名截止等级">
+          <el-select v-model="form.lateRegistrationLevel" placeholder="选项" style="width: 200px">
+            <el-option v-for="item in itemOptionsStructuresLevel" :key="item.id" :label="item.label" :value="item.id" />
+          </el-select>
+        </el-form-item>
+
+        <!-- 奖励内容 -->
+        <el-form-item label="奖励内容">
+          <div v-for="(reward, index) in formPrize.rewards" :key="index" style="display: flex; align-items: center; margin-bottom: 8px">
+            <span>第{{ reward.ranking }}名</span>
+
+            <!-- 道具选择 -->
+            <el-select v-model="reward.itemId" placeholder="选项" style="width: 120px; margin-right: 8px">
+              <el-option v-for="item in itemOptions" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+
+            <!-- 数量输入 -->
+            <el-input v-model="reward.quantity" placeholder="请输入数量" style="width: 100px; margin-right: 8px"></el-input>
 
 
+            <!-- 操作按钮 -->
+            <el-button type="primary" @click="addReward" v-if="index === 0">+</el-button>
+            <el-button type="primary" @click="removeReward(index)" v-if="index !== 0">-</el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
     <!-- 分配模态框 -->
     <!-- 分配模态框 -->
     <el-dialog title="分配盲注信息" v-model="assignDialog.visible" width="800px" append-to-body destroy-on-close>
     <el-dialog title="分配盲注信息" v-model="assignDialog.visible" width="800px" append-to-body destroy-on-close>
       <el-form ref="assignFormRef" :model="assignForm" label-width="100px">
       <el-form ref="assignFormRef" :model="assignForm" label-width="100px">
         <!-- 可勾选的列表 -->
         <!-- 可勾选的列表 -->
         <el-form-item label="">
         <el-form-item label="">
-          <div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
-            <div style="width: 100%; height: 400px; overflow-y: auto; padding: 0;">
-              <el-table
-                border
-                ref="assignTable"
-                :data="assignList"
-
-                :row-key="'blindStructuresId'"
-                style="width: 100%"
-              >
+          <div style="display: flex; flex-direction: column; align-items: center; width: 100%">
+            <div style="width: 100%; height: 400px; overflow-y: auto; padding: 0">
+              <el-table border ref="assignTable" :data="assignList" :row-key="'blindStructuresId'" style="width: 100%">
                 <el-table-column label="操作" align="center" width="55">
                 <el-table-column label="操作" align="center" width="55">
                   <template #default="{ row }">
                   <template #default="{ row }">
                     <el-radio v-model="selectedRadio" :label="row.blindStructuresId">&nbsp;</el-radio>
                     <el-radio v-model="selectedRadio" :label="row.blindStructuresId">&nbsp;</el-radio>
                   </template>
                   </template>
                 </el-table-column>
                 </el-table-column>
-<!--                <el-table-column label="blindStructuresId" align="center" prop="blindStructuresId" width="100" show-overflow-tooltip />-->
+                <!--                <el-table-column label="blindStructuresId" align="center" prop="blindStructuresId" width="100" show-overflow-tooltip />-->
                 <el-table-column prop="blindStructuresName" label="盲注结构名称" align="center" width="150" show-overflow-tooltip />
                 <el-table-column prop="blindStructuresName" label="盲注结构名称" align="center" width="150" show-overflow-tooltip />
                 <el-table-column prop="blindDescription" label="盲注结构描述" align="center" width="200" show-overflow-tooltip />
                 <el-table-column prop="blindDescription" label="盲注结构描述" align="center" width="200" show-overflow-tooltip />
                 <el-table-column prop="allocationStatus" label="绑定情况" align="center" width="100">
                 <el-table-column prop="allocationStatus" label="绑定情况" align="center" width="100">
@@ -183,6 +280,11 @@
         </div>
         </div>
       </template>
       </template>
     </el-dialog>
     </el-dialog>
+
+    <el-dialog v-model="levelsDialogVisible" title="盲注等级列表" width="80%">
+      <!-- 使用 component 动态加载目标组件 -->
+      <levels-index :blind-structure-id="dialogParams.blindStructureId" :name="dialogParams.name" />
+    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -194,9 +296,15 @@ import {
   addTournaments,
   addTournaments,
   updateTournaments,
   updateTournaments,
   getSelectTournamentBlindStructuresList,
   getSelectTournamentBlindStructuresList,
-  assignTournamentBlindStructures
+  assignTournamentBlindStructures,
+  uploadTournament
 } from '@/api/system/business/tournaments';
 } from '@/api/system/business/tournaments';
+import { selectItemsSelList } from '@/api/system/business/items';
+import { selectBlindLevelsById } from '@/api/system/business/levels';
+import { selectBlingStructuresInfo } from '@/api/system/business/structures';
 import { TournamentsVO, TournamentsQuery, TournamentsForm, TournamentsBindStructuresVO } from '@/api/system/business/tournaments/types';
 import { TournamentsVO, TournamentsQuery, TournamentsForm, TournamentsBindStructuresVO } from '@/api/system/business/tournaments/types';
+import { ref } from 'vue';
+import LevelsIndex from '@/views/system/business/levels/index.vue';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const tournamentsList = ref<TournamentsVO[]>([]);
 const tournamentsList = ref<TournamentsVO[]>([]);
@@ -226,6 +334,129 @@ const assignForm = reactive({
   selectedIds: [] as number[]
   selectedIds: [] as number[]
 });
 });
 
 
+// 控制 Dialog 是否显示
+const levelsDialogVisible = ref(false);
+// 传递给子组件的参数
+const dialogParams = ref({
+  blindStructureId: null,
+  name: null
+});
+
+// 下拉选项数据 selectBlingStructuresInfo
+const itemOptions = ref<{ id: number; label: string }[]>([]);
+
+// 加载报名条件选项
+const loadItemOptions = async () => {
+  try {
+    const res = await selectItemsSelList();
+    if (res.code === 200) {
+      // 使用 unknown 中间类型进行类型转换
+      const data = res.data as unknown as { id: number; name: string }[];
+      const list = [];
+      for (let i = 0; i < data.length; i++) {
+        const item = data[i];
+        list.push({
+          id: item.id,
+          label: item.name
+        });
+      }
+      itemOptions.value = list;
+    } else {
+      alert('加载失败:' + res.msg);
+    }
+  } catch (error) {
+    console.error('请求出错:', error);
+  }
+};
+
+// 下拉选项数据 selectBlingStructuresInfo
+const itemOptionsStructures = ref<{ id: number; label: string }[]>([]);
+
+// 加载报名条件选项
+const loadItemStructuresOptions = async () => {
+  try {
+    const res = await selectBlingStructuresInfo();
+    if (res.code === 200) {
+      // 使用 unknown 中间类型进行类型转换
+      const data = res.data as unknown as { id: number; name: string }[];
+      const list = [];
+      for (let i = 0; i < data.length; i++) {
+        const item = data[i];
+        list.push({
+          id: item.id,
+          label: item.name
+        });
+      }
+      itemOptionsStructures.value = list;
+    } else {
+      alert('加载失败:' + res.msg);
+    }
+  } catch (error) {
+    console.error('请求出错:', error);
+  }
+};
+
+// 下拉选项数据
+const itemOptionsStructuresLevel = ref<{ id: number; label: string }[]>([]);
+
+// 加载报名条件选项
+const handleBlindStructureChange = async (value: number) => {
+  try {
+    const res = await selectBlindLevelsById(value);
+    if (res.code === 200) {
+      // 使用 unknown 中间类型进行类型转换
+      const data = res.data as unknown as { id: number; levelNumber: number }[];
+      const list = [];
+      for (let i = 0; i < data.length; i++) {
+        const item = data[i];
+        list.push({
+          id: item.levelNumber,
+          label: item.levelNumber
+        });
+      }
+      itemOptionsStructuresLevel.value = list;
+    } else {
+      alert('加载失败:' + res.msg);
+    }
+  } catch (error) {
+    console.error('请求出错:', error);
+  }
+};
+const formPrize = reactive({
+  // ...其他表单字段
+  rewards: [
+    {
+      ranking: 1,
+      itemId: '',
+      quantity: ''
+    }
+  ]
+});
+
+const addReward = () => {
+  const currentLength = formPrize.rewards.length;
+
+  // 判断是否超过 itemOptions 的数量限制
+  if (currentLength < itemOptions.value.length) {
+    formPrize.rewards.push({
+      ranking: currentLength + 1,  // 自动生成排名,从 1 开始
+      itemId: null,
+      quantity: ''
+    });
+  } else {
+    ElMessage.warning(`最多只能添加 ${itemOptions.value.length} 个奖励项`);
+  }
+};
+
+const removeReward = (index: number) => {
+  formPrize.rewards.splice(index, 1);
+
+  // 删除后重新设置排名
+  formPrize.rewards.forEach((r, i) => {
+    r.ranking = i + 1;
+  });
+};
+
 // 单选控制变量(用于绑定 el-radio)
 // 单选控制变量(用于绑定 el-radio)
 const selectedRadio = ref<number | null>(null);
 const selectedRadio = ref<number | null>(null);
 
 
@@ -254,7 +485,13 @@ const initFormData: TournamentsForm = {
   maxPlayers: undefined,
   maxPlayers: undefined,
   status: undefined,
   status: undefined,
   createdAt: undefined,
   createdAt: undefined,
-  updatedAt: undefined
+  updatedAt: undefined,
+  signTime: null,
+  competitionIcon: null,
+  itemsId: null,
+  itemsNum: null,
+  blindStructureId: null,
+  itemsPrizeList: []
 };
 };
 const data = reactive<PageData<TournamentsForm, TournamentsQuery>>({
 const data = reactive<PageData<TournamentsForm, TournamentsQuery>>({
   form: { ...initFormData },
   form: { ...initFormData },
@@ -301,6 +538,7 @@ const getList = async () => {
 const cancel = () => {
 const cancel = () => {
   reset();
   reset();
   dialog.visible = false;
   dialog.visible = false;
+  fileList.value = [];
 };
 };
 
 
 /** 表单重置 */
 /** 表单重置 */
@@ -332,7 +570,7 @@ const handleSelectionChange = (selection: TournamentsVO[]) => {
 const handleAdd = () => {
 const handleAdd = () => {
   reset();
   reset();
   dialog.visible = true;
   dialog.visible = true;
-  dialog.title = '添加【赛事信息】';
+  dialog.title = '创建比赛';
 };
 };
 
 
 /** 修改按钮操作 */
 /** 修改按钮操作 */
@@ -342,22 +580,49 @@ const handleUpdate = async (row?: TournamentsVO) => {
   const res = await getTournaments(_id);
   const res = await getTournaments(_id);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.visible = true;
-  dialog.title = '修改【赛事信息】';
+  dialog.title = '编辑比赛';
 };
 };
 
 
 /** 提交按钮 */
 /** 提交按钮 */
 const submitForm = () => {
 const submitForm = () => {
   tournamentsFormRef.value?.validate(async (valid: boolean) => {
   tournamentsFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
+    // 校验奖励内容是否至少填写了一项
+    if (!formPrize.rewards.some((r) => r.itemId && r.quantity)) {
+      ElMessage.warning('请至少填写一项奖励内容');
+      return;
+    }
+
+    if (!valid) return;
+    try {
       buttonLoading.value = true;
       buttonLoading.value = true;
-      if (form.value.id) {
-        await updateTournaments(form.value).finally(() => (buttonLoading.value = false));
+
+      // 构造最终要提交的数据对象
+      const formData: TournamentsForm = {
+        ...data.form,
+        competitionIcon: data.form.competitionIcon, // 确保 iconUrl 存在
+        itemsPrizeList: formPrize.rewards.map((reward) => ({
+          ranking: Number(reward.ranking),
+          itemId: Number(reward.itemId),
+          quantity: Number(reward.quantity)
+        }))
+      };
+
+      // 提交数据(区分新增/编辑)
+      let response;
+      if (formData.id) {
+        response = await updateTournaments(formData);
       } else {
       } else {
-        await addTournaments(form.value).finally(() => (buttonLoading.value = false));
+        response = await addTournaments(formData);
       }
       }
+
       proxy?.$modal.msgSuccess('操作成功');
       proxy?.$modal.msgSuccess('操作成功');
       dialog.visible = false;
       dialog.visible = false;
       await getList();
       await getList();
+    } catch (error) {
+      console.error('提交失败:', error);
+      proxy?.$modal.msgError('提交失败,请重试');
+    } finally {
+      buttonLoading.value = false;
     }
     }
   });
   });
 };
 };
@@ -384,6 +649,8 @@ const handleExport = () => {
 
 
 onMounted(() => {
 onMounted(() => {
   getList();
   getList();
+  loadItemOptions();
+  loadItemStructuresOptions();
 });
 });
 
 
 async function getAssignList() {
 async function getAssignList() {
@@ -416,7 +683,6 @@ async function submitAssign() {
     proxy?.$modal.msgError('请选择至少一个盲注结构');
     proxy?.$modal.msgError('请选择至少一个盲注结构');
     return;
     return;
   }
   }
-  debugger;
   buttonLoading.value = true;
   buttonLoading.value = true;
   try {
   try {
     await assignTournamentBlindStructures({
     await assignTournamentBlindStructures({
@@ -466,4 +732,50 @@ watch(
     }
     }
   }
   }
 );
 );
+
+const fileList = ref([]);
+const handleIconChange = async (file) => {
+  const index = fileList.value.findIndex((f) => f.uid === file.uid);
+  fileList.value = [];
+  if (index === -1) {
+    // 如果文件不在列表中,则添加进去
+    fileList.value.push(file);
+  }
+
+  try {
+    const rawFile = file.raw;
+    const res = await uploadTournament(rawFile);
+    if (res.code === 200) {
+      // 更新文件状态为成功
+      fileList.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      data.form.competitionIcon = fileList.value[index].response;
+      ElMessage.success('上传成功');
+    } else {
+      throw new Error(res.msg);
+    }
+  } catch (error) {
+    // 更新文件状态为失败
+    fileList.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('上传失败,请重试');
+  }
+};
+
+const handleViewLevels = () => {
+  const blindStructureId = data.form.blindStructureId;
+  if (blindStructureId === null || blindStructureId === undefined) {
+    ElMessage.warning('请先选择一个盲注表');
+    return;
+  }
+  dialogParams.value.blindStructureId = blindStructureId;
+  /*  dialogParams.value.name = row.name;*/
+  levelsDialogVisible.value = true;
+};
 </script>
 </script>