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:
2026-05-29 15:02:20 +08:00
parent 58a80cb196
commit 5414c9c865
24 changed files with 1309 additions and 295 deletions

View File

@@ -1,20 +1,30 @@
import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { listMarket } from '../lib/api'
import { MarketItem, RUNTIME_OPTIONS } from '../types'
import { listMarket, listCategories } from '../lib/api'
import { MarketItem, RuntimeCategory } from '../types'
export default function Market() {
const [items, setItems] = useState<MarketItem[]>([])
const [categories, setCategories] = useState<RuntimeCategory[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [runtime, setRuntime] = useState('')
const [categoryId, setCategoryId] = useState(0)
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
listCategories().then(setCategories).catch(() => {})
}, [])
const fetchMarket = useCallback(async () => {
setLoading(true)
try {
const res = await listMarket({ page, per_page: 20, runtime, search })
const res = await listMarket({
page,
per_page: 20,
category_id: categoryId || undefined,
search: search || undefined,
})
setItems(res.items)
setTotal(res.total)
} catch {
@@ -22,7 +32,7 @@ export default function Market() {
} finally {
setLoading(false)
}
}, [page, runtime, search])
}, [page, categoryId, search])
useEffect(() => { fetchMarket() }, [fetchMarket])
@@ -45,13 +55,13 @@ export default function Market() {
className="flex-1 px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-blue-500"
/>
<select
value={runtime}
onChange={(e) => { setRuntime(e.target.value); setPage(1) }}
value={categoryId}
onChange={(e) => { setCategoryId(Number(e.target.value)); setPage(1) }}
className="px-3 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-blue-500"
>
<option value=""></option>
{RUNTIME_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
<option value={0}></option>
{categories.map((cat) => (
<option key={cat.ID} value={cat.ID}>{cat.Icon} {cat.Label}</option>
))}
</select>
</div>
@@ -77,7 +87,7 @@ export default function Market() {
{item.title}
</span>
<span className="px-1.5 py-0.5 bg-blue-600/20 text-blue-400 text-xs rounded border border-blue-600/30">
{item.runtime}
{item.category_icon} {item.category_label}
</span>
</div>
{item.description && (