feature/login-screen #2

Merged
zyb merged 6 commits from feature/login-screen into master 2025-06-27 17:18:19 +08:00
7 changed files with 75 additions and 2336 deletions
Showing only changes of commit 7676649f06 - Show all commits

View File

@ -1,926 +0,0 @@
<template>
<div class="code-generator-container">
<div class="editor-pane">
<h2 class="title">MySQL 表结构生成前端代码</h2>
<p class="description">
在左侧输入 <code>CREATE TABLE</code> 语句右侧将实时预览生成的UI界面
</p>
<div class="form-group">
<label for="sql-input">SQL 输入</label>
<textarea
id="sql-input"
v-model="sqlInput"
rows="15"
placeholder="在此处粘贴 CREATE TABLE 语句..."
@input="generateCode"
></textarea>
</div>
<div class="actions">
<button
@click="downloadCode"
:disabled="!generatedCode"
class="action-button download-button"
>
下载 .vue 文件
</button>
<button
@click="downloadApiFile"
:disabled="!generatedApiCode"
class="action-button download-api-button"
>
下载 API 文件
</button>
</div>
<div v-if="generatedCode" class="generated-code-preview">
<h3 class="preview-title">代码预览 ({{ generatedFileName }})</h3>
<pre><code>{{ generatedCode }}</code></pre>
</div>
</div>
<div class="preview-pane">
<h2 class="preview-area-title">界面预览</h2>
<div v-if="parsedTableInfo" class="preview-content-wrapper">
<div class="device-frame">
<!-- 这里是模拟的组件预览 -->
<div class="service-hall-container-preview">
<div class="actions-bar">
<h3>{{ parsedTableInfo.tableComment || toPascalCase(parsedTableInfo.tableName) + '管理' }}</h3>
<div class="buttons-wrapper">
<button class="add-button">新增{{ parsedTableInfo.tableComment || toPascalCase(parsedTableInfo.tableName) }}</button>
<button class="refresh-button">刷新列表</button>
</div>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
<th v-for="column in parsedTableInfo.columns" :key="column.name">{{ column.comment || column.name }}</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="column in parsedTableInfo.columns" :key="column.name">示例数据 1</td>
<td>
<button class="action-button edit">编辑</button>
<button class="action-button delete">删除</button>
</td>
</tr>
<tr>
<td v-for="column in parsedTableInfo.columns" :key="column.name">示例数据 2</td>
<td>
<button class="action-button edit">编辑</button>
<button class="action-button delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 模拟弹窗 -->
<div class="modal-overlay-preview-active">
<div class="modal-content-preview">
<h3>新增 {{ parsedTableInfo.tableComment || toPascalCase(parsedTableInfo.tableName) }}</h3>
<form>
<div class="form-group-preview" v-for="column in parsedTableInfo.columns.filter(c => c.name !== parsedTableInfo.primaryKey)" :key="column.name">
<label>{{ column.comment || column.name }}:</label>
<input type="text" :placeholder="`请输入${column.comment || column.name}`" />
</div>
<div class="form-actions-preview">
<button type="submit" class="submit-button">保存</button>
<button type="button" class="cancel-button">取消</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-preview">
<p>输入合法的建表语句后<br/>将在此处显示界面预览</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const sqlInput = ref('');
const generatedCode = ref('');
const generatedFileName = ref('');
const generatedApiCode = ref('');
const generatedApiFileName = ref('');
const parsedTableInfo = ref(null);
/**
* 将字符串转换为帕斯卡命名法大驼峰
* @param {string} str 输入字符串 (e.g., user_list)
* @returns {string} 帕斯卡命名法的字符串 (e.g., UserList)
*/
const toPascalCase = (str) => {
if (!str) return '';
return str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.join('');
};
/**
* 解析 CREATE TABLE SQL 语句
* @param {string} sql - CREATE TABLE 语句
* @returns {object|null} 解析后的表结构对象或在解析失败时返回 null
*/
const parseSql = (sql) => {
try {
const tableInfo = {
tableName: '',
tableComment: '',
columns: [],
primaryKey: 'id' // id
};
if (!sql || !sql.trim().toLowerCase().startsWith('create table')) {
return null;
}
// 1.
const tableNameMatch = sql.match(/CREATE TABLE `?(\w+)`?/i);
if (!tableNameMatch) throw new Error('无法解析表名');
tableInfo.tableName = tableNameMatch[1];
// 2.
const tableCommentMatch = sql.match(/\)\s*ENGINE=.*COMMENT='([^']*)'/i);
if (tableCommentMatch) {
tableInfo.tableComment = tableCommentMatch[1];
}
// 3.
const columnsContentMatch = sql.match(/\(([\s\S]*)\)/);
if (!columnsContentMatch) throw new Error('无法解析字段定义');
const columnsContent = columnsContentMatch[1];
const lines = columnsContent.split('\n');
let columns = [];
lines.forEach(line => {
const trimmedLine = line.trim();
//
const primaryKeyMatch = trimmedLine.match(/PRIMARY KEY \(`?(\w+)`?\)/i);
if (primaryKeyMatch) {
tableInfo.primaryKey = primaryKeyMatch[1];
return; //
}
// KEY `...` ()
if (!trimmedLine.startsWith('`')) return;
const columnMatch = trimmedLine.match(/`(\w+)`\s+([\w\(\),_ ]+)\s*(.*)/);
if (columnMatch) {
const name = columnMatch[1];
const type = columnMatch[2].trim();
const rest = columnMatch[3];
let comment = '';
const commentMatch = rest.match(/COMMENT '([^']*)'/i);
if (commentMatch) {
comment = commentMatch[1];
}
columns.push({ name, type, comment: comment || name });
}
});
if (columns.length === 0) throw new Error('未能解析任何字段');
tableInfo.columns = columns;
return tableInfo;
} catch (error) {
console.error('SQL 解析失败:', error);
alert(`SQL 解析失败: ${error.message}`);
return null;
}
};
/**
* 根据表结构生成 API 文件代码
* @param {object} tableInfo - 解析后的表结构
* @returns {string} API 文件代码字符串
*/
const generateApiFile = (tableInfo) => {
const componentName = toPascalCase(tableInfo.tableName);
const resourceName = tableInfo.tableName;
const primaryKey = tableInfo.primaryKey || 'id';
return `
import axiosInstance from './axiosConfig';
const API_BASE_URL = '/api/${resourceName}';
/**
* 获取${tableInfo.tableComment || componentName}列表
* @param {object} params - 查询参数
*/
export const get${componentName}List = (params) => {
return axiosInstance.get(API_BASE_URL, { params });
};
/**
* 获取单个${tableInfo.tableComment || componentName}详情
* @param {number|string} id - 主键ID
*/
export const get${componentName}ById = (id) => {
return axiosInstance.get(\`\${API_BASE_URL}/\${id}\`);
};
/**
* 创建新的${tableInfo.tableComment || componentName}
* @param {object} data - ${tableInfo.tableComment || componentName}数据
*/
export const create${componentName} = (data) => {
return axiosInstance.post(API_BASE_URL, data);
};
/**
* 更新${tableInfo.tableComment || componentName}
* @param {number|string} id - 主键ID
* @param {object} data - 需要更新的数据
*/
export const update${componentName} = (id, data) => {
return axiosInstance.put(\`\${API_BASE_URL}/\${id}\`, data);
};
/**
* 删除${tableInfo.tableComment || componentName}
* @param {number|string} id - 主键ID
*/
export const delete${componentName} = (id) => {
return axiosInstance.delete(\`\${API_BASE_URL}/\${id}\`);
};
`;
};
/**
* 根据表结构生成 Vue 组件代码
* @param {object} tableInfo - 解析后的表结构
* @returns {string} Vue 组件代码字符串
*/
const generateVueComponent = (tableInfo) => {
const componentName = toPascalCase(tableInfo.tableName);
const tableTitle = tableInfo.tableComment || `${componentName}管理`;
const tableComment = tableInfo.tableComment || componentName;
const formFields = tableInfo.columns
.filter(c => c.name !== tableInfo.primaryKey)
.map(c => `
<div class="form-group">
<label for="add-${c.name}">${c.comment || c.name}:</label>
<input id="add-${c.name}" type="text" v-model="editableItem.${c.name}" placeholder="请输入${c.comment || c.name}" />
</div>`).join('');
const dataFields = tableInfo.columns.map(c => ` ${c.name}: '',`).join('\n');
const tableColumns = tableInfo.columns.map(c => {
if (c.type.toLowerCase().includes('datetime') || c.type.toLowerCase().includes('timestamp')) {
return ` <td>{{ formatDate(item.${c.name}) }}</td>`;
}
return ` <td>{{ item.${c.name} }}</td>`;
}).join('\n');
const styleContent = `
.generated-component-container {
/* This is the root container, no extra styles needed */
}
.actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #dee2e6;
}
.actions-bar h3 {
margin: 0;
font-size: 1.5rem;
color: #333;
}
.buttons-wrapper {
display: flex;
gap: 10px;
}
.add-button, .refresh-button {
padding: 10px 15px;
border: none;
color: #fff;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.add-button {
background-color: #007bff;
}
.add-button:hover {
background-color: #0056b3;
}
.refresh-button {
background-color: #6c757d;
}
.refresh-button:hover {
background-color: #5a6268;
}
.table-responsive-wrapper {
overflow-x: auto;
}
.custom-table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
.custom-table th, .custom-table td {
border: 1px solid #dee2e6;
padding: 12px 15px;
text-align: left;
vertical-align: middle;
}
.custom-table thead th {
background-color: #e9ecef;
color: #495057;
font-weight: 600;
}
.custom-table tbody tr:nth-of-type(even) {
background-color: #f8f9fa;
}
.custom-table tbody tr:hover {
background-color: #e2e6ea;
}
.action-button {
padding: 5px 10px;
border: 1px solid transparent;
cursor: pointer;
margin-right: 5px;
color: #fff;
}
.action-button.edit {
background-color: #ffc107;
}
.action-button.delete {
background-color: #dc3545;
}
/* 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: 1050;
}
.modal-content {
background: white;
padding: 25px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
width: 90%;
max-width: 500px;
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.submit-button, .cancel-button {
padding: 10px 20px;
border: none;
cursor: pointer;
}
.submit-button {
background-color: #28a745;
color: white;
}
.cancel-button {
background-color: #6c757d;
color: white;
}
.error-message {
color: #dc3545;
margin-top: 15px;
}`;
return `<template>
<div class="generated-component-container">
<div class="actions-bar">
<h3>${tableTitle}</h3>
<div class="buttons-wrapper">
<button @click="openModal()" class="add-button">新增${tableComment}</button>
<button @click="fetchItems" class="refresh-button">刷新列表</button>
</div>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
${tableInfo.columns.map(c => ` <th>${c.comment || c.name}</th>`).join('\n')}
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="${tableInfo.columns.length + 1}" style="text-align: center;">加载中...</td>
</tr>
<tr v-else-if="items.length === 0">
<td :colspan="${tableInfo.columns.length + 1}" style="text-align: center;">暂无数据</td>
</tr>
<tr v-for="item in items" :key="item.${tableInfo.primaryKey}">
${tableColumns}
<td>
<button @click="openModal(item)" class="action-button edit">编辑</button>
<button @click="confirmDeleteItem(item.${tableInfo.primaryKey})" class="action-button delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 弹窗 -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal-content">
<h2>{{ isEditing ? '编辑' : '新增' }}${tableComment}</h2>
<form @submit.prevent="saveItem">
${formFields}
<div class="form-actions">
<button type="submit" class="submit-button">保存</button>
<button type="button" @click="closeModal" class="cancel-button">取消</button>
</div>
<p v-if="editError" class="error-message">{{ editError }}</p>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import {
get${componentName}List,
get${componentName}ById,
create${componentName},
update${componentName},
delete${componentName}
} from '@/api/${tableInfo.tableName}';
const items = ref([]);
const loading = ref(false);
const showModal = ref(false);
const isEditing = ref(false);
const editableItem = ref({
${dataFields}
});
const editError = ref('');
onMounted(() => {
fetchItems();
});
const fetchItems = async () => {
loading.value = true;
try {
const response = await get${componentName}List();
items.value = response.data;
} catch (error) {
console.error('获取列表失败:', error);
items.value = [];
} finally {
loading.value = false;
}
};
const openModal = async (item = null) => {
if (item) {
isEditing.value = true;
try {
const response = await get${componentName}ById(item.${tableInfo.primaryKey});
editableItem.value = response.data;
} catch (error) {
console.error('获取详情失败:', error);
editableItem.value = { ...item };
}
} else {
isEditing.value = false;
editableItem.value = {
${dataFields}
};
}
editError.value = '';
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
const saveItem = async () => {
editError.value = '';
try {
if (isEditing.value) {
await update${componentName}(editableItem.value.${tableInfo.primaryKey}, editableItem.value);
} else {
await create${componentName}(editableItem.value);
}
closeModal();
fetchItems();
alert('操作成功!');
} catch (error) {
console.error('保存失败:', error);
editError.value = error.response?.data?.message || '操作失败,请重试。';
}
};
const confirmDeleteItem = async (id) => {
if (window.confirm('确定要删除此项吗?')) {
try {
await delete${componentName}(id);
fetchItems();
alert('删除成功!');
} catch (error) {
console.error('删除失败:', error);
alert('删除失败: ' + (error.response?.data?.message || '请稍后重试'));
}
}
};
//
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
return isNaN(date.getTime()) ? dateString : date.toLocaleString();
} catch (e) {
return dateString;
}
};
</sc' + 'ript>
<style scoped>
${styleContent}
</style>`;
};
const generateCode = () => {
if (!sqlInput.value.trim()) {
parsedTableInfo.value = null;
generatedCode.value = '';
generatedApiCode.value = '';
return;
}
const tableInfo = parseSql(sqlInput.value);
if (tableInfo) {
generatedCode.value = generateVueComponent(tableInfo);
generatedFileName.value = `${toPascalCase(tableInfo.tableName)}.vue`;
generatedApiCode.value = generateApiFile(tableInfo);
generatedApiFileName.value = `${tableInfo.tableName}.js`;
parsedTableInfo.value = tableInfo;
} else {
generatedCode.value = '';
generatedFileName.value = '';
generatedApiCode.value = '';
generatedApiFileName.value = '';
parsedTableInfo.value = null;
}
};
const downloadFile = (filename, content) => {
if (!content) return;
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const href = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
}
const downloadCode = () => {
downloadFile(generatedFileName.value, generatedCode.value);
};
const downloadApiFile = () => {
downloadFile(generatedApiFileName.value, generatedApiCode.value);
};
</script>
<style scoped>
.code-generator-container {
display: flex;
height: calc(100vh - 100px);
background-color: #fff;
border: 1px solid #dee2e6;
}
.editor-pane {
flex: 1;
padding: 25px;
overflow-y: auto;
border-right: 1px solid #dee2e6;
}
.preview-pane {
flex: 1;
padding: 25px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.title {
color: #2c3e50;
text-align: center;
margin-bottom: 10px;
}
.description {
color: #555;
text-align: center;
margin-bottom: 25px;
line-height: 1.6;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 6px;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.5;
box-sizing: border-box;
resize: vertical;
min-height: 200px;
}
.actions {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 25px;
}
.action-button {
padding: 10px 20px;
border-radius: 6px;
border: none;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
color: white;
}
.action-button:active {
transform: scale(0.98);
}
.download-button {
background-color: #007bff;
}
.download-button:hover {
background-color: #0069d9;
}
.download-api-button {
background-color: #28a745;
}
.download-api-button:hover {
background-color: #218838;
}
.action-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.generated-code-preview {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.preview-title, .preview-area-title {
color: #333;
margin-bottom: 15px;
text-align: center;
}
pre {
background-color: #2d2d2d;
color: #f1f1f1;
padding: 15px;
border-radius: 6px;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
}
/* Preview Pane Styles */
.no-preview {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: #f8f9fa;
color: #6c757d;
text-align: center;
font-size: 16px;
}
.preview-content-wrapper {
flex-grow: 1;
}
.device-frame {
/* The frame is now the container itself, no extra styles needed */
}
/* Styles for the simulated component inside preview */
.service-hall-container-preview .actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #dee2e6;
}
.service-hall-container-preview h3 {
margin: 0;
font-size: 1.5rem;
color: #333;
}
.service-hall-container-preview .buttons-wrapper {
display: flex;
gap: 10px;
}
.service-hall-container-preview .add-button,
.service-hall-container-preview .refresh-button {
padding: 10px 15px;
border: none;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.service-hall-container-preview .add-button { background-color: #007bff; }
.service-hall-container-preview .refresh-button { background-color: #6c757d; }
.service-hall-container-preview .table-responsive-wrapper {
overflow-x: auto;
}
.service-hall-container-preview .custom-table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
.service-hall-container-preview .custom-table th,
.service-hall-container-preview .custom-table td {
border: 1px solid #dee2e6;
padding: 12px 15px;
text-align: left;
}
.service-hall-container-preview .custom-table thead th {
background-color: #e9ecef;
}
.service-hall-container-preview .action-button {
padding: 5px 10px;
border: none;
color: white;
margin-right: 5px;
}
.service-hall-container-preview .action-button.edit { background-color: #ffc107; }
.service-hall-container-preview .action-button.delete { background-color: #dc3545; }
.modal-overlay-preview-active {
position: relative;
margin-top: 20px;
padding: 20px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
}
.modal-content-preview {
background: white;
padding: 25px;
border: 1px solid #ccc;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
width: 100%;
}
.modal-content-preview h3 {
margin-top: 0;
margin-bottom: 20px;
}
.form-group-preview {
margin-bottom: 15px;
}
.form-group-preview label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group-preview input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
}
.form-actions-preview {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.form-actions-preview .submit-button,
.form-actions-preview .cancel-button {
background-color: #28a745;
color: white;
border: none;
padding: 10px 20px;
}
.form-actions-preview .cancel-button {
background-color: #6c757d;
}
</style>

View File

@ -1,328 +0,0 @@
<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

@ -1,530 +0,0 @@
<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>
<ErrorDialog :visible="showErrorDialog" :message="errorDialogMsg" @close="showErrorDialog = false" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; //
import ErrorDialog from '@/components/ErrorDialog.vue'
const demands = ref([]);
const loading = ref(false);
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 showErrorDialog = ref(false)
const errorDialogMsg = 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 {
const payload = {
requester: editForm.value.requester,
qq_code: editForm.value.qq_code,
sendcontent: editForm.value.sendcontent,
reward: editForm.value.reward,
date: editForm.value.date
};
await updateDemand(currentDemand.value.id, payload);
alert('需求更新成功!');
closeEditModal();
fetchDemandsAdmin();
} catch (error) {
errorDialogMsg.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
showErrorDialog.value = true;
}
};
const confirmDeleteDemand = async (id) => {
if (window.confirm('确定要删除此需求吗?')) {
try {
await deleteDemand(id);
alert('需求删除成功!');
fetchDemandsAdmin();
} catch (error) {
errorDialogMsg.value = error.response?.data?.detail || error.message || '删除失败,请稍后重试';
showErrorDialog.value = true;
}
}
};
// --- 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

@ -1,210 +0,0 @@
<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

@ -1,294 +0,0 @@
<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

@ -5,16 +5,52 @@
<button @click="isMobileSidebarOpen = !isMobileSidebarOpen" class="hamburger-button">
<span class="hamburger-icon"></span>
</button>
<span class="mobile-header-title">后台管理</span>
<span class="mobile-header-title">管理后台</span>
</div>
<div class="admin-sidebar">
<div class="sidebar-header">
<h3>后台管理</h3>
<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>
<li v-if="hasPrivilege(currentUserData.value.privilege, 'lv-admin')">
<div @click="dropdownOpen = !dropdownOpen" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding:15px 20px;">
<span>用户管理</span>
<span :style="{transform: dropdownOpen ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.2s'}"></span>
</div>
<ul v-show="dropdownOpen" style="list-style:none;padding-left:10px;">
<li :class="{active: currentAdminView === 'permission-review'}">
<a @click="selectAdminView('permission-review')">权限审核</a>
</li>
<li :class="{active: currentAdminView === 'user-management'}">
<a @click="selectAdminView('user-management')">用户管理</a>
</li>
</ul>
</li>
<li v-if="hasPrivilege(currentUserData.value.privilege, 'lv-admin')">
<div @click="dropdownOpen2 = !dropdownOpen2" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding:15px 20px;">
<span>赛事管理</span>
<span :style="{transform: dropdownOpen2 ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.2s'}"></span>
</div>
<ul v-show="dropdownOpen2" style="list-style:none;padding-left:10px;">
<li :class="{active: currentAdminView === 'event-info-management'}">
<a @click="selectAdminView('event-info-management')">赛事信息管理</a>
</li>
<li :class="{active: currentAdminView === 'player-management'}">
<a @click="selectAdminView('player-management')">玩家管理</a>
</li>
</ul>
</li>
<li v-if="hasPrivilege(currentUserData.value.privilege, ['lv-admin', 'lv-mod', 'lv-map', 'lv-user', 'lv-competitor'])">
<div @click="dropdownOpen3 = !dropdownOpen3" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding:15px 20px;">
<span>办事大厅</span>
<span :style="{transform: dropdownOpen3 ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.2s'}"></span>
</div>
<ul v-show="dropdownOpen3" style="list-style:none;padding-left:10px;">
<li :class="{active: currentAdminView === 'affair-management'}">
<a @click="selectAdminView('affair-management')">事项管理</a>
</li>
</ul>
</li>
</ul>
<div class="sidebar-footer">
<button @click="selectAdminView('code-generator')" :class="['sidebar-button', 'code-generator-button', { 'active': currentAdminView === 'code-generator' }]">
@ -29,18 +65,7 @@
</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 v-else-if="currentAdminView === 'code-generator'">
<CodeGenerator />
</div>
</div>
</div>
</div>
@ -49,22 +74,23 @@
<script setup>
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'
import CodeGenerator from '../../components/backend/CodeGenerator.vue'
import { getUserInfo } from '@/utils/jwt'
import { hasPrivilege } from '@/utils/privilege'
const router = useRouter()
const hasToken = ref(false)
const userId = ref(localStorage.getItem('user_id'))
const currentUserData = ref(null)
const currentAdminView = ref('event-management')
const isMobileSidebarOpen = ref(false)
const dropdownOpen = ref(false)
const dropdownOpen2 = ref(false)
const dropdownOpen3 = ref(false)
const isAdmin = computed(() => {
return userId.value === '1846172115' || userId.value === '1400429906' || userId.value === '3422054939'
return currentUserData.value && currentUserData.value.privilege === 'lv-admin';
})
onMounted(() => {
onMounted(async () => {
// token
const token = localStorage.getItem('access_token')
hasToken.value = !!token
@ -73,29 +99,21 @@ onMounted(() => {
return;
}
// userId localStorage
// localStorage
const currentStoredUserId = localStorage.getItem('user_id');
if (currentStoredUserId) {
// ref localStorage null
if (userId.value !== currentStoredUserId) {
userId.value = currentStoredUserId;
console.log('Dashboard.vue - 用户ID已在 onMounted 中更新:', userId.value);
} else {
console.log('Dashboard.vue - 当前用户ID (onMounted, 与初始值一致):', userId.value);
//
try {
const userInfo = await getUserInfo();
currentUserData.value = userInfo;
if (!userInfo || userInfo.privilege !== 'lv-admin') {
router.push('/')
}
} else {
// userId.value null null onMounted localStorage null
//
console.log('Dashboard.vue - 未在 localStorage 中检测到用户ID (onMounted)');
} catch (e) {
router.push('/')
}
})
const handleLogout = () => {
// token
localStorage.removeItem('access_token')
// ID
localStorage.removeItem('user_id')
// ()
currentAdminView.value = 'event-management'
isMobileSidebarOpen.value = false; // 退

View File

@ -2,17 +2,14 @@
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { getUserInfo } from '@/utils/jwt'
import { useRouter } from 'vue-router'
import { hasPrivilege } from '@/utils/privilege'
const isLoggedIn = computed(() => {
return !!localStorage.getItem('access_token') && !!currentUserData.value
})
const isAdmin = computed(() => {
if (currentUserData.value && currentUserData.value.qq_code) {
const adminQQNumbers = ['1846172115', '1400429906', '3422054939'];
return adminQQNumbers.indexOf(String(currentUserData.value.qq_code)) !== -1;
}
return false;
return currentUserData.value && currentUserData.value.privilege === 'lv-admin';
})
const showMobileMenu = ref(false)
@ -83,9 +80,21 @@ onUnmounted(() => {
<router-link to="/weekly" class="nav-link">热门下载地图</router-link>
<router-link to="/author" class="nav-link">活跃作者推荐</router-link>
<router-link to="/terrain" class="nav-link">地形图列表</router-link>
<router-link v-if="isLoggedIn" to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
<router-link v-if="isLoggedIn" to="/competition" class="nav-link">赛程信息</router-link>
<router-link v-if="isLoggedIn" to="/demands" class="nav-link">办事大厅</router-link>
<router-link
v-if="isLoggedIn && currentUserData && hasPrivilege(currentUserData.privilege, ['lv-admin', 'lv-mod'])"
to="/weapon-match"
class="nav-link"
>Weapon 匹配</router-link>
<router-link
v-if="isLoggedIn && currentUserData && hasPrivilege(currentUserData.privilege, ['lv-admin', 'lv-competitor'])"
to="/competition"
class="nav-link"
>赛程信息</router-link>
<router-link
v-if="isLoggedIn && currentUserData && hasPrivilege(currentUserData.privilege, ['lv-admin', 'lv-mod', 'lv-map', 'lv-user', 'lv-competitor'])"
to="/demands"
class="nav-link"
>办事大厅</router-link>
</div>
<div class="nav-right" :class="{ active: showMobileMenu }">
<router-link v-if="!isLoggedIn" to="/backend/login" class="nav-link login-btn">