Files
scriptforge/frontend/src/pages/Market.tsx
zhilv 5414c9c865 feat: 分类/变体体系 + 用户认证 + 管理后台
- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体
- 用户注册/登录(JWT + bcrypt),首个注册用户为管理员
- 管理后台 /admin 动态管理分类和变体
- 脚本市场支持按分类筛选
- CodeMirror 语言模式根据分类名称自动切换
- 结果页展示该分类下所有变体的运行命令
- source 命令变体用于 Shell 类继承环境变量

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:02:20 +08:00

128 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
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 [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,
category_id: categoryId || undefined,
search: search || undefined,
})
setItems(res.items)
setTotal(res.total)
} catch {
setItems([])
} finally {
setLoading(false)
}
}, [page, categoryId, search])
useEffect(() => { fetchMarket() }, [fetchMarket])
const totalPages = Math.ceil(total / 20)
return (
<div className="space-y-6">
<div className="text-center">
<h1 className="text-2xl font-bold mb-1"></h1>
<p className="text-gray-400 text-sm"></p>
</div>
{/* Search + Filter */}
<div className="flex gap-4">
<input
type="text"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
placeholder="搜索脚本..."
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={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={0}></option>
{categories.map((cat) => (
<option key={cat.ID} value={cat.ID}>{cat.Icon} {cat.Label}</option>
))}
</select>
</div>
{/* Results */}
{loading ? (
<div className="text-center py-10 text-gray-500 animate-pulse">...</div>
) : items.length === 0 ? (
<div className="text-center py-10">
<p className="text-gray-500"></p>
<Link to="/create" className="text-blue-400 hover:underline text-sm mt-2 block"></Link>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{items.map((item) => (
<Link
key={item.id}
to={`/s/${item.id}`}
className="bg-gray-800/60 border border-gray-700 rounded-lg p-4 hover:border-blue-500/50 transition-colors group"
>
<div className="flex items-center gap-2 mb-2">
<span className="font-medium text-sm group-hover:text-blue-400 transition-colors">
{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.category_icon} {item.category_label}
</span>
</div>
{item.description && (
<p className="text-gray-400 text-xs line-clamp-2">{item.description}</p>
)}
<div className="mt-2 text-xs text-gray-500 font-mono">
{item.id}
</div>
</Link>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center gap-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-900 disabled:text-gray-600 rounded text-sm"
>
</button>
<span className="text-sm text-gray-400 py-1">
{page} / {totalPages} ({total} )
</span>
<button
onClick={() => setPage(Math.min(totalPages, page + 1))}
disabled={page === totalPages}
className="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-900 disabled:text-gray-600 rounded text-sm"
>
</button>
</div>
)}
</div>
)
}