Compare commits

..

47 Commits

Author SHA1 Message Date
zyb
6d6a6cd03a Merge pull request 'feature/login-screen' (#14) from feature/login-screen into master
Reviewed-on: #14
2025-12-10 21:15:20 +08:00
zyb
95bf8141f0 Merge branch 'master' into feature/login-screen 2025-12-10 21:14:16 +08:00
WIN-5KTJHN9GRFL\LENOVO
c8905622e2 红色警戒3工具平台 2025-12-10 21:01:32 +08:00
WIN-5KTJHN9GRFL\LENOVO
c065514679 版本信息页面 2025-11-20 13:25:58 +08:00
WIN-5KTJHN9GRFL\LENOVO
ef4272c952 版本信息页面 2025-11-12 09:56:06 +08:00
WIN-5KTJHN9GRFL\LENOVO
2af7d67b26 版本信息页面 2025-11-12 09:53:04 +08:00
WIN-5KTJHN9GRFL\LENOVO
cd220e46b4 版本信息页面 2025-11-12 09:07:34 +08:00
WIN-5KTJHN9GRFL\LENOVO
e67cb1c4fb 版本信息页面 2025-11-12 08:54:17 +08:00
WIN-5KTJHN9GRFL\LENOVO
d7242a9e9f 首页未登录点击没有任何界面,直接跳转到登陆页面 2025-10-30 11:32:42 +08:00
WIN-5KTJHN9GRFL\LENOVO
6f8f58feab 首页api 2025-10-26 14:16:14 +08:00
Kunagisa
1874ea7180 修改api 2025-10-25 16:39:42 +08:00
Kunagisa
d6432ceb69 单败重构 2025-08-05 21:34:19 +08:00
Kunagisa
a72c6fcd2c 单败重构 2025-08-05 17:10:06 +08:00
Kunagisa
afd5323ade 单败重构 2025-08-03 13:53:54 +08:00
Kunagisa
f5bf2f01f5 单败重构 2025-08-01 20:37:02 +08:00
Kunagisa
292d913305 单败重构 2025-08-01 02:05:23 +08:00
Kunagisa
5aabaddc31 单败重构 2025-08-01 01:29:18 +08:00
Kunagisa
47f97cd291 重构赛事信息(修改树状图前) 2025-07-31 00:59:05 +08:00
Kunagisa
605d60ec7e 重构赛事信息(修改树状图前) 2025-07-31 00:17:22 +08:00
Kunagisa
0274eb5407 重构赛事的树状图 2025-07-29 16:38:07 +08:00
Kunagisa
2762a8b5f1 重构赛事的树状图 2025-07-29 16:37:03 +08:00
Kunagisa
1977b791e5 重构赛事的树状图 2025-07-29 16:15:54 +08:00
Kunagisa
874e2fc4dc 赛事定到@/views/competition/Competition.vue了 2025-07-25 23:01:43 +08:00
Kunagisa
1175524448 重置密码的60s冷却🐱🐱 2025-07-25 22:55:01 +08:00
Kunagisa
073321ed7a 重置密码的60s冷却🐱🐱 2025-07-25 22:37:05 +08:00
Kunagisa
72c6bda35a 重置密码的60s冷却🐱🐱 2025-07-25 22:36:56 +08:00
Kunagisa
4828d0bcd6 重置密码的60s冷却🐱🐱 2025-07-25 19:31:28 +08:00
Kunagisa
4c15c42ebc 重置密码的60s冷却🐱🐱 2025-07-25 00:58:00 +08:00
Kunagisa
0537bdb86e 重置密码的60s冷却🐱🐱 2025-07-24 21:53:02 +08:00
Kunagisa
79f1daf525 首页🐱🐱 2025-07-24 21:19:29 +08:00
Kunagisa
e4b562ed86 首页🐱🐱 2025-07-24 21:19:23 +08:00
Kunagisa
b588915cb2 首页🐱🐱 2025-07-24 21:17:17 +08:00
Kunagisa
2a85a09bc8 修改备案号🐱🐱 2025-07-22 13:26:42 +08:00
Kunagisa
c6632b124c 修改备案号🐱🐱 2025-07-22 10:16:50 +08:00
Kunagisa
bf3b49e72b 修改密码之后扬了登陆 2025-07-19 14:40:27 +08:00
Kunagisa
d44a793019 修改密码之后扬了登陆 2025-07-19 14:23:20 +08:00
Kunagisa
c3ccf8d73d Merge remote-tracking branch 'origin/feature/login-screen' into feature/login-screen 2025-07-19 13:40:04 +08:00
Kunagisa
04bf94df0c 添加了Config.xml 编辑器 2025-07-19 13:39:02 +08:00
zyb
b7eb16fb2f Merge pull request 'feature/login-screen' (#11) from feature/login-screen into master
Reviewed-on: #11
2025-07-19 08:17:11 +08:00
zyb
278d028e36 更新 README.md 2025-07-19 08:16:48 +08:00
zyb
15a2981a15 Merge branch 'master' into feature/login-screen 2025-07-19 08:14:43 +08:00
zyb
176aa42648 更新 README.md 2025-07-19 08:14:24 +08:00
Kunagisa
247cfbd0a9 忘记密码,确信(,另外加了个在编译的时候会去掉log,应该能编译成功吧 2025-07-18 01:43:37 +08:00
Kunagisa
cf0713bb80 忘记密码,确信(,另外加了个在编译的时候会去掉log,应该能编译成功吧 2025-07-18 01:29:42 +08:00
Kunagisa
ba077c8e9c 忘记密码,确信(,另外加了个在编译的时候会去掉log,应该能编译成功吧 2025-07-18 01:29:24 +08:00
Kunagisa
6fcc13b31f Merge remote-tracking branch 'origin/feature/login-screen' into feature/login-screen 2025-07-13 21:40:58 +08:00
Kunagisa
46fa0a7668 登录后修改密码🐱 2025-07-13 21:40:32 +08:00
62 changed files with 20511 additions and 15024 deletions

View File

@@ -1,11 +1,21 @@
#DCF
# 中台前端功能设计文档
0.未来功能
0.未来功能 <br>
添加git可视化分析内容
- 可视化分析:
- 1.根据git链接一键下载按钮、一键分析文件按钮。
- 2.临时修改git文件内容、提交鉴权
- 3.sooon
<br>
添加mod教程功能
- 根据mod教程提供对应的源码参考
- 支持上传和插入自定义代码片段
- 有一个教程列表,可以按照更新时间/浏览量来排序教程顺序
- 用户可以收藏教程
<br>
添加赛事新赛制
- 双败
- 积分
## 一、核心功能概述
中台前端主要承担展示后端内容、渲染界面的职责,当前提供以下功能模块:

View File

@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<title>红色警戒3数据分析中心</title>
<title>红色警戒3工具平台</title>
</head>
<body>
<div id="app"></div>

440
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +1,6 @@
{
"name": "untitled2",
"version": "0.0.0",
"version": "V.1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -54,6 +54,7 @@
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.1.tgz",
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -839,6 +840,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -930,6 +932,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",
@@ -986,6 +996,377 @@
"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==",
"peer": true,
"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",
@@ -1043,6 +1424,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",
@@ -1440,6 +1829,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",
@@ -1450,6 +1850,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",
@@ -1641,6 +2049,18 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/marked": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
"integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1893,11 +2313,17 @@
"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",
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
"dev": true,
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -1944,11 +2370,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",
@@ -2156,6 +2592,7 @@
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -2313,6 +2750,7 @@
"version": "3.5.13",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",

View File

@@ -1,52 +1,64 @@
{
"hash": "3498b3cb",
"configHash": "9c7a641a",
"lockfileHash": "c997fc3c",
"browserHash": "38ceb684",
"hash": "f6d37eef",
"configHash": "4eee8ee7",
"lockfileHash": "17b35e94",
"browserHash": "38da36af",
"optimized": {
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "97de2fb4",
"fileHash": "a5a07628",
"needsInterop": false
},
"d3": {
"src": "../../d3/src/index.js",
"file": "d3.js",
"fileHash": "828cdd5e",
"needsInterop": false
},
"jszip": {
"src": "../../jszip/dist/jszip.min.js",
"file": "jszip.js",
"fileHash": "3cd8a10a",
"fileHash": "c0e06a93",
"needsInterop": true
},
"mitt": {
"src": "../../mitt/dist/mitt.mjs",
"file": "mitt.js",
"fileHash": "8e22dbfe",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "74f3ac3b",
"fileHash": "7a7fc2f0",
"needsInterop": false
},
"vue-router": {
"src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js",
"fileHash": "d2d7abce",
"fileHash": "58c64081",
"needsInterop": false
},
"xlsx": {
"src": "../../xlsx/xlsx.mjs",
"file": "xlsx.js",
"fileHash": "dbbdc859",
"fileHash": "d95fb4c8",
"needsInterop": false
},
"mitt": {
"src": "../../mitt/dist/mitt.mjs",
"file": "mitt.js",
"fileHash": "ac6eb5ab",
"marked": {
"src": "../../marked/lib/marked.esm.js",
"file": "marked.js",
"fileHash": "a5f98500",
"needsInterop": false
}
},
"chunks": {
"chunk-U3LI7FBV": {
"file": "chunk-U3LI7FBV.js"
"chunk-YBGSFZ7G": {
"file": "chunk-YBGSFZ7G.js"
},
"chunk-5FUTL2UF": {
"file": "chunk-5FUTL2UF.js"
"chunk-FM7WUVZV": {
"file": "chunk-FM7WUVZV.js"
}
}
}

2
node_modules/.vite/deps/axios.js generated vendored
View File

@@ -1,6 +1,6 @@
import {
__export
} from "./chunk-5FUTL2UF.js";
} from "./chunk-FM7WUVZV.js";
// node_modules/axios/lib/helpers/bind.js
function bind(fn, thisArg) {

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +0,0 @@
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
export {
__require,
__commonJS,
__export
};

View File

@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
node_modules/.vite/deps/jszip.js generated vendored
View File

@@ -1,7 +1,7 @@
import {
__commonJS,
__require
} from "./chunk-5FUTL2UF.js";
} from "./chunk-FM7WUVZV.js";
// node_modules/jszip/dist/jszip.min.js
var require_jszip_min = __commonJS({

File diff suppressed because one or more lines are too long

2
node_modules/.vite/deps/mitt.js generated vendored
View File

@@ -1,4 +1,4 @@
import "./chunk-5FUTL2UF.js";
import "./chunk-FM7WUVZV.js";
// node_modules/mitt/dist/mitt.mjs
function mitt_default(n) {

View File

@@ -16,8 +16,8 @@ import {
unref,
watch,
watchEffect
} from "./chunk-U3LI7FBV.js";
import "./chunk-5FUTL2UF.js";
} from "./chunk-YBGSFZ7G.js";
import "./chunk-FM7WUVZV.js";
// node_modules/@vue/devtools-api/lib/esm/env.js
function getDevtoolsGlobalHook() {

File diff suppressed because one or more lines are too long

4
node_modules/.vite/deps/vue.js generated vendored
View File

@@ -168,8 +168,8 @@ import {
withMemo,
withModifiers,
withScopeId
} from "./chunk-U3LI7FBV.js";
import "./chunk-5FUTL2UF.js";
} from "./chunk-YBGSFZ7G.js";
import "./chunk-FM7WUVZV.js";
export {
BaseTransition,
BaseTransitionPropsValidators,

2
node_modules/.vite/deps/xlsx.js generated vendored
View File

@@ -1,4 +1,4 @@
import "./chunk-5FUTL2UF.js";
import "./chunk-FM7WUVZV.js";
// node_modules/xlsx/xlsx.mjs
var XLSX = {};

File diff suppressed because one or more lines are too long

View File

@@ -40,14 +40,6 @@ export {};
expose?: (exposed: T) => void,
}
};
type __VLS_NormalizeSlotReturns<S, R = NonNullable<S> extends (...args: any) => infer K ? K : any> = R extends any[] ? {
[K in keyof R]: R[K] extends infer V
? V extends Element ? V
: V extends new (...args: any) => infer R ? ReturnType<__VLS_FunctionalComponent<R>>
: V extends (...args: any) => infer R ? R
: any
: never
} : R;
type __VLS_IsFunction<T, K> = K extends keyof T
? __VLS_IsAny<T[K]> extends false
? unknown extends T[K]
@@ -113,7 +105,7 @@ export {};
index: number,
][];
function __VLS_getSlotParameters<S, D extends S>(slot: S, decl?: D):
__VLS_PickNotAny<NonNullable<D>, (...args: any) => any> extends (...args: infer P) => any ? P : any[];
D extends (...args: infer P) => any ? P : any[];
function __VLS_asFunctionalDirective<T>(dir: T): T extends import('vue').ObjectDirective
? NonNullable<T['created' | 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated' | 'beforeUnmount' | 'unmounted']>
: T extends (...args: any) => any

444
package-lock.json generated
View File

@@ -1,15 +1,17 @@
{
"name": "untitled2",
"version": "0.0.0",
"version": "V.1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "untitled2",
"version": "0.0.0",
"version": "V.1.0.0",
"dependencies": {
"axios": "^1.9.0",
"d3": "^7.9.0",
"jszip": "^3.10.1",
"marked": "^17.0.0",
"process": "^0.11.10",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
@@ -72,6 +74,7 @@
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.1.tgz",
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1488,6 +1491,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -1579,6 +1583,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 +1647,377 @@
"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==",
"peer": true,
"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 +2075,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 +2494,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 +2515,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",
@@ -2304,6 +2714,18 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/marked": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
"integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2556,11 +2978,17 @@
"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",
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
"dev": true,
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -2607,11 +3035,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",
@@ -2819,6 +3257,7 @@
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -2976,6 +3415,7 @@
"version": "3.5.13",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",

View File

@@ -1,6 +1,6 @@
{
"name": "untitled2",
"version": "0.0.0",
"version": "V.1.0.1",
"private": true,
"type": "module",
"scripts": {
@@ -10,7 +10,9 @@
},
"dependencies": {
"axios": "^1.9.0",
"d3": "^7.9.0",
"jszip": "^3.10.1",
"marked": "^17.0.0",
"process": "^0.11.10",
"vue": "^3.5.13",
"vue-router": "^4.5.1",

View File

@@ -0,0 +1,545 @@
<?xml version="1.0" encoding="utf-8"?>
<AssetDeclaration xmlns="uri:ea.com:eala:asset" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xai="uri:ea.com:eala:asset:instance">
<Tags></Tags>
<Includes>
<Include type="all" source="DATA:GlobalData/GlobalDefines.xml" />
<Include
type="all"
source="ART:AUAntiAirShip_D.xml" />
<Include
type="all"
source="ART:AUAntiAirShip_FX.w3x" />
<Include
type="all"
source="ART:AUAntiAirShip_FPZ.w3x" />
<Include
type="all"
source="ART:AUAntiAirShip_SKN.w3x" />
<Include
type="all"
source="ART:FXGradient01.xml" />
<Include
type="all"
source="ART:FXTracer.xml" />
<Include
type="all"
source="ART:FXTracerHeroic.xml" />
<!-- needed for temp laserEndParticleSystemFX? -->
<Include
type="all"
source="ART:SUAntiVehicleVehicleTech3_FX.w3x" />
<Include
type="instance"
source="ART:FXGrid_3.xml" />
<Include
type="instance"
source="ART:FXHarpoonBeam.xml" />
<!-- Base Object -->
<Include
type="instance"
source="DATA:BaseObjects/BaseVehicle.xml" />
</Includes>
<!-- aka The Hydrofoil -->
<GameObject
id="AlliedAntiAirShip"
inheritFrom="BaseVehicle"
SelectPortrait="Portrait_AlliedAntiAirShip"
ButtonImage="Button_AlliedAntiAirShip_on"
Side="Allies"
SubGroupPriority="440"
EditorSorting="UNIT"
HealthBoxHeightOffset="30"
TransportSlotCount="10"
BuildTime="10"
CommandSet="AlliedAntiAirShipCommandSet"
KindOf="SELECTABLE CAN_ATTACK CAN_CAST_REFLECTIONS SCORE VEHICLE SHIP CAN_BE_FAVORITE_UNIT"
RadarPriority="UNIT"
ProductionQueueType="WATERCRAFT"
UnitCategory="VEHICLE"
WeaponCategory="CANNON"
VoicePriority="188"
EditorName="AlliedAntiAirShip"
Description="Desc:AlliedAntiAirShip"
TypeDescription="Type:AlliedAntiAirShip"
UnitIntro="Allied_Hydrofoil_UnitIntro">
<DisplayName
xai:joinAction="Replace" xmlns:xai="uri:ea.com:eala:asset:instance">Name:AlliedAntiAirShip</DisplayName>
<ObjectResourceInfo>
<BuildCost Account="=$ACCOUNT_ORE" Amount="900"/>
</ObjectResourceInfo>
<ArmorSet
Armor="AlliedAntiAirShipArmor"
DamageFX="VehicleDamageFX" />
<LocomotorSet
id="DefaultWaterLocomotorSet"
Locomotor="AlliedAntiAirShipWaterLocomotor"
Condition="NORMAL"
Speed="125.0" />
<LocomotorSet
Locomotor="AlliedAntiAirShipWaterLocomotor_LeavingFactory"
Condition="EXITING_PRODUCTION_STRUCTURE"
Speed="125.0" />
<SkirmishAIInformation
UnitBuilderStandardCombatUnit="true" />
<Draws>
<ScriptedModelDraw
id="ModuleTag_Draw"
OkToChangeModelColor="true"
InitialRecoilSpeed="0.1"
MaxRecoilDistance="0.1"
RecoilDamping="2.0"
RecoilSettleSpeed="3.0"
ExtraPublicBone="FX_Weapon_01 FX_Weapon_02" >
<ModelConditionState
ParseCondStateType="PARSE_DEFAULT"
RetainSubObjects="true">
<Model
Name="AUAntiAirShip_SKN" />
<WeaponFireFXBone
WeaponSlotID="1"
WeaponSlotType="PRIMARY_WEAPON"
BoneName="FX_Weapon_01" />
<WeaponRecoilBone
WeaponSlotID="1"
WeaponSlotType="PRIMARY_WEAPON"
BoneName="FX_Weapon_01" />
<WeaponLaunchBone
WeaponSlotID="1"
WeaponSlotType="PRIMARY_WEAPON"
BoneName="FX_Weapon_01" />
<WeaponFireFXBone
WeaponSlotID="1"
WeaponSlotType="SECONDARY_WEAPON"
BoneName="FX_Weapon_02" />
<WeaponRecoilBone
WeaponSlotID="1"
WeaponSlotType="SECONDARY_WEAPON"
BoneName="FX_Weapon_02" />
<WeaponLaunchBone
WeaponSlotID="1"
WeaponSlotType="SECONDARY_WEAPON"
BoneName="FX_Weapon_02" />
<Turret
TurretNameKey="turret"
TurretPitch="Turret_Pitch"
TurretID="1" />
</ModelConditionState>
<ModelConditionState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="FORMATION_PREVIEW">
<Model
Name="AUAntiAirShip_SKN" />
<Material
ShaderName="FX_FormPreview.fx"
TechniqueName="Default">
<Constants>
<Texture Name="SpecMap">
<Value>FXGradient01</Value>
</Texture>
</Constants>
</Material>
</ModelConditionState>
<ModelConditionState
ParseCondStateType="PARSE_NORMAL"
RetainSubObjects="true"
ConditionsYes="REALLYDAMAGED">
<Model
Name="AUAntiAirShip_SKN" />
<Texture
Original="AUAntiAirShip"
New="AUAntiAirShip_D" />
</ModelConditionState>
<AnimationState
ParseCondStateType="PARSE_DEFAULT">
<Script>
CurDrawableShowSubObjectPermanently("GUN")
CurDrawableHideSubObjectPermanently("BEAM")
</Script>
<ParticleSysBone
BoneName="None"
FXParticleSystemTemplate="SmallShipWakeIdle"
FollowBone="false" />
</AnimationState>
<AnimationState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="MOVING">
<ParticleSysBone
BoneName="NONE"
FXParticleSystemTemplate="AUHydrofoilWaterWake"
FollowBone="false" />
</AnimationState>
<AnimationState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="WEAPONSTATE_TWO">
<Script>
CurDrawableHideSubObjectPermanently("GUN")
CurDrawableShowSubObjectPermanently("BEAM")
</Script>
</AnimationState>
</ScriptedModelDraw>
<ScriptedModelDraw
id="ModuleTag_DrawZ"
OkToChangeModelColor="true"
InitialRecoilSpeed="0.1"
MaxRecoilDistance="0.1"
RecoilDamping="2.0"
RecoilSettleSpeed="3.0"
ExtraPublicBone="FX_Weapon_01 FX_Weapon_02">
<ModelConditionState
ParseCondStateType="PARSE_DEFAULT"
RetainSubObjects="true">
<Model
Name="" />
</ModelConditionState>
<ModelConditionState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="FORMATION_PREVIEW">
<Model
Name="AUAntiAirShip_FPZ" />
</ModelConditionState>
<AnimationState
ParseCondStateType="PARSE_DEFAULT">
<Script>
CurDrawableShowSubObjectPermanently("GUN")
CurDrawableHideSubObjectPermanently("BEAM")
</Script>
</AnimationState>
</ScriptedModelDraw>
<!-- Used in the Weapon Scramble Beam -->
<LaserDraw
id="ModuleTag_LaserDraw"
Texture1_UTile="1"
Texture1_VTile="1"
Texture1_UScrollRate="0"
Texture1_VScrollRate="0"
Texture1_NumFrames="1"
Texture1_FrameRate="30"
Texture2_UTile="1"
Texture2_VTile="1"
Texture2_UScrollRate="0"
Texture2_VScrollRate="1"
Texture2_NumFrames="1"
Texture2_FrameRate="30"
LaserWidth="40"
LaserStateID="1">
<FXShader
ShaderName="Laser.fx"
TechniqueIndex="0">
<Constants>
<Texture
Name="Texture1">
<Value>FXGrid_3</Value>
</Texture>
<Texture
Name="Texture2">
<Value>FXInterlacedMask2</Value>
</Texture>
<Float Name="ColorEmissive">
<Value>0.00000</Value>
<Value>2.00000</Value>
<Value>1.000000</Value>
</Float>
</Constants>
</FXShader>
</LaserDraw>
<!-- Used for the Phalanx Gun -->
<TracerModelDraw
id="ModuleTag_TracerModelDraw"
MinLength="10.0"
MaxLength="25.0"
Width="15.0"
MinSpeed="22"
MaxSpeed="32"
SweepSpeed="3.0"
SpreadAngle="5.0"
MinTracersPerFrame="0.4"
MaxTracersPerFrame="0.4"
FrameLifeTime="25"
WeaponSlotType="PRIMARY_WEAPON"
Texture="FXTracer"
UseAdditiveBlending="true" >
<HeadColor
r="1.0"
g="1.0"
b="1.0"
a="1.0" />
<TailColor
r="1.0"
g="0.75"
b="0.65"
a="0.0" />
<ObjectStatusValidation
ForbiddenStatus="WEAPON_UPGRADED_01 GENERIC_TOGGLE_STATE" />
</TracerModelDraw>
<TracerModelDraw
id="ModuleTag_TracerModelDrawVeterancy"
MinLength="10.0"
MaxLength="25.0"
Width="15.0"
MinSpeed="30"
MaxSpeed="30"
SweepSpeed="0.5"
SpreadAngle="0.5"
MinTracersPerFrame="0.4"
MaxTracersPerFrame="0.4"
FrameLifeTime="35"
WeaponSlotType="PRIMARY_WEAPON"
Texture="FXTracerHeroic"
UseAdditiveBlending="true" >
<HeadColor
r="1.0"
g="0.0"
b="0.0"
a="1.0" />
<TailColor
r="1.0"
g="0.75"
b="0.65"
a="0.0" />
<ObjectStatusValidation
RequiredStatus="WEAPON_UPGRADED_01"
ForbiddenStatus="GENERIC_TOGGLE_STATE" />
</TracerModelDraw>
<!-- DRAW PARTICLES -->
<ScriptedModelDraw
id="ModuleTag_Draw_FX"
OkToChangeModelColor="true">
<ModelConditionState
ParseCondStateType="PARSE_DEFAULT">
<Model
Name="AUAntiAirShip_FX" />
</ModelConditionState>
<ModelConditionState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="DAMAGED">
<Model
Name="AUAntiAirShip_FX" />
<ParticleSysBone
BoneName="FX_BONE01"
FXParticleSystemTemplate="VehicleDamageSmoke"
FollowBone="true" />
</ModelConditionState>
<ModelConditionState
ParseCondStateType="PARSE_NORMAL"
ConditionsYes="REALLYDAMAGED">
<Model
Name="AUAntiAirShip_FX" />
<ParticleSysBone
BoneName="FX_BONE01"
FXParticleSystemTemplate="VehicleDamageSmoke"
FollowBone="true" />
<ParticleSysBone
BoneName="FX_BONE01"
FXParticleSystemTemplate="VehicleDamageFire"
FollowBone="true" />
<ParticleSysBone
BoneName="FX_BONE01"
FXParticleSystemTemplate="VehicleDamageFire02"
FollowBone="true" />
</ModelConditionState>
</ScriptedModelDraw>
</Draws>
<Behaviors>
<WeaponSetUpdate
id="ModuleTag_WeaponSetUpdate">
<WeaponSlotTurret
ID="1">
<!-- This weapon is always around, but the weapon template itself prevents it
from being able to be fired once it's upgraded. -->
<Weapon
Ordering="PRIMARY_WEAPON"
Template="AlliedAntiAirShipPhalanxGun"
ForbiddenObjectStatus="GENERIC_TOGGLE_STATE"
/>
<Weapon
Ordering="SECONDARY_WEAPON"
Template="AlliedAntiAirShipWeaponScrambler"
ObjectStatus="GENERIC_TOGGLE_STATE"/>
<TurretSettings
TurretTurnRate="360"
MinimumPitch="0d"
AllowsPitch="true"
TurretPitchRate="180"
MinIdleScanTime="1.0s"
MaxIdleScanTime="5.0s"
MinIdleScanAngle="10.0"
MaxIdleScanAngle="90.0"
ComeToHaltJiggle="3d">
<TurretAITargetChooserData
IdleScanDelay="=$FAST_IDLE_SCAN_DELAY"
CanAcquireDynamicIfAssignedOutOfRange="true" />
</TurretSettings>
</WeaponSlotTurret>
</WeaponSetUpdate>
<Physics
id="ModuleTag_Physics" />
<CreateObjectDie
id="ModuleTag_CreateObjectDie"
CreationList="AUAntiAirShip_Die_OCL">
<DieMuxData
DeathTypes="ALL"
DeathTypesForbidden="FLOODED"/>
</CreateObjectDie>
<CreateObjectDie
id="ModuleTag_CreateObjectDieWhole"
CreationList="AUAntiAirShip_Die_OCL">
<DieMuxData
DeathTypes="FLOODED" />
</CreateObjectDie>
<DynamicsUpdate
id="ModuleTag_DefaultDynamicsUpdate"
xai:joinAction="Remove" />
<DestroyDie
id="ModuleTag_Die">
<DieMuxData
DeathTypes="ALL" />
</DestroyDie>
<FXListBehavior
id="ModuleTag_FXList">
<DieMuxData
DeathTypes="ALL" />
<Event
Index="onDeath"
FX="FX_ALL_HydrofoilDie" />
</FXListBehavior>
<!-- The toggle for the Weapon Scrambler -->
<SpecialPower
id="ModuleTag_ActivateWeaponScrambler"
SpecialPowerTemplate="SpecialPower_ToggleWeaponScrambler"
UpdateModuleStartsAttack="true" />
<ToggleStatusSpecialAbilityUpdate
id="ModuleTag_ActivateWeaponScramblerUpdate"
SpecialPowerTemplate="SpecialPower_ToggleWeaponScrambler"
Options="RECONSTITUTE_STORED_COMMAND">
<ToggleState
EnterStateSound="ALL_HydroFoil_ScramblerToggleOffMS">
<SkirmishAiInfo
ToggleHint="TOGGLE_DEFAULT">
<StateWeapon
Weapon="AlliedAntiAirShipPhalanxGun" />
</SkirmishAiInfo>
</ToggleState>
<ToggleState
ObjectStatus="GENERIC_TOGGLE_STATE"
ModelConditions="WEAPONSTATE_TWO"
EnterStateSound="ALL_HydroFoil_ScramblerToggleOnMS">
<SkirmishAiInfo
ToggleHint="TOGGLE_LOCKDOWN"
NeverUseInState="RETREAT">
<StateWeapon
Weapon="AlliedAntiAirShipWeaponScrambler" />
</SkirmishAiInfo>
</ToggleState>
</ToggleStatusSpecialAbilityUpdate>
<!-- The special power that is used by the weapon -->
<SpecialPower
id="ModuleTag_WeaponScrambler"
SpecialPowerTemplate="SpecialPower_WeaponScrambler"
TriggerFX="FX_None"
UpdateModuleStartsAttack="false" />
<LaserState
id="ModuleTag_LaserState"
LaserId="1" >
<LaserEndParticleSystem>AlliedHydroScrambler_Sparks</LaserEndParticleSystem>
<LaserStartParticleSystem>AlliedHydroScrambler_Start</LaserStartParticleSystem>
</LaserState>
<StatusBitsUpgrade
id="ModuleTag_VeterancyUpgrade"
StatusToSet="WEAPON_UPGRADED_01">
<TriggeredBy>Upgrade_Veterancy_HEROIC</TriggeredBy>
</StatusBitsUpgrade>
</Behaviors>
<AI>
<AIUpdate
id="ModuleTag_AI"
AutoAcquireEnemiesWhenIdle="YES"
StateMachine="UnitAIStateMachine">
<UnitAITargetChooserData
CanPickDynamicTargets="false"/>
</AIUpdate>
</AI>
<Body>
<ActiveBody
id="ModuleTag_02"
MaxHealth="400" />
</Body>
<ClientBehaviors>
<ModelConditionAudioLoopClientBehavior id="ModuleTag_ShrunkenVoice">
<ModelConditionSound Sound="ALL_Hydrofoil_VoiceShrunken" RequiredFlags="SHRINK_EFFECT" />
</ModelConditionAudioLoopClientBehavior>
<ModelConditionSoundSelectorClientBehavior id="ModuleTag_VoiceAttackWeaponJammer">
<Override RequiredFlags="WEAPONSTATE_TWO">
<AudioArrayVoice>
<AudioEntry Sound="ALL_Hydrofoil_VoiceAttackSpecial" AudioType="voiceAttack" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceSelectMS" AudioType="voiceSelectBattle" />
</AudioArrayVoice>
</Override>
</ModelConditionSoundSelectorClientBehavior>
<ModelConditionAudioLoopClientBehavior xai:joinAction="Replace" xmlns:xai="uri:ea.com:eala:asset:instance" id="ModuleTag_MagneticSatelliteSuckedAway">
<ModelConditionSound Sound="SOV_MagneticSatellite_SuckedAwayWater" RequiredFlags="SUCKED_UP_HIGH" />
</ModelConditionAudioLoopClientBehavior>
</ClientBehaviors>
<Geometry>
<Shape
Type="BOX"
MajorRadius="30.0"
MinorRadius="18.0"
Height="20.0"
ContactPointGeneration="VEHICLE"/>
</Geometry>
<AudioArrayVoice>
<AudioEntry Sound="ALL_Hydrofoil_VoiceAttack" AudioType="voiceAttack" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceMoveAttack" AudioType="voiceAttackAfterMoving" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceCreate" AudioType="voiceCreated" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceMove" AudioType="voiceMove" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceRetreat" AudioType="voiceRetreatToCastle" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceSelectMS" AudioType="voiceSelect" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceSelectBattleMS" AudioType="voiceSelectBattle" />
<AudioEntry Sound="ALL_Hydrofoil_VoiceSelectUnderFireMS" AudioType="voiceSelectUnderFire" />
<!-- <NamedEntry Sound="ALL_Hydrofoil_VoiceAttackSpecial" Name="VoiceWeaponScrambler" /> oops plays on toggle -->
</AudioArrayVoice>
<AudioArraySound>
<AudioEntry
Sound="ALL_Hydrofoil_MoveStart"
AudioType="soundMoveStart" />
<AudioEntry
Sound="VehicleCrush"
AudioType="soundCrushing" />
<AudioEntry
Sound="Ship_Small_MoveLoopWater"
AudioType="soundMoveLoop" />
</AudioArraySound>
<VisionInfo
VisionRange="325"
ShroudClearingRange="=$STANDARD_SHROUD_CLEAR" />
<CrusherInfo
id="id_CrusherInfo"
CrusherLevel="1"
CrushableLevel="20" />
</GameObject>
</AssetDeclaration>

File diff suppressed because it is too large Load Diff

3253
public/Locomotor.xml Normal file

File diff suppressed because it is too large Load Diff

2291
public/LogicCommand.xml Normal file

File diff suppressed because it is too large Load Diff

1097
public/LogicCommandSet.xml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import axios from 'axios';
import { logoutUser } from '../utils/jwt'; // logoutUser会处理清除存储和重定向
const API_BASE_URL = 'https://api.zybdatasupport.online';
const API_BASE_URL = 'http://zybdatasupport.online:8000'
// const API_BASE_URL = 'http://110.42.61.148/'
// const API_BASE_URL = 'https://api.zybdatasupport.online';
//const API_BASE_URL = 'http://hk.zybdatasupport.online:8000/';
const axiosInstance = axios.create({

16
src/api/info.js Normal file
View File

@@ -0,0 +1,16 @@
import axiosInstance from './axiosConfig'
/**
* 获取后端版本信息
* GET /info
* 基于 axiosInstance 的 baseURL`http://zybdatasupport.online:8000`
* 返回后端提供的原始 JSON 数据
*/
export const getBackendInfo = async () => {
try {
const { data } = await axiosInstance.get('/info')
return data
} catch (error) {
throw error
}
}

View File

@@ -154,12 +154,12 @@ export const changeUserName = async (name) => {
* 用户请求修改密码(发送重置请求)
* 路由: /user/resetpassword
* 方法: POST
* 需要登录
* @param {string} uuid - 要修改的用户的uuid
* @returns {Promise<void>} 无返回值,成功即为请求成功
*/
export const requestResetPassword = async () => {
export const requestResetPassword = async (uuid) => {
try {
await axiosInstance.post('/user/resetpassword');
await axiosInstance.post('/user/resetpassword', null, { params: { uuid } });
} catch (error) {
throw error;
}
@@ -182,3 +182,18 @@ export const resetPassword = async (token, password) => {
}
}
/**
* 忘记密码
* @param {string} qq - QQ号
* @param {string} new_password - 新密码
* @param {string} token - 验证码token
* @param {string} captcha - 用户输入的验证码
*/
export const forgetPassword = async (qq, new_password, token, captcha) => {
try {
} catch (error) {
throw error;
}
}

64
src/api/record.js Normal file
View File

@@ -0,0 +1,64 @@
import axiosInstance from './axiosConfig';
/**
* 上传处理后的录像文件
* 路由: /record/upload
* 方法: POST
* 需要admin权限
* @param {file} file - 表单负载"file"上传
* @returns {id<int>} - HTTP_202_ACCEPTED 录像id
*/
export const uploadRecord = async (file) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await axiosInstance.post('/record/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};
/**
* 获取录像解析状态
* 路由: /record/{id}
* 方法: GET
* 需要登录
* @param {int} id - 录像id
* @returns {id<int>} id - 录像id
* @returns {status<string>} status - 状态processing 处理中success 处理成功fail 处理失败
* @returns {data<json>} data - 录像数据仅当处理成功时有值
*/
export const getRecordStatus = async (id) => {
try {
const response = await axiosInstance.get(`/record/${id}`);
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* 获取单位信息
* 路由: /unit
* 方法: GET
* 三个参数仅使用一个即可,如果传入多个优先选择上面的
* @param {Object} params - 参数 { id, code, name }
* @returns {id<string>} id
* @returns {code<string>} code
* @returns {name<string>} name
*/
export const unitInfo = async (params = {}) => {
try {
const response = await axiosInstance.get('/unit', { params });
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}

View File

@@ -3,239 +3,225 @@ import axiosInstance from './axiosConfig';
/**
* 添加赛事
* @param {Object} tournamentData - 赛事数据
* @param {string} tournamentData.name - 赛事名称
* @param {string} tournamentData.format - 赛事类型(single, double, count)
* @param {number} tournamentData.id - 数据库中id
* @param {string} tournamentData.name - 名称
* @param {string} tournamentData.format - 类型(single, double, count)
* @param {string} tournamentData.organizer - 组织者
* @param {string} tournamentData.qq_code - QQ号
* @param {string} tournamentData.status - 状态(prepare, finish, starting)
* @param {string} tournamentData.start_time - 开始时间(格式年/月/日例2025/05/24)
* @param {string} tournamentData.end_time - 结束时间(格式年/月/日例2025/05/24)
* @returns {Promise} 返回添加赛事的响应数据
* @returns {Promise<Object>} 返回添加赛事的响应数据
*/
export const addTournament = async (tournamentData) => {
try {
const response = await axiosInstance.post('/tournament/add', tournamentData)
return response.data
const response = await axiosInstance.post('/tournament/add', tournamentData);
return response.data;
} catch (error) {
console.error('添加赛事失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('添加赛事失败:', error);
throw error;
}
}
};
/**
* 获取赛事列表
* @returns {Promise} 返回赛事列表数据
* @returns {Promise<Array<Object>>} 返回赛事列表数据
*/
export const getTournamentList = async () => {
try {
const response = await axiosInstance.get('/tournament/getlist')
return response.data
const response = await axiosInstance.get('/tournament/getlist');
return response.data;
} catch (error) {
console.error('获取赛事列表失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('获取赛事列表失败:', error);
throw error;
}
}
};
// 更新赛事
export const updateTournament = async (id, data) => {
/**
* 更新赛事
* @param {number} id - 赛事ID
* @param {Object} tournamentData - 赛事数据
* @param {number} tournamentData.id - 数据库中id
* @param {string} tournamentData.name - 名称
* @param {string} tournamentData.format - 类型(single, double, count)
* @param {string} tournamentData.organizer - 组织者
* @param {string} tournamentData.qq_code - QQ号
* @param {string} tournamentData.status - 状态(prepare, finish, starting)
* @param {string} tournamentData.start_time - 开始时间(格式年/月/日例2025/05/24)
* @param {string} tournamentData.end_time - 结束时间(格式年/月/日例2025/05/24)
* @returns {Promise<Object>} 返回更新赛事的响应数据
*/
export const updateTournament = async (id, tournamentData) => {
try {
console.log('更新赛事,发送数据:', data)
const response = await axiosInstance.put(`/tournament/update/${id}`, {
name: data.name,
format: data.format,
organizer: data.organizer,
qq_code: data.qq_code,
start_time: data.start_time,
end_time: data.end_time,
status: data.status
})
return response.data
const response = await axiosInstance.put(`/tournament/update/${id}`, tournamentData);
return response.data;
} catch (error) {
console.error('更新赛事失败:', error)
if (error.response) {
console.error('错误详情:', {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
config: error.config
})
// 如果有详细的错误信息,抛出它
if (error.response.data?.detail) {
throw new Error(error.response.data.detail)
}
}
throw error
console.error('更新赛事失败:', error);
throw error;
}
}
};
// 删除赛事
/**
* 删除赛事
* @param {number} id - 赛事ID
* @returns {Promise<Object>} 返回删除赛事的响应数据
*/
export const deleteTournament = async (id) => {
try {
const response = await axiosInstance.delete(`/tournament/delete/${id}`)
return response.data
const response = await axiosInstance.delete(`/tournament/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除赛事失败:', error)
throw error
console.error('删除赛事失败:', error);
throw error;
}
}
};
// 添加报名结果
export const addSignUpResult = async (data) => {
/**
* 添加玩家报名
* @param {Object} signupData - 报名数据
* @param {number} signupData.id - 数据库中id
* @param {string} signupData.type - 类型
* @param {string} signupData.teamname - 队伍名称
* @param {string} signupData.faction - 阵营(allied、soviet、empire、ob、voice、random)
* @param {string} signupData.username - 用户名
* @param {string} signupData.qq - QQ号
* @returns {Promise<Object>} 返回添加报名的响应数据
*/
export const addSignUp = async (signupData) => {
try {
const response = await axiosInstance.post('/tournament/signup_result/add', {
tournament_id: parseInt(data.tournament_id),
tournament_name: data.tournament_name,
team_name: data.team_name,
sign_name: data.sign_name.trim(),
win: '0',
lose: '0',
status: 'tie'
})
return response.data
const response = await axiosInstance.post('/tournament/signup/add', signupData);
return response.data;
} catch (error) {
console.error('请求错误:', error)
if (error.response?.data?.detail) {
throw new Error(error.response.data.detail)
}
throw error
console.error('添加报名失败:', error);
throw error;
}
}
};
// 获取参赛结果列表
/**
* 获取玩家报名列表
* @returns {Promise<Array<Object>>} 返回报名列表数据
*/
export const getSignUpList = async () => {
try {
const response = await axiosInstance.get('/tournament/signup/getlist');
return response.data;
} catch (error) {
console.error('获取报名列表失败:', error);
throw error;
}
};
/**
* 更新玩家报名
* @param {number} id - 报名ID
* @param {Object} signupData - 报名数据
* @param {number} signupData.id - 数据库中id
* @param {string} signupData.type - 类型
* @param {string} signupData.teamname - 队伍名称
* @param {string} signupData.faction - 阵营(allied、soviet、empire、ob、voice、random)
* @param {string} signupData.username - 用户名
* @param {string} signupData.qq - QQ号
* @returns {Promise<Object>} 返回更新报名的响应数据
*/
export const updateSignUp = async (id, signupData) => {
try {
const response = await axiosInstance.put(`/tournament/signup/update/${id}`, signupData);
return response.data;
} catch (error) {
console.error('更新报名失败:', error);
throw error;
}
};
/**
* 删除玩家报名
* @param {number} id - 报名ID
* @returns {Promise<Object>} 返回删除报名的响应数据
*/
export const deleteSignUp = async (id) => {
try {
const response = await axiosInstance.delete(`/tournament/signup/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除报名失败:', error);
throw error;
}
};
/**
* 添加报名结果
* @param {Object} resultData - 结果数据
* @param {number} resultData.tournament_id - 赛事id(int)
* @param {string} resultData.tournament_name - 赛事名称
* @param {string} resultData.team_name - 队伍名称(可选)
* @param {string} resultData.sign_name - 参赛人员名称
* @param {string} resultData.win - 参赛人员胜利局数(str)
* @param {string} resultData.lose - 参赛人员失败局数(str)
* @param {string} resultData.status - 参赛人员对局状态(win,lose,tie)
* @param {string} resultData.round - 轮数(str)
* @param {string} resultData.rival_name - 对方name(str)
* @returns {Promise<Object>} 返回添加报名结果的响应数据
*/
export const addSignUpResult = async (resultData) => {
try {
const response = await axiosInstance.post('/tournament/signup_result/add', resultData);
return response.data;
} catch (error) {
console.error('添加报名结果失败:', error);
throw error;
}
};
/**
* 获取报名结果列表
* @returns {Promise<Array<Object>>} 返回报名结果列表数据
*/
export const getSignUpResultList = async () => {
try {
const response = await axiosInstance.get('/tournament/signup_result/getlist')
return response.data
const response = await axiosInstance.get('/tournament/signup_result/getlist');
return response.data;
} catch (error) {
console.error('获取参赛结果列表失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('获取报名结果列表失败:', error);
throw error;
}
}
};
// 更新参赛结果
export const updateSignUpResult = async (id, data) => {
/**
* 更新报名结果
* @param {number} id - 结果ID
* @param {Object} resultData - 结果数据
* @param {number} resultData.tournament_id - 赛事id(int)
* @param {string} resultData.tournament_name - 赛事名称
* @param {string} resultData.team_name - 队伍名称(可选)
* @param {string} resultData.sign_name - 参赛人员名称
* @param {string} resultData.win - 参赛人员胜利局数(str)
* @param {string} resultData.lose - 参赛人员失败局数(str)
* @param {string} resultData.status - 参赛人员对局状态(win,lose,tie)
* @param {string} resultData.round - 轮数(str)
* @param {string} resultData.rival_name - 对方name(str)
* @returns {Promise<Object>} 返回更新报名结果的响应数据
*/
export const updateSignUpResult = async (id, resultData) => {
try {
// // 更新报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('更新报名信息...')
// await axiosInstance.put(`/tournament/signup/update/${id}`, {
// tournament_id: parseInt(data.tournament_id),
// type: data.team_name ? 'teamname' : 'individual',
// teamname: data.team_name || '',
// faction: data.faction || 'random',
// username: data.sign_name,
// qq: data.qq || ''
// })
// console.log('报名信息更新成功')
// 更新报名结果
console.log('更新报名结果...')
await axiosInstance.put(`/tournament/signup_result/update/${id}`, {
tournament_id: parseInt(data.tournament_id),
tournament_name: data.tournament_name,
team_name: data.team_name || null,
sign_name: data.sign_name,
win: data.win || '0',
lose: data.lose || '0',
status: data.status || 'tie'
})
console.log('报名结果更新成功')
return { success: true }
const response = await axiosInstance.put(`/tournament/signup_result/update/${id}`, resultData);
return response.data;
} catch (error) {
console.error('更新参赛结果失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('更新报名结果失败:', error);
throw error;
}
}
};
// 删除参赛选手
/**
* 删除报名结果
* @param {number} id - 结果ID
* @returns {Promise<Object>} 返回删除报名结果的响应数据
*/
export const deleteSignUpResult = async (id) => {
try {
// 删除报名结果
console.log('删除报名结果...')
await axiosInstance.delete(`/tournament/signup_result/delete/${id}`)
console.log('报名结果删除成功')
// // 删除报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('删除报名信息...')
// await axiosInstance.delete(`/tournament/signup/delete/${id}`)
// console.log('报名信息删除成功')
return { success: true }
const response = await axiosInstance.delete(`/tournament/signup_result/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除参赛选手失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('删除报名结果失败:', error);
throw error;
}
}
// 添加报名
export const addSignUp = async (data) => {
try {
console.log('开始报名流程,数据:', data)
// 调用报名 API
console.log('调用报名 API...')
await axiosInstance.post('/tournament/signup/add', {
tournament_id: data.id,
type: data.type,
teamname: data.team_name || '',
faction: data.faction || 'random',
username: data.sign_name,
qq: data.qq || ''
})
console.log('报名 API 调用成功')
// 调用报名结果 API
console.log('调用报名结果 API...')
await axiosInstance.post('/tournament/signup_result/add', {
tournament_id: data.id,
tournament_name: data.tournament_name,
team_name: data.team_name || null,
sign_name: data.sign_name,
win: '0',
lose: '0',
status: 'tie'
})
console.log('报名结果 API 调用成功')
return {
signup: { success: true },
result: { success: true }
}
} catch (error) {
console.error('报名请求错误:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
config: error.config
})
// 如果是服务器返回的错误信息,直接使用
if (error.response?.data?.detail) {
throw new Error(error.response.data.detail)
}
// 其他错误,包装成更友好的错误信息
throw new Error('报名失败,请检查网络连接后重试')
}
}
};

BIN
src/assets/login_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

BIN
src/assets/login_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

19
src/assets/version.md Normal file
View File

@@ -0,0 +1,19 @@
### 后端
## V.2025.07.30.1
1、修改服务器地址
------
### 前端
## v.1.0.1
1、谢谢你gemini3 pro改了下赛事样式
## v.1.0.0
1、添加版本信息页面
2、修改服务器地址

View File

@@ -0,0 +1,210 @@
<template>
<div v-if="visible" class="password-dialog-overlay" @click.self="handleClose">
<div class="password-dialog">
<div class="password-dialog-content">
<div class="dialog-title">修改密码</div>
<div class="confirm-content">
<p class="confirm-message">是否要修改密码</p>
</div>
</div>
<div class="password-dialog-footer">
<button class="cancel-button" @click="handleClose" :disabled="loading">取消</button>
<button
class="confirm-button"
@click="handleSubmit"
:disabled="loading"
>
{{ loading ? '修改中...' : '确定' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue'
import { requestResetPassword, resetPassword } from '@/api/login.js'
import {getStoredUser} from "@/utils/jwt.js";
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'success', 'error'])
const loading = ref(false)
// 监听弹窗显示状态
watch(() => props.visible, (visible) => {
if (visible) {
loading.value = false
}
})
const handleClose = () => {
if (!loading.value) {
emit('close')
}
}
const handleSubmit = async () => {
if (loading.value) {
return
}
loading.value = true
try {
const user = getStoredUser()
console.log(user.uuid)
// 请求修改密码
await requestResetPassword(user.uuid)
emit('success', '密码修改请求已发送!')
emit('close')
} catch (error) {
console.error('请求修改密码失败:', error)
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || '请求修改密码失败,请重试'
emit('error', errorMessage)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.password-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.password-dialog {
background: white;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
animation: dialog-fade-in 0.3s ease;
}
.password-dialog-content {
padding: 20px;
text-align: center;
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.confirm-content {
margin-bottom: 0;
}
.confirm-message {
font-size: 16px;
color: #666;
margin: 0;
line-height: 1.5;
}
.password-dialog-footer {
padding: 10px 20px 20px;
text-align: center;
display: flex;
justify-content: center;
gap: 12px;
}
.cancel-button, .confirm-button {
padding: 8px 24px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.cancel-button {
background-color: #909399;
color: white;
}
.cancel-button:hover:not(:disabled) {
background-color: #a6a9ad;
}
.confirm-button {
background-color: #67c23a;
color: white;
}
.confirm-button:hover:not(:disabled) {
background-color: #85ce61;
}
.confirm-button:disabled {
background-color: #c0c4cc;
cursor: not-allowed;
}
.cancel-button:disabled {
background-color: #c0c4cc;
cursor: not-allowed;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 移动端适配 */
@media screen and (max-width: 480px) {
.password-dialog {
width: 85%;
}
.dialog-title {
font-size: 16px;
}
.password-dialog-content {
padding: 16px;
}
.confirm-message {
font-size: 14px;
}
.password-dialog-footer {
flex-direction: column;
gap: 8px;
}
.cancel-button, .confirm-button {
width: 100%;
padding: 10px;
font-size: 13px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ const handleClose = () => {
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 2000;
}
.error-dialog {

View File

@@ -25,7 +25,7 @@ const props = defineProps({
})
const emit = defineEmits(['close', 'apply'])
function handleClose() {
function handleClose() {
emit('close')
}
function handleApply() {

View File

@@ -12,7 +12,7 @@
<div class="player-info">
<div class="player-name">{{ item.username }}</div>
<div class="player-faction">{{ item.faction }}</div>
<div class="player-score">{{ item.score }}</div>
<div class="player-score" :title="`积分: ${item.points}`">{{ item.score }}</div>
</div>
</div>
</div>
@@ -31,7 +31,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { getSignUpResultList } from '@/api/tournament'
const props = defineProps({
@@ -46,31 +46,77 @@ const rankData = ref([])
const fetchRankData = async () => {
try {
const response = await getSignUpResultList()
// 筛选当前赛事的玩家并按胜场数排序
const results = response
.filter(player => player.tournament_id === props.tournamentId)
.map((player, index) => ({
rank: index + 1,
username: player.sign_name,
faction: player.faction,
win: parseInt(player.win) || 0,
lose: parseInt(player.lose) || 0,
score: `${player.win}${player.lose}`
}))
.sort((a, b) => {
// 首先按胜场数排序
if (b.win !== a.win) {
return b.win - a.win
console.log('RankContestant 原始数据:', response)
// 筛选当前赛事的玩家
const filteredPlayers = response.filter(player => player.tournament_id === props.tournamentId)
console.log('RankContestant 筛选后数据:', filteredPlayers)
// 按玩家名称分组,计算总胜利数和总失败数
const playerStats = {}
filteredPlayers.forEach((player, index) => {
const playerName = player.sign_name
const win = parseInt(player.win || 0)
const lose = parseInt(player.lose || 0)
const round = player.round || '0'
console.log(`RankContestant 处理第${index + 1}条数据:`, {
id: player.id,
sign_name: playerName,
round: round,
win: win,
lose: lose,
status: player.status
})
if (!playerStats[playerName]) {
playerStats[playerName] = {
username: playerName,
faction: player.faction || player.team_name || '',
totalWin: 0,
totalLose: 0,
totalPoints: 0,
rounds: []
}
// 如果胜场数相同,则按负场数排序(负场数少的排前面)
return a.lose - b.lose
}
playerStats[playerName].totalWin += win
playerStats[playerName].totalLose += lose
playerStats[playerName].rounds.push(round)
// 计算积分(胜场数)
playerStats[playerName].totalPoints = playerStats[playerName].totalWin
})
console.log('RankContestant 分组后统计:', playerStats)
// 转换为数组并按总胜利数排序
const sortedPlayers = Object.values(playerStats)
.sort((a, b) => {
// 首先按总胜利数排序
if (b.totalWin !== a.totalWin) {
return b.totalWin - a.totalWin
}
// 如果胜利数相同,按失败数排序(失败数少的排名靠前)
return a.totalLose - b.totalLose
})
.map((player, index) => ({
...player,
rank: index + 1
}))
id: index + 1, // 使用索引作为ID
rank: (index + 1).toString(),
username: player.username,
faction: player.faction,
win: player.totalWin,
lose: player.totalLose,
points: player.totalPoints,
status: '',
bracket_type: 'winners',
score: `${player.totalWin}${player.totalLose}负 (${player.totalPoints.toFixed(1)}分)`,
rounds: player.rounds.join(',')
}))
rankData.value = results
console.log('RankContestant 最终排名结果:', sortedPlayers)
rankData.value = sortedPlayers
} catch (error) {
console.error('获取排名数据失败:', error)
}
@@ -79,6 +125,13 @@ const fetchRankData = async () => {
onMounted(() => {
fetchRankData()
})
// 当比赛ID变化时重新获取数据
watch(() => props.tournamentId, () => {
if (props.tournamentId) {
fetchRankData()
}
})
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
<template>
<div class="temp-privilege-form">
<h2>管理员使用qq发送修改密码邮件</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="uuid">用户qq</label>
<input id="uuid" v-model="qq_code" type="text" placeholder="请输入用户qq" required />
</div>
<button class="submit-btn" type="submit" :disabled="loading">提交</button>
</form>
<div v-if="msg" :class="{'success-msg': msgType==='success', 'error-msg': msgType==='error'}">{{ msg }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {getUserByInfo,requestResetPassword} from '@/api/login'
const qq_code = ref('')
const loading = ref(false)
const msg = ref('')
const msgType = ref('')
async function handleSubmit() {
msg.value = ''
msgType.value = ''
loading.value = true
try {
//api
const user = await getUserByInfo({qq_code:qq_code.value})
console.log(user)
await requestResetPassword(user.uuid)
msg.value = '发送成功!'
msgType.value = 'success'
qq_code.value = ''
} catch (e) {
msg.value = '发送失败请稍后再试'
msgType.value = 'error'
}
loading.value = false
}
</script>
<style scoped>
.temp-privilege-form {
background: #fff;
border-radius: 8px;
padding: 32px 24px 24px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
max-width: 420px;
margin: 40px auto 0 auto;
}
.temp-privilege-form h2 {
margin-bottom: 24px;
color: #2563eb;
text-align: center;
}
.form-group {
margin-bottom: 18px;
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 6px;
color: #333;
font-weight: 500;
}
.form-group input,
.form-group select {
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 15px;
outline: none;
transition: border 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #2563eb;
}
.custom-exp-input {
margin-top: 8px;
}
.submit-btn {
width: 100%;
background: #2563eb;
color: #fff;
border: none;
border-radius: 4px;
padding: 10px 0;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.submit-btn:disabled {
background: #a5b4fc;
cursor: not-allowed;
}
.success-msg {
color: #22c55e;
text-align: center;
margin-top: 18px;
}
.error-msg {
color: #ef4444;
text-align: center;
margin-top: 18px;
}
</style>

View File

@@ -0,0 +1,574 @@
<template>
<div class="login-form">
<div>忘记密码</div>
<!-- <form class="login-form-container" @submit.prevent="handleForgetPassword">-->
<!-- <div class="input-container">-->
<!-- <label for="username">QQ号</label>-->
<!-- <input-->
<!-- type="text"-->
<!-- id="username"-->
<!-- v-model="username"-->
<!-- placeholder="请输入QQ号"-->
<!-- :class="{ 'error': usernameError }"-->
<!-- />-->
<!-- <span class="error-message" v-if="usernameError">{{ usernameError }}</span>-->
<!-- </div>-->
<!-- <div class="input-container">-->
<!-- <label for="newPassword">新密码</label>-->
<!-- <input-->
<!-- type="password"-->
<!-- id="newPassword"-->
<!-- v-model="newPassword"-->
<!-- placeholder="请输入新密码"-->
<!-- :class="{ 'error': newPasswordError }"-->
<!-- />-->
<!-- <span class="error-message" v-if="newPasswordError">{{ newPasswordError }}</span>-->
<!-- </div>-->
<!-- <div class="input-container">-->
<!-- <label for="confirmPassword">确认新密码</label>-->
<!-- <input-->
<!-- type="password"-->
<!-- id="confirmPassword"-->
<!-- v-model="confirmPassword"-->
<!-- placeholder="请再次输入新密码"-->
<!-- :class="{ 'error': confirmPasswordError }"-->
<!-- />-->
<!-- <span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span>-->
<!-- </div>-->
<!-- <div class="input-container captcha-container">-->
<!-- <label for="captcha">验证码</label>-->
<!-- <div class="captcha-wrapper">-->
<!-- <input-->
<!-- type="text"-->
<!-- id="captcha"-->
<!-- v-model="captcha"-->
<!-- placeholder="请输入验证码"-->
<!-- :class="{ 'error': captchaError }"-->
<!-- />-->
<!-- <img-->
<!-- v-if="captchaImage"-->
<!-- :src="captchaImage"-->
<!-- alt="验证码"-->
<!-- class="captcha-image"-->
<!-- @click="refreshCaptcha"-->
<!-- />-->
<!-- </div>-->
<!-- <span class="error-message" v-if="captchaError">{{ captchaError }}</span>-->
<!-- </div>-->
<!-- <div class="login-button">-->
<!-- <button type="submit" :disabled="isSubmitting">-->
<!-- {{ isSubmitting ? '提交中...' : '重置密码' }}-->
<!-- </button>-->
<!-- </div>-->
<!-- <div class="register-link">-->
<!-- <a @click.prevent="$emit('login')">返回登录</a>-->
<!-- </div>-->
<!-- </form>-->
<form form class="login-form-container" @submit.prevent="uuid_handleForgetPassword">
<!-- <div class="input-container">-->
<!-- <label for="username">UUID</label>-->
<!-- <input-->
<!-- id="username"-->
<!-- type="text"-->
<!-- placeholder="请输入UUID"-->
<!-- v-model="uuid"-->
<!-- />-->
<!-- </div>-->
<div class="input-container">
<label for="username">QQ号</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入QQ号"
:class="{ 'error': usernameError }"
/>
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div>
<div class="login-button">
<button type="submit" :disabled="isSubmitting || cooldown > 0">
<template v-if="isSubmitting">提交中...</template>
<template v-else-if="cooldown > 0">请稍候 ({{ cooldown }}s)</template>
<template v-else>重置密码</template>
</button>
</div>
<div class="register-link">
<a @click.prevent="$emit('login')">返回登录</a>
</div>
</form>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
<SuccessDialog
:visible="showSuccess"
:title="successTitle"
:message="successMessage"
@close="handleSuccessClose"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {getCaptcha, requestResetPassword, getUserByInfo} from '../api/login'
import ErrorDialog from './ErrorDialog.vue'
import SuccessDialog from './SuccessDialog.vue'
const username = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const captcha = ref('')
const captchaImage = ref('')
const captchaToken = ref('')
const uuid = ref('')
// 错误信息
const usernameError = ref('')
const newPasswordError = ref('')
const confirmPasswordError = ref('')
const captchaError = ref('')
const uuidError = ref('')
// 状态
const isSubmitting = ref(false)
const cooldown = ref(0)
let cooldownTimer = null
// 错误弹窗相关
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
// 成功弹窗相关
const showSuccess = ref(false)
const successTitle = ref('成功')
const successMessage = ref('密码重置成功,请使用新密码登录')
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
const showSuccessMessage = (message, title = '成功') => {
successMessage.value = message
successTitle.value = title
showSuccess.value = true
}
const refreshCaptcha = async () => {
try {
const response = await getCaptcha()
captchaImage.value = `data:image/png;base64,${response.img}`
captchaToken.value = response.token
captcha.value = '' // 清空验证码输入
} catch (error) {
console.error('获取验证码失败:', error)
const apiError = error.response?.data?.detail || error.response?.data?.message
if (apiError) {
showErrorMessage(apiError)
} else {
showErrorMessage('获取验证码失败,请刷新页面重试')
}
}
}
const handleForgetPassword = async () => {
try {
// 验证表单
if (!validateForm()) {
return
}
isSubmitting.value = true
// 调用忘记密码API
await forgetPassword(
username.value,
newPassword.value,
captchaToken.value,
captcha.value
)
// 显示成功消息
showSuccessMessage('密码重置成功,请使用新密码登录')
} catch (error) {
console.error('重置密码失败:', error)
if (error.response) {
const apiError = error.response?.data?.detail || error.response?.data?.message
if (apiError) {
showErrorMessage(apiError)
} else {
showErrorMessage('重置密码失败,请稍后重试')
}
} else if (error.message === 'Network Error') {
showErrorMessage('无法连接服务器,请稍后重试')
} else {
showErrorMessage(error.message || '重置密码失败,请稍后重试')
}
// 重置失败时刷新验证码
refreshCaptcha()
} finally {
isSubmitting.value = false
}
}
const uuid_handleForgetPassword = async () => {
try{
if (!uuid_validateForm()) {
return
}
const user = await getUserByInfo({qq_code:username.value})
await requestResetPassword(user.uuid)
isSubmitting.value = true
cooldown.value = 60
cooldownTimer = setInterval(() => {
if (cooldown.value > 0) {
cooldown.value--
} else {
clearInterval(cooldownTimer)
cooldownTimer = null
}
}, 1000)
}catch ( error){
showErrorMessage(error.message || '不是正确的uuid')
}finally {
isSubmitting.value = false
}
}
const uuid_validateForm = () =>{
usernameError.value = ''
if (!username.value) {
usernameError.value = '请输入qq'
return false
}
return true
}
const validateForm = () => {
// 重置所有错误信息
usernameError.value = ''
newPasswordError.value = ''
confirmPasswordError.value = ''
captchaError.value = ''
// 验证用户名
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
// 验证新密码
if (!newPassword.value) {
newPasswordError.value = '请输入新密码'
return false
}
if (newPassword.value.length < 4) {
newPasswordError.value = '密码长度不能小于4个字符'
return false
}
if (newPassword.value.length > 20) {
newPasswordError.value = '密码长度不能超过20个字符'
return false
}
// 验证确认密码
if (!confirmPassword.value) {
confirmPasswordError.value = '请确认新密码'
return false
}
if (newPassword.value !== confirmPassword.value) {
confirmPasswordError.value = '两次输入的密码不一致'
return false
}
// 验证验证码
if (!captcha.value) {
captchaError.value = '请输入验证码'
return false
}
if (captcha.value.length !== 4) {
captchaError.value = '验证码长度不正确'
return false
}
return true
}
const handleSuccessClose = () => {
showSuccess.value = false
// 重置表单
// resetForm()
uuid_resetForm()
// 切换到登录页面
$emit('login')
}
// 重置表单数据
const resetForm = () => {
username.value = ''
newPassword.value = ''
confirmPassword.value = ''
captcha.value = ''
usernameError.value = ''
newPasswordError.value = ''
confirmPasswordError.value = ''
captchaError.value = ''
refreshCaptcha()
}
const uuid_resetForm = () =>{
username.value = ''
}
// 暴露resetForm方法给父组件
defineExpose({
// resetForm
uuid_resetForm
})
onMounted(() => {
// refreshCaptcha()
})
</script>
<style scoped>
.login-form {
width: 340px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.10);
padding: 36px 32px 28px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.login-form>div:first-child {
font-size: 26px;
font-weight: bold;
color: #222;
margin-bottom: 28px;
letter-spacing: 2px;
}
.login-form-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 18px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-container label {
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.input-container input {
height: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 0 12px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
}
.input-container input:focus {
border: 1.5px solid #409eff;
outline: none;
background: #fff;
}
.input-container input.error {
border-color: #f56c6c;
}
.error-message {
color: #f56c6c;
font-size: 12px;
margin-top: 2px;
}
.login-button {
margin-top: 10px;
}
.login-button button {
width: 100%;
height: 42px;
background: linear-gradient(90deg, #409eff 0%, #6dd5fa 100%);
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.10);
transition: background 0.2s;
}
.login-button button:hover {
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
}
.login-button button:disabled {
background: #a0cfff;
cursor: not-allowed;
}
.register-link {
margin-top: 12px;
text-align: right;
}
.register-link a {
color: #409eff;
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.register-link a:hover {
color: #1a73e8;
text-decoration: underline;
}
/* VAPTCHA 相关样式 */
.VAPTCHA-init-main {
display: table;
width: 100%;
height: 100%;
background-color: #f7fbfd;
border-radius: 6px;
border: 1px solid #d0d7de;
}
.VAPTCHA-init-loading {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.VAPTCHA-init-loading>a {
display: inline-block;
width: 18px;
height: 18px;
border: none;
}
.VAPTCHA-init-loading .VAPTCHA-text {
font-family: sans-serif;
font-size: 12px;
color: #666;
vertical-align: middle;
}
.captcha-container {
margin-bottom: 16px;
width: 100%;
}
.captcha-wrapper {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
position: relative;
}
.captcha-wrapper input {
flex: 1;
height: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 0 12px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
width: calc(100% - 120px); /* 减去验证码图片的宽度和间距 */
}
.captcha-wrapper input:focus {
border: 1.5px solid #409eff;
outline: none;
background: #fff;
}
.captcha-image {
width: 110px;
height: 40px;
border-radius: 6px;
cursor: pointer;
object-fit: cover;
border: 1px solid #d0d7de;
transition: all 0.3s;
}
.captcha-image:hover {
opacity: 0.8;
}
/* 移动端适配 */
@media screen and (max-width: 480px) {
.login-form {
width: 90%;
max-width: 340px;
padding: 24px 20px 20px 20px;
}
.captcha-wrapper {
gap: 6px;
}
.captcha-wrapper input {
width: calc(100% - 100px);
}
.captcha-image {
width: 90px;
}
.input-container input,
.captcha-wrapper input {
height: 38px;
font-size: 14px;
}
.login-button button {
height: 40px;
font-size: 15px;
}
}
/* 超小屏幕适配 */
@media screen and (max-width: 320px) {
.login-form {
padding: 20px 16px 16px 16px;
}
.captcha-wrapper input {
width: calc(100% - 90px);
padding: 0 8px;
}
.captcha-image {
width: 80px;
}
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@@ -47,7 +47,10 @@
<div class="login-button">
<button type="submit">登录</button>
</div>
<div class="forget-password">
</div>
<div class="register-link">
<a @click.prevent="$emit('forget')">忘记密码</a>
<a @click.prevent="$emit('register')">注册账号</a>
</div>
</form>
@@ -296,8 +299,9 @@ onMounted(() => {
}
.register-link {
display: flex;
justify-content: space-between;
margin-top: 12px;
text-align: right;
}
.register-link a {
@@ -313,6 +317,7 @@ onMounted(() => {
text-decoration: underline;
}
/* VAPTCHA 相关样式 */
.VAPTCHA-init-main {
display: table;

View File

@@ -16,7 +16,8 @@ const routes = [
children: [
{
path: '',
redirect: '/maps'
name: 'Home',
component: () => import('@/views/index/Home.vue')
},
{
path: 'demands',
@@ -48,33 +49,47 @@ const routes = [
path: 'weapon-match',
name: 'WeaponMatch',
component: () => import('@/views/weapon/WeaponMatch.vue'),
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
// meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
meta: { requiresAuth: true}
},
{
path: 'configEditor',
name: 'ConfigEditor',
component: () => import('@/views/index/ConfigEditor.vue'),
// meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
meta: { requiresAuth: true}
},
{
path: 'competition',
name: 'Competition',
component: () => import('@/views/index/Competition.vue'),
component: () => import('@/views/competition/Competition.vue'),
// meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-competitor'] }
meta: { requiresAuth: true}
},
{
path: 'competition/add',
name: 'AddCompetition',
component: () => import('@/views/index/AddContestant.vue'),
component: () => import('@/views/competition/AddContestant.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/detail',
name: 'CompetitionDetail',
component: () => import('@/views/index/CompetitionDetail.vue'),
component: () => import('@/views/competition/CompetitionDetail.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/signup',
name: 'CompetitionSignUp',
component: () => import('@/views/index/CompetitionSignUp.vue'),
component: () => import('@/views/competition/CompetitionSignUp.vue'),
meta: { requiresAuth: true }
},
// {
// path: 'competition',
// name: 'Competition',
// component: () => import('@/views/competition/Competition.vue'),
// meta: { requiresAuth: true}
// },
{
path: 'editors-maps',
name: 'EditorsMaps',
@@ -89,13 +104,18 @@ const routes = [
path: 'PIC2TGA',
name: 'PIC2TGA',
component: () => import('@/views/index/PIC2TGA.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
// meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
},
{
path: 'terrainGenerate',
name: 'TerrainGenerate',
component: () => import('@/views/index/TerrainGenerate.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
// meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
},
{
path: 'version',
name: 'VersionInfo',
component: () => import('@/views/index/VersionInfo.vue')
}
]
},
@@ -119,6 +139,11 @@ const routes = [
meta: { requiresAuth: true }
}
]
},
{
path: '/user/resetpassword/:token',
name: 'ResetPassword',
component: () => import('@/views/ResetPassword.vue')
}
]
@@ -158,7 +183,7 @@ router.beforeEach(async (to, from, next) => {
// 登录校验:如果页面需要登录但本地没有有效 token则跳转登录页
if (requiresAuth && !hasValidToken()) {
return next({
path: '/maps',
path: '/',
query: { redirect: to.fullPath }
})
}
@@ -169,7 +194,7 @@ router.beforeEach(async (to, from, next) => {
if (!user || !hasPrivilegeWithTemp(user, requiredPrivilege)) {
// 通过事件总线通知弹窗
eventBus.emit('no-privilege')
return next({ path: '/maps', replace: true })
return next({ path: '/', replace: true })
}
}

View File

@@ -98,7 +98,6 @@ export const loginSuccess = (accessToken, userId) => {
if (userId) {
localStorage.setItem('user_id', userId);
}
// 设置登录标志
justLoggedIn.value = true;

452
src/views/ResetPassword.vue Normal file
View File

@@ -0,0 +1,452 @@
<template>
<div class="reset-password-container">
<div class="bg-container">
<img :src="bgImg" alt="重置密码背景" />
</div>
<div class="bottom-text">
<p>© Byz解忧杂货铺</p>
</div>
<div class="content-container">
<div class="form-content">
<div class="reset-form">
<div class="form-title">重置密码</div>
<form class="reset-form-container" @submit.prevent="handleResetPassword">
<div class="input-container">
<label for="newPassword">新密码</label>
<div class="password-input-wrapper">
<input
v-model="newPassword"
:type="showPassword ? 'text' : 'password'"
id="newPassword"
placeholder="请输入新密码"
:disabled="loading"
@keyup.enter="handleResetPassword"
/>
<i
:class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
class="password-toggle"
@click="showPassword = !showPassword"
></i>
</div>
</div>
<div class="input-container">
<label for="confirmPassword">确认新密码</label>
<div class="password-input-wrapper">
<input
v-model="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
id="confirmPassword"
placeholder="请再次输入新密码"
:disabled="loading"
@keyup.enter="handleResetPassword"
/>
<i
:class="showConfirmPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
class="password-toggle"
@click="showConfirmPassword = !showConfirmPassword"
></i>
</div>
</div>
<div class="password-tips">
<p>密码长度4-20个字符</p>
</div>
<div class="reset-button">
<button type="submit" :disabled="loading || !isFormValid">
{{ loading ? '重置中...' : '重置密码' }}
</button>
</div>
<!-- <div class="back-link">-->
<!-- <a @click.prevent="goToHome">返回登录页</a>-->
<!-- </div>-->
</form>
</div>
</div>
</div>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
<SuccessDialog
:visible="showSuccess"
:title="successTitle"
:message="successMessage"
@close="handleSuccessClose"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { resetPassword } from '@/api/login.js'
import ErrorDialog from '@/components/ErrorDialog.vue'
import SuccessDialog from '@/components/SuccessDialog.vue'
import loginBg from '@/assets/login_1.jpg'
import loginBg1 from '@/assets/login_2.jpg'
import loginBg3 from '@/assets/login_3.jpg'
import loginBg4 from '@/assets/login_4.jpg'
import loginBg5 from '@/assets/login_5.jpg'
import {hasValidToken, logoutUser} from "@/utils/jwt.js";
const route = useRoute()
const router = useRouter()
const images = [loginBg, loginBg1, loginBg3,loginBg4, loginBg5]
const randomIndex = Math.floor(Math.random() * images.length)
const bgImg = ref(images[randomIndex])
const newPassword = ref('')
const confirmPassword = ref('')
const showPassword = ref(false)
const showConfirmPassword = ref(false)
const loading = ref(false)
// 错误弹窗相关
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
// 成功弹窗相关
const showSuccess = ref(false)
const successTitle = ref('成功')
const successMessage = ref('密码重置成功!')
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
const showSuccessMessage = (message, title = '成功') => {
successMessage.value = message
successTitle.value = title
showSuccess.value = true
}
// 验证表单是否有效
const isFormValid = computed(() => {
return newPassword.value.trim() &&
confirmPassword.value.trim() &&
newPassword.value === confirmPassword.value &&
newPassword.value.length >= 4 &&
newPassword.value.length <= 20
})
const handleResetPassword = async () => {
if (!isFormValid.value || loading.value) {
return
}
// 获取token
const token = route.params.token
if (!token) {
showErrorMessage('无效的重置链接')
return
}
loading.value = true
try {
// 调用重置密码API
await resetPassword(token, newPassword.value)
// 密码重置成功后显示提示并跳转到登录页
showSuccessMessage('密码重置成功!')
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
if (hasValidToken()) {
logoutUser()
}
router.push('/backend/login')
}, 1500)
} catch (error) {
console.error('重置密码失败:', error)
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || '重置密码失败,请重试'
showErrorMessage(errorMessage)
} finally {
loading.value = false
}
}
const handleSuccessClose = () => {
showSuccess.value = false
goToHome()
}
const goToHome = () => {
if (hasValidToken()) {
logoutUser()
}
router.push('/backend/login')
// router.push('/')
}
onMounted(() => {
// 检查是否有token参数
const token = route.params.token
if (!token) {
showErrorMessage('无效的重置链接')
}
})
</script>
<style scoped>
.reset-password-container {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
.bg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.bg-container img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: left;
}
.content-container {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
z-index: 2;
padding: 2rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.form-content {
width: 100%;
margin-top: 48px;
display: flex;
flex-direction: column;
align-items: center;
}
.reset-form {
width: 340px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.10);
padding: 36px 32px 28px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.form-title {
font-size: 26px;
font-weight: bold;
color: #222;
margin-bottom: 28px;
letter-spacing: 2px;
}
.reset-form-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 18px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-container label {
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrapper input {
width: 100%;
padding: 12px 16px;
padding-right: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
outline: none;
box-sizing: border-box;
}
.password-input-wrapper input:focus {
border: 1.5px solid #409eff;
background: #fff;
}
.password-input-wrapper input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #666;
font-size: 16px;
transition: color 0.3s;
z-index: 1;
}
.password-toggle:hover {
color: #409eff;
}
.password-tips {
margin-top: 12px;
text-align: left;
}
.password-tips p {
margin: 0;
font-size: 12px;
color: #666;
}
.reset-button {
margin-top: 10px;
}
.reset-button button {
width: 100%;
height: 42px;
background: linear-gradient(90deg, #409eff 0%, #6dd5fa 100%);
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64,158,255,0.10);
transition: background 0.2s;
}
.reset-button button:hover:not(:disabled) {
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
}
.reset-button button:disabled {
background: #a0cfff;
cursor: not-allowed;
}
.back-link {
margin-top: 12px;
text-align: center;
}
.back-link a {
color: #409eff;
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-link a:hover {
color: #1a73e8;
text-decoration: underline;
}
.bottom-text {
position: absolute;
left: 24px;
bottom: 24px;
z-index: 2;
color: #fff;
font-size: 16px;
font-weight: 300;
letter-spacing: 1px;
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
background: rgba(0, 0, 0, 0.18);
border-radius: 8px;
padding: 6px 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
user-select: none;
transition: background 0.3s;
}
.bottom-text p {
margin: 0;
opacity: 0.92;
}
@media (max-width: 600px) {
.content-container {
width: 100vw;
height: 100vh;
min-width: 0;
left: 0;
top: 0;
right: 0;
bottom: 0;
transform: none;
border-radius: 0;
background: rgba(0,0,0,0.18);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
padding: 0 8px;
justify-content: flex-start;
}
.form-content {
margin-top: 64px;
width: 100%;
align-items: center;
}
.reset-form {
width: 90%;
max-width: 340px;
padding: 24px 20px 20px 20px;
}
.password-input-wrapper input {
height: 38px;
font-size: 14px;
}
.reset-button button {
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -27,6 +27,9 @@
<li :class="{active: currentAdminView === 'admin-edit-user-privilege'}">
<a @click="selectAdminView('admin-edit-user-privilege')">管理员修改用户权限</a>
</li>
<li :class="{active: currentAdminView === 'admin-edit-user-password'}">
<a @click="selectAdminView('admin-edit-user-password')">管理员把你密码扬了</a>
</li>
</ul>
</li>
<li>
@@ -71,6 +74,7 @@
<AdminEditUserPrivilege v-if="currentAdminView === 'admin-edit-user-privilege'" />
<AffairManagement v-if="currentAdminView === 'affair-management'" />
<TempPrivilegeReview v-if="currentAdminView === 'permission-review'" />
<AdminChangesPwd v-if="currentAdminView === 'admin-edit-user-password'" />
</div>
</div>
</div>
@@ -83,6 +87,7 @@ import { getUserInfo } from '@/utils/jwt'
import AdminEditUserPrivilege from '@/components/backend/AdminEditUserPrivilege.vue'
import AffairManagement from '@/components/backend/AffairManagement.vue'
import TempPrivilegeReview from '@/components/backend/TempPrivilegeReview.vue'
import AdminChangesPwd from '@/components/backend/AdminChangesPwd.vue'
const router = useRouter()
const hasToken = ref(false)

View File

@@ -11,8 +11,20 @@
返回主界面
</button>
<div class="form-content">
<LoginModule ref="loginModuleRef" v-if="!showRegister" @register="showRegister = true" />
<RegisterModule v-else @login="handleSwitchToLogin" />
<LoginModule
ref="loginModuleRef"
v-if="!showRegister && !showForget"
@register="showRegister = true"
@forget="handleSwitchToForget"
/>
<RegisterModule
v-else-if="showRegister"
@login="handleSwitchToLogin"
/>
<ForgetModule
v-else-if="showForget"
@login="handleSwitchToLogin"
/>
</div>
</div>
</div>
@@ -25,15 +37,19 @@ import { hasValidToken } from '@/utils/jwt'
import loginBg from '@/assets/login_1.jpg'
import loginBg1 from '@/assets/login_2.jpg'
import loginBg3 from '@/assets/login_3.jpg'
import loginBg4 from '@/assets/login_4.jpg'
import loginBg5 from '@/assets/login_5.jpg'
import ForgetModule from "@/components/forget_module.vue";
import LoginModule from '@/components/login_module.vue'
import RegisterModule from '@/components/register_module.vue'
const images = [loginBg, loginBg1,loginBg3]
const images = [loginBg, loginBg1,loginBg3, loginBg4,loginBg5]
const randomIndex = Math.floor(Math.random() * images.length)
const bgImg = ref(images[randomIndex])
const router = useRouter()
const showRegister = ref(false)
const showForget = ref(false)
const loginModuleRef = ref(null)
const handleBack = () => {
@@ -43,6 +59,7 @@ const handleBack = () => {
// 处理从注册模块切换到登录模块
const handleSwitchToLogin = () => {
showRegister.value = false
showForget.value = false
// 在下一个tick中重置登录表单确保组件已经渲染
setTimeout(() => {
if (loginModuleRef.value) {
@@ -51,6 +68,12 @@ const handleSwitchToLogin = () => {
}, 0)
}
// 处理切换到忘记密码页面
const handleSwitchToForget = () => {
showForget.value = true
showRegister.value = false
}
// 检查登录状态,如果已登录则跳转到首页
onMounted(() => {
if (hasValidToken()) {

View File

@@ -3,61 +3,62 @@
<div class="page-header">
<h1>添加新赛事</h1>
<div class="header-actions">
<button class="btn-excel" @click="handleExcelImport">
<i class="fas fa-file-excel"></i>
通过表格添加
</button>
<!-- <button class="btn-excel" @click="handleExcelImport">-->
<!-- <i class="fas fa-file-excel"></i>-->
<!-- 通过表格添加-->
<!-- </button>-->
</div>
</div>
<!-- Excel导入弹窗 -->
<div v-if="showExcelDialog" class="excel-dialog-overlay">
<div class="excel-dialog">
<h3>通过Excel导入赛事信息</h3>
<div class="excel-upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleFileDrop"
:class="{ 'is-dragover': isDragover }">
<input type="file" ref="fileInput" accept=".xlsx" @change="handleFileSelect" style="display: none">
<div class="upload-content">
<i class="fas fa-file-excel"></i>
<p>点击或拖拽Excel文件到此处</p>
</div>
</div>
<div v-if="eventPreviewData.length > 0" class="preview-table">
<h4>赛事信息表预览</h4>
<table>
<thead>
<tr>
<th v-for="h in eventPreviewHeaders" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in eventPreviewData" :key="index">
<td v-for="h in eventPreviewHeaders" :key="h">{{ item[h] }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="playerPreviewData.length > 0" class="preview-table">
<h4>选手报名表预览</h4>
<table>
<thead>
<tr>
<th v-for="h in playerPreviewHeaders" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in playerPreviewData" :key="index">
<td v-for="h in playerPreviewHeaders" :key="h">{{ item[h] }}</td>
</tr>
</tbody>
</table>
</div>
<div class="dialog-actions">
<button class="confirm-btn" @click="confirmImport" :disabled="eventPreviewData.length === 0">确认导入</button>
<button class="cancel-btn" @click="closeExcelDialog">取消</button>
</div>
</div>
</div>
<!-- &lt;!&ndash; Excel导入弹窗 &ndash;&gt;-->
<!-- <div v-if="showExcelDialog" class="excel-dialog-overlay">-->
<!-- <div class="excel-dialog">-->
<!-- <h3>通过Excel导入赛事信息</h3>-->
<!-- <div class="excel-upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleFileDrop"-->
<!-- :class="{ 'is-dragover': isDragover }">-->
<!-- <input type="file" ref="fileInput" accept=".xlsx" @change="handleFileSelect" style="display: none">-->
<!-- <div class="upload-content">-->
<!-- <i class="fas fa-file-excel"></i>-->
<!-- <p>点击或拖拽Excel文件到此处</p>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div v-if="eventPreviewData.length > 0" class="preview-table">-->
<!-- <h4>赛事信息表预览</h4>-->
<!-- <table>-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th v-for="h in eventPreviewHeaders" :key="h">{{ h }}</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="(item, index) in eventPreviewData" :key="index">-->
<!-- <td v-for="h in eventPreviewHeaders" :key="h">{{ item[h] }}</td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- <div v-if="playerPreviewData.length > 0" class="preview-table">-->
<!-- <h4>选手报名表预览</h4>-->
<!-- <table>-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th v-for="h in playerPreviewHeaders" :key="h">{{ h }}</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="(item, index) in playerPreviewData" :key="index">-->
<!-- <td v-for="h in playerPreviewHeaders" :key="h">{{ item[h] }}</td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- <div class="dialog-actions">-->
<!-- <button class="confirm-btn" @click="confirmImport" :disabled="eventPreviewData.length === 0">确认导入</button>-->
<!-- <button class="cancel-btn" @click="closeExcelDialog">取消</button>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<form @submit.prevent="handleSubmit" class="contest-form">
<div class="form-group">
@@ -100,14 +101,28 @@
<button type="button" class="cancel-btn" @click="handleCancel">取消</button>
</div>
</form>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament'
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament.js'
import * as XLSX from 'xlsx'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
const router = useRouter()
const showExcelDialog = ref(false)
@@ -129,6 +144,10 @@ const formData = ref({
description: ''
})
// Dialog state
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
// Excel
const excelFieldMap = {
'赛事名称': 'name',
@@ -190,11 +209,11 @@ const handleSubmit = async () => {
// API
await addTournament(tournamentData)
alert('添加赛事成功!')
successDialog.value = { visible: true, message: '添加赛事成功!' }
router.push('/competition')
} catch (error) {
console.error('提交失败:', error)
alert(error.response?.data?.message || '添加赛事失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '添加赛事失败,请重试' }
}
}
@@ -310,7 +329,7 @@ const processExcelFile = (file) => {
}
}
} catch (error) {
alert('Excel文件格式错误请检查文件内容')
errorDialog.value = { visible: true, message: 'Excel文件格式错误请检查文件内容' }
}
}
reader.readAsArrayBuffer(file)
@@ -340,7 +359,7 @@ const confirmImport = async () => {
try {
await addTournament(tournamentData)
alert('赛事导入成功!')
successDialog.value = { visible: true, message: '赛事导入成功!' }
const tournamentList = await getTournamentList()
console.log('获取到的赛事列表:', tournamentList)
@@ -377,16 +396,16 @@ const confirmImport = async () => {
await addSignUp(signUpData)
} catch (error) {
console.error('报名失败:', error)
alert(`报名失败: ${row['队伍名称或者个人参赛名称']}${error.message}`)
errorDialog.value = { visible: true, message: `报名失败: ${row['队伍名称或者个人参赛名称']}${error.message}` }
}
}
alert('选手报名表导入完成!')
successDialog.value = { visible: true, message: '选手报名表导入完成!' }
}
router.push('/competition')
} catch (error) {
console.error('导入失败:', error)
alert(error.response?.data?.message || '赛事导入失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '赛事导入失败,请重试' }
}
closeExcelDialog()
}

View File

@@ -34,6 +34,7 @@
</div>
<select v-model="filterStatus" @change="handleFilter" class="filter-select">
<option value="all">全部状态</option>
<option value="prepare">筹备中</option>
<option value="ongoing">进行中</option>
<option value="finished">已结束</option>
</select>
@@ -49,57 +50,60 @@
</button>
</div>
<div class="table-container" :class="{ 'loading': isLoading }">
<table class="competition-table">
<thead>
<tr>
<th>序号</th>
<th>赛程名称</th>
<th>开始时间</th>
<th>结束时间</th>
<th>状态</th>
<th>组织者</th>
<th>QQ号</th>
<th>赛制类型</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(competition, index) in filteredCompetitions"
:key="index"
class="competition-row"
@click="handleView(competition)">
<td>{{ index + 1 }}</td>
<td class="competition-name">{{ competition.name }}</td>
<td>{{ formatDate(competition.start_time) }}</td>
<td>{{ formatDate(competition.end_time) }}</td>
<td>
<span :class="['status-tag', competition.status]">
{{ competition.status === 'prepare' ? '筹备中' :
competition.status === 'starting' ? '进行中' : '已结束' }}
</span>
</td>
<td>{{ competition.organizer }}</td>
<td>{{ competition.qq_code }}</td>
<td>{{ competition.format === 'single' ? '单败淘汰' :
competition.format === 'double' ? '双败淘汰' : '积分赛' }}</td>
<td class="action-cell">
<button class="action-btn view" @click.stop="handleSignUp(competition)" :disabled="competition.status === 'finish'">
报名
</button>
</td>
</tr>
</tbody>
</table>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载赛程数据...</p>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-overlay">
<i class="fas fa-spinner fa-spin"></i>
<span>加载中...</span>
<!-- 赛程卡片网格 -->
<div v-else class="competition-grid">
<div
v-for="(competition, index) in filteredCompetitions"
:key="index"
class="competition-card"
@click="handleView(competition)"
>
<div class="card-header">
<div class="status-badge" :class="competition.status">
{{ getStatusText(competition.status) }}
</div>
<div class="format-badge">{{ competition.format === 'single' ? '单败' : competition.format === 'double' ? '双败' : '积分' }}</div>
</div>
<div class="card-body">
<h3 class="competition-title">{{ competition.name }}</h3>
<div class="info-row">
<i class="fas fa-user-tie"></i>
<span>主办方{{ competition.organizer }}</span>
</div>
<div class="info-row">
<i class="fab fa-qq"></i>
<span>QQ{{ competition.qq_code }}</span>
</div>
<div class="info-row time-row">
<i class="far fa-calendar-alt"></i>
<span>{{ formatDate(competition.start_time) }} - {{ formatDate(competition.end_time) }}</span>
</div>
</div>
<div class="card-footer">
<button
class="action-btn view"
@click.stop="handleSignUp(competition)"
:disabled="competition.status === 'finish'"
:class="{ 'disabled': competition.status === 'finish' }"
>
{{ competition.status === 'finish' ? '已结束' : '立即报名' }}
</button>
</div>
</div>
<!-- 空状态显示 -->
<div v-else-if="filteredCompetitions.length === 0" class="empty-state">
<div v-if="filteredCompetitions.length === 0" class="empty-state">
<i class="fas fa-calendar-times"></i>
<p>暂无赛程信息</p>
</div>
@@ -110,7 +114,7 @@
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { getTournamentList } from '@/api/tournament'
import { getTournamentList } from '@/api/tournament.js'
const router = useRouter()
@@ -135,9 +139,13 @@ const filteredCompetitions = computed(() => {
//
if (filterStatus.value !== 'all') {
result = result.filter(comp =>
filterStatus.value === 'ongoing' ? comp.status === 'starting' : comp.status === 'finish'
)
if (filterStatus.value === 'ongoing') {
result = result.filter(comp => comp.status === 'starting')
} else if (filterStatus.value === 'finished') {
result = result.filter(comp => comp.status === 'finish')
} else if (filterStatus.value === 'prepare') {
result = result.filter(comp => comp.status === 'prepare')
}
}
return result
@@ -145,9 +153,19 @@ const filteredCompetitions = computed(() => {
//
const formatDate = (date) => {
if (!date) return ''
return date.replace(/\//g, '-')
}
const getStatusText = (status) => {
const map = {
'prepare': '筹备中',
'starting': '进行中',
'finish': '已结束'
}
return map[status] || status
}
const handleSearch = () => {
//
}
@@ -213,60 +231,74 @@ refreshCompetitions()
<style scoped>
.competition-page {
padding: 16px;
padding: 24px;
max-width: 1400px;
margin: 0 auto;
min-height: 100vh;
background-color: #f5f7fa;
}
.page-header {
margin-bottom: 20px;
margin-bottom: 24px;
text-align: center;
}
.page-header h1 {
font-size: 22px;
font-size: 28px;
color: #1a237e;
margin: 0 0 6px 0;
margin: 0 0 8px 0;
font-weight: 600;
}
.header-subtitle {
color: #666;
font-size: 13px;
font-size: 14px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 12px;
gap: 16px;
background: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.left-actions,
.right-actions {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
position: relative;
flex-grow: 1;
width: 240px;
}
.search-box input {
padding: 6px 10px 6px 28px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
width: 100%;
max-width: 220px;
padding: 8px 12px 8px 32px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.search-box input:focus {
border-color: #409EFF;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
outline: none;
}
.search-icon {
position: absolute;
left: 8px;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
@@ -274,215 +306,259 @@ refreshCompetitions()
}
.filter-select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background: white;
min-width: 100px;
min-width: 120px;
cursor: pointer;
transition: all 0.3s;
}
.filter-select:focus {
border-color: #409EFF;
outline: none;
}
.btn-common {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
font-size: 13px;
gap: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
border-radius: 4px;
border: 1px solid #b6d2ff;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-gradient {
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
color: white;
border: none;
box-shadow: 0 4px 12px rgba(65, 107, 223, 0.2);
}
.btn-gradient:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(65, 107, 223, 0.3);
}
.btn-light {
background: white;
background: #f0f7ff;
color: #2563eb;
border: 1px solid #dbeafe;
}
.btn-light:hover {
background: #f5f7fa;
background: #dbeafe;
border-color: #2563eb;
}
.table-container {
/* 网格布局 */
.competition-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.competition-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
margin-bottom: 20px;
position: relative;
min-height: 200px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.competition-table {
width: 100%;
min-width: 800px;
border-collapse: collapse;
.competition-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
border-color: #e0e0e0;
}
.competition-table th,
.competition-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
.card-header {
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f5f5f5;
background: #fafafa;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-badge.prepare { background-color: #fff7e6; color: #fa8c16; border: 1px solid #ffd591; }
.status-badge.starting { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.status-badge.finish { background-color: #f5f5f5; color: #8c8c8c; border: 1px solid #d9d9d9; }
.format-badge {
font-size: 12px;
color: #666;
background: #e6f7ff;
padding: 2px 8px;
border-radius: 10px;
border: 1px solid #bae7ff;
}
.card-body {
padding: 20px;
flex: 1;
}
.competition-title {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #1a237e;
line-height: 1.4;
}
.info-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
color: #666;
font-size: 14px;
}
.info-row i {
width: 16px;
text-align: center;
color: #909399;
}
.time-row {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #f0f0f0;
font-size: 13px;
}
.competition-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #1a237e;
}
.competition-row {
cursor: pointer;
transition: all 0.3s ease;
}
.competition-row:hover {
background-color: #f0f7ff;
transform: translateY(-1px);
}
.competition-name {
font-weight: 500;
color: #1a237e;
}
.status-tag {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
}
.status-tag.prepare { background-color: #e6a23c; color: #fff; }
.status-tag.starting { background-color: #67c23a; color: #fff; }
.status-tag.finish { background-color: #909399; color: #fff; }
.action-cell {
display: flex;
gap: 6px;
.card-footer {
padding: 16px;
border-top: 1px solid #f5f5f5;
text-align: center;
}
.action-btn {
padding: 5px 10px;
border-radius: 4px;
width: 100%;
padding: 10px;
border-radius: 6px;
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: white;
font-size: 13px;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.action-btn:hover {
opacity: 0.9;
}
.action-btn.disabled {
background: #f5f5f5;
color: #999;
cursor: not-allowed;
background-image: none;
}
.empty-state {
padding: 30px;
grid-column: 1 / -1;
padding: 60px;
text-align: center;
font-size: 14px;
color: #909399;
background: white;
border-radius: 12px;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
color: #409EFF;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background-color: #fef0f0;
color: #f56c6c;
padding: 10px 14px;
border-radius: 4px;
padding: 12px 16px;
border-radius: 8px;
display: flex;
gap: 8px;
font-size: 13px;
align-items: center;
gap: 10px;
margin-bottom: 20px;
border: 1px solid #fde2e2;
}
.retry-btn {
margin-left: auto;
padding: 4px 10px;
padding: 4px 12px;
font-size: 12px;
background: #f56c6c;
color: white;
border: none;
border-radius: 3px;
border-radius: 4px;
cursor: pointer;
}
.loading-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #409EFF;
font-size: 14px;
}
.table-container.loading {
opacity: 0.6;
pointer-events: none;
}
.btn-common:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn:disabled {
background: #e0e0e0 !important;
color: #b0b0b0 !important;
cursor: not-allowed;
border: none;
opacity: 1;
}
@media (max-width: 768px) {
.competition-page {
padding: 12px;
padding: 16px;
}
.action-bar {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.left-actions, .right-actions {
flex-direction: column;
gap: 8px;
width: 100%;
}
.table-container {
margin: 0 -12px;
border-radius: 0;
}
.competition-table th, .competition-table td {
padding: 10px;
font-size: 12px;
}
.search-box input, .filter-select {
.search-box {
width: 100%;
max-width: 100%;
}
.status-tag {
font-size: 10px;
padding: 2px 5px;
.filter-select {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,974 @@
<template>
<div class="competition-detail-container">
<!-- 顶部横幅 -->
<div class="detail-header">
<div class="header-content">
<div class="title-section">
<button class="back-btn" @click="goBack">
<i class="fas fa-arrow-left"></i>
</button>
<h1>{{ competition.name }}</h1>
<span class="status-badge" :class="competition.status">
{{ formatStatus(competition.status) }}
</span>
</div>
<div class="info-grid">
<div class="info-item">
<i class="fas fa-trophy"></i>
<span>赛制: {{ competition.format }}</span>
</div>
<div class="info-item">
<i class="fas fa-user-tie"></i>
<span>举办方: {{ competition.organizer }}</span>
</div>
<div class="info-item">
<i class="fas fa-calendar-alt"></i>
<span>时间: {{ formatDate(competition.start_time) }} - {{ formatDate(competition.end_time) }}</span>
</div>
</div>
</div>
</div>
<!-- 主要内容区 -->
<div class="main-content">
<!-- 侧边导航/标签页 -->
<div class="tabs-nav">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
>
<i :class="tab.icon"></i>
{{ tab.name }}
</button>
</div>
<!-- 标签页内容 -->
<div class="tab-content">
<!-- 概览 -->
<div v-if="currentTab === 'overview'" class="tab-pane fade-in">
<div class="overview-card">
<h2>赛事公告</h2>
<div class="markdown-body" v-html="renderedDescription"></div>
</div>
<div class="overview-card" v-if="competition.status === 'finish'">
<h2>最终排名</h2>
<RankContestant :tournamentId="competition.id" />
</div>
</div>
<!-- 对阵图 -->
<div v-if="currentTab === 'bracket'" class="tab-pane fade-in">
<div v-if="competition.status === 'prepare'" class="empty-state">
<i class="fas fa-hourglass-start"></i>
<p>比赛尚未开始请等待组织者生成对阵图</p>
<button v-if="isOrganizer" class="btn-primary" @click="startTournament">
开始比赛并生成对阵
</button>
</div>
<div v-else class="bracket-wrapper">
<TournamentBracket
v-if="competition.format === '单败淘汰'"
:tournamentId="competition.id"
@refreshPlayers="fetchRegisteredPlayers"
/>
<DoubleEliminationBracket
v-else-if="competition.format === '双败淘汰'"
:tournamentId="competition.id"
/>
</div>
</div>
<!-- 参赛人员 -->
<div v-if="currentTab === 'participants'" class="tab-pane fade-in">
<div class="participants-header">
<h2>参赛选手 ({{ registeredPlayers.length }})</h2>
<div class="actions" v-if="isOrganizer && competition.status === 'prepare'">
<button class="btn-secondary" @click="shufflePlayers" title="随机打乱顺序">
<i class="fas fa-random"></i> 随机种子
</button>
<button class="btn-primary" @click="openAddPlayerModal">
<i class="fas fa-plus"></i> 添加选手
</button>
</div>
</div>
<div class="participants-list">
<div v-for="(player, index) in registeredPlayers" :key="player.id" class="participant-card">
<div class="player-rank">种子 #{{ index + 1 }}</div>
<div class="player-info">
<div class="player-name">{{ player.sign_name }}</div>
<div class="player-team" v-if="player.team_name">{{ player.team_name }}</div>
</div>
<div class="player-actions" v-if="isOrganizer && competition.status === 'prepare'">
<button class="icon-btn" @click="movePlayerUp(index)" :disabled="index === 0" title="上移">
<i class="fas fa-arrow-up"></i>
</button>
<button class="icon-btn" @click="movePlayerDown(index)" :disabled="index === registeredPlayers.length - 1" title="下移">
<i class="fas fa-arrow-down"></i>
</button>
<button class="icon-btn delete" @click="confirmRemovePlayer(player)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 设置 (仅组织者) -->
<div v-if="currentTab === 'settings' && isOrganizer" class="tab-pane fade-in">
<div class="settings-card">
<h2>赛事管理</h2>
<form @submit.prevent="handleUpdateTournament">
<div class="setting-group">
<label>赛事名称</label>
<input v-model="editingCompetition.name" type="text" required class="form-input" />
</div>
<div class="setting-group">
<label>举办方</label>
<input v-model="editingCompetition.organizer" type="text" required class="form-input" />
</div>
<div class="setting-group">
<label>开始时间</label>
<input v-model="editingCompetition.start_time" type="date" class="form-input" />
</div>
<div class="setting-group">
<label>结束时间</label>
<input v-model="editingCompetition.end_time" type="date" class="form-input" />
</div>
<div class="setting-group">
<label>赛事状态</label>
<select v-model="editingCompetition.status" class="form-select">
<option value="prepare">筹备中</option>
<option value="starting">进行中</option>
<option value="finish">已结束</option>
</select>
</div>
<div class="setting-actions">
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i> 保存修改
</button>
<button type="button" class="btn-danger" @click="confirmDeleteTournament">
<i class="fas fa-trash-alt"></i> 删除赛事
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 弹窗组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
<!-- 确认删除弹窗 -->
<div v-if="showDeleteConfirm" class="modal-overlay">
<div class="modal-content">
<h3>确认删除</h3>
<p>确定要删除该赛事吗此操作不可恢复</p>
<div class="modal-actions">
<button class="btn-secondary" @click="showDeleteConfirm = false">取消</button>
<button class="btn-danger" @click="handleDeleteTournament">确认删除</button>
</div>
</div>
</div>
<!-- 添加选手弹窗 -->
<div v-if="showAddPlayerModal" class="modal-overlay">
<div class="modal-content">
<h3>添加选手</h3>
<div class="form-group">
<label>选手名称</label>
<input v-model="newPlayerName" placeholder="请输入选手名称" class="form-input" />
</div>
<div class="form-group">
<label>QQ号 (可选)</label>
<input v-model="newPlayerQQ" placeholder="请输入选手QQ号" class="form-input" />
</div>
<div class="modal-actions">
<button class="btn-secondary" @click="showAddPlayerModal = false">取消</button>
<button class="btn-primary" @click="handleAddPlayer">添加</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { marked } from 'marked'
import {
getTournamentList,
updateTournament,
deleteTournament,
getSignUpList,
deleteSignUp,
addSignUpResult,
getSignUpResultList,
updateSignUpResult,
addSignUp
} from '@/api/tournament'
import { getStoredUser } from '@/utils/jwt.js'
import TournamentBracket from '@/components/TournamentBracket.vue'
import DoubleEliminationBracket from '@/components/DoubleEliminationBracket.vue'
import RankContestant from '@/components/RankContestant.vue'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
const route = useRoute()
const router = useRouter()
// 状态变量
const competition = ref({})
const editingCompetition = ref({}) // 用于编辑的副本
const registeredPlayers = ref([])
const isOrganizer = ref(false)
const isLoading = ref(false)
const currentTab = ref('overview')
const showDeleteConfirm = ref(false)
const showAddPlayerModal = ref(false)
const newPlayerName = ref('')
const newPlayerQQ = ref('')
// 弹窗状态
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
// 标签页配置
const tabs = [
{ id: 'overview', name: '概览', icon: 'fas fa-info-circle' },
{ id: 'bracket', name: '对阵图', icon: 'fas fa-sitemap' },
{ id: 'participants', name: '参赛人员', icon: 'fas fa-users' },
{ id: 'settings', name: '设置', icon: 'fas fa-cog' }
]
// Markdown 渲染
const renderedDescription = computed(() => {
return competition.value.description ? marked(competition.value.description) : '暂无描述'
})
// 初始化
onMounted(async () => {
await fetchTournamentDetail()
})
// 监听 competition 变化,更新 editingCompetition
watch(competition, (newVal) => {
if (newVal) {
editingCompetition.value = { ...newVal }
// 格式化日期以适应 input[type="date"]
if (editingCompetition.value.start_time) {
editingCompetition.value.start_time = formatDateForInput(editingCompetition.value.start_time)
}
if (editingCompetition.value.end_time) {
editingCompetition.value.end_time = formatDateForInput(editingCompetition.value.end_time)
}
}
}, { deep: true })
// 返回上一页
const goBack = () => {
router.push('/competition')
}
// 获取赛事详情
const fetchTournamentDetail = async () => {
try {
isLoading.value = true
const tournamentId = parseInt(route.query.id)
const data = await getTournamentList()
const tournament = data.find(item => item.id === tournamentId)
if (tournament) {
competition.value = {
...tournament,
format: formatType(tournament.format)
}
// 权限判断
const user = getStoredUser()
if (user && user.qq_code && tournament.qq_code) {
isOrganizer.value = String(user.qq_code) === String(tournament.qq_code)
}
// 获取报名玩家
await fetchRegisteredPlayers()
} else {
errorDialog.value = { visible: true, message: '未找到赛事信息' }
router.push('/competition')
}
} catch (error) {
console.error('获取赛事详情失败:', error)
errorDialog.value = { visible: true, message: '获取赛事详情失败' }
} finally {
isLoading.value = false
}
}
// 获取报名玩家列表
const fetchRegisteredPlayers = async () => {
try {
const res = await getSignUpList()
// 过滤出当前赛事的报名者
registeredPlayers.value = res.filter(item =>
String(item.tournament_id) === String(competition.value.id)
)
} catch (error) {
console.error('获取报名列表失败:', error)
}
}
// 格式化赛制
const formatType = (type) => {
const map = {
'single_elimination': '单败淘汰',
'double_elimination': '双败淘汰',
'round_robin': '循环赛'
}
return map[type] || type
}
// 格式化日期 (用于显示)
const formatDate = (dateStr) => {
if (!dateStr) return '待定'
return new Date(dateStr).toLocaleDateString()
}
// 格式化日期 (用于 input type="date")
const formatDateForInput = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 格式化状态
const formatStatus = (status) => {
const map = {
'prepare': '筹备中',
'starting': '进行中',
'finish': '已结束'
}
return map[status] || status
}
// 随机打乱选手
const shufflePlayers = () => {
registeredPlayers.value.sort(() => Math.random() - 0.5)
successDialog.value = { visible: true, message: '选手顺序已随机打乱' }
}
// 上移选手
const movePlayerUp = (index) => {
if (index > 0) {
const temp = registeredPlayers.value[index]
registeredPlayers.value[index] = registeredPlayers.value[index - 1]
registeredPlayers.value[index - 1] = temp
}
}
// 下移选手
const movePlayerDown = (index) => {
if (index < registeredPlayers.value.length - 1) {
const temp = registeredPlayers.value[index]
registeredPlayers.value[index] = registeredPlayers.value[index + 1]
registeredPlayers.value[index + 1] = temp
}
}
// 打开添加选手弹窗
const openAddPlayerModal = () => {
newPlayerName.value = ''
newPlayerQQ.value = ''
showAddPlayerModal.value = true
}
// 添加选手
const handleAddPlayer = async () => {
if (!newPlayerName.value.trim()) {
errorDialog.value = { visible: true, message: '请输入选手名称' }
return
}
try {
await addSignUp({
tournament_id: competition.value.id,
tournament_name: competition.value.name,
sign_name: newPlayerName.value,
qq: newPlayerQQ.value || '', // 关联QQ
contact_info: ''
})
await fetchRegisteredPlayers()
showAddPlayerModal.value = false
successDialog.value = { visible: true, message: '添加成功' }
} catch (error) {
console.error('添加选手失败:', error)
errorDialog.value = { visible: true, message: '添加选手失败' }
}
}
// 确认删除选手
const confirmRemovePlayer = async (player) => {
if (confirm(`确定要移除选手 ${player.sign_name} 吗?`)) {
try {
await deleteSignUp(player.id)
await fetchRegisteredPlayers()
successDialog.value = { visible: true, message: '移除成功' }
} catch (error) {
console.error('移除选手失败:', error)
errorDialog.value = { visible: true, message: '移除选手失败' }
}
}
}
// 开始比赛
const startTournament = async () => {
if (registeredPlayers.value.length < 2) {
errorDialog.value = { visible: true, message: '至少需要2名选手才能开始比赛' }
return
}
try {
// 1. 更新赛事状态
await updateTournament(competition.value.id, {
...competition.value,
status: 'starting'
})
competition.value.status = 'starting'
// 2. 生成对阵
await generateOpponentsForAllPlayers()
successDialog.value = { visible: true, message: '比赛已开始,对阵图生成中...' }
currentTab.value = 'bracket'
} catch (error) {
console.error('开始比赛失败:', error)
errorDialog.value = { visible: true, message: '开始比赛失败' }
}
}
// 生成标准种子顺序
const getStandardBracketOrder = (n) => {
if (n <= 0) return [];
let rounds = Math.ceil(Math.log2(n));
let bracket = [1, 2];
for (let i = 1; i < rounds; i++) {
let nextBracket = [];
let sum = Math.pow(2, i + 1) + 1;
for (let j = 0; j < bracket.length; j++) {
nextBracket.push(bracket[j]);
nextBracket.push(sum - bracket[j]);
}
bracket = nextBracket;
}
return bracket;
}
// 生成对阵逻辑 (基于当前列表顺序作为种子顺序)
const generateOpponentsForAllPlayers = async () => {
const players = registeredPlayers.value
const totalPlayers = players.length
// 计算需要的总槽位数最近的2的幂
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(totalPlayers)))
// 获取标准种子顺序
const order = getStandardBracketOrder(totalPlayers)
// 遍历种子顺序,每两个一组生成对阵
for (let i = 0; i < order.length; i += 2) {
const seedA = order[i]
const seedB = order[i+1]
// 注意seed是1-based数组是0-based
const pA = players[seedA - 1]
const pB = players[seedB - 1]
if (pA && pB) {
// pA vs pB
await createMatchRecord(pA, pB.sign_name)
await createMatchRecord(pB, pA.sign_name)
} else if (pA && !pB) {
// pA vs 轮空
await createMatchRecord(pA, '轮空')
} else if (!pA && pB) {
// pB vs 轮空 (理论上不应该发生,因为我们按顺序填充)
await createMatchRecord(pB, '轮空')
}
}
}
// 创建比赛记录辅助函数
const createMatchRecord = async (player, rivalName) => {
try {
await addSignUpResult({
tournament_id: competition.value.id,
tournament_name: competition.value.name,
team_name: player.team_name || null,
sign_name: player.sign_name,
win: '0',
lose: '0',
status: 'tie',
round: '0',
rival_name: rivalName,
qq_code: player.qq || '' // 关联QQ
})
} catch (error) {
console.error(`创建比赛记录失败 (${player.sign_name} vs ${rivalName}):`, error)
}
}
// 更新赛事信息
const handleUpdateTournament = async () => {
try {
// 反向映射 format
const formatMap = {
'单败淘汰': 'single_elimination',
'双败淘汰': 'double_elimination',
'循环赛': 'round_robin'
}
const rawFormat = formatMap[editingCompetition.value.format] || editingCompetition.value.format
const updateData = {
...editingCompetition.value,
format: rawFormat,
// 确保日期格式正确 (YYYY/MM/DD)
start_time: editingCompetition.value.start_time ? editingCompetition.value.start_time.replace(/-/g, '/') : '',
end_time: editingCompetition.value.end_time ? editingCompetition.value.end_time.replace(/-/g, '/') : ''
}
await updateTournament(competition.value.id, updateData)
// 更新本地状态
competition.value = { ...editingCompetition.value }
successDialog.value = { visible: true, message: '赛事信息已更新' }
} catch (error) {
console.error('更新赛事失败:', error)
errorDialog.value = { visible: true, message: '更新赛事失败' }
}
}
// 确认删除赛事
const confirmDeleteTournament = () => {
showDeleteConfirm.value = true
}
// 删除赛事
const handleDeleteTournament = async () => {
try {
await deleteTournament(competition.value.id)
showDeleteConfirm.value = false
router.push('/competition')
} catch (error) {
console.error('删除赛事失败:', error)
errorDialog.value = { visible: true, message: '删除赛事失败' }
}
}
</script>
<style scoped>
.competition-detail-container {
min-height: 100vh;
background: #f5f7fa;
display: flex;
flex-direction: column;
}
.detail-header {
background: white;
padding: 20px 40px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
z-index: 10;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
}
.title-section {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.back-btn {
border: none;
background: none;
font-size: 20px;
color: #606266;
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: all 0.3s;
}
.back-btn:hover {
background: #f0f2f5;
color: #409EFF;
}
h1 {
margin: 0;
font-size: 24px;
color: #303133;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.prepare { background: #e6a23c; color: white; }
.status-badge.starting { background: #67c23a; color: white; }
.status-badge.finish { background: #909399; color: white; }
.info-grid {
display: flex;
gap: 30px;
color: #606266;
font-size: 14px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.main-content {
flex: 1;
display: flex;
max-width: 1200px;
margin: 20px auto;
width: 100%;
gap: 20px;
padding: 0 20px;
}
.tabs-nav {
width: 200px;
display: flex;
flex-direction: column;
gap: 10px;
}
.tab-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border: none;
background: white;
border-radius: 8px;
color: #606266;
cursor: pointer;
transition: all 0.3s;
text-align: left;
font-size: 15px;
}
.tab-btn:hover {
background: #f5f7fa;
color: #409EFF;
}
.tab-btn.active {
background: #409EFF;
color: white;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.tab-content {
flex: 1;
background: white;
border-radius: 12px;
padding: 30px;
min-height: 600px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
.tab-pane {
height: 100%;
}
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.overview-card {
margin-bottom: 30px;
}
.overview-card h2 {
font-size: 18px;
color: #303133;
margin-bottom: 15px;
border-left: 4px solid #409EFF;
padding-left: 10px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #909399;
gap: 20px;
}
.empty-state i {
font-size: 48px;
color: #dcdfe6;
}
.bracket-wrapper {
height: 100%;
min-height: 600px;
}
.participants-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.participants-header h2 {
margin: 0;
font-size: 18px;
color: #303133;
}
.participants-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.participant-card {
display: flex;
align-items: center;
padding: 15px;
background: #f9fafc;
border: 1px solid #ebeef5;
border-radius: 8px;
transition: all 0.3s;
}
.participant-card:hover {
border-color: #c6e2ff;
background: #ecf5ff;
}
.player-rank {
width: 80px;
font-weight: bold;
color: #909399;
}
.player-info {
flex: 1;
}
.player-name {
font-weight: 600;
color: #303133;
}
.player-team {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.player-actions {
display: flex;
gap: 8px;
}
.icon-btn {
width: 32px;
height: 32px;
border: none;
background: white;
border-radius: 4px;
cursor: pointer;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
border: 1px solid #dcdfe6;
}
.icon-btn:hover:not(:disabled) {
color: #409EFF;
border-color: #c6e2ff;
}
.icon-btn:disabled {
color: #c0c4cc;
cursor: not-allowed;
background: #f5f7fa;
}
.icon-btn.delete:hover {
color: #F56C6C;
border-color: #fbc4c4;
}
.settings-card {
max-width: 600px;
}
.setting-group {
margin-bottom: 20px;
}
.setting-group label {
display: block;
margin-bottom: 8px;
color: #606266;
font-weight: 500;
}
.form-input, .form-select {
width: 100%;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-input:focus, .form-select:focus {
border-color: #409EFF;
outline: none;
}
.setting-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s;
}
.btn-primary {
background: #409EFF;
color: white;
border: none;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-secondary {
background: white;
border: 1px solid #dcdfe6;
color: #606266;
}
.btn-secondary:hover {
border-color: #409EFF;
color: #409EFF;
}
.btn-danger {
background: #F56C6C;
color: white;
border: none;
}
.btn-danger:hover {
background: #f78989;
}
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: white;
width: 400px;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
color: #303133;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #606266;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
</style>

View File

@@ -124,13 +124,27 @@
<button class="btn-submit" @click="handleSubmit">提交报名</button>
</div>
</div>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { addSignUp } from '@/api/tournament'
import {addSignUp, addSignUpResult} from '@/api/tournament.js'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
defineOptions({
name: 'CompetitionSignUp'
@@ -158,6 +172,10 @@ const signupForm = ref({
qq: ''
})
// Dialog state
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
const formatDate = (date) => {
if (!date) return ''
// //--
@@ -190,12 +208,12 @@ const handleSubmit = async () => {
//
if (signupForm.value.type === 'teamname') {
if (!signupForm.value.teamName || !signupForm.value.username) {
alert('请填写完整的队伍信息')
errorDialog.value = { visible: true, message: '请填写完整的队伍信息' }
return
}
} else {
if (!signupForm.value.username) {
alert('请填写完整的个人信息')
errorDialog.value = { visible: true, message: '请填写完整的个人信息' }
return
}
}
@@ -203,61 +221,67 @@ const handleSubmit = async () => {
// sign_name
const signName = signupForm.value.username.trim()
if (!signName) {
alert('参赛人员名称不能为空')
errorDialog.value = { visible: true, message: '参赛人员名称不能为空' }
return
}
if (signName.length < 2) {
alert('参赛人员名称至少需要2个字符')
errorDialog.value = { visible: true, message: '参赛人员名称至少需要2个字符' }
return
}
if (signName.length > 20) {
alert('参赛人员名称不能超过20个字符')
errorDialog.value = { visible: true, message: '参赛人员名称不能超过20个字符' }
return
}
// 线
if (!/^[\u4e00-\u9fa5a-zA-Z0-9_]+$/.test(signName)) {
alert('参赛人员名称只能包含中文、英文、数字和下划线')
errorDialog.value = { visible: true, message: '参赛人员名称只能包含中文、英文、数字和下划线' }
return
}
//
if (/^\d+$/.test(signName)) {
alert('参赛人员名称不能为纯数字')
errorDialog.value = { visible: true, message: '参赛人员名称不能为纯数字' }
return
}
// 线
if (/^_+$/.test(signName)) {
alert('参赛人员名称不能为纯下划线')
errorDialog.value = { visible: true, message: '参赛人员名称不能为纯下划线' }
return
}
try {
//
if (!competitionInfo.value.id || !competitionInfo.value.name) {
alert('比赛信息不完整,请返回重试')
errorDialog.value = { visible: true, message: '比赛信息不完整,请返回重试' }
return
}
const submitData = {
id: parseInt(competitionInfo.value.id),
tournament_name: competitionInfo.value.name,
tournament_id: parseInt(competitionInfo.value.id),
type: signupForm.value.type,
team_name: signupForm.value.type === 'teamname' ? signupForm.value.teamName : '',
sign_name: signName,
teamname: signupForm.value.type === 'teamname' ? signupForm.value.teamName : ' ',
username: signupForm.value.username,
faction: signupForm.value.faction,
qq_code: String(competitionInfo.value.qq_code) // qq_code
qq: signupForm.value.qq
}
const signupResultData = {
tournament_id: parseInt(competitionInfo.value.id),
tournament_name: competitionInfo.value.name,
team_name: signupForm.value.type === 'teamname' ? signupForm.value.teamName : ' ',
sign_name: signupForm.value.username,
win: '0',
lose: '0',
status: 'tie',
round: '0',
rival_name: ''
}
console.log('提交的报名数据:', submitData)
const result = await addSignUp(submitData)
console.log('报名结果:', result)
if (result.signup && result.result) {
alert('报名成功!')
const resultSignup = await addSignUpResult(signupResultData)
console.log('提交的报名数据:', submitData, signupResultData)
console.log('报名结果:', result, resultSignup)
successDialog.value = { visible: true, message: '报名成功!' }
setTimeout(() => {
router.push('/competition')
} else {
console.error('报名结果不完整:', result)
throw new Error('报名数据不完整,请重试')
}
}, 1500)
} catch (error) {
console.error('报名失败:', error)
console.error('错误详情:', {
@@ -268,13 +292,13 @@ const handleSubmit = async () => {
//
if (error.message.includes('返回数据为空')) {
alert('服务器返回数据为空,请稍后重试')
errorDialog.value = { visible: true, message: '服务器返回数据为空,请稍后重试' }
} else if (error.message.includes('数据不完整')) {
alert('报名数据不完整,请重试')
errorDialog.value = { visible: true, message: '报名数据不完整,请重试' }
} else if (error.message.includes('网络连接')) {
alert('网络连接失败,请检查网络后重试')
errorDialog.value = { visible: true, message: '网络连接失败,请检查网络后重试' }
} else {
alert(error.message || '报名失败,请稍后重试')
errorDialog.value = { visible: true, message: error.message || '报名失败,请稍后重试' }
}
}
}

View File

@@ -9,6 +9,7 @@ const PrivilegeRequestDialog = defineAsyncComponent(() => import('@/components/P
const SuccessDialog = defineAsyncComponent(() => import('@/components/SuccessDialog.vue'))
const ErrorDialog = defineAsyncComponent(() => import('@/components/ErrorDialog.vue'))
const ChangeUsernameDialog = defineAsyncComponent(() => import('@/components/ChangeUsernameDialog.vue'))
const ChangePasswordDialog = defineAsyncComponent(() => import('@/components/ChangePasswordDialog.vue'))
const isLoggedIn = computed(() => {
return !!localStorage.getItem('access_token') && !!currentUserData.value
@@ -210,6 +211,7 @@ const privilegeDialogName = ref('')
const privilegeDialogKey = ref('')
const successDialog = ref({ visible: false, message: '' })
const changeUsernameDialogVisible = ref(false)
const changePasswordDialogVisible = ref(false)
const privilegeDisplayNames = {
'lv-admin': '管理员',
@@ -284,6 +286,12 @@ function showChangeUsernameDialog() {
showDropdown.value = false
}
// 显示修改密码对话框
function showChangePasswordDialog() {
changePasswordDialogVisible.value = true
showDropdown.value = false
}
// 处理修改用户名成功
function handleUsernameChangeSuccess(newUsername) {
if (currentUserData.value) {
@@ -297,17 +305,32 @@ function handleUsernameChangeError(errorMessage) {
errorDialogMessage.value = errorMessage
errorDialogVisible.value = true
}
// 处理修改密码成功
function handlePasswordChangeSuccess(message) {
successDialog.value = { visible: true, message: message }
}
// 处理修改密码错误
function handlePasswordChangeError(errorMessage) {
errorDialogMessage.value = errorMessage
errorDialogVisible.value = true
// 关闭修改密码弹窗
changePasswordDialogVisible.value = false
}
</script>
<template>
<div class="app">
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">红色警戒3数据分析中心</div>
<a href="/" class="nav-brand">红色警戒3工具平台</a>
<!-- <div class="nav-brand">红色警戒3数据分析中心</div>-->
<button class="mobile-menu-toggle" @click="toggleMobileMenu">
<i class="fas fa-bars"></i>
</button>
<div class="nav-left" :class="{ active: showMobileMenu }">
<a href="/" class="nav-link">首页</a>
<!-- 地图 一级菜单 -->
<div class="nav-dropdown">
<span class="nav-link">地图与作者推荐</span>
@@ -322,41 +345,16 @@ function handleUsernameChangeError(errorMessage) {
<span class="nav-link">地形与纹理</span>
<div class="dropdown-content">
<router-link to="/terrain" class="nav-link">地形图列表</router-link>
<!-- <router-link v-if="isLoggedIn" to="/terrainGenerate" class="nav-link" @click.prevent="handleNavClick('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-competitor'])">地形纹理合成工具</router-link>-->
<router-link to="/terrainGenerate" class="nav-link" @click.prevent="handleNavClick('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-competitor'])">地形纹理合成工具</router-link>
</div>
</div>
<!-- <template v-if="isLoggedIn">-->
<!-- &lt;!&ndash; 在线工具 一级菜单 &ndash;&gt;-->
<!-- <div class="nav-dropdown">-->
<!-- <span class="nav-link">在线工具</span>-->
<!-- <div class="dropdown-content">-->
<!-- <router-link to="/weapon-match" class="nav-link" @click.prevent="handleNavClick('/weapon-match', ['lv-admin','lv-mod'])">Weapon 匹配</router-link>-->
<!-- <router-link to="/PIC2TGA" class="nav-link" @click.prevent="handleNavClick('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-competitor'])">在线转tga工具</router-link>-->
<!-- </div>-->
<!-- </div>-->
<!-- &lt;!&ndash; 赛事信息 一级菜单 &ndash;&gt;-->
<!-- <div class="nav-dropdown">-->
<!-- <span class="nav-link">赛事信息</span>-->
<!-- <div class="dropdown-content">-->
<!--&lt;!&ndash; <router-link to="/competition" class="nav-link" @click.prevent="handleNavClick('/competition', ['lv-admin','lv-competitor'])">赛程信息</router-link>&ndash;&gt;-->
<!-- <router-link to="/competition" class="nav-link">赛程信息</router-link>-->
<!-- </div>-->
<!-- </div>-->
<!-- &lt;!&ndash; 公共信息区 一级菜单 &ndash;&gt;-->
<!-- <div class="nav-dropdown">-->
<!-- <span class="nav-link">公共信息区</span>-->
<!-- <div class="dropdown-content">-->
<!-- <router-link to="/demands" class="nav-link">办事大厅</router-link>-->
<!-- </div>-->
<!-- </div>-->
<!-- </template>-->
<!-- 需要登陆才能访问如果没有登陆则点击跳转到登陆页面-->
<!-- 如果登陆了才能执行权限判断-->
<div class="nav-dropdown">
<span class="nav-link">在线工具</span>
<div class="dropdown-content">
<router-link to="/weapon-match" class="nav-link" @click.prevent="handleNavClick('/weapon-match', ['lv-admin','lv-mod'])">Weapon 匹配</router-link>
<router-link to="/configEditor" class="nav-link" @click.prevent="handleNavClick('/configEditor', ['lv-admin','lv-mod'])">Config.xml 编辑器</router-link>
<router-link to="/PIC2TGA" class="nav-link" @click.prevent="handleNavClick('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-competitor'])">在线转tga工具</router-link>
</div>
</div>
@@ -365,7 +363,7 @@ function handleUsernameChangeError(errorMessage) {
<span class="nav-link">赛事信息</span>
<div class="dropdown-content">
<!-- <router-link to="/competition" class="nav-link" @click.prevent="handleNavClick('/competition', ['lv-admin','lv-competitor'])">赛程信息</router-link>-->
<router-link to="/competition" class="nav-link" @click.prevent="handleNavClick('/competition')">赛程信息</router-link>
<router-link to="competition" class="nav-link" @click.prevent="handleNavClick('/competition')">赛程信息</router-link>
</div>
</div>
<!-- 公共信息区 一级菜单 -->
@@ -375,8 +373,12 @@ function handleUsernameChangeError(errorMessage) {
<router-link to="/demands" class="nav-link" @click.prevent="handleNavClick('/demands')">办事大厅</router-link>
</div>
</div>
<!-- 版本更新 -->
<div class="nav-dropdown">
<router-link to="/version" class="nav-link">版本信息</router-link>
</div>
</div>
<div class="nav-right" :class="{ active: showMobileMenu }">
<router-link v-if="!isLoggedIn" to="/backend/login" class="nav-link login-btn">
<i class="fas fa-user"></i>
@@ -392,6 +394,10 @@ function handleUsernameChangeError(errorMessage) {
<i class="fas fa-edit"></i>
修改用户名
</div>
<div class="dropdown-item" @click.stop="showChangePasswordDialog">
<i class="fas fa-lock"></i>
修改密码
</div>
<div v-if="isAdmin" class="dropdown-item" @click.stop="router.push('/backend/dashboard'); showDropdown = false">
<i class="fas fa-cog"></i>
管理后台
@@ -412,33 +418,39 @@ function handleUsernameChangeError(errorMessage) {
<div class="footer-top">
<div class="footer-brand">
<img src="../assets/logo.png" class="footer-logo">
<span class="footer-title">红色警戒3数据分析中心</span>
<span class="footer-title">红色警戒3工具平台</span>
</div>
</div>
<div class="footer-bottom">
<p>Byz解忧杂货铺</p>
<div>
<p>Byz解忧杂货铺</p>
</div>
<p class="beian">
<a href="https://beian.miit.gov.cn/" rel="noreferrer" target="_blank">注册号 : 京ICP备2025120142号-1</a>
</p>
<p class="beian">
<img src="../assets/备案图标.png" alt="公安备案图标" class="police-icon"/>
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802045768" rel="noreferrer" target="_blank">京公网安备11010802045768号</a>
</p>
</div>
</footer>
<PrivilegeRequestDialog
:visible="privilegeDialogVisible"
:privilegeName="privilegeDialogName"
@close="privilegeDialogVisible = false"
@apply="handlePrivilegeApply"
/>
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialogVisible"
:message="errorDialogMessage"
@close="errorDialogVisible = false"
/>
<!-- <PrivilegeRequestDialog-->
<!-- :visible="privilegeDialogVisible"-->
<!-- :privilegeName="privilegeDialogName"-->
<!-- @close="privilegeDialogVisible = false"-->
<!-- @apply="handlePrivilegeApply"-->
<!-- />-->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ChangeUsernameDialog
:visible="changeUsernameDialogVisible"
:currentUsername="currentUserData?.username || ''"
@@ -446,6 +458,12 @@ function handleUsernameChangeError(errorMessage) {
@success="handleUsernameChangeSuccess"
@error="handleUsernameChangeError"
/>
<ChangePasswordDialog
:visible="changePasswordDialogVisible"
@close="changePasswordDialogVisible = false"
@success="handlePasswordChangeSuccess"
@error="handlePasswordChangeError"
/>
</div>
</template>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1093
src/views/index/Home.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@
error: null,
currentPage: 1,
itemsPerPage: 100,
apiBaseUrl: 'https://api.zybdatasupport.online',
apiBaseUrl: 'http://zybdatasupport.online:8000',
categoryList: [],
selectedCategory: '全部',
};

View File

@@ -0,0 +1,103 @@
<template>
<div class="version-page">
<div class="page-header">
<h1>版本信息</h1>
<div class="header-subtitle">
<span>当前前端应用版本{{ appVersion }}</span>
<br />
<span>当前后端应用版本V.{{ backendVersion }}</span>
<h3>联系邮箱</h3>
<ul>
<li v-for="(email, name) in contactEmails" :key="name">
{{ name }}: {{ email }}
</li>
<li>kuangisa: 1549184870@qq.com</li>
</ul>
</div>
</div>
<h2>更新日志</h2>
<div class="md-content" v-html="mdHtml"></div>
</div>
</template>
<script setup>
import pkg from '../../../package.json'
import { marked } from 'marked'
import versionMd from '@/assets/version.md?raw'
import { getBackendInfo } from '@/api/info'
import { ref, onMounted } from 'vue'
const backendVersion = ref('')
const contactEmails = ref({})
onMounted(async () => {
try {
const info = await getBackendInfo()
backendVersion.value = info.version || '未知'
if (info.contact && info.contact.email) {
try {
const emailString = info.contact.email.replace(/'/g, '"')
contactEmails.value = JSON.parse(emailString)
} catch (e) {
contactEmails.value = {}
}
}
} catch (_) {
backendVersion.value = '获取失败'
}
})
const appVersion = pkg.version || '未知'
const mdHtml = marked.parse(versionMd || '')
</script>
<style scoped>
.page-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.page-header h1 {
margin-bottom: 16px;
}
.header-subtitle {
color: #666;
font-size: 14px;
}
.header-subtitle span {
display: inline-block;
margin-bottom: 8px;
}
.header-subtitle h3 {
margin-top: 16px;
margin-bottom: 8px;
font-size: 16px;
color: #333;
}
.header-subtitle ul {
padding-left: 0;
list-style: none;
}
.header-subtitle li {
margin-bottom: 4px;
}
.md-content {
line-height: 1.8;
}
.md-content h1, .md-content h2, .md-content h3 {
margin: 12px 0 8px;
}
.md-content p {
margin: 8px 0;
}
.md-content ul, .md-content ol {
padding-left: 20px;
}
</style>

View File

@@ -4,6 +4,14 @@ import path from 'path'
export default defineConfig({
plugins: [vue()],
build: {
terserOptions:{
compress:{
drop_console:true,
drop_debugger:true,
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')