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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user