- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体 - 用户注册/登录(JWT + bcrypt),首个注册用户为管理员 - 管理后台 /admin 动态管理分类和变体 - 脚本市场支持按分类筛选 - CodeMirror 语言模式根据分类名称自动切换 - 结果页展示该分类下所有变体的运行命令 - source 命令变体用于 Shell 类继承环境变量 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
247 lines
11 KiB
TypeScript
247 lines
11 KiB
TypeScript
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>
|
|
)
|
|
} |