Просмотр исходного кода

Merge branch 'master' of http://115.190.200.22:3000/xpg/ui

# Conflicts:
#	src/views/system/business/levels/index.vue
#	src/views/system/physical/blindLevels/index.vue
#	src/views/system/physical/blindStructures/index.vue
wengan01 2 недель назад
Родитель
Сommit
2f5a41c191

+ 1 - 1
.env.development

@@ -8,7 +8,7 @@ VITE_APP_ENV = 'development'
 VITE_APP_BASE_API = '/dev-api'
 
 # 应用访问路径 例如使用前缀 /admin/
-VITE_APP_CONTEXT_PATH = '/'
+VITE_APP_CONTEXT_PATH = '/admin/'
 
 # 监控地址
 VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications'

+ 5 - 1
.env.production

@@ -5,7 +5,7 @@ VITE_APP_TITLE = X-Poker Games后台管理系统
 VITE_APP_ENV = 'production'
 
 # 应用访问路径 例如使用前缀 /admin/
-VITE_APP_CONTEXT_PATH = '/'
+VITE_APP_CONTEXT_PATH = '/admin/'
 
 # 监控地址
 VITE_APP_MONITOR_ADMIN = '/admin/applications'
@@ -36,3 +36,7 @@ VITE_APP_WEBSOCKET = false
 
 # sse 开关
 VITE_APP_SSE = true
+
+
+
+

+ 314 - 0
git reset --soft c13f9ff

@@ -0,0 +1,314 @@
+
+                   SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
+
+      Commands marked with * may be preceded by a number, _N.
+      Notes in parentheses indicate the behavior if _N is given.
+      A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
+
+  h  H                 Display this help.
+  q  :q  Q  :Q  ZZ     Exit.
+ ---------------------------------------------------------------------------
+
+                           MMOOVVIINNGG
+
+  e  ^E  j  ^N  CR  *  Forward  one line   (or _N lines).
+  y  ^Y  k  ^K  ^P  *  Backward one line   (or _N lines).
+  f  ^F  ^V  SPACE  *  Forward  one window (or _N lines).
+  b  ^B  ESC-v      *  Backward one window (or _N lines).
+  z                 *  Forward  one window (and set window to _N).
+  w                 *  Backward one window (and set window to _N).
+  ESC-SPACE         *  Forward  one window, but don't stop at end-of-file.
+  d  ^D             *  Forward  one half-window (and set half-window to _N).
+  u  ^U             *  Backward one half-window (and set half-window to _N).
+  ESC-)  RightArrow *  Right one half screen width (or _N positions).
+  ESC-(  LeftArrow  *  Left  one half screen width (or _N positions).
+  ESC-}  ^RightArrow   Right to last column displayed.
+  ESC-{  ^LeftArrow    Left  to first column.
+  F                    Forward forever; like "tail -f".
+  ESC-F                Like F but stop when search pattern is found.
+  r  ^R  ^L            Repaint screen.
+  R                    Repaint screen, discarding buffered input.
+        ---------------------------------------------------
+        Default "window" is the screen height.
+        Default "half-window" is half of the screen height.
+ ---------------------------------------------------------------------------
+
+                          SSEEAARRCCHHIINNGG
+
+  /_p_a_t_t_e_r_n          *  Search forward for (_N-th) matching line.
+  ?_p_a_t_t_e_r_n          *  Search backward for (_N-th) matching line.
+  n                 *  Repeat previous search (for _N-th occurrence).
+  N                 *  Repeat previous search in reverse direction.
+  ESC-n             *  Repeat previous search, spanning files.
+  ESC-N             *  Repeat previous search, reverse dir. & spanning files.
+  ^O^N  ^On         *  Search forward for (_N-th) OSC8 hyperlink.
+  ^O^P  ^Op         *  Search backward for (_N-th) OSC8 hyperlink.
+  ^O^L  ^Ol            Jump to the currently selected OSC8 hyperlink.
+  ESC-u                Undo (toggle) search highlighting.
+  ESC-U                Clear search highlighting.
+  &_p_a_t_t_e_r_n          *  Display only matching lines.
+        ---------------------------------------------------
+        A search pattern may begin with one or more of:
+        ^N or !  Search for NON-matching lines.
+        ^E or *  Search multiple files (pass thru END OF FILE).
+        ^F or @  Start search at FIRST file (for /) or last file (for ?).
+        ^K       Highlight matches, but don't move (KEEP position).
+        ^R       Don't use REGULAR EXPRESSIONS.
+        ^S _n     Search for match in _n-th parenthesized subpattern.
+        ^W       WRAP search if no match found.
+        ^L       Enter next character literally into pattern.
+ ---------------------------------------------------------------------------
+
+                           JJUUMMPPIINNGG
+
+  g  <  ESC-<       *  Go to first line in file (or line _N).
+  G  >  ESC->       *  Go to last line in file (or line _N).
+  p  %              *  Go to beginning of file (or _N percent into file).
+  t                 *  Go to the (_N-th) next tag.
+  T                 *  Go to the (_N-th) previous tag.
+  {  (  [           *  Find close bracket } ) ].
+  }  )  ]           *  Find open bracket { ( [.
+  ESC-^F _<_c_1_> _<_c_2_>  *  Find close bracket _<_c_2_>.
+  ESC-^B _<_c_1_> _<_c_2_>  *  Find open bracket _<_c_1_>.
+        ---------------------------------------------------
+        Each "find close bracket" command goes forward to the close bracket 
+          matching the (_N-th) open bracket in the top line.
+        Each "find open bracket" command goes backward to the open bracket 
+          matching the (_N-th) close bracket in the bottom line.
+
+  m_<_l_e_t_t_e_r_>            Mark the current top line with <letter>.
+  M_<_l_e_t_t_e_r_>            Mark the current bottom line with <letter>.
+  '_<_l_e_t_t_e_r_>            Go to a previously marked position.
+  ''                   Go to the previous position.
+  ^X^X                 Same as '.
+  ESC-m_<_l_e_t_t_e_r_>        Clear a mark.
+        ---------------------------------------------------
+        A mark is any upper-case or lower-case letter.
+        Certain marks are predefined:
+             ^  means  beginning of the file
+             $  means  end of the file
+ ---------------------------------------------------------------------------
+
+                        CCHHAANNGGIINNGG FFIILLEESS
+
+  :e [_f_i_l_e]            Examine a new file.
+  ^X^V                 Same as :e.
+  :n                *  Examine the (_N-th) next file from the command line.
+  :p                *  Examine the (_N-th) previous file from the command line.
+  :x                *  Examine the first (or _N-th) file from the command line.
+  ^O^O                 Open the currently selected OSC8 hyperlink.
+  :d                   Delete the current file from the command line list.
+  =  ^G  :f            Print current file name.
+ ---------------------------------------------------------------------------
+
+                    MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
+
+  -_<_f_l_a_g_>              Toggle a command line option [see OPTIONS below].
+  --_<_n_a_m_e_>             Toggle a command line option, by name.
+  __<_f_l_a_g_>              Display the setting of a command line option.
+  ___<_n_a_m_e_>             Display the setting of an option, by name.
+  +_c_m_d                 Execute the less cmd each time a new file is examined.
+
+  !_c_o_m_m_a_n_d             Execute the shell command with $SHELL.
+  #_c_o_m_m_a_n_d             Execute the shell command, expanded like a prompt.
+  |XX_c_o_m_m_a_n_d            Pipe file between current pos & mark XX to shell command.
+  s _f_i_l_e               Save input to a file.
+  v                    Edit the current file with $VISUAL or $EDITOR.
+  V                    Print version number of "less".
+ ---------------------------------------------------------------------------
+
+                           OOPPTTIIOONNSS
+
+        Most options may be changed either on the command line,
+        or from within less by using the - or -- command.
+        Options may be given in one of two forms: either a single
+        character preceded by a -, or a name preceded by --.
+
+  -?  ........  --help
+                  Display help (from command line).
+  -a  ........  --search-skip-screen
+                  Search skips current screen.
+  -A  ........  --SEARCH-SKIP-SCREEN
+                  Search starts just after target line.
+  -b [_N]  ....  --buffers=[_N]
+                  Number of buffers.
+  -B  ........  --auto-buffers
+                  Don't automatically allocate buffers for pipes.
+  -c  ........  --clear-screen
+                  Repaint by clearing rather than scrolling.
+  -d  ........  --dumb
+                  Dumb terminal.
+  -D xx_c_o_l_o_r  .  --color=xx_c_o_l_o_r
+                  Set screen colors.
+  -e  -E  ....  --quit-at-eof  --QUIT-AT-EOF
+                  Quit at end of file.
+  -f  ........  --force
+                  Force open non-regular files.
+  -F  ........  --quit-if-one-screen
+                  Quit if entire file fits on first screen.
+  -g  ........  --hilite-search
+                  Highlight only last match for searches.
+  -G  ........  --HILITE-SEARCH
+                  Don't highlight any matches for searches.
+  -h [_N]  ....  --max-back-scroll=[_N]
+                  Backward scroll limit.
+  -i  ........  --ignore-case
+                  Ignore case in searches that do not contain uppercase.
+  -I  ........  --IGNORE-CASE
+                  Ignore case in all searches.
+  -j [_N]  ....  --jump-target=[_N]
+                  Screen position of target lines.
+  -J  ........  --status-column
+                  Display a status column at left edge of screen.
+  -k _f_i_l_e  ...  --lesskey-file=_f_i_l_e
+                  Use a compiled lesskey file.
+  -K  ........  --quit-on-intr
+                  Exit less in response to ctrl-C.
+  -L  ........  --no-lessopen
+                  Ignore the LESSOPEN environment variable.
+  -m  -M  ....  --long-prompt  --LONG-PROMPT
+                  Set prompt style.
+  -n .........  --line-numbers
+                  Suppress line numbers in prompts and messages.
+  -N .........  --LINE-NUMBERS
+                  Display line number at start of each line.
+  -o [_f_i_l_e] ..  --log-file=[_f_i_l_e]
+                  Copy to log file (standard input only).
+  -O [_f_i_l_e] ..  --LOG-FILE=[_f_i_l_e]
+                  Copy to log file (unconditionally overwrite).
+  -p _p_a_t_t_e_r_n .  --pattern=[_p_a_t_t_e_r_n]
+                  Start at pattern (from command line).
+  -P [_p_r_o_m_p_t]   --prompt=[_p_r_o_m_p_t]
+                  Define new prompt.
+  -q  -Q  ....  --quiet  --QUIET  --silent --SILENT
+                  Quiet the terminal bell.
+  -r  -R  ....  --raw-control-chars  --RAW-CONTROL-CHARS
+                  Output "raw" control characters.
+  -s  ........  --squeeze-blank-lines
+                  Squeeze multiple blank lines.
+  -S  ........  --chop-long-lines
+                  Chop (truncate) long lines rather than wrapping.
+  -t _t_a_g  ....  --tag=[_t_a_g]
+                  Find a tag.
+  -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
+                  Use an alternate tags file.
+  -u  -U  ....  --underline-special  --UNDERLINE-SPECIAL
+                  Change handling of backspaces, tabs and carriage returns.
+  -V  ........  --version
+                  Display the version number of "less".
+  -w  ........  --hilite-unread
+                  Highlight first new line after forward-screen.
+  -W  ........  --HILITE-UNREAD
+                  Highlight first new line after any forward movement.
+  -x [_N[,...]]  --tabs=[_N[,...]]
+                  Set tab stops.
+  -X  ........  --no-init
+                  Don't use termcap init/deinit strings.
+  -y [_N]  ....  --max-forw-scroll=[_N]
+                  Forward scroll limit.
+  -z [_N]  ....  --window=[_N]
+                  Set size of window.
+  -" [_c[_c]]  .  --quotes=[_c[_c]]
+                  Set shell quote characters.
+  -~  ........  --tilde
+                  Don't display tildes after end of file.
+  -# [_N]  ....  --shift=[_N]
+                  Set horizontal scroll amount (0 = one half screen width).
+
+                --exit-follow-on-close
+                  Exit F command on a pipe when writer closes pipe.
+                --file-size
+                  Automatically determine the size of the input file.
+                --follow-name
+                  The F command changes files if the input file is renamed.
+                --header=[_L[,_C[,_N]]]
+                  Use _L lines (starting at line _N) and _C columns as headers.
+                --incsearch
+                  Search file as each pattern character is typed in.
+                --intr=[_C]
+                  Use _C instead of ^X to interrupt a read.
+                --lesskey-context=_t_e_x_t
+                  Use lesskey source file contents.
+                --lesskey-src=_f_i_l_e
+                  Use a lesskey source file.
+                --line-num-width=[_N]
+                  Set the width of the -N line number field to _N characters.
+                --match-shift=[_N]
+                  Show at least _N characters to the left of a search match.
+                --modelines=[_N]
+                  Read _N lines from the input file and look for vim modelines.
+                --mouse
+                  Enable mouse input.
+                --no-keypad
+                  Don't send termcap keypad init/deinit strings.
+                --no-histdups
+                  Remove duplicates from command history.
+                --no-number-headers
+                  Don't give line numbers to header lines.
+                --no-search-header-lines
+                  Searches do not include header lines.
+                --no-search-header-columns
+                  Searches do not include header columns.
+                --no-search-headers
+                  Searches do not include header lines or columns.
+                --no-vbell
+                  Disable the terminal's visual bell.
+                --redraw-on-quit
+                  Redraw final screen when quitting.
+                --rscroll=[_C]
+                  Set the character used to mark truncated lines.
+                --save-marks
+                  Retain marks across invocations of less.
+                --search-options=[EFKNRW-]
+                  Set default options for every search.
+                --show-preproc-errors
+                  Display a message if preprocessor exits with an error status.
+                --proc-backspace
+                  Process backspaces for bold/underline.
+                --PROC-BACKSPACE
+                  Treat backspaces as control characters.
+                --proc-return
+                  Delete carriage returns before newline.
+                --PROC-RETURN
+                  Treat carriage returns as control characters.
+                --proc-tab
+                  Expand tabs to spaces.
+                --PROC-TAB
+                  Treat tabs as control characters.
+                --status-col-width=[_N]
+                  Set the width of the -J status column to _N characters.
+                --status-line
+                  Highlight or color the entire line containing a mark.
+                --use-backslash
+                  Subsequent options use backslash as escape char.
+                --use-color
+                  Enables colored text.
+                --wheel-lines=[_N]
+                  Each click of the mouse wheel moves _N lines.
+                --wordwrap
+                  Wrap lines at spaces.
+
+
+ ---------------------------------------------------------------------------
+
+                          LLIINNEE EEDDIITTIINNGG
+
+        These keys can be used to edit text being entered 
+        on the "command line" at the bottom of the screen.
+
+ RightArrow ..................... ESC-l ... Move cursor right one character.
+ LeftArrow ...................... ESC-h ... Move cursor left one character.
+ ctrl-RightArrow  ESC-RightArrow  ESC-w ... Move cursor right one word.
+ ctrl-LeftArrow   ESC-LeftArrow   ESC-b ... Move cursor left one word.
+ HOME ........................... ESC-0 ... Move cursor to start of line.
+ END ............................ ESC-$ ... Move cursor to end of line.
+ BACKSPACE ................................ Delete char to left of cursor.
+ DELETE ......................... ESC-x ... Delete char under cursor.
+ ctrl-BACKSPACE   ESC-BACKSPACE ........... Delete word to left of cursor.
+ ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
+ ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
+ UpArrow ........................ ESC-k ... Retrieve previous command line.
+ DownArrow ...................... ESC-j ... Retrieve next command line.
+ TAB ...................................... Complete filename & cycle.
+ SHIFT-TAB ...................... ESC-TAB   Complete filename & reverse cycle.
+ ctrl-L ................................... Complete filename, list all.

+ 5 - 1
package.json

@@ -24,6 +24,10 @@
     "@element-plus/icons-vue": "2.3.1",
     "@highlightjs/vue-plugin": "2.1.0",
     "@tinymce/tinymce-vue": "^6.2.0",
+    "@vue/reactivity": "^3.5.13",
+    "@vue/runtime-core": "^3.5.13",
+    "@vue/runtime-dom": "^3.5.13",
+    "@vue/shared": "^3.5.13",
     "@vueup/vue-quill": "1.2.0",
     "@vueuse/core": "13.1.0",
     "animate.css": "4.1.1",
@@ -44,7 +48,7 @@
     "pinia": "3.0.2",
     "screenfull": "6.0.2",
     "tinymce": "^7.9.1",
-    "vue": "3.5.13",
+    "vue": "^3.5.13",
     "vue-cropper": "1.1.1",
     "vue-i18n": "^11.1.10",
     "vue-json-pretty": "2.4.0",

+ 105 - 0
src/api/system/physical/tournamentImage/index.ts

@@ -0,0 +1,105 @@
+  import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TournamentImageVO, TournamentImageForm, TournamentImageQuery } from '@/api/system/physical/tournamentImage/types';
+
+/**
+ * 上传赛事图片
+ * @param file 图片文件
+ * @returns {*}
+ */
+// TODO: 上传图片到OSS服务器
+export const uploadTournamentImage = (file: File): AxiosPromise<{ url: string }> => {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: '/physical/tournamentImage/upload',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 查询赛事图片列表
+ * @param query
+ * @returns {*}
+ */
+// TODO: 获取分页图片列表数据
+export const listTournamentImage = (query?: TournamentImageQuery): AxiosPromise<TournamentImageVO[]> => {
+  return request({
+    url: '/physical/tournamentImage/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询赛事图片详细
+ * @param id
+ */
+// TODO: 根据ID获取单张图片详情
+export const getTournamentImage = (id: string | number): AxiosPromise<TournamentImageVO> => {
+  return request({
+    url: '/physical/tournamentImage/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增赛事图片
+ * @param data
+ */
+// TODO: 创建新的图片记录
+export const addTournamentImage = (data: TournamentImageForm) => {
+  return request({
+    url: '/physical/tournamentImage',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改赛事图片
+ * @param data
+ */
+// TODO: 更新现有图片信息
+export const updateTournamentImage = (data: TournamentImageForm) => {
+  return request({
+    url: '/physical/tournamentImage',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除赛事图片
+ * @param ids 单个ID或ID数组
+ */
+// TODO: 批量删除图片记录
+export const delTournamentImage = (ids: string | number | Array<string | number>) => {
+  // 如果是数组,转换为逗号分隔的字符串
+  const idStr = Array.isArray(ids) ? ids.join(',') : ids;
+  return request({
+    url: '/physical/tournamentImage/' + idStr,
+    method: 'delete'
+  });
+};
+
+/**
+ * 根据图片类型和使用场景查询图片
+ * 用于创建赛事时选择图标/背景图
+ * 后端会自动过滤 status=enabled 且 sceneType 匹配(online/offline/both)的图片
+ * @param imageType 图片类型: background=背景图, icon=图标
+ * @param sceneType 使用场景: online=线上, offline=线下
+ */
+// TODO: 按类型和场景筛选可用图片
+export const selectByTypeAndScene = (imageType: string, sceneType: string): AxiosPromise<TournamentImageVO[]> => {
+  return request({
+    url: '/physical/tournamentImage/selectByTypeAndScene',
+    method: 'get',
+    params: { imageType, sceneType }
+  });
+};
+

+ 92 - 0
src/api/system/physical/tournamentImage/types.ts

@@ -0,0 +1,92 @@
+// TODO: 赛事图片视图对象 - 用于展示图片信息
+export interface TournamentImageVO {
+  /** 主键ID */
+  id: string | number;
+
+  /** 图片名称(自定义,非文件名) */
+  imageName: string;
+
+  /** 图片URL(OSS地址) */
+  imageUrl: string;
+
+  /**
+   * 图片类型
+   * background=背景图, icon=图标
+   */
+  imageType: string;
+
+  /**
+   * 使用场景
+   * online=线上, offline=线下, both=通用
+   */
+  sceneType: string;
+
+  /** 排序号,越小越靠前 */
+  sortOrder: number;
+
+  /**
+   * 状态
+   * enabled=启用, disabled=禁用
+   */
+  status: string;
+
+  /** 备注 */
+  remark?: string;
+
+  /** 创建时间 */
+  createTime: string;
+
+  /** 更新时间 */
+  updateTime: string;
+}
+
+// TODO: 赛事图片表单对象 - 用于创建和编辑图片
+export interface TournamentImageForm extends BaseEntity {
+  /** 主键ID */
+  id?: string | number;
+
+  /** 图片名称(自定义,非文件名) */
+  imageName?: string;
+
+  /** 图片URL(OSS地址) */
+  imageUrl?: string;
+
+  /** 图片类型: background=背景图, icon=图标 */
+  imageType?: string;
+
+  /** 使用场景: online=线上, offline=线下, both=通用 */
+  sceneType?: string;
+
+  /** 排序号,越小越靠前 */
+  sortOrder?: number;
+
+  /** 状态: enabled=启用, disabled=禁用 */
+  status?: string;
+
+  /** 备注 */
+  remark?: string;
+
+  /** 创建时间 */
+  createTime?: string;
+
+  /** 更新时间 */
+  updateTime?: string;
+}
+
+// TODO: 赛事图片查询对象 - 用于列表查询条件
+export interface TournamentImageQuery extends PageQuery {
+  /** 图片名称 */
+  imageName?: string;
+
+  /** 图片类型: background=背景图, icon=图标 */
+  imageType?: string;
+
+  /** 使用场景: online=线上, offline=线下, both=通用 */
+  sceneType?: string;
+
+  /** 状态: enabled=启用, disabled=禁用 */
+  status?: string;
+
+  /** 日期范围参数 */
+  params?: any;
+}

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

@@ -39,11 +39,13 @@ export interface TournamentsVO {
   isShow: number;
   isOffline?: number; // 👈 新增字段
   isFace: number;
+  serialNumber?: string;
 }
 
 export interface TournamentsForm extends BaseEntity {
   id?: string | number;
   name?: string;
+  serialNumber?: string;
   competitionLocation?: string;
   startTime?: string;
   endTime?: string;
@@ -95,6 +97,7 @@ export interface TournamentsForm extends BaseEntity {
 export interface TournamentsQuery extends PageQuery {
   id?: string;
   name?: string;
+  serialNumber?: string;
   startTime?: string;
   startTimeOne?: string;
   endTime?: string;

+ 513 - 0
src/components/TournamentImageSelect/index.vue

@@ -0,0 +1,513 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="800px"
+    append-to-body
+    @open="handleOpen"
+    @close="handleClose"
+  >
+    <!-- 标签页切换 -->
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane label="图标" name="icon" />
+      <el-tab-pane label="背景图" name="background" />
+    </el-tabs>
+
+    <!-- 操作栏 -->
+    <div class="filter-bar">
+      <el-radio-group v-model="sceneFilter" @change="loadImages">
+        <el-radio-button label="online">线上</el-radio-button>
+        <el-radio-button label="offline">线下</el-radio-button>
+        <el-radio-button label="both">通用</el-radio-button>
+        <el-radio-button label="all">全部</el-radio-button>
+      </el-radio-group>
+      <el-button type="primary" icon="Upload" @click="openUploadDialog">上传新图片</el-button>
+    </div>
+
+    <!-- 图片网格 -->
+    <div v-loading="loading" class="image-grid-container">
+      <div v-if="imageList.length === 0 && !loading" class="empty-tip">
+        <el-empty description="暂无可用图片" />
+      </div>
+
+      <div v-else class="image-grid">
+        <div
+          v-for="item in imageList"
+          :key="item.id"
+          class="image-card"
+          :class="{ selected: selectedImageUrl === item.imageUrl }"
+          @click="handleSelect(item)"
+        >
+          <div class="image-wrapper">
+            <img :src="item.imageUrl" :alt="item.imageName" />
+          </div>
+          <div class="image-info">
+            <span class="image-name" :title="item.imageName">{{ item.imageName }}</span>
+            <div class="image-tags">
+              <el-tag size="small" :type="item.imageType === 'icon' ? 'success' : 'warning'">
+                {{ item.imageType === 'icon' ? '图标' : '背景' }}
+              </el-tag>
+              <el-tag size="small" :type="getSceneTagType(item.sceneType)">
+                {{ getSceneLabel(item.sceneType) }}
+              </el-tag>
+            </div>
+          </div>
+          <div v-if="selectedImageUrl === item.imageUrl" class="selected-badge">
+            <el-icon><Check /></el-icon>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 已选预览 -->
+    <div v-if="selectedImageUrl" class="selected-preview">
+      <span class="preview-label">已选择:</span>
+      <img :src="selectedImageUrl" alt="已选图片" class="preview-img" />
+      <span class="preview-name">{{ selectedImageName }}</span>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" :disabled="!selectedImageUrl" @click="handleConfirm">确 定</el-button>
+      </div>
+    </template>
+  </el-dialog>
+
+  <!-- 上传图片对话框 -->
+  <el-dialog
+    v-model="uploadDialogVisible"
+    title="上传赛事图片"
+    width="500px"
+    append-to-body
+    @close="resetUploadForm"
+  >
+    <el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="80px">
+      <el-form-item label="图片名称" prop="imageName">
+        <el-input v-model="uploadForm.imageName" placeholder="请输入图片名称" />
+      </el-form-item>
+      <el-form-item label="图片类型" prop="imageType">
+        <el-select v-model="uploadForm.imageType" placeholder="请选择" style="width: 100%">
+          <el-option label="图标" value="icon" />
+          <el-option label="背景图" value="background" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="使用场景" prop="sceneType">
+        <el-select v-model="uploadForm.sceneType" placeholder="请选择" style="width: 100%">
+          <el-option label="线上" value="online" />
+          <el-option label="线下" value="offline" />
+          <el-option label="通用" value="both" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-switch v-model="uploadForm.status" active-value="enabled" inactive-value="disabled" />
+      </el-form-item>
+      <el-form-item label="上传图片" prop="file">
+        <el-upload
+          ref="uploadRef"
+          drag
+          action="#"
+          :auto-upload="false"
+          :on-change="handleFileChange"
+          :limit="1"
+          accept="image/*"
+        >
+          <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+          <div class="el-upload__text">
+            将文件拖到此处,或<em>点击上传</em>
+          </div>
+          <template #tip>
+            <div class="el-upload__tip">只能上传jpg/png文件,且不超过5MB</div>
+          </template>
+        </el-upload>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="uploadDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="uploadLoading" @click="submitUpload">确 定</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { selectByTypeAndScene, listTournamentImage, addTournamentImage } from '@/api/system/physical/tournamentImage';
+import { TournamentImageVO, TournamentImageForm } from '@/api/system/physical/tournamentImage/types';
+import { Check, UploadFilled } from '@element-plus/icons-vue';
+import { uploadTournament } from '@/api/system/business/tournaments';
+import { ElMessage } from 'element-plus';
+
+const props = withDefaults(
+  defineProps<{
+    /** 是否显示对话框 */
+    modelValue: boolean;
+    /** 对话框标题 */
+    title?: string;
+    /** 默认选中的图片类型: icon / background */
+    defaultType?: string;
+    /** 默认选中的场景: online / offline */
+    defaultScene?: string;
+    /** 是否只查询启用的图片 */
+    onlyEnabled?: boolean;
+  }>(),
+  {
+    title: '选择图片',
+    defaultType: 'icon',
+    defaultScene: 'online',
+    onlyEnabled: true
+  }
+);
+
+const emit = defineEmits<{
+  'update:modelValue': [val: boolean];
+  confirm: [url: string, image: TournamentImageVO];
+  cancel: [];
+}>();
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const loading = ref(false);
+const imageList = ref<TournamentImageVO[]>([]);
+const activeTab = ref('icon');
+const sceneFilter = ref('online');
+const selectedImageUrl = ref('');
+const selectedImageName = ref('');
+const selectedImage = ref<TournamentImageVO | null>(null);
+
+// 上传相关
+const uploadDialogVisible = ref(false);
+const uploadLoading = ref(false);
+const uploadFormRef = ref();
+const uploadRef = ref();
+const uploadForm = reactive<TournamentImageForm>({
+  imageName: '',
+  imageUrl: '',
+  imageType: 'icon',
+  sceneType: 'online',
+  status: 'enabled'
+});
+const uploadRules = {
+  imageName: [{ required: true, message: '请输入图片名称', trigger: 'blur' }],
+  imageType: [{ required: true, message: '请选择图片类型', trigger: 'change' }],
+  sceneType: [{ required: true, message: '请选择使用场景', trigger: 'change' }]
+};
+const uploadFile = ref<File | null>(null);
+
+/** 初始化 */
+// TODO: 打开对话框时加载图片列表
+const handleOpen = () => {
+  activeTab.value = props.defaultType || 'icon';
+  sceneFilter.value = props.defaultScene || 'online';
+  selectedImageUrl.value = '';
+  selectedImageName.value = '';
+  selectedImage.value = null;
+  loadImages();
+};
+
+/** 关闭 */
+// TODO: 关闭对话框时清空选中状态
+const handleClose = () => {
+  selectedImageUrl.value = '';
+  selectedImageName.value = '';
+  selectedImage.value = null;
+};
+
+/** 标签页切换 */
+// TODO: 切换图标/背景图标签时重新加载数据
+const handleTabChange = () => {
+  loadImages();
+};
+
+/** 加载图片列表 */
+// TODO: 根据类型和场景加载可用图片
+const loadImages = async () => {
+  loading.value = true;
+  try {
+    // 如果场景选了"全部",走 list 接口(不带场景过滤)
+    if (sceneFilter.value === 'all') {
+      const res = await listTournamentImage({
+        imageType: activeTab.value,
+        status: props.onlyEnabled ? 'enabled' : undefined,
+        pageNum: 1,
+        pageSize: 100
+      });
+      imageList.value = res.rows || [];
+    } else {
+      // 否则走 selectByTypeAndScene 接口,后端自动匹配 online/offline/both
+      const res = await selectByTypeAndScene(activeTab.value, sceneFilter.value);
+      imageList.value = res.data || [];
+    }
+  } catch (error) {
+    console.error('加载图片失败', error);
+    imageList.value = [];
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 选择图片 */
+// TODO: 用户点击选择某张图片
+const handleSelect = (item: TournamentImageVO) => {
+  selectedImageUrl.value = item.imageUrl;
+  selectedImageName.value = item.imageName;
+  selectedImage.value = item;
+};
+
+/** 确认选择 */
+// TODO: 确认选择并返回选中的图片信息
+const handleConfirm = () => {
+  if (selectedImage.value) {
+    emit('confirm', selectedImageUrl.value, selectedImage.value);
+  }
+  visible.value = false;
+};
+
+/** 取消 */
+// TODO: 取消选择并关闭对话框
+const handleCancel = () => {
+  // 先触发 cancel 事件
+  emit('cancel');
+  // 然后关闭对话框(使用 nextTick 确保事件先发出)
+  nextTick(() => {
+    visible.value = false;
+  });
+};
+
+/** 获取场景标签类型 */
+// TODO: 根据场景返回对应的标签样式
+const getSceneTagType = (scene: string) => {
+  switch (scene) {
+    case 'online':
+      return 'primary';
+    case 'offline':
+      return 'info';
+    case 'both':
+      return 'success';
+    default:
+      return '';
+  }
+};
+
+/** 获取场景标签文字 */
+// TODO: 将场景代码转换为中文显示
+const getSceneLabel = (scene: string) => {
+  switch (scene) {
+    case 'online':
+      return '线上';
+    case 'offline':
+      return '线下';
+    case 'both':
+      return '通用';
+    default:
+      return scene;
+  }
+};
+
+/** 打开上传对话框 */
+// TODO: 打开上传图片的弹窗
+const openUploadDialog = () => {
+  uploadForm.imageType = activeTab.value; // 默认当前标签页的类型
+  uploadForm.sceneType = sceneFilter.value === 'all' ? 'online' : sceneFilter.value;
+  uploadDialogVisible.value = true;
+};
+
+/** 重置上传表单 */
+// TODO: 清空上传表单的所有字段
+const resetUploadForm = () => {
+  uploadFormRef.value?.resetFields();
+  uploadForm.imageName = '';
+  uploadForm.imageUrl = '';
+  uploadForm.imageType = 'icon';
+  uploadForm.sceneType = 'online';
+  uploadForm.status = 'enabled';
+  uploadFile.value = null;
+  if (uploadRef.value) {
+    uploadRef.value.clearFiles();
+  }
+};
+
+/** 处理文件选择 */
+// TODO: 监听用户上传的文件
+const handleFileChange = (file: any) => {
+  uploadFile.value = file.raw;
+};
+
+/** 提交上传 */
+// TODO: 上传图片到OSS并保存到数据库
+const submitUpload = async () => {
+  const valid = await uploadFormRef.value?.validate().catch(() => false);
+  if (!valid) return;
+
+  if (!uploadFile.value) {
+    ElMessage.warning('请选择要上传的图片');
+    return;
+  }
+
+  uploadLoading.value = true;
+  try {
+    // 先上传到 OSS
+    const ossRes = await uploadTournament(uploadFile.value);
+    if (ossRes.code !== 200) {
+      throw new Error(ossRes.msg || '上传失败');
+    }
+
+    // 设置图片 URL
+    uploadForm.imageUrl = ossRes.data.url;
+
+    // 保存到数据库
+    const res = await addTournamentImage(uploadForm);
+    if (res.code === 200) {
+      ElMessage.success('上传成功');
+      uploadDialogVisible.value = false;
+      // 重新加载列表
+      loadImages();
+      // 自动选中新上传的图片
+      const newItem = res.data;
+      if (newItem && newItem.imageUrl) {
+        selectedImageUrl.value = newItem.imageUrl;
+        selectedImageName.value = newItem.imageName;
+        selectedImage.value = newItem;
+      }
+    } else {
+      throw new Error(res.msg || '保存失败');
+    }
+  } catch (error: any) {
+    console.error('上传失败', error);
+    ElMessage.error(error.message || '上传失败,请重试');
+  } finally {
+    uploadLoading.value = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.filter-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.image-grid-container {
+  min-height: 200px;
+  max-height: 450px;
+  overflow-y: auto;
+}
+
+.empty-tip {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 40px 0;
+}
+
+.image-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+  gap: 12px;
+}
+
+.image-card {
+  position: relative;
+  border: 2px solid #e4e7ed;
+  border-radius: 8px;
+  overflow: hidden;
+  cursor: pointer;
+  transition: all 0.2s ease;
+
+  &:hover {
+    border-color: #409eff;
+    box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
+  }
+
+  &.selected {
+    border-color: #409eff;
+    background-color: rgba(64, 158, 255, 0.05);
+  }
+}
+
+.image-wrapper {
+  width: 100%;
+  height: 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #f5f7fa;
+  overflow: hidden;
+
+  img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+  }
+}
+
+.image-info {
+  padding: 8px;
+  text-align: center;
+}
+
+.image-name {
+  display: block;
+  font-size: 13px;
+  color: #303133;
+  margin-bottom: 4px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.image-tags {
+  display: flex;
+  gap: 4px;
+  justify-content: center;
+  flex-wrap: wrap;
+}
+
+.selected-badge {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 24px;
+  height: 24px;
+  background-color: #409eff;
+  color: #fff;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+}
+
+.selected-preview {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 12px;
+  padding: 8px 12px;
+  background-color: #f0f9ff;
+  border-radius: 4px;
+
+  .preview-label {
+    color: #606266;
+    font-size: 14px;
+  }
+
+  .preview-img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    object-fit: cover;
+  }
+
+  .preview-name {
+    color: #303133;
+    font-size: 14px;
+    font-weight: 500;
+  }
+}
+</style>

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

@@ -17,7 +17,7 @@
               <el-select v-model="queryParams.claimed" placeholder="请选择状态" clearable style="width: 200px">
                 <el-option label="未领取" :value="0" />
                 <el-option label="已领取" :value="1" />
-                <el-option label="已提交待审核" :value="2" />
+                <el-option label="待审核" :value="2" />
               </el-select>
             </el-form-item>
             <el-form-item>
@@ -73,7 +73,7 @@
           <template #default="scope">
             <el-tag v-if="scope.row.claimed === 0" type="info">未领取</el-tag>
             <el-tag v-else-if="scope.row.claimed === 1" type="success">已领取</el-tag>
-            <el-tag v-else-if="scope.row.claimed === 2" type="warning">已提交待审核</el-tag>
+            <el-tag v-else-if="scope.row.claimed === 2" type="warning">待审核</el-tag>
             <el-tag v-else type="danger">未知</el-tag>
           </template>
         </el-table-column>

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

@@ -2022,14 +2022,14 @@ const handleGoToTag = () => {
   dialog.visible = false; // 关闭弹窗
   // 使用 nextTick 确保关闭动画完成后跳转(可选)
   nextTick(() => {
-    proxy?.$router.push('/service/tag');
+    proxy?.$router.push('/operation/tag');
   });
 };
 const handleGoToCategory = () => {
   dialog.visible = false; // 关闭弹窗
   // 使用 nextTick 确保关闭动画完成后跳转(可选)
   nextTick(() => {
-    proxy?.$router.push('/service/catory');
+    proxy?.$router.push('/operation/catory');
   });
 };
 // 获取奖励提示内容

+ 13 - 5
src/views/system/physical/participants/index.vue

@@ -74,14 +74,22 @@
         <el-table-column label="赛事ID" align="center" prop="tournamentId" v-if="true" />
         <el-table-column label="赛事名称" align="center" prop="tournamentsName" />
         <el-table-column label="赛事状态" align="center" prop="statusText" />
+        <el-table-column label="取票状态" align="center" prop="ticketStatus">
+          <template #default="scope">
+            <el-tag v-if="scope.row.ticketStatus === 0" type="info">未取票</el-tag>
+            <el-tag v-else-if="scope.row.ticketStatus === 1" type="success">已取票</el-tag>
+            <el-tag v-else-if="scope.row.ticketStatus === 2" type="warning">已核销</el-tag>
+            <el-tag v-else type="info">未知</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="报名用户" align="center" prop="userName" />
         <el-table-column label="手机号" align="center" prop="phone" />
         <el-table-column label="报名条件" align="center" prop="tournamentCondition" />
         <el-table-column label="报名时间" align="center" prop="registrationTime" />
         <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="getNextTournament(scope.row)">晋级</el-button>
+            <el-tooltip content="手动取票" placement="top">
+              <el-button link type="primary" icon="Edit" @click="getNextTournament(scope.row)">手动取票</el-button>
             </el-tooltip>
           </template>
         </el-table-column>
@@ -236,10 +244,10 @@ const getList = async () => {
   console.log('=== getList 开始 ===');
   console.log('当前 loading:', loading.value);
   console.log('查询参数:', queryParams.value);
-  
+
   loading.value = true;
   console.log('设置 loading 为 true:', loading.value);
-  
+
   try {
     console.log('准备调用 API...');
     const res = await listParticipants(queryParams.value);
@@ -247,7 +255,7 @@ const getList = async () => {
     console.log('完整 API 响应:', res);
     console.log('响应码:', res.code);
     console.log('数据:', res.data);
-    
+
     // 检查响应结构
     if (res.code === 200) {
       // 如果 data 是数组,直接使用;如果是对象且有 rows 属性,使用 rows

+ 725 - 0
src/views/system/physical/tournamentImage/index.vue

@@ -0,0 +1,725 @@
+<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="imageName">
+              <el-input v-model="queryParams.imageName" placeholder="请输入图片名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="图片类型" prop="imageType">
+              <el-select v-model="queryParams.imageType" placeholder="请选择图片类型" clearable style="width: 180px">
+                <el-option label="背景图" value="background" />
+                <el-option label="图标" value="icon" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="使用场景" prop="sceneType">
+              <el-select v-model="queryParams.sceneType" placeholder="请选择使用场景" clearable style="width: 180px">
+                <el-option label="线上" value="online" />
+                <el-option label="线下" value="offline" />
+                <el-option label="通用" value="both" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
+                <el-option label="启用" value="enabled" />
+                <el-option label="禁用" value="disabled" />
+              </el-select>
+            </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:tournamentImage:add']">上传图片</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:tournamentImage:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:tournamentImage:remove']">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="tournamentImageList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="编号" align="center" prop="id" width="70" />
+        <el-table-column label="图片预览" align="center" width="100">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.imageUrl"
+              :src="scope.row.imageUrl"
+              style="width: 50px; height: 50px; 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" prop="imageName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="图片类型" align="center" prop="imageType" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.imageType === 'background'" type="warning">背景图</el-tag>
+            <el-tag v-else-if="scope.row.imageType === 'icon'" type="success">图标</el-tag>
+            <span v-else>{{ scope.row.imageType }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="使用场景" align="center" prop="sceneType" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.sceneType === 'online'" type="primary">线上</el-tag>
+            <el-tag v-else-if="scope.row.sceneType === 'offline'" type="info">线下</el-tag>
+            <el-tag v-else-if="scope.row.sceneType === 'both'" type="success">通用</el-tag>
+            <span v-else>{{ scope.row.sceneType }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="状态" align="center" prop="status" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.status"
+              active-value="enabled"
+              inactive-value="disabled"
+              @change="handleStatusChange(scope.row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="创建时间" align="center" prop="createTime" width="170">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="120" 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:tournamentImage:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:tournamentImage: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 @close="handleDialogClose">
+      <el-form ref="tournamentImageFormRef" :model="form" :rules="rules" label-width="100px">
+        <!-- 基本信息区域 -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="图片名称" prop="imageName">
+              <el-input 
+                v-model="form.imageName" 
+                placeholder="请输入自定义名称(如:篮球赛事图标)" 
+                clearable
+              />
+            </el-form-item>
+          </el-col>
+          
+          <el-col :span="12">
+            <el-form-item label="图片类型" prop="imageType">
+              <el-select v-model="form.imageType" placeholder="请选择图片类型" style="width: 100%">
+                <el-option label="背景图" value="background" />
+                <el-option label="图标" value="icon" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="使用场景" prop="sceneType">
+              <el-select v-model="form.sceneType" placeholder="请选择使用场景" style="width: 100%">
+                <el-option label="线上赛事" value="online" />
+                <el-option label="线下赛事" value="offline" />
+                <el-option label="通用(线上线下均可)" value="both" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          
+          <el-col :span="12">
+            <el-form-item label="状态" prop="status">
+              <el-radio-group v-model="form.status">
+                <el-radio value="enabled">启用</el-radio>
+                <el-radio value="disabled">禁用</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 图片预览和上传区域 -->
+        <el-form-item label="图片">
+          <div v-if="form.imageUrl || imagePreviewUrl" class="image-upload-wrapper">
+            <div class="image-preview-container">
+              <img :src="imagePreviewUrl || form.imageUrl" alt="预览图" class="preview-image" />
+              <div class="image-actions">
+                <el-button link type="primary" size="small" @click="handlePreviewClick(null)">查看大图</el-button>
+                <el-button link type="warning" size="small" @click="handleClearImage">更换图片</el-button>
+              </div>
+            </div>
+          </div>
+          <el-upload
+            ref="modifyUploadRef"
+            v-else
+            drag
+            :auto-upload="false"
+            :on-change="handleModifyFileChange"
+            :limit="1"
+            accept="image/*"
+            class="image-upload-area"
+          >
+            <el-icon class="upload-icon"><UploadFilled /></el-icon>
+            <div class="upload-text">
+              <p>将文件拖到此处,或<em>点击上传</em></p>
+              <p class="upload-tip">只能上传 jpg/png 文件,且不超过 5MB</p>
+            </div>
+          </el-upload>
+        </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="uploadDialogVisible"
+      title="上传赛事图片"
+      width="500px"
+      append-to-body
+      @close="resetUploadForm"
+    >
+      <el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="80px">
+        <el-form-item label="图片名称" prop="imageName">
+          <el-input v-model="uploadForm.imageName" placeholder="请输入图片名称" />
+        </el-form-item>
+        <el-form-item label="图片类型" prop="imageType">
+          <el-select v-model="uploadForm.imageType" placeholder="请选择" style="width: 100%">
+            <el-option label="图标" value="icon" />
+            <el-option label="背景图" value="background" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="使用场景" prop="sceneType">
+          <el-select v-model="uploadForm.sceneType" placeholder="请选择" style="width: 100%">
+            <el-option label="线上" value="online" />
+            <el-option label="线下" value="offline" />
+            <el-option label="通用" value="both" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-switch v-model="uploadForm.status" active-value="enabled" inactive-value="disabled" />
+        </el-form-item>
+        <el-form-item label="上传图片" prop="file">
+          <el-upload
+            ref="uploadRef"
+            drag
+            action="#"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :limit="1"
+            accept="image/*"
+          >
+            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+            <div class="el-upload__text">
+              将文件拖到此处,或<em>点击上传</em>
+            </div>
+            <template #tip>
+              <div class="el-upload__tip">只能上传jpg/png文件,且不超过5MB</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="uploadDialogVisible = false">取 消</el-button>
+          <el-button type="primary" :loading="uploadLoading" @click="submitUpload">确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览弹窗 -->
+    <el-dialog v-model="previewDialog.visible" title="图片预览" width="50%" append-to-body>
+      <img :src="previewDialog.src" alt="预览图片" style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="TournamentImage" lang="ts">
+import {
+  listTournamentImage,
+  getTournamentImage,
+  delTournamentImage,
+  addTournamentImage,
+  updateTournamentImage
+} from '@/api/system/physical/tournamentImage';
+import { TournamentImageVO, TournamentImageQuery, TournamentImageForm } from '@/api/system/physical/tournamentImage/types';
+import { UploadFilled, Picture } from '@element-plus/icons-vue';
+import request from '@/utils/request';
+import { uploadTournament } from '@/api/system/business/tournaments';
+import { ElMessage } from 'element-plus';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const tournamentImageList = ref<TournamentImageVO[]>([]);
+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 tournamentImageFormRef = ref<ElFormInstance>();
+const modifyUploadRef = ref(); // 修改对话框的上传组件引用
+const imagePreviewUrl = ref('');
+
+// 上传相关
+const uploadDialogVisible = ref(false);
+const uploadLoading = ref(false);
+const uploadFormRef = ref();
+const uploadRef = ref();
+const uploadFile = ref<File | null>(null);
+const modifyUploadFile = ref<File | null>(null); // 修改对话框的文件
+const uploadForm = reactive<TournamentImageForm>({
+  imageName: '',
+  imageUrl: '',
+  imageType: 'icon',
+  sceneType: 'online',
+  status: 'enabled'
+});
+const uploadRules = {
+  imageName: [{ required: true, message: '请输入图片名称', trigger: 'blur' }],
+  imageType: [{ required: true, message: '请选择图片类型', trigger: 'change' }],
+  sceneType: [{ required: true, message: '请选择使用场景', trigger: 'change' }]
+};
+
+const previewDialog = reactive({
+  visible: false,
+  src: ''
+});
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: TournamentImageForm = {
+  id: undefined,
+  imageName: undefined,
+  imageUrl: undefined,
+  imageType: undefined,
+  sceneType: undefined,
+  sortOrder: 0,
+  status: 'enabled',
+  remark: undefined,
+  createTime: undefined,
+  updateTime: undefined
+};
+
+const data = reactive<PageData<TournamentImageForm, TournamentImageQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    imageName: undefined,
+    imageType: undefined,
+    sceneType: undefined,
+    status: undefined,
+    params: {}
+  },
+  rules: {
+    imageName: [{ required: true, message: '图片名称不能为空', trigger: 'blur' }],
+    imageUrl: [{ required: true, message: '请上传图片', trigger: 'change' }],
+    imageType: [{ required: true, message: '请选择图片类型', trigger: 'change' }],
+    sceneType: [{ required: true, message: '请选择使用场景', trigger: 'change' }],
+    status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询赛事图片列表 */
+// TODO: 获取分页数据并更新列表
+const getList = async () => {
+  loading.value = true;
+  const res = await listTournamentImage(queryParams.value);
+  tournamentImageList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+// TODO: 重置表单并关闭对话框
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  tournamentImageFormRef.value?.resetFields();
+  imagePreviewUrl.value = '';
+  modifyUploadFile.value = null; // 清空修改对话框的文件
+  if (modifyUploadRef.value) {
+    modifyUploadRef.value.clearFiles(); // 清空上传组件
+  }
+  // 恢复 imageUrl 的验证规则
+  rules.value.imageUrl = [{ required: true, message: '请上传图片', trigger: 'change' }];
+};
+
+/** 对话框关闭 */
+const handleDialogClose = () => {
+  reset();
+  // 清空表单验证错误
+  tournamentImageFormRef.value?.clearValidate();
+};
+
+/** 搜索按钮操作 */
+// TODO: 执行查询并重置页码为1
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: TournamentImageVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+// TODO: 打开上传图片对话框
+const handleAdd = () => {
+  reset();
+  // 打开上传对话框
+  openUploadDialog();
+};
+
+/** 打开上传对话框 */
+const openUploadDialog = () => {
+  uploadForm.imageType = 'icon'; // 默认图标类型
+  uploadForm.sceneType = 'online'; // 默认线上场景
+  uploadForm.status = 'enabled'; // 默认启用
+  uploadDialogVisible.value = true;
+};
+
+/** 重置上传表单 */
+const resetUploadForm = () => {
+  uploadFormRef.value?.resetFields();
+  uploadForm.imageName = '';
+  uploadForm.imageUrl = '';
+  uploadForm.imageType = 'icon';
+  uploadForm.sceneType = 'online';
+  uploadForm.status = 'enabled';
+  uploadFile.value = null;
+  if (uploadRef.value) {
+    uploadRef.value.clearFiles();
+  }
+};
+
+/** 处理文件选择 */
+const handleFileChange = (file: any) => {
+  uploadFile.value = file.raw;
+};
+
+/** 提交上传 */
+const submitUpload = async () => {
+  const valid = await uploadFormRef.value?.validate().catch(() => false);
+  if (!valid) return;
+
+  if (!uploadFile.value) {
+    ElMessage.warning('请选择要上传的图片');
+    return;
+  }
+
+  uploadLoading.value = true;
+  try {
+    // 先上传到 OSS
+    const ossRes = await uploadTournament(uploadFile.value);
+    if (ossRes.code !== 200) {
+      throw new Error(ossRes.msg || '上传失败');
+    }
+
+    // 设置图片 URL
+    uploadForm.imageUrl = ossRes.data.url;
+
+    // 判断是从修改对话框还是新增按钮调用的
+    // 如果 form.id 存在,说明是从修改对话框调用的(更换图片)
+    if (form.value.id) {
+      // 从修改对话框调用:只更新当前表单的图片 URL
+      form.value.imageUrl = ossRes.data.url;
+      form.value.imageName = uploadForm.imageName;
+      form.value.imageType = uploadForm.imageType;
+      form.value.sceneType = uploadForm.sceneType;
+      form.value.status = uploadForm.status;
+      
+      uploadDialogVisible.value = false;
+      ElMessage.success('图片更换成功');
+    } else {
+      // 从新增按钮调用:保存新图片
+      const res = await addTournamentImage(uploadForm);
+      if (res.code === 200) {
+        ElMessage.success('上传成功');
+        uploadDialogVisible.value = false;
+        // 重新加载列表
+        getList();
+      } else {
+        throw new Error(res.msg || '保存失败');
+      }
+    }
+  } catch (error: any) {
+    console.error('上传失败', error);
+    ElMessage.error(error.message || '上传失败,请重试');
+  } finally {
+    uploadLoading.value = false;
+  }
+};
+
+/** 修改按钮操作 */
+// TODO: 加载详情数据并打开编辑对话框
+const handleUpdate = async (row?: TournamentImageVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getTournamentImage(_id);
+  Object.assign(form.value, res.data);
+  // 确保 imageUrl 被正确赋值
+  if (res.data.imageUrl) {
+    form.value.imageUrl = res.data.imageUrl;
+    imagePreviewUrl.value = res.data.imageUrl;
+  }
+  // 清空修改对话框的文件
+  modifyUploadFile.value = null;
+  dialog.visible = true;
+  dialog.title = '修改赛事图片';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  // 如果是修改模式,临时移除 imageUrl 的必填验证
+  const isUpdateMode = !!form.value.id;
+  if (isUpdateMode) {
+    // 修改模式:不需要验证 imageUrl(因为图片已存在)
+    rules.value.imageUrl = [];
+  }
+  
+  tournamentImageFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        if (form.value.id) {
+          // 修改模式:如果有新图片,先上传
+          let imageUrl = form.value.imageUrl;
+          
+          if (modifyUploadFile.value) {
+            // 先上传到 OSS
+            const ossRes = await uploadTournament(modifyUploadFile.value);
+            if (ossRes.code !== 200) {
+              throw new Error(ossRes.msg || '上传图片失败');
+            }
+            imageUrl = ossRes.data.url; // 使用新图片 URL
+          }
+          
+          // 更新基本信息和新图片 URL
+          await updateTournamentImage({
+            id: form.value.id,
+            imageName: form.value.imageName,  // 直接使用用户输入的名称
+            imageType: form.value.imageType,
+            sceneType: form.value.sceneType,
+            status: form.value.status,
+            imageUrl: imageUrl // 新图片 URL 或原图片 URL
+          }).finally(() => {
+            buttonLoading.value = false;
+            dialog.visible = false;
+            getList();
+          });
+          proxy?.$modal.msgSuccess('修改成功');
+        } else {
+          // 新增模式:需要包含所有字段
+          await addTournamentImage({
+            imageName: form.value.imageName,
+            imageUrl: form.value.imageUrl,
+            imageType: form.value.imageType,
+            sceneType: form.value.sceneType,
+            status: form.value.status
+          }).finally(() => {
+            buttonLoading.value = false;
+            dialog.visible = false;
+            getList();
+          });
+          proxy?.$modal.msgSuccess('添加成功');
+        }
+      } catch (error) {
+        console.error('提交失败:', error);
+        proxy?.$modal.msgError('操作失败,请重试');
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: TournamentImageVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除赛事图片编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delTournamentImage(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+
+
+/** 状态变更 */
+const handleStatusChange = async (row: TournamentImageVO) => {
+  const statusText = row.status === 'enabled' ? '启用' : '禁用';
+  try {
+    await updateTournamentImage({
+      id: row.id,
+      status: row.status
+    } as TournamentImageForm);
+    proxy?.$modal.msgSuccess(`${statusText}成功`);
+  } catch {
+    proxy?.$modal.msgError(`${statusText}失败`);
+    await getList();
+  }
+};
+
+/** 清除图片(更换图片时调用) */
+const handleClearImage = () => {
+  // 清空 URL
+  form.value.imageUrl = undefined;
+  imagePreviewUrl.value = '';
+  modifyUploadFile.value = null;
+  if (modifyUploadRef.value) {
+    modifyUploadRef.value.clearFiles();
+  }
+};
+
+/** 处理修改对话框文件选择 */
+const handleModifyFileChange = (file: any) => {
+  modifyUploadFile.value = file.raw;
+  // 预览图片
+  const reader = new FileReader();
+  reader.onload = (e: any) => {
+    imagePreviewUrl.value = e.target.result;
+  };
+  if (file.raw) {
+    reader.readAsDataURL(file.raw);
+  }
+};
+
+/** 更换图片(删除旧方法,改为直接在下方上传区域操作) */
+// const handleReplaceImage = () => { ... }; // 已删除
+
+/** 图片预览 */
+const handlePreviewClick = (file?: any) => {
+  const currentSrc = imagePreviewUrl.value || form.value.imageUrl;
+  if (currentSrc) {
+    previewDialog.src = currentSrc;
+    previewDialog.visible = true;
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style lang="scss" scoped>
+.image-upload-wrapper {
+  width: 100%;
+}
+
+.image-preview-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 15px;
+  padding: 20px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 8px;
+  background-color: #fafafa;
+}
+
+.preview-image {
+  max-width: 250px;
+  max-height: 200px;
+  object-fit: cover;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.image-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.image-upload-area {
+  width: 100%;
+}
+
+.upload-icon {
+  font-size: 48px;
+  color: #c0c4cc;
+}
+
+.upload-text {
+  margin-top: 15px;
+  color: var(--el-text-color-secondary);
+  
+  p {
+    margin: 5px 0;
+  }
+  
+  .upload-tip {
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+  }
+}
+
+.upload-container {
+  width: 100%;
+}
+
+.upload-trigger {
+  padding: 30px 20px;
+  text-align: center;
+  color: var(--el-text-color-secondary);
+}
+
+.el-upload-tip {
+  margin-top: 8px;
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+:deep(.el-form-item__label) {
+  font-weight: 600;
+}
+</style>

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

@@ -177,6 +177,9 @@
         <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.serialNumber" placeholder="请输入赛事编号" />
+        </el-form-item>
         <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" />

+ 2 - 1
src/views/system/physical/tournamentsRegistration/index.vue

@@ -209,7 +209,8 @@ import {
 import { ElSelect } from 'element-plus';
 import { LeagueTournamentVO } from '@/api/system/physical/leagueTournament/types';
 import { parseTime } from '@/utils/dateUtils';
-import { useRoute, onMounted } from 'vue-router';
+import { useRoute } from 'vue-router';
+import { onMounted } from 'vue';
 const route = useRoute();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;