修改目录

This commit is contained in:
Kunagisa 2025-05-17 16:18:36 +08:00
parent 8e052811e3
commit 6bacf24d12
33 changed files with 1851 additions and 12787 deletions

View File

@ -4,7 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="./src//js/v3.js"></script>
<title>红色警戒3数据分析中心</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

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

@ -2191,6 +2191,14 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue-tournament-bracket": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/vue-tournament-bracket/-/vue-tournament-bracket-3.0.0.tgz",
"integrity": "sha512-ewugJG94WYzJ+LIyAD1oDIxj5V3RFUGF8+UB9GBJfjhmIzjEGOxKQxNMa2kA+7bsJjydbiAgn4cV6lt+KTGNHQ==",
"dependencies": {
"vue": "^3.2.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@ -1,34 +1,43 @@
{ {
"hash": "884a29ef", "hash": "840712f5",
"configHash": "dbe75e76", "configHash": "a76fca65",
"lockfileHash": "05472006", "lockfileHash": "ab320c33",
"browserHash": "8f3502fc", "browserHash": "dcab2661",
"optimized": { "optimized": {
"axios": { "axios": {
"src": "../../axios/index.js", "src": "../../axios/index.js",
"file": "axios.js", "file": "axios.js",
"fileHash": "ecea6d22", "fileHash": "b46b61da",
"needsInterop": false "needsInterop": false
}, },
"vue": { "vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js", "src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js", "file": "vue.js",
"fileHash": "b7828a52", "fileHash": "a29ac7ab",
"needsInterop": false "needsInterop": false
}, },
"vue-router": { "vue-router": {
"src": "../../vue-router/dist/vue-router.mjs", "src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js", "file": "vue-router.js",
"fileHash": "2fae1e26", "fileHash": "5cd170d9",
"needsInterop": false "needsInterop": false
},
"vue-tournament-bracket": {
"src": "../../vue-tournament-bracket/dist/vue-tournament-bracket.common.js",
"file": "vue-tournament-bracket.js",
"fileHash": "e7817223",
"needsInterop": true
} }
}, },
"chunks": { "chunks": {
"chunk-YBGSFZ7G": { "chunk-2365HCQJ": {
"file": "chunk-YBGSFZ7G.js" "file": "chunk-2365HCQJ.js"
}, },
"chunk-PZ5AY32C": { "chunk-IQSFVINN": {
"file": "chunk-PZ5AY32C.js" "file": "chunk-IQSFVINN.js"
},
"chunk-DZZM6G22": {
"file": "chunk-DZZM6G22.js"
} }
} }
} }

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

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

View File

@ -1,9 +0,0 @@
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
export {
__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

View File

@ -1,3 +1,4 @@
import "./chunk-2365HCQJ.js";
import { import {
computed, computed,
defineComponent, defineComponent,
@ -16,8 +17,8 @@ import {
unref, unref,
watch, watch,
watchEffect watchEffect
} from "./chunk-YBGSFZ7G.js"; } from "./chunk-IQSFVINN.js";
import "./chunk-PZ5AY32C.js"; import "./chunk-DZZM6G22.js";
// node_modules/@vue/devtools-api/lib/esm/env.js // node_modules/@vue/devtools-api/lib/esm/env.js
function getDevtoolsGlobalHook() { function getDevtoolsGlobalHook() {

File diff suppressed because one or more lines are too long

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

@ -1,3 +1,6 @@
import {
compile
} from "./chunk-2365HCQJ.js";
import { import {
BaseTransition, BaseTransition,
BaseTransitionPropsValidators, BaseTransitionPropsValidators,
@ -25,7 +28,6 @@ import {
capitalize, capitalize,
cloneVNode, cloneVNode,
compatUtils, compatUtils,
compile,
computed, computed,
createApp, createApp,
createBaseVNode, createBaseVNode,
@ -168,8 +170,8 @@ import {
withMemo, withMemo,
withModifiers, withModifiers,
withScopeId withScopeId
} from "./chunk-YBGSFZ7G.js"; } from "./chunk-IQSFVINN.js";
import "./chunk-PZ5AY32C.js"; import "./chunk-DZZM6G22.js";
export { export {
BaseTransition, BaseTransition,
BaseTransitionPropsValidators, BaseTransitionPropsValidators,

11
package-lock.json generated
View File

@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"vue-tournament-bracket": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
@ -2850,6 +2851,14 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue-tournament-bracket": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/vue-tournament-bracket/-/vue-tournament-bracket-3.0.0.tgz",
"integrity": "sha512-ewugJG94WYzJ+LIyAD1oDIxj5V3RFUGF8+UB9GBJfjhmIzjEGOxKQxNMa2kA+7bsJjydbiAgn4cV6lt+KTGNHQ==",
"dependencies": {
"vue": "^3.2.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"vue-tournament-bracket": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",

View File

@ -3,183 +3,9 @@
</script> </script>
<template> <template>
<div class="app"> <router-view />
<nav class="navbar">
<div class="nav-container">
<div class="nav-left">
<div class="nav-brand">红色警戒3数据分析中心</div>
<router-link to="/" class="nav-link">最近上传地图</router-link>
<router-link to="/weekly" class="nav-link">热门下载地图</router-link>
<router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
<router-link to="/demands" class="nav-link">办事大厅</router-link>
</div>
</div>
</nav>
<main class="main-content">
<router-view></router-view>
</main>
<footer class="footer">
<div class="footer-bottom">
<p>Byz解忧杂货铺</p>
</div>
</footer>
</div>
</template> </template>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f7fa;
color: #2c3e50;
line-height: 1.6;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
padding: 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 60px;
display: flex;
align-items: center;
}
.nav-left {
display: flex;
align-items: center;
gap: 30px;
}
.nav-brand {
color: white;
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
.nav-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.3s ease;
font-weight: 500;
}
.nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-link.router-link-active {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.main-content {
margin-top: 60px;
padding: 20px;
flex: 1;
max-width: 1400px;
width: 100%;
margin-left: auto;
margin-right: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
padding: 0 15px;
}
.nav-left {
gap: 15px;
}
.nav-brand {
font-size: 1.2rem;
}
.nav-link {
padding: 6px 12px;
}
.main-content {
padding: 15px;
}
}
.footer {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
color: white;
padding: 40px 0 20px;
margin-top: auto;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 40px;
}
.footer-section h3 {
font-size: 1.2rem;
margin-bottom: 15px;
color: white;
}
.footer-section p {
margin: 8px 0;
color: rgba(255, 255, 255, 0.9);
font-size: 0.9rem;
}
.footer-bottom {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-bottom p {
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr;
gap: 30px;
}
.footer {
padding: 30px 0 15px;
}
}
</style> </style>

102
src/api/login.js Normal file
View File

@ -0,0 +1,102 @@
import axios from 'axios'
const API_BASE_URL = 'http://zybdatasupport.online:8000'
// 创建 axios 实例
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000 // 添加超时设置
})
// 设置请求拦截器,自动添加 token
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 添加响应拦截器
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response) {
// 服务器返回错误状态码
console.error('请求错误:', {
status: error.response.status,
data: error.response.data,
config: error.config
})
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误:', error.request)
} else {
// 请求配置出错
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
export const userLogin = async (username, password, server, token) => {
try {
console.log('登录请求参数:', { username, password, server, token })
const response = await axiosInstance.post('/user/login', {
username,
password,
server,
token
})
// 保存 token 到 localStorage
if (response.data.access_token) {
localStorage.setItem('access_token', response.data.access_token)
}
return response.data
} catch (error) {
console.error('登录失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
config: error.config
})
throw error
}
}
export const userRegister = async (qq_code, password, server, token) => {
try {
const requestData = {
qq_code,
password,
server,
token
}
console.log('注册请求URL:', `${API_BASE_URL}/user/register/`)
const response = await axiosInstance.post('/user/register', requestData)
console.log('注册响应数据:', response.data)
return response.data
} catch (error) {
console.error('注册请求失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
url: error.config?.url
})
throw error
}
}

BIN
src/assets/login_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

BIN
src/assets/login_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
src/assets/login_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,3 +1,18 @@
/* 全局样式 */
html, body {
margin: 0;
padding: 0;
min-height: 100vh;
width: 100%;
overflow-x: hidden;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 表格通用样式 */ /* 表格通用样式 */
.table-container { .table-container {
background: white; background: white;

View File

@ -0,0 +1,347 @@
<template>
<div class="login-form">
<div>登陆</div>
<form class="login-form-container" @submit.prevent = "handleLogin">
<div class="input-container">
<label for="username">QQ号</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入QQ号"
@blur="validateUsername"
:class="{ 'error': usernameError }"
/>
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div>
<div class="input-container">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
@blur="validatePassword"
:class="{ 'error': passwordError }"
/>
<span class="error-message" v-if="passwordError">{{ passwordError }}</span>
</div>
<div class="input-container">
<!-- 人机验证 -->
<div id="VAPTCHAContainer" style="width: 100%; height: 36px;">
<div class="VAPTCHA-init-main">
<div class="VAPTCHA-init-loading">
<a href="/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px"
height="60px" viewBox="0 0 24 30"
style="enable-background: new 0 0 50 50; width: 14px; height: 14px; vertical-align: middle"
xml:space="preserve">
<rect x="0" y="9.22656" width="4" height="12.5469" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
<rect x="10" y="5.22656" width="4" height="20.5469" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0.15s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0.15s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
<rect x="20" y="8.77344" width="4" height="13.4531" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0.3s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0.3s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
</svg>
</a>
<span class="VAPTCHA-text">Vaptcha Initializing...</span>
</div>
</div>
</div>
</div>
<div class="login-button">
<button type="submit" :disabled="!isFormValid" >登录</button>
</div>
<div class="register-link">
<a @click.prevent="$emit('register')">注册账号</a>
</div>
</form>
</div>
</template>
<script setup>
import {ref, onMounted, computed} from 'vue'
import { useRouter } from 'vue-router'
import { userLogin } from '../api/login'
const router = useRouter()
const VAPTCHAObj = ref(null)
const username = ref('')
const password = ref('')
//
const usernameError = ref('')
const passwordError = ref('')
const isVaptchaVerified = ref(false)
const validateUsername = () => {
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
if (!/^[/\d/g]+$/.test(username.value)) {
usernameError.value = 'QQ号码只能包含数字'
return false
}
if (username.value.length < 4) {
usernameError.value = 'QQ号码长度不能小于4个字符'
return false
}
usernameError.value = ''
return true
}
const validatePassword = () => {
if (!password.value) {
passwordError.value = '请输入密码'
return false
}
if (password.value.length < 4) {
passwordError.value = '密码长度不能小于4个字符'
return false
}
if (password.value.length > 20) {
passwordError.value = '密码长度不能超过20个字符'
return false
}
passwordError.value = ''
return true
}
const isFormValid = computed(() => {
return !usernameError.value &&
!passwordError.value &&
username.value &&
password.value &&
VAPTCHAObj.value && //
isVaptchaVerified.value //
})
onMounted(() => {
window.vaptcha({
vid: '6828264bdc0ff12924d9bfe3',
mode: 'click',
scene: 1,
container: '#VAPTCHAContainer',
area: 'auto'
}).then(function (obj) {
VAPTCHAObj.value = obj
obj.render()
//
obj.listen('pass', function () {
isVaptchaVerified.value = true
})
//
obj.listen('fail', function () {
isVaptchaVerified.value = false
})
//
obj.listen('close', function () {
isVaptchaVerified.value = false
})
})
})
const handleLogin = async () => {
try {
//
if (!validateForm()) {
return
}
//
if (!VAPTCHAObj.value) {
alert('请完成人机验证')
return
}
const serverToken = await VAPTCHAObj.value.getServerToken()
console.log('获取到的 serverToken:', serverToken)
if (!serverToken || !serverToken.token || !serverToken.server) {
alert('获取验证token失败请重新验证')
return
}
const registerData = {
username: username.value,
password: password.value,
server: serverToken.server,
token: serverToken.token
}
// API
const response = await userLogin(
registerData.username,
registerData.password,
registerData.server,
registerData.token
)
//
if (response.access_token) {
router.push('/dashboard')
} else {
throw new Error('登录失败:未获取到访问令牌')
}
} catch (error) {
console.error('登录失败:', error)
alert(error.response?.data?.message || error.message || '登录失败,请稍后重试')
}
}
const validateForm = () => {
if (!username.value || !password.value) {
alert('请输入用户名和密码')
return false
}
return true
}
</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;
}
.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%);
}
.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;
}
.error-message {
color: #f56c6c;
font-size: 12px;
margin-top: 2px;
}
.input-container input.error {
border-color: #f56c6c;
}
</style>

View File

@ -0,0 +1,383 @@
<template>
<div class="login-form">
<div>注册</div>
<form class="login-form-container" @submit.prevent="handleRegister">
<div class="input-container">
<label for="username">QQ号</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入QQ号"
@blur="validateUsername"
:class="{ 'error': usernameError }"
/>
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div>
<div class="input-container">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
@blur="validatePassword"
:class="{ 'error': passwordError }"
/>
<span class="error-message" v-if="passwordError">{{ passwordError }}</span>
</div>
<div class="input-container">
<label for="confirmPassword">再次输入密码</label>
<input
type="password"
id="confirmPassword"
v-model="confirmPassword"
placeholder="请输入密码"
@blur="validateConfirmPassword"
:class="{ 'error': confirmPasswordError }"
/>
<span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span>
</div>
<div class="input-container">
<!-- 人机验证 -->
<div id="VAPTCHAContainer" style="width: 100%; height: 36px;">
<div class="VAPTCHA-init-main">
<div class="VAPTCHA-init-loading">
<a href="/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px"
height="60px" viewBox="0 0 24 30"
style="enable-background: new 0 0 50 50; width: 14px; height: 14px; vertical-align: middle"
xml:space="preserve">
<rect x="0" y="9.22656" width="4" height="12.5469" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
<rect x="10" y="5.22656" width="4" height="20.5469" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0.15s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0.15s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
<rect x="20" y="8.77344" width="4" height="13.4531" fill="#CCCCCC">
<animate attributeName="height" attributeType="XML" values="5;21;5" begin="0.3s" dur="0.6s"
repeatCount="indefinite"></animate>
<animate attributeName="y" attributeType="XML" values="13; 5; 13" begin="0.3s" dur="0.6s"
repeatCount="indefinite"></animate>
</rect>
</svg>
</a>
<span class="VAPTCHA-text">Vaptcha Initializing...</span>
</div>
</div>
</div>
</div>
<div class="login-button">
<button type="submit" :disabled="!isFormValid">注册</button>
</div>
<div class="register-link">
<a @click.prevent="$emit('login')">返回登陆</a>
</div>
</form>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { userRegister } from "@/api/login.js";
const router = useRouter()
const VAPTCHAObj = ref(null)
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
//
const usernameError = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
//
const validateUsername = () => {
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
//
if (!/^\d+$/.test(username.value)) {
usernameError.value = 'QQ号只能包含数字'
return false
}
usernameError.value = ''
return true
}
const validatePassword = () => {
if (!password.value) {
passwordError.value = '请输入密码'
return false
}
if (password.value.length < 6) {
passwordError.value = '密码长度不能小于6个字符'
return false
}
passwordError.value = ''
return true
}
const validateConfirmPassword = () => {
if (!confirmPassword.value) {
confirmPasswordError.value = '请再次输入密码'
return false
}
if (confirmPassword.value !== password.value) {
confirmPasswordError.value = '两次输入的密码不一致'
return false
}
confirmPasswordError.value = ''
return true
}
const isVaptchaVerified = ref(false)
//
const isFormValid = computed(() => {
return !usernameError.value &&
!passwordError.value &&
!confirmPasswordError.value &&
username.value &&
password.value &&
confirmPassword.value &&
VAPTCHAObj.value && //
isVaptchaVerified.value //
})
//
onMounted(() => {
window.vaptcha({
vid: '6828264bdc0ff12924d9bfe3',
mode: 'click',
scene: 2,
container: '#VAPTCHAContainer',
area: 'auto'
}).then(function (obj) {
VAPTCHAObj.value = obj
obj.render()
//
obj.listen('pass', function () {
isVaptchaVerified.value = true
console.log('验证通过')
})
//
obj.listen('fail', function () {
isVaptchaVerified.value = false
console.log('验证失败')
})
//
obj.listen('close', function () {
isVaptchaVerified.value = false
console.log('验证关闭')
})
})
})
const handleRegister = async () => {
try {
//
if (!validateUsername() || !validatePassword() || !validateConfirmPassword()) {
return
}
//
if (!isVaptchaVerified.value || !VAPTCHAObj.value) {
alert('请完成人机验证')
return
}
// token
try {
const serverToken = await VAPTCHAObj.value.getServerToken()
console.log('获取到的 serverToken:', serverToken)
if (!serverToken || !serverToken.token || !serverToken.server) {
alert('获取验证token失败请重新验证')
return
}
//
const registerData = {
qq_code: username.value,
password: password.value,
server: serverToken.server,
token: serverToken.token
}
console.log('注册请求参数:', registerData)
//
await userRegister(
registerData.qq_code,
registerData.password,
registerData.server,
registerData.token
)
alert('注册成功')
//
emit('login')
} catch (tokenError) {
console.error('获取验证token失败:', tokenError)
alert('获取验证token失败请重新验证')
}
} catch (error) {
console.error('注册失败:', error)
alert(error.message || '注册失败')
}
}
</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:not(:disabled) {
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;
}
</style>

1
src/js/v3.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,35 +2,100 @@ import { createRouter, createWebHistory } from 'vue-router'
const routes = [ const routes = [
{ {
path: '/demands', path: '/dashboard',
name: 'DemandList', redirect: '/backend/dashboard'
component: () => import('../views/DemandList.vue')
}, },
{ {
path: '/', path: '/',
component: () => import('@/views/index.vue'),
children: [
{
path: '',
redirect: '/maps'
},
{
path: 'demands',
name: 'DemandList',
component: () => import('@/views/index/DemandList.vue')
},
{
path: 'maps',
name: 'Maps', name: 'Maps',
component: () => import('../views/Maps.vue') component: () => import('@/views/index/Maps.vue')
}, },
{ {
path: '/weekly', path: 'weekly',
name: 'WeeklyRecommend', name: 'WeeklyRecommend',
component: () => import('../views/WeeklyRecommend.vue') component: () => import('@/views/index/WeeklyRecommend.vue')
}, },
{ {
path: '/map/:id', path: 'map/:id',
name: 'MapDetail', name: 'MapDetail',
component: () => import('../views/MapDetail.vue') component: () => import('@/views/index/MapDetail.vue')
}, },
{ {
path: '/weapon-match', path: 'weapon-match',
name: 'WeaponMatch', name: 'WeaponMatch',
component: () => import('../views/WeaponMatch.vue') component: () => import('@/views/index/WeaponMatch.vue')
},
{
path: 'competition',
name: 'Competition',
component: () => import('@/views/index/Competition.vue')
},
{
path: 'competition/:id',
name: 'CompetitionDetail',
component: () => import('@/views/index/CompetitionDetail.vue')
}
]
},
{
path: '/backend',
component: () => import('@/views/backend.vue'),
children: [
{
path: '',
redirect: 'dashboard'
},
{
path: 'login',
name: 'Login',
component: () => import('@/views/backend/Login.vue')
},
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/backend/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
} }
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes,
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active'
})
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token')
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!token) {
next({
path: '/backend/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next()
}
}) })
export default router export default router

11
src/views/backend.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>

176
src/views/backend/Login.vue Normal file
View File

@ -0,0 +1,176 @@
<template>
<div class="login-container">
<div class="bg-container">
<img :src="bgImg" alt="登录背景" />
</div>
<div class="bottom-text">
<p>© Byz解忧杂货铺</p>
</div>
<div class="content-container">
<div class="login-right">
<button class="back-btn" @click="handleBack" title="返回主界面">
返回主界面
</button>
<LoginModule v-if="!showRegister" @register="showRegister = true" />
<RegisterModule v-else @login="showRegister = false" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import loginBg from '@/assets/login_1.jpg'
import loginBg1 from '@/assets/login_2.jpg'
import loginBg3 from '@/assets/login_3.jpg'
import LoginModule from '@/components/login_module.vue'
import RegisterModule from '@/components/register_module.vue'
const images = [loginBg, loginBg1,loginBg3]
const randomIndex = Math.floor(Math.random() * images.length)
const bgImg = ref(images[randomIndex])
const router = useRouter()
const showRegister = ref(false)
const handleBack = () => {
router.push('/')
}
</script>
<style scoped>
.login-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: absolute;
top: 0;
right: 0;
width: 25%;
min-width: 320px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.login-right {
position: relative;
width: 100%;
height: calc(100vh - 120px);
margin: 0;
background: linear-gradient(135deg, #afe9ef 0%, #a0c4ff 100%);
border-radius: 16px 0 0 20px;
box-shadow: -8px 0 25px rgba(0, 0, 0, 0.51);
display: flex;
align-items: center;
justify-content: center;
}
.back-btn {
position: absolute;
top: 18px;
left: 28px;
background: #e6f7ff;
color: #409eff;
border: none;
border-radius: 18px;
padding: 6px 18px 6px 14px;
font-size: 15px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(64,158,255,0.10);
cursor: pointer;
transition: background 0.2s, color 0.2s;
z-index: 10;
display: flex;
align-items: center;
gap: 4px;
}
.back-btn:hover {
background: #b3e5fc;
color: #1976d2;
}
@media (max-width: 900px) {
.content-container {
width: 40%;
min-width: 220px;
}
.login-right {
}
.login-form {
max-width: 220px;
}
}
@media (max-width: 600px) {
.content-container {
width: 100%;
min-width: 0;
left: 0;
right: 0;
}
.login-right {
border-radius: 0;
margin: 0;
height: 100vh;
min-width: 0;
max-width: 100vw;
box-shadow: none;
background: linear-gradient(135deg, #afe9ef 0%, #b6d0ff 100%);
}
.login-form {
width: 96%;
min-width: 0;
max-width: 100vw;
}
.back-btn {
display: none;
}
}
.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;
}
</style>

262
src/views/index.vue Normal file
View File

@ -0,0 +1,262 @@
<script setup lang="ts">
</script>
<template>
<div class="app">
<nav class="navbar">
<div class="nav-container">
<div class="nav-left">
<div class="nav-brand">红色警戒3数据分析中心</div>
<router-link to="/maps" class="nav-link">最近上传地图</router-link>
<router-link to="/weekly" class="nav-link">热门下载地图</router-link>
<router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
<router-link to="/competition" class="nav-link">赛程信息</router-link>
<router-link to="/demands" class="nav-link">办事大厅</router-link>
</div>
<div class="nav-right">
<router-link to="/backend/login" class="nav-link login-btn">
<i class="fas fa-user"></i>
管理登录
</router-link>
</div>
</div>
</nav>
<main class="main-content">
<router-view></router-view>
</main>
<footer class="footer">
<div class="footer-bottom">
<p>Byz解忧杂货铺</p>
</div>
</footer>
</div>
</template>
<style scoped>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
width: 100%;
}
.main-content {
margin-top: 60px;
padding: 20px;
flex: 1;
max-width: 1400px;
width: 100%;
margin-left: auto;
margin-right: auto;
margin-bottom: 0;
}
.footer {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
color: white;
padding: 2rem 0;
margin: 0;
width: 100vw;
position: relative;
left: 50%;
right: 50%;
margin-left: -50vw;
margin-right: -50vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.navbar {
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
padding: 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-left {
display: flex;
align-items: center;
gap: 30px;
}
.nav-right {
display: flex;
align-items: center;
}
.nav-brand {
color: white;
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
.nav-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.3s ease;
font-weight: 500;
position: relative;
}
.nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.router-link-active {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.router-link-exact-active {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.login-btn {
display: flex;
align-items: center;
gap: 6px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 20px;
transition: all 0.3s ease;
}
.login-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.login-btn i {
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
padding: 0 15px;
}
.nav-left {
gap: 15px;
}
.nav-brand {
font-size: 1.2rem;
}
.nav-link {
padding: 6px 12px;
}
.login-btn {
padding: 6px 12px;
}
.main-content {
padding: 15px;
}
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
width: 100%;
padding: 0 2rem;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-section h3 {
font-size: 1.25rem;
font-weight: 600;
color: white;
margin-bottom: 1rem;
position: relative;
}
.footer-section h3::after {
content: '';
position: absolute;
bottom: -0.5rem;
left: 0;
width: 2rem;
height: 2px;
background: rgba(255, 255, 255, 0.3);
}
.footer-section p {
color: rgba(255, 255, 255, 0.9);
font-size: 0.95rem;
line-height: 1.6;
margin: 0;
}
.footer-bottom {
width: 100%;
max-width: 1200px;
text-align: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-bottom p {
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
margin: 0;
}
@media (max-width: 768px) {
.footer {
padding: 1.5rem 1rem;
}
.footer-content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.footer-section h3 {
font-size: 1.1rem;
}
.footer-bottom {
margin-top: 1.5rem;
padding-top: 1rem;
}
}
</style>

View File

@ -0,0 +1,193 @@
<template>
<div class="demand-hall">
<div class="page-header">
<h1>赛程信息</h1>
<div class="header-subtitle">
<span class="date-range">点击即可查看和报名</span>
</div>
</div>
<div class="action-buttons">
<button class="btn-common btn-gradient btn-margin-right" @click="addNewCompetition">添加赛程</button>
<button class="btn-common btn-light" @click="refreshCompetitions">刷新赛程</button>
</div>
<div class="table-container">
<table class="maps-table">
<thead>
<tr>
<th>序号</th>
<th>赛程名称</th>
<th>比赛时间</th>
<th>是否结赛</th>
<th>主办人</th>
<th>联系方式</th>
<th>比赛方式</th>
<th>比赛类型</th>
</tr>
</thead>
<tbody>
<tr v-for="(competition, index) in competitions"
:key="competition.id"
@click="handleCompetitionClick(competition)"
class="competition-row">
<td>{{ index + 1 }}</td>
<td>{{ competition.name }}</td>
<td>{{ competition.time }}</td>
<td>
<span :class="['status-tag', competition.isFinished ? 'finished' : 'ongoing']">
{{ competition.isFinished ? '已结束' : '进行中' }}
</span>
</td>
<td>{{ competition.organizer }}</td>
<td>{{ competition.contact }}</td>
<td>{{ competition.mode }}</td>
<td>{{ competition.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const competitions = ref([
{
id: 1,
name: '单淘汰赛',
time: '2024-04-01 14:00',
isFinished: false,
organizer: '赵六',
contact: '13600136000',
mode: '单阶段',
type: '单淘汰'
}
])
//
const handleCompetitionClick = (competition) => {
router.push({
path: `/competition/${competition.id}`,
query: {
name: competition.name,
time: competition.time,
organizer: competition.organizer,
contact: competition.contact,
mode: competition.mode,
type: competition.type
}
})
}
//
const refreshCompetitions = () => {
// API
console.log('刷新赛程数据')
}
//
const addNewCompetition = () => {
//
console.log('添加新赛程')
}
</script>
<style scoped>
.btn-common {
display: inline-block;
padding: 8px 22px;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
border: 1px solid #b6d2ff;
cursor: pointer;
transition: background 0.2s, color 0.2s, border 0.2s;
outline: none;
box-shadow: none;
margin-bottom: 20px;
}
.btn-gradient {
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: #fff;
border: 1px solid #71eaeb;
}
.btn-gradient:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
color: #fff;
border: 1.5px solid #416bdf;
}
.btn-light {
background: linear-gradient(90deg, #e3f0ff 0%, #f7fbff 100%);
color: #2563eb;
border: 1px solid #b6d2ff;
}
.btn-light:hover {
background: linear-gradient(90deg, #d0e7ff 0%, #eaf4ff 100%);
color: #174ea6;
border: 1.5px solid #2563eb;
}
/* 按钮间距 */
.btn-margin-right {
margin-right: 16px;
}
.action-buttons {
margin-bottom: 20px;
}
.competition-row {
cursor: pointer;
transition: background-color 0.2s;
}
.competition-row:hover {
background-color: #f5f7fa;
}
.status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-tag.ongoing {
background-color: #e1f3d8;
color: #67c23a;
}
.status-tag.finished {
background-color: #f4f4f5;
color: #909399;
}
.maps-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.maps-table th,
.maps-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.maps-table th {
background-color: #f5f7fa;
color: #606266;
font-weight: 500;
}
.maps-table tbody tr:last-child td {
border-bottom: none;
}
</style>

View File

@ -0,0 +1,216 @@
<template>
<div class="competition-detail">
<div class="page-header">
<div class="header-top">
<button class="back-button" @click="handleBack">
<i class="back-icon"></i>
返回列表
</button>
<h1>{{ competition.name }}</h1>
</div>
<div class="header-subtitle">
<span class="date-range">{{ competition.time }}</span>
</div>
</div>
<div class="tournament-container">
<div class="tournament-info">
<div class="info-item">
<span class="label">主办人</span>
<span class="value">{{ competition.organizer }}</span>
</div>
<div class="info-item">
<span class="label">联系方式</span>
<span class="value">{{ competition.contact }}</span>
</div>
<div class="info-item">
<span class="label">比赛方式</span>
<span class="value">{{ competition.mode }}</span>
</div>
<div class="info-item">
<span class="label">比赛类型</span>
<span class="value">{{ competition.type }}</span>
</div>
</div>
<!-- 单淘汰赛展示 -->
<div v-if="competition.type === '单淘汰'" class="tournament-bracket-container">
<div class="section-title">赛事对阵图</div>
<div class="bracket-wrapper">
<TournamentBracket
:rounds="tournamentRounds"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import TournamentBracket from '@/components/TournamentBracket.vue'
const route = useRoute()
const router = useRouter()
const competition = ref({
name: route.query.name || '',
time: route.query.time || '',
organizer: route.query.organizer || '',
contact: route.query.contact || '',
mode: route.query.mode || '',
type: route.query.type || ''
})
//
const tournamentRounds = ref([
//
{
games: [
{
player1: { id: "1", name: "战队 Alpha", score: 2, winner: true },
player2: { id: "2", name: "战队 Beta", score: 1, winner: false }
},
{
player1: { id: "3", name: "战队 Gamma", score: 0, winner: false },
player2: { id: "4", name: "战队 Delta", score: 2, winner: true }
}
]
},
//
{
games: [
{
player1: { id: "1", name: "战队 Alpha", score: 3, winner: true },
player2: { id: "4", name: "战队 Delta", score: 1, winner: false }
}
]
}
])
const handleBack = () => {
router.push('/competition')
}
</script>
<style scoped>
.competition-detail {
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 24px;
color: #333;
margin-bottom: 8px;
}
.header-subtitle {
color: #666;
}
.tournament-container {
display: flex;
flex-direction: column;
gap: 30px;
}
.tournament-info {
background: #f5f7fa;
padding: 20px;
border-radius: 8px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.info-item .label {
color: #666;
font-weight: 500;
}
.info-item .value {
color: #333;
}
.header-top {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 8px;
}
.back-button {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.back-button:hover {
background: #e6e8eb;
color: #409eff;
border-color: #c6e2ff;
}
.back-icon {
font-size: 16px;
line-height: 1;
}
@media (max-width: 768px) {
.tournament-info {
grid-template-columns: 1fr;
}
}
.tournament-bracket-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 20px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.bracket-wrapper {
overflow-x: auto;
padding: 20px 0;
}
@media (max-width: 768px) {
.tournament-bracket-container {
padding: 15px;
}
.section-title {
font-size: 16px;
margin-bottom: 15px;
}
}
</style>

View File

@ -42,7 +42,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { getMapDetail } from '../api/maps' import { getMapDetail } from '../../api/maps.js'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@ -114,9 +114,9 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { getMaps, getAllTags } from '../api/maps' import { getMaps, getAllTags } from '../../api/maps.js'
import '../assets/styles/common.css' import '../../assets/styles/common.css'
import '../assets/styles/Maps.css' import '../../assets/styles/Maps.css'
const router = useRouter() const router = useRouter()
const maps = ref([]) const maps = ref([])

View File

@ -49,9 +49,9 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getWeeklyTopMaps } from '../api/maps' import { getWeeklyTopMaps } from '../../api/maps.js'
import '../assets/styles/common.css' import '../../assets/styles/common.css'
import '../assets/styles/WeeklyRecommend.css' import '../../assets/styles/WeeklyRecommend.css'
const router = useRouter() const router = useRouter()
const recommendedMaps = ref([]) const recommendedMaps = ref([])