Compare commits

...

12 Commits

Author SHA1 Message Date
4828d0bcd6 重置密码的60s冷却🐱🐱 2025-07-25 19:31:28 +08:00
4c15c42ebc 重置密码的60s冷却🐱🐱 2025-07-25 00:58:00 +08:00
0537bdb86e 重置密码的60s冷却🐱🐱 2025-07-24 21:53:02 +08:00
79f1daf525 首页🐱🐱 2025-07-24 21:19:29 +08:00
e4b562ed86 首页🐱🐱 2025-07-24 21:19:23 +08:00
b588915cb2 首页🐱🐱 2025-07-24 21:17:17 +08:00
2a85a09bc8 修改备案号🐱🐱 2025-07-22 13:26:42 +08:00
c6632b124c 修改备案号🐱🐱 2025-07-22 10:16:50 +08:00
bf3b49e72b 修改密码之后扬了登陆 2025-07-19 14:40:27 +08:00
d44a793019 修改密码之后扬了登陆 2025-07-19 14:23:20 +08:00
c3ccf8d73d Merge remote-tracking branch 'origin/feature/login-screen' into feature/login-screen 2025-07-19 13:40:04 +08:00
04bf94df0c 添加了Config.xml 编辑器 2025-07-19 13:39:02 +08:00
18 changed files with 2723 additions and 78 deletions

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

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;
}
}

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

View File

@ -0,0 +1,528 @@
<template>
<div class="tournament-bracket-root">
<h1>双败淘汰赛赛程树状图</h1>
<div class="container">
<!-- 控制面板 -->
<div class="control-panel">
<h2>选择比赛</h2>
<div class="tournament-info">
<select v-model="selectedTournamentId" @change="handleTournamentChange">
<option value="">-- 请选择比赛 --</option>
<option v-for="tournament in tournaments" :key="tournament.id" :value="tournament.id">
{{ tournament.name }}
</option>
</select>
<div v-if="selectedTournamentId">
<strong>已选择:</strong> {{ selectedTournamentName }}
</div>
</div>
<h2>参赛者列表</h2>
<div id="player-list">
<div v-if="!selectedTournamentId" class="loading">请先选择比赛</div>
<div v-else-if="loading" class="loading">加载中...</div>
<div v-else>
<p v-if="participants.length === 0">暂无参赛者</p>
<template v-else>
<p> {{ participants.length }} 位参赛者</p>
<ul>
<li v-for="participant in participants" :key="participant.id">
{{ participant.name }}
</li>
</ul>
</template>
</div>
</div>
<h2>生成赛程</h2>
<button @click="generateDoubleEliminationBracket" :disabled="!canGenerateBracket">
生成双败淘汰赛程
</button>
</div>
<!-- 胜者组 -->
<div class="bracket-section">
<h2>胜者组</h2>
<ul>
<li v-for="(round, rIndex) in winnersBracket" :key="'w-' + rIndex">
<h3>胜者组 - {{ rIndex + 1 }} </h3>
<ul>
<li v-for="(match, mIndex) in round" :key="'w-' + rIndex + '-' + mIndex">
<div class="match">
<div class="participant" :class="{ winner: match.winner && match.participant1 && match.winner.id === match.participant1.id }">
{{ match.participant1 ? match.participant1.name : '轮空' }}
<input type="number" v-model.number="match.score1" :disabled="match.decided || !match.participant1 || !match.participant2" placeholder="0">
</div>
<div class="participant" :class="{ winner: match.winner && match.participant2 && match.winner.id === match.participant2.id }">
{{ match.participant2 ? match.participant2.name : '轮空' }}
<input type="number" v-model.number="match.score2" :disabled="match.decided || !match.participant1 || !match.participant2" placeholder="0">
</div>
<button @click="confirmScore(match, 'winners', rIndex, mIndex)" :disabled="match.decided || !match.participant1 || !match.participant2">
确认比分
</button>
</div>
</li>
</ul>
</li>
</ul>
</div>
<!-- 败者组 -->
<div class="bracket-section">
<h2>败者组</h2>
<ul>
<li v-for="(round, rIndex) in losersBracket" :key="'l-' + rIndex">
<h3>败者组 - {{ rIndex + 1 }} </h3>
<ul>
<li v-for="(match, mIndex) in round" :key="'l-' + rIndex + '-' + mIndex">
<div class="match">
<div class="participant" :class="{ winner: match.winner && match.participant1 && match.winner.id === match.participant1.id }">
{{ match.participant1 ? match.participant1.name : '轮空' }}
<input type="number" v-model.number="match.score1" :disabled="match.decided || !match.participant1 || !match.participant2" placeholder="0">
</div>
<div class="participant" :class="{ winner: match.winner && match.participant2 && match.winner.id === match.participant2.id }">
{{ match.participant2 ? match.participant2.name : '轮空' }}
<input type="number" v-model.number="match.score2" :disabled="match.decided || !match.participant1 || !match.participant2" placeholder="0">
</div>
<button @click="confirmScore(match, 'losers', rIndex, mIndex)" :disabled="match.decided || !match.participant1 || !match.participant2">
确认比分
</button>
</div>
</li>
</ul>
</li>
</ul>
</div>
<!-- 决赛 -->
<div class="bracket-section" v-if="finalMatch">
<h2>决赛 (BO5)</h2>
<div class="match">
<div class="participant" :class="{ winner: finalMatch.decided && finalMatch.score1 > finalMatch.score2 }">
{{ finalMatch.participant1 ? finalMatch.participant1.name : '待定' }}
<input type="number" v-model.number="finalMatch.score1" :disabled="finalMatch.decided" placeholder="0">
</div>
<div class="participant" :class="{ winner: finalMatch.decided && finalMatch.score2 > finalMatch.score1 }">
{{ finalMatch.participant2 ? finalMatch.participant2.name : '待定' }}
<input type="number" v-model.number="finalMatch.score2" :disabled="finalMatch.decided" placeholder="0">
</div>
<button @click="confirmFinal" :disabled="finalMatch.decided">确认决赛比分</button>
</div>
<div v-if="finalMatch.decided" class="final-ranking">
<h3>最终排名</h3>
<p v-if="champion">🏆 冠军: {{ champion }}</p>
<p v-if="runnerUp">🥈 亚军: {{ runnerUp }}</p>
<p v-if="thirdPlace">🥉 季军: {{ thirdPlace }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
export default {
data() {
return {
selectedTournamentId: '',
selectedTournamentName: '',
tournaments: [],
participants: [],
winnersBracket: [],
losersBracket: [],
finalMatch: null,
champion: null,
runnerUp: null,
thirdPlace: null,
loading: false,
pointsMultiplier: {
winners: 1,
losers: 0.5
}
};
},
computed: {
canGenerateBracket() {
return this.participants.length >= 4;
}
},
mounted() {
this.fetchTournamentList();
},
methods: {
async fetchTournamentList() {
try {
const data = await getTournamentList();
this.tournaments = data;
} catch (err) {
console.error('获取比赛列表失败:', err);
}
},
async handleTournamentChange() {
if (!this.selectedTournamentId) {
this.selectedTournamentName = '';
this.participants = [];
return;
}
const selected = this.tournaments.find(t => t.id === this.selectedTournamentId);
this.selectedTournamentName = selected ? selected.name : '';
this.loadParticipants();
},
async loadParticipants() {
this.loading = true;
try {
const data = await getSignUpResultList();
const filtered = data.filter(item => item.tournament_id == this.selectedTournamentId);
this.participants = filtered.map(item => ({
id: item.id,
name: item.sign_name,
team_name: item.team_name || '',
win: item.win || '0',
lose: item.lose || '0',
qq_code: item.qq_code || '',
status: item.status || ''
}));
} catch (err) {
console.error('获取参赛者失败:', err);
} finally {
this.loading = false;
}
},
generateDoubleEliminationBracket() {
const round1 = [];
for (let i = 0; i < this.participants.length; i += 2) {
round1.push({
participant1: this.participants[i],
participant2: this.participants[i + 1] || null,
score1: 0,
score2: 0,
decided: false,
winner: null,
loser: null
});
}
this.winnersBracket = [round1];
this.losersBracket = [];
this.finalMatch = null;
this.champion = this.runnerUp = this.thirdPlace = null;
},
async confirmScore(match, bracketType, rIndex, mIndex) {
if (match.score1 === match.score2) return alert('不能平局');
match.decided = true;
match.winner = match.score1 > match.score2 ? match.participant1 : match.participant2;
match.loser = match.score1 > match.score2 ? match.participant2 : match.participant1;
//
await this.updatePlayerStats(match.participant1, match.participant2, match.score1, match.score2, bracketType);
//
const bracket = bracketType === 'winners' ? this.winnersBracket : this.losersBracket;
if (!bracket[rIndex + 1]) {
bracket[rIndex + 1] = [];
}
const nextRound = bracket[rIndex + 1];
const insertIndex = Math.floor(mIndex / 2);
const isEven = mIndex % 2 === 0;
if (!nextRound[insertIndex]) {
nextRound[insertIndex] = { participant1: null, participant2: null, score1: 0, score2: 0, decided: false, winner: null, loser: null };
}
if (isEven) {
nextRound[insertIndex].participant1 = match.winner;
} else {
nextRound[insertIndex].participant2 = match.winner;
}
if (bracketType === 'winners') {
this.winnersBracket[rIndex + 1] = nextRound;
//
if (!this.losersBracket[rIndex]) {
this.losersBracket[rIndex] = [];
}
const losersR = this.losersBracket[rIndex];
if (!losersR[insertIndex]) {
losersR[insertIndex] = { participant1: null, participant2: null, score1: 0, score2: 0, decided: false, winner: null, loser: null };
}
if (isEven) {
losersR[insertIndex].participant1 = match.loser;
} else {
losersR[insertIndex].participant2 = match.loser;
}
this.losersBracket[rIndex] = losersR;
} else {
//
this.losersBracket[rIndex + 1] = nextRound;
//
const lastWinnersRound = this.winnersBracket[this.winnersBracket.length - 1];
const lastLosersRound = this.losersBracket[this.losersBracket.length - 1];
if (lastWinnersRound && lastWinnersRound.length === 1 && lastLosersRound && lastLosersRound.length === 1) {
const winnerFromWinners = lastWinnersRound[0].winner;
const winnerFromLosers = lastLosersRound[0].winner;
if (winnerFromWinners && winnerFromLosers) {
this.finalMatch = {
participant1: winnerFromWinners,
participant2: winnerFromLosers,
score1: 0,
score2: 0,
decided: false
};
}
}
}
},
async confirmFinal() {
const match = this.finalMatch;
if (!match || match.score1 === match.score2) return alert('决赛不能平局');
match.decided = true;
// participant1participant2
//
const winnerFromWinners = this.winnersBracket[this.winnersBracket.length - 1][0].winner;
const winnerFromLosers = this.losersBracket[this.losersBracket.length - 1][0].winner;
//
const winner = match.score1 > match.score2 ? match.participant1 : match.participant2;
const loser = match.score1 > match.score2 ? match.participant2 : match.participant1;
// - 使
await this.updatePlayerStats(match.participant1, match.participant2, match.score1, match.score2, 'winners');
//
this.champion = winner.name || winner;
this.runnerUp = loser.name || loser;
//
//
//
//
if (this.losersBracket.length > 0) {
const lastLoserRound = this.losersBracket[this.losersBracket.length - 1];
if (lastLoserRound && lastLoserRound.length > 0) {
const lastLoserMatch = lastLoserRound[lastLoserRound.length - 1];
if (lastLoserMatch && lastLoserMatch.loser) {
this.thirdPlace = lastLoserMatch.loser.name || lastLoserMatch.loser;
}
}
}
//
if (!this.thirdPlace && this.losersBracket.length > 1) {
const semiLoserRound = this.losersBracket[this.losersBracket.length - 2];
if (semiLoserRound && semiLoserRound.length > 0) {
const semiLoserMatch = semiLoserRound[semiLoserRound.length - 1];
if (semiLoserMatch && semiLoserMatch.loser) {
this.thirdPlace = semiLoserMatch.loser.name || semiLoserMatch.loser;
}
}
}
if (!this.thirdPlace) {
this.thirdPlace = '待定';
}
//
// statuswinstatuslose
try {
const latestData = await getSignUpResultList();
//
const championData = latestData.find(item => item.sign_name === this.champion);
if (championData) {
await updateSignUpResult(championData.id, {
...championData,
status: 'win',
rank: '1'
});
}
//
const runnerUpData = latestData.find(item => item.sign_name === this.runnerUp);
if (runnerUpData) {
await updateSignUpResult(runnerUpData.id, {
...runnerUpData,
status: 'lose',
rank: '2'
});
}
//
if (this.thirdPlace && this.thirdPlace !== '待定') {
const thirdPlaceData = latestData.find(item => item.sign_name === this.thirdPlace);
if (thirdPlaceData) {
await updateSignUpResult(thirdPlaceData.id, {
...thirdPlaceData,
status: 'lose',
rank: '3'
});
}
}
} catch (err) {
console.error('更新最终排名失败:', err);
}
},
//
formatMatch(match, isFinal = false) {
if (!match) return '';
const p1Name = match.participant1 ? match.participant1.name : '待定';
const p2Name = match.participant2 ? match.participant2.name : '待定';
return `${p1Name} vs ${p2Name}`;
},
// API
async updatePlayerStats(p1, p2, s1, s2, bracketType = 'winners') {
if (!p1 || !p2) return;
try {
//
const latestData = await getSignUpResultList();
const p1Latest = latestData.find(item => item.id === p1.id);
const p2Latest = latestData.find(item => item.id === p2.id);
if (!p1Latest || !p2Latest) return;
// -
const pointMultiplier = bracketType === 'winners' ? 1 : 0.5;
const winPoints = s1 > s2 ? 1 * pointMultiplier : 0;
const losePoints = s2 > s1 ? 1 * pointMultiplier : 0;
// 1
const p1Data = {
tournament_id: this.selectedTournamentId,
tournament_name: this.selectedTournamentName,
team_name: p1.team_name || '',
sign_name: p1.name,
win: String(Number(p1Latest.win || 0) + s1),
lose: String(Number(p1Latest.lose || 0) + s2),
status: s1 > s2 ? 'win' : 'lose',
qq_code: p1.qq_code || '',
points: String(Number(p1Latest.points || 0) + (s1 > s2 ? winPoints : 0)),
bracket_type: bracketType //
};
// 2
const p2Data = {
tournament_id: this.selectedTournamentId,
tournament_name: this.selectedTournamentName,
team_name: p2.team_name || '',
sign_name: p2.name,
win: String(Number(p2Latest.win || 0) + s2),
lose: String(Number(p2Latest.lose || 0) + s1),
status: s2 > s1 ? 'win' : 'lose',
qq_code: p2.qq_code || '',
points: String(Number(p2Latest.points || 0) + (s2 > s1 ? winPoints : 0)),
bracket_type: bracketType //
};
// API
await updateSignUpResult(p1.id, p1Data);
await updateSignUpResult(p2.id, p2Data);
//
await this.loadParticipants();
} catch (err) {
console.error('更新选手数据失败:', err);
alert('更新选手数据失败');
}
}
}
};
</script>
<style scoped>
.tournament-bracket-root {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.control-panel {
flex: 1;
min-width: 250px;
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.bracket-section {
flex: 2;
min-width: 300px;
background: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.match {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
}
.participant {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.participant.winner {
font-weight: bold;
color: #2a7f2a;
}
input[type="number"] {
width: 40px;
text-align: center;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-top: 5px;
width: 100%;
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.final-ranking {
background: #fffbdb;
border: 1px solid #f0e68c;
padding: 15px;
border-radius: 6px;
margin-top: 20px;
}
.final-ranking h3 {
margin-top: 0;
color: #b8860b;
}
.final-ranking p {
margin: 5px 0;
font-size: 16px;
}
</style>

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,29 +46,74 @@ 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,
//
const filteredPlayers = response.filter(player => player.tournament_id === props.tournamentId)
//
const playersWithPoints = filteredPlayers.map(player => {
//
let points = parseFloat(player.points || 0)
//
if (!player.points) {
const wins = parseInt(player.win) || 0
//
const multiplier = player.bracket_type === 'losers' ? 0.5 : 1
points = wins * multiplier
}
//
const rank = player.rank || ''
return {
id: player.id,
rank: rank,
username: player.sign_name,
faction: player.faction,
faction: player.faction || player.team_name || '',
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
}
//
return a.lose - b.lose
})
.map((player, index) => ({
...player,
rank: index + 1
}))
points: points,
status: player.status || '',
bracket_type: player.bracket_type || 'winners', //
score: `${player.win}${player.lose}负 (${points.toFixed(1)}分)`
}
})
//
const sortedPlayers = [...playersWithPoints].sort((a, b) => {
//
if (a.rank && b.rank) {
return parseInt(a.rank) - parseInt(b.rank)
}
// winlose
if (a.status === 'win' && b.status === 'lose') return -1
if (a.status === 'lose' && b.status === 'win') return 1
//
if (b.points !== a.points) {
return b.points - a.points
}
//
if (a.bracket_type !== b.bracket_type) {
return a.bracket_type === 'winners' ? -1 : 1
}
//
if (b.win !== a.win) {
return b.win - a.win
}
//
return a.lose - b.lose
})
//
const results = sortedPlayers.map((player, index) => ({
...player,
rank: player.rank || (index + 1).toString()
}))
rankData.value = results
} catch (error) {
@ -79,6 +124,13 @@ const fetchRankData = async () => {
onMounted(() => {
fetchRankData()
})
// ID
watch(() => props.tournamentId, () => {
if (props.tournamentId) {
fetchRankData()
}
})
</script>
<style scoped>

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

@ -86,8 +86,10 @@
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div>
<div class="login-button">
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '重置密码' }}
<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">
@ -111,7 +113,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import {getCaptcha, forgetPassword, requestResetPassword, getUserByInfo} from '../api/login'
import {getCaptcha, requestResetPassword, getUserByInfo} from '../api/login'
import ErrorDialog from './ErrorDialog.vue'
import SuccessDialog from './SuccessDialog.vue'
@ -132,6 +134,8 @@ const uuidError = ref('')
//
const isSubmitting = ref(false)
const cooldown = ref(0)
let cooldownTimer = null
//
const showError = ref(false)
@ -218,17 +222,23 @@ const uuid_handleForgetPassword = async () => {
if (!uuid_validateForm()) {
return
}
//console.log(uuid.value);
const user = await getUserByInfo({qq_code:username.value})
console.log(user)
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
}
}

View File

@ -16,7 +16,8 @@ const routes = [
children: [
{
path: '',
redirect: '/maps'
name: 'Home',
component: () => import('@/views/index/Home.vue')
},
{
path: 'demands',
@ -50,6 +51,12 @@ const routes = [
component: () => import('@/views/weapon/WeaponMatch.vue'),
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
},
{
path: 'configEditor',
name: 'ConfigEditor',
component: () => import('@/views/index/ConfigEditor.vue'),
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
},
{
path: 'competition',
name: 'Competition',
@ -163,7 +170,7 @@ router.beforeEach(async (to, from, next) => {
// 登录校验:如果页面需要登录但本地没有有效 token则跳转登录页
if (requiresAuth && !hasValidToken()) {
return next({
path: '/maps',
path: '/',
query: { redirect: to.fullPath }
})
}
@ -174,7 +181,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;

View File

@ -55,9 +55,9 @@
{{ loading ? '重置中...' : '重置密码' }}
</button>
</div>
<div class="back-link">
<a @click.prevent="goToHome">返回首页</a>
</div>
<!-- <div class="back-link">-->
<!-- <a @click.prevent="goToHome">返回登录页</a>-->
<!-- </div>-->
</form>
</div>
</div>
@ -86,11 +86,14 @@ 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]
const images = [loginBg, loginBg1, loginBg3,loginBg4, loginBg5]
const randomIndex = Math.floor(Math.random() * images.length)
const bgImg = ref(images[randomIndex])
@ -149,7 +152,17 @@ const handleResetPassword = async () => {
// API
await resetPassword(token, newPassword.value)
showSuccessMessage('密码重置成功!请使用新密码登录。')
//
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 || '重置密码失败,请重试'
@ -165,7 +178,11 @@ const handleSuccessClose = () => {
}
const goToHome = () => {
router.push('/')
if (hasValidToken()) {
logoutUser()
}
router.push('/backend/login')
// router.push('/')
}
onMounted(() => {

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

@ -37,11 +37,13 @@ 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])

View File

@ -324,11 +324,13 @@ function handlePasswordChangeError(errorMessage) {
<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>
@ -343,41 +345,16 @@ function handlePasswordChangeError(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>
@ -439,9 +416,15 @@ function handlePasswordChangeError(errorMessage) {
<img src="../assets/logo.png" class="footer-logo">
<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>

View File

@ -75,8 +75,9 @@
v-if="competition.status === 'finish'"
:tournament-id="parseInt(route.query.id)"
/>
<DoubleEliminationBracket v-if="competition.format === '双败淘汰' && competition.status === 'starting'" />
<tournament-bracket
v-if="competition.status === 'starting'"
v-else-if="competition.status === 'starting'"
:tournament-id="parseInt(route.query.id)"
@refreshPlayers="fetchRegisteredPlayers"
/>
@ -217,6 +218,7 @@ import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import TournamentBracket from '@/components/TournamentBracket.vue'
import RankContestant from '@/components/RankContestant.vue'
import DoubleEliminationBracket from '@/components/DoubleEliminationBracket.vue'
import {
getTournamentList,
updateTournament,

View File

@ -0,0 +1,799 @@
<template>
<div class="config-editor">
<div class="page-header">
<h1>Config.xml 编辑器</h1>
</div>
<div class="editor-container">
<!-- 新增的侧边栏 -->
<div class="sidebar" v-if="tagList.length > 0">
<h3>标签列表</h3>
<!-- 添加搜索框 -->
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="输入ID搜索..."
@input="searchTags"
/>
<button @click="clearSearch" class="clear-btn">×</button>
</div>
<div class="tag-list">
<div
v-for="tag in filteredTags"
:key="tag.id"
class="tag-item"
draggable="true"
@dragstart="dragStart(tag)"
@click="insertTag(tag)"
>
<span class="tag-type">{{ tag.type }}</span>
<span class="tag-id">{{ tag.id }}</span>
<!-- 添加缓存值预览 -->
<span class="tag-preview" v-if="tag.content.length > 50">
{{ tag.content.substring(0, 50) }}...
</span>
<span class="tag-preview" v-else>
{{ tag.content }}
</span>
</div>
</div>
</div>
<div class="input-section">
<div class="button-group">
<button @click="addEntry" class="gradient-btn">添加</button>
<button @click="validateXml" class="gradient-btn">验证 XML</button>
<button @click="download" class="gradient-btn">下载 XML</button>
<button @click="insertTemplate('LogicCommand')" class="gradient-btn">预设 LogicCommand</button>
<button @click="insertTemplate('LogicCommandSet')" class="gradient-btn">预设 LogicCommandSet</button>
<button @click="clearAll" class="gradient-btn">清空所有</button>
<input type="file" id="fileInput" accept=".xml" @change="handleFileUpload" style="display: none">
<button @click="triggerFileInput" class="gradient-btn">上传 XML</button>
</div>
<div class="message-section">
<p v-if="errorMsg" class="error-message">{{ errorMsg }}</p>
<p v-if="warningMsg" class="warning-message">{{ warningMsg }}</p>
</div>
<div class="input-group">
<label for="newEntry">输入 LogicCommand LogicCommandSet 标签</label>
<textarea
id="newEntry"
v-model="newEntry"
placeholder="输入 LogicCommand 或 LogicCommandSet 标签"
class="entry-textarea"
@dragover.prevent
@drop="dropTag"
></textarea>
</div>
</div>
<div class="editor-section">
<h3>编辑区 <span class="entry-count">({{ entries.length }} 个条目)</span></h3>
<div class="xml-stats" v-if="xmlContent">
<span>LogicCommand: {{ getLogicCommandCount() }}</span>
<span>LogicCommandSet: {{ getLogicCommandSetCount() }}</span>
<span>文件大小: {{ getFileSize() }}</span>
<span v-if="lastModified">最后修改: {{ lastModified }}</span>
</div>
<textarea
v-model="xmlContent"
class="xml-textarea"
placeholder="XML内容将在这里显示..."
></textarea>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import '../../assets/styles/common.css'
//
const header = ref(`<?xml version="1.0" encoding="utf-8"?>\n<AssetDeclaration xmlns="uri:ea.com:eala:asset">`)
const footer = ref(`</AssetDeclaration>`)
const entries = ref([])
const newEntry = ref('')
const xmlContent = ref('')
const errorMsg = ref('')
const warningMsg = ref('')
const lastModified = ref('')
const draggedTag = ref(null)
const searchQuery = ref('')
const filteredTags = ref([])
// entriesuploadedTags
const tagList = computed(() => {
const tags = []
//
entries.value.forEach(entry => {
const parser = new DOMParser()
const doc = parser.parseFromString(entry, 'application/xml')
const element = doc.documentElement
const id = element.getAttribute('id')
if (id) {
tags.push({
id: id,
type: element.tagName,
content: entry,
source: 'editor'
})
}
})
//
uploadedTags.value.forEach(tag => {
tags.push({
id: tag.id,
type: tag.type,
content: tag.content,
source: 'upload'
})
})
//
filteredTags.value = [...tags]
return tags
})
//
const dragStart = (tag) => {
draggedTag.value = tag
}
//
const searchTags = () => {
if (!searchQuery.value.trim()) {
filteredTags.value = [...tagList.value]
return
}
const query = searchQuery.value.toLowerCase()
filteredTags.value = tagList.value.filter(tag =>
tag.id.toLowerCase().includes(query) ||
tag.content.toLowerCase().includes(query)
)
}
//
const clearSearch = () => {
searchQuery.value = ''
filteredTags.value = [...tagList.value]
}
//
const dropTag = (e) => {
if (draggedTag.value) {
insertTag(draggedTag.value)
draggedTag.value = null
}
}
//
const insertTag = (tag) => {
// 使
newEntry.value = tag.content
}
//
const triggerFileInput = () => {
document.getElementById('fileInput').click()
}
//
const handleFileUpload = (event) => {
const file = event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const content = e.target.result
parseAndImportXml(content)
} catch (error) {
errorMsg.value = '文件解析失败: ' + error.message
}
}
reader.readAsText(file)
}
//
const uploadedTags = ref([])
// XML
const parseAndImportXml = (xmlString) => {
try {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(xmlString, 'application/xml')
//
const parserError = xmlDoc.querySelector('parsererror')
if (parserError) {
throw new Error('XML格式错误: ' + parserError.textContent)
}
// LogicCommandLogicCommandSet
const commands = xmlDoc.getElementsByTagName('LogicCommand')
const commandSets = xmlDoc.getElementsByTagName('LogicCommandSet')
//
uploadedTags.value = []
//
for (let i = 0; i < commands.length; i++) {
const command = commands[i]
const id = command.getAttribute('id')
if (id) {
// XMLSerializer
const rawContent = command.outerHTML
.replace(/ xmlns="[^"]*"/g, '') // xmlns
.replace(/\s+/g, ' ') //
.trim()
uploadedTags.value.push({
id: id,
type: 'LogicCommand',
content: rawContent
})
}
}
for (let i = 0; i < commandSets.length; i++) {
const commandSet = commandSets[i]
const id = commandSet.getAttribute('id')
if (id) {
// XMLSerializer
const rawContent = commandSet.outerHTML
.replace(/ xmlns="[^"]*"/g, '') // xmlns
.replace(/\s+/g, ' ') //
.trim()
uploadedTags.value.push({
id: id,
type: 'LogicCommandSet',
content: rawContent
})
}
}
warningMsg.value = `成功解析 ${commands.length + commandSets.length} 个标签,可在标签列表中选择添加到编辑区`
errorMsg.value = ''
} catch (error) {
errorMsg.value = 'XML解析错误: ' + error.message
warningMsg.value = ''
}
}
//
const addEntry = () => {
const trimmed = newEntry.value.trim()
if (!trimmed.startsWith('<LogicCommand') && !trimmed.startsWith('<LogicCommandSet')) {
errorMsg.value = '只能添加 LogicCommand 或 LogicCommandSet 标签'
return
}
try {
const parser = new DOMParser()
const doc = parser.parseFromString(trimmed, 'application/xml')
const errorNode = doc.querySelector('parsererror')
if (errorNode) throw new Error()
// ID
const idMatch = trimmed.match(/id=["']([^"']+)["']/)
if (!idMatch || !idMatch[1]) {
errorMsg.value = '标签必须包含 id 属性'
return
}
// ID
const newId = idMatch[1]
const allEntries = xmlContent.value.split('\n').join(' ') + ' ' + trimmed
const idRegex = /id=["']([^"']+)["']/g
const ids = []
let match
while ((match = idRegex.exec(allEntries)) !== null) {
ids.push(match[1])
}
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index)
if (duplicateIds.includes(newId)) {
errorMsg.value = `ID "${newId}" 已经存在请使用唯一的ID`
return
}
entries.value.push(trimmed)
updateContent()
newEntry.value = ''
errorMsg.value = ''
warningMsg.value = '条目添加成功!'
formatXml()
} catch (e) {
errorMsg.value = 'XML 标签不合法,请检查格式'
}
}
const updateContent = () => {
const joined = entries.value.join('\n')
xmlContent.value = `${header.value}\n${joined}\n${footer.value}`
lastModified.value = new Date().toLocaleString()
}
const formatXml = () => {
if (!xmlContent.value.trim()) {
errorMsg.value = 'XML内容不能为空'
warningMsg.value = ''
return
}
try {
let content = xmlContent.value.trim()
// XML
const hasXmlDeclaration = content.includes('<?xml version="1.0" encoding="utf-8"?>')
const hasAssetDeclaration = content.includes('<AssetDeclaration xmlns="uri:ea.com:eala:asset">')
if (hasXmlDeclaration && hasAssetDeclaration) {
// XML
const parser = new DOMParser()
const xml = parser.parseFromString(content, 'application/xml')
const pretty = prettify(xml.documentElement, 0)
xmlContent.value = `<?xml version="1.0" encoding="utf-8"?>\n${pretty}`
} else {
// headerfooter
const xmlToFormat = `${header.value}\n${content}\n${footer.value}`
const parser = new DOMParser()
const xml = parser.parseFromString(xmlToFormat, 'application/xml')
const pretty = prettify(xml.documentElement, 0)
xmlContent.value = `${header.value}\n${pretty}\n${footer.value}`
}
errorMsg.value = ''
warningMsg.value = 'XML格式化成功'
} catch (e) {
errorMsg.value = '无法格式化,请检查 XML 内容'
warningMsg.value = ''
}
}
const prettify = (node, level) => {
let indent = ' '.repeat(level)
let output = `${indent}<${node.nodeName}`
for (let attr of node.attributes) {
output += ` ${attr.name}="${attr.value}"`
}
if (node.childNodes.length === 0) {
return output + `/>`
} else {
output += `>`
let children = Array.from(node.childNodes).filter(n => n.nodeType === 1)
if (children.length > 0) {
output += `\n` + children.map(c => prettify(c, level + 1)).join('\n') + `\n${indent}`
} else {
let text = node.textContent.trim()
output += text
}
return output + `</${node.nodeName}>`
}
}
const validateXml = () => {
errorMsg.value = ''
warningMsg.value = ''
//
if (!xmlContent.value.trim()) {
errorMsg.value = 'XML内容不能为空'
return false
}
//
const hasHeader = xmlContent.value.includes(header.value)
const hasFooter = xmlContent.value.includes(footer.value)
if (!hasHeader && !hasFooter) {
errorMsg.value = '缺少XML文件头和文件尾'
return false
}
if (!hasHeader) {
errorMsg.value = '缺少XML文件头'
return false
}
if (!hasFooter) {
errorMsg.value = '缺少XML文件尾'
return false
}
// /
const headerCount = (xmlContent.value.match(new RegExp(header.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length
const footerCount = (xmlContent.value.match(new RegExp(footer.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length
if (headerCount > 1) {
errorMsg.value = 'XML文件头重复'
return false
}
if (footerCount > 1) {
errorMsg.value = 'XML文件尾重复'
return false
}
// ID
try {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(xmlContent.value, 'application/xml')
const commands = xmlDoc.getElementsByTagName('LogicCommand')
const commandSets = xmlDoc.getElementsByTagName('LogicCommandSet')
const allElements = [...commands, ...commandSets]
const ids = []
for (let element of allElements) {
const id = element.getAttribute('id')
if (!id) {
errorMsg.value = `发现缺少id属性的标签: ${element.tagName}`
return false
}
if (ids.includes(id)) {
errorMsg.value = `发现重复的id: ${id}`
return false
}
ids.push(id)
}
warningMsg.value = `XML验证通过共找到 ${allElements.length} 个有效标签。`
return true
} catch (e) {
errorMsg.value = 'XML解析错误: ' + e.message
return false
}
}
const download = () => {
if (!validateXml()) {
return
}
const blob = new Blob([xmlContent.value], { type: 'application/xml' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'config.xml'
link.click()
}
const insertTemplate = (type) => {
if (type === 'LogicCommand') {
newEntry.value = `<LogicCommand
Type="UNIT_BUILD"
id="Command_ConstructSovietAntiVehicleVehicleTech2">
<Object>SovietAntiVehicleVehicleTech2</Object>
</LogicCommand>`
} else if (type === 'LogicCommandSet') {
newEntry.value = `<LogicCommandSet
id="GenericCommandSet">
<Cmd>Command_Stop</Cmd>
</LogicCommandSet>`
}
}
const clearAll = () => {
entries.value = []
newEntry.value = ''
xmlContent.value = ''
errorMsg.value = ''
warningMsg.value = ''
lastModified.value = ''
}
const getLogicCommandCount = () => {
if (!xmlContent.value) return 0
const matches = xmlContent.value.match(/<LogicCommand/g)
return matches ? matches.length : 0
}
const getLogicCommandSetCount = () => {
if (!xmlContent.value) return 0
const matches = xmlContent.value.match(/<LogicCommandSet/g)
return matches ? matches.length : 0
}
const getFileSize = () => {
if (!xmlContent.value) return '0 B'
const bytes = new Blob([xmlContent.value]).size
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
</script>
<style scoped>
/* 新增的搜索框样式 */
.search-box {
position: relative;
margin-bottom: 12px;
}
.search-box input {
width: 100%;
padding: 8px 30px 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.search-box input:focus {
outline: none;
border-color: #1a237e;
}
.clear-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: #999;
font-size: 16px;
padding: 0 4px;
}
.clear-btn:hover {
color: #666;
}
.config-editor {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
.editor-container {
display: flex;
gap: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
padding: 24px;
}
/* 新增的侧边栏样式 */
.sidebar {
width: 250px;
min-width: 250px;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #e0e0e0;
overflow-y: auto;
max-height: 800px;
}
.sidebar h3 {
color: #1a237e;
margin-bottom: 12px;
font-size: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
.tag-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.tag-item {
padding: 10px;
background: white;
border-radius: 6px;
border: 1px solid #e0e0e0;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
gap: 4px;
}
.tag-preview {
font-size: 12px;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: 'Courier New', monospace;
}
.tag-item:hover {
border-color: #1a237e;
background: #f0f2ff;
}
.tag-item:active {
transform: scale(0.98);
}
.tag-type {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.tag-id {
font-family: 'Courier New', monospace;
font-size: 13px;
word-break: break-all;
}
.input-section {
flex: 1;
margin-bottom: 24px;
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1a237e;
}
.entry-textarea {
width: 100%;
height: 120px;
font-family: 'Courier New', monospace;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
resize: vertical;
transition: all 0.3s ease;
}
.entry-textarea:focus {
outline: none;
border-color: #1a237e;
box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
}
.button-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.button-group .gradient-btn {
margin: 0;
padding: 8px 16px;
font-size: 14px;
}
.message-section {
margin-bottom: 16px;
}
.error-message {
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 12px;
margin: 0;
font-size: 14px;
}
.warning-message {
color: #856404;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 12px;
margin: 0;
font-size: 14px;
}
.editor-section {
flex: 1;
}
.editor-section h3 {
color: #1a237e;
margin-bottom: 12px;
font-size: 18px;
}
.entry-count {
color: #666;
font-size: 14px;
font-weight: normal;
}
.xml-stats {
display: flex;
gap: 16px;
margin-bottom: 12px;
font-size: 14px;
color: #666;
}
.xml-stats span {
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.xml-textarea {
width: 100%;
height: 400px;
font-family: 'Courier New', monospace;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
resize: vertical;
transition: all 0.3s ease;
background: #f8f9fa;
}
.xml-textarea:focus {
outline: none;
border-color: #1a237e;
box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
background: white;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.editor-container {
flex-direction: column;
}
.sidebar {
width: 100%;
max-height: 200px;
}
}
@media (max-width: 768px) {
.config-editor {
padding: 16px;
}
.editor-container {
padding: 16px;
}
.button-group {
flex-direction: column;
}
.button-group .gradient-btn {
width: 100%;
}
.entry-textarea,
.xml-textarea {
font-size: 13px;
}
.xml-stats {
flex-wrap: wrap;
}
.xml-stats span {
margin-bottom: 4px;
}
}
</style>

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

File diff suppressed because it is too large Load Diff