添加了Config.xml 编辑器

This commit is contained in:
Kunagisa 2025-07-19 13:39:02 +08:00
parent 247cfbd0a9
commit 04bf94df0c
9 changed files with 658 additions and 3 deletions

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

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

View File

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

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

@ -111,7 +111,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'

View File

@ -50,6 +50,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',

View File

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

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

@ -378,6 +378,7 @@ function handlePasswordChangeError(errorMessage) {
<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>

View File

@ -0,0 +1,468 @@
<template>
<div class="config-editor">
<div class="page-header">
<h1>Config.xml 编辑器</h1>
</div>
<div class="editor-container">
<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>
</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"
></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 } 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 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 = ''
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>
.config-editor {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.editor-container {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
padding: 24px;
}
.input-section {
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 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: 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;
}
}
</style>