@zhin.js/console 1.0.59 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/client/src/layouts/dashboard.tsx +1 -1
- package/client/src/main.tsx +24 -6
- package/client/src/pages/cron.tsx +529 -0
- package/client/src/pages/dashboard.tsx +60 -1
- package/client/src/pages/marketplace.tsx +464 -0
- package/dist/assets/index-BMkFI-uN.js +124 -0
- package/dist/assets/style-CtySe6_R.css +3 -0
- package/dist/client.js +10 -2
- package/dist/index.html +2 -2
- package/dist/style.css +1 -1
- package/lib/index.js +146 -6
- package/lib/websocket.js +146 -6
- package/package.json +7 -6
- package/src/bot-hub.ts +18 -4
- package/src/websocket.ts +151 -2
- package/dist/assets/index-B1ihXBk4.js +0 -124
- package/dist/assets/style-kkLO-vsa.css +0 -3
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import { useNavigate } from 'react-router'
|
|
3
|
-
import { Bot, AlertCircle, Activity, Package, Clock, Cpu, MemoryStick, FileText, TrendingUp } from 'lucide-react'
|
|
3
|
+
import { Bot, AlertCircle, Activity, Package, Clock, Cpu, MemoryStick, FileText, TrendingUp, RotateCw } from 'lucide-react'
|
|
4
4
|
import { apiFetch } from '../utils/auth'
|
|
5
|
+
import { useWebSocket } from '@zhin.js/client'
|
|
5
6
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../components/ui/card'
|
|
6
7
|
import { Badge } from '../components/ui/badge'
|
|
7
8
|
import { Button } from '../components/ui/button'
|
|
8
9
|
import { Alert, AlertDescription } from '../components/ui/alert'
|
|
9
10
|
import { Skeleton } from '../components/ui/skeleton'
|
|
11
|
+
import {
|
|
12
|
+
Dialog, DialogContent, DialogHeader, DialogFooter,
|
|
13
|
+
DialogTitle, DialogDescription, DialogClose,
|
|
14
|
+
} from '../components/ui/dialog'
|
|
10
15
|
|
|
11
16
|
interface Stats {
|
|
12
17
|
plugins: { total: number; active: number }
|
|
@@ -31,6 +36,9 @@ export default function HomePage() {
|
|
|
31
36
|
const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null)
|
|
32
37
|
const [loading, setLoading] = useState(true)
|
|
33
38
|
const [error, setError] = useState<string | null>(null)
|
|
39
|
+
const [restartDialogOpen, setRestartDialogOpen] = useState(false)
|
|
40
|
+
const [restarting, setRestarting] = useState(false)
|
|
41
|
+
const { sendRequest } = useWebSocket()
|
|
34
42
|
|
|
35
43
|
useEffect(() => {
|
|
36
44
|
fetchData()
|
|
@@ -68,6 +76,19 @@ export default function HomePage() {
|
|
|
68
76
|
|
|
69
77
|
const formatMemory = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
|
70
78
|
|
|
79
|
+
const handleRestart = async () => {
|
|
80
|
+
setRestarting(true)
|
|
81
|
+
try {
|
|
82
|
+
await sendRequest({ type: 'system:restart' })
|
|
83
|
+
} catch {
|
|
84
|
+
// 连接断开是预期行为(进程重启中)
|
|
85
|
+
}
|
|
86
|
+
// 显示重启中状态,然后自动刷新页面
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
window.location.reload()
|
|
89
|
+
}, 3000)
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
if (loading) {
|
|
72
93
|
return (
|
|
73
94
|
<div className="space-y-6">
|
|
@@ -223,9 +244,47 @@ export default function HomePage() {
|
|
|
223
244
|
<span className="text-xs text-muted-foreground">查看运行日志</span>
|
|
224
245
|
</div>
|
|
225
246
|
</Button>
|
|
247
|
+
<Button
|
|
248
|
+
variant="outline"
|
|
249
|
+
className="h-auto p-4 justify-start border-orange-200 hover:border-orange-400 hover:bg-orange-50 dark:border-orange-800 dark:hover:border-orange-600 dark:hover:bg-orange-950"
|
|
250
|
+
onClick={() => setRestartDialogOpen(true)}
|
|
251
|
+
disabled={restarting}
|
|
252
|
+
>
|
|
253
|
+
<div className="flex flex-col items-start gap-1">
|
|
254
|
+
<RotateCw className={`h-5 w-5 mb-1 text-orange-500 ${restarting ? 'animate-spin' : ''}`} />
|
|
255
|
+
<span className="font-medium">{restarting ? '重启中...' : '重启服务'}</span>
|
|
256
|
+
<span className="text-xs text-muted-foreground">{restarting ? '请稍候,页面将自动刷新' : '重启机器人进程'}</span>
|
|
257
|
+
</div>
|
|
258
|
+
</Button>
|
|
226
259
|
</div>
|
|
227
260
|
</CardContent>
|
|
228
261
|
</Card>
|
|
262
|
+
|
|
263
|
+
{/* Restart confirmation dialog */}
|
|
264
|
+
<Dialog open={restartDialogOpen} onOpenChange={setRestartDialogOpen}>
|
|
265
|
+
<DialogContent>
|
|
266
|
+
<DialogHeader>
|
|
267
|
+
<DialogTitle>确认重启</DialogTitle>
|
|
268
|
+
<DialogDescription>
|
|
269
|
+
确定要重启机器人进程吗?所有连接将临时断开,进行中的对话会被中断。进程将在几秒内自动恢复。
|
|
270
|
+
</DialogDescription>
|
|
271
|
+
</DialogHeader>
|
|
272
|
+
<DialogFooter>
|
|
273
|
+
<DialogClose asChild>
|
|
274
|
+
<Button variant="outline">取消</Button>
|
|
275
|
+
</DialogClose>
|
|
276
|
+
<Button
|
|
277
|
+
variant="destructive"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
setRestartDialogOpen(false)
|
|
280
|
+
handleRestart()
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
确认重启
|
|
284
|
+
</Button>
|
|
285
|
+
</DialogFooter>
|
|
286
|
+
</DialogContent>
|
|
287
|
+
</Dialog>
|
|
229
288
|
</div>
|
|
230
289
|
)
|
|
231
290
|
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Search, Package, Download, ExternalLink, AlertCircle,
|
|
4
|
+
ArrowUpDown, RefreshCw, ShieldCheck, Globe,
|
|
5
|
+
type LucideIcon,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { Card, CardContent } from '../components/ui/card'
|
|
8
|
+
import { Badge } from '../components/ui/badge'
|
|
9
|
+
import { Button } from '../components/ui/button'
|
|
10
|
+
import { Input } from '../components/ui/input'
|
|
11
|
+
import { Alert, AlertDescription } from '../components/ui/alert'
|
|
12
|
+
import { Skeleton } from '../components/ui/skeleton'
|
|
13
|
+
import { Separator } from '../components/ui/separator'
|
|
14
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
|
|
15
|
+
import {
|
|
16
|
+
Dialog, DialogContent, DialogHeader, DialogFooter,
|
|
17
|
+
DialogTitle, DialogDescription, DialogClose,
|
|
18
|
+
} from '../components/ui/dialog'
|
|
19
|
+
|
|
20
|
+
interface MarketPlugin {
|
|
21
|
+
name: string
|
|
22
|
+
version: string
|
|
23
|
+
description: string
|
|
24
|
+
author: string
|
|
25
|
+
isOfficial: boolean
|
|
26
|
+
keywords: string[]
|
|
27
|
+
npm: string
|
|
28
|
+
date: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PluginDetail {
|
|
32
|
+
name: string
|
|
33
|
+
version: string
|
|
34
|
+
description: string
|
|
35
|
+
license: string
|
|
36
|
+
author: string
|
|
37
|
+
homepage: string
|
|
38
|
+
repository: string
|
|
39
|
+
keywords: string[]
|
|
40
|
+
engines: Record<string, string>
|
|
41
|
+
peerDependencies: Record<string, string>
|
|
42
|
+
downloads: { weekly: number; monthly: number }
|
|
43
|
+
readme: string
|
|
44
|
+
versions: string[]
|
|
45
|
+
lastPublish: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface UpdateInfo {
|
|
49
|
+
name: string
|
|
50
|
+
current: string
|
|
51
|
+
latest: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type SortKey = 'name' | 'date'
|
|
55
|
+
type Category = '' | 'adapter' | 'service' | 'util' | 'game' | 'feature'
|
|
56
|
+
|
|
57
|
+
const CATEGORIES: { value: Category; label: string; icon: LucideIcon }[] = [
|
|
58
|
+
{ value: '', label: '全部', icon: Package },
|
|
59
|
+
{ value: 'adapter', label: '适配器', icon: Globe },
|
|
60
|
+
{ value: 'service', label: '服务', icon: ShieldCheck },
|
|
61
|
+
{ value: 'util', label: '工具', icon: Package },
|
|
62
|
+
{ value: 'game', label: '游戏', icon: Package },
|
|
63
|
+
{ value: 'feature', label: '特性', icon: Package },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
function formatDate(dateStr: string) {
|
|
67
|
+
if (!dateStr) return ''
|
|
68
|
+
return new Date(dateStr).toLocaleDateString('zh-CN')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatDownloads(n: number): string {
|
|
72
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`
|
|
73
|
+
return String(n)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function MarketplacePage() {
|
|
77
|
+
const [plugins, setPlugins] = useState<MarketPlugin[]>([])
|
|
78
|
+
const [loading, setLoading] = useState(true)
|
|
79
|
+
const [error, setError] = useState<string | null>(null)
|
|
80
|
+
const [search, setSearch] = useState('')
|
|
81
|
+
const [category, setCategory] = useState<Category>('')
|
|
82
|
+
const [officialOnly, setOfficialOnly] = useState(false)
|
|
83
|
+
const [sortKey, setSortKey] = useState<SortKey>('name')
|
|
84
|
+
const [updates, setUpdates] = useState<UpdateInfo[]>([])
|
|
85
|
+
const [updatesLoading, setUpdatesLoading] = useState(false)
|
|
86
|
+
|
|
87
|
+
// Detail dialog
|
|
88
|
+
const [detailOpen, setDetailOpen] = useState(false)
|
|
89
|
+
const [detailLoading, setDetailLoading] = useState(false)
|
|
90
|
+
const [detail, setDetail] = useState<PluginDetail | null>(null)
|
|
91
|
+
|
|
92
|
+
const fetchPlugins = useCallback(async () => {
|
|
93
|
+
setLoading(true)
|
|
94
|
+
setError(null)
|
|
95
|
+
try {
|
|
96
|
+
const params = new URLSearchParams()
|
|
97
|
+
if (search) params.set('q', search)
|
|
98
|
+
if (category) params.set('category', category)
|
|
99
|
+
if (officialOnly) params.set('official', 'true')
|
|
100
|
+
params.set('limit', '50')
|
|
101
|
+
|
|
102
|
+
const res = await fetch(`/pub/marketplace/search?${params}`)
|
|
103
|
+
if (!res.ok) throw new Error('搜索失败')
|
|
104
|
+
const data = await res.json()
|
|
105
|
+
if (data.success) {
|
|
106
|
+
setPlugins(data.data)
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(data.error || '数据格式错误')
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError((err as Error).message)
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false)
|
|
114
|
+
}
|
|
115
|
+
}, [search, category, officialOnly])
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
fetchPlugins()
|
|
119
|
+
}, [fetchPlugins])
|
|
120
|
+
|
|
121
|
+
const checkUpdates = useCallback(async () => {
|
|
122
|
+
setUpdatesLoading(true)
|
|
123
|
+
try {
|
|
124
|
+
const token = localStorage.getItem('zhin_api_token')
|
|
125
|
+
const res = await fetch('/api/marketplace/updates', {
|
|
126
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
127
|
+
})
|
|
128
|
+
if (res.ok) {
|
|
129
|
+
const data = await res.json()
|
|
130
|
+
if (data.success) setUpdates(data.data)
|
|
131
|
+
}
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
finally { setUpdatesLoading(false) }
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
const openDetail = useCallback(async (name: string) => {
|
|
137
|
+
setDetailOpen(true)
|
|
138
|
+
setDetailLoading(true)
|
|
139
|
+
setDetail(null)
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(`/pub/marketplace/detail/${encodeURIComponent(name)}`)
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
const data = await res.json()
|
|
144
|
+
if (data.success) setDetail(data.data)
|
|
145
|
+
}
|
|
146
|
+
} catch { /* ignore */ }
|
|
147
|
+
finally { setDetailLoading(false) }
|
|
148
|
+
}, [])
|
|
149
|
+
|
|
150
|
+
const sorted = useMemo(() => {
|
|
151
|
+
const arr = [...plugins]
|
|
152
|
+
if (sortKey === 'date') {
|
|
153
|
+
arr.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
154
|
+
} else {
|
|
155
|
+
arr.sort((a, b) => a.name.localeCompare(b.name))
|
|
156
|
+
}
|
|
157
|
+
return arr
|
|
158
|
+
}, [plugins, sortKey])
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="space-y-4">
|
|
162
|
+
{/* Header */}
|
|
163
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
164
|
+
<div>
|
|
165
|
+
<h1 className="text-2xl font-bold tracking-tight">插件市场</h1>
|
|
166
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
167
|
+
探索 Zhin.js 生态中的 {plugins.length} 个插件
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex gap-2">
|
|
171
|
+
<Button
|
|
172
|
+
variant="outline"
|
|
173
|
+
size="sm"
|
|
174
|
+
onClick={checkUpdates}
|
|
175
|
+
disabled={updatesLoading}
|
|
176
|
+
>
|
|
177
|
+
<RefreshCw className={`w-4 h-4 mr-1 ${updatesLoading ? 'animate-spin' : ''}`} />
|
|
178
|
+
检查更新
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Updates banner */}
|
|
184
|
+
{updates.length > 0 && (
|
|
185
|
+
<Alert>
|
|
186
|
+
<RefreshCw className="h-4 w-4" />
|
|
187
|
+
<AlertDescription>
|
|
188
|
+
有 {updates.length} 个插件可更新:
|
|
189
|
+
{updates.map(u => (
|
|
190
|
+
<Badge key={u.name} variant="secondary" className="ml-1">
|
|
191
|
+
{u.name} {u.current} → {u.latest}
|
|
192
|
+
</Badge>
|
|
193
|
+
))}
|
|
194
|
+
</AlertDescription>
|
|
195
|
+
</Alert>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{/* Search & Filters */}
|
|
199
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
200
|
+
<div className="relative flex-1">
|
|
201
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
202
|
+
<Input
|
|
203
|
+
placeholder="搜索插件名称、描述..."
|
|
204
|
+
value={search}
|
|
205
|
+
onChange={e => setSearch(e.target.value)}
|
|
206
|
+
className="pl-9"
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="flex gap-2 flex-wrap">
|
|
210
|
+
<Button
|
|
211
|
+
variant={officialOnly ? 'default' : 'outline'}
|
|
212
|
+
size="sm"
|
|
213
|
+
onClick={() => setOfficialOnly(!officialOnly)}
|
|
214
|
+
>
|
|
215
|
+
<ShieldCheck className="w-4 h-4 mr-1" />
|
|
216
|
+
仅官方
|
|
217
|
+
</Button>
|
|
218
|
+
<Button
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
onClick={() => setSortKey(sortKey === 'name' ? 'date' : 'name')}
|
|
222
|
+
>
|
|
223
|
+
<ArrowUpDown className="w-4 h-4 mr-1" />
|
|
224
|
+
{sortKey === 'name' ? '名称' : '更新时间'}
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Category Tabs */}
|
|
230
|
+
<Tabs value={category} onValueChange={v => setCategory(v as Category)}>
|
|
231
|
+
<TabsList>
|
|
232
|
+
{CATEGORIES.map(c => (
|
|
233
|
+
<TabsTrigger key={c.value} value={c.value}>
|
|
234
|
+
{c.label}
|
|
235
|
+
</TabsTrigger>
|
|
236
|
+
))}
|
|
237
|
+
</TabsList>
|
|
238
|
+
</Tabs>
|
|
239
|
+
|
|
240
|
+
{/* Error */}
|
|
241
|
+
{error && (
|
|
242
|
+
<Alert variant="destructive">
|
|
243
|
+
<AlertCircle className="h-4 w-4" />
|
|
244
|
+
<AlertDescription>{error}</AlertDescription>
|
|
245
|
+
</Alert>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Loading */}
|
|
249
|
+
{loading && (
|
|
250
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
251
|
+
{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-40" />)}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{/* Plugin Grid */}
|
|
256
|
+
{!loading && (
|
|
257
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
258
|
+
{sorted.map(plugin => (
|
|
259
|
+
<Card
|
|
260
|
+
key={plugin.name}
|
|
261
|
+
className="cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5"
|
|
262
|
+
onClick={() => openDetail(plugin.name)}
|
|
263
|
+
>
|
|
264
|
+
<CardContent className="p-4 space-y-3">
|
|
265
|
+
{/* Name & Badge */}
|
|
266
|
+
<div className="flex justify-between items-start">
|
|
267
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
268
|
+
<Package className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
269
|
+
<span className="font-semibold text-sm truncate">{plugin.name}</span>
|
|
270
|
+
</div>
|
|
271
|
+
<div className="flex gap-1 shrink-0">
|
|
272
|
+
{plugin.isOfficial && (
|
|
273
|
+
<Badge variant="default" className="text-[10px]">官方</Badge>
|
|
274
|
+
)}
|
|
275
|
+
<Badge variant="secondary" className="text-[10px]">v{plugin.version}</Badge>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
280
|
+
{plugin.description || '暂无描述'}
|
|
281
|
+
</p>
|
|
282
|
+
|
|
283
|
+
<Separator />
|
|
284
|
+
|
|
285
|
+
{/* Footer */}
|
|
286
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
287
|
+
<span>{plugin.author}</span>
|
|
288
|
+
<span>{formatDate(plugin.date)}</span>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Keywords */}
|
|
292
|
+
{plugin.keywords.length > 0 && (
|
|
293
|
+
<div className="flex flex-wrap gap-1">
|
|
294
|
+
{plugin.keywords.slice(0, 4).map(kw => (
|
|
295
|
+
<Badge key={kw} variant="outline" className="text-[10px] px-1.5 py-0">
|
|
296
|
+
{kw}
|
|
297
|
+
</Badge>
|
|
298
|
+
))}
|
|
299
|
+
{plugin.keywords.length > 4 && (
|
|
300
|
+
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
|
301
|
+
+{plugin.keywords.length - 4}
|
|
302
|
+
</Badge>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</CardContent>
|
|
307
|
+
</Card>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{/* Empty */}
|
|
313
|
+
{!loading && sorted.length === 0 && (
|
|
314
|
+
<Card>
|
|
315
|
+
<CardContent className="flex flex-col items-center gap-3 py-12">
|
|
316
|
+
<Package className="w-12 h-12 text-muted-foreground" />
|
|
317
|
+
<h3 className="text-lg font-semibold">未找到插件</h3>
|
|
318
|
+
<p className="text-sm text-muted-foreground">尝试调整搜索条件或分类筛选</p>
|
|
319
|
+
</CardContent>
|
|
320
|
+
</Card>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{/* Detail Dialog */}
|
|
324
|
+
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|
325
|
+
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
|
|
326
|
+
{detailLoading ? (
|
|
327
|
+
<div className="space-y-3">
|
|
328
|
+
<Skeleton className="h-6 w-48" />
|
|
329
|
+
<Skeleton className="h-4 w-full" />
|
|
330
|
+
<Skeleton className="h-20 w-full" />
|
|
331
|
+
</div>
|
|
332
|
+
) : detail ? (
|
|
333
|
+
<>
|
|
334
|
+
<DialogHeader>
|
|
335
|
+
<DialogTitle className="flex items-center gap-2">
|
|
336
|
+
<Package className="w-5 h-5" />
|
|
337
|
+
{detail.name}
|
|
338
|
+
<Badge variant="secondary">v{detail.version}</Badge>
|
|
339
|
+
</DialogTitle>
|
|
340
|
+
<DialogDescription>{detail.description}</DialogDescription>
|
|
341
|
+
</DialogHeader>
|
|
342
|
+
|
|
343
|
+
<div className="space-y-4">
|
|
344
|
+
{/* Stats */}
|
|
345
|
+
<div className="grid grid-cols-3 gap-2 text-center">
|
|
346
|
+
<div className="rounded-md bg-secondary p-2">
|
|
347
|
+
<div className="text-lg font-bold">{formatDownloads(detail.downloads.weekly)}</div>
|
|
348
|
+
<div className="text-[10px] text-muted-foreground">周下载</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div className="rounded-md bg-secondary p-2">
|
|
351
|
+
<div className="text-lg font-bold">{formatDownloads(detail.downloads.monthly)}</div>
|
|
352
|
+
<div className="text-[10px] text-muted-foreground">月下载</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="rounded-md bg-secondary p-2">
|
|
355
|
+
<div className="text-lg font-bold">{detail.versions.length}</div>
|
|
356
|
+
<div className="text-[10px] text-muted-foreground">版本数</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<Separator />
|
|
361
|
+
|
|
362
|
+
{/* Metadata */}
|
|
363
|
+
<div className="space-y-2 text-sm">
|
|
364
|
+
{detail.author && (
|
|
365
|
+
<div className="flex justify-between">
|
|
366
|
+
<span className="text-muted-foreground">作者</span>
|
|
367
|
+
<span>{detail.author}</span>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
{detail.license && (
|
|
371
|
+
<div className="flex justify-between">
|
|
372
|
+
<span className="text-muted-foreground">许可证</span>
|
|
373
|
+
<span>{detail.license}</span>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
{detail.lastPublish && (
|
|
377
|
+
<div className="flex justify-between">
|
|
378
|
+
<span className="text-muted-foreground">最后发布</span>
|
|
379
|
+
<span>{formatDate(detail.lastPublish)}</span>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
{detail.engines && Object.keys(detail.engines).length > 0 && (
|
|
383
|
+
<div className="flex justify-between">
|
|
384
|
+
<span className="text-muted-foreground">Node.js</span>
|
|
385
|
+
<span>{detail.engines.node || '-'}</span>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Peer Dependencies */}
|
|
391
|
+
{detail.peerDependencies && Object.keys(detail.peerDependencies).length > 0 && (
|
|
392
|
+
<>
|
|
393
|
+
<Separator />
|
|
394
|
+
<div>
|
|
395
|
+
<h4 className="text-sm font-medium mb-2">对等依赖</h4>
|
|
396
|
+
<div className="flex flex-wrap gap-1">
|
|
397
|
+
{Object.entries(detail.peerDependencies).map(([name, ver]) => (
|
|
398
|
+
<Badge key={name} variant="outline" className="text-xs">
|
|
399
|
+
{name}@{ver}
|
|
400
|
+
</Badge>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* README excerpt */}
|
|
408
|
+
{detail.readme && (
|
|
409
|
+
<>
|
|
410
|
+
<Separator />
|
|
411
|
+
<div>
|
|
412
|
+
<h4 className="text-sm font-medium mb-2">README</h4>
|
|
413
|
+
<p className="text-xs text-muted-foreground whitespace-pre-line leading-relaxed">
|
|
414
|
+
{detail.readme}
|
|
415
|
+
</p>
|
|
416
|
+
</div>
|
|
417
|
+
</>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{/* Install command */}
|
|
421
|
+
<Separator />
|
|
422
|
+
<div>
|
|
423
|
+
<h4 className="text-sm font-medium mb-2">安装命令</h4>
|
|
424
|
+
<code className="block text-xs bg-secondary rounded-md p-2">
|
|
425
|
+
pnpm add {detail.name}
|
|
426
|
+
</code>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<DialogFooter className="gap-2">
|
|
431
|
+
{detail.homepage && (
|
|
432
|
+
<Button variant="outline" size="sm" asChild>
|
|
433
|
+
<a href={detail.homepage} target="_blank" rel="noopener noreferrer">
|
|
434
|
+
<ExternalLink className="w-3 h-3 mr-1" /> 主页
|
|
435
|
+
</a>
|
|
436
|
+
</Button>
|
|
437
|
+
)}
|
|
438
|
+
{detail.npm || (
|
|
439
|
+
<Button variant="outline" size="sm" asChild>
|
|
440
|
+
<a
|
|
441
|
+
href={`https://www.npmjs.com/package/${detail.name}`}
|
|
442
|
+
target="_blank"
|
|
443
|
+
rel="noopener noreferrer"
|
|
444
|
+
>
|
|
445
|
+
<Download className="w-3 h-3 mr-1" /> npm
|
|
446
|
+
</a>
|
|
447
|
+
</Button>
|
|
448
|
+
)}
|
|
449
|
+
<DialogClose asChild>
|
|
450
|
+
<Button variant="secondary" size="sm">关闭</Button>
|
|
451
|
+
</DialogClose>
|
|
452
|
+
</DialogFooter>
|
|
453
|
+
</>
|
|
454
|
+
) : (
|
|
455
|
+
<Alert variant="destructive">
|
|
456
|
+
<AlertCircle className="h-4 w-4" />
|
|
457
|
+
<AlertDescription>加载插件详情失败</AlertDescription>
|
|
458
|
+
</Alert>
|
|
459
|
+
)}
|
|
460
|
+
</DialogContent>
|
|
461
|
+
</Dialog>
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
}
|