// Admin Dashboard page component // Full CRUD management + statistics dashboard // Renders inside main content area via React component const { useState, useEffect, useMemo } = React; // ----------------------------- // Confirm Dialog // ----------------------------- function ConfirmDialog({ isOpen, title, message, onConfirm, onCancel, danger }) { if (!isOpen) return null; return (
e.stopPropagation()}>

{title}

{message}

); } const confirmOverlayStyle = { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(2px)', }; const confirmBoxStyle = { background: 'linear-gradient(180deg, var(--bg-2), var(--bg-1))', border: '1px solid var(--border-1)', borderRadius: 6, padding: 24, width: 420, maxWidth: '90vw', boxShadow: '0 20px 60px rgba(0,0,0,0.5), 0 0 40px rgba(196,90,58,0.1)', }; const btnPrimaryStyle = { padding: '7px 18px', background: 'linear-gradient(135deg, var(--accent-rust), var(--accent-rust-dim))', border: '1px solid var(--accent-rust)', borderRadius: 3, color: '#fff', cursor: 'pointer', fontSize: 12, fontWeight: 500, boxShadow: '0 2px 6px rgba(196,90,58,0.3)', fontFamily: 'var(--sans)', }; const btnSecondaryStyle = { padding: '7px 18px', background: 'var(--bg-2)', border: '1px solid var(--border-1)', borderRadius: 3, color: 'var(--text-1)', cursor: 'pointer', fontSize: 12, fontFamily: 'var(--sans)', }; const btnGhostStyle = { padding: '4px 12px', background: 'transparent', border: '1px solid var(--border-1)', borderRadius: 2, color: 'var(--text-2)', cursor: 'pointer', fontSize: 11, fontFamily: 'var(--sans)', }; const inputStyle = { width: '100%', height: 36, padding: '0 10px', background: 'var(--bg-0)', border: '1px solid var(--border-1)', borderRadius: 3, color: 'var(--text-0)', fontSize: 12, fontFamily: 'var(--sans)', outline: 'none', }; const labelStyle = { display: 'block', color: 'var(--text-2)', fontSize: 11, marginBottom: 5, letterSpacing: 0.5, }; const tableStyle = { width: '100%', borderCollapse: 'collapse', background: 'var(--bg-1)', fontSize: 12, }; const thStyle = { background: 'linear-gradient(180deg, var(--bg-2), var(--bg-3))', padding: '10px 12px', textAlign: 'left', color: 'var(--accent-mustard)', fontWeight: 600, borderBottom: '2px solid var(--accent-rust-dim)', fontSize: 12, whiteSpace: 'nowrap', }; const tdStyle = { padding: '8px 12px', borderBottom: '1px solid var(--border-0)', color: 'var(--text-1)', fontFamily: 'var(--sans)', }; // ----------------------------- // Admin Login Page // ----------------------------- function AdminLoginPage({ onLogin }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(''); try { const data = await window.api.adminLogin(username, password); if (data.success) { localStorage.setItem('admin_token', data.token); onLogin(data.admin, data.token); } else { setError('账号或密码错误,请重试'); } } catch (err) { setError('网络错误,请检查后端服务是否运行'); } setLoading(false); }; return (
🔐

管理员登录

ADMIN LOGIN

setUsername(e.target.value)} style={inputStyle} placeholder="admin" autoFocus />
setPassword(e.target.value)} style={inputStyle} placeholder="admin123" />
{error && (
{error}
)}
默认账号 admin / admin123
); } const loginBoxStyle = { width: 380, maxWidth: '90vw', padding: 32, background: 'linear-gradient(180deg, var(--bg-1), var(--bg-2))', border: '1px solid var(--border-0)', borderRadius: 6, boxShadow: '0 20px 60px rgba(0,0,0,0.4), 0 0 40px rgba(196,90,58,0.08)', position: 'relative', }; // ----------------------------- // Admin Sidebar // ----------------------------- function AdminSidebar({ activeTab, onTabChange, admin, onLogout }) { const tabs = [ { key: 'dashboard', label: '统计面板', icon: '📊' }, { key: 'items', label: '物品管理', icon: '📦' }, { key: 'recipes', label: '合成配方', icon: '⚗️' }, { key: 'medicines', label: '药品效果', icon: '💊' }, { key: 'buildings', label: '建筑耐久', icon: '🏗️' }, { key: 'players', label: '玩家管理', icon: '👥' }, { key: 'password', label: '修改密码', icon: '🔑' }, { key: 'data', label: '数据管理', icon: '💾' }, ]; return (
{admin.username.charAt(0).toUpperCase()}
{admin.username}
ADMIN
{tabs.map((t) => (
onTabChange(t.key)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 18px', cursor: 'pointer', fontSize: 13, color: activeTab === t.key ? 'var(--accent-mustard)' : 'var(--text-1)', background: activeTab === t.key ? 'rgba(196,90,58,0.12)' : 'transparent', borderLeft: activeTab === t.key ? '3px solid var(--accent-rust)' : '3px solid transparent', transition: 'all 0.15s', }} > {t.icon} {t.label}
))}
🚪 退出登录
); } // ----------------------------- // Generic CRUD Table // ----------------------------- function GenericCrudPage({ title, description, columns, data, onAdd, onEdit, onDelete, searchable = true, addLabel = '新增' }) { const [search, setSearch] = useState(''); const [confirmDelete, setConfirmDelete] = useState(null); const filtered = useMemo(() => { if (!search || !searchable) return data; const s = search.toLowerCase(); return data.filter(row => columns.some(col => col.key && row[col.key]?.toString().toLowerCase().includes(s) ) ); }, [data, search, columns, searchable]); return (

{title}

{description}

{searchable && ( setSearch(e.target.value)} style={{ ...inputStyle, width: 200 }} /> )}
{columns.map(c => ( ))} {filtered.length === 0 && ( )} {filtered.map((row, i) => ( {columns.map(c => ( ))} ))}
{c.label}操作
暂无数据
{c.render ? c.render(row) : row[c.key]}
共 {filtered.length} 条数据
{confirmDelete && ( { onDelete(confirmDelete); setConfirmDelete(null); }} onCancel={() => setConfirmDelete(null)} /> )}
); } // ----------------------------- // Item Form Dialog // ----------------------------- function ItemFormDialog({ item, categories, onSave, onClose }) { const [form, setForm] = useState(item || { item_id: '', name_en: '', name_zh: '', category: 'head' }); const [error, setError] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); setError(''); const result = await onSave(form); if (result?.error) setError(result.error); }; return (
e.stopPropagation()}>

{item ? '编辑物品' : '新增物品'}

setForm({ ...form, item_id: e.target.value })} required />
setForm({ ...form, name_en: e.target.value })} required />
setForm({ ...form, name_zh: e.target.value })} required />
{error &&
{error}
}
); } // ----------------------------- // Medicine Form Dialog // ----------------------------- function MedicineFormDialog({ item, onSave, onClose }) { const [form, setForm] = useState(item || { name_en: '', name_zh: '', health: 0, radiation: 0, tier: 'low', special_effects: [] }); const toggleEffect = (eff) => { const effects = form.special_effects || []; const has = effects.includes(eff); setForm({ ...form, special_effects: has ? effects.filter(e => e !== eff) : [...effects, eff], }); }; const handleSubmit = (e) => { e.preventDefault(); onSave(form); }; return (
e.stopPropagation()}>

{item ? '编辑药品' : '新增药品'}

setForm({ ...form, name_en: e.target.value })} required />
setForm({ ...form, name_zh: e.target.value })} required />
setForm({ ...form, health: parseInt(e.target.value) || 0 })} />
setForm({ ...form, radiation: parseInt(e.target.value) || 0 })} />
{[['bleed', '止血'], ['brokenLeg', '治断腿'], ['stamina', '回体力']].map(([k, v]) => ( ))}
); } // ----------------------------- // Recipe Form Dialog // ----------------------------- function RecipeFormDialog({ recipe, onSave, onClose }) { const [form, setForm] = useState(recipe || { result_zh: '', result_en: '', category: 'tools', craftsman_level: 1, result_count: 1, materials: [{ name_zh: '', name_en: '', count: 1 }] }); const [error, setError] = useState(''); const handleMaterialChange = (index, field, value) => { const newMaterials = [...form.materials]; newMaterials[index][field] = value; setForm({ ...form, materials: newMaterials }); }; const addMaterial = () => { setForm({ ...form, materials: [...form.materials, { name_zh: '', name_en: '', count: 1 }] }); }; const removeMaterial = (index) => { if (form.materials.length <= 1) return; const newMaterials = form.materials.filter((_, i) => i !== index); setForm({ ...form, materials: newMaterials }); }; const handleSubmit = async (e) => { e.preventDefault(); setError(''); for (const m of form.materials) { if (!m.name_zh.trim() || !m.name_en.trim() || m.count < 1) { return setError('请完整填写所有材料(名称和数量)'); } } const result = await onSave(form); if (result?.error) setError(result.error); }; const categoriesList = [ { key: 'medicine', name_zh: '药品', name_en: 'Medicine' }, { key: 'building', name_zh: '建筑', name_en: 'Building' }, { key: 'storage', name_zh: '储物', name_en: 'Storage' }, { key: 'defense', name_zh: '防御', name_en: 'Defense' }, { key: 'tools', name_zh: '工具', name_en: 'Tools' }, { key: 'components', name_zh: '材料', name_en: 'Components' }, { key: 'military', name_zh: '军用', name_en: 'Military' }, { key: 'clothes', name_zh: '衣物', name_en: 'Clothes' }, { key: 'furniture', name_zh: '家具', name_en: 'Furniture' }, ]; return (
e.stopPropagation()}>

{recipe ? '编辑配方' : '新增配方'}

setForm({ ...form, result_zh: e.target.value })} required />
setForm({ ...form, result_en: e.target.value })} required />
setForm({ ...form, result_count: parseInt(e.target.value) || 1 })} min="1" required />
{form.materials.map((mat, index) => (
handleMaterialChange(index, 'name_zh', e.target.value)} required />
handleMaterialChange(index, 'name_en', e.target.value)} required />
handleMaterialChange(index, 'count', parseInt(e.target.value) || 1)} min="1" required />
))}
{error &&
{error}
}
); } // ----------------------------- // Player Edit Dialog // ----------------------------- function PlayerEditDialog({ player, onSave, onClose }) { const [form, setForm] = useState({ qq_number: player?.qq_number || '', game_id: player?.game_id || '', }); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { const updates = {}; if (form.qq_number !== player.qq_number) updates.qq_number = form.qq_number; if (form.game_id !== player.game_id) updates.game_id = form.game_id; if (Object.keys(updates).length === 0) { setError('没有修改任何内容'); setLoading(false); return; } await onSave(player.id, updates); setLoading(false); } catch (err) { setError(err.message || '保存失败'); setLoading(false); } }; return (
e.stopPropagation()}>

编辑玩家信息

setForm({ ...form, qq_number: e.target.value })} placeholder="请输入QQ号" />
setForm({ ...form, game_id: e.target.value })} placeholder="请输入游戏ID" />
⚠️ 修改后立即生效,玩家下次登录时使用新信息。
{error &&
{error}
}
); } // ----------------------------- // Admin Dashboard // ----------------------------- function AdminDashboard() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { loadStats(); }, []); const loadStats = async () => { setLoading(true); const data = await window.api.getStats(); setStats(data); setLoading(false); }; if (loading) { return
加载中...
; } if (!stats || stats.error) { return
加载失败:{stats?.error || '未知错误'}
; } const statCards = [ { label: '总访问量', value: stats.totalVisits, icon: '📈', color: 'var(--accent-mustard)' }, { label: '今日访问', value: stats.todayVisits, icon: '📅', color: 'var(--accent-green)' }, { label: '本周访问', value: stats.weekVisits, icon: '🗓️', color: 'var(--accent-rust)' }, { label: '注册玩家', value: stats.totalPlayers, icon: '👥', color: '#5ab8d8' }, ]; return (

统计面板

{statCards.map((s, i) => (
{s.icon}
{s.value.toLocaleString()}
{s.label}
))}

🏆 热门物品 Top 10

{stats.topItems?.length === 0 &&
暂无数据
}
{stats.topItems?.map((item, i) => (
{i + 1}
{item.target_name || item.target_id}
{item.view_count}
))}

📊 访问页面分布

{stats.topPages?.length === 0 &&
暂无数据
}
{stats.topPages?.map((p, i) => { const max = Math.max(...stats.topPages.map(x => x.view_count)); const pct = (p.view_count / max) * 100; return (
{p.page_type} {p.view_count}
); })}

📈 近7天访问趋势

{stats.trend?.length === 0 &&
暂无数据
} {stats.trend?.map((d, i) => { const max = Math.max(...stats.trend.map(x => x.count), 1); const pct = (d.count / max) * 100; return (
{d.date?.slice(5)}
{d.count}
); })}

👤 最近注册玩家

{stats.recentPlayers?.length === 0 &&
暂无注册
}
{stats.recentPlayers?.map((p, i) => (
{p.game_id?.charAt(0) || '?'}
{p.game_id}
QQ: {p.qq_number}
{p.created_at?.slice(5, 10)}
))}

🕐 最近访问记录

{stats.recentVisits?.length === 0 &&
暂无记录
} {stats.recentVisits?.map((v, i) => (
{v.page_type === 'item' ? '📦' : v.page_type === 'medicine' ? '💊' : v.page_type === 'recipe' ? '⚗️' : v.page_type === 'building' ? '🏗️' : '📄'}
{v.target_name || v.page_type} {v.player_game_id && ( · {v.player_game_id} (QQ: {v.player_qq}) )}
{v.created_at?.slice(0, 19).replace('T', ' ')}
))}
); } const panelStyle = { background: 'linear-gradient(180deg, var(--bg-1), var(--bg-2))', border: '1px solid var(--border-0)', borderRadius: 5, padding: 18, boxShadow: '0 2px 8px rgba(0,0,0,0.15)', }; const panelTitleStyle = { color: 'var(--text-0)', fontSize: 14, fontWeight: 600, margin: '0 0 14px 0', paddingBottom: 10, borderBottom: '1px solid var(--border-0)', }; // ----------------------------- // Password change page // ----------------------------- function ChangePasswordPage({ onSuccess }) { const [oldPw, setOldPw] = useState(''); const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); setError(''); setSuccess(''); if (newPw !== confirmPw) return setError('两次输入的新密码不一致'); if (newPw.length < 6) return setError('新密码至少6位'); const data = await window.api.adminChangePassword(oldPw, newPw); if (data.success) { setSuccess('密码修改成功!'); setOldPw(''); setNewPw(''); setConfirmPw(''); } else { setError(data.error || '修改失败'); } }; return (

修改管理员密码

定期修改密码以保证后台安全

setOldPw(e.target.value)} required />
setNewPw(e.target.value)} required />
setConfirmPw(e.target.value)} required />
{error &&
{error}
} {success &&
{success}
}
); } // ----------------------------- // Data Management page // ----------------------------- function DataManagementPage() { const [confirmReset, setConfirmReset] = useState(false); const [message, setMessage] = useState(''); const doReset = async () => { const data = await window.api.resetData(); setMessage(data.message || data.error || '操作完成'); setConfirmReset(false); }; return (

数据管理

从初始种子数据重建数据库。警告:会覆盖所有手动修改的数据!

⚠️ 危险操作

「一键导入初始数据」会将所有物品、配方、药品、建筑等数据重置为初始版本。
手动新增或修改的内容将被覆盖。

{message &&
{message}
}
setConfirmReset(false)} />
); } // ----------------------------- // Players management page(完整版,含编辑功能) // ----------------------------- function PlayersPage() { const [players, setPlayers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [editingPlayer, setEditingPlayer] = useState(null); const [showEditDialog, setShowEditDialog] = useState(false); const load = async () => { setLoading(true); try { const data = await window.api.getPlayers({ limit: 200, q: search }); setPlayers(data || []); } catch (err) { console.error('加载玩家失败:', err); setPlayers([]); } setLoading(false); }; useEffect(() => { load(); }, [search]); const handleDelete = async (player) => { await window.api.deletePlayer(player.id); load(); }; const handleEdit = (player) => { setEditingPlayer(player); setShowEditDialog(true); }; const handleSaveEdit = async (id, updates) => { await window.api.updatePlayer(id, updates); setShowEditDialog(false); setEditingPlayer(null); load(); }; const columns = [ { key: 'id', label: 'ID' }, { key: 'qq_number', label: 'QQ号' }, { key: 'game_id', label: '游戏ID' }, { key: 'created_at', label: '注册时间', render: (r) => {r.created_at?.slice(0,19).replace('T',' ')} }, ]; return ( <> {}} onEdit={handleEdit} onDelete={handleDelete} /> {showEditDialog && editingPlayer && ( { setShowEditDialog(false); setEditingPlayer(null); }} /> )} ); } // ----------------------------- // Main Admin Page // ----------------------------- function AdminPage() { const [admin, setAdmin] = useState(null); const [tab, setTab] = useState('dashboard'); const [items, setItems] = useState([]); const [categories, setCategories] = useState([]); const [editItem, setEditItem] = useState(null); const [showItemForm, setShowItemForm] = useState(false); const [medicines, setMedicines] = useState([]); const [editMed, setEditMed] = useState(null); const [showMedForm, setShowMedForm] = useState(false); const [buildings, setBuildings] = useState([]); const [recipes, setRecipes] = useState([]); const [editRecipe, setEditRecipe] = useState(null); const [showRecipeForm, setShowRecipeForm] = useState(false); const [confirmDeleteRecipe, setConfirmDeleteRecipe] = useState(null); // Check existing token useEffect(() => { const token = localStorage.getItem('admin_token'); if (token) { window.api.adminMe().then(data => { if (data.admin) setAdmin(data.admin); }).catch(() => {}); } }, []); const handleLogin = (adminData, token) => { setAdmin(adminData); loadAll(); window.dispatchEvent(new CustomEvent('adminLogin', { detail: { admin: adminData, token: token } })); }; const handleLogout = () => { localStorage.removeItem('admin_token'); setAdmin(null); window.location.hash = '#home'; }; const loadAll = async () => { const [itemData, medData, buildingData, recipeData] = await Promise.all([ window.api.getItems({ limit: 500 }), window.api.getMedicinesAdmin(), window.api.getBuildingsAdmin(), window.api.getRecipes({ limit: 200 }), ]); setItems(itemData || []); setMedicines(medData || []); setBuildings(buildingData || []); setRecipes(recipeData || []); try { const meta = await window.api.getAllData(); setCategories(meta.categories || []); } catch {} }; // Items const handleSaveItem = async (form) => { let data; if (editItem) { data = await window.api.updateItem(editItem.id, form); } else { data = await window.api.createItem(form); } if (data.error) return { error: data.error }; loadAll(); setShowItemForm(false); setEditItem(null); return {}; }; const handleDeleteItem = async (item) => { await window.api.deleteItem(item.id); loadAll(); }; // Medicines const handleSaveMed = async (form) => { if (editMed) { await window.api.updateMedicine(editMed.id, form); } else { await window.api.createMedicine(form); } loadAll(); setShowMedForm(false); setEditMed(null); }; const handleDeleteMed = async (item) => { await window.api.deleteMedicine(item.id); loadAll(); }; // Recipes const handleSaveRecipe = async (form) => { let data; if (editRecipe) { data = await window.api.updateRecipe(editRecipe.id, form); } else { data = await window.api.createRecipe(form); } if (data.error) return { error: data.error }; loadAll(); setShowRecipeForm(false); setEditRecipe(null); return {}; }; const handleDeleteRecipe = async (recipe) => { await window.api.deleteRecipe(recipe.id); loadAll(); setConfirmDeleteRecipe(null); }; // Buildings const handleDeleteBuilding = async (b) => { await window.api.deleteBuilding(b.id); loadAll(); }; if (!admin) { return ; } return (
{tab === 'dashboard' && } {tab === 'items' && ( { setEditItem(null); setShowItemForm(true); }} onEdit={(item) => { setEditItem(item); setShowItemForm(true); }} onDelete={handleDeleteItem} addLabel="物品" /> )} {tab === 'recipes' && (

合成配方管理

管理所有合成配方,支持添加、编辑、删除

{recipes.length === 0 && ( )} {recipes.map((r, i) => ( ))}
ID 产物(中) 产物(英) 分类 等级 数量 材料 操作
暂无配方数据
{r.id} {r.result_zh} {r.result_en} {r.category} {r.craftsman_level} {r.result_count} {r.materials ? (typeof r.materials === 'string' ? r.materials : JSON.stringify(r.materials)) : '—'}
共 {recipes.length} 条配方
)} {tab === 'medicines' && ( (r.special_effects?.length ? r.special_effects.join(', ') : '—') }, ]} data={medicines} onAdd={() => { setEditMed(null); setShowMedForm(true); }} onEdit={(m) => { setEditMed(m); setShowMedForm(true); }} onDelete={handleDeleteMed} addLabel="药品" /> )} {tab === 'buildings' && ( ( {JSON.stringify(r.weapon_damage)} )}, ]} data={buildings} onAdd={() => alert('建筑数据含复杂JSON,建议直接操作数据库或后续完善表单')} onEdit={() => alert('建筑数据含复杂JSON,建议直接操作数据库或后续完善表单')} onDelete={handleDeleteBuilding} addLabel="建筑" /> )} {tab === 'players' && } {tab === 'password' && } {tab === 'data' && }
{/* 物品编辑弹窗 */} {showItemForm && ( { setShowItemForm(false); setEditItem(null); }} /> )} {/* 药品编辑弹窗 */} {showMedForm && ( { setShowMedForm(false); setEditMed(null); }} /> )} {/* 配方编辑弹窗 */} {showRecipeForm && ( { setShowRecipeForm(false); setEditRecipe(null); }} /> )} {/* 配方删除确认 */} {confirmDeleteRecipe && ( handleDeleteRecipe(confirmDeleteRecipe)} onCancel={() => setConfirmDeleteRecipe(null)} /> )}
); } window.AdminPage = AdminPage;