feat: 分类/变体体系 + 用户认证 + 管理后台
- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体 - 用户注册/登录(JWT + bcrypt),首个注册用户为管理员 - 管理后台 /admin 动态管理分类和变体 - 脚本市场支持按分类筛选 - CodeMirror 语言模式根据分类名称自动切换 - 结果页展示该分类下所有变体的运行命令 - source 命令变体用于 Shell 类继承环境变量 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
247
frontend/src/pages/Admin.tsx
Normal file
247
frontend/src/pages/Admin.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { listCategories, getMe, logout, adminCreateCategory, adminUpdateCategory, adminDeleteCategory, adminCreateVariant, adminUpdateVariant, adminDeleteVariant } from '../lib/api'
|
||||
import { RuntimeCategory, RuntimeVariant } from '../types'
|
||||
|
||||
export default function Admin() {
|
||||
const [categories, setCategories] = useState<RuntimeCategory[]>([])
|
||||
const [user, setUser] = useState<{ id: number; username: string; role: string } | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Category form
|
||||
const [editingCat, setEditingCat] = useState<RuntimeCategory | null>(null)
|
||||
const [catName, setCatName] = useState('')
|
||||
const [catLabel, setCatLabel] = useState('')
|
||||
const [catIcon, setCatIcon] = useState('')
|
||||
const [catOrder, setCatOrder] = useState(0)
|
||||
|
||||
// Variant form
|
||||
const [selectedCatId, setSelectedCatId] = useState<number>(0)
|
||||
const [editingVariant, setEditingVariant] = useState<RuntimeVariant | null>(null)
|
||||
const [vName, setVName] = useState('')
|
||||
const [vLabel, setVLabel] = useState('')
|
||||
const [vExt, setVExt] = useState('')
|
||||
const [vMime, setVMime] = useState('')
|
||||
const [vCmd, setVCmd] = useState('')
|
||||
const [vSrcCmd, setVSrcCmd] = useState('')
|
||||
const [vDefault, setVDefault] = useState(false)
|
||||
const [vOrder, setVOrder] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getMe().catch(() => null),
|
||||
listCategories().catch(() => [] as RuntimeCategory[]),
|
||||
]).then(([u, cats]) => {
|
||||
setUser(u)
|
||||
setCategories(cats)
|
||||
setLoading(false)
|
||||
if (!u || u.role !== 'admin') setError('无权访问管理后台')
|
||||
})
|
||||
}, [])
|
||||
|
||||
const refresh = () => listCategories().then(setCategories)
|
||||
|
||||
const resetCatForm = () => {
|
||||
setEditingCat(null)
|
||||
setCatName('')
|
||||
setCatLabel('')
|
||||
setCatIcon('')
|
||||
setCatOrder(0)
|
||||
}
|
||||
|
||||
const editCat = (cat: RuntimeCategory) => {
|
||||
setEditingCat(cat)
|
||||
setCatName(cat.Name)
|
||||
setCatLabel(cat.Label)
|
||||
setCatIcon(cat.Icon)
|
||||
setCatOrder(cat.SortOrder)
|
||||
}
|
||||
|
||||
const saveCategory = async () => {
|
||||
try {
|
||||
if (editingCat) {
|
||||
await adminUpdateCategory(editingCat.ID, { Name: catName, Label: catLabel, Icon: catIcon, SortOrder: catOrder })
|
||||
} else {
|
||||
await adminCreateCategory({ Name: catName, Label: catLabel, Icon: catIcon, SortOrder: catOrder })
|
||||
}
|
||||
resetCatForm()
|
||||
await refresh()
|
||||
} catch (e) {
|
||||
alert('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCategory = async (id: number) => {
|
||||
if (!confirm('确定删除此分类?相关变体也会被删除。')) return
|
||||
try {
|
||||
await adminDeleteCategory(id)
|
||||
await refresh()
|
||||
} catch { alert('删除失败') }
|
||||
}
|
||||
|
||||
const resetVForm = () => {
|
||||
setEditingVariant(null)
|
||||
setVName('')
|
||||
setVLabel('')
|
||||
setVExt('')
|
||||
setVMime('')
|
||||
setVCmd('')
|
||||
setVSrcCmd('')
|
||||
setVDefault(false)
|
||||
setVOrder(0)
|
||||
}
|
||||
|
||||
const editVariant = (v: RuntimeVariant) => {
|
||||
setEditingVariant(v)
|
||||
setSelectedCatId(v.CategoryID)
|
||||
setVName(v.Name)
|
||||
setVLabel(v.Label)
|
||||
setVExt(v.Extension)
|
||||
setVMime(v.MIMEType)
|
||||
setVCmd(v.CommandTemplate)
|
||||
setVSrcCmd(v.SourceTemplate)
|
||||
setVDefault(v.IsDefault)
|
||||
setVOrder(v.SortOrder)
|
||||
}
|
||||
|
||||
const saveVariant = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
Name: vName,
|
||||
Label: vLabel,
|
||||
Extension: vExt,
|
||||
MIMEType: vMime,
|
||||
CommandTemplate: vCmd,
|
||||
SourceTemplate: vSrcCmd,
|
||||
IsDefault: vDefault,
|
||||
SortOrder: vOrder,
|
||||
}
|
||||
if (editingVariant) {
|
||||
await adminUpdateVariant(editingVariant.ID, payload)
|
||||
} else {
|
||||
await adminCreateVariant(selectedCatId, payload)
|
||||
}
|
||||
resetVForm()
|
||||
await refresh()
|
||||
} catch { alert('操作失败') }
|
||||
}
|
||||
|
||||
const deleteVariant = async (id: number) => {
|
||||
if (!confirm('确定删除此变体?')) return
|
||||
try {
|
||||
await adminDeleteVariant(id)
|
||||
await refresh()
|
||||
} catch { alert('删除失败') }
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-center py-20 text-gray-500 animate-pulse">加载中...</div>
|
||||
|
||||
if (error || !user || user.role !== 'admin') {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-6xl mb-4">🔒</div>
|
||||
<h2 className="text-xl font-bold mb-2">无权访问</h2>
|
||||
<p className="text-gray-400 mb-4">{error || '请使用管理员账号登录'}</p>
|
||||
<a href="/login" className="text-blue-400 hover:underline">登录</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const selectedCat = categories.find(c => c.ID === selectedCatId)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">管理后台</h1>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-gray-400">{user.username}</span>
|
||||
<button onClick={() => { logout(); window.location.href = '/' }} className="text-gray-500 hover:text-red-400 transition-colors">退出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Form */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4">
|
||||
<h2 className="text-sm font-medium mb-3">{editingCat ? '编辑分类' : '新增分类'}</h2>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<input value={catName} onChange={e => setCatName(e.target.value)} placeholder="名称 (shell)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={catLabel} onChange={e => setCatLabel(e.target.value)} placeholder="标签 (Shell)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={catIcon} onChange={e => setCatIcon(e.target.value)} placeholder="图标 (🐚)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={catOrder} onChange={e => setCatOrder(Number(e.target.value))} placeholder="排序" type="number" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button onClick={saveCategory} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-xs">{editingCat ? '更新' : '创建'}</button>
|
||||
{editingCat && <button onClick={resetCatForm} className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs">取消</button>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories List */}
|
||||
{categories.map(cat => (
|
||||
<div key={cat.ID} className="bg-gray-800/30 border border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{cat.Icon}</span>
|
||||
<span className="font-medium">{cat.Label}</span>
|
||||
<span className="text-xs text-gray-500 font-mono">({cat.Name})</span>
|
||||
<span className="text-xs text-gray-600">排序: {cat.SortOrder}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => editCat(cat)} className="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-xs">编辑</button>
|
||||
<button onClick={() => deleteCategory(cat.ID)} className="px-2 py-1 bg-red-700 hover:bg-red-600 rounded text-xs">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variants */}
|
||||
<div className="ml-4 space-y-2">
|
||||
{cat.Variants?.map(v => (
|
||||
<div key={v.ID} className="flex items-center justify-between bg-gray-900/50 rounded px-3 py-2">
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<span className="font-mono text-blue-400">{v.Name}</span>
|
||||
<span className="text-gray-400">{v.Label}</span>
|
||||
<span className="text-gray-600">.{v.Extension}</span>
|
||||
{v.IsDefault && <span className="text-green-500">默认</span>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => { setSelectedCatId(cat.ID); editVariant(v) }} className="px-2 py-0.5 bg-gray-700 hover:bg-gray-600 rounded text-xs">编辑</button>
|
||||
<button onClick={() => deleteVariant(v.ID)} className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-xs">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => { resetVForm(); setSelectedCatId(cat.ID) }} className="text-xs text-blue-400 hover:underline">+ 添加变体</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Variant Form */}
|
||||
{(selectedCatId > 0 || editingVariant) && (
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4">
|
||||
<h2 className="text-sm font-medium mb-3">
|
||||
{editingVariant ? '编辑变体' : `为 ${selectedCat?.Label || selectedCatId} 添加变体`}
|
||||
</h2>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<input value={vName} onChange={e => setVName(e.target.value)} placeholder="名称 (bash)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={vLabel} onChange={e => setVLabel(e.target.value)} placeholder="标签 (Bash)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={vExt} onChange={e => setVExt(e.target.value)} placeholder="扩展名 (.sh)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={vMime} onChange={e => setVMime(e.target.value)} placeholder="MIME" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||
<input value={vCmd} onChange={e => setVCmd(e.target.value)} placeholder="命令模板 (curl {url} | bash)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
<input value={vSrcCmd} onChange={e => setVSrcCmd(e.target.value)} placeholder="source模板 (可选)" className="px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6 mt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="number" value={vOrder} onChange={e => setVOrder(Number(e.target.value))} placeholder="排序" className="w-20 px-3 py-2 bg-gray-800 border border-gray-700 rounded text-xs" />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<input type="checkbox" checked={vDefault} onChange={e => setVDefault(e.target.checked)} className="w-3 h-3" />
|
||||
默认变体
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button onClick={saveVariant} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 rounded text-xs">{editingVariant ? '更新' : '创建'}</button>
|
||||
<button onClick={resetVForm} className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user