Files
workspace/website/naga/index.html
2026-01-12 10:49:31 +08:00

916 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Naga解析系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.admin-badge {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
margin-top: 10px;
}
.auth-section {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.auth-controls {
display: flex;
gap: 10px;
align-items: center;
}
.auth-controls input {
flex: 1;
max-width: 300px;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.form-section {
padding: 30px;
background: #fafbfc;
border-bottom: 2px solid #e0e0e0;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
.form-group input,
.form-group textarea {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.char-count {
font-size: 12px;
color: #666;
margin-top: 5px;
text-align: right;
}
.char-count.warning {
color: #e74c3c;
}
.submit-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.submit-btn:hover {
transform: translateY(-2px);
}
.submit-btn:active {
transform: translateY(0);
}
.table-section {
padding: 30px;
overflow-x: auto;
}
.table-section h2 {
margin-bottom: 20px;
color: #333;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 15px;
flex-wrap: wrap;
}
.sort-control {
display: flex;
gap: 10px;
align-items: center;
}
.sort-btn {
padding: 8px 15px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.sort-btn:hover {
background: #5568d3;
}
.sort-btn.active {
background: #764ba2;
}
.pagination {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 20px;
}
.page-btn {
padding: 8px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.page-btn:hover {
background: #e0e0e0;
}
.page-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 13px;
color: #666;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
th {
padding: 15px 10px;
text-align: left;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
}
td {
padding: 12px 10px;
border-bottom: 1px solid #e0e0e0;
font-size: 14px;
}
tbody tr:hover {
background: #f8f9fa;
}
.link-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.link-cell a {
color: #667eea;
text-decoration: none;
}
.link-cell a:hover {
text-decoration: underline;
}
.status-yes {
color: #27ae60;
font-weight: 600;
}
.status-no {
color: #e74c3c;
font-weight: 600;
}
.edit-input {
width: 100%;
padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
.edit-btn {
padding: 5px 12px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 5px;
}
.edit-btn:hover {
background: #5568d3;
}
.save-btn {
background: #27ae60;
}
.save-btn:hover {
background: #229954;
}
.cancel-btn {
background: #95a5a6;
}
.cancel-btn:hover {
background: #7f8c8d;
}
.delete-btn {
background: #e74c3c;
}
.delete-btn:hover {
background: #c0392b;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.error-message {
background: #fee;
color: #c33;
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 14px;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
table {
font-size: 12px;
}
th, td {
padding: 8px 5px;
}
.table-controls {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Naga解析系统</h1>
<div id="roleDisplay"></div>
</div>
<div class="auth-section">
<div class="auth-controls">
<input type="password" id="adminPassword" placeholder="输入管理员密码">
<button class="btn btn-primary" onclick="login()">登录为管理员</button>
<button class="btn btn-secondary" onclick="logout()">退出管理员</button>
</div>
</div>
<div class="form-section">
<h2>提交新数据</h2>
<div id="errorMessage"></div>
<form id="dataForm">
<div class="form-grid">
<div class="form-group">
<label for="userName">人名 *</label>
<input type="text" id="userName" required>
</div>
<div class="form-group">
<label for="recordLink">牌谱链接 *</label>
<input type="url" id="recordLink" required placeholder="https://tenhou.net/0/" maxlength="200">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label for="remarks">备注说明 (选填少于100字)</label>
<textarea id="remarks" maxlength="100"></textarea>
<div class="char-count" id="charCount">0 / 100</div>
</div>
</div>
<button type="submit" class="submit-btn">提交数据</button>
</form>
</div>
<div class="table-section">
<h2>数据列表</h2>
<div class="table-controls">
<div class="sort-control">
<span>排序方式:</span>
<button class="sort-btn active" onclick="setSortOrder('desc')">降序</button>
<button class="sort-btn" onclick="setSortOrder('asc')">升序</button>
</div>
<div class="page-info" id="pageInfo"></div>
</div>
<div id="tableContainer"></div>
<div class="pagination" id="pagination"></div>
</div>
</div>
<script>
// 数据存储
let dataList = [];
let isAdmin = false;
let editingId = null;
// 分页和排序
let currentPage = 1;
let itemsPerPage = 10;
let sortOrder = 'desc'; // 'asc' 或 'desc'
// 初始化
async function init() {
await loadData();
updateRoleDisplay();
renderTable();
setupEventListeners();
}
// 加载数据
async function loadData() {
try {
const response = await fetch('api.php');
const data = await response.json();
dataList = data;
} catch (error) {
console.error('加载数据失败:', error);
dataList = [];
}
const adminStatus = localStorage.getItem('isAdmin');
if (adminStatus === 'true') {
isAdmin = true;
}
}
// 保存数据
async function saveData() {
try {
const response = await fetch('api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(dataList)
});
const result = await response.json();
if (!result.success) {
console.error('保存失败');
}
} catch (error) {
console.error('保存数据失败:', error);
}
}
// 设置排序顺序
function setSortOrder(order) {
sortOrder = order;
currentPage = 1;
// 更新按钮状态
document.querySelectorAll('.sort-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
renderTable();
}
// 获取排序后的数据
function getSortedData() {
const sorted = [...dataList];
if (sortOrder === 'asc') {
return sorted.reverse();
}
return sorted;
}
// 获取当前页数据
function getCurrentPageData() {
const sorted = getSortedData();
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
return sorted.slice(start, end);
}
// 渲染分页控件
function renderPagination() {
const totalPages = Math.ceil(dataList.length / itemsPerPage);
const paginationDiv = document.getElementById('pagination');
if (totalPages <= 1) {
paginationDiv.innerHTML = '';
return;
}
let html = '';
// 上一页按钮
html += `<button class="page-btn" onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>上一页</button>`;
// 页码按钮
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="changePage(${i})">${i}</button>`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
html += `<span>...</span>`;
}
}
// 下一页按钮
html += `<button class="page-btn" onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>下一页</button>`;
paginationDiv.innerHTML = html;
// 更新页面信息
const start = (currentPage - 1) * itemsPerPage + 1;
const end = Math.min(currentPage * itemsPerPage, dataList.length);
document.getElementById('pageInfo').textContent = `显示 ${start}-${end} / 共 ${dataList.length}`;
}
// 切换页码
function changePage(page) {
const totalPages = Math.ceil(dataList.length / itemsPerPage);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderTable();
}
// 更新角色显示
function updateRoleDisplay() {
const display = document.getElementById('roleDisplay');
if (isAdmin) {
display.innerHTML = '<span class="admin-badge">✓ 管理员模式</span>';
} else {
display.innerHTML = '<span class="admin-badge">访客模式</span>';
}
}
// 登录
async function login() {
const password = document.getElementById('adminPassword').value;
try {
const response = await fetch('api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=login&password=${encodeURIComponent(password)}`
});
const result = await response.json();
if (result.success) {
isAdmin = true;
localStorage.setItem('isAdmin', 'true');
updateRoleDisplay();
renderTable();
document.getElementById('adminPassword').value = '';
showError('登录成功!', 'success');
} else {
showError('密码错误!');
}
} catch (error) {
showError('登录失败,请重试!');
}
}
// 登出
function logout() {
isAdmin = false;
localStorage.removeItem('isAdmin');
updateRoleDisplay();
renderTable();
showError('已退出管理员模式', 'success');
}
// 显示错误信息
function showError(message, type = 'error') {
const errorDiv = document.getElementById('errorMessage');
errorDiv.innerHTML = `<div class="error-message" style="background: ${type === 'success' ? '#d4edda' : '#fee'}; color: ${type === 'success' ? '#155724' : '#c33'};">${message}</div>`;
setTimeout(() => {
errorDiv.innerHTML = '';
}, 3000);
}
// 设置事件监听
function setupEventListeners() {
// 表单提交
document.getElementById('dataForm').addEventListener('submit', handleSubmit);
// 字数统计
const remarks = document.getElementById('remarks');
remarks.addEventListener('input', function() {
const count = this.value.length;
const countDiv = document.getElementById('charCount');
countDiv.textContent = `${count} / 100`;
if (count > 100) {
countDiv.classList.add('warning');
} else {
countDiv.classList.remove('warning');
}
});
}
// 处理表单提交
function handleSubmit(e) {
e.preventDefault();
const userName = document.getElementById('userName').value.trim();
const recordLink = document.getElementById('recordLink').value.trim();
const remarks = document.getElementById('remarks').value.trim();
if (remarks.length > 100) {
showError('备注说明不能超过100字!');
return;
}
// URL验证
try {
new URL(recordLink);
} catch {
showError('请输入有效的URL链接!');
return;
}
// 创建新数据
const newData = {
id: Date.now(),
submitTime: formatDate(new Date()),
userName,
recordLink,
remarks,
nagaLink: '',
nagaRemarks: ''
};
dataList.unshift(newData);
saveData();
currentPage = 1; // 回到第一页
renderTable();
// 重置表单
document.getElementById('dataForm').reset();
document.getElementById('charCount').textContent = '0 / 100';
showError('提交成功!', 'success');
}
// 格式化日期
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// 渲染表格
function renderTable() {
const container = document.getElementById('tableContainer');
if (dataList.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<p>暂无数据,请提交第一条记录</p>
</div>
`;
document.getElementById('pagination').innerHTML = '';
document.getElementById('pageInfo').textContent = '';
return;
}
const pageData = getCurrentPageData();
const sortedData = getSortedData();
const startIndex = (currentPage - 1) * itemsPerPage;
let html = '<table><thead><tr>';
html += '<th>序号</th>';
html += '<th>提交时间</th>';
html += '<th>人名</th>';
html += '<th>牌谱链接</th>';
html += '<th>备注说明</th>';
html += '<th>是否有Naga链接</th>';
html += '<th>Naga链接</th>';
html += '<th>Naga说明</th>';
if (isAdmin) {
html += '<th>操作</th>';
}
html += '</tr></thead><tbody>';
pageData.forEach((item, index) => {
const isEditing = editingId === item.id;
const hasNaga = item.nagaLink && item.nagaLink.trim() !== '';
const displayIndex = startIndex + index + 1;
html += '<tr>';
html += `<td>${displayIndex}</td>`;
html += `<td>${item.submitTime}</td>`;
// 管理员可编辑人名
if (isEditing && isAdmin) {
html += `<td><input type="text" class="edit-input" id="editUserName_${item.id}" value="${escapeHtml(item.userName)}"></td>`;
} else {
html += `<td>${escapeHtml(item.userName)}</td>`;
}
// 管理员可编辑牌谱链接
if (isEditing && isAdmin) {
html += `<td><input type="url" class="edit-input" id="editRecordLink_${item.id}" value="${escapeHtml(item.recordLink)}"></td>`;
} else {
html += `<td class="link-cell"><a href="${escapeHtml(item.recordLink)}" target="_blank">${escapeHtml(item.recordLink)}</a></td>`;
}
// 管理员可编辑备注
if (isEditing && isAdmin) {
html += `<td><input type="text" class="edit-input" id="editRemarks_${item.id}" value="${escapeHtml(item.remarks)}" maxlength="100"></td>`;
} else {
html += `<td>${escapeHtml(item.remarks)}</td>`;
}
html += `<td class="${hasNaga ? 'status-yes' : 'status-no'}">${hasNaga ? '✔ 有' : '✘ 无'}</td>`;
if (isEditing && isAdmin) {
html += `<td><input type="url" class="edit-input" id="editNagaLink_${item.id}" value="${escapeHtml(item.nagaLink)}"></td>`;
html += `<td><input type="text" class="edit-input" id="editNagaRemarks_${item.id}" value="${escapeHtml(item.nagaRemarks)}" maxlength="100"></td>`;
} else {
html += `<td class="link-cell">${item.nagaLink ? `<a href="${escapeHtml(item.nagaLink)}" target="_blank">${escapeHtml(item.nagaLink)}</a>` : '-'}</td>`;
html += `<td>${item.nagaRemarks ? escapeHtml(item.nagaRemarks) : '-'}</td>`;
}
if (isAdmin) {
if (isEditing) {
html += `<td>
<button class="edit-btn save-btn" onclick="saveEdit(${item.id})">保存</button>
<button class="edit-btn cancel-btn" onclick="cancelEdit()">取消</button>
</td>`;
} else {
html += `<td>
<button class="edit-btn" onclick="startEdit(${item.id})">编辑</button>
<button class="edit-btn delete-btn" onclick="deleteRecord(${item.id})">删除</button>
</td>`;
}
}
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
renderPagination();
}
// 开始编辑
function startEdit(id) {
editingId = id;
renderTable();
}
// 保存编辑
function saveEdit(id) {
const userName = document.getElementById(`editUserName_${id}`).value.trim();
const recordLink = document.getElementById(`editRecordLink_${id}`).value.trim();
const remarks = document.getElementById(`editRemarks_${id}`).value.trim();
const nagaLink = document.getElementById(`editNagaLink_${id}`).value.trim();
const nagaRemarks = document.getElementById(`editNagaRemarks_${id}`).value.trim();
// 验证人名
if (!userName) {
showError('人名不能为空!');
return;
}
// URL验证 - 牌谱链接
try {
new URL(recordLink);
} catch {
showError('请输入有效的牌谱链接URL!');
return;
}
// URL验证 - Naga链接
if (nagaLink && nagaLink !== '') {
try {
new URL(nagaLink);
} catch {
showError('请输入有效的Naga链接URL!');
return;
}
}
// 字数验证
if (remarks.length > 100) {
showError('备注说明不能超过100字!');
return;
}
if (nagaRemarks.length > 100) {
showError('Naga说明不能超过100字!');
return;
}
const item = dataList.find(d => d.id === id);
if (item) {
item.userName = userName;
item.recordLink = recordLink;
item.remarks = remarks;
item.nagaLink = nagaLink;
item.nagaRemarks = nagaRemarks;
saveData();
editingId = null;
renderTable();
showError('更新成功!', 'success');
}
}
// 取消编辑
function cancelEdit() {
editingId = null;
renderTable();
}
// 删除记录
function deleteRecord(id) {
if (confirm('确定要删除这条记录吗?此操作不可恢复!')) {
const index = dataList.findIndex(d => d.id === id);
if (index !== -1) {
dataList.splice(index, 1);
saveData();
// 如果当前页没有数据了,回到上一页
const totalPages = Math.ceil(dataList.length / itemsPerPage);
if (currentPage > totalPages && currentPage > 1) {
currentPage = totalPages;
}
renderTable();
showError('删除成功!', 'success');
}
}
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 页面加载时初始化
init();
</script>
</body>
</html>