create-fullstack-scaffold 0.1.1 → 0.2.0

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.
Files changed (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -0,0 +1,297 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Card, Table, Tag, Button, Space, message, Modal, Input, Popconfirm, Tooltip } from 'antd'
3
+ import {
4
+ Package,
5
+ Clock,
6
+ Download,
7
+ Users,
8
+ CheckCircle,
9
+ XCircle,
10
+ Star,
11
+ TrendingUp,
12
+ } from 'lucide-react'
13
+ import type { ColumnsType } from 'antd/es/table'
14
+ import { apiClient, api } from '../services/apiClient'
15
+ import type { AdminDashboardStats, AdminPlugin } from '@shared/modules/plugins'
16
+
17
+ const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
18
+ pending: { color: 'orange', label: '待审核' },
19
+ approved: { color: 'green', label: '已通过' },
20
+ rejected: { color: 'red', label: '已拒绝' },
21
+ }
22
+
23
+ export const PluginDashboardPage: React.FC = () => {
24
+ const [stats, setStats] = useState<AdminDashboardStats | null>(null)
25
+ const [recentSubmissions, setRecentSubmissions] = useState<AdminPlugin[]>([])
26
+ const [loading, setLoading] = useState(true)
27
+ const [rejectModalVisible, setRejectModalVisible] = useState(false)
28
+ const [rejectTarget, setRejectTarget] = useState<string | null>(null)
29
+ const [rejectReason, setRejectReason] = useState('')
30
+
31
+ const fetchDashboard = useCallback(async () => {
32
+ setLoading(true)
33
+ try {
34
+ const data = await api(apiClient.api.stats.dashboard.$get()).withLoading('加载中...').json()
35
+ setStats(data)
36
+ setRecentSubmissions(data.recentSubmissions || [])
37
+ } catch {
38
+ // 错误已由 api-request 自动处理
39
+ } finally {
40
+ setLoading(false)
41
+ }
42
+ }, [])
43
+
44
+ useEffect(() => {
45
+ fetchDashboard()
46
+ }, [fetchDashboard])
47
+
48
+ const handleApprove = async (slug: string) => {
49
+ try {
50
+ await api(apiClient.api.plugins[':slug'].approve.$put({ param: { slug } }))
51
+ .withLoading('审核通过中...')
52
+ .json()
53
+ message.success('插件已通过审核')
54
+ fetchDashboard()
55
+ } catch {
56
+ // 错误已由 api-request 自动处理
57
+ }
58
+ }
59
+
60
+ const handleReject = async () => {
61
+ if (!rejectTarget || !rejectReason.trim()) {
62
+ message.warning('请填写拒绝原因')
63
+ return
64
+ }
65
+ try {
66
+ await api(
67
+ apiClient.api.plugins[':slug'].reject.$put({
68
+ param: { slug: rejectTarget },
69
+ json: { reason: rejectReason },
70
+ })
71
+ )
72
+ .withLoading('拒绝中...')
73
+ .json()
74
+ message.success('插件已拒绝')
75
+ setRejectModalVisible(false)
76
+ setRejectTarget(null)
77
+ setRejectReason('')
78
+ fetchDashboard()
79
+ } catch {
80
+ // 错误已由 api-request 自动处理
81
+ }
82
+ }
83
+
84
+ const openRejectModal = (slug: string) => {
85
+ setRejectTarget(slug)
86
+ setRejectReason('')
87
+ setRejectModalVisible(true)
88
+ }
89
+
90
+ if (loading && !stats) {
91
+ return (
92
+ <div className="flex items-center justify-center h-64">
93
+ <div className="text-gray-500">Loading...</div>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ const statCards = [
99
+ {
100
+ title: '总插件数',
101
+ value: stats?.totalPlugins ?? 0,
102
+ icon: Package,
103
+ color: 'bg-blue-500',
104
+ },
105
+ {
106
+ title: '待审核',
107
+ value: stats?.pendingPlugins ?? 0,
108
+ icon: Clock,
109
+ color: 'bg-orange-500',
110
+ },
111
+ {
112
+ title: '总下载量',
113
+ value: stats?.totalDownloads ?? 0,
114
+ icon: Download,
115
+ color: 'bg-green-500',
116
+ },
117
+ {
118
+ title: '开发者数',
119
+ value: stats?.totalDevelopers ?? 0,
120
+ icon: Users,
121
+ color: 'bg-purple-500',
122
+ },
123
+ ]
124
+
125
+ const columns: ColumnsType<AdminPlugin> = [
126
+ {
127
+ title: '插件名称',
128
+ dataIndex: 'name',
129
+ key: 'name',
130
+ render: (name: string, record: AdminPlugin) => (
131
+ <div>
132
+ <div className="font-medium">{name}</div>
133
+ <div className="text-xs text-gray-400">{record.slug}</div>
134
+ </div>
135
+ ),
136
+ },
137
+ {
138
+ title: '作者',
139
+ dataIndex: 'authorName',
140
+ key: 'authorName',
141
+ },
142
+ {
143
+ title: '状态',
144
+ dataIndex: 'status',
145
+ key: 'status',
146
+ render: (status: string) => {
147
+ const cfg = STATUS_CONFIG[status]
148
+ return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : <Tag>{status}</Tag>
149
+ },
150
+ },
151
+ {
152
+ title: '下载',
153
+ dataIndex: 'downloadCount',
154
+ key: 'downloadCount',
155
+ sorter: (a: AdminPlugin, b: AdminPlugin) => a.downloadCount - b.downloadCount,
156
+ render: (count: number) => (
157
+ <span className="flex items-center gap-1">
158
+ <Download className="w-3 h-3" /> {count}
159
+ </span>
160
+ ),
161
+ },
162
+ {
163
+ title: '评分',
164
+ key: 'rating',
165
+ render: (_: unknown, record: AdminPlugin) =>
166
+ record.avgRating != null ? (
167
+ <span className="flex items-center gap-1">
168
+ <Star className="w-3 h-3 text-yellow-500" /> {record.avgRating.toFixed(1)}
169
+ </span>
170
+ ) : (
171
+ '-'
172
+ ),
173
+ },
174
+ {
175
+ title: '提交时间',
176
+ dataIndex: 'createdAt',
177
+ key: 'createdAt',
178
+ render: (ts: number) => new Date(ts).toLocaleString('zh-CN'),
179
+ },
180
+ {
181
+ title: '操作',
182
+ key: 'actions',
183
+ render: (_: unknown, record: AdminPlugin) => (
184
+ <Space>
185
+ {record.status === 'pending' && (
186
+ <>
187
+ <Popconfirm
188
+ title="确定通过此插件审核?"
189
+ onConfirm={() => handleApprove(record.slug)}
190
+ okText="通过"
191
+ cancelText="取消"
192
+ >
193
+ <Button type="link" size="small" icon={<CheckCircle className="w-4 h-4" />}>
194
+ 通过
195
+ </Button>
196
+ </Popconfirm>
197
+ <Button
198
+ type="link"
199
+ size="small"
200
+ danger
201
+ icon={<XCircle className="w-4 h-4" />}
202
+ onClick={() => openRejectModal(record.slug)}
203
+ >
204
+ 拒绝
205
+ </Button>
206
+ </>
207
+ )}
208
+ {record.status === 'approved' && (
209
+ <Tag color="green" icon={<CheckCircle className="w-3 h-3" />}>
210
+ 已通过
211
+ </Tag>
212
+ )}
213
+ {record.status === 'rejected' && (
214
+ <Tooltip title={record.rejectReason}>
215
+ <Tag color="red">已拒绝</Tag>
216
+ </Tooltip>
217
+ )}
218
+ </Space>
219
+ ),
220
+ },
221
+ ]
222
+
223
+ return (
224
+ <div>
225
+ <div className="flex justify-between items-center mb-6">
226
+ <div>
227
+ <h1 className="text-2xl font-bold text-gray-900">插件市场</h1>
228
+ <p className="text-gray-600 mt-1">插件管理与审核概览</p>
229
+ </div>
230
+ </div>
231
+
232
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
233
+ {statCards.map((card, idx) => {
234
+ const Icon = card.icon
235
+ return (
236
+ <Card key={idx} className="shadow-sm">
237
+ <div className="flex items-center justify-between">
238
+ <div>
239
+ <div className="text-sm text-gray-500">{card.title}</div>
240
+ <div className="text-2xl font-bold mt-1">
241
+ {typeof card.value === 'number' ? card.value.toLocaleString() : card.value}
242
+ </div>
243
+ </div>
244
+ <div className={`p-3 rounded-lg ${card.color}`}>
245
+ <Icon className="w-6 h-6 text-white" />
246
+ </div>
247
+ </div>
248
+ </Card>
249
+ )
250
+ })}
251
+ </div>
252
+
253
+ <Card
254
+ title={
255
+ <span className="flex items-center gap-2">
256
+ <TrendingUp className="w-4 h-4" /> 最近提交
257
+ </span>
258
+ }
259
+ className="shadow-sm"
260
+ >
261
+ <Table
262
+ columns={columns}
263
+ dataSource={recentSubmissions}
264
+ rowKey="id"
265
+ pagination={{ pageSize: 5 }}
266
+ size="middle"
267
+ />
268
+ </Card>
269
+
270
+ <Modal
271
+ title="拒绝插件"
272
+ open={rejectModalVisible}
273
+ onOk={handleReject}
274
+ onCancel={() => {
275
+ setRejectModalVisible(false)
276
+ setRejectTarget(null)
277
+ setRejectReason('')
278
+ }}
279
+ okText="确认拒绝"
280
+ cancelText="取消"
281
+ okButtonProps={{ danger: true }}
282
+ >
283
+ <div className="mb-4">
284
+ <p className="text-sm text-gray-500 mb-2">请填写拒绝原因:</p>
285
+ <Input.TextArea
286
+ rows={4}
287
+ value={rejectReason}
288
+ onChange={e => setRejectReason(e.target.value)}
289
+ placeholder="请输入拒绝原因..."
290
+ maxLength={500}
291
+ showCount
292
+ />
293
+ </div>
294
+ </Modal>
295
+ </div>
296
+ )
297
+ }
@@ -0,0 +1,340 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Table, Card, Tag, Button, Space, Select, Input, Modal, Popconfirm, message } from 'antd'
3
+ import { CheckCircle, XCircle, Star, Download, Trash2, Search, RefreshCw } from 'lucide-react'
4
+ import type { ColumnsType } from 'antd/es/table'
5
+ import { apiClient, api } from '../services/apiClient'
6
+ import type { Plugin } from '@shared/modules/plugins'
7
+
8
+ type PluginStatus = 'pending' | 'approved' | 'rejected'
9
+
10
+ const STATUS_VALUES = ['pending', 'approved', 'rejected'] as const
11
+
12
+ const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
13
+ pending: { color: 'orange', label: '待审核' },
14
+ approved: { color: 'green', label: '已通过' },
15
+ rejected: { color: 'red', label: '已拒绝' },
16
+ }
17
+
18
+ const STATUS_OPTIONS = [
19
+ { value: '', label: '全部状态' },
20
+ { value: 'pending', label: '待审核' },
21
+ { value: 'approved', label: '已通过' },
22
+ { value: 'rejected', label: '已拒绝' },
23
+ ]
24
+
25
+ export const PluginManagementPage: React.FC = () => {
26
+ const [plugins, setPlugins] = useState<Plugin[]>([])
27
+ const [total, setTotal] = useState(0)
28
+ const [page, setPage] = useState(1)
29
+ const [loading, setLoading] = useState(false)
30
+ const [search, setSearch] = useState('')
31
+ const [statusFilter, setStatusFilter] = useState<string>('')
32
+ const [rejectModalVisible, setRejectModalVisible] = useState(false)
33
+ const [rejectTarget, setRejectTarget] = useState<string | null>(null)
34
+ const [rejectReason, setRejectReason] = useState('')
35
+
36
+ const fetchPlugins = useCallback(async () => {
37
+ setLoading(true)
38
+ try {
39
+ const query: Record<string, unknown> = { page, limit: 20 }
40
+ if (statusFilter && STATUS_VALUES.includes(statusFilter as PluginStatus)) {
41
+ query.status = statusFilter
42
+ }
43
+ const result = await api(apiClient.api.plugins.$get({ query })).withLoading().json()
44
+ setPlugins(result.plugins)
45
+ setTotal(result.total)
46
+ } catch {
47
+ // 错误已由 api-request 自动处理
48
+ } finally {
49
+ setLoading(false)
50
+ }
51
+ }, [page, statusFilter])
52
+
53
+ useEffect(() => {
54
+ fetchPlugins()
55
+ }, [fetchPlugins])
56
+
57
+ const handleSearch = () => {
58
+ const term = search.toLowerCase().trim()
59
+ if (!term) {
60
+ fetchPlugins()
61
+ return
62
+ }
63
+ setPlugins(prev =>
64
+ prev.filter(
65
+ p =>
66
+ p.name.toLowerCase().includes(term) ||
67
+ p.authorName.toLowerCase().includes(term) ||
68
+ p.slug.toLowerCase().includes(term)
69
+ )
70
+ )
71
+ }
72
+
73
+ const handleApprove = async (slug: string) => {
74
+ try {
75
+ await api(apiClient.api.plugins[':slug'].approve.$put({ param: { slug } }))
76
+ .withLoading('审核通过中...')
77
+ .json()
78
+ message.success('插件已通过审核')
79
+ fetchPlugins()
80
+ } catch {
81
+ // 错误已由 api-request 自动处理
82
+ }
83
+ }
84
+
85
+ const openRejectModal = (slug: string) => {
86
+ setRejectTarget(slug)
87
+ setRejectReason('')
88
+ setRejectModalVisible(true)
89
+ }
90
+
91
+ const handleReject = async () => {
92
+ if (!rejectTarget || !rejectReason.trim()) {
93
+ message.warning('请填写拒绝原因')
94
+ return
95
+ }
96
+ try {
97
+ await api(
98
+ apiClient.api.plugins[':slug'].reject.$put({
99
+ param: { slug: rejectTarget },
100
+ json: { reason: rejectReason },
101
+ })
102
+ )
103
+ .withLoading('拒绝中...')
104
+ .json()
105
+ message.success('插件已拒绝')
106
+ setRejectModalVisible(false)
107
+ setRejectTarget(null)
108
+ setRejectReason('')
109
+ fetchPlugins()
110
+ } catch {
111
+ // 错误已由 api-request 自动处理
112
+ }
113
+ }
114
+
115
+ const handleToggleFeatured = async (slug: string, current: boolean) => {
116
+ try {
117
+ await api(apiClient.api.plugins[':slug'].feature.$put({ param: { slug } }))
118
+ .withLoading(current ? '取消推荐中...' : '设为推荐中...')
119
+ .json()
120
+ message.success(current ? '已取消推荐' : '已设为推荐')
121
+ fetchPlugins()
122
+ } catch {
123
+ // 错误已由 api-request 自动处理
124
+ }
125
+ }
126
+
127
+ const handleRemove = async (slug: string) => {
128
+ try {
129
+ await api(apiClient.api.plugins[':slug'].$delete({ param: { slug } }))
130
+ .withLoading('删除中...')
131
+ .json()
132
+ message.success('插件已删除')
133
+ fetchPlugins()
134
+ } catch {
135
+ // 错误已由 api-request 自动处理
136
+ }
137
+ }
138
+
139
+ const columns: ColumnsType<Plugin> = [
140
+ {
141
+ title: '插件名称',
142
+ dataIndex: 'name',
143
+ key: 'name',
144
+ render: (name: string, record: Plugin) => (
145
+ <div>
146
+ <div className="font-medium">{name}</div>
147
+ <div className="text-xs text-gray-400">{record.slug}</div>
148
+ </div>
149
+ ),
150
+ },
151
+ {
152
+ title: '作者',
153
+ dataIndex: 'authorName',
154
+ key: 'authorName',
155
+ },
156
+ {
157
+ title: '状态',
158
+ dataIndex: 'status',
159
+ key: 'status',
160
+ render: (status: string) => {
161
+ const cfg = STATUS_CONFIG[status]
162
+ return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : <Tag>{status}</Tag>
163
+ },
164
+ },
165
+ {
166
+ title: '下载量',
167
+ dataIndex: 'downloadCount',
168
+ key: 'downloadCount',
169
+ sorter: (a: Plugin, b: Plugin) => a.downloadCount - b.downloadCount,
170
+ render: (count: number) => (
171
+ <span className="flex items-center gap-1">
172
+ <Download className="w-3 h-3" /> {count.toLocaleString()}
173
+ </span>
174
+ ),
175
+ },
176
+ {
177
+ title: '推荐',
178
+ dataIndex: 'featured',
179
+ key: 'featured',
180
+ render: (featured: boolean) =>
181
+ featured ? (
182
+ <Tag color="gold" icon={<Star className="w-3 h-3 inline" />}>
183
+ 推荐
184
+ </Tag>
185
+ ) : (
186
+ <Tag>普通</Tag>
187
+ ),
188
+ filters: [
189
+ { text: '推荐', value: true },
190
+ { text: '普通', value: false },
191
+ ],
192
+ onFilter: (value, record) => record.featured === value,
193
+ },
194
+ {
195
+ title: '版本',
196
+ dataIndex: 'version',
197
+ key: 'version',
198
+ render: (v: string) => <code className="text-xs">{v}</code>,
199
+ },
200
+ {
201
+ title: '更新时间',
202
+ dataIndex: 'updatedAt',
203
+ key: 'updatedAt',
204
+ render: (ts: number) => new Date(ts).toLocaleString('zh-CN'),
205
+ sorter: (a: Plugin, b: Plugin) => a.updatedAt - b.updatedAt,
206
+ defaultSortOrder: 'descend',
207
+ },
208
+ {
209
+ title: '操作',
210
+ key: 'actions',
211
+ width: 280,
212
+ render: (_: unknown, record: Plugin) => (
213
+ <Space size="small" wrap>
214
+ {record.status === 'pending' && (
215
+ <>
216
+ <Popconfirm
217
+ title="确定通过此插件审核?"
218
+ onConfirm={() => handleApprove(record.slug)}
219
+ okText="通过"
220
+ cancelText="取消"
221
+ >
222
+ <Button type="link" size="small" className="text-green-600">
223
+ <CheckCircle className="w-3.5 h-3.5 mr-1 inline" />
224
+ 通过
225
+ </Button>
226
+ </Popconfirm>
227
+ <Button type="link" size="small" danger onClick={() => openRejectModal(record.slug)}>
228
+ <XCircle className="w-3.5 h-3.5 mr-1 inline" />
229
+ 拒绝
230
+ </Button>
231
+ </>
232
+ )}
233
+ <Button
234
+ type="link"
235
+ size="small"
236
+ onClick={() => handleToggleFeatured(record.slug, record.featured)}
237
+ >
238
+ <Star className="w-3.5 h-3.5 mr-1 inline" />
239
+ {record.featured ? '取消推荐' : '推荐'}
240
+ </Button>
241
+ <Popconfirm
242
+ title="确定要删除此插件吗?此操作不可恢复"
243
+ onConfirm={() => handleRemove(record.slug)}
244
+ okText="删除"
245
+ cancelText="取消"
246
+ okButtonProps={{ danger: true }}
247
+ >
248
+ <Button type="link" size="small" danger>
249
+ <Trash2 className="w-3.5 h-3.5 mr-1 inline" />
250
+ 删除
251
+ </Button>
252
+ </Popconfirm>
253
+ </Space>
254
+ ),
255
+ },
256
+ ]
257
+
258
+ return (
259
+ <div>
260
+ <div className="flex justify-between items-center mb-6">
261
+ <div>
262
+ <h1 className="text-2xl font-bold text-gray-900">插件管理</h1>
263
+ <p className="text-gray-600 mt-1">审核、管理和配置所有插件</p>
264
+ </div>
265
+ </div>
266
+
267
+ <Card className="shadow-sm">
268
+ <div className="flex items-center gap-4 mb-4">
269
+ <Input.Search
270
+ placeholder="搜索插件名称、作者、Slug..."
271
+ value={search}
272
+ onChange={e => setSearch(e.target.value)}
273
+ onSearch={handleSearch}
274
+ style={{ width: 320 }}
275
+ allowClear
276
+ enterButton={
277
+ <span className="flex items-center gap-1">
278
+ <Search className="w-4 h-4" /> 搜索
279
+ </span>
280
+ }
281
+ />
282
+ <Select
283
+ placeholder="筛选状态"
284
+ style={{ width: 140 }}
285
+ value={statusFilter || undefined}
286
+ onChange={(val: string) => {
287
+ setStatusFilter(val || '')
288
+ setPage(1)
289
+ }}
290
+ options={STATUS_OPTIONS}
291
+ allowClear
292
+ />
293
+ <Button icon={<RefreshCw className="w-4 h-4" />} onClick={fetchPlugins}>
294
+ 刷新
295
+ </Button>
296
+ </div>
297
+
298
+ <Table
299
+ columns={columns}
300
+ dataSource={plugins}
301
+ rowKey="id"
302
+ loading={loading}
303
+ pagination={{
304
+ current: page,
305
+ pageSize: 20,
306
+ total,
307
+ showTotal: t => `共 ${t} 个插件`,
308
+ onChange: p => setPage(p),
309
+ }}
310
+ />
311
+ </Card>
312
+
313
+ <Modal
314
+ title="拒绝插件"
315
+ open={rejectModalVisible}
316
+ onOk={handleReject}
317
+ onCancel={() => {
318
+ setRejectModalVisible(false)
319
+ setRejectTarget(null)
320
+ setRejectReason('')
321
+ }}
322
+ okText="确认拒绝"
323
+ cancelText="取消"
324
+ okButtonProps={{ danger: true }}
325
+ >
326
+ <div className="mb-4">
327
+ <p className="text-sm text-gray-500 mb-2">请填写拒绝原因:</p>
328
+ <Input.TextArea
329
+ rows={4}
330
+ value={rejectReason}
331
+ onChange={e => setRejectReason(e.target.value)}
332
+ placeholder="请输入拒绝原因..."
333
+ maxLength={500}
334
+ showCount
335
+ />
336
+ </div>
337
+ </Modal>
338
+ </div>
339
+ )
340
+ }