权限控制
This commit is contained in:
parent
8f30b943a7
commit
7676649f06
@ -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>
|
@ -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>
|
@ -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 // 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 {
|
||||
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>
|
@ -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>
|
@ -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>
|
@ -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; // 退出时关闭侧边栏
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user