Przeglądaj źródła

feat(physical): 新增锦标赛联赛功能并优化商品管理

- 新增锦标赛联赛模块,包括API接口、类型定义和视图组件
- 实现锦标赛联赛的增删改查功能
- 添加联赛关联赛事功能,支持查看下级比赛
- 优化商品管理中的图片处理逻辑
- 统一图片上传和删除的处理方式
- 重构商品图片加载和保存流程
- 添加裁判选择功能到赛事管理
- 优化门店编号输入提示信息
- 集成联赛和裁判选择下拉框到赛事表单
fugui001 2 miesięcy temu
rodzic
commit
5761a14be0

+ 7 - 0
src/api/system/physical/judge/index.ts

@@ -1,6 +1,7 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import { JudgeVO, JudgeForm, JudgeQuery } from '@/api/system/physical/judge/types';
+import { LeagueTournamentVO } from '@/api/system/physical/leagueTournament/types';
 
 /**
  * 查询裁判管理列表
@@ -82,3 +83,9 @@ export const uploadJudgeFileImport = (data: FormData) => {
     }
   });
 };
+export const selectPhysicalJudgeSelList = (): AxiosPromise<JudgeVO> => {
+  return request({
+    url: '/physical/judge/selectPhysicalJudgeSelList',
+    method: 'get'
+  });
+};

+ 70 - 0
src/api/system/physical/leagueTournament/index.ts

@@ -0,0 +1,70 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { LeagueTournamentVO, LeagueTournamentForm, LeagueTournamentQuery } from '@/api/system/physical/leagueTournament/types';
+import { StoreVO } from '@/api/system/physical/store/types';
+
+/**
+ * 查询锦标赛联赛列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listLeagueTournament = (query?: LeagueTournamentQuery): AxiosPromise<LeagueTournamentVO[]> => {
+  return request({
+    url: '/physical/leagueTournament/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询锦标赛联赛详细
+ * @param id
+ */
+export const getLeagueTournament = (id: string | number): AxiosPromise<LeagueTournamentVO> => {
+  return request({
+    url: '/physical/leagueTournament/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增锦标赛联赛
+ * @param data
+ */
+export const addLeagueTournament = (data: LeagueTournamentForm) => {
+  return request({
+    url: '/physical/leagueTournament',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改锦标赛联赛
+ * @param data
+ */
+export const updateLeagueTournament = (data: LeagueTournamentForm) => {
+  return request({
+    url: '/physical/leagueTournament',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除锦标赛联赛
+ * @param id
+ */
+export const delLeagueTournament = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/leagueTournament/' + id,
+    method: 'delete'
+  });
+};
+export const selectPhysicalLeagueTournamentSelList = (): AxiosPromise<LeagueTournamentVO> => {
+  return request({
+    url: '/physical/leagueTournament/selectPhysicalLeagueTournamentSelList',
+    method: 'get'
+  });
+};

+ 159 - 0
src/api/system/physical/leagueTournament/types.ts

@@ -0,0 +1,159 @@
+export interface LeagueTournamentVO {
+  /**
+   * 锦标赛联赛ID
+   */
+  id: string | number;
+
+  /**
+   * 赛事标题
+   */
+  title: string;
+
+  /**
+   * 上传图片路径或描述
+   */
+  imageUrl: string;
+
+  /**
+   * 赛事开始时间
+   */
+  startTime: string;
+
+  /**
+   * 赛事结束时间
+   */
+  endTime: string;
+
+  /**
+   * 比赛项目,如德州扑克
+   */
+  gameType: string;
+
+  /**
+   * 比赛地址
+   */
+  location: string;
+
+  /**
+   * 状态:open=开启,closed=关闭
+   */
+  status: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt: string;
+
+  /**
+   * 更新时间
+   */
+  updatedAt: string;
+
+  gameVariant: string | number;
+}
+
+export interface LeagueTournamentForm extends BaseEntity {
+  /**
+   * 锦标赛联赛ID
+   */
+  id?: string | number;
+
+  /**
+   * 赛事标题
+   */
+  title?: string;
+
+  gameVariant: string | number;
+
+  /**
+   * 上传图片路径或描述
+   */
+  imageUrl?: string;
+
+  /**
+   * 赛事开始时间
+   */
+  startTime?: string;
+
+  /**
+   * 赛事结束时间
+   */
+  endTime?: string;
+
+  /**
+   * 比赛项目,如德州扑克
+   */
+  gameType?: string;
+
+  /**
+   * 比赛地址
+   */
+  location?: string;
+
+  /**
+   * 状态:open=开启,closed=关闭
+   */
+  status?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 更新时间
+   */
+  updatedAt?: string;
+}
+
+export interface LeagueTournamentQuery extends PageQuery {
+  /**
+   * 赛事标题
+   */
+  title?: string;
+
+  /**
+   * 上传图片路径或描述
+   */
+  imageUrl?: string;
+
+  /**
+   * 赛事开始时间
+   */
+  startTime?: string;
+
+  /**
+   * 赛事结束时间
+   */
+  endTime?: string;
+
+  /**
+   * 比赛项目,如德州扑克
+   */
+  gameType?: string;
+
+  /**
+   * 比赛地址
+   */
+  location?: string;
+
+  /**
+   * 状态:open=开启,closed=关闭
+   */
+  status?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 4 - 0
src/api/system/physical/tournaments/types.ts

@@ -327,6 +327,9 @@ export interface TournamentsForm extends BaseEntity {
   qualifierValue?: number;
   itemsPrizeList?: ItemsPrize[];
   tournamentsIntroduction?: string;
+  leagueTournamentId?: number;
+  leagueTournamentRegion?: string;
+  judgeId?: number[];
 }
 export interface ItemsPrize {
   ranking: number;
@@ -492,4 +495,5 @@ export interface TournamentsQuery extends PageQuery {
    * 日期范围参数
    */
   params?: any;
+  leagueTournamentId?: string | number;
 }

+ 0 - 1
src/views/system/physical/judge/index.vue

@@ -185,7 +185,6 @@
     </el-dialog>
   </div>
 </template>
-
 <script setup name="Judge" lang="ts">
 import { listJudge, getJudge, delJudge, addJudge, updateJudge, downloadImportTemplates, uploadJudgeFileImport } from '@/api/system/physical/judge';
 import { JudgeVO, JudgeQuery, JudgeForm } from '@/api/system/physical/judge/types';

+ 471 - 0
src/views/system/physical/leagueTournament/index.vue

@@ -0,0 +1,471 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="赛事标题" prop="title">
+              <el-input v-model="queryParams.title" placeholder="请输入赛事标题" clearable @keyup.enter="handleQuery" />
+            </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>
+            <el-form-item label="赛事结束时间" prop="endTime">
+              <el-date-picker clearable v-model="queryParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择赛事结束时间" />
+            </el-form-item>
+            <el-form-item label="比赛地址" prop="location">
+              <el-input v-model="queryParams.location" placeholder="请输入比赛地址" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:leagueTournament:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:leagueTournament:edit']"
+              >修改</el-button
+            >
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              type="danger"
+              plain
+              icon="Delete"
+              :disabled="multiple"
+              @click="handleDelete()"
+              v-hasPermi="['physical:leagueTournament:remove']"
+              >删除</el-button
+            >
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:leagueTournament:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="leagueTournamentList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="锦标赛联赛ID" align="center" prop="id" v-if="true" />
+        <el-table-column label="赛事标题" align="center" prop="title" />
+        <el-table-column label="上传图片" align="center" width="90">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.imageUrl"
+              :src="scope.row.imageUrl"
+              style="width: 40px; height: 40px; border-radius: 4px; cursor: zoom-in"
+              :preview-src-list="[scope.row.imageUrl]"
+              :preview-teleported="true"
+              fit="cover"
+            />
+            <span v-else></span>
+          </template>
+        </el-table-column>
+        <el-table-column label="赛事时间" align="center" width="300">
+          <template #default="scope">
+            <span>
+              {{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+              ~
+              {{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="比赛项目" align="center" prop="gameVariant">
+          <template #default="scope">
+            {{ getGameVariantText(scope.row.gameVariant) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="比赛地址" align="center" prop="location" />
+        <el-table-column label="下级比赛" align="center">
+          <template #default="scope">
+            <el-button type="text" @click="handleViewSubTournaments(scope.row.id)">点击查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="120">
+          <template #default="scope">
+            <span v-if="scope.row.status === 'open'">开启</span>
+            <span v-else-if="scope.row.status === 'closed'">关闭</span>
+            <span v-else>未知</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+<!--        <el-table-column label="更新时间" align="center" prop="updatedAt" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.updatedAt, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>-->
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:leagueTournament:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button
+                link
+                type="primary"
+                icon="Delete"
+                @click="handleDelete(scope.row)"
+                v-hasPermi="['physical:leagueTournament:remove']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改锦标赛联赛对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
+      <el-form ref="leagueTournamentFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="赛事标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入赛事标题" />
+        </el-form-item>
+        <el-form-item label="比赛项目" prop="gameVariant">
+          <el-select aria-required="true" v-model="form.gameVariant" placeholder="请选择">
+            <el-option v-for="dict in game_variant_type" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="上传图片" prop="imageUrl">
+          <div class="upload-container">
+            <el-upload
+              class="upload-icon"
+              action="#"
+              :on-change="handleIconChange"
+              :on-remove="handleIconRemove"
+              :file-list="fileList"
+              :auto-upload="false"
+              accept="image/*"
+            >
+              <template #trigger>
+                <el-button type="primary">点击选择图片</el-button>
+              </template>
+
+              <!-- 预览图区域 -->
+              <template #default>
+                <div class="preview-area" @click="handlePreviewClick">
+                  <img
+                    v-if="iconPreviewUrl || form.imageUrl"
+                    :src="iconPreviewUrl || form.imageUrl"
+                    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="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="结束时间" prop="endTime">
+          <el-date-picker clearable v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择赛事结束时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="比赛地址" prop="location">
+          <el-input v-model="form.location" type="textarea" :rows="3" placeholder="请输入比赛地址" />
+        </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 v-model="dialogVisible" title="图片预览" width="60%">
+      <img :src="iconPreviewUrl || form.imageUrl" alt="预览" style="max-width: 100%; max-height: 80vh" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="LeagueTournament" lang="ts">
+import { getCurrentInstance, ref, reactive, toRefs, onMounted } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import {
+  listLeagueTournament,
+  getLeagueTournament,
+  delLeagueTournament,
+  addLeagueTournament,
+  updateLeagueTournament
+} from '@/api/system/physical/leagueTournament';
+import { LeagueTournamentVO, LeagueTournamentQuery, LeagueTournamentForm } from '@/api/system/physical/leagueTournament/types';
+import { uploadTournament } from '@/api/system/business/tournaments';
+import { parseTime } from '@/utils/dateUtils';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { game_variant_type, tournaments_time, physical_tournaments_type } = toRefs<any>(
+  proxy?.useDict('game_variant_type', 'tournaments_time', 'physical_tournaments_type')
+);
+const leagueTournamentList = ref<LeagueTournamentVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const leagueTournamentFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: LeagueTournamentForm = {
+  id: undefined,
+  title: undefined,
+  imageUrl: undefined,
+  startTime: undefined,
+  endTime: undefined,
+  gameType: undefined,
+  location: undefined,
+  status: 'open',
+  createdAt: undefined,
+  updatedAt: undefined
+};
+const data = reactive<PageData<LeagueTournamentForm, LeagueTournamentQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    title: undefined,
+    imageUrl: undefined,
+    startTime: undefined,
+    endTime: undefined,
+    gameType: undefined,
+    location: undefined,
+    status: undefined,
+    createdAt: undefined,
+    updatedAt: undefined,
+    params: {}
+  },
+  rules: {
+    id: [{ required: true, message: '锦标赛联赛ID不能为空', trigger: 'blur' }],
+    title: [{ required: true, message: '赛事标题不能为空', trigger: 'blur' }],
+    startTime: [{ required: true, message: '赛事开始时间不能为空', trigger: 'blur' }],
+    endTime: [{ required: true, message: '赛事结束时间不能为空', trigger: 'blur' }],
+    gameType: [{ required: true, message: '比赛项目不能为空', trigger: 'change' }],
+    location: [{ required: true, message: '比赛地址不能为空', trigger: 'blur' }],
+    status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询锦标赛联赛列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listLeagueTournament(queryParams.value);
+  leagueTournamentList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  leagueTournamentFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: LeagueTournamentVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  // 清空文件列表
+  fileList.value = [];
+  iconPreviewUrl.value = '';
+  dialog.visible = true;
+  dialog.title = '添加锦标赛联赛';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: LeagueTournamentVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getLeagueTournament(_id);
+  Object.assign(form.value, res.data);
+  form.value.gameVariant = String(res.data.gameVariant);
+  dialog.visible = true;
+  dialog.title = '修改锦标赛联赛';
+  // 清空文件列表,避免残留
+  fileList.value = [];
+  // 如果有图片URL,创建虚拟文件对象以便显示
+  if (res.data.imageUrl) {
+    fileList.value = [
+      {
+        name: '已上传',
+        url: res.data.imageUrl,
+        status: 'success'
+      }
+    ];
+  }
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  leagueTournamentFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateLeagueTournament(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addLeagueTournament(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: LeagueTournamentVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除锦标赛联赛编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delLeagueTournament(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'physical/leagueTournament/export',
+    {
+      ...queryParams.value
+    },
+    `leagueTournament_${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getList();
+});
+// 图片上传相关
+const fileList = ref([]); // ← 新增
+const iconPreviewUrl = ref(''); // ← 新增
+const dialogVisible = ref(false); // ← 新增
+
+// 处理图片上传变化
+const handleIconChange = async (file) => {
+  const index = fileList.value.findIndex((f) => f.uid === file.uid);
+  fileList.value = [];
+
+  if (file.raw) {
+    iconPreviewUrl.value = URL.createObjectURL(file.raw);
+  }
+
+  if (index === -1) {
+    fileList.value.push(file);
+  }
+
+  try {
+    const rawFile = file.raw;
+    if (!rawFile) return;
+
+    const res = await uploadTournament(rawFile);
+    if (res.code === 200 && res.data?.url) {
+      fileList.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      form.value.imageUrl = res.data.url;
+      iconPreviewUrl.value = res.data.url;
+      ElMessage.success('上传成功');
+    } else {
+      throw new Error(res.msg || '上传失败');
+    }
+  } catch (error) {
+    fileList.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('上传失败,请重试');
+  }
+};
+
+// 删除图片处理
+const handleIconRemove = (file, updatedFileList) => {
+  fileList.value = updatedFileList;
+  iconPreviewUrl.value = '';
+  form.value.imageUrl = '';
+};
+
+// 点击预览图放大
+const handlePreviewClick = () => {
+  if (iconPreviewUrl.value || form.value.imageUrl) {
+    dialogVisible.value = true;
+  }
+};
+const getGameVariantText = (value: string | number | null | undefined): string => {
+  const numValue = Number(value);
+  switch (numValue) {
+    case 0:
+      return '德州扑克';
+    case 1:
+      return '奥马哈';
+    case 2:
+      return '短牌';
+    default:
+      return '未知';
+  }
+};
+const handleViewSubTournaments = (leagueTournamentId: number) => {
+  proxy?.$router.push({
+    path: '/physical/tournaments',
+    query: {
+      leagueTournamentId: String(leagueTournamentId) // 转为字符串以兼容 URL 参数
+    }
+  });
+};
+</script>

+ 38 - 185
src/views/system/physical/product/index.vue

@@ -131,12 +131,15 @@
 </template>
 
 <script setup name="Product" lang="ts">
+import { getCurrentInstance, reactive, ref, toRefs, watch, onMounted } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
 import { addProduct, delProduct, getProduct, listProduct, updateProduct, deletePhysicalProductImageByOsId } from '@/api/system/physical/product';
 import { ProductForm, ProductQuery, ProductVO } from '@/api/system/physical/product/types';
 import { selectAllPhysicalTagsSelList } from '@/api/system/physical/tag';
 import { selectPhysicalStoreSelList } from '@/api/system/physical/store';
 import { listByIds } from '@/api/system/oss';
 import { parseTime } from '@/utils/dateUtils';
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const productList = ref<ProductVO[]>([]);
@@ -248,10 +251,10 @@ const handleUpdate = async (row?: ProductVO) => {
   const _id = row?.id || ids.value[0];
   const res = await getProduct(_id);
   Object.assign(form.value, res.data);
-  // 处理商品图片 (productImagesOsId)
-  if (res.data.productImages && Array.isArray(res.data.productImages)) {
+
+  const processImages = async (images: any[]) => {
     const imageUrls = await Promise.all(
-      res.data.productImages.map(async (image) => {
+      images.map(async (image) => {
         try {
           const res = await getIds(image.osId);
           const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
@@ -266,39 +269,24 @@ const handleUpdate = async (row?: ProductVO) => {
         }
       })
     );
-    const validImageUrls = imageUrls.filter((item) => item.url !== '');
-    // 将有效的 URL 字符串数组赋值给 form 字段
-    // 注意:这里应该传递 osId 字符串,让 ImageUpload 内部去解析
-    // 修正:传递 osId 列表,而不是 URL 列表
-    form.value.productImagesOsId = validImageUrls.map((item) => item.ossId).join(',');
+    return imageUrls
+      .filter((item) => item.url !== '')
+      .map((item) => item.ossId)
+      .join(',');
+  };
+
+  if (res.data.productImages && Array.isArray(res.data.productImages)) {
+    form.value.productImagesOsId = await processImages(res.data.productImages);
   } else {
     form.value.productImagesOsId = [];
   }
 
-  // 处理详情图片 (productDetailImgOsId),逻辑类似
   if (res.data.productDetailImg && Array.isArray(res.data.productDetailImg)) {
-    const detailImageUrls = await Promise.all(
-      res.data.productDetailImg.map(async (image) => {
-        try {
-          const res = await getIds(image.osId);
-          const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
-          return {
-            url: firstItem?.url || firstItem?.picUrl,
-            ossId: image.osId,
-            name: image.osId
-          };
-        } catch (error) {
-          console.error('获取详情图片URL失败:', error);
-          return { url: '', ossId: '' };
-        }
-      })
-    );
-    const validDetailImageUrls = detailImageUrls.filter((item) => item.url !== '');
-    // 同样传递 osId 列表
-    form.value.productDetailImgOsId = validDetailImageUrls.map((item) => item.ossId).join(',');
+    form.value.productDetailImgOsId = await processImages(res.data.productDetailImg);
   } else {
     form.value.productDetailImgOsId = [];
   }
+
   dialog.visible = true;
   dialog.title = '修改商品';
 };
@@ -307,52 +295,26 @@ const submitForm = () => {
   productFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       buttonLoading.value = true;
-      // 处理多个 osId 的情况
-      if (form.value.productImagesOsId) {
-        const osIds = form.value.productImagesOsId
+
+      const processOsIds = async (osIdString: any, imageType: string) => {
+        if (!osIdString) return [];
+        const osIds = osIdString
           .toString()
           .split(',')
-          .map((id) => id.trim());
-        // 创建一个数组来存储所有图片信息
+          .map((id: string) => id.trim());
         const imageList = [];
         for (const osId of osIds) {
           const res = await getIds(osId);
           const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
           const picUrl = firstItem?.url || firstItem?.picUrl;
-          // 将每个图片信息添加到列表中
-          imageList.push({
-            osId: osId,
-            imageUrl: picUrl,
-            imageType: 'SHOW'
-          });
+          imageList.push({ osId, imageUrl: picUrl, imageType });
         }
-        // 将图片列表赋值给表单
-        form.value.productImages = imageList;
-      }
+        return imageList;
+      };
 
-      if (form.value.productDetailImgOsId) {
-        const osIds = form.value.productDetailImgOsId
-          .toString()
-          .split(',')
-          .map((id) => id.trim());
-        // 创建一个数组来存储所有图片信息
-        const imageList2 = [];
-        for (const osId of osIds) {
-          const res = await getIds(osId);
-          const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
-          const picUrl = firstItem?.url || firstItem?.picUrl;
-          // 将每个图片信息添加到列表中
-          imageList2.push({
-            osId: osId,
-            imageUrl: picUrl,
-            imageType: 'DETAIL'
-          });
-        }
-        // 将图片列表赋值给表单
-        form.value.productDetailImg = imageList2;
-      }
+      form.value.productImages = await processOsIds(form.value.productImagesOsId, 'SHOW');
+      form.value.productDetailImg = await processOsIds(form.value.productDetailImgOsId, 'DETAIL');
 
-      // 提交数据
       if (form.value.id) {
         await updateProduct(form.value).finally(() => (buttonLoading.value = false));
       } else {
@@ -372,7 +334,7 @@ const getIds = async (ids: string | number) => {
 /** 删除按钮操作 */
 const handleDelete = async (row?: ProductVO) => {
   const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除商品编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await proxy?.$modal.confirm(`是否确认删除商品编号为"${_ids}"的数据项?`).finally(() => (loading.value = false));
   await delProduct(_ids);
   proxy?.$modal.msgSuccess('删除成功');
   await getList();
@@ -389,44 +351,16 @@ const handleExport = () => {
   );
 };
 const itemOptionsType = ref<{ id: number; label: string }[]>([]);
-const loadItemStructuresOptions = async () => {
-  try {
-    const res = await selectAllPhysicalTagsSelList();
-    if (res.code === 200) {
-      // 使用 unknown 中间类型进行类型转换
-      const data = res.data as unknown as { id: number; serviceName: string }[];
-      const list = [];
-      for (let i = 0; i < data.length; i++) {
-        const item = data[i];
-        list.push({
-          id: item.id,
-          label: item.serviceName
-        });
-      }
-      itemOptionsType.value = list;
-    } else {
-      alert('加载失败:' + res.msg);
-    }
-  } catch (error) {
-    console.error('请求出错:', error);
-  }
-};
 const itemOptionsStore = ref<{ id: number; label: string }[]>([]);
-const loadStoreOptions = async () => {
+
+const loadOptions = async (apiFunc: () => Promise<any>, targetRef: any, labelKey: string = 'serviceName') => {
   try {
-    const res = await selectPhysicalStoreSelList();
+    const res = await apiFunc();
     if (res.code === 200) {
-      // 使用 unknown 中间类型进行类型转换
-      const data = res.data as unknown as { id: number; serviceName: string }[];
-      const list = [];
-      for (let i = 0; i < data.length; i++) {
-        const item = data[i];
-        list.push({
-          id: item.id,
-          label: item.serviceName
-        });
-      }
-      itemOptionsStore.value = list;
+      targetRef.value = (res.data as unknown as any[]).map((item) => ({
+        id: item.id,
+        label: item[labelKey]
+      }));
     } else {
       alert('加载失败:' + res.msg);
     }
@@ -434,24 +368,24 @@ const loadStoreOptions = async () => {
     console.error('请求出错:', error);
   }
 };
+
+const loadItemStructuresOptions = () => loadOptions(selectAllPhysicalTagsSelList, itemOptionsType);
+const loadStoreOptions = () => loadOptions(selectPhysicalStoreSelList, itemOptionsStore);
 onMounted(() => {
   getList();
   loadItemStructuresOptions();
   loadStoreOptions();
 });
-// 在 setup 中添加 watch
+
 watch(
   () => form.value.productImagesOsId,
   (newVal, oldVal) => {
-    console.log('商品图片已更新:', newVal);
-    // 将 oldVal 和 newVal 转换为字符串数组
     const oldIds = Array.isArray(oldVal) ? oldVal : oldVal?.split(',') || [];
     const newIds = Array.isArray(newVal) ? newVal : newVal?.split(',') || [];
 
     if (oldIds.length > newIds.length) {
       const removedIds = oldIds.filter((id) => !newIds.includes(id));
       if (removedIds.length > 0) {
-        console.log('商品图片被删除:', removedIds);
         removedIds.forEach((osId) => {
           if (osId && typeof osId === 'string' && osId.trim() !== '') {
             deletePhysicalProductImageByOsId(osId);
@@ -466,15 +400,12 @@ watch(
 watch(
   () => form.value.productDetailImgOsId,
   (newVal, oldVal) => {
-    console.log('详情图片已更新:', newVal);
-    // 将 oldVal 和 newVal 转换为字符串数组
     const oldIds = Array.isArray(oldVal) ? oldVal : oldVal?.split(',') || [];
     const newIds = Array.isArray(newVal) ? newVal : newVal?.split(',') || [];
 
     if (oldIds.length > newIds.length) {
       const removedIds = oldIds.filter((id) => !newIds.includes(id));
       if (removedIds.length > 0) {
-        console.log('详情图片被删除:', removedIds);
         removedIds.forEach((osId) => {
           if (osId && typeof osId === 'string' && osId.trim() !== '') {
             deletePhysicalProductImageByOsId(osId);
@@ -485,84 +416,6 @@ watch(
   },
   { deep: true }
 );
-
-/*const imageFileList = ref([]);
-const imageUrls = ref([]);
-/!** 处理图片上传 *!/
-const handleImageChange = async (file, fileList) => {
-  imageFileList.value = fileList;
-  // 为新上传的文件生成预览
-  if (file.raw) {
-    try {
-      const rawFile = file.raw;
-      const res = await uploadTournament(rawFile);
-      if (res.code === 200) {
-        // 更新文件状态为成功并保存URL
-        const index = imageFileList.value.findIndex((f) => f.uid === file.uid);
-        if (index !== -1) {
-          imageFileList.value[index] = {
-            ...file,
-            status: 'success',
-            response: res.data.url,
-            url: res.data.url
-          };
-
-          // 更新 imageUrls 数组
-          updateImageUrls();
-        }
-        proxy?.$modal.msgSuccess('上传成功');
-      } else {
-        throw new Error(res.msg);
-      }
-    } catch (error) {
-      // 更新文件状态为失败
-      const index = imageFileList.value.findIndex((f) => f.uid === file.uid);
-      if (index !== -1) {
-        imageFileList.value[index] = {
-          ...file,
-          status: 'fail',
-          error: '上传失败'
-        };
-      }
-      proxy?.$modal.msgError('上传失败,请重试');
-    }
-  } else {
-    // 对于已有文件,直接更新 URLs
-    updateImageUrls();
-  }
-};
-
-/!** 处理图片删除 *!/
-const handleImageRemove = (file, fileList) => {
-  imageFileList.value = fileList;
-  // 更新图片 URLs 数组
-  updateImageUrls();
-
-  // 同步到 form 数据
-  if (form.value.productImagesOsId) {
-    const osIds = form.value.productImagesOsId
-      .toString()
-      .split(',')
-      .map((id) => id.trim());
-    const removedId = file.response || file.url;
-
-    // 过滤掉被删除的图片
-    form.value.productImagesOsId = osIds.filter((osId) => osId !== removedId);
-  }
-};
-
-/!** 图片数量超出限制 *!/
-const handleImageExceed = (files, fileList) => {
-  proxy?.$modal.msgWarning('最多只能上传5张图片');
-};
-
-/!** 更新图片 URLs 数组 *!/
-const updateImageUrls = () => {
-  const urls = imageFileList.value.filter((file) => file.status === 'success' || file.url).map((file) => file.response || file.url);
-  // 确保返回的是字符串数组
-  imageUrls.value = urls as string[];
-  form.value.images = urls as string[];
-};*/
 </script>
 <style scoped>
 .upload-images :deep(.el-upload-list__item) {

+ 1 - 1
src/views/system/physical/store/index.vue

@@ -108,7 +108,7 @@
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
       <el-form ref="storeFormRef" :model="form" :rules="rules" label-width="120px">
         <el-form-item label="门店编号" prop="storeCode">
-          <el-input v-model="form.storeCode" placeholder="请输入门店编号,如M0001" />
+          <el-input v-model="form.storeCode" placeholder="请输入门店编号" />
         </el-form-item>
         <el-form-item label="门店名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入门店名称,最多20字" />

+ 70 - 3
src/views/system/physical/tournaments/index.vue

@@ -142,9 +142,17 @@
         <el-form-item label="赛事名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入赛事名称" />
         </el-form-item>
-        <el-form-item label="赛事位置" prop="name">
-          <el-input v-model="form.competitionLocation" placeholder="请输入赛事位置" />
+        <el-form-item label="所属联赛" prop="leagueTournamentId">
+          <el-select v-model="form.leagueTournamentId" placeholder="请选择联赛" style="width: 100%">
+            <el-option v-for="item in leagueTournamentOptions" :key="item.id" :label="item.title" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所属赛区" prop="leagueTournamentRegion">
+          <el-input v-model="form.leagueTournamentRegion" placeholder="请输入赛区" />
         </el-form-item>
+<!--        <el-form-item label="赛事位置" prop="competitionLocation">
+          <el-input v-model="form.competitionLocation" placeholder="请输入赛事位置" />
+        </el-form-item>-->
         <!-- 比赛图标 -->
         <el-form-item label="比赛图标" prop="icon">
           <div class="upload-container">
@@ -324,6 +332,21 @@
             :rows="4"
           />
         </el-form-item>
+        <el-form-item label="裁判" prop="judgeId">
+          <div style="display: flex; align-items: center">
+            <el-select
+              v-model="form.judgeId"
+              placeholder="选项"
+              style="width: 200px"
+              :disabled="dialog.mode === 'view'"
+              multiple
+              collapse-tags
+              collapse-tags-tooltip
+            >
+              <el-option v-for="item in judgeOptions" :key="item.id" :label="item.name" :value="item.id" />
+            </el-select>
+          </div>
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -353,8 +376,13 @@ import { selectItemsSelList } from '@/api/system/business/items';
 import { selectPhysicalBlingStructuresInfo } from '@/api/system/physical/blindStructures';
 import { selectPhysicalBlindLevelsById } from '@/api/system/physical/blindLevels';
 import { uploadTournament } from '@/api/system/business/tournaments';
+import { selectPhysicalLeagueTournamentSelList } from '@/api/system/physical/leagueTournament';
+import { selectPhysicalJudgeSelList } from '@/api/system/physical/judge';
 import { parseTime } from '@/utils/dateUtils';
 import LevelsIndex from '@/views/system/physical/blindLevels/index.vue';
+import { LeagueTournamentVO } from '@/api/system/physical/leagueTournament/types';
+import { JudgeVO } from '@/api/system/physical/judge/types';
+import { ElSelect } from 'element-plus';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { tournaments_type, tournaments_time, physical_tournaments_type } = toRefs<any>(
   proxy?.useDict('tournaments_type', 'tournaments_time', 'physical_tournaments_type')
@@ -449,7 +477,8 @@ const data = reactive<PageData<TournamentsForm, TournamentsQuery>>({
     targetTournamentId: undefined,
     qualifierType: undefined,
     qualifierValue: undefined,
-    params: {}
+    params: {},
+    leagueTournamentId: undefined // ← 新增字段
   },
   rules: {
     id: [{ required: true, message: '不能为空', trigger: 'blur' }],
@@ -732,9 +761,15 @@ const handleExport = () => {
 };
 
 onMounted(() => {
+  const route = proxy?.$route;
+  if (route.query.leagueTournamentId) {
+    queryParams.value.leagueTournamentId = String(route.query.leagueTournamentId);
+  }
   getList();
   loadItemOptions();
   loadItemStructuresOptions();
+  loadLeagueTournamentOptions();
+  loadJudgeOptions();
 });
 const itemOptions = ref<{ id: number; label: string }[]>([]);
 // 加载报名条件选项
@@ -1043,4 +1078,36 @@ const handleCopy = async (row?: TournamentsVO) => {
   dialog.visible = true;
   dialog.title = '创建比赛';
 };
+// 响应式变量
+const leagueTournamentOptions = ref<LeagueTournamentVO[]>([]);
+
+const loadLeagueTournamentOptions = async () => {
+  try {
+    const res = await selectPhysicalLeagueTournamentSelList();
+    if (res.code === 200 && Array.isArray(res.data)) {
+      leagueTournamentOptions.value = res.data;
+    } else {
+      ElMessage.error('加载失败:' + res.msg);
+    }
+  } catch (error) {
+    console.error('请求出错:', error);
+    ElMessage.error('请求失败,请检查网络');
+  }
+};
+
+const judgeOptions = ref<JudgeVO[]>([]);
+
+const loadJudgeOptions = async () => {
+  try {
+    const res = await selectPhysicalJudgeSelList();
+    if (res.code === 200 && Array.isArray(res.data)) {
+      judgeOptions.value = res.data;
+    } else {
+      ElMessage.error('加载失败:' + res.msg);
+    }
+  } catch (error) {
+    console.error('请求出错:', error);
+    ElMessage.error('请求失败,请检查网络');
+  }
+};
 </script>