This commit is contained in:
Kunagisa 2025-05-28 18:36:11 +08:00
parent e3db03f6ad
commit 88a0f04a4f
8 changed files with 2092 additions and 4 deletions

136
src/api/demands.js Normal file
View File

@ -0,0 +1,136 @@
import axios from 'axios';
const API_BASE_URL = 'http://zybdatasupport.online:8000'; // 与 tournament.js 一致
// 创建 axios 实例,可以复用 tournament.js 中的拦截器逻辑,或者单独设置
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000
});
// 请求拦截器 (添加token)
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器 (基本错误处理)
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response) {
console.error('API请求错误 (demands.js):', {
status: error.response.status,
data: error.response.data,
config: error.config
});
} else if (error.request) {
console.error('网络错误 (demands.js):', error.request);
} else {
console.error('请求配置错误 (demands.js):', error.message);
}
return Promise.reject(error);
}
);
/**
* 获取需求列表
* @returns {Promise<Array<Object>>} 返回需求列表数据
*/
export const getDemandsList = async () => {
try {
const response = await axiosInstance.get('/demands/getlist');
return response.data; // 假设直接返回数组
} catch (error) {
console.error('获取需求列表失败:', error);
throw error;
}
};
/**
* 添加需求 (注意管理员后台可能不常用此功能)
* @param {Object} demandData - 需求数据
* @param {string} [demandData.requester] - 请求者
* @param {string} [demandData.qq_code] - QQ号
* @param {string} demandData.sendcontent - 请求内容 (对应后端的 sendcontent content)
* @param {string} [demandData.reward] - 悬赏金额
* @param {string} demandData.date - 日期 (后端API会自动生成但模型中包含此字段前端添加时也提交此字段如YYYY-MM-DD HH:MM:SS)
* @returns {Promise<Object>} 返回添加需求的响应数据
*/
export const addDemand = async (demandData) => {
try {
const payload = {
...demandData,
content: demandData.sendcontent // 确保 content 与 sendcontent 一致
};
const response = await axiosInstance.post('/demands/add', payload);
return response.data;
} catch (error) {
console.error('添加需求失败:', error);
throw error;
}
};
/**
* 更新需求
* @param {number} id - 需求ID
* @param {Object} dataToUpdate - 需要更新的数据
* @param {string} dataToUpdate.sendcontent - 新的请求内容
* // 如果后端允许更新其他字段,在此处添加
* @returns {Promise<Object>} 返回更新需求的响应数据
*/
export const updateDemand = async (id, dataToUpdate) => {
try {
// 根据后端PUT /demands/update/{id} 的定义它期望整个DemandModel作为item
// 但只更新了 sendcontent。为安全起见先获取原始数据再更新。
// 或者如果API设计为只接受要修改的字段则直接发送 { sendcontent: dataToUpdate.sendcontent }
// 这里假设API会处理部分更新或者前端会发送完整的模型即使只改一个字段
// 为简单起见我们先假设API能接受只包含sendcontent的更新或者前端需要先获取完整模型再提交
// 参照后端 item: DemandModel, 它会接收一个完整的模型,但仅使用了 item.sendcontent
// 因此,我们需要传递一个至少包含 sendcontent 的对象,但为了模型验证,最好是完整的模型结构
const payload = {
sendcontent: dataToUpdate.sendcontent,
// 根据 DemandModel补齐其他必填或可选字段即使它们不被后端 update 逻辑使用
// 这部分需要参照 DemandModel 的具体定义来决定哪些字段是必要的
// 以下为推测,需要根据实际 DemandModel 调整
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', ' ') // 确保有日期
};
const response = await axiosInstance.put(`/demands/update/${id}`, payload);
return response.data;
} catch (error) {
console.error('更新需求失败:', error);
throw error;
}
};
/**
* 删除需求
* @param {number} id - 需求ID
* @returns {Promise<Object>} 返回删除需求的响应数据
*/
export const deleteDemand = async (id) => {
try {
const response = await axiosInstance.delete(`/demands/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除需求失败:', error);
throw error;
}
};

View File

@ -0,0 +1,260 @@
<template>
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal-content">
<h2>{{ isEditMode ? '编辑参赛记录' : '添加参赛记录' }}</h2>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="tournament_id">选择赛事:</label>
<select id="tournament_id" v-model="form.tournament_id" @change="updateTournamentName" required :disabled="isEditMode">
<option disabled value="">请选择赛事</option>
<option v-for="t in availableTournaments" :key="t.id" :value="t.id">
{{ t.name }} (ID: {{ t.id }})
</option>
</select>
<input type="hidden" v-model="form.tournament_name">
</div>
<div class="form-group">
<label for="sign_name">报名者/队长名称:</label>
<input type="text" id="sign_name" v-model="form.sign_name" required>
</div>
<div class="form-group">
<label for="team_name">队伍名称 (可选):</label>
<input type="text" id="team_name" v-model="form.team_name">
</div>
<!-- API addSignUp 参数中包含 type, faction, qq, 这里简化处理 -->
<!-- 编辑模式下显示胜负和状态 -->
<template v-if="isEditMode">
<div class="form-group">
<label for="win">胜场数:</label>
<input type="number" id="win" v-model.number="form.win" min="0">
</div>
<div class="form-group">
<label for="lose">负场数:</label>
<input type="number" id="lose" v-model.number="form.lose" min="0">
</div>
<div class="form-group">
<label for="status">状态 (tie, win, lose):</label>
<input type="text" id="status" v-model="form.status">
</div>
<!-- 编辑模式下显示QQ号如果API支持 -->
<div class="form-group">
<label for="qq">QQ号 (可选, 编辑时):</label>
<input type="text" id="qq" v-model="form.qq">
</div>
</template>
<!-- 添加模式下需要QQ号如果addSignUp API需要 -->
<div v-if="!isEditMode" class="form-group">
<label for="qq_add">QQ号 (用于报名):</label>
<input type="text" id="qq_add" v-model="form.qq">
</div>
<div class="form-actions">
<button type="submit" class="submit-button">{{ isEditMode ? '更新记录' : '添加记录' }}</button>
<button type="button" @click="close" class="cancel-button">取消</button>
</div>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
</form>
</div>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';
import { addSignUp, updateSignUpResult } from '../../api/tournament';
const props = defineProps({
visible: Boolean,
playerData: Object, //
isEditMode: Boolean,
availableTournaments: Array //
});
const emits = defineEmits(['close', 'save']);
const initialFormState = () => ({
tournament_id: null,
tournament_name: '',
sign_name: '',
team_name: '',
qq: '', //
//
win: '0',
lose: '0',
status: 'tie',
// addSignUp ()
type: 'individual', // 'teamname' or 'individual'
faction: 'random',
id: null // player idtournament id (API addSignUp tournament_id as id)
});
const form = ref(initialFormState());
const errorMessage = ref('');
watch(() => props.playerData, (newData) => {
if (newData && props.isEditMode) {
form.value = {
...initialFormState(), // 使
...newData,
id: newData.id // id player.id
};
} else {
form.value = initialFormState();
}
}, { immediate: true, deep: true });
watch(() => props.visible, (isVisible) => {
if (isVisible && !props.isEditMode) {
form.value = initialFormState(); //
}
});
const updateTournamentName = () => {
const selected = props.availableTournaments.find(t => t.id === form.value.tournament_id);
if (selected) {
form.value.tournament_name = selected.name;
}
};
const submitForm = async () => {
errorMessage.value = '';
if (!form.value.tournament_id) {
errorMessage.value = '请选择一个赛事。';
return;
}
updateTournamentName(); // tournament_name
try {
if (props.isEditMode) {
const { id, ...dataToUpdate } = form.value;
// updateSignUpResult API player id
await updateSignUpResult(id, dataToUpdate);
alert('参赛记录更新成功!');
} else {
// addSignUp API tournament_id as id, tournament_name
const dataToAdd = {
...form.value,
id: form.value.tournament_id, // tournament_id addSignUp id
type: form.value.team_name ? 'teamname' : 'individual'
};
await addSignUp(dataToAdd);
alert('参赛记录添加成功!');
}
emits('save');
close();
} catch (error) {
console.error('保存参赛记录失败:', error);
errorMessage.value = error.response?.data?.detail || error.message || '操作失败,请检查数据或联系管理员。';
}
};
const close = () => {
emits('close');
errorMessage.value = '';
// watch
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
width: 90%;
max-width: 550px; /* 可以适当增大宽度 */
z-index: 1001;
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #555;
font-size: 0.9rem;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-size: 0.9rem;
}
.form-group input[type="text"]:focus,
.form-group input[type="number"]:focus,
.form-group select:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-actions {
margin-top: 25px;
text-align: right;
}
.submit-button, .cancel-button {
padding: 10px 18px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
}
.submit-button {
background-color: #1890ff;
color: white;
margin-right: 10px;
}
.submit-button:hover {
background-color: #40a9ff;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.error-message {
color: red;
margin-top: 10px;
font-size: 0.85rem;
}
</style>

View File

@ -0,0 +1,328 @@
<template>
<div class="player-list-container">
<div class="actions-bar">
<div class="filters">
<label for="tournamentFilter">按赛事筛选:</label>
<select id="tournamentFilter" v-model="selectedTournamentId" @change="fetchPlayers">
<option :value="null">所有赛事</option>
<option v-for="t in availableTournaments" :key="t.id" :value="t.id">{{ t.name }} (ID: {{ t.id }})</option>
</select>
</div>
<button @click="showAddModal = true" class="add-button">添加参赛记录</button>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
<th>ID</th>
<th>赛事ID</th>
<th>赛事名称</th>
<th>队伍名称</th>
<th>报名者/队长</th>
<th></th>
<th></th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="9" style="text-align: center;">加载中...</td>
</tr>
<tr v-else-if="filteredPlayers.length === 0">
<td colspan="9" style="text-align: center;">暂无参赛记录</td>
</tr>
<tr v-for="player in filteredPlayers" :key="player.id">
<td>{{ player.id }}</td>
<td>{{ player.tournament_id }}</td>
<td>{{ player.tournament_name }}</td>
<td>{{ player.team_name || '-' }}</td>
<td>{{ player.sign_name }}</td>
<td>{{ player.win }}</td>
<td>{{ player.lose }}</td>
<td>{{ player.status }}</td>
<td>
<button @click="editPlayer(player)" class="action-button edit">编辑</button>
<button @click="confirmDeletePlayer(player.id)" class="action-button delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<PlayerForm
v-if="showAddModal || showEditModal"
:visible="showAddModal || showEditModal"
:playerData="currentPlayer"
:isEditMode="showEditModal"
:availableTournaments="availableTournaments"
@close="closeModal"
@save="handleSavePlayer"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { getSignUpResultList, deleteSignUpResult, getTournamentList } from '../../api/tournament';
import PlayerForm from './PlayerForm.vue';
const allPlayers = ref([]);
const loading = ref(false);
const showAddModal = ref(false);
const showEditModal = ref(false);
const currentPlayer = ref(null);
const availableTournaments = ref([]);
const selectedTournamentId = ref(null);
const fetchTournamentsForFilter = async () => {
try {
const response = await getTournamentList();
availableTournaments.value = response.data || response || [];
if (!Array.isArray(availableTournaments.value)) {
availableTournaments.value = [];
}
} catch (error) {
console.error('获取赛事列表 (for filter) 失败:', error);
availableTournaments.value = [];
}
};
const fetchPlayers = async () => {
loading.value = true;
try {
const response = await getSignUpResultList();
allPlayers.value = response.data || response || [];
if (!Array.isArray(allPlayers.value)) {
console.warn('getSignUpResultList did not return an array:', allPlayers.value);
allPlayers.value = [];
}
} catch (error) {
console.error('获取参赛记录列表失败:', error);
allPlayers.value = [];
} finally {
loading.value = false;
}
};
const filteredPlayers = computed(() => {
if (!selectedTournamentId.value) {
return allPlayers.value;
}
return allPlayers.value.filter(p => p.tournament_id === selectedTournamentId.value);
});
const editPlayer = (player) => {
currentPlayer.value = { ...player };
showEditModal.value = true;
};
const confirmDeletePlayer = async (id) => {
if (window.confirm('确定要删除这条参赛记录吗?')) {
try {
await deleteSignUpResult(id);
fetchPlayers();
alert('参赛记录删除成功!');
} catch (error) {
console.error('删除参赛记录失败:', error);
alert('删除参赛记录失败: ' + (error.message || '请稍后重试'));
}
}
};
const closeModal = () => {
showAddModal.value = false;
showEditModal.value = false;
currentPlayer.value = null;
};
const handleSavePlayer = () => {
closeModal();
fetchPlayers();
};
onMounted(() => {
fetchTournamentsForFilter();
fetchPlayers();
});
</script>
<style scoped>
.player-list-container {
padding: 15px;
}
.actions-bar {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 15px;
}
.actions-bar .filters {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.actions-bar label {
font-size: 0.9rem;
margin-right: 5px;
}
.actions-bar select,
.actions-bar .add-button,
.actions-bar .refresh-button {
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 0.85rem;
}
.actions-bar .add-button,
.actions-bar .refresh-button {
background-color: #1890ff;
color: white;
border: none;
cursor: pointer;
}
.actions-bar .add-button:hover,
.actions-bar .refresh-button:hover {
background-color: #40a9ff;
}
.table-responsive-wrapper {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.custom-table {
width: 100%;
min-width: 800px;
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.12);
}
.custom-table th, .custom-table td {
border: 1px solid #e8e8e8;
padding: 10px 12px;
text-align: left;
font-size: 0.85rem;
vertical-align: middle;
white-space: nowrap;
}
.custom-table th {
background-color: #fafafa;
font-weight: 600;
}
.custom-table tbody tr:hover {
background-color: #f5f5f5;
}
.action-button {
padding: 5px 8px;
border-radius: 3px;
border: none;
cursor: pointer;
font-size: 0.8rem;
margin-right: 5px;
color: white;
}
.action-button.edit { background-color: #52c41a; }
.action-button.edit:hover { background-color: #73d13d; }
.action-button.delete { background-color: #ff4d4f; }
.action-button.delete:hover { background-color: #ff7875; }
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 4px;
width: 90%;
max-width: 550px;
}
.modal-content h2 { font-size: 1.3rem; margin-bottom: 15px; }
.form-group { margin-bottom: 12px; }
.form-group label { font-size: 0.85rem; margin-bottom: 4px; }
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
padding: 8px 10px;
font-size: 0.85rem;
width: 100%;
box-sizing: border-box;
}
.form-actions { margin-top: 20px; }
.submit-button, .cancel-button {
padding: 8px 15px;
font-size: 0.85rem;
}
@media (max-width: 768px) {
.actions-bar {
flex-direction: column;
align-items: stretch;
}
.actions-bar .filters {
width: 100%;
justify-content: space-between;
margin-bottom: 10px;
}
.actions-bar .buttons-group {
width: 100%;
display: flex;
justify-content: space-between;
}
.actions-bar .add-button,
.actions-bar .refresh-button {
flex-grow: 1;
margin: 0 5px;
}
.actions-bar .add-button:first-child { margin-left: 0; }
.actions-bar .refresh-button:last-child { margin-right: 0; }
.custom-table {
min-width: auto;
}
}
@media (max-width: 480px) {
.actions-bar .filters {
flex-direction: column;
gap: 8px;
}
.actions-bar .filters label,
.actions-bar .filters select {
width: 100%;
margin-right: 0;
}
.actions-bar .buttons-group button {
padding: 8px 10px;
font-size: 0.8rem;
}
.modal-content {
padding: 15px;
}
.modal-content h2 {
font-size: 1.2rem;
}
}
</style>

View File

@ -0,0 +1,526 @@
<template>
<div class="service-hall-container">
<div class="actions-bar">
<h3>办事大厅</h3>
<div class="buttons-wrapper">
<button @click="showAddDemandModal = true" class="add-button">添加需求</button>
<button @click="fetchDemandsAdmin" class="refresh-button">刷新列表</button>
</div>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
<th>ID</th>
<th>请求者</th>
<th>QQ号</th>
<th>请求内容</th>
<th>悬赏金额</th>
<th>创建日期</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="7" style="text-align: center;">加载中...</td>
</tr>
<tr v-else-if="demands.length === 0">
<td :colspan="7" style="text-align: center;">暂无需求数据</td>
</tr>
<tr v-for="demand in demands" :key="demand.id">
<td>{{ demand.id }}</td>
<td>{{ demand.requester || '-' }}</td>
<td>{{ demand.qq_code || '-' }}</td>
<td class="content-cell" :title="demand.content">{{ truncateText(demand.content, 50) }}</td>
<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>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 编辑需求模态框 -->
<div v-if="showEditDemandModal" class="modal-overlay" @click.self="closeEditModal">
<div class="modal-content">
<h2>编辑需求</h2>
<form @submit.prevent="submitEditForm">
<div class="form-group">
<label :for="`edit-requester-${currentDemand.id}`">请求者:</label>
<input :id="`edit-requester-${currentDemand.id}`" type="text" v-model="editForm.requester">
</div>
<div class="form-group">
<label :for="`edit-qq-${currentDemand.id}`">QQ号:</label>
<input :id="`edit-qq-${currentDemand.id}`" type="text" v-model="editForm.qq_code">
</div>
<div class="form-group">
<label :for="`edit-content-${currentDemand.id}`">需求内容:</label>
<textarea :id="`edit-content-${currentDemand.id}`" v-model="editForm.sendcontent" rows="4" required></textarea>
</div>
<div class="form-group">
<label :for="`edit-reward-${currentDemand.id}`">悬赏金额:</label>
<input :id="`edit-reward-${currentDemand.id}`" type="text" v-model="editForm.reward">
</div>
<div class="form-group">
<label :for="`edit-date-${currentDemand.id}`">日期 (YYYY-MM-DD HH:MM:SS):</label>
<input :id="`edit-date-${currentDemand.id}`" type="text" v-model="editForm.date" readonly>
</div>
<div class="form-actions">
<button type="submit" class="submit-button">更新</button>
<button type="button" @click="closeEditModal" class="cancel-button">取消</button>
</div>
<p v-if="editError" class="error-message">{{ editError }}</p>
</form>
</div>
</div>
<!-- 添加需求模态框 -->
<div v-if="showAddDemandModal" class="modal-overlay" @click.self="closeAddModal">
<div class="modal-content">
<h2>添加新需求</h2>
<form @submit.prevent="submitAddForm">
<div class="form-group">
<label for="add-requester">请求者 (可选):</label>
<input id="add-requester" type="text" v-model="addForm.requester">
</div>
<div class="form-group">
<label for="add-qq">QQ号 (可选):</label>
<input id="add-qq" type="text" v-model="addForm.qq_code">
</div>
<div class="form-group">
<label for="add-content">需求内容:</label>
<textarea id="add-content" v-model="addForm.sendcontent" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="add-reward">悬赏金额 (可选):</label>
<input id="add-reward" type="text" v-model="addForm.reward">
</div>
<div class="form-actions">
<button type="submit" class="submit-button" :disabled="addLoading">{{ addLoading ? '提交中...' : '提交' }}</button>
<button type="button" @click="closeAddModal" class="cancel-button">取消</button>
</div>
<p v-if="addError" class="error-message">{{ addError }}</p>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; //
const demands = ref([]);
const loading = ref(false);
const showEditDemandModal = ref(false);
const currentDemand = ref(null);
const editForm = ref({});
const editError = ref('');
// Refs for Add Demand Modal
const showAddDemandModal = ref(false);
const addLoading = ref(false);
const initialAddFormState = {
requester: '',
qq_code: '',
sendcontent: '',
reward: ''
// date is generated on submit
};
const addForm = ref({ ...initialAddFormState });
const addError = ref('');
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString; //
return date.toLocaleString(); //
} catch (e) {
return dateString;
}
};
const truncateText = (text, length) => {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
};
const fetchDemandsAdmin = async () => {
loading.value = true;
try {
const response = await getDemandsList();
demands.value = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('获取需求列表失败 (admin):', error);
demands.value = [];
} finally {
loading.value = false;
}
};
const editDemand = (demand) => {
currentDemand.value = { ...demand };
// editForm 使 demand date
editForm.value = {
...demand,
sendcontent: demand.content || demand.sendcontent // APIcontentsendcontent
};
editError.value = '';
showEditDemandModal.value = true;
};
const closeEditModal = () => {
showEditDemandModal.value = false;
currentDemand.value = null;
editError.value = '';
};
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
};
await updateDemand(currentDemand.value.id, payload);
alert('需求更新成功!');
closeEditModal();
fetchDemandsAdmin();
} catch (error) {
console.error('更新需求失败:', error);
editError.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
}
};
const confirmDeleteDemand = async (id) => {
if (window.confirm('确定要删除此需求吗?')) {
try {
await deleteDemand(id);
alert('需求删除成功!');
fetchDemandsAdmin();
} catch (error) {
console.error('删除需求失败:', error);
alert('删除需求失败: ' + (error.message || '请稍后重试'));
}
}
};
// --- Add Demand Modal Logic ---
const openAddModal = () => {
addForm.value = { ...initialAddFormState };
addError.value = '';
showAddDemandModal.value = true;
};
const closeAddModal = () => {
showAddDemandModal.value = false;
addError.value = '';
};
const submitAddForm = async () => {
if (!addForm.value.sendcontent.trim()) {
addError.value = '需求内容不能为空';
return;
}
addLoading.value = true;
addError.value = '';
try {
const now = new Date();
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 payload = {
...addForm.value,
date: dateStr,
// content will be handled by addDemand API function if sendcontent is provided
};
await addDemand(payload);
alert('新需求添加成功!');
closeAddModal();
fetchDemandsAdmin(); // Refresh list
} catch (error) {
console.error('添加需求失败:', error);
addError.value = error.response?.data?.detail || error.message || '添加失败,请稍后重试。';
} finally {
addLoading.value = false;
}
};
onMounted(() => {
fetchDemandsAdmin();
});
</script>
<style scoped>
.service-hall-container {
padding: 15px; /* Reduced padding for mobile */
}
.actions-bar {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap; /* Allow wrapping */
justify-content: space-between;
align-items: center;
gap: 10px; /* Add gap for wrapped items */
}
.actions-bar h3 {
margin: 0;
font-size: 1.4rem; /* Slightly reduced for mobile */
color: #333;
}
/* Container for buttons on the right */
.actions-bar .buttons-wrapper {
display: flex;
gap: 10px; /* Space between buttons */
}
.refresh-button, .add-button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 12px; /* Slightly reduced padding */
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem; /* Slightly reduced font size */
}
.refresh-button:hover, .add-button:hover {
background-color: #40a9ff;
}
.table-responsive-wrapper { /* New wrapper for table */
width: 100%;
overflow-x: auto; /* Enable horizontal scrolling */
-webkit-overflow-scrolling: touch; /* Smoother scrolling on iOS */
}
.custom-table {
width: 100%;
min-width: 750px; /* Minimum width before scrolling starts, adjust as needed */
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.12); /* Softer shadow */
}
.custom-table th, .custom-table td {
border: 1px solid #e8e8e8;
padding: 10px 12px; /* Adjusted padding */
text-align: left;
font-size: 0.85rem; /* Adjusted font size */
vertical-align: middle;
/* white-space: nowrap; /* Consider if content should wrap or not. Ellipsis is handled by truncateText */
}
.custom-table th {
background-color: #fafafa;
font-weight: 600;
color: #000000d9;
white-space: nowrap; /* Keep headers on one line */
}
.custom-table tbody tr:hover {
background-color: #f5f5f5;
}
.content-cell {
max-width: 250px; /* Adjust as needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.action-button {
padding: 5px 8px; /* Adjusted padding */
border-radius: 3px; /* Adjusted border-radius */
border: none;
cursor: pointer;
font-size: 0.8rem; /* Adjusted font size */
margin-right: 5px;
color: white;
white-space: nowrap; /* Keep button text on one line */
}
.action-button.edit {
background-color: #52c41a;
}
.action-button.edit:hover {
background-color: #73d13d;
}
.action-button.delete {
background-color: #ff4d4f;
}
.action-button.delete:hover {
background-color: #ff7875;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 10px; /* Add padding for very small screens */
}
.modal-content {
background-color: white;
padding: 20px; /* Adjusted padding */
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
width: 90%;
max-width: 550px;
z-index: 1001;
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 1.3rem; /* Adjusted font size */
}
.form-group {
margin-bottom: 12px; /* Adjusted margin */
}
.form-group label {
display: block;
margin-bottom: 4px;
font-size: 0.85rem; /* Adjusted font size */
font-weight: 500;
color: #555;
}
.form-group input[type="text"],
.form-group textarea {
padding: 8px 10px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
width: 100%; /* Ensure form elements take full width */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
border: 1px solid #ccc;
border-radius: 4px;
}
.form-group input[type="text"]:focus,
.form-group textarea:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-actions {
margin-top: 20px; /* Adjusted margin */
}
.submit-button, .cancel-button {
padding: 8px 15px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
border-radius: 4px;
border: none;
cursor: pointer;
}
.submit-button {
background-color: #1890ff;
color: white;
margin-right: 10px;
}
.submit-button:hover {
background-color: #40a9ff;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.error-message {
color: red;
margin-top: 10px;
font-size: 0.85rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.actions-bar {
flex-direction: column; /* Stack title and buttons vertically */
align-items: flex-start; /* Align items to the start */
}
.actions-bar h3 {
margin-bottom: 10px; /* Add space below title when stacked */
}
.actions-bar .buttons-wrapper { /* Container for buttons */
width: 100%; /* Full width on mobile */
/* justify-content: space-between; /* Remove this if you don't want them to spread far apart */
/* Consider justify-content: flex-start; or let gap handle spacing */
}
.refresh-button, .add-button {
/* flex-grow: 1; /* Remove flex-grow if buttons should take natural width */
/* margin: 0 5px; /* Remove or adjust margin, gap is now on buttons-wrapper */
/* width: auto; /* Ensure buttons take their content width or a defined width */
padding: 8px 15px; /* Ensure decent padding */
}
/* If you want them to still be somewhat spread on mobile but not full flex-grow:
.actions-bar .buttons-wrapper {
display: flex;
justify-content: flex-end; /* Aligns buttons to the right on mobile when stacked
}
.refresh-button, .add-button {
margin-left: 10px;
}
.refresh-button:first-child, .add-button:first-child { margin-left: 0; }
*/
}
@media (max-width: 480px) {
.modal-content {
padding: 15px;
}
.modal-content h2 {
font-size: 1.2rem;
}
.actions-bar .buttons-wrapper button { /* Target buttons within the wrapper more specifically if needed */
padding: 8px 10px; /* Even smaller padding for very small buttons */
font-size: 0.8rem;
/* width: 100%; /* Optionally make buttons full width on very small screens */
/* margin-bottom: 5px; /* If they stack vertically */
}
/* If buttons stack vertically on very small screens (example):
.actions-bar .buttons-wrapper {
flex-direction: column;
align-items: stretch;
}
.actions-bar .buttons-wrapper button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
.actions-bar .buttons-wrapper button:last-child {
margin-bottom: 0;
}
*/
.content-cell {
max-width: 100px; /* Further reduce for very small screens */
}
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal-content">
<h2>{{ isEditMode ? '编辑赛事' : '添加赛事' }}</h2>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="name">赛事名称:</label>
<input type="text" id="name" v-model="form.name" required>
</div>
<div class="form-group">
<label for="format">赛事类型:</label>
<select id="format" v-model="form.format" required>
<option value="single">单人赛 (single)</option>
<option value="double">双人赛 (double)</option>
<option value="count">计数赛 (count)</option>
</select>
</div>
<div class="form-group">
<label for="organizer">组织者:</label>
<input type="text" id="organizer" v-model="form.organizer" required>
</div>
<div class="form-group">
<label for="qq_code">组织者QQ:</label>
<input type="text" id="qq_code" v-model="form.qq_code" required>
</div>
<div class="form-group">
<label for="status">状态:</label>
<select id="status" v-model="form.status" required>
<option value="prepare">准备中 (prepare)</option>
<option value="starting">进行中 (starting)</option>
<option value="finish">已结束 (finish)</option>
</select>
</div>
<div class="form-group">
<label for="start_time">开始时间 (//):</label>
<input type="text" id="start_time" v-model="form.start_time" placeholder="例如: 2025/05/24" required>
</div>
<div class="form-group">
<label for="end_time">结束时间 (//):</label>
<input type="text" id="end_time" v-model="form.end_time" placeholder="例如: 2025/06/24" required>
</div>
<div class="form-actions">
<button type="submit" class="submit-button">{{ isEditMode ? '更新' : '创建' }}</button>
<button type="button" @click="close" class="cancel-button">取消</button>
</div>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
</form>
</div>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';
import { addTournament, updateTournament } from '../../api/tournament';
const props = defineProps({
visible: Boolean,
tournamentData: Object, //
isEditMode: Boolean
});
const emits = defineEmits(['close', 'save']);
const initialFormState = {
name: '',
format: 'single',
organizer: '',
qq_code: '',
status: 'prepare',
start_time: '',
end_time: ''
};
const form = ref({ ...initialFormState });
const errorMessage = ref('');
watch(() => props.tournamentData, (newData) => {
if (newData && props.isEditMode) {
form.value = { ...newData };
} else {
form.value = { ...initialFormState };
}
}, { immediate: true, deep: true });
const submitForm = async () => {
errorMessage.value = '';
try {
if (props.isEditMode) {
// API updateTournament id data
const { id, ...dataToUpdate } = form.value;
await updateTournament(id, dataToUpdate);
alert('赛事更新成功!');
} else {
await addTournament(form.value);
alert('赛事添加成功!');
}
emits('save');
close();
} catch (error) {
console.error('保存赛事失败:', error);
errorMessage.value = error.response?.data?.detail || error.message || '操作失败,请检查数据或联系管理员。';
// alert(': ' + (error.message || ''));
}
};
const close = () => {
emits('close');
form.value = { ...initialFormState }; //
errorMessage.value = '';
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
width: 90%;
max-width: 500px;
z-index: 1001;
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #555;
font-size: 0.9rem;
}
.form-group input[type="text"],
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-size: 0.9rem;
}
.form-group input[type="text"]:focus,
.form-group select:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-actions {
margin-top: 25px;
text-align: right;
}
.submit-button, .cancel-button {
padding: 10px 18px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
}
.submit-button {
background-color: #1890ff;
color: white;
margin-right: 10px;
}
.submit-button:hover {
background-color: #40a9ff;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.error-message {
color: red;
margin-top: 10px;
font-size: 0.85rem;
}
</style>

View File

@ -0,0 +1,294 @@
<template>
<div class="tournament-list-container">
<div class="actions-bar">
<h3>赛事列表</h3>
<div>
<button @click="showAddModal = true" class="add-button">添加赛事</button>
<button @click="fetchTournaments" class="refresh-button" :disabled="loading">刷新列表</button>
</div>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
<th>ID</th>
<th>赛事名称</th>
<th>类型</th>
<th>组织者</th>
<th>QQ号</th>
<th>状态</th>
<th>开始时间</th>
<th>结束时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="9" style="text-align: center;">加载中...</td>
</tr>
<tr v-else-if="tournaments.length === 0">
<td colspan="9" style="text-align: center;">暂无赛事数据</td>
</tr>
<tr v-for="tournament in tournaments" :key="tournament.id">
<td>{{ tournament.id }}</td>
<td>{{ tournament.name }}</td>
<td>{{ tournament.format }}</td>
<td>{{ tournament.organizer }}</td>
<td>{{ tournament.qq_code }}</td>
<td>{{ tournament.status }}</td>
<td>{{ tournament.start_time }}</td>
<td>{{ tournament.end_time }}</td>
<td>
<button @click="editTournament(tournament)" class="action-button edit">编辑</button>
<button @click="confirmDeleteTournament(tournament.id)" class="action-button delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 添加/编辑 模态框 -->
<TournamentForm
v-if="showAddModal || showEditModal"
:visible="showAddModal || showEditModal"
:tournamentData="currentTournament"
:isEditMode="showEditModal"
@close="closeModal"
@save="handleSaveTournament"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getTournamentList, deleteTournament } from '../../api/tournament'; //
import TournamentForm from './TournamentForm.vue'; //
const tournaments = ref([]);
const loading = ref(false);
const showAddModal = ref(false);
const showEditModal = ref(false);
const currentTournament = ref(null);
const fetchTournaments = async () => {
loading.value = true;
try {
const response = await getTournamentList();
tournaments.value = response.data || response; // API {data: []} []
if (!Array.isArray(tournaments.value)) {
console.warn('getTournamentList did not return an array:', tournaments.value);
tournaments.value = []; //
}
} catch (error) {
console.error('获取赛事列表失败:', error);
tournaments.value = []; //
} finally {
loading.value = false;
}
};
const editTournament = (tournament) => {
currentTournament.value = { ...tournament };
showEditModal.value = true;
};
const confirmDeleteTournament = async (id) => {
if (window.confirm('确定要删除这项赛事吗?')) {
try {
await deleteTournament(id);
fetchTournaments(); //
alert('赛事删除成功!');
} catch (error) {
console.error('删除赛事失败:', error);
alert('删除赛事失败: ' + (error.message || '请稍后重试'));
}
}
};
const closeModal = () => {
showAddModal.value = false;
showEditModal.value = false;
currentTournament.value = null;
};
const handleSaveTournament = () => {
closeModal();
fetchTournaments(); //
};
onMounted(() => {
fetchTournaments();
});
</script>
<style scoped>
.tournament-list-container {
padding: 15px; /* Reduced padding for mobile */
background-color: #fff;
}
.actions-bar {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap; /* Allow wrapping on small screens */
justify-content: space-between;
align-items: center;
gap: 10px; /* Add gap for wrapped items */
}
.actions-bar h3 {
margin: 0;
font-size: 1.4rem; /* Slightly reduced for mobile */
color: #333;
}
.add-button, .refresh-button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 12px; /* Slightly reduced padding */
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem; /* Slightly reduced font size */
}
.add-button:hover, .refresh-button:hover {
background-color: #40a9ff;
}
.table-responsive-wrapper { /* New wrapper for table */
width: 100%;
overflow-x: auto; /* Enable horizontal scrolling */
-webkit-overflow-scrolling: touch; /* Smoother scrolling on iOS */
}
.custom-table {
width: 100%;
min-width: 700px; /* Minimum width before scrolling starts, adjust as needed */
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.12); /* Softer shadow */
}
.custom-table th, .custom-table td {
border: 1px solid #e8e8e8;
padding: 10px 12px; /* Adjusted padding */
text-align: left;
font-size: 0.85rem; /* Adjusted font size */
vertical-align: middle;
white-space: nowrap; /* Prevent content wrapping that might break layout */
}
.custom-table th {
background-color: #fafafa;
font-weight: 600;
color: #000000d9;
}
.custom-table tbody tr:hover {
background-color: #f5f5f5;
}
.action-button {
padding: 5px 8px; /* Adjusted padding */
border-radius: 3px; /* Adjusted border-radius */
border: none;
cursor: pointer;
font-size: 0.8rem; /* Adjusted font size */
margin-right: 5px;
color: white;
}
.action-button.edit {
background-color: #52c41a;
}
.action-button.edit:hover {
background-color: #73d13d;
}
.action-button.delete {
background-color: #ff4d4f;
}
.action-button.delete:hover {
background-color: #ff7875;
}
/* Modal Styles - generally okay, ensure max-width and percentage width */
.modal-overlay {
/* ... existing styles ... */
padding: 10px; /* Add padding for very small screens */
}
.modal-content {
/* ... existing styles ... */
width: 90%;
max-width: 500px; /* Max width for larger screens */
padding: 20px; /* Adjusted padding */
}
.modal-content h2 {
font-size: 1.3rem; /* Adjusted font size */
margin-bottom: 15px;
}
.form-group {
margin-bottom: 12px; /* Adjusted margin */
}
.form-group label {
font-size: 0.85rem; /* Adjusted font size */
margin-bottom: 4px;
}
.form-group input[type="text"],
.form-group input[type="datetime-local"],
.form-group textarea {
padding: 8px 10px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
}
.form-actions {
margin-top: 20px; /* Adjusted margin */
}
.submit-button, .cancel-button {
padding: 8px 15px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
}
/* Responsive adjustments specifically for smaller screens if needed beyond table scroll */
@media (max-width: 768px) {
.actions-bar {
flex-direction: column; /* Stack title and buttons vertically */
align-items: flex-start; /* Align items to the start */
}
.actions-bar h3 {
margin-bottom: 10px; /* Add space below title when stacked */
}
.actions-bar div { /* Container for buttons */
width: 100%;
display: flex;
justify-content: space-between; /* Space out buttons if two, or start if one */
}
.add-button, .refresh-button {
flex-grow: 1; /* Allow buttons to grow if space */
margin: 0 5px;
}
.add-button:first-child { margin-left: 0; }
.add-button:last-child { margin-right: 0; }
/* Further fine-tuning for table cells if absolutely necessary, but scrolling is primary */
.custom-table th, .custom-table td {
/* font-size: 0.8rem; /* Example: even smaller font if needed */
}
}
@media (max-width: 480px) {
.modal-content {
padding: 15px;
}
.modal-content h2 {
font-size: 1.2rem;
}
.actions-bar div button {
padding: 8px 10px; /* Even smaller padding for very small buttons */
font-size: 0.8rem;
}
}
</style>

View File

@ -191,7 +191,11 @@ const handleLogin = async () => {
//
if (response.access_token) {
// token ID (QQ)
localStorage.setItem('access_token', response.access_token)
localStorage.setItem('user_id', username.value) // username.value QQ
router.push('/')
} else {
throw new Error('登录失败:未获取到访问令牌')
}

View File

@ -1,15 +1,71 @@
<template>
<button @click="handleLogout">
退出登录
</button>
<div class="dashboard-wrapper">
<div v-if="isAdmin" class="admin-layout" :class="{ 'sidebar-open': isMobileSidebarOpen }">
<div class="mobile-header">
<button @click="isMobileSidebarOpen = !isMobileSidebarOpen" class="hamburger-button">
<span class="hamburger-icon"></span>
</button>
<span class="mobile-header-title">后台管理</span>
</div>
<div class="admin-sidebar">
<div class="sidebar-header">
<h3>后台管理</h3>
</div>
<ul class="sidebar-nav">
<li @click="selectAdminView('event-management')" :class="{ active: currentAdminView === 'event-management' }"><a>赛事管理</a></li>
<li @click="selectAdminView('player-management')" :class="{ active: currentAdminView === 'player-management' }"><a>赛事玩家管理</a></li>
<li @click="selectAdminView('service-hall')" :class="{ active: currentAdminView === 'service-hall' }"><a>办事大厅</a></li>
</ul>
<div class="sidebar-footer">
<button @click="goToHomePage" class="home-button sidebar-button">
返回主界面
</button>
<button @click="handleLogout" class="logout-button sidebar-button">
退出登录
</button>
</div>
</div>
<div class="admin-main-content">
<div v-if="currentAdminView === 'event-management'">
<TournamentList />
</div>
<div v-else-if="currentAdminView === 'player-management'">
<PlayerList />
</div>
<div v-else-if="currentAdminView === 'service-hall'">
<ServiceHallView />
</div>
</div>
</div>
<div v-else class="dashboard-content non-admin-view">
<div class="button-group">
<button @click="handleLogout" class="logout-button">
退出登录
</button>
<button @click="goToHomePage" class="home-button">
返回主页面
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import TournamentList from '../../components/backend/TournamentList.vue'
import PlayerList from '../../components/backend/PlayerList.vue'
import ServiceHallView from '../../components/backend/ServiceHallView.vue'
const router = useRouter()
const hasToken = ref(false)
const userId = ref(null)
const currentAdminView = ref('event-management')
const isMobileSidebarOpen = ref(false)
const isAdmin = computed(() => {
return userId.value === '1846172115' || userId.value === '1400429906'
})
onMounted(() => {
// token
@ -18,17 +74,291 @@ onMounted(() => {
console.log('检测到token')
if (!token) {
console.log('未检测到token')
router.push('/')
}
// ID
const storedUserId = localStorage.getItem('user_id')
if (storedUserId) {
userId.value = storedUserId
console.log('当前用户ID:', userId.value)
} else {
console.log('未检测到用户ID')
}
})
const handleLogout = () => {
// token
localStorage.removeItem('access_token')
// ID
localStorage.removeItem('user_id')
// ()
currentAdminView.value = 'event-management'
isMobileSidebarOpen.value = false; // 退
//
router.push('/')
}
const goToHomePage = () => {
router.push('/') // 假设主页路由是 '/'
isMobileSidebarOpen.value = false; //
}
//
const selectAdminView = (viewName) => {
currentAdminView.value = viewName
if (window.innerWidth <= 768) {
isMobileSidebarOpen.value = false
}
}
</script>
<style scoped>
/* Global reset and base styles (optional, but good for consistency) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, .dashboard-wrapper, .admin-layout {
height: 100%;
width: 100%;
overflow-x: hidden; /* Prevent horizontal scroll on body/wrapper */
}
.dashboard-wrapper {
font-family: 'Arial', sans-serif;
background-color: #f0f2f5; /* Default background for non-admin or loading states */
}
/* Admin Layout Styles */
.admin-layout {
display: flex;
position: relative; /* For mobile sidebar positioning */
}
.admin-sidebar {
width: 240px; /* Sidebar width */
background-color: #e0f2fe; /* 淡蓝色 - light sky blue, adjust as needed */
color: #075985; /* Darker blue for text for contrast */
display: flex;
flex-direction: column;
height: 100%; /* Full height */
position: fixed; /* Fixed position */
left: 0;
top: 0;
z-index: 1000;
transition: transform 0.3s ease;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
}
.sidebar-header {
padding: 20px;
text-align: center;
border-bottom: 1px solid #bae6fd; /* Lighter blue for border */
}
.sidebar-header h3 {
color: #0c4a6e; /* Even darker blue for header */
margin: 0;
font-size: 1.6rem;
}
.sidebar-nav {
list-style: none;
flex-grow: 1;
overflow-y: auto; /* Allow scrolling for many nav items */
}
.sidebar-nav li a {
display: block;
padding: 15px 20px;
color: #075985;
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
cursor: pointer;
font-weight: 500;
}
.sidebar-nav li a:hover {
background-color: #7dd3fc; /* Lighter blue for hover */
color: #0c4a6e;
}
.sidebar-nav li.active a {
background-color: #38bdf8; /* Medium blue for active */
color: #ffffff;
font-weight: 600;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #bae6fd;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-button {
width: 100%;
padding: 12px 15px;
border-radius: 6px;
text-align: center;
font-weight: 500;
transition: background-color 0.2s, opacity 0.2s;
border: none;
color: #fff;
cursor: pointer;
}
.sidebar-button.home-button {
background-color: #0ea5e9; /* Sky blue */
}
.sidebar-button.home-button:hover {
background-color: #0284c7; /* Darker sky blue */
}
.sidebar-button.logout-button {
background-color: #ef4444; /* Red */
}
.sidebar-button.logout-button:hover {
background-color: #dc2626; /* Darker Red */
}
.admin-main-content {
flex-grow: 1;
background-color: #ffffff; /* White background for content */
padding: 20px;
margin-left: 240px; /* Same as sidebar width */
height: 100%;
overflow-y: auto;
transition: margin-left 0.3s ease;
}
/* Non-Admin View (Centered content, similar to before but within new wrapper) */
.dashboard-content.non-admin-view {
display: flex; /* Use flex to center content */
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%; /* Take full height of the wrapper */
width: 100%;
padding: 20px;
/* background-color: #fff; Already white from admin-main-content, but can be explicit if needed */
}
.non-admin-view .button-group {
background-color: #fff;
padding: 30px 40px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
width: 100%;
}
/* Mobile Header Styles */
.mobile-header {
display: none; /* Hidden by default, shown on mobile */
background-color: #e0f2fe; /* Same as sidebar */
color: #0c4a6e;
padding: 10px 15px;
align-items: center;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1001; /* Above sidebar when closed */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.hamburger-button {
background: none;
border: none;
cursor: pointer;
padding: 10px;
display: flex; /* For centering span inside */
align-items: center;
justify-content: center;
}
.hamburger-icon {
display: block;
width: 24px;
height: 2px;
background-color: #0c4a6e;
position: relative;
}
.hamburger-icon::before,
.hamburger-icon::after {
content: '';
position: absolute;
width: 24px;
height: 2px;
background-color: #0c4a6e;
left: 0;
}
.hamburger-icon::before {
top: -7px;
}
.hamburger-icon::after {
bottom: -7px;
}
.mobile-header-title {
margin-left: 15px;
font-size: 1.2rem;
font-weight: 600;
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.admin-sidebar {
transform: translateX(-100%); /* Hide sidebar off-screen */
box-shadow: none; /* Hide shadow when off-screen or covered */
}
.admin-layout.sidebar-open .admin-sidebar {
transform: translateX(0); /* Show sidebar */
box-shadow: 2px 0 8px rgba(0,0,0,0.15); /* Shadow for open sidebar on mobile */
}
.admin-main-content {
margin-left: 0; /* Content takes full width */
padding-top: 70px; /* Space for fixed mobile header */
}
.mobile-header {
display: flex;
}
.sidebar-header h3 {
font-size: 1.3rem; /* Slightly smaller header for mobile sidebar */
}
.sidebar-nav li a {
padding: 12px 20px; /* Adjust padding if needed */
}
/* Overlay for when mobile sidebar is open */
.admin-layout.sidebar-open::after {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.3);
z-index: 999; /* Below sidebar, above content */
}
}
/* Styles for the placeholder non-admin page if it's not the main content area */
.dashboard-container { /* This class was used before, ensure it doesn't conflict or remove if not needed */
/* If this is for the non-admin view specifically, and it's outside admin-layout */
}
</style>