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:
commit
1025e475dd
@ -13,12 +13,13 @@ const axiosInstance = axios.create({
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
/**
|
||||
export function setupInterceptors() {
|
||||
/**
|
||||
* 请求拦截器
|
||||
* - 对需要认证的请求,在请求头中添加Authorization
|
||||
* - 对登录、注册和获取列表的请求,不添加Authorization
|
||||
*/
|
||||
axiosInstance.interceptors.request.use(
|
||||
axiosInstance.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const url = config.url;
|
||||
@ -26,8 +27,7 @@ axiosInstance.interceptors.request.use(
|
||||
// 定义不需要Token的接口条件
|
||||
const noAuthRequired =
|
||||
url === '/user/login' ||
|
||||
url === '/user/register' || // 明确添加注册接口
|
||||
url.endsWith('/getlist');
|
||||
url === '/user/register';
|
||||
|
||||
if (token && !noAuthRequired) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@ -37,13 +37,13 @@ axiosInstance.interceptors.request.use(
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
/**
|
||||
/**
|
||||
* 响应拦截器
|
||||
* - 如果收到401错误(未授权),并且不是来自登录请求,则调用logoutUser函数清除用户凭证
|
||||
*/
|
||||
axiosInstance.interceptors.response.use(
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
@ -55,6 +55,7 @@ axiosInstance.interceptors.response.use(
|
||||
// 不需要额外的console.error,错误会自然地在调用处被捕获或显示在网络请求中
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default axiosInstance;
|
@ -28,9 +28,10 @@ export const addDemand = async (demandData) => {
|
||||
try {
|
||||
const payload = {
|
||||
...demandData,
|
||||
content: demandData.sendcontent // 确保 content 与 sendcontent 一致
|
||||
content: demandData.content // 直接使用传入的 content
|
||||
};
|
||||
const response = await axiosInstance.post('/demands/add', payload);
|
||||
console.log('添加需求的数据:', payload);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('添加需求失败:', error);
|
||||
@ -49,14 +50,10 @@ export const addDemand = async (demandData) => {
|
||||
export const updateDemand = async (id, dataToUpdate) => {
|
||||
try {
|
||||
const payload = {
|
||||
sendcontent: dataToUpdate.sendcontent,
|
||||
// 根据 DemandModel,补齐其他必填或可选字段,即使它们不被后端 update 逻辑使用
|
||||
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', ' ') // 确保有日期
|
||||
...dataToUpdate,
|
||||
content: dataToUpdate.content // 直接使用传入的 content
|
||||
};
|
||||
console.log('更新需求的数据:', payload);
|
||||
const response = await axiosInstance.put(`/demands/update/${id}`, payload);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
@ -36,8 +36,8 @@
|
||||
<td>{{ demand.reward || '无赏金' }}</td>
|
||||
<td>{{ formatDate(demand.date) }}</td>
|
||||
<td>
|
||||
<!-- <button @click="editDemand(demand)" class="action-button edit">编辑</button>-->
|
||||
<!-- <button @click="confirmDeleteDemand(demand.id)" class="action-button delete">删除</button>-->
|
||||
<button @click="editDemand(demand)" class="action-button edit">编辑</button>
|
||||
<button @click="confirmDeleteDemand(demand.id)" class="action-button delete">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -108,12 +108,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ErrorDialog :visible="showErrorDialog" :message="errorDialogMsg" @close="showErrorDialog = false" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; // 确认路径
|
||||
import ErrorDialog from '@/components/ErrorDialog.vue'
|
||||
|
||||
const demands = ref([]);
|
||||
const loading = ref(false);
|
||||
@ -135,6 +138,9 @@ const initialAddFormState = {
|
||||
const addForm = ref({ ...initialAddFormState });
|
||||
const addError = ref('');
|
||||
|
||||
const showErrorDialog = ref(false)
|
||||
const errorDialogMsg = ref('')
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
@ -185,22 +191,20 @@ const submitEditForm = async () => {
|
||||
if (!currentDemand.value || !currentDemand.value.id) return;
|
||||
editError.value = '';
|
||||
try {
|
||||
// 构造符合 updateDemand API 的 payload
|
||||
const payload = {
|
||||
requester: editForm.value.requester,
|
||||
qq_code: editForm.value.qq_code,
|
||||
sendcontent: editForm.value.sendcontent,
|
||||
reward: editForm.value.reward,
|
||||
date: editForm.value.date // 使用表单中的日期
|
||||
// content 会在API层通过 sendcontent 补齐
|
||||
date: editForm.value.date
|
||||
};
|
||||
await updateDemand(currentDemand.value.id, payload);
|
||||
alert('需求更新成功!');
|
||||
closeEditModal();
|
||||
fetchDemandsAdmin();
|
||||
} catch (error) {
|
||||
console.error('更新需求失败:', error);
|
||||
editError.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
|
||||
errorDialogMsg.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
|
||||
showErrorDialog.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -211,8 +215,8 @@ const confirmDeleteDemand = async (id) => {
|
||||
alert('需求删除成功!');
|
||||
fetchDemandsAdmin();
|
||||
} catch (error) {
|
||||
console.error('删除需求失败:', error);
|
||||
alert('删除需求失败: ' + (error.message || '请稍后重试'));
|
||||
errorDialogMsg.value = error.response?.data?.detail || error.message || '删除失败,请稍后重试';
|
||||
showErrorDialog.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -112,9 +112,6 @@ const refreshCaptcha = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshCaptcha()
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
@ -203,6 +200,11 @@ const validateForm = () => {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshCaptcha()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -2,7 +2,13 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/styles/common.css'
|
||||
import { setupInterceptors } from './api/axiosConfig'; // 导入拦截器设置函数
|
||||
|
||||
// 在应用初始化最开始就设置好拦截器
|
||||
setupInterceptors();
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 EditorsMaps from '@/views/index/EditorsMaps.vue'
|
||||
|
||||
@ -113,40 +113,44 @@ const router = createRouter({
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
||||
|
||||
if (requiresAuth) {
|
||||
const tokenExists = hasValidToken();
|
||||
if (!requiresAuth) {
|
||||
return next(); // 如果页面不需要认证,直接放行
|
||||
}
|
||||
|
||||
// 方案1: 检查SessionStorage中是否已有用户信息 (刷新后最先检查这里)
|
||||
if (getStoredUser()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 方案2: 如果刚登录过,直接放行
|
||||
if (justLoggedIn.value) {
|
||||
justLoggedIn.value = false;
|
||||
if (tokenExists) {
|
||||
next();
|
||||
} else {
|
||||
logoutUser();
|
||||
next({ path: '/backend/login', query: { redirect: to.fullPath, sessionExpired: 'true' }});
|
||||
}
|
||||
return;
|
||||
// 此时token肯定有效,但可能还没来得及请求用户信息
|
||||
// 为确保用户信息被存储,可以调用一次,但不阻塞导航
|
||||
getUserInfo().catch(err => console.error("Error fetching user info after login:", err));
|
||||
return next();
|
||||
}
|
||||
|
||||
if (tokenExists) {
|
||||
const user = await getUserInfo();
|
||||
if (user) {
|
||||
next();
|
||||
} else {
|
||||
logoutUser();
|
||||
next({
|
||||
// 方案3: 检查localStorage中是否有token (刷新后SessionStorage没有用户信息时)
|
||||
if (hasValidToken()) {
|
||||
try {
|
||||
await getUserInfo(); // 尝试获取用户信息,该函数会把用户信息存入SessionStorage
|
||||
return next(); // 获取成功,放行
|
||||
} catch (error) {
|
||||
// 获取失败 (token无效, 网络问题等)
|
||||
logoutUser(); // 清除所有凭证
|
||||
return next({
|
||||
path: '/backend/login',
|
||||
query: { redirect: to.fullPath, sessionExpired: 'true' }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next({
|
||||
}
|
||||
|
||||
// 方案4: 如果以上条件都不满足,跳转登录页
|
||||
return next({
|
||||
path: '/backend/login',
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router
|
@ -4,6 +4,23 @@ import { justLoggedIn } from './authSessionState'; // Import the flag
|
||||
|
||||
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是否存在且(理论上)是否在有效期内。
|
||||
* 服务端 /user接口会验证实际有效期。
|
||||
@ -22,24 +39,18 @@ export const hasValidToken = () => {
|
||||
*/
|
||||
export const getUserInfo = async () => {
|
||||
if (!hasValidToken()) {
|
||||
console.log(' No token found, skipping getUserInfo.');
|
||||
return null;
|
||||
throw new Error('No valid token found');
|
||||
}
|
||||
try {
|
||||
// console.log('jwt.js: Attempting to fetch user info from', USER_INFO_URL);
|
||||
const response = await axiosInstance.get(USER_INFO_URL);
|
||||
if (response.status !== 200) {
|
||||
router.push('/backend/login');
|
||||
return null;
|
||||
}
|
||||
// console.log('jwt.js: User info received:', response.data);
|
||||
return response.data;
|
||||
const user = response.data;
|
||||
// 将获取到的用户信息存入 sessionStorage
|
||||
sessionStorage.setItem('currentUser', JSON.stringify(user));
|
||||
return user;
|
||||
} catch (error) {
|
||||
router.push('/backend/login');
|
||||
// console.error('jwt.js: Error fetching user info:', error.response ? error.response.status : error.message);
|
||||
// 401错误会被响应拦截器处理(清除token),然后错误会传播到这里
|
||||
// 其他网络错误等也会被捕获
|
||||
return null;
|
||||
// 清除可能存在的无效用户信息
|
||||
sessionStorage.removeItem('currentUser');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@ -51,6 +62,7 @@ export const logoutUser = () => { // 不再是 async,因为它不执行异步
|
||||
// console.log('jwt.js: logoutUser called. Clearing local storage.');
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_id');
|
||||
//sessionStorage.removeItem('currentUser'); // 同时清除sessionStorage中的用户信息
|
||||
// 导航将由调用者(如路由守卫)处理
|
||||
};
|
||||
|
||||
|
@ -84,7 +84,7 @@
|
||||
<td>{{ competition.format === 'single' ? '单败淘汰' :
|
||||
competition.format === 'double' ? '双败淘汰' : '积分赛' }}</td>
|
||||
<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>
|
||||
</td>
|
||||
@ -440,6 +440,14 @@ refreshCompetitions()
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
background: #e0e0e0 !important;
|
||||
color: #b0b0b0 !important;
|
||||
cursor: not-allowed;
|
||||
border: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.competition-page {
|
||||
padding: 12px;
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div v-else class="detail-card">
|
||||
<div class="nav-back">
|
||||
<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="edit-btn" @click="openEditDialog">编辑赛事</button>
|
||||
<button class="delete-btn" @click="confirmDelete">删除赛事</button>
|
||||
@ -60,7 +60,7 @@
|
||||
<td>{{ player.lose }}</td>
|
||||
<td>{{ player.status }}</td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -225,6 +225,7 @@ import {
|
||||
updateSignUpResult,
|
||||
deleteSignUpResult
|
||||
} from '@/api/tournament'
|
||||
import { getStoredUser } from '@/utils/jwt'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -263,7 +264,7 @@ const competition = ref({
|
||||
})
|
||||
const finalResults = ref([])
|
||||
const isEditMode = ref(false)
|
||||
const isOrganizer = ref(false) // TODO: 从用户状态获取
|
||||
const isOrganizer = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const showEditForm = ref(false)
|
||||
const isUpdating = ref(false)
|
||||
@ -421,6 +422,14 @@ const fetchTournamentDetail = async () => {
|
||||
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') {
|
||||
await fetchRegisteredPlayers()
|
||||
|
@ -2,9 +2,12 @@
|
||||
<div class="demand-hall">
|
||||
<div class="page-header">
|
||||
<h1>需求列表</h1>
|
||||
<h3 class="warning-tip">
|
||||
免责声明:该功能仅做预约联系使用,不设计现实中的货币、账户、一般等价物;请用户分辨明细,防止电信诈骗;如出现任何问题与该平台无关。
|
||||
</h3>
|
||||
</div>
|
||||
<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">
|
||||
<table class="maps-table">
|
||||
<thead>
|
||||
@ -52,6 +55,13 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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">
|
||||
<span class="label">昵称:</span>
|
||||
<input
|
||||
@ -71,7 +81,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span class="label">相关建议:</span>
|
||||
<span class="label">内容:</span>
|
||||
<textarea
|
||||
v-model="addForm.sendcontent"
|
||||
class="input"
|
||||
@ -97,8 +107,14 @@
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<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">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="detail-item">
|
||||
<span class="label">QQ号:</span>
|
||||
@ -140,9 +156,9 @@
|
||||
<div v-if="selectedDemand?.sendcontent">
|
||||
<div v-for="(reply, index) in selectedDemand.sendcontent.split('|')" :key="index" class="reply-content">
|
||||
<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">
|
||||
<b>{{ reply.split('):')[0] + ')' }}</b>{{ reply.split('):')[1] }}
|
||||
<b>{{ parseReplyText(reply).user }}</b> {{ parseReplyText(reply).content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -164,6 +180,9 @@
|
||||
<h2>添加需求</h2>
|
||||
<button class="close-btn" @click="closeAddModal">×</button>
|
||||
</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;">⚠</span>需求一经发布不许修改
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="add-modal-form" @submit.prevent="submitAddForm">
|
||||
<div class="form-row">
|
||||
@ -172,9 +191,16 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<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 class="form-row">
|
||||
<span class="label">需求内容:</span>
|
||||
<textarea
|
||||
@ -198,12 +224,59 @@
|
||||
</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">×</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { getDemandsList, addDemand, updateDemand } from '../../api/demands'
|
||||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||||
import { getDemandsList, addDemand, updateDemand, deleteDemand } from '../../api/demands'
|
||||
import { getStoredUser } from '@/utils/jwt'
|
||||
import ErrorDialog from '@/components/ErrorDialog.vue'
|
||||
|
||||
// 响应式数据
|
||||
const demands = ref([])
|
||||
@ -222,10 +295,24 @@ const addForm = ref({
|
||||
qq_code: '',
|
||||
sendcontent: '',
|
||||
author: '',
|
||||
author_contact: ''
|
||||
author_contact: '',
|
||||
replyTo: ''
|
||||
})
|
||||
const addLoading = ref(false)
|
||||
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) => {
|
||||
@ -255,6 +342,11 @@ const fetchDemands = async () => {
|
||||
try {
|
||||
const data = await getDemandsList()
|
||||
demands.value = data
|
||||
replyOptions.value = data.map(demand => ({
|
||||
id: demand.id,
|
||||
author: demand.requester,
|
||||
content: demand.content
|
||||
}))
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err.message}`
|
||||
console.error('加载需求列表失败:', err)
|
||||
@ -267,6 +359,24 @@ const fetchDemands = async () => {
|
||||
const showReply = (demand) => {
|
||||
reply.value = demand
|
||||
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();
|
||||
}
|
||||
|
||||
@ -281,7 +391,8 @@ const resetReplyForm = () => {
|
||||
addForm.value = {
|
||||
sendcontent: '',
|
||||
author: '',
|
||||
author_contact: ''
|
||||
author_contact: '',
|
||||
replyTo: ''
|
||||
};
|
||||
addError.value = '';
|
||||
// 重置文本框高度
|
||||
@ -303,8 +414,20 @@ const closeModal = () => {
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const openAddModal = (demand) => {
|
||||
reply.value = demand;
|
||||
const openAddModal = () => {
|
||||
// 重置表单数据
|
||||
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;
|
||||
}
|
||||
|
||||
@ -314,12 +437,25 @@ function closeAddModal() {
|
||||
addError.value = ''
|
||||
}
|
||||
|
||||
//提交表单
|
||||
// 在 script setup 部分添加验证函数
|
||||
const validateQQ = (event) => {
|
||||
// 只保留数字
|
||||
addForm.value.qq_code = event.target.value.replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
// 修改 submitAddForm 函数,添加 QQ 号验证
|
||||
async function submitAddForm() {
|
||||
if (!addForm.value.content?.trim()) {
|
||||
addError.value = '需求内容不能为空';
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果填写了QQ号,验证是否为纯数字
|
||||
if (addForm.value.qq_code && !/^\d+$/.test(addForm.value.qq_code)) {
|
||||
addError.value = 'QQ号必须为纯数字';
|
||||
return;
|
||||
}
|
||||
|
||||
addLoading.value = true;
|
||||
addError.value = '';
|
||||
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 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
|
||||
? `${reply.value.sendcontent}|${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] 是昵称(QQ号),match[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>
|
||||
|
||||
<style scoped>
|
||||
@ -794,4 +1031,98 @@ function autoResize() {
|
||||
color: #2563eb;
|
||||
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>
|
@ -301,7 +301,16 @@ const changePage = (page) => {
|
||||
|
||||
// 跳转到地图详情
|
||||
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}`)
|
||||
}
|
||||
|
||||
@ -324,13 +333,26 @@ const scrollToTop = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedMode = sessionStorage.getItem('maps_view_mode')
|
||||
if (savedMode) {
|
||||
viewMode.value = savedMode
|
||||
} else if (window.innerWidth <= 700) {
|
||||
// 恢复保存的状态
|
||||
const savedState = sessionStorage.getItem('maps_state')
|
||||
if (savedState) {
|
||||
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'
|
||||
}
|
||||
fetchMaps(1)
|
||||
}
|
||||
|
||||
getAllTags().then(tags => {
|
||||
tagOptions.value = tags
|
||||
})
|
||||
|
@ -725,24 +725,316 @@ const allAttributes = computed(() => {
|
||||
const attrs = new Set();
|
||||
|
||||
// 属性名翻译映射表
|
||||
//标了问号的就是我不确定是啥的
|
||||
const attributeTranslations = {
|
||||
'id': '单位ID',
|
||||
'weapon': '武器',
|
||||
'name': '名称',
|
||||
'type': '类型',
|
||||
'damage': '伤害值',
|
||||
'range': '射程',
|
||||
'speed': '速度',
|
||||
'accuracy': '精准度',
|
||||
'ammo': '弹药量',
|
||||
'reload': '装弹时间',
|
||||
'level': '等级',
|
||||
'cost': '消耗',
|
||||
'cooldown': '冷却时间',
|
||||
'effect': '效果',
|
||||
'target': '目标类型',
|
||||
'radius': '作用半径',
|
||||
'duration': '持续时间'
|
||||
// ================= 武器模板核心属性 =================
|
||||
'id': '武器ID',
|
||||
'inheritFrom': '继承自',
|
||||
'AttackRange': '射程',
|
||||
'MinimumAttackRange': '最小射程',
|
||||
'RangeBonusMinHeight': '射程加成最小高度',// 高度相关射程加成,没出现过
|
||||
'RangeBonus': '射程加成值',// 高度相关射程加成,没出现过
|
||||
'RangeBonusPerFoot': '每英尺射程加成',// 高度相关射程加成,没出现过
|
||||
'RequestAssistRange': '请求援助范围',// 意义不明且没出现过
|
||||
'AcceptableAimDelta': '允许瞄准偏差角',
|
||||
'AimDirection': '基准瞄准方向', // 意义不明
|
||||
'ScatterRadius': '散射半径',
|
||||
'ScatterLength': '散射长度',
|
||||
'ScatterIndependently': '多抛射物独立散射', //只在V4散弹那里出现一次
|
||||
'WeaponSpeed': '武器/抛射物速度',
|
||||
'MinWeaponSpeed': '最小抛射物速度',
|
||||
'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': '生命吸取倍率', // 吸血比例,没出现过,在RAAA中只出现了Leech的吸血弹头
|
||||
'DrainLife': '生命吸取', // 吸血效果开关,没出现过,在RAAA中只出现了Leech的吸血弹头
|
||||
'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': '散射所需模型状态'
|
||||
// 可以根据需要添加更多翻译
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user