后台
This commit is contained in:
parent
e3db03f6ad
commit
88a0f04a4f
136
src/api/demands.js
Normal file
136
src/api/demands.js
Normal 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;
|
||||
}
|
||||
};
|
260
src/components/backend/PlayerForm.vue
Normal file
260
src/components/backend/PlayerForm.vue
Normal 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 id,添加模式下是tournament 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>
|
328
src/components/backend/PlayerList.vue
Normal file
328
src/components/backend/PlayerList.vue
Normal 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>
|
526
src/components/backend/ServiceHallView.vue
Normal file
526
src/components/backend/ServiceHallView.vue
Normal 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 // API中content和sendcontent可能混用
|
||||
};
|
||||
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>
|
210
src/components/backend/TournamentForm.vue
Normal file
210
src/components/backend/TournamentForm.vue
Normal 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>
|
294
src/components/backend/TournamentList.vue
Normal file
294
src/components/backend/TournamentList.vue
Normal 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>
|
@ -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('登录失败:未获取到访问令牌')
|
||||
}
|
||||
|
@ -1,15 +1,71 @@
|
||||
<template>
|
||||
<button @click="handleLogout">
|
||||
<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>
|
Loading…
x
Reference in New Issue
Block a user