feat: 添加信息员角色,支持管理员/信息员/用户三种权限
This commit is contained in:
parent
0e7b5cbfdf
commit
80effaaa29
|
|
@ -0,0 +1,359 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>甲辰管理后台</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@1/dist/axios.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body, #root { min-height: 100vh; width: 100%; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #1a1a2e; color: #fff; }
|
||||
input, button, select, table, textarea { font-family: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
// ============== 工具函数 ==============
|
||||
const formatDate = (date) => date ? new Date(date).replace('+08:00','+0800').toLocaleString('zh-CN') : '-';
|
||||
const levelColors = { '黄金': '#f59e0b', '铂金': '#94a3b8', '钻石': '#3b82f6', '青铜': '#10b981' };
|
||||
|
||||
// ============== 登录组件 ==============
|
||||
function Login({ onLogin }) {
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('admin123');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username || !password) { setError('请输入用户名和密码'); return; }
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const res = await axios.post('/api/auth/login', new URLSearchParams({ username, password }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||
if (res.data.access_token) {
|
||||
localStorage.setItem('adminToken', res.data.access_token);
|
||||
onLogin({ username: username });
|
||||
} else if (res.data.code === 0) {
|
||||
localStorage.setItem('adminToken', res.data.data.token);
|
||||
onLogin(res.data.data.user || { username: username });
|
||||
} else { setError(res.data.message || '登录失败'); }
|
||||
} catch (e) { setError('网络错误: ' + (e.message || '请检查后端服务')); }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return React.createElement('div', { style: { minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)' } },
|
||||
React.createElement('div', { style: { width: '360px', padding: '40px 30px', background: 'rgba(255,255,255,0.05)', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.1)' } },
|
||||
React.createElement('h1', { style: { textAlign: 'center', marginBottom: '30px', fontSize: '24px' } }, '甲辰管理后台'),
|
||||
React.createElement('input', { type: 'text', placeholder: '请输入管理员账号', value: username, onChange: e => setUsername(e.target.value), style: { width: '100%', padding: '14px', marginBottom: '16px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' } }),
|
||||
React.createElement('input', { type: 'password', placeholder: '请输入密码', value: password, onChange: e => setPassword(e.target.value), onKeyPress: e => e.key === 'Enter' && handleLogin(), style: { width: '100%', padding: '14px', marginBottom: '20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' } }),
|
||||
error && React.createElement('div', { style: { color: '#f87171', marginBottom: '16px', textAlign: 'center' } }, error),
|
||||
React.createElement('button', { onClick: handleLogin, disabled: loading, style: { width: '100%', padding: '14px', borderRadius: '8px', border: 'none', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: '#fff', fontSize: '16px', cursor: loading ? 'not-allowed' : 'pointer' } }, loading ? '登录中...' : '登录')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============== 用户编辑弹窗 ==============
|
||||
function UserEditModal({ user, onClose, onSave }) {
|
||||
const [form, setForm] = useState({
|
||||
username: user.username || '',
|
||||
email: user.email || '',
|
||||
role: user.role || 'user',
|
||||
user_code: user.user_code || '',
|
||||
level: user.level || '',
|
||||
points: user.points || 0,
|
||||
balance: user.balance || 0,
|
||||
totalAmount: user.totalAmount || 0,
|
||||
aiCount: user.aiCount || 0,
|
||||
searchCount: user.searchCount || 0,
|
||||
loginCount: user.loginCount || 0,
|
||||
phone: user.phone || ''
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const updateData = {
|
||||
username: form.username,
|
||||
email: form.email,
|
||||
role: form.role,
|
||||
user_code: form.user_code,
|
||||
level: form.level,
|
||||
points: form.points,
|
||||
balance: form.balance,
|
||||
totalAmount: form.totalAmount,
|
||||
aiCount: form.aiCount,
|
||||
searchCount: form.searchCount
|
||||
};
|
||||
await axios.put('/api/admin/users/' + user.id, updateData, { headers: { Authorization: 'Bearer ' + token } });
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (e) { alert('保存失败: ' + e.message); }
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const field = (label, key, type = 'text') => React.createElement('div', { style: { marginBottom: '14px' } },
|
||||
React.createElement('div', { style: { color: '#94a3b8', marginBottom: '6px', fontSize: '13px' } }, label),
|
||||
type === 'checkbox'
|
||||
? React.createElement('input', { type: 'checkbox', checked: form[key], onChange: e => setForm({...form, [key]: e.target.checked}), style: { width: '20px', height: '20px' } })
|
||||
: type === 'select'
|
||||
? React.createElement('select', { value: form[key], onChange: e => setForm({...form, [key]: e.target.value}), style: { width: '100%', padding: '10px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff' } },
|
||||
React.createElement('option', { value: 'user' }, '普通用户'),
|
||||
React.createElement('option', { value: 'admin' }, '管理员')
|
||||
)
|
||||
: React.createElement('input', { type: type, value: form[key], onChange: e => setForm({...form, [key]: type === 'number' ? (parseFloat(e.target.value) || 0) : e.target.value}), style: { width: '100%', padding: '10px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff' } })
|
||||
);
|
||||
|
||||
return React.createElement('div', { style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.85)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, overflow: 'auto', padding: '20px' } },
|
||||
React.createElement('div', { style: { width: '520px', background: '#1e293b', borderRadius: '16px', padding: '28px', boxShadow: '0 20px 60px rgba(0,0,0,0.5)' } },
|
||||
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' } },
|
||||
React.createElement('h2', { fontSize: '20px' }, '✏️ 编辑用户'),
|
||||
React.createElement('button', { onClick: onClose, style: { background: 'none', border: 'none', color: '#94a3b8', fontSize: '24px', cursor: 'pointer' } }, '×')
|
||||
),
|
||||
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' } },
|
||||
field('用户名', 'username'),
|
||||
field('用户编码', 'user_code'),
|
||||
field('邮箱', 'email'),
|
||||
field('手机号', 'phone'),
|
||||
field('角色', 'role', 'select'),
|
||||
field('会员等级', 'level'),
|
||||
field('账户余额', 'balance', 'number'),
|
||||
field('累计金额', 'totalAmount', 'number'),
|
||||
field('积分', 'points', 'number'),
|
||||
field('AI识别次数', 'aiCount', 'number'),
|
||||
field('寻号次数', 'searchCount', 'number'),
|
||||
field('登录次数', 'loginCount', 'number')
|
||||
),
|
||||
React.createElement('div', { style: { display: 'flex', gap: '12px', marginTop: '28px' } },
|
||||
React.createElement('button', { onClick: handleSave, disabled: saving, style: { flex: 1, padding: '14px', borderRadius: '8px', border: 'none', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', color: '#fff', fontSize: '15px', cursor: saving ? 'not-allowed' : 'pointer', opacity: saving ? 0.7 : 1 } }, saving ? '保存中...' : '保存修改'),
|
||||
React.createElement('button', { onClick: onClose, style: { flex: 1, padding: '14px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'transparent', color: '#fff', fontSize: '15px', cursor: 'pointer' } }, '取消')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============== 用户管理组件 ==============
|
||||
function UserManager() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
|
||||
useEffect(() => { loadUsers(); }, []);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await axios.get('/api/admin/users', { headers: { Authorization: 'Bearer ' + token } });
|
||||
if (res.data && res.data.length) setUsers(res.data);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (userId) => {
|
||||
if (!confirm('确定删除该用户吗?')) return;
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await axios.delete('/api/admin/users/' + userId, { headers: { Authorization: 'Bearer ' + token } });
|
||||
loadUsers();
|
||||
} catch (e) { alert('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const filtered = users.filter(u =>
|
||||
(u.username || '').includes(search) ||
|
||||
(u.phone || '').includes(search) ||
|
||||
(u.id || '').includes(search) ||
|
||||
(u.user_code || '').includes(search)
|
||||
);
|
||||
|
||||
return React.createElement('div', { style: { padding: '24px' } },
|
||||
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' } },
|
||||
React.createElement('h2', { fontSize: '20px' }, '👥 用户管理'),
|
||||
React.createElement('span', { color: '#94a3b8', fontSize: '14px' }, '共 ' + filtered.length + ' 用户')
|
||||
),
|
||||
React.createElement('div', { style: { position: 'relative', marginBottom: '20px' } },
|
||||
React.createElement('input', { type: 'text', placeholder: '🔍 搜索用户名、手机号、用户编码...', value: search, onChange: e => setSearch(e.target.value), style: { width: '100%', padding: '14px 16px 14px 44px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '14px' } }),
|
||||
React.createElement('span', { position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', fontSize: '16px' }, '🔍')
|
||||
),
|
||||
loading ? React.createElement('div', { style: { textAlign: 'center', padding: '60px', color: '#94a3b8' } }, '加载中...') :
|
||||
React.createElement('div', { style: { background: '#1e293b', borderRadius: '12px', overflow: 'hidden' } },
|
||||
React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } },
|
||||
React.createElement('thead', null,
|
||||
React.createElement('tr', { style: { background: 'rgba(0,0,0,0.3)' } },
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'left', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '用户'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'left', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '编码'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'left', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '联系方式'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'center', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '角色'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'center', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '等级'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'right', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '余额'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'right', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '积分'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'center', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '藏品'),
|
||||
React.createElement('th', { style: { padding: '14px 12px', textAlign: 'center', fontSize: '12px', color: '#94a3b8', fontWeight: 'normal' } }, '操作')
|
||||
)
|
||||
),
|
||||
React.createElement('tbody', null,
|
||||
filtered.map(u => React.createElement('tr', { key: u.id, style: { borderBottom: '1px solid rgba(255,255,255,0.05)' } },
|
||||
React.createElement('td', { style: { padding: '14px 12px' } },
|
||||
React.createElement('div', { fontWeight: '500' }, u.username || '-'),
|
||||
React.createElement('div', { fontSize: '11px', color: '#64748b' }, u.email || '-')
|
||||
),
|
||||
React.createElement('td', { style: { padding: '14px 12px', color: '#f59e0b', fontWeight: '500' } }, u.user_code || '-'),
|
||||
React.createElement('td', { style: { padding: '14px 12px', fontSize: '13px' } }, u.phone || '-'),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'center' } }, u.role === 'admin' ? React.createElement('span', { style: { padding: '4px 10px', borderRadius: '4px', background: 'rgba(245,158,11,0.2)', color: '#f59e0b', fontSize: '12px' } }, '管理员') : '-'),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'center' } }, u.level ? React.createElement('span', { style: { padding: '4px 10px', borderRadius: '4px', background: 'rgba(16,185,129,0.2)', color: levelColors[u.level] || '#10b981', fontSize: '12px' } }, u.level) : '-'),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'right', color: u.balance > 0 ? '#10b981' : '#fff' } }, '¥' + (u.balance || 0)),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'right' } }, u.points || 0),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'center' } }, u.collectionCount || 0),
|
||||
React.createElement('td', { style: { padding: '14px 12px', textAlign: 'center' } },
|
||||
React.createElement('button', { onClick: () => setEditingUser(u), style: { marginRight: '8px', padding: '6px 14px', borderRadius: '6px', border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: '12px' } }, '编辑'),
|
||||
React.createElement('button', { onClick: () => handleDelete(u.id), style: { padding: '6px 14px', borderRadius: '6px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', cursor: 'pointer', fontSize: '12px' } }, '删除')
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
editingUser && React.createElement(UserEditModal, { user: editingUser, onClose: () => setEditingUser(null), onSave: loadUsers })
|
||||
);
|
||||
}
|
||||
|
||||
// ============== 统计分析组件 ==============
|
||||
function StatsManager() {
|
||||
const [stats, setStats] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => { loadStats(); }, []);
|
||||
|
||||
const loadStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await axios.get('/api/admin/users/stats', { headers: { Authorization: 'Bearer ' + token } });
|
||||
if (res.data) setStats(res.data);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const statCards = [
|
||||
{ label: '用户总数', value: stats.totalUsers || 0, color: '#3b82f6', icon: '👥' },
|
||||
{ label: '藏品总数', value: stats.totalItems || 0, color: '#10b981', icon: '📚' },
|
||||
{ label: '账户总余额', value: '¥' + (stats.totalBalance || 0).toFixed(2), color: '#f59e0b', icon: '💰' },
|
||||
{ label: '会员人数', value: stats.totalMembers || 0, color: '#8b5cf6', icon: '⭐' }
|
||||
];
|
||||
|
||||
return React.createElement('div', { style: { padding: '24px' } },
|
||||
React.createElement('h2', { fontSize: '20px', marginBottom: '24px' }, '📊 统计分析概览'),
|
||||
loading ? React.createElement('div', { textAlign: 'center', padding: '60px', color: '#94a3b8' }, '加载中...') :
|
||||
React.createElement('div', { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '20px' },
|
||||
statCards.map((s, i) => React.createElement('div', { key: i, style: { padding: '24px', background: '#1e293b', borderRadius: '16px', border: '1px solid ' + s.color + '30' } },
|
||||
React.createElement('div', { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' },
|
||||
React.createElement('span', { fontSize: '28px' }, s.icon),
|
||||
React.createElement('span', { color: '#94a3b8', fontSize: '14px' }, s.label)
|
||||
),
|
||||
React.createElement('div', { fontSize: '36px', fontWeight: 'bold', color: s.color } }, s.value)
|
||||
))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============== 信息发布组件 ==============
|
||||
function InfoPublish() {
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [type, setType] = useState('notice');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!title || !content) { setMsg('请填写标题和内容'); return; }
|
||||
setSending(true); setMsg('');
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await axios.post('/api/information/publish', { title, content, info_type: type }, { headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' } });
|
||||
if (res.data && res.data.id) { setMsg('发布成功!'); setTitle(''); setContent(''); }
|
||||
else { setMsg(res.data?.message || '发布失败'); }
|
||||
} catch (e) { setMsg('发布失败: ' + e.message); }
|
||||
setSending(false);
|
||||
};
|
||||
|
||||
return React.createElement('div', { style: { padding: '24px' } },
|
||||
React.createElement('h2', { fontSize: '20px', marginBottom: '24px' }, '📢 发布信息'),
|
||||
React.createElement('div', { background: '#1e293b', borderRadius: '16px', padding: '24px' } },
|
||||
React.createElement('div', { marginBottom: '16px' },
|
||||
React.createElement('div', { color: '#94a3b8', marginBottom: '8px', fontSize: '14px' }, '信息类型'),
|
||||
React.createElement('select', { value: type, onChange: e => setType(e.target.value), style: { width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '14px' } },
|
||||
React.createElement('option', { value: 'notice' }, '📌 系统通知'),
|
||||
React.createElement('option', { value: 'deal' }, '💹 成交数据')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { marginBottom: '16px' },
|
||||
React.createElement('div', { color: '#94a3b8', marginBottom: '8px', fontSize: '14px' }, '标题'),
|
||||
React.createElement('input', { type: 'text', placeholder: '请输入标题', value: title, onChange: e => setTitle(e.target.value), style: { width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '14px' } })
|
||||
),
|
||||
React.createElement('div', { marginBottom: '20px' },
|
||||
React.createElement('div', { color: '#94a3b8', marginBottom: '8px', fontSize: '14px' }, '内容'),
|
||||
React.createElement('textarea', { placeholder: '请输入内容...', value: content, onChange: e => setContent(e.target.value), rows: 8, style: { width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '14px', resize: 'vertical' } })
|
||||
),
|
||||
msg && React.createElement('div', { color: msg.includes('成功') ? '#10b981' : '#f87171', marginBottom: '16px', padding: '12px', borderRadius: '8px', background: msg.includes('成功') ? 'rgba(16,185,129,0.1)' : 'rgba(248,113,113,0.1)' }, msg),
|
||||
React.createElement('button', { onClick: handlePublish, disabled: sending, style: { padding: '14px 32px', borderRadius: '8px', border: 'none', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', color: '#fff', fontSize: '15px', cursor: sending ? 'not-allowed' : 'pointer', opacity: sending ? 0.7 : 1 } }, sending ? '发布中...' : '🚀 发布')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============== 主应用 ==============
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userStr = localStorage.getItem('adminUser');
|
||||
if (token && userStr) {
|
||||
try { setUser(JSON.parse(userStr)); } catch (e) {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('adminToken');
|
||||
localStorage.removeItem('adminUser');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
if (!user) return React.createElement(Login, { onLogin: (u) => { localStorage.setItem('adminUser', JSON.stringify(u)); setUser(u); } });
|
||||
|
||||
const tabs = [
|
||||
{ id: 'users', label: '用户管理', icon: '👥' },
|
||||
{ id: 'stats', label: '统计分析', icon: '📊' },
|
||||
{ id: 'publish', label: '信息发布', icon: '📢' }
|
||||
];
|
||||
|
||||
return React.createElement('div', { style: { minHeight: '100vh', background: '#1a1a2e' } },
|
||||
React.createElement('div', { style: { padding: '20px 28px', background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)' } },
|
||||
React.createElement('h1', { fontSize: '22px', fontWeight: '600' } }, '🐉 甲辰管理后台'),
|
||||
React.createElement('div', { display: 'flex', alignItems: 'center', gap: '16px' },
|
||||
React.createElement('span', { color: '#94a3b8', fontSize: '14px' }, '👤 ' + (user.username || '管理员')),
|
||||
React.createElement('button', { onClick: handleLogout, style: { padding: '8px 16px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.2)', background: 'transparent', color: '#94a3b8', cursor: 'pointer', fontSize: '13px' } }, '退出')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { display: 'flex', borderBottom: '1px solid rgba(255,255,255,0.1)', padding: '0 28px' },
|
||||
tabs.map(tab => React.createElement('div', { key: tab.id, onClick: () => setActiveTab(tab.id), style: { padding: '16px 24px', cursor: 'pointer', borderBottom: activeTab === tab.id ? '3px solid #f59e0b' : '3px solid transparent', color: activeTab === tab.id ? '#fff' : '#94a3b8', fontSize: '14px', fontWeight: activeTab === tab.id ? '500' : 'normal' } }, tab.icon + ' ' + tab.label))
|
||||
),
|
||||
React.createElement('div', { minHeight: 'calc(100vh - 130px)' },
|
||||
activeTab === 'users' && React.createElement(UserManager),
|
||||
activeTab === 'stats' && React.createElement(StatsManager),
|
||||
activeTab === 'publish' && React.createElement(InfoPublish)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(App));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "admin-standalone",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
const API_BASE = ''
|
||||
|
||||
// 登录页面
|
||||
function Login({ onLogin }) {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username || !password) {
|
||||
setError('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE}/api/auth/login`,
|
||||
new URLSearchParams({ username, password }),
|
||||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||
)
|
||||
if (res.data.code === 0) {
|
||||
localStorage.setItem('adminToken', res.data.data.token)
|
||||
localStorage.setItem('adminUser', JSON.stringify(res.data.data.user))
|
||||
onLogin(res.data.data.user)
|
||||
} else {
|
||||
setError(res.data.message || '登录失败')
|
||||
}
|
||||
} catch (e) {
|
||||
setError('网络错误,请检查后端服务')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '360px',
|
||||
padding: '40px 30px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
<h1 style={{ textAlign: 'center', marginBottom: '30px', fontSize: '24px' }}>甲辰管理后台</h1>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入管理员账号"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 16px',
|
||||
marginBottom: '16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '15px'
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onKeyPress={e => e.key === 'Enter' && handleLogin()}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 16px',
|
||||
marginBottom: '20px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '15px'
|
||||
}}
|
||||
/>
|
||||
{error && <div style={{ color: '#f87171', marginBottom: '16px', textAlign: 'center' }}>{error}</div>}
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
const userStr = localStorage.getItem('adminUser')
|
||||
if (token && userStr) {
|
||||
try { setUser(JSON.parse(userStr)) } catch (e) {}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('adminToken')
|
||||
localStorage.removeItem('adminUser')
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
if (!user) return <Login onLogin={setUser} />
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#1a1a2e', color: '#fff', padding: '20px' }}>
|
||||
<h1>甲辰管理后台 - 登录成功</h1>
|
||||
<p>欢迎 {user.username || user.f01_01_name}</p>
|
||||
<button onClick={handleLogout} style={{ marginTop: '20px', padding: '10px 20px' }}>退出</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body, #root {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #fff;
|
||||
}
|
||||
input, textarea, select, button {
|
||||
font-family: inherit;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
host: '0.0.0.0'
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>甲辰管理后台</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@1/dist/axios.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body, #root { min-height: 100vh; width: 100%; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #1a1a2e; color: #fff; }
|
||||
input, button { font-family: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function Login({ onLogin }) {
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('admin123');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username || !password) { setError('请输入用户名和密码'); return; }
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const res = await axios.post('/api/auth/login', new URLSearchParams({ username, password }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||
if (res.data.access_token) {
|
||||
localStorage.setItem('adminToken', res.data.access_token);
|
||||
onLogin({ username: username });
|
||||
} else if (res.data.code === 0) {
|
||||
localStorage.setItem('adminToken', res.data.data.token);
|
||||
onLogin(res.data.data.user || { username: username });
|
||||
} else { setError(res.data.message || '登录失败'); }
|
||||
} catch (e) { setError('网络错误: ' + (e.message || '请检查后端服务')); }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return React.createElement('div', { style: { minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)' } },
|
||||
React.createElement('div', { style: { width: '360px', padding: '40px 30px', background: 'rgba(255,255,255,0.05)', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.1)' } },
|
||||
React.createElement('h1', { style: { textAlign: 'center', marginBottom: '30px', fontSize: '24px' } }, '甲辰管理后台'),
|
||||
React.createElement('input', { type: 'text', placeholder: '请输入管理员账号', value: username, onChange: e => setUsername(e.target.value), style: { width: '100%', padding: '14px', marginBottom: '16px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' } }),
|
||||
React.createElement('input', { type: 'password', placeholder: '请输入密码', value: password, onChange: e => setPassword(e.target.value), onKeyPress: e => e.key === 'Enter' && handleLogin(), style: { width: '100%', padding: '14px', marginBottom: '20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' } }),
|
||||
error && React.createElement('div', { style: { color: '#f87171', marginBottom: '16px', textAlign: 'center' } }, error),
|
||||
React.createElement('button', { onClick: handleLogin, disabled: loading, style: { width: '100%', padding: '14px', borderRadius: '8px', border: 'none', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: '#fff', fontSize: '16px', cursor: loading ? 'not-allowed' : 'pointer' } }, loading ? '登录中...' : '登录')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userStr = localStorage.getItem('adminUser');
|
||||
if (token && userStr) {
|
||||
try { setUser(JSON.parse(userStr)); } catch (e) {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('adminToken');
|
||||
localStorage.removeItem('adminUser');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
if (!user) return React.createElement(Login, { onLogin: (u) => { localStorage.setItem('adminUser', JSON.stringify(u)); setUser(u); } });
|
||||
|
||||
return React.createElement('div', { style: { minHeight: '100vh', background: '#1a1a2e', color: '#fff', padding: '20px' } },
|
||||
React.createElement('h1', null, '甲辰管理后台 - 登录成功'),
|
||||
React.createElement('p', null, '欢迎 ', user.username || '管理员'),
|
||||
React.createElement('button', { onClick: handleLogout, style: { marginTop: '20px', padding: '10px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'transparent', color: '#fff', cursor: 'pointer' } }, '退出')
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(App));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>甲辰管理后台</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@1/dist/axios.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body, #root { min-height: 100vh; width: 100%; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #1a1a2e; color: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function Login({ onLogin }) {
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('admin123');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username || !password) { setError('请输入用户名和密码'); return; }
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const res = await axios.post('/api/auth/login', new URLSearchParams({ username, password }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||
if (res.data.access_token) {
|
||||
localStorage.setItem('adminToken', res.data.access_token);
|
||||
onLogin({ username: username });
|
||||
} else if (res.data.code === 0) {
|
||||
localStorage.setItem('adminToken', res.data.data.token);
|
||||
onLogin(res.data.data.user || { username: username });
|
||||
} else { setError(res.data.message || '登录失败'); }
|
||||
} catch (e) { setError('网络错误: ' + e.message); }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return React.createElement('div', { style: { minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#1a1a2e' } },
|
||||
React.createElement('div', { style: { width: '320px', padding: '30px', background: '#16213e', borderRadius: '12px' } },
|
||||
React.createElement('h2', { style: { textAlign: 'center', marginBottom: '20px' } }, '甲辰管理后台'),
|
||||
React.createElement('input', { type: 'text', value: username, onChange: e => setUsername(e.target.value), placeholder: '账号', style: { width: '100%', padding: '12px', marginBottom: '12px', background: '#0f172a', border: '1px solid #334155', color: '#fff', borderRadius: '6px' } }),
|
||||
React.createElement('input', { type: 'password', value: password, onChange: e => setPassword(e.target.value), placeholder: '密码', style: { width: '100%', padding: '12px', marginBottom: '12px', background: '#0f172a', border: '1px solid #334155', color: '#fff', borderRadius: '6px' } }),
|
||||
error && React.createElement('div', { style: { color: '#f87171', marginBottom: '12px', fontSize: '14px' } }, error),
|
||||
React.createElement('button', { onClick: handleLogin, disabled: loading, style: { width: '100%', padding: '12px', background: '#f59e0b', border: 'none', borderRadius: '6px', color: '#000', cursor: 'pointer' } }, loading ? '登录中...' : '登录')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function UserManager() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => { loadUsers(); }, []);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await axios.get('/api/admin/users', { headers: { Authorization: 'Bearer ' + token } });
|
||||
if (res.data) setUsers(res.data);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const filtered = users.filter(u => (u.username || '').includes(search) || (u.phone || '').includes(search));
|
||||
|
||||
return React.createElement('div', { padding: '20px' },
|
||||
React.createElement('h2', { marginBottom: '16px' }, '用户列表'),
|
||||
React.createElement('input', { type: 'text', value: search, onChange: e => setSearch(e.target.value), placeholder: '搜索...', style: { width: '100%', padding: '10px', marginBottom: '16px', background: '#0f172a', border: '1px solid #334155', color: '#fff', borderRadius: '6px' } }),
|
||||
loading ? React.createElement('div', { textAlign: 'center', padding: '40px' }, '加载中...') :
|
||||
React.createElement('table', { width: '100%', borderCollapse: 'collapse' },
|
||||
React.createElement('thead', null,
|
||||
React.createElement('tr', null,
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'left' } }, '用户名'),
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'left' } }, '编码'),
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'left' } }, '手机'),
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'left' } }, '角色'),
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'right' } }, '余额'),
|
||||
React.createElement('th', { style: { padding: '10px', textAlign: 'right' } }, '积分')
|
||||
)
|
||||
),
|
||||
React.createElement('tbody', null,
|
||||
filtered.map(u => React.createElement('tr', { key: u.id, style: { borderBottom: '1px solid #334155' } },
|
||||
React.createElement('td', { style: { padding: '10px' } }, u.username || '-'),
|
||||
React.createElement('td', { style: { padding: '10px', color: '#f59e0b' } }, u.user_code || '-'),
|
||||
React.createElement('td', { style: { padding: '10px' } }, u.phone || '-'),
|
||||
React.createElement('td', { style: { padding: '10px' } }, u.role === 'admin' ? '管理员' : '用户'),
|
||||
React.createElement('td', { style: { padding: '10px', textAlign: 'right' } }, '¥' + (u.balance || 0)),
|
||||
React.createElement('td', { style: { padding: '10px', textAlign: 'right' } }, u.points || 0)
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userStr = localStorage.getItem('adminUser');
|
||||
if (token && userStr) {
|
||||
try { setUser(JSON.parse(userStr)); } catch (e) {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!user) return React.createElement(Login, { onLogin: (u) => { localStorage.setItem('adminUser', JSON.stringify(u)); setUser(u); } });
|
||||
|
||||
return React.createElement('div', { minHeight: '100vh', background: '#1a1a2e' },
|
||||
React.createElement('div', { padding: '16px', background: '#16213e', display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #334155' },
|
||||
React.createElement('h1', { fontSize: '18px' }, '管理后台'),
|
||||
React.createElement('button', { onClick: () => { localStorage.clear(); setUser(null); }, style: { background: 'transparent', border: '1px solid #334155', color: '#94a3b8', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer' } }, '退出')
|
||||
),
|
||||
React.createElement('div', { display: 'flex', borderBottom: '1px solid #334155' },
|
||||
React.createElement('div', { onClick: () => setActiveTab('users'), style: { padding: '12px 20px', cursor: 'pointer', borderBottom: activeTab === 'users' ? '2px solid #f59e0b' : '2px solid transparent', color: activeTab === 'users' ? '#fff' : '#94a3b8' } }, '用户'),
|
||||
React.createElement('div', { onClick: () => setActiveTab('stats'), style: { padding: '12px 20px', cursor: 'pointer', borderBottom: activeTab === 'stats' ? '2px solid #f59e0b' : '2px solid transparent', color: activeTab === 'stats' ? '#fff' : '#94a3b8' } }, '统计')
|
||||
),
|
||||
React.createElement('div', null,
|
||||
activeTab === 'users' && React.createElement(UserManager),
|
||||
activeTab === 'stats' && React.createElement('div', { padding: '20px', color: '#94a3b8' }, '统计功能开发中...')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(App));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -205,3 +205,19 @@ class InformationLike(Base):
|
|||
__table_args__ = (
|
||||
UniqueConstraint('information_id', 'user_id', name='uq_information_user'),
|
||||
)
|
||||
|
||||
|
||||
# 角色常量
|
||||
class UserRole:
|
||||
ADMIN = "admin" # 管理员:全部权限
|
||||
EDITOR = "editor" # 信息员:可发布信息、管理资讯
|
||||
USER = "user" # 普通用户:基本功能
|
||||
|
||||
@classmethod
|
||||
def get_role_name(cls, role):
|
||||
names = {
|
||||
cls.ADMIN: "管理员",
|
||||
cls.EDITOR: "信息员",
|
||||
cls.USER: "用户"
|
||||
}
|
||||
return names.get(role, "用户")
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ def get_users(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取用户列表(仅管理员)"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
|
||||
total = db.query(User).count()
|
||||
|
|
@ -125,7 +125,7 @@ def get_user(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取单个用户信息"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
|
@ -150,7 +150,7 @@ def get_user_collections(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取指定用户的藏品列表"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
|
||||
collections = db.query(Collection).filter(
|
||||
|
|
@ -167,7 +167,7 @@ def get_user_collection_count(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取指定用户的藏品数量"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
|
||||
|
||||
count = db.query(Collection).filter(Collection.user_id == user_id).count()
|
||||
|
|
@ -192,7 +192,7 @@ def update_user(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新用户信息(仅管理员)"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
|
||||
|
||||
user = db.query(User).filter(User.f99_90_id == user_id).first()
|
||||
|
|
@ -270,7 +270,7 @@ def delete_user(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除用户(仅管理员)"""
|
||||
if current_user.role != "admin":
|
||||
if current_user.role not in ["admin", "editor"]:
|
||||
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
|
||||
|
||||
# 不能删除自己
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>甲辰收藏 v0.0.0</title>
|
||||
<title>甲辰收藏 v1.2.14</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue