Compare commits
12 Commits
master
...
feature/lo
Author | SHA1 | Date | |
---|---|---|---|
4828d0bcd6 | |||
4c15c42ebc | |||
0537bdb86e | |||
79f1daf525 | |||
e4b562ed86 | |||
b588915cb2 | |||
2a85a09bc8 | |||
c6632b124c | |||
bf3b49e72b | |||
d44a793019 | |||
c3ccf8d73d | |||
04bf94df0c |
10
node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts
generated
vendored
10
node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts
generated
vendored
@ -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
64
src/api/record.js
Normal 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
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
BIN
src/assets/login_5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
528
src/components/DoubleEliminationBracket.vue
Normal file
528
src/components/DoubleEliminationBracket.vue
Normal 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;
|
||||
|
||||
// 确保胜者组选手是participant1,败者组选手是participant2
|
||||
// 这样在排名时可以确保胜者组的选手排名更高
|
||||
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 = '待定';
|
||||
}
|
||||
|
||||
// 更新最终排名状态
|
||||
// 确保胜者的status为win,败者的status为lose
|
||||
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>
|
@ -25,7 +25,7 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['close', 'apply'])
|
||||
|
||||
function handleClose() {
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
function handleApply() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// 如果是决赛选手,按照状态排序(win在前,lose在后)
|
||||
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>
|
||||
|
112
src/components/backend/AdminChangesPwd.vue
Normal file
112
src/components/backend/AdminChangesPwd.vue
Normal 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>
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,6 @@ export const loginSuccess = (accessToken, userId) => {
|
||||
if (userId) {
|
||||
localStorage.setItem('user_id', userId);
|
||||
}
|
||||
|
||||
// 设置登录标志
|
||||
justLoggedIn.value = true;
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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">-->
|
||||
<!-- <!– 在线工具 一级菜单 –>-->
|
||||
<!-- <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>-->
|
||||
<!-- <!– 赛事信息 一级菜单 –>-->
|
||||
<!-- <div class="nav-dropdown">-->
|
||||
<!-- <span class="nav-link">赛事信息</span>-->
|
||||
<!-- <div class="dropdown-content">-->
|
||||
<!--<!– <router-link to="/competition" class="nav-link" @click.prevent="handleNavClick('/competition', ['lv-admin','lv-competitor'])">赛程信息</router-link>–>-->
|
||||
<!-- <router-link to="/competition" class="nav-link">赛程信息</router-link>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!– 公共信息区 一级菜单 –>-->
|
||||
<!-- <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>
|
||||
|
@ -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,
|
||||
|
799
src/views/index/ConfigEditor.vue
Normal file
799
src/views/index/ConfigEditor.vue
Normal 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([])
|
||||
// 修改计算属性:从entries和uploadedTags中提取标签列表
|
||||
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)
|
||||
}
|
||||
|
||||
// 提取LogicCommand和LogicCommandSet节点
|
||||
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 {
|
||||
// 用户输入的是片段,添加header和footer
|
||||
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
1073
src/views/index/Home.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user