Accept Merge Request #48: (feature/login-screen -> master)

Merge Request: 推master

Created By: @数据分析中台
Accepted By: @数据分析中台
URL: https://serverless-100021553598.coding.net/p/zybdatacenter/d/DCFronted/git/merge/48?initial=true
This commit is contained in:
数据分析中台 2025-06-20 14:00:25 +08:00 committed by Coding
commit 1025e475dd
12 changed files with 835 additions and 147 deletions

View File

@ -13,12 +13,13 @@ const axiosInstance = axios.create({
timeout: 10000 timeout: 10000
}); });
/** export function setupInterceptors() {
/**
* 请求拦截器 * 请求拦截器
* - 对需要认证的请求在请求头中添加Authorization * - 对需要认证的请求在请求头中添加Authorization
* - 对登录注册和获取列表的请求不添加Authorization * - 对登录注册和获取列表的请求不添加Authorization
*/ */
axiosInstance.interceptors.request.use( axiosInstance.interceptors.request.use(
config => { config => {
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
const url = config.url; const url = config.url;
@ -26,8 +27,7 @@ axiosInstance.interceptors.request.use(
// 定义不需要Token的接口条件 // 定义不需要Token的接口条件
const noAuthRequired = const noAuthRequired =
url === '/user/login' || url === '/user/login' ||
url === '/user/register' || // 明确添加注册接口 url === '/user/register';
url.endsWith('/getlist');
if (token && !noAuthRequired) { if (token && !noAuthRequired) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
@ -37,13 +37,13 @@ axiosInstance.interceptors.request.use(
error => { error => {
return Promise.reject(error); return Promise.reject(error);
} }
); );
/** /**
* 响应拦截器 * 响应拦截器
* - 如果收到401错误未授权并且不是来自登录请求则调用logoutUser函数清除用户凭证 * - 如果收到401错误未授权并且不是来自登录请求则调用logoutUser函数清除用户凭证
*/ */
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
response => response, response => response,
error => { error => {
const originalRequest = error.config; const originalRequest = error.config;
@ -55,6 +55,7 @@ axiosInstance.interceptors.response.use(
// 不需要额外的console.error错误会自然地在调用处被捕获或显示在网络请求中 // 不需要额外的console.error错误会自然地在调用处被捕获或显示在网络请求中
return Promise.reject(error); return Promise.reject(error);
} }
); );
}
export default axiosInstance; export default axiosInstance;

View File

@ -28,9 +28,10 @@ export const addDemand = async (demandData) => {
try { try {
const payload = { const payload = {
...demandData, ...demandData,
content: demandData.sendcontent // 确保 content 与 sendcontent 一致 content: demandData.content // 直接使用传入的 content
}; };
const response = await axiosInstance.post('/demands/add', payload); const response = await axiosInstance.post('/demands/add', payload);
console.log('添加需求的数据:', payload);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('添加需求失败:', error); console.error('添加需求失败:', error);
@ -49,14 +50,10 @@ export const addDemand = async (demandData) => {
export const updateDemand = async (id, dataToUpdate) => { export const updateDemand = async (id, dataToUpdate) => {
try { try {
const payload = { const payload = {
sendcontent: dataToUpdate.sendcontent, ...dataToUpdate,
// 根据 DemandModel补齐其他必填或可选字段即使它们不被后端 update 逻辑使用 content: dataToUpdate.content // 直接使用传入的 content
requester: dataToUpdate.requester || '',
qq_code: dataToUpdate.qq_code || '',
content: dataToUpdate.sendcontent, // 保持一致
reward: dataToUpdate.reward || '',
date: dataToUpdate.date || new Date().toISOString().slice(0, 19).replace('T', ' ') // 确保有日期
}; };
console.log('更新需求的数据:', payload);
const response = await axiosInstance.put(`/demands/update/${id}`, payload); const response = await axiosInstance.put(`/demands/update/${id}`, payload);
return response.data; return response.data;
} catch (error) { } catch (error) {

View File

@ -36,8 +36,8 @@
<td>{{ demand.reward || '无赏金' }}</td> <td>{{ demand.reward || '无赏金' }}</td>
<td>{{ formatDate(demand.date) }}</td> <td>{{ formatDate(demand.date) }}</td>
<td> <td>
<!-- <button @click="editDemand(demand)" class="action-button edit">编辑</button>--> <button @click="editDemand(demand)" class="action-button edit">编辑</button>
<!-- <button @click="confirmDeleteDemand(demand.id)" class="action-button delete">删除</button>--> <button @click="confirmDeleteDemand(demand.id)" class="action-button delete">删除</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -108,12 +108,15 @@
</div> </div>
</div> </div>
<ErrorDialog :visible="showErrorDialog" :message="errorDialogMsg" @close="showErrorDialog = false" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; // import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; //
import ErrorDialog from '@/components/ErrorDialog.vue'
const demands = ref([]); const demands = ref([]);
const loading = ref(false); const loading = ref(false);
@ -135,6 +138,9 @@ const initialAddFormState = {
const addForm = ref({ ...initialAddFormState }); const addForm = ref({ ...initialAddFormState });
const addError = ref(''); const addError = ref('');
const showErrorDialog = ref(false)
const errorDialogMsg = ref('')
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return 'N/A'; if (!dateString) return 'N/A';
try { try {
@ -185,22 +191,20 @@ const submitEditForm = async () => {
if (!currentDemand.value || !currentDemand.value.id) return; if (!currentDemand.value || !currentDemand.value.id) return;
editError.value = ''; editError.value = '';
try { try {
// updateDemand API payload
const payload = { const payload = {
requester: editForm.value.requester, requester: editForm.value.requester,
qq_code: editForm.value.qq_code, qq_code: editForm.value.qq_code,
sendcontent: editForm.value.sendcontent, sendcontent: editForm.value.sendcontent,
reward: editForm.value.reward, reward: editForm.value.reward,
date: editForm.value.date // 使 date: editForm.value.date
// content API sendcontent
}; };
await updateDemand(currentDemand.value.id, payload); await updateDemand(currentDemand.value.id, payload);
alert('需求更新成功!'); alert('需求更新成功!');
closeEditModal(); closeEditModal();
fetchDemandsAdmin(); fetchDemandsAdmin();
} catch (error) { } catch (error) {
console.error('更新需求失败:', error); errorDialogMsg.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
editError.value = error.response?.data?.detail || error.message || '更新失败,请重试。'; showErrorDialog.value = true;
} }
}; };
@ -211,8 +215,8 @@ const confirmDeleteDemand = async (id) => {
alert('需求删除成功!'); alert('需求删除成功!');
fetchDemandsAdmin(); fetchDemandsAdmin();
} catch (error) { } catch (error) {
console.error('删除需求失败:', error); errorDialogMsg.value = error.response?.data?.detail || error.message || '删除失败,请稍后重试';
alert('删除需求失败: ' + (error.message || '请稍后重试')); showErrorDialog.value = true;
} }
} }
}; };

View File

@ -112,9 +112,6 @@ const refreshCaptcha = async () => {
} }
} }
onMounted(() => {
refreshCaptcha()
})
const handleLogin = async () => { const handleLogin = async () => {
try { try {
@ -203,6 +200,11 @@ const validateForm = () => {
return true return true
} }
onMounted(() => {
refreshCaptcha()
})
</script> </script>
<style scoped> <style scoped>

View File

@ -2,7 +2,13 @@ import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import './assets/styles/common.css' import './assets/styles/common.css'
import { setupInterceptors } from './api/axiosConfig'; // 导入拦截器设置函数
// 在应用初始化最开始就设置好拦截器
setupInterceptors();
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { hasValidToken, getUserInfo, logoutUser } from '../utils/jwt'; import { hasValidToken, getUserInfo, logoutUser, getStoredUser } from '../utils/jwt';
import { justLoggedIn } from '../utils/authSessionState'; import { justLoggedIn } from '../utils/authSessionState';
import EditorsMaps from '@/views/index/EditorsMaps.vue' import EditorsMaps from '@/views/index/EditorsMaps.vue'
@ -113,40 +113,44 @@ const router = createRouter({
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth); const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth) { if (!requiresAuth) {
const tokenExists = hasValidToken(); return next(); // 如果页面不需要认证,直接放行
}
// 方案1: 检查SessionStorage中是否已有用户信息 (刷新后最先检查这里)
if (getStoredUser()) {
return next();
}
// 方案2: 如果刚登录过,直接放行
if (justLoggedIn.value) { if (justLoggedIn.value) {
justLoggedIn.value = false; justLoggedIn.value = false;
if (tokenExists) { // 此时token肯定有效但可能还没来得及请求用户信息
next(); // 为确保用户信息被存储,可以调用一次,但不阻塞导航
} else { getUserInfo().catch(err => console.error("Error fetching user info after login:", err));
logoutUser(); return next();
next({ path: '/backend/login', query: { redirect: to.fullPath, sessionExpired: 'true' }});
}
return;
} }
if (tokenExists) { // 方案3: 检查localStorage中是否有token (刷新后SessionStorage没有用户信息时)
const user = await getUserInfo(); if (hasValidToken()) {
if (user) { try {
next(); await getUserInfo(); // 尝试获取用户信息该函数会把用户信息存入SessionStorage
} else { return next(); // 获取成功,放行
logoutUser(); } catch (error) {
next({ // 获取失败 (token无效, 网络问题等)
logoutUser(); // 清除所有凭证
return next({
path: '/backend/login', path: '/backend/login',
query: { redirect: to.fullPath, sessionExpired: 'true' } query: { redirect: to.fullPath, sessionExpired: 'true' }
}); });
} }
} else { }
next({
// 方案4: 如果以上条件都不满足,跳转登录页
return next({
path: '/backend/login', path: '/backend/login',
query: { redirect: to.fullPath } query: { redirect: to.fullPath }
}); });
}
} else {
next();
}
}); });
export default router export default router

View File

@ -4,6 +4,23 @@ import { justLoggedIn } from './authSessionState'; // Import the flag
const USER_INFO_URL = '/user'; // 获取用户信息的API端点 const USER_INFO_URL = '/user'; // 获取用户信息的API端点
/**
* sessionStorage 中安全地读取和解析用户信息
* @returns {Object|null} 用户信息对象或null
*/
export const getStoredUser = () => {
const storedUser = sessionStorage.getItem('currentUser');
if (storedUser) {
try {
return JSON.parse(storedUser);
} catch (e) {
console.error('Error parsing stored user info:', e);
return null;
}
}
return null;
};
/** /**
* 检查Token是否存在且理论上是否在有效期内 * 检查Token是否存在且理论上是否在有效期内
* 服务端 /user接口会验证实际有效期 * 服务端 /user接口会验证实际有效期
@ -22,24 +39,18 @@ export const hasValidToken = () => {
*/ */
export const getUserInfo = async () => { export const getUserInfo = async () => {
if (!hasValidToken()) { if (!hasValidToken()) {
console.log(' No token found, skipping getUserInfo.'); throw new Error('No valid token found');
return null;
} }
try { try {
// console.log('jwt.js: Attempting to fetch user info from', USER_INFO_URL);
const response = await axiosInstance.get(USER_INFO_URL); const response = await axiosInstance.get(USER_INFO_URL);
if (response.status !== 200) { const user = response.data;
router.push('/backend/login'); // 将获取到的用户信息存入 sessionStorage
return null; sessionStorage.setItem('currentUser', JSON.stringify(user));
} return user;
// console.log('jwt.js: User info received:', response.data);
return response.data;
} catch (error) { } catch (error) {
router.push('/backend/login'); // 清除可能存在的无效用户信息
// console.error('jwt.js: Error fetching user info:', error.response ? error.response.status : error.message); sessionStorage.removeItem('currentUser');
// 401错误会被响应拦截器处理清除token然后错误会传播到这里 throw error;
// 其他网络错误等也会被捕获
return null;
} }
}; };
@ -51,6 +62,7 @@ export const logoutUser = () => { // 不再是 async因为它不执行异步
// console.log('jwt.js: logoutUser called. Clearing local storage.'); // console.log('jwt.js: logoutUser called. Clearing local storage.');
localStorage.removeItem('access_token'); localStorage.removeItem('access_token');
localStorage.removeItem('user_id'); localStorage.removeItem('user_id');
//sessionStorage.removeItem('currentUser'); // 同时清除sessionStorage中的用户信息
// 导航将由调用者(如路由守卫)处理 // 导航将由调用者(如路由守卫)处理
}; };

View File

@ -84,7 +84,7 @@
<td>{{ competition.format === 'single' ? '单败淘汰' : <td>{{ competition.format === 'single' ? '单败淘汰' :
competition.format === 'double' ? '双败淘汰' : '积分赛' }}</td> competition.format === 'double' ? '双败淘汰' : '积分赛' }}</td>
<td class="action-cell"> <td class="action-cell">
<button class="action-btn view" @click.stop="handleSignUp(competition)"> <button class="action-btn view" @click.stop="handleSignUp(competition)" :disabled="competition.status === 'finish'">
报名 报名
</button> </button>
</td> </td>
@ -440,6 +440,14 @@ refreshCompetitions()
cursor: not-allowed; cursor: not-allowed;
} }
.action-btn:disabled {
background: #e0e0e0 !important;
color: #b0b0b0 !important;
cursor: not-allowed;
border: none;
opacity: 1;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.competition-page { .competition-page {
padding: 12px; padding: 12px;

View File

@ -6,7 +6,7 @@
<div v-else class="detail-card"> <div v-else class="detail-card">
<div class="nav-back"> <div class="nav-back">
<button class="back-btn" @click="handleBack"> 返回列表</button> <button class="back-btn" @click="handleBack"> 返回列表</button>
<div class="action-buttons"> <div class="action-buttons" v-if="isOrganizer">
<button class="status-btn" @click="openStatusDialog">修改状态</button> <button class="status-btn" @click="openStatusDialog">修改状态</button>
<button class="edit-btn" @click="openEditDialog">编辑赛事</button> <button class="edit-btn" @click="openEditDialog">编辑赛事</button>
<button class="delete-btn" @click="confirmDelete">删除赛事</button> <button class="delete-btn" @click="confirmDelete">删除赛事</button>
@ -60,7 +60,7 @@
<td>{{ player.lose }}</td> <td>{{ player.lose }}</td>
<td>{{ player.status }}</td> <td>{{ player.status }}</td>
<td class="action-buttons"> <td class="action-buttons">
<button class="edit-player-btn" @click="handleEditPlayer(player)">修改</button> <button class="edit-player-btn" @click="handleEditPlayer(player)" >修改</button>
<button class="remove-btn" @click="handleRemovePlayer(player.id)">移除</button> <button class="remove-btn" @click="handleRemovePlayer(player.id)">移除</button>
</td> </td>
</tr> </tr>
@ -225,6 +225,7 @@ import {
updateSignUpResult, updateSignUpResult,
deleteSignUpResult deleteSignUpResult
} from '@/api/tournament' } from '@/api/tournament'
import { getStoredUser } from '@/utils/jwt'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -263,7 +264,7 @@ const competition = ref({
}) })
const finalResults = ref([]) const finalResults = ref([])
const isEditMode = ref(false) const isEditMode = ref(false)
const isOrganizer = ref(false) // TODO: const isOrganizer = ref(false)
const isSaving = ref(false) const isSaving = ref(false)
const showEditForm = ref(false) const showEditForm = ref(false)
const isUpdating = ref(false) const isUpdating = ref(false)
@ -421,6 +422,14 @@ const fetchTournamentDetail = async () => {
format: formatType(tournament.format) format: formatType(tournament.format)
} }
//
const user = getStoredUser()
if (user && user.qq_code && tournament.qq_code) {
isOrganizer.value = String(user.qq_code) === String(tournament.qq_code)
} else {
isOrganizer.value = false
}
// //
if (tournament.status === 'prepare') { if (tournament.status === 'prepare') {
await fetchRegisteredPlayers() await fetchRegisteredPlayers()

View File

@ -2,9 +2,12 @@
<div class="demand-hall"> <div class="demand-hall">
<div class="page-header"> <div class="page-header">
<h1>需求列表</h1> <h1>需求列表</h1>
<h3 class="warning-tip">
免责声明该功能仅做预约联系使用不设计现实中的货币账户一般等价物请用户分辨明细防止电信诈骗如出现任何问题与该平台无关
</h3>
</div> </div>
<button class="btn-common btn-gradient btn-margin-right" @click="openAddModal">添加需求</button> <button class="btn-common btn-gradient btn-margin-right" @click="openAddModal">添加需求</button>
<button class="btn-common btn-light" @click="fetchDemands">刷新</button> <button class="btn-common btn-light" @click="onRefresh">刷新</button>
<div class="table-container"> <div class="table-container">
<table class="maps-table"> <table class="maps-table">
<thead> <thead>
@ -52,6 +55,13 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="add-modal-form" @submit.prevent="submitReply"> <form class="add-modal-form" @submit.prevent="submitReply">
<div class="form-row">
<span class="label">回复消息</span>
<select v-model="addForm.replyTo" class="input">
<option value=""></option>
<option v-for="msg in replyOptions" :key="msg.id" :value="msg.id">{{ msg.text }}</option>
</select>
</div>
<div class="form-row"> <div class="form-row">
<span class="label">昵称</span> <span class="label">昵称</span>
<input <input
@ -71,7 +81,7 @@
/> />
</div> </div>
<div class="form-row"> <div class="form-row">
<span class="label">相关建议</span> <span class="label">内容</span>
<textarea <textarea
v-model="addForm.sendcontent" v-model="addForm.sendcontent"
class="input" class="input"
@ -97,8 +107,14 @@
<div class="modal-content" @click.stop> <div class="modal-content" @click.stop>
<div class="modal-header"> <div class="modal-header">
<h2>需求详情</h2> <h2>需求详情</h2>
<div style="display:flex;align-items:center;gap:16px;">
<template v-if="isOwner">
<button class="edit-btn big-action-btn" @click="openEditModal">修改</button>
<button class="delete-btn big-action-btn" @click="handleDeleteDemand">删除</button>
</template>
<button class="close-btn" @click="closeModal">&times;</button> <button class="close-btn" @click="closeModal">&times;</button>
</div> </div>
</div>
<div class="modal-body"> <div class="modal-body">
<div class="detail-item"> <div class="detail-item">
<span class="label">QQ号</span> <span class="label">QQ号</span>
@ -140,9 +156,9 @@
<div v-if="selectedDemand?.sendcontent"> <div v-if="selectedDemand?.sendcontent">
<div v-for="(reply, index) in selectedDemand.sendcontent.split('|')" :key="index" class="reply-content"> <div v-for="(reply, index) in selectedDemand.sendcontent.split('|')" :key="index" class="reply-content">
<div class="reply-with-avatar"> <div class="reply-with-avatar">
<img :src="`https://q1.qlogo.cn/g?b=qq&nk=${reply.match(/\((\d+)\)/)?.[1] || ''}&s=40`" alt="User Avatar" class="reply-avatar" /> <img :src="`https://q1.qlogo.cn/g?b=qq&nk=${extractQQ(reply)}&s=40`" alt="User Avatar" class="reply-avatar" />
<div class="reply-text"> <div class="reply-text">
<b>{{ reply.split(')')[0] + '' }}</b>{{ reply.split(')')[1] }} <b>{{ parseReplyText(reply).user }}</b> {{ parseReplyText(reply).content }}
</div> </div>
</div> </div>
</div> </div>
@ -164,6 +180,9 @@
<h2>添加需求</h2> <h2>添加需求</h2>
<button class="close-btn" @click="closeAddModal">&times;</button> <button class="close-btn" @click="closeAddModal">&times;</button>
</div> </div>
<div class="modal-body" style="background:linear-gradient(90deg,#fffbe6 0%,#fff1b8 100%);color:#ad8b00;font-weight:bold;text-align:center;border-radius:8px;padding:12px 10px;margin:18px 0 18px 0;font-size:16px;box-shadow:0 2px 8px rgba(173,139,0,0.08);display:flex;align-items:center;gap:8px;justify-content:center;">
<span style="font-size:20px;color:#faad14;margin-right:8px;">&#9888;</span>需求一经发布不许修改
</div>
<div class="modal-body"> <div class="modal-body">
<form class="add-modal-form" @submit.prevent="submitAddForm"> <form class="add-modal-form" @submit.prevent="submitAddForm">
<div class="form-row"> <div class="form-row">
@ -172,9 +191,16 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<span class="label">QQ号</span> <span class="label">QQ号</span>
<input v-model="addForm.qq_code" class="input" placeholder="可选" /> <input
v-model="addForm.qq_code"
class="input"
placeholder="可选"
type="text"
pattern="[0-9]*"
inputmode="numeric"
readonly
/>
</div> </div>
<div class="form-row"> <div class="form-row">
<span class="label">需求内容</span> <span class="label">需求内容</span>
<textarea <textarea
@ -198,12 +224,59 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 修改需求弹窗 -->
<div v-if="showEditModal" class="modal-overlay" @click="showEditModal=false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2>修改需求</h2>
<button class="close-btn" @click="showEditModal=false">&times;</button>
</div>
<div class="modal-body">
<form class="add-modal-form" @submit.prevent="handleEditSubmit">
<div class="form-row">
<span class="label">请求者</span>
<input v-model="editForm.requester" class="input" required />
</div>
<div class="form-row">
<span class="label">QQ号</span>
<input v-model="editForm.qq_code" class="input" required readonly />
</div>
<div class="form-row">
<span class="label">需求内容</span>
<textarea v-model="editForm.content" class="input" rows="3" required></textarea>
</div>
<div class="form-row">
<span class="label">赏金</span>
<input v-model="editForm.reward" class="input" />
</div>
<button class="btn-common btn-gradient submit-btn" type="submit">保存</button>
</form>
</div>
</div>
</div>
<div v-if="showDeleteConfirm" class="modal-overlay" style="z-index:2000;">
<div class="modal-content" style="max-width:350px;text-align:center;">
<div class="modal-header">
<h2 style="color:#F56C6C;">删除确认</h2>
</div>
<div class="modal-body" style="font-size:16px;">确定要删除该需求吗此操作不可恢复</div>
<div class="delete-dialog-footer" style="display:flex;justify-content:center;gap:18px;margin:18px 0 8px 0;">
<button class="confirm-button" @click="confirmDelete">确认删除</button>
<button class="cancel-button" @click="cancelDelete">取消</button>
</div>
</div>
</div>
<ErrorDialog :visible="showErrorDialog" :message="errorDialogMsg" @close="showErrorDialog = false" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick, computed } from 'vue'
import { getDemandsList, addDemand, updateDemand } from '../../api/demands' import { getDemandsList, addDemand, updateDemand, deleteDemand } from '../../api/demands'
import { getStoredUser } from '@/utils/jwt'
import ErrorDialog from '@/components/ErrorDialog.vue'
// //
const demands = ref([]) const demands = ref([])
@ -222,10 +295,24 @@ const addForm = ref({
qq_code: '', qq_code: '',
sendcontent: '', sendcontent: '',
author: '', author: '',
author_contact: '' author_contact: '',
replyTo: ''
}) })
const addLoading = ref(false) const addLoading = ref(false)
const autoTextarea = ref(null) const autoTextarea = ref(null)
const replyOptions = ref([])
const showEditModal = ref(false)
const editForm = ref({
requester: '',
qq_code: '',
content: '',
reward: '',
id: null
})
const showErrorDialog = ref(false)
const errorDialogMsg = ref('')
const showDeleteConfirm = ref(false)
const pendingDeleteId = ref(null)
// //
const isNoReward = (reward) => { const isNoReward = (reward) => {
@ -255,6 +342,11 @@ const fetchDemands = async () => {
try { try {
const data = await getDemandsList() const data = await getDemandsList()
demands.value = data demands.value = data
replyOptions.value = data.map(demand => ({
id: demand.id,
author: demand.requester,
content: demand.content
}))
} catch (err) { } catch (err) {
error.value = `加载失败: ${err.message}` error.value = `加载失败: ${err.message}`
console.error('加载需求列表失败:', err) console.error('加载需求列表失败:', err)
@ -267,6 +359,24 @@ const fetchDemands = async () => {
const showReply = (demand) => { const showReply = (demand) => {
reply.value = demand reply.value = demand
replyModal.value = true replyModal.value = true
// sendcontent
if (demand.sendcontent) {
replyOptions.value = demand.sendcontent.split('|').map((msg, idx) => {
// qq
// qq (qq)
let match = msg.match(/^(.+?)[(](\d+)[)]/)
let nickname = match ? match[1] : ''
let qq = match ? match[2] : ''
return {
id: idx,
text: msg,
nickname,
qq
}
})
} else {
replyOptions.value = []
}
resetReplyForm(); resetReplyForm();
} }
@ -281,7 +391,8 @@ const resetReplyForm = () => {
addForm.value = { addForm.value = {
sendcontent: '', sendcontent: '',
author: '', author: '',
author_contact: '' author_contact: '',
replyTo: ''
}; };
addError.value = ''; addError.value = '';
// //
@ -303,8 +414,20 @@ const closeModal = () => {
} }
// //
const openAddModal = (demand) => { const openAddModal = () => {
reply.value = demand; //
const user = getStoredUser()
addForm.value = {
requester: '',
content: '',
reward: '',
qq_code: user && user.qq_code ? user.qq_code : '',
sendcontent: '',
author: '',
author_contact: '',
replyTo: ''
};
addError.value = '';
showAddModal.value = true; showAddModal.value = true;
} }
@ -314,12 +437,25 @@ function closeAddModal() {
addError.value = '' addError.value = ''
} }
// // script setup
const validateQQ = (event) => {
//
addForm.value.qq_code = event.target.value.replace(/[^\d]/g, '');
}
// submitAddForm QQ
async function submitAddForm() { async function submitAddForm() {
if (!addForm.value.content?.trim()) { if (!addForm.value.content?.trim()) {
addError.value = '需求内容不能为空'; addError.value = '需求内容不能为空';
return; return;
} }
// QQ
if (addForm.value.qq_code && !/^\d+$/.test(addForm.value.qq_code)) {
addError.value = 'QQ号必须为纯数字';
return;
}
addLoading.value = true; addLoading.value = true;
addError.value = ''; addError.value = '';
try { try {
@ -373,7 +509,15 @@ const submitReply = async () => {
const dateStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`; const dateStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
// //
const newReply = `${addForm.value.author}(${addForm.value.author_contact})${addForm.value.sendcontent}`; let newReply = '';
if (addForm.value.replyTo !== '' && replyOptions.value.length > 0) {
const targetMsg = replyOptions.value.find(msg => String(msg.id) === String(addForm.value.replyTo));
let targetName = targetMsg && targetMsg.nickname ? targetMsg.nickname : '';
newReply = `${addForm.value.author}${addForm.value.author_contact})回复${targetName ? ' ' + targetName : ''}${addForm.value.sendcontent}`;
} else {
newReply = `${addForm.value.author}${addForm.value.author_contact}${addForm.value.sendcontent}`;
}
const formattedContent = reply.value.sendcontent const formattedContent = reply.value.sendcontent
? `${reply.value.sendcontent}|${newReply}` ? `${reply.value.sendcontent}|${newReply}`
: newReply; : newReply;
@ -416,6 +560,99 @@ function autoResize() {
} }
}) })
} }
function extractQQ(str) {
const match = str.match(/[(]([1-9][0-9]{4,})[)]/)
return match ? match[1] : ''
}
function parseReplyText(str) {
// /
const match = str.match(/^(.+?[(][1-9][0-9]{4,}[)])(.*)$/)
if (match) {
// match[1] QQmatch[2] " xxx "
return {
user: match[1].trim(),
content: match[2].replace(/^|^:/, '').trim()
}
}
// fallback
return {
user: '',
content: str
}
}
const isOwner = computed(() => {
const user = getStoredUser()
return user && selectedDemand.value && user.qq_code && selectedDemand.value.qq_code && String(user.qq_code) === String(selectedDemand.value.qq_code)
})
function openEditModal() {
if (!selectedDemand.value) return
const user = getStoredUser()
editForm.value = {
requester: selectedDemand.value.requester || '',
qq_code: user && user.qq_code ? user.qq_code : selectedDemand.value.qq_code || '',
content: selectedDemand.value.content || '',
reward: selectedDemand.value.reward || '',
id: selectedDemand.value.id
}
showEditModal.value = true
}
async function handleEditSubmit() {
if (!editForm.value.content?.trim()) {
errorDialogMsg.value = '需求内容不能为空'
showErrorDialog.value = true
return
}
try {
await updateDemand(editForm.value.id, {
requester: editForm.value.requester,
qq_code: editForm.value.qq_code,
content: editForm.value.content,
reward: editForm.value.reward,
date: selectedDemand.value.date,
sendcontent: selectedDemand.value.sendcontent
})
showEditModal.value = false
fetchDemands()
closeModal()
} catch (e) {
errorDialogMsg.value = '修改失败: ' + (e.response?.data?.detail || e.message)
showErrorDialog.value = true
}
}
function handleDeleteDemand() {
if (!selectedDemand.value) return
pendingDeleteId.value = selectedDemand.value.id
showDeleteConfirm.value = true
}
async function confirmDelete() {
try {
await deleteDemand(pendingDeleteId.value)
fetchDemands()
closeModal()
} catch (e) {
errorDialogMsg.value = '删除失败: ' + (e.response?.data?.detail || e.message)
showErrorDialog.value = true
} finally {
showDeleteConfirm.value = false
pendingDeleteId.value = null
}
}
function cancelDelete() {
showDeleteConfirm.value = false
pendingDeleteId.value = null
}
const onRefresh = () => {
fetchDemands()
}
</script> </script>
<style scoped> <style scoped>
@ -794,4 +1031,98 @@ function autoResize() {
color: #2563eb; color: #2563eb;
font-size: 15px; font-size: 15px;
} }
.warning-tip {
background: linear-gradient(90deg, #ffeaea 0%, #ffd6d6 100%);
color: #d32f2f;
font-weight: bold;
text-align: center;
border-radius: 8px;
padding: 14px 18px;
margin: 18px 0 24px 0;
font-size: 18px;
box-shadow: 0 2px 8px rgba(211,47,47,0.08);
display: flex;
align-items: center;
gap: 10px;
}
.warning-tip::before {
content: '\26A0'; /* 警告符号 */
font-size: 22px;
color: #d32f2f;
margin-right: 8px;
}
.big-action-btn {
padding: 10px 28px;
font-size: 18px;
border-radius: 8px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(65, 107, 223, 0.10);
transition: background 0.2s, box-shadow 0.2s, color 0.2s;
}
.big-action-btn.btn-gradient:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
color: #fff;
}
.big-action-btn.btn-light:hover {
background: #f5f7fa;
color: #2563eb;
border-color: #2563eb;
}
.edit-btn.big-action-btn {
background: #409EFF;
color: #fff;
border: none;
padding: 7px 18px;
font-size: 15px;
border-radius: 8px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(64,158,255,0.10);
transition: background 0.2s, box-shadow 0.2s, color 0.2s;
}
.edit-btn.big-action-btn:hover {
background: #66b1ff;
}
.delete-btn.big-action-btn {
background: #F56C6C;
color: #fff;
border: none;
padding: 7px 18px;
font-size: 15px;
border-radius: 8px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(245,108,108,0.10);
transition: background 0.2s, box-shadow 0.2s, color 0.2s;
}
.delete-btn.big-action-btn:hover {
background: #f78989;
}
.confirm-button {
background-color: #f56c6c;
color: white;
border: none;
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.confirm-button:hover {
background-color: #f78989;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
border: none;
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
</style> </style>

View File

@ -301,7 +301,16 @@ const changePage = (page) => {
// //
const goToMapDetail = (id) => { const goToMapDetail = (id) => {
sessionStorage.setItem('maps_view_mode', viewMode.value) //
const state = {
viewMode: viewMode.value,
currentPage: currentPage.value,
searchValue: searchValue.value,
playerCountFilter: playerCountFilter.value,
tagFilter: tagFilter.value,
selectedOrder: selectedOrder.value
}
sessionStorage.setItem('maps_state', JSON.stringify(state))
router.push(`/map/${id}`) router.push(`/map/${id}`)
} }
@ -324,13 +333,26 @@ const scrollToTop = () => {
} }
onMounted(() => { onMounted(() => {
const savedMode = sessionStorage.getItem('maps_view_mode') //
if (savedMode) { const savedState = sessionStorage.getItem('maps_state')
viewMode.value = savedMode if (savedState) {
} else if (window.innerWidth <= 700) { const state = JSON.parse(savedState)
viewMode.value = state.viewMode
currentPage.value = state.currentPage
searchValue.value = state.searchValue
playerCountFilter.value = state.playerCountFilter
tagFilter.value = state.tagFilter
selectedOrder.value = state.selectedOrder
// 使
fetchMaps(currentPage.value)
} else {
//
if (window.innerWidth <= 700) {
viewMode.value = 'card' viewMode.value = 'card'
} }
fetchMaps(1) fetchMaps(1)
}
getAllTags().then(tags => { getAllTags().then(tags => {
tagOptions.value = tags tagOptions.value = tags
}) })

View File

@ -725,24 +725,316 @@ const allAttributes = computed(() => {
const attrs = new Set(); const attrs = new Set();
// //
//
const attributeTranslations = { const attributeTranslations = {
'id': '单位ID', // ================= =================
'weapon': '武器', 'id': '武器ID',
'name': '名称', 'inheritFrom': '继承自',
'type': '类型', 'AttackRange': '射程',
'damage': '伤害值', 'MinimumAttackRange': '最小射程',
'range': '射程', 'RangeBonusMinHeight': '射程加成最小高度',//
'speed': '速度', 'RangeBonus': '射程加成值',//
'accuracy': '精准度', 'RangeBonusPerFoot': '每英尺射程加成',//
'ammo': '弹药量', 'RequestAssistRange': '请求援助范围',//
'reload': '装弹时间', 'AcceptableAimDelta': '允许瞄准偏差角',
'level': '等级', 'AimDirection': '基准瞄准方向', //
'cost': '消耗', 'ScatterRadius': '散射半径',
'cooldown': '冷却时间', 'ScatterLength': '散射长度',
'effect': '效果', 'ScatterIndependently': '多抛射物独立散射', //V4
'target': '目标类型', 'WeaponSpeed': '武器/抛射物速度',
'radius': '作用半径', 'MinWeaponSpeed': '最小抛射物速度',
'duration': '持续时间' 'MaxWeaponSpeed': '最大抛射物速度',
'ScaleWeaponSpeed': '缩放抛射物速度',//
'IgnoresContactPoints': '忽略碰撞体积(将武器瞄准目标几何中心)',
'ScaleAttackRangeByAmmoRemaining': '射程随弹药剩余量缩放',//
'CanBeDodged': '可被躲避',//
'IdleAfterFiringDelaySeconds': '开火后待机延迟',//
'HoldAfterFiringDelaySeconds': '开火后保持延迟',//
'HoldDuringReload': '保持先前姿态时即可装填',
'CanFireWhileMoving': '移动射击能力',
'WeaponRecoil': '启用后坐力',
'MinTargetPitch': '最小俯仰角',
'MaxTargetPitch': '最大俯仰角',
'PreferredTargetBone': '首选目标骨骼',
'FireSound': '开火音效',
'FireSoundPerClip': '整弹匣开火音效',
'FiringLoopSound': '持续开火音效',
'FiringLoopSoundContinuesDuringReload': '装填时保持开火音效',
'FireFX': '开火特效',
'FireVeteranFX': '升级后开火特效', //
'FireFlankFX': '侧翼开火特效',
'PreAttackFX': '预攻击特效',
'ClipSize': '弹匣容量',
'ContinuousFireOne': '第一阶段连续射击次数',
'ContinuousFireTwo': '第二阶段连续射击次数',
'ContinuousFireCoastSeconds': '连续射击间隔', //
'AutoReloadWhenIdleSeconds': '空闲时自动装弹时间',//
'ShotsPerBarrel': '每炮管射弹数',
'DamageDealtAtSelfPosition': '伤害作用于自身位置', //
'RequiredFiringObjectStatus': '开火所需状态标志',
'ForbiddenFiringObjectStatus': '禁止开火的状态标志',
'CheckStatusFlagsInRangeChecks': '射程检查时包含状态标志',
'ProjectileSelf': '发射自身作为抛射物', //
'MeleeWeapon': '近战武器',
'ChaseWeapon': '追击武器', //
'LeechRangeWeapon': '吸血射程武器',
'HitStoredTarget': '会攻击预设目标',
'CapableOfFollowingWaypoints': '能否跟随路径点',
'ShowsAmmoPips': '是否显示弹药指示器',
'AllowAttackGarrisonedBldgs': '能否攻击驻军建筑',
'PlayFXWhenStealthed': '隐身时是否播放特效',//
'ContinueAttackRange': '持续攻击范围', //
'SuspendFXDelaySeconds': '特效暂停延迟',//
'IsAimingWeapon': '是否为瞄准武器',
'NoVictimNeeded': '是否需要攻击目标',
'HitPercentage': '命中率', //
'HitPassengerPercentage': '乘客命中概率',
'PassengerProportionalAttack': '是否按乘客比例攻击',
'HealthProportionalResolution': '生命值比例分辨率', //
'MaxAttackPassengers': '最大攻击乘客数',
'FinishAttackOnceStarted': '强制完成已开始攻击',//
'RestrictedHeightRange': '高度限制范围',
'CannotTargetCastleVictims': '能否攻击城堡保护目标',
'RequireFollowThru': '攻击动作必须完整执行', //
'ShareTimers': '共享计时器',//
'ShouldPlayUnderAttackEvaEvent': '是否播放受袭语音',
'InstantLoadClipOnActivate': '激活武器时才开始装填(无法立刻发射)',
'Flags': '行为控制标志集',
'LockWhenUsing': '使用时锁定',//
'BombardType': '轰炸类型武器',//
'UseInnateAttributes': '使用先天属性',//
'PreAttackType': '攻击前准备类型',
'ReAcquireDetailType': '目标重锁定模式',
'AutoReloadsClip': '自动装填机制 (AUTO-自动再装填/NONE-无法再装填/RETURN_TO_BASE-只能在基地再装填)',
'SingleAmmoReloadedNotFullSound': '单发装填音效', // CC3
'ClipReloadedSound': '弹匣重装音效', // CC3
'RadiusDamageAffects': '范围伤害影响对象',
'FXTrigger': '特效触发类型',
'ProjectileCollidesWith': '抛射物碰撞对象类型',
'RequiredAntiMask': '可攻击的目标类型',
'ForbiddenAntiMask': '禁止攻击的目标类型',
'StopFiringOnCanBeInvisible': '隐身时停止开火',//
'ProjectileStreamName': '投射物流名称',
'ContactWeapon': '接触式武器',
'UseCenterForRangeCheck': '使用几何中心点计算射程',
'VirtualDamage': '自动分弹模式/虚拟伤害类型(NONE-不进行自动分弹/SOLO-自动分弹时只计算自己/SHARED-自动分弹时计算所有单位的总火力)',
'PreAttackWeapon': '预攻击武器',
'RevealShroudOnFire': '开火时揭露战争迷雾',
'ShouldPlayTargetDeadEvaEvent': '目标死亡时播放语音',
// ================= =================
'MinSeconds': '最小持续时间(秒)',
'MaxSeconds': '最大持续时间(秒)',
// ================= AI =================
'IsAntiGarrisonWeapon': '反驻军武器',
'MaxSpeedOfTarget': '可命中的目标最大速度',
'UseLongLockOnTimeCode': '使用长锁定时间逻辑', //
'UseAsWarheadForDamageCalculations': 'AI伤害计算弹头',
// ================= =================
'Radius': '作用半径',
'PartitionFilterTestType': '属性过滤器作用区域形状',
'ForbiddenTargetObjectStatus': '瘫痪/冲击波失效的目标对象状态',
'ForbiddenTargetModelCondition': '瘫痪/冲击波失效的目标模型状态',
'RequiredUpgrade': '所需升级',
'ForbiddenUpgrade': '禁止升级',
// ================= =================
'Damage': '基础伤害值',
'DamageTaperOff': '伤害衰减系数', //
'MinRadius': '最小作用半径', //
'DamageArc': '扇形伤害范围角度', //
'DamageArcInverted': '反转伤害扇形', //
'DamageMaxHeight': '伤害最大高度', // F使
'DamageMaxHeightAboveTerrain': '伤害最大离地高度',//
'FlankingBonus': '侧翼攻击加成', //
'FlankedScalar': '被侧翼攻击倍率', //
'DelayTimeSeconds': '伤害延迟时间(秒)', //
'DamageType': '伤害类型', // 穿/
'DeathType': '死亡效果类型', // /
'DamageFXType': '伤害特效类型',
'DamageSubType': '伤害子类型', //
'OnlyKillOwnerWhenTriggered': '仅在被触发时杀死拥有者',
'DrainLifeMultiplier': '生命吸取倍率', // RAAALeech
'DrainLife': '生命吸取', // RAAALeech
'DamageSpeed': '伤害传播速度', // F
'UnderAttackOverrideEvaEvent': '覆盖受袭语音事件',
'VictimShroudRevealer': '目标战争迷雾揭示者', //
'NotifyOwnerOnVictimDeath': '目标死亡通知拥有者', //Leech稿BUG
'NotifyObserversOnPreDamageEffectPosition': '伤害位置预报', //
'ForceFXPositionToVictim': '特效绑定目标位置', //
'RadiusAffectsBridges': '影响桥梁',
'InvalidTargetStatus': '无效目标状态标志',
'DamageScalarDetails': '伤害比例详情',
'Scalar': '伤害比例',
'Filter': '对象过滤器',
// ================= =================
'DamageInterval': '伤害间隔(秒)', // 使
'DamageDuration': '伤害总时长(秒)', // 使
'RemoveIfHealed': '治疗时移除效果', // 使
// ================= =================
'PercentDamageToHeal': '伤害转化为治疗百分比',//
'PercentMaxHealthToTake': '最大生命值吸取比例', //
// ================= =================
'ProjectileTemplate': '抛射物模板',
'WarheadTemplate': '弹头模板',
'WeaponLaunchBoneSlotOverride': '武器发射骨骼槽', //
'AttackOffset': '攻击位置偏移',
'VeterancyLevel': '老兵等级',
'SpecificBarrelOverride': '特定炮管覆盖',
'x': 'X轴偏移',
'y': 'Y轴偏移',
'z': 'Z轴偏移',
// ================= =================
'Suppression': '压制强度', //
'DurationSeconds': '效果持续时间(秒)',
'SuppressionTaperOff': '压制衰减系数', //
'SuppressionArc': '压制扇形角度', //
'SuppressionArcInverted': '反转压制扇形', //
// ================= =================
'Lifetime': '激光持续时间',
'LaserId': '激光ID', //
'HitGroundFX': '命中地面特效',
'OverShootDistance': '激光延长线距离',
// ================= =================
'WeaponOCL': '武器对象创建列表', // OCL
'TargetAsPrimaryObject': '设目标为主要对象', //
// ================= =================
'EffectArc': '麻痹效果扇形角度',//
'DurationSeconds': '麻痹持续时间(秒)',
'ParalyzeType': '麻痹类型', // EMP/
'RemoveParalyzeType': '解除麻痹类型', // 使
'ParalyzeFX': '麻痹特效',
// ================= =================
'InfoWarType': '信息战类型', //
'RadarJamRadius': '雷达干扰半径',//
'RadarJamDuration': '雷达干扰持续时间',//
// ================= =================
'AmountToSpend': '消耗泰矿数量',
// ================= =================
'ShockWaveAmount': '冲击波强度',
'ShockWaveRadius': '冲击波半径',
'ShockWaveArc': '扇形冲击波范围角度',
'ShockWaveTaperOff': '冲击波衰减',
'ShockWaveSpeed': '冲击波速度',//
'ShockWaveZMult': '垂直方向冲击波系数', //
'CyclonicFactor': '气旋因子', //
'ShockwaveDelaySeconds': '冲击波延迟时间(秒)',
'InvertShockWave': '反转冲击波方向', //
'FlipDirection': '翻转方向',
'OnlyWhenJustDied': '仅在目标刚死亡时触发',
'ShockWaveClearRadius': '冲击波清除半径', //
'ShockWaveClearWaveMult': '清除冲击波倍数',
'ShockWaveClearFlingHeight': '清除抛射高度',
'KillObjectFilter': '击杀对象过滤器',
// ================= =================
'SpecialPowerTemplate': '特殊能力模板',
// ================= =================
'AttributeModifierName': '属性修改器名称',
'AttributeModifierOwnerName': '属性修改器所有者', //
'DamageFXType': '伤害特效类型',
'DamageArc': '伤害作用弧度',
'AntiCategories': '反制类别', //
'AntiFX': '反制特效', //
// ================= 线 =================
//线
'OffsetAngle': '线性伤害偏移角度', //
'LineWidth': '线性伤害宽度', //
'LineLengthLeadIn': '起始线长',//
'LineLengthLeadOut': '结束线长',//
'UseDynamicLineLength': '线性伤害使用动态线长',
'OverShootDistance': '线性伤害延长距离',
// ================= =================
//Tint/
'PreColorTime': '着色淡入时间',
'SustainedColorTime': '持续着色时间',
'PostColorTime': '着色淡出时间',
'Frequency': '脉冲频率', //
'Amplitude': '脉冲幅度', //
'Color': '着色颜色',
'r': '红色分量',
'g': '绿色分量',
'b': '蓝色分量',
// ================= =================
'Depth': '弹坑深度',//
'Lift': '地形隆起比例', //
// ================= =================
'FieldAmount': '矿场数量',//
'SpawnedInFieldBonus': '矿场内生成加成',//
// ================= =================
'ScatterMin': '最小散射角度',//
'ScatterMax': '最大散射角度',//
// ================= =================
'AttachModuleId': 'ATTR模块ID',
// ================= =================
'AmountToStrip': '生命值剥离百分比', //
// ================= =================
'Weapon': '使用的武器',
'FireOnVictimObject': '对目标使用武器', // 使
'VictimMustBeAlive': '目标必须存活',//
'Filter': '对象过滤器',
// ================= =================
'MinTimeToImpactFudgeFactor': '最小命中时间容差', //
'MaxTimeToImpactFudgeFactor': '最大命中时间容差', //
// ================= =================
'Rule': '生效规则(ALL/ANY/NONE)',
'Include': '包含对象类型',
'Exclude': '排除对象类型',
'Relationship': '阵营关系(敌/友/中等)',
'StatusBitFlags': '目标需要的状态标志', //
'StatusBitFlagsExclude': '排除目标的状态标志',
'RequiredModelConditions': '所需模型状态',
// ================= =================
'SpecialObjectFilter': '特殊对象过滤器',
'OverrideVoiceAttackSound': '覆盖攻击语音',//
'OverrideVoiceEnterStateAttackSound': '覆盖状态切换攻击语音',//
'SurpriseAttackObjectFilter': '突袭攻击对象过滤器',
'CombinedAttackObjectFilter': '联合攻击对象过滤器',
'HitStoredObjectFilter': '命中存储对象过滤器',
'ScatterRadiusVsType': '散射类型设置',
'IncompatibleAttributeModifier': '不兼容属性修改器',
// ================= =================
'SpawnTemplate': '生成模板',
'SpawnProbability': '生成概率',
'SpawnedModelConditionFlags': '生成模型状态标志',
'Amount': '伤害量',
'PercentDamageToContained': '对包含单位的伤害比例',
'DamageObjectFilter': '伤害对象过滤器',
'MaxUnitsToDamage': '最大伤害单位数',
'WindowBlastFX': '窗口爆炸特效',
'EventName': '事件名称',
'SendToEnemies': '发送给敌人',
'SendToAllies': '发送给盟友',
'SendToNeutral': '发送给中立单位',
// ================= =================
'ScatterRadiusVsType.Radius': '散射半径',
'ScatterRadiusVsType.RequiredModelConditions': '散射所需模型状态'
// //
}; };