重构赛事的树状图

This commit is contained in:
Kunagisa 2025-07-29 16:15:54 +08:00
parent 874e2fc4dc
commit 1977b791e5
8 changed files with 2358 additions and 658 deletions

421
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.9.0",
"d3": "^7.9.0",
"jszip": "^3.10.1",
"process": "^0.11.10",
"vue": "^3.5.13",
@ -1579,6 +1580,14 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -1635,6 +1644,376 @@
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d3": {
"version": "7.9.0",
"resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz",
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
"dependencies": {
"d3-array": "3",
"d3-axis": "3",
"d3-brush": "3",
"d3-chord": "3",
"d3-color": "3",
"d3-contour": "4",
"d3-delaunay": "6",
"d3-dispatch": "3",
"d3-drag": "3",
"d3-dsv": "3",
"d3-ease": "3",
"d3-fetch": "3",
"d3-force": "3",
"d3-format": "3",
"d3-geo": "3",
"d3-hierarchy": "3",
"d3-interpolate": "3",
"d3-path": "3",
"d3-polygon": "3",
"d3-quadtree": "3",
"d3-random": "3",
"d3-scale": "4",
"d3-scale-chromatic": "3",
"d3-selection": "3",
"d3-shape": "3",
"d3-time": "3",
"d3-time-format": "4",
"d3-timer": "3",
"d3-transition": "3",
"d3-zoom": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-axis": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz",
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-brush": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz",
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "3",
"d3-transition": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-chord": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
"dependencies": {
"d3-path": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-contour": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz",
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
"dependencies": {
"d3-array": "^3.2.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"dependencies": {
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dsv": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
"dependencies": {
"commander": "7",
"iconv-lite": "0.6",
"rw": "1"
},
"bin": {
"csv2json": "bin/dsv2json.js",
"csv2tsv": "bin/dsv2dsv.js",
"dsv2dsv": "bin/dsv2dsv.js",
"dsv2json": "bin/dsv2json.js",
"json2csv": "bin/json2dsv.js",
"json2dsv": "bin/json2dsv.js",
"json2tsv": "bin/json2dsv.js",
"tsv2csv": "bin/dsv2dsv.js",
"tsv2json": "bin/dsv2json.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-fetch": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
"dependencies": {
"d3-dsv": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-force": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-geo": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"dependencies": {
"d3-array": "2.5.0 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-polygon": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz",
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-random": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"dependencies": {
"d3-color": "1 - 3",
"d3-interpolate": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz",
@ -1692,6 +2071,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delaunator": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"dependencies": {
"robust-predicates": "^3.0.2"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -2103,6 +2490,17 @@
"node": ">=18.18.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
@ -2113,6 +2511,14 @@
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
@ -2556,6 +2962,11 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
},
"node_modules/rollup": {
"version": "4.40.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.40.1.tgz",
@ -2607,11 +3018,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"axios": "^1.9.0",
"d3": "^7.9.0",
"jszip": "^3.10.1",
"process": "^0.11.10",
"vue": "^3.5.13",

File diff suppressed because it is too large Load Diff

View File

@ -3,26 +3,18 @@
<h1>单败淘汰赛赛程树状图</h1>
<div class="container">
<div class="control-panel">
<h2>选择比赛</h2>
<div class="tournament-info">
<select v-model="selectedTournamentId" @change="handleTournamentChange">
<option value="">-- 请选择比赛 --</option>
<option v-for="tournament in tournaments" :key="tournament.id" :value="tournament.id">
{{ tournament.name }}
</option>
</select>
<div v-if="selectedTournamentId">
<strong>已选择:</strong> {{ selectedTournamentName }}
<div>
<strong>当前比赛:</strong> {{ tournament.name || '加载中...' }}
</div>
</div>
<h2>参赛者列表</h2>
<div id="player-list">
<div v-if="!selectedTournamentId" class="loading">请先选择比赛</div>
<div v-else-if="loading" class="loading">加载中...</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-else>
<p v-if="participants.length === 0">暂无参赛者</p>
<template v-else>
<template v-else>
<p> {{ participants.length }} 位参赛者</p>
<ul>
<li v-for="participant in participants" :key="participant.id">
@ -45,42 +37,15 @@
</div>
<div class="bracket-container" id="bracketContainer">
<div class="bracket" id="bracket">
<template v-for="(roundMatches, rIdx) in roundsMatches" :key="rIdx">
<div class="round">
<div class="round-title"> {{ rIdx + 1 }} </div>
<template v-for="match in roundMatches" :key="match.id">
<div class="match" :id="'match-' + match.id">
<!-- 参赛者1 -->
<div class="participant" :class="{ winner: match.winner && match.participant1 && match.winner.id === match.participant1.id }">
{{ match.participant1 ? match.participant1.name : '轮空' }}
<input type="number" min="0" class="score-input"
v-model.number="match.score1"
:disabled="match.decided || !match.participant1 || !match.participant2"
:placeholder="match.participant1 ? (match.participant1.win || '0') : '0'"
>
</div>
<!-- 参赛者2 -->
<div class="participant" :class="{ winner: match.winner && match.participant2 && match.winner.id === match.participant2.id }">
{{ match.participant2 ? match.participant2.name : '轮空' }}
<input type="number" min="0" class="score-input"
v-model.number="match.score2"
:disabled="match.decided || !match.participant1 || !match.participant2"
:placeholder="match.participant2 ? (match.participant2.win || '0') : '0'"
>
</div>
<!-- 确认比分按钮 -->
<button class="score-btn"
:disabled="match.decided || !match.participant1 || !match.participant2"
@click="confirmScore(match)">
确认比分
</button>
</div>
</template>
</div>
</template>
<div class="zoom-controls">
<button class="zoom-btn" @click="zoomIn" title="放大 (+)">+</button>
<button class="zoom-btn" @click="zoomOut" title="缩小 (-)">-</button>
<button class="zoom-btn" @click="resetZoom" title="重置视图 (0)"></button>
</div>
<svg class="bracket-lines" id="bracketLines"></svg>
<div class="interaction-hint" v-if="tournament.matches.length > 0">
<span>💡 拖拽移动 | 滚轮缩放 | 快捷键: +放大 -缩小 0重置</span>
</div>
<svg id="d3-bracket" class="d3-bracket"></svg>
</div>
</div>
</div>
@ -89,6 +54,7 @@
<script setup>
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
import * as d3 from 'd3';
const props = defineProps({
tournamentId: {
@ -97,13 +63,14 @@ const props = defineProps({
}
});
const tournaments = ref([]);
const selectedTournamentId = ref('');
const selectedTournamentName = ref('');
const participants = ref([]);
const loading = ref(false);
const finalRanking = ref(null);
// Store zoom behavior for external control
let zoomBehavior = null;
let svgSelection = null;
const tournament = ref({
id: props.tournamentId,
name: '',
@ -113,7 +80,7 @@ const tournament = ref({
});
const canGenerateBracket = computed(() => {
return selectedTournamentId.value && participants.value.length >= 2;
return participants.value.length >= 2;
});
const roundsMatches = computed(() => {
@ -125,45 +92,62 @@ const roundsMatches = computed(() => {
});
onMounted(() => {
fetchTournamentList();
loadTournamentData();
// Add keyboard shortcuts for zoom control
const handleKeydown = (event) => {
if (event.target.tagName === 'INPUT') return; // Don't trigger when typing in inputs
switch(event.key) {
case '+':
case '=':
event.preventDefault();
zoomIn();
break;
case '-':
event.preventDefault();
zoomOut();
break;
case '0':
event.preventDefault();
resetZoom();
break;
}
};
document.addEventListener('keydown', handleKeydown);
// Store cleanup function
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
const svg = d3.select('#d3-bracket');
if (svg.node()) svg.selectAll('*').remove();
});
});
const fetchTournamentList = async () => {
const loadTournamentData = async () => {
if (!props.tournamentId) return;
loading.value = true;
tournament.value.id = props.tournamentId;
try {
const data = await getTournamentList();
tournaments.value = data;
} catch (err) {
console.error('获取比赛列表失败:', err);
}
};
const handleTournamentChange = async () => {
if (!selectedTournamentId.value) {
selectedTournamentName.value = '';
participants.value = [];
tournament.value.id = null;
tournament.value.name = '';
tournament.value.participants = [];
return;
}
const selectedTournament = tournaments.value.find(t => t.id === selectedTournamentId.value);
if (selectedTournament) {
selectedTournamentName.value = selectedTournament.name;
tournament.value.id = selectedTournamentId.value;
tournament.value.name = selectedTournament.name;
loading.value = true;
try {
const data = await getSignUpResultList();
const filtered = data.filter(item => item.tournament_id == selectedTournamentId.value);
participants.value = filtered.map(item => ({ id: item.id, name: item.sign_name }));
tournament.value.participants = participants.value;
} catch (err) {
console.error('获取参赛者失败:', err);
} finally {
loading.value = false;
// Get tournament list to find the tournament name
const tournaments = await getTournamentList();
const selectedTournament = tournaments.find(t => t.id === props.tournamentId);
if (selectedTournament) {
tournament.value.name = selectedTournament.name;
}
// Get participants for this tournament
const data = await getSignUpResultList();
const filtered = data.filter(item => item.tournament_id == props.tournamentId);
participants.value = filtered.map(item => ({ id: item.id, name: item.sign_name }));
tournament.value.participants = participants.value;
} catch (err) {
console.error('获取比赛数据失败:', err);
} finally {
loading.value = false;
}
};
@ -171,7 +155,7 @@ const generateBracket = () => {
if (!canGenerateBracket.value) return;
finalRanking.value = null;
generateSingleEliminationBracket();
nextTick(() => drawConnections());
nextTick(() => drawD3Bracket());
};
const generateSingleEliminationBracket = async () => {
@ -253,52 +237,262 @@ const generateSingleEliminationBracket = async () => {
}
};
const drawConnections = () => {
try {
const bracketLinesSVG = document.getElementById('bracketLines');
const bracketDiv = document.getElementById('bracket');
if (!bracketDiv || !bracketLinesSVG) return;
bracketLinesSVG.innerHTML = '';
const bracketRect = bracketDiv.getBoundingClientRect();
const matchElements = {};
document.querySelectorAll('.match').forEach(el => {
const id = el.id.replace('match-', '');
matchElements[id] = el;
const drawD3Bracket = () => {
const svg = d3.select('#d3-bracket');
svg.selectAll('*').remove(); // Clear previous content
if (!tournament.value.matches.length) return;
const margin = { top: 40, right: 40, bottom: 40, left: 40 };
const matchWidth = 180;
const matchHeight = 120;
const roundGap = 50;
const matchGap = 20;
const rounds = tournament.value.rounds;
const maxMatchesInRound = Math.pow(2, rounds - 1);
const totalWidth = rounds * (matchWidth + roundGap) + margin.left + margin.right;
const totalHeight = maxMatchesInRound * (matchHeight + matchGap) + margin.top + margin.bottom;
// Get container dimensions for proper sizing
const container = document.getElementById('bracketContainer');
const containerWidth = container.clientWidth - 30; // Account for padding
const containerHeight = container.clientHeight - 30;
svg
.attr('width', containerWidth)
.attr('height', containerHeight)
.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`)
.attr('preserveAspectRatio', 'xMidYMid meet');
// Create zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 3]) // Allow zoom from 10% to 300%
.on('zoom', (event) => {
g.attr('transform', `translate(${margin.left},${margin.top}) ${event.transform}`);
});
tournament.value.matches.forEach(match => {
if (match.round === tournament.value.rounds) return;
if (!match.decided) return;
const nextRound = match.round + 1;
const nextMatchNumber = Math.floor((match.matchNumber - 1) / 2) + 1;
const nextMatchId = `${nextRound}-${nextMatchNumber}`;
const nextMatchEl = matchElements[nextMatchId];
const currentMatchEl = matchElements[match.id];
if (!nextMatchEl || !currentMatchEl) return;
const winnerIndex = (match.winner.id === (match.participant1 && match.participant1.id)) ? 1 : 2;
const curRect = currentMatchEl.getBoundingClientRect();
const nextRect = nextMatchEl.getBoundingClientRect();
const startX = curRect.right - bracketRect.left;
const startY = curRect.top - bracketRect.top + (winnerIndex === 1 ? 20 : 50);
const endX = nextRect.left - bracketRect.left;
const endY = nextRect.top - bracketRect.top + (match.matchNumber % 2 === 1 ? 20 : 50);
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('stroke', '#666');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-width', '2');
const midX = (startX + endX) / 2;
const d = `M${startX},${startY} L${midX},${startY} L${midX},${endY} L${endX},${endY}`;
path.setAttribute('d', d);
bracketLinesSVG.appendChild(path);
// Store references for external control
zoomBehavior = zoom;
svgSelection = svg;
// Apply zoom to SVG
svg.call(zoom);
// Add cursor styles for pan/zoom
svg.style('cursor', 'grab')
.on('mousedown', () => svg.style('cursor', 'grabbing'))
.on('mouseup', () => svg.style('cursor', 'grab'));
// Create main group for content
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Draw matches for each round
for (let round = 1; round <= rounds; round++) {
const roundMatches = tournament.value.matches.filter(m => m.round === round);
const matchesInRound = roundMatches.length;
const roundX = (round - 1) * (matchWidth + roundGap);
const roundStartY = (maxMatchesInRound * (matchHeight + matchGap) - matchesInRound * (matchHeight + matchGap)) / 2;
// Round title
g.append('text')
.attr('x', roundX + matchWidth / 2)
.attr('y', -10)
.attr('text-anchor', 'middle')
.attr('class', 'round-title')
.style('font-weight', 'bold')
.style('font-size', '14px')
.text(`${round}`);
roundMatches.forEach((match, idx) => {
const matchY = roundStartY + idx * (matchHeight + matchGap);
// Match container
const matchGroup = g.append('g')
.attr('class', 'match-group')
.attr('data-match-id', match.id);
// Match background
matchGroup.append('rect')
.attr('x', roundX)
.attr('y', matchY)
.attr('width', matchWidth)
.attr('height', matchHeight)
.attr('class', 'match-bg')
.style('fill', '#fafafa')
.style('stroke', '#ddd')
.style('stroke-width', 1)
.style('rx', 6);
// Participant 1
const p1Group = matchGroup.append('g')
.attr('class', 'participant-group')
.attr('transform', `translate(${roundX + 10}, ${matchY + 20})`);
p1Group.append('rect')
.attr('width', matchWidth - 20)
.attr('height', 30)
.style('fill', match.winner && match.participant1 && match.winner.id === match.participant1.id ? '#e8f5e8' : '#fff')
.style('stroke', '#ccc')
.style('rx', 3);
p1Group.append('text')
.attr('x', 8)
.attr('y', 20)
.style('font-size', '12px')
.style('font-weight', match.winner && match.participant1 && match.winner.id === match.participant1.id ? 'bold' : 'normal')
.style('fill', match.winner && match.participant1 && match.winner.id === match.participant1.id ? '#2a7f2a' : '#333')
.text(match.participant1 ? match.participant1.name : '轮空');
// Score input for participant 1
if (match.participant1 && match.participant2 && !match.decided) {
const scoreInput1 = p1Group.append('foreignObject')
.attr('x', matchWidth - 60)
.attr('y', 5)
.attr('width', 40)
.attr('height', 20);
scoreInput1.append('xhtml:input')
.attr('type', 'number')
.attr('min', '0')
.style('width', '35px')
.style('height', '18px')
.style('font-size', '11px')
.style('text-align', 'center')
.style('border', '1px solid #aaa')
.style('border-radius', '3px')
.property('value', match.score1 || '')
.on('input', function() {
match.score1 = +this.value;
});
} else if (match.decided) {
p1Group.append('text')
.attr('x', matchWidth - 35)
.attr('y', 20)
.style('font-size', '12px')
.style('font-weight', 'bold')
.style('text-anchor', 'middle')
.text(match.score1 || 0);
}
// Participant 2
const p2Group = matchGroup.append('g')
.attr('class', 'participant-group')
.attr('transform', `translate(${roundX + 10}, ${matchY + 55})`);
p2Group.append('rect')
.attr('width', matchWidth - 20)
.attr('height', 30)
.style('fill', match.winner && match.participant2 && match.winner.id === match.participant2.id ? '#e8f5e8' : '#fff')
.style('stroke', '#ccc')
.style('rx', 3);
p2Group.append('text')
.attr('x', 8)
.attr('y', 20)
.style('font-size', '12px')
.style('font-weight', match.winner && match.participant2 && match.winner.id === match.participant2.id ? 'bold' : 'normal')
.style('fill', match.winner && match.participant2 && match.winner.id === match.participant2.id ? '#2a7f2a' : '#333')
.text(match.participant2 ? match.participant2.name : '轮空');
// Score input for participant 2
if (match.participant1 && match.participant2 && !match.decided) {
const scoreInput2 = p2Group.append('foreignObject')
.attr('x', matchWidth - 60)
.attr('y', 5)
.attr('width', 40)
.attr('height', 20);
scoreInput2.append('xhtml:input')
.attr('type', 'number')
.attr('min', '0')
.style('width', '35px')
.style('height', '18px')
.style('font-size', '11px')
.style('text-align', 'center')
.style('border', '1px solid #aaa')
.style('border-radius', '3px')
.property('value', match.score2 || '')
.on('input', function() {
match.score2 = +this.value;
});
} else if (match.decided) {
p2Group.append('text')
.attr('x', matchWidth - 35)
.attr('y', 20)
.style('font-size', '12px')
.style('font-weight', 'bold')
.style('text-anchor', 'middle')
.text(match.score2 || 0);
}
// Confirm score button
if (match.participant1 && match.participant2 && !match.decided) {
const buttonGroup = matchGroup.append('g')
.attr('transform', `translate(${roundX + 10}, ${matchY + 90})`);
const button = buttonGroup.append('rect')
.attr('width', matchWidth - 20)
.attr('height', 25)
.style('fill', '#007acc')
.style('stroke', 'none')
.style('rx', 4)
.style('cursor', 'pointer')
.on('click', () => confirmScore(match));
buttonGroup.append('text')
.attr('x', (matchWidth - 20) / 2)
.attr('y', 17)
.style('fill', 'white')
.style('font-size', '12px')
.style('font-weight', 'bold')
.style('text-anchor', 'middle')
.style('pointer-events', 'none')
.text('确认比分');
}
// Store match position for connection drawing
match._x = roundX + matchWidth;
match._y = matchY + matchHeight / 2;
});
} catch (e) {
console.warn('drawConnections error:', e);
}
// Draw connections between matches
drawD3Connections(g, matchWidth, matchHeight, roundGap, matchGap);
};
onUnmounted(() => {
const bracketLinesSVG = document.getElementById('bracketLines');
if (bracketLinesSVG) bracketLinesSVG.innerHTML = '';
});
const drawD3Connections = (g, matchWidth, matchHeight, roundGap, matchGap) => {
const rounds = tournament.value.rounds;
for (let round = 1; round < rounds; round++) {
const currentRoundMatches = tournament.value.matches.filter(m => m.round === round && m.decided);
currentRoundMatches.forEach(match => {
const nextRound = round + 1;
const nextMatchNumber = Math.floor((match.matchNumber - 1) / 2) + 1;
const nextMatch = tournament.value.matches.find(m => m.round === nextRound && m.matchNumber === nextMatchNumber);
if (nextMatch && match.winner) {
const startX = match._x;
const startY = match._y;
const endX = nextMatch._x - matchWidth;
const endY = nextMatch._y;
const midX = startX + roundGap / 2;
// Draw connection line
g.append('path')
.attr('d', `M${startX},${startY} L${midX},${startY} L${midX},${endY} L${endX},${endY}`)
.style('stroke', '#4CAF50')
.style('stroke-width', 2)
.style('fill', 'none')
.style('opacity', 0.8);
}
});
}
};
const emit = defineEmits(['refreshPlayers']);
@ -318,8 +512,8 @@ async function confirmScore(match) {
match.winner = s1 > s2 ? match.participant1 : match.participant2;
match.decided = true;
updateNextRound(match);
// 线
nextTick(() => drawConnections());
// D3
nextTick(() => drawD3Bracket());
if (match.round === tournament.value.rounds) {
calculateFinalRanking();
}
@ -357,8 +551,8 @@ async function confirmScore(match) {
};
await updateSignUpResult(p1.id, p1Data);
await updateSignUpResult(p2.id, p2Data);
//
await handleTournamentChange();
//
await loadTournamentData();
// participants tournament.value.participants
participants.value = [...participants.value];
tournament.value.participants = [...participants.value];
@ -464,10 +658,43 @@ const calculateFinalRanking = () => {
};
};
// 线
// Zoom control functions
const zoomIn = () => {
if (svgSelection && zoomBehavior) {
svgSelection.transition().duration(300).call(
zoomBehavior.scaleBy, 1.5
);
}
};
const zoomOut = () => {
if (svgSelection && zoomBehavior) {
svgSelection.transition().duration(300).call(
zoomBehavior.scaleBy, 1 / 1.5
);
}
};
const resetZoom = () => {
if (svgSelection && zoomBehavior) {
svgSelection.transition().duration(500).call(
zoomBehavior.transform,
d3.zoomIdentity
);
}
};
// D3
watch(roundsMatches, () => {
nextTick(() => drawConnections());
nextTick(() => drawD3Bracket());
});
// tournamentId
watch(() => props.tournamentId, (newId) => {
if (newId) {
loadTournamentData();
}
}, { immediate: false });
</script>
<style>
@ -530,7 +757,7 @@ watch(roundsMatches, () => {
.tournament-bracket-root .bracket-container {
flex: 3;
position: relative;
overflow-x: auto;
overflow: auto;
background: #fff;
padding: 15px;
border-radius: 6px;
@ -541,76 +768,73 @@ watch(roundsMatches, () => {
align-items: flex-start;
}
.tournament-bracket-root .bracket {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 220px;
gap: 40px 10px;
position: relative;
padding-bottom: 50px;
min-width: 600px;
min-height: 350px;
}
.tournament-bracket-root .round {
display: grid;
grid-auto-rows: 70px;
gap: 20px;
}
.tournament-bracket-root .round-title {
text-align: center;
font-weight: bold;
margin-bottom: 15px;
}
.tournament-bracket-root .match {
background: #fafafa;
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px 12px;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
position: relative;
font-size: 14px;
}
.tournament-bracket-root .participant {
.tournament-bracket-root .zoom-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
justify-content: space-between;
margin-bottom: 6px;
cursor: default;
user-select: none;
flex-direction: column;
gap: 5px;
z-index: 10;
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
padding: 5px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.tournament-bracket-root .participant.winner {
font-weight: bold;
color: #2a7f2a;
}
.tournament-bracket-root .score-input {
width: 40px;
font-size: 14px;
padding: 2px 4px;
margin-left: 6px;
border: 1px solid #aaa;
border-radius: 3px;
}
.tournament-bracket-root .score-btn {
margin-top: 4px;
width: 100%;
.tournament-bracket-root .zoom-btn {
width: 32px;
height: 32px;
background: #007acc;
border: none;
color: white;
font-weight: bold;
padding: 6px 0;
border-radius: 4px;
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.tournament-bracket-root .score-btn:disabled {
background: #aaa;
cursor: not-allowed;
.tournament-bracket-root .zoom-btn:hover {
background: #005a9e;
transform: scale(1.05);
}
.tournament-bracket-root .zoom-btn:active {
transform: scale(0.95);
}
.tournament-bracket-root .interaction-hint {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 5;
user-select: none;
opacity: 0.8;
transition: opacity 0.3s ease;
}
.tournament-bracket-root .interaction-hint:hover {
opacity: 1;
}
.tournament-bracket-root .d3-bracket {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.tournament-bracket-root .d3-bracket:active {
cursor: grabbing;
}
.tournament-bracket-root #finalRanking {
@ -621,21 +845,6 @@ watch(roundsMatches, () => {
border-radius: 6px;
}
.tournament-bracket-root svg.bracket-lines {
position: absolute;
top: 40px;
left: 0;
pointer-events: none;
overflow: visible;
height: 100%;
width: 100%;
}
.tournament-bracket-root svg.bracket-lines path {
stroke: #666;
fill: none;
stroke-width: 2;
}
.tournament-bracket-root .tournament-info {
margin-bottom: 15px;
padding: 10px;
@ -647,12 +856,4 @@ watch(roundsMatches, () => {
color: #666;
font-style: italic;
}
.tournament-bracket-root select {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border-radius: 4px;
border: 1px solid #ccc;
}
</style>

View File

@ -57,37 +57,37 @@ const routes = [
component: () => import('@/views/index/ConfigEditor.vue'),
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
},
// {
// path: 'competition',
// name: 'Competition',
// component: () => import('@/views/index/Competition.vue'),
// // meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-competitor'] }
// meta: { requiresAuth: true}
// },
// {
// path: 'competition/add',
// name: 'AddCompetition',
// component: () => import('@/views/index/AddContestant.vue'),
// meta: { requiresAuth: true }
// },
// {
// path: 'competition/detail',
// name: 'CompetitionDetail',
// component: () => import('@/views/index/CompetitionDetail.vue'),
// meta: { requiresAuth: true }
// },
// {
// path: 'competition/signup',
// name: 'CompetitionSignUp',
// component: () => import('@/views/index/CompetitionSignUp.vue'),
// meta: { requiresAuth: true }
// },
{
path: 'competition',
name: 'Competition',
component: () => import('@/views/competition/Competition.vue'),
component: () => import('@/views/index/Competition.vue'),
// meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-competitor'] }
meta: { requiresAuth: true}
},
{
path: 'competition/add',
name: 'AddCompetition',
component: () => import('@/views/index/AddContestant.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/detail',
name: 'CompetitionDetail',
component: () => import('@/views/index/CompetitionDetail.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/signup',
name: 'CompetitionSignUp',
component: () => import('@/views/index/CompetitionSignUp.vue'),
meta: { requiresAuth: true }
},
// {
// path: 'competition',
// name: 'Competition',
// component: () => import('@/views/competition/Competition.vue'),
// meta: { requiresAuth: true}
// },
{
path: 'editors-maps',
name: 'EditorsMaps',

View File

@ -1,11 +1,271 @@
<script setup lang="ts">
<script setup>
import { ref, onMounted } from 'vue';
import TournamentBracket from '@/components/TournamentBracket.vue';
//
const activeTab = ref('upcoming'); // upcoming, ongoing, completed
const tournaments = ref([]);
const loading = ref(false);
// ID
const selectedTournamentId = ref(null);
//
onMounted(() => {
fetchTournaments();
});
//
const fetchTournaments = async () => {
loading.value = true;
try {
// API
//
tournaments.value = [
{ id: 1, name: '2023年夏季赛', status: 'completed', startDate: '2023-06-01', endDate: '2023-08-30' },
{ id: 2, name: '2023年秋季赛', status: 'ongoing', startDate: '2023-09-01', endDate: '2023-11-30' },
{ id: 3, name: '2024年春季赛', status: 'upcoming', startDate: '2024-03-01', endDate: '2024-05-30' }
];
} catch (error) {
console.error('获取比赛列表失败:', error);
} finally {
loading.value = false;
}
};
//
const selectTournament = (tournamentId) => {
selectedTournamentId.value = tournamentId;
};
//
const refreshParticipants = () => {
//
console.log('刷新参赛者列表');
};
//
const filteredTournaments = () => {
return tournaments.value.filter(tournament => {
if (activeTab.value === 'upcoming') return tournament.status === 'upcoming';
if (activeTab.value === 'ongoing') return tournament.status === 'ongoing';
if (activeTab.value === 'completed') return tournament.status === 'completed';
return true;
});
};
</script>
<template>
<div class="competition-container">
<h1 class="page-title">比赛中心</h1>
<!-- 比赛类型选项卡 -->
<div class="tabs">
<div
class="tab"
:class="{ active: activeTab === 'upcoming' }"
@click="activeTab = 'upcoming'"
>
即将开始
</div>
<div
class="tab"
:class="{ active: activeTab === 'ongoing' }"
@click="activeTab = 'ongoing'"
>
正在进行
</div>
<div
class="tab"
:class="{ active: activeTab === 'completed' }"
@click="activeTab = 'completed'"
>
已结束
</div>
</div>
<!-- 比赛列表 -->
<div class="tournament-list">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="filteredTournaments().length === 0" class="no-data">
暂无{{ activeTab === 'upcoming' ? '即将开始' : activeTab === 'ongoing' ? '正在进行' : '已结束' }}的比赛
</div>
<div
v-else
v-for="tournament in filteredTournaments()"
:key="tournament.id"
class="tournament-card"
:class="{ active: selectedTournamentId === tournament.id }"
@click="selectTournament(tournament.id)"
>
<div class="tournament-header">
<h3>{{ tournament.name }}</h3>
<span class="status-badge" :class="tournament.status">
{{ tournament.status === 'upcoming' ? '即将开始' :
tournament.status === 'ongoing' ? '正在进行' : '已结束' }}
</span>
</div>
<div class="tournament-dates">
<span>开始日期: {{ tournament.startDate }}</span>
<span>结束日期: {{ tournament.endDate }}</span>
</div>
</div>
</div>
<!-- 比赛详情 -->
<div v-if="selectedTournamentId" class="tournament-detail">
<TournamentBracket
:tournamentId="selectedTournamentId"
@refreshPlayers="refreshParticipants"
/>
</div>
<div v-else class="select-prompt">
请从左侧选择一个比赛查看详情
</div>
</div>
</template>
<style scoped>
.competition-container {
display: flex;
flex-direction: column;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-title {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.tab {
padding: 10px 20px;
cursor: pointer;
font-weight: 500;
color: #666;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.tab:hover {
color: #007acc;
}
.tab.active {
color: #007acc;
border-bottom: 2px solid #007acc;
}
.tournament-list {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.tournament-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.tournament-card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.tournament-card.active {
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
}
.tournament-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.tournament-header h3 {
margin: 0;
font-size: 18px;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge.upcoming {
background-color: #e6f7ff;
color: #1890ff;
}
.status-badge.ongoing {
background-color: #f6ffed;
color: #52c41a;
}
.status-badge.completed {
background-color: #f5f5f5;
color: #666;
}
.tournament-dates {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #666;
}
.loading, .no-data, .select-prompt {
padding: 20px;
text-align: center;
color: #666;
background: #f9f9f9;
border-radius: 8px;
font-style: italic;
}
.tournament-detail {
margin-top: 20px;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
@media (min-width: 768px) {
.competition-container {
flex-direction: row;
flex-wrap: wrap;
}
.page-title, .tabs {
width: 100%;
}
.tournament-list {
width: 30%;
margin-right: 2%;
}
.tournament-detail, .select-prompt {
width: 68%;
}
}
</style>

View File

@ -75,7 +75,10 @@
v-if="competition.status === 'finish'"
:tournament-id="parseInt(route.query.id)"
/>
<DoubleEliminationBracket v-if="competition.format === '双败淘汰' && competition.status === 'starting'" />
<DoubleEliminationBracket
v-if="competition.format === '双败淘汰' && competition.status === 'starting'"
:tournament-id="parseInt(route.query.id)"
/>
<tournament-bracket
v-else-if="competition.status === 'starting'"
:tournament-id="parseInt(route.query.id)"

View File

@ -150,34 +150,34 @@
<!-- 右侧边栏 -->
<div class="sidebar">
<!-- &lt;!&ndash; 赛事信息 &ndash;&gt;-->
<!-- <div class="section-card">-->
<!-- <div class="section-header">-->
<!-- <h3>-->
<!-- <i class="fas fa-trophy"></i>-->
<!-- 最新赛事-->
<!-- </h3>-->
<!-- <router-link to="/competition" class="more-link">查看全部</router-link>-->
<!-- </div>-->
<!-- <div class="competitions-list">-->
<!-- <div-->
<!-- v-for="competition in latestCompetitions"-->
<!-- :key="competition.id"-->
<!-- class="competition-item"-->
<!-- @click="viewCompetition(competition.id)"-->
<!-- >-->
<!-- <div class="competition-status" :class="competition.status">-->
<!-- {{ getStatusText(competition.status) }}-->
<!-- </div>-->
<!-- <h4>{{ competition.name }}</h4>-->
<!-- <div class="competition-time">-->
<!-- <i class="fas fa-calendar"></i>-->
<!-- {{ formatDate(competition.start_time) }}-->
<!-- </div>-->
<!-- <p>主办方{{ competition.organizer }}</p>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- 赛事信息 -->
<div class="section-card">
<div class="section-header">
<h3>
<i class="fas fa-trophy"></i>
最新赛事
</h3>
<router-link to="/competition" class="more-link">查看全部</router-link>
</div>
<div class="competitions-list">
<div
v-for="competition in latestCompetitions"
:key="competition.id"
class="competition-item"
@click="viewCompetition(competition.id)"
>
<div class="competition-status" :class="competition.status">
{{ getStatusText(competition.status) }}
</div>
<h4>{{ competition.name }}</h4>
<div class="competition-time">
<i class="fas fa-calendar"></i>
{{ formatDate(competition.start_time) }}
</div>
<p>主办方{{ competition.organizer }}</p>
</div>
</div>
</div>
<!-- 办事大厅 -->
<div class="section-card">