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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- package/template/package-lock.json +0 -14554
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
Button,
|
|
5
|
+
Space,
|
|
6
|
+
Tag,
|
|
7
|
+
Empty,
|
|
8
|
+
Spin,
|
|
9
|
+
Input,
|
|
10
|
+
Modal,
|
|
11
|
+
Descriptions,
|
|
12
|
+
message,
|
|
13
|
+
Popconfirm,
|
|
14
|
+
} from 'antd'
|
|
15
|
+
import { CheckCircle, XCircle, ExternalLink, ArrowRight } from 'lucide-react'
|
|
16
|
+
import { apiClient, api } from '../services/apiClient'
|
|
17
|
+
import type { Plugin } from '@shared/modules/plugins'
|
|
18
|
+
|
|
19
|
+
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
|
20
|
+
pending: { color: 'orange', label: '待审核' },
|
|
21
|
+
approved: { color: 'green', label: '已通过' },
|
|
22
|
+
rejected: { color: 'red', label: '已拒绝' },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PluginReviewPage: React.FC = () => {
|
|
26
|
+
const [pending, setPending] = useState<Plugin[]>([])
|
|
27
|
+
const [loading, setLoading] = useState(false)
|
|
28
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
29
|
+
const [rejectModalVisible, setRejectModalVisible] = useState(false)
|
|
30
|
+
const [rejectReason, setRejectReason] = useState('')
|
|
31
|
+
|
|
32
|
+
const fetchPending = useCallback(async () => {
|
|
33
|
+
setLoading(true)
|
|
34
|
+
try {
|
|
35
|
+
const result = await api(
|
|
36
|
+
apiClient.api.plugins.$get({ query: { status: 'pending', limit: 50, page: 1 } })
|
|
37
|
+
)
|
|
38
|
+
.withLoading()
|
|
39
|
+
.json()
|
|
40
|
+
setPending(result.plugins)
|
|
41
|
+
setCurrentIndex(0)
|
|
42
|
+
} catch {
|
|
43
|
+
// handled by api-request
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false)
|
|
46
|
+
}
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
fetchPending()
|
|
51
|
+
}, [fetchPending])
|
|
52
|
+
|
|
53
|
+
const current = pending[currentIndex]
|
|
54
|
+
|
|
55
|
+
const handleApprove = async () => {
|
|
56
|
+
if (!current) return
|
|
57
|
+
try {
|
|
58
|
+
await api(apiClient.api.plugins[':slug'].approve.$put({ param: { slug: current.slug } }))
|
|
59
|
+
.withLoading('审核通过中...')
|
|
60
|
+
.json()
|
|
61
|
+
message.success(`插件 "${current.name}" 已通过审核`)
|
|
62
|
+
setPending(prev => prev.filter(p => p.id !== current.id))
|
|
63
|
+
setCurrentIndex(prev => Math.min(prev, Math.max(0, pending.length - 2)))
|
|
64
|
+
} catch {
|
|
65
|
+
// handled
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const openRejectModal = () => {
|
|
70
|
+
setRejectReason('')
|
|
71
|
+
setRejectModalVisible(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handleReject = async () => {
|
|
75
|
+
if (!current || !rejectReason.trim()) {
|
|
76
|
+
message.warning('请填写拒绝原因')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await api(
|
|
81
|
+
apiClient.api.plugins[':slug'].reject.$put({
|
|
82
|
+
param: { slug: current.slug },
|
|
83
|
+
json: { reason: rejectReason },
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
.withLoading('拒绝中...')
|
|
87
|
+
.json()
|
|
88
|
+
message.success(`插件 "${current.name}" 已拒绝`)
|
|
89
|
+
setPending(prev => prev.filter(p => p.id !== current.id))
|
|
90
|
+
setCurrentIndex(prev => Math.min(prev, Math.max(0, pending.length - 2)))
|
|
91
|
+
setRejectModalVisible(false)
|
|
92
|
+
} catch {
|
|
93
|
+
// handled
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const goNext = () => setCurrentIndex(prev => Math.min(prev + 1, pending.length - 1))
|
|
98
|
+
const goPrev = () => setCurrentIndex(prev => Math.max(prev - 1, 0))
|
|
99
|
+
|
|
100
|
+
if (loading) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex justify-center items-center h-64">
|
|
103
|
+
<Spin size="large" tip="加载待审核插件..." />
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<div className="flex justify-between items-center mb-6">
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-2xl font-bold text-gray-900">插件审核</h1>
|
|
113
|
+
<p className="text-gray-600 mt-1">
|
|
114
|
+
待审核插件:{pending.length} 个
|
|
115
|
+
{pending.length > 0 && ` (${currentIndex + 1}/${pending.length})`}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
<Button onClick={fetchPending}>刷新列表</Button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{pending.length === 0 ? (
|
|
122
|
+
<Card>
|
|
123
|
+
<Empty description="没有待审核的插件" />
|
|
124
|
+
</Card>
|
|
125
|
+
) : (
|
|
126
|
+
current && (
|
|
127
|
+
<Card
|
|
128
|
+
className="shadow-sm"
|
|
129
|
+
title={
|
|
130
|
+
<div className="flex items-center gap-3">
|
|
131
|
+
<span className="text-lg font-semibold">{current.name}</span>
|
|
132
|
+
<Tag color={STATUS_CONFIG[current.status]?.color}>
|
|
133
|
+
{STATUS_CONFIG[current.status]?.label}
|
|
134
|
+
</Tag>
|
|
135
|
+
{current.featured && <Tag color="gold">推荐</Tag>}
|
|
136
|
+
</div>
|
|
137
|
+
}
|
|
138
|
+
extra={
|
|
139
|
+
<Space>
|
|
140
|
+
<Button
|
|
141
|
+
onClick={goPrev}
|
|
142
|
+
disabled={currentIndex === 0}
|
|
143
|
+
icon={<ArrowRight className="w-4 h-4 rotate-180" />}
|
|
144
|
+
>
|
|
145
|
+
上一个
|
|
146
|
+
</Button>
|
|
147
|
+
<Button
|
|
148
|
+
onClick={goNext}
|
|
149
|
+
disabled={currentIndex >= pending.length - 1}
|
|
150
|
+
icon={<ArrowRight className="w-4 h-4" />}
|
|
151
|
+
>
|
|
152
|
+
下一个
|
|
153
|
+
</Button>
|
|
154
|
+
</Space>
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
<Descriptions bordered column={2}>
|
|
158
|
+
<Descriptions.Item label="Slug">
|
|
159
|
+
<code className="text-xs">{current.slug}</code>
|
|
160
|
+
</Descriptions.Item>
|
|
161
|
+
<Descriptions.Item label="作者">{current.authorName}</Descriptions.Item>
|
|
162
|
+
<Descriptions.Item label="版本">
|
|
163
|
+
<code className="text-xs">{current.version}</code>
|
|
164
|
+
</Descriptions.Item>
|
|
165
|
+
<Descriptions.Item label="下载量">{current.downloadCount}</Descriptions.Item>
|
|
166
|
+
<Descriptions.Item label="描述" span={2}>
|
|
167
|
+
{current.description}
|
|
168
|
+
</Descriptions.Item>
|
|
169
|
+
{current.repositoryUrl && (
|
|
170
|
+
<Descriptions.Item label="仓库地址">
|
|
171
|
+
<a
|
|
172
|
+
href={current.repositoryUrl}
|
|
173
|
+
target="_blank"
|
|
174
|
+
rel="noopener noreferrer"
|
|
175
|
+
className="flex items-center gap-1"
|
|
176
|
+
>
|
|
177
|
+
<ExternalLink className="w-3 h-3" /> {current.repositoryUrl}
|
|
178
|
+
</a>
|
|
179
|
+
</Descriptions.Item>
|
|
180
|
+
)}
|
|
181
|
+
{current.homepageUrl && (
|
|
182
|
+
<Descriptions.Item label="主页">
|
|
183
|
+
<a
|
|
184
|
+
href={current.homepageUrl}
|
|
185
|
+
target="_blank"
|
|
186
|
+
rel="noopener noreferrer"
|
|
187
|
+
className="flex items-center gap-1"
|
|
188
|
+
>
|
|
189
|
+
<ExternalLink className="w-3 h-3" /> {current.homepageUrl}
|
|
190
|
+
</a>
|
|
191
|
+
</Descriptions.Item>
|
|
192
|
+
)}
|
|
193
|
+
{current.license && (
|
|
194
|
+
<Descriptions.Item label="许可证">{current.license}</Descriptions.Item>
|
|
195
|
+
)}
|
|
196
|
+
{current.npmPackage && (
|
|
197
|
+
<Descriptions.Item label="NPM 包">
|
|
198
|
+
<code className="text-xs">{current.npmPackage}</code>
|
|
199
|
+
</Descriptions.Item>
|
|
200
|
+
)}
|
|
201
|
+
{current.tags && current.tags.length > 0 && (
|
|
202
|
+
<Descriptions.Item label="标签" span={2}>
|
|
203
|
+
{current.tags.map(tag => (
|
|
204
|
+
<Tag key={tag}>{tag}</Tag>
|
|
205
|
+
))}
|
|
206
|
+
</Descriptions.Item>
|
|
207
|
+
)}
|
|
208
|
+
</Descriptions>
|
|
209
|
+
|
|
210
|
+
<div className="flex justify-end gap-3 mt-6 pt-4 border-t">
|
|
211
|
+
<Popconfirm
|
|
212
|
+
title="确定通过此插件审核?"
|
|
213
|
+
onConfirm={handleApprove}
|
|
214
|
+
okText="通过"
|
|
215
|
+
cancelText="取消"
|
|
216
|
+
>
|
|
217
|
+
<Button type="primary" icon={<CheckCircle className="w-4 h-4" />}>
|
|
218
|
+
通过审核
|
|
219
|
+
</Button>
|
|
220
|
+
</Popconfirm>
|
|
221
|
+
<Button danger icon={<XCircle className="w-4 h-4" />} onClick={openRejectModal}>
|
|
222
|
+
拒绝
|
|
223
|
+
</Button>
|
|
224
|
+
</div>
|
|
225
|
+
</Card>
|
|
226
|
+
)
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<Modal
|
|
230
|
+
title={`拒绝插件${current ? ` - ${current.name}` : ''}`}
|
|
231
|
+
open={rejectModalVisible}
|
|
232
|
+
onOk={handleReject}
|
|
233
|
+
onCancel={() => {
|
|
234
|
+
setRejectModalVisible(false)
|
|
235
|
+
setRejectReason('')
|
|
236
|
+
}}
|
|
237
|
+
okText="确认拒绝"
|
|
238
|
+
cancelText="取消"
|
|
239
|
+
okButtonProps={{ danger: true }}
|
|
240
|
+
>
|
|
241
|
+
<div className="mb-4">
|
|
242
|
+
<p className="text-sm text-gray-500 mb-2">请填写拒绝原因:</p>
|
|
243
|
+
<Input.TextArea
|
|
244
|
+
rows={4}
|
|
245
|
+
value={rejectReason}
|
|
246
|
+
onChange={e => setRejectReason(e.target.value)}
|
|
247
|
+
placeholder="请输入拒绝原因..."
|
|
248
|
+
maxLength={500}
|
|
249
|
+
showCount
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
</Modal>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
@@ -29,6 +29,15 @@ export const SystemLogsPage: React.FC = () => {
|
|
|
29
29
|
resourceType: '',
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
const safeFormatJson = (val: string | null | undefined): string => {
|
|
33
|
+
if (!val) return '-'
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(JSON.parse(val), null, 2)
|
|
36
|
+
} catch {
|
|
37
|
+
return val
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
useEffect(() => {
|
|
33
42
|
fetchLogs()
|
|
34
43
|
}, [fetchLogs])
|
|
@@ -199,14 +208,14 @@ export const SystemLogsPage: React.FC = () => {
|
|
|
199
208
|
{selectedLog.oldValue && (
|
|
200
209
|
<Descriptions.Item label="旧值">
|
|
201
210
|
<pre style={{ margin: 0, maxHeight: '200px', overflow: 'auto' }}>
|
|
202
|
-
{
|
|
211
|
+
{safeFormatJson(selectedLog.oldValue)}
|
|
203
212
|
</pre>
|
|
204
213
|
</Descriptions.Item>
|
|
205
214
|
)}
|
|
206
215
|
{selectedLog.newValue && (
|
|
207
216
|
<Descriptions.Item label="新值">
|
|
208
217
|
<pre style={{ margin: 0, maxHeight: '200px', overflow: 'auto' }}>
|
|
209
|
-
{
|
|
218
|
+
{safeFormatJson(selectedLog.newValue)}
|
|
210
219
|
</pre>
|
|
211
220
|
</Descriptions.Item>
|
|
212
221
|
)}
|
|
@@ -137,12 +137,13 @@ export const UsersPage: React.FC = () => {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const getStatusTag = (status: string) => {
|
|
140
|
-
const statusConfig = {
|
|
140
|
+
const statusConfig: Record<string, { color: string; text: string }> = {
|
|
141
141
|
active: { color: 'green', text: '正常' },
|
|
142
142
|
inactive: { color: 'orange', text: '未激活' },
|
|
143
143
|
locked: { color: 'red', text: '已锁定' },
|
|
144
144
|
}
|
|
145
|
-
const config = statusConfig[status
|
|
145
|
+
const config = statusConfig[status]
|
|
146
|
+
if (!config) return <Tag>{status}</Tag>
|
|
146
147
|
return <Tag color={config.color}>{config.text}</Tag>
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -10,12 +10,14 @@ interface AdminState {
|
|
|
10
10
|
isAuthenticated: boolean
|
|
11
11
|
stats: SystemStats | null
|
|
12
12
|
loading: boolean
|
|
13
|
+
error: string | null
|
|
13
14
|
|
|
14
15
|
login: (username: string, password: string) => Promise<LoginResponse>
|
|
15
16
|
logout: () => void
|
|
16
17
|
fetchStats: () => Promise<void>
|
|
17
18
|
setUser: (user: AuthUserResponse) => void
|
|
18
19
|
setToken: (token: string) => void
|
|
20
|
+
clearError: () => void
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const useAdminStore = create<AdminState>()(
|
|
@@ -26,6 +28,7 @@ export const useAdminStore = create<AdminState>()(
|
|
|
26
28
|
isAuthenticated: false,
|
|
27
29
|
stats: null,
|
|
28
30
|
loading: false,
|
|
31
|
+
error: null,
|
|
29
32
|
|
|
30
33
|
login: async (username: string, password: string) => {
|
|
31
34
|
set({ loading: true })
|
|
@@ -70,11 +73,13 @@ export const useAdminStore = create<AdminState>()(
|
|
|
70
73
|
}
|
|
71
74
|
} catch (error) {
|
|
72
75
|
console.error('Failed to fetch stats:', error)
|
|
76
|
+
set({ error: error instanceof Error ? error.message : 'Failed to fetch stats' })
|
|
73
77
|
}
|
|
74
78
|
},
|
|
75
79
|
|
|
76
80
|
setUser: (user: AuthUserResponse) => set({ user }),
|
|
77
81
|
setToken: (token: string) => set({ token }),
|
|
82
|
+
clearError: () => set({ error: null }),
|
|
78
83
|
}),
|
|
79
84
|
{
|
|
80
85
|
name: 'admin-storage',
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { setBaseUrl } from './utils/api'
|
|
4
|
-
import { createLogger } from './utils/logger'
|
|
1
|
+
import { Core, type CoreConfig } from '@dyyz1993/xcli-core'
|
|
2
|
+
import { registerBuiltinCommands } from './modules'
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
createLogger({ verbose: options.verbose })
|
|
15
|
-
if (options.url) {
|
|
16
|
-
setBaseUrl(options.url)
|
|
17
|
-
}
|
|
18
|
-
})
|
|
4
|
+
const coreConfig: CoreConfig = {
|
|
5
|
+
name: 'biomimic',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
description: 'Biomimic CLI - RPC service & code generation tools',
|
|
8
|
+
configDirName: '.biomimic',
|
|
9
|
+
envPrefix: 'BIOMIMIC',
|
|
10
|
+
pluginDirs: [],
|
|
11
|
+
}
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
const app = new Core(coreConfig)
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
// Register builtin commands (todo/notification/config modules)
|
|
16
|
+
registerBuiltinCommands(app)
|
|
17
|
+
|
|
18
|
+
// Execute CLI
|
|
19
|
+
const exitCode = await app.run(process.argv.slice(2))
|
|
20
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SiteInstance } from '@dyyz1993/xcli-core'
|
|
2
|
+
import { ok, fail } from '@dyyz1993/xcli-core'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { getClient } from '@cli/utils/api'
|
|
5
|
+
|
|
6
|
+
export function registerAuthCommands(site: SiteInstance) {
|
|
7
|
+
site.command('register', {
|
|
8
|
+
description: 'Register a new developer account',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
username: z.string().min(2).max(50).describe('Username'),
|
|
11
|
+
email: z.string().email().describe('Email'),
|
|
12
|
+
password: z.string().min(6).describe('Password'),
|
|
13
|
+
}),
|
|
14
|
+
handler: async (params: unknown) => {
|
|
15
|
+
const p = params as { username: string; email: string; password: string }
|
|
16
|
+
try {
|
|
17
|
+
const client = getClient()
|
|
18
|
+
const res = await client.api.auth.register.$post({ json: p })
|
|
19
|
+
const data = await res.json()
|
|
20
|
+
return ok(data, ['Registration successful'])
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return fail(err instanceof Error ? err.message : 'Failed to register')
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
site.command('login', {
|
|
28
|
+
description: 'Login to get API key',
|
|
29
|
+
parameters: z.object({
|
|
30
|
+
account: z.string().describe('Email or username'),
|
|
31
|
+
password: z.string().min(6).describe('Password'),
|
|
32
|
+
}),
|
|
33
|
+
handler: async (params: unknown) => {
|
|
34
|
+
const p = params as { account: string; password: string }
|
|
35
|
+
try {
|
|
36
|
+
const client = getClient()
|
|
37
|
+
const res = await client.api.auth.login.$post({ json: p })
|
|
38
|
+
const data = await res.json()
|
|
39
|
+
return ok(data, ['Login successful'])
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return fail(err instanceof Error ? err.message : 'Failed to login')
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
site.command('verify', {
|
|
47
|
+
description: 'Verify API key',
|
|
48
|
+
parameters: z.object({
|
|
49
|
+
token: z.string().describe('API key to verify'),
|
|
50
|
+
}),
|
|
51
|
+
handler: async (params: unknown) => {
|
|
52
|
+
const p = params as { token: string }
|
|
53
|
+
try {
|
|
54
|
+
const client = getClient()
|
|
55
|
+
const res = await client.api.auth.verify.$get({
|
|
56
|
+
headers: { Authorization: `Bearer ${p.token}` },
|
|
57
|
+
})
|
|
58
|
+
const data = await res.json()
|
|
59
|
+
return ok(data)
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return fail(err instanceof Error ? err.message : 'Failed to verify')
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SiteInstance } from '@dyyz1993/xcli-core'
|
|
2
|
+
import { ok, fail } from '@dyyz1993/xcli-core'
|
|
3
|
+
import { z } from 'zod'
|
|
2
4
|
import { getBaseUrl, setBaseUrl, getClient } from '@cli/utils/api'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import os from 'os'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import os from 'node:os'
|
|
7
8
|
|
|
8
9
|
const CONFIG_DIR = path.join(os.homedir(), '.biomimic')
|
|
9
10
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
@@ -37,96 +38,92 @@ function saveConfig(config: Config) {
|
|
|
37
38
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function registerConfigCommands(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const logger = getLogger()
|
|
41
|
+
export function registerConfigCommands(site: SiteInstance) {
|
|
42
|
+
site.command('config-get', {
|
|
43
|
+
description: 'Show current configuration',
|
|
44
|
+
parameters: z.object({
|
|
45
|
+
key: z.string().optional().describe('Get specific config key'),
|
|
46
|
+
}),
|
|
47
|
+
handler: async (params: unknown) => {
|
|
48
|
+
const p = params as { key?: string }
|
|
49
49
|
const cfg = loadConfig()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
logger.info(`${options.key}: ${value ?? 'not set'}`)
|
|
54
|
-
} else {
|
|
55
|
-
logger.info(JSON.stringify(cfg, null, 2))
|
|
50
|
+
if (p.key) {
|
|
51
|
+
const value = cfg[p.key]
|
|
52
|
+
return ok({ [p.key]: value ?? 'not set' })
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
54
|
+
return ok(cfg)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
site.command('config-set', {
|
|
59
|
+
description: 'Set configuration value',
|
|
60
|
+
parameters: z.object({
|
|
61
|
+
url: z.string().optional().describe('Set server URL'),
|
|
62
|
+
}),
|
|
63
|
+
handler: async (params: unknown) => {
|
|
64
|
+
const p = params as { url?: string }
|
|
65
65
|
const cfg = loadConfig()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setBaseUrl(options.url)
|
|
70
|
-
logger.success(`Server URL set to: ${options.url}`)
|
|
66
|
+
if (p.url) {
|
|
67
|
+
cfg.baseUrl = p.url
|
|
68
|
+
setBaseUrl(p.url)
|
|
71
69
|
}
|
|
72
|
-
|
|
73
70
|
saveConfig(cfg)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
return ok(cfg, ['Configuration saved'])
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
site.command('config-url', {
|
|
76
|
+
description: 'Show or set server URL',
|
|
77
|
+
parameters: z.object({
|
|
78
|
+
url: z.string().optional().describe('New server URL'),
|
|
79
|
+
}),
|
|
80
|
+
handler: async (params: unknown) => {
|
|
81
|
+
const p = params as { url?: string }
|
|
82
|
+
if (p.url) {
|
|
83
83
|
const cfg = loadConfig()
|
|
84
|
-
cfg.baseUrl = url
|
|
85
|
-
setBaseUrl(url)
|
|
84
|
+
cfg.baseUrl = p.url
|
|
85
|
+
setBaseUrl(p.url)
|
|
86
86
|
saveConfig(cfg)
|
|
87
|
-
|
|
88
|
-
} else {
|
|
89
|
-
logger.info(`Current server URL: ${getBaseUrl()}`)
|
|
87
|
+
return ok({ url: p.url }, [`Server URL set to: ${p.url}`])
|
|
90
88
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
return ok({ url: getBaseUrl() })
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
site.command('config-status', {
|
|
94
|
+
description: 'Check server connection status',
|
|
95
|
+
parameters: z.object({}),
|
|
96
|
+
handler: async () => {
|
|
100
97
|
try {
|
|
98
|
+
const client = getClient()
|
|
101
99
|
type HealthClient = { health: { $get: () => Promise<Response> } }
|
|
102
100
|
const res = await (client as unknown as HealthClient).health.$get()
|
|
103
101
|
const data = await res.json()
|
|
104
|
-
|
|
105
|
-
logger.info(JSON.stringify(data, null, 2))
|
|
102
|
+
return ok(data, ['Server is reachable'])
|
|
106
103
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
logger.error(String(error))
|
|
104
|
+
return fail(`Server not reachable: ${getBaseUrl()} - ${String(error)}`)
|
|
109
105
|
}
|
|
110
|
-
}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
111
108
|
|
|
112
|
-
config
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
const logger = getLogger()
|
|
109
|
+
site.command('config-reset', {
|
|
110
|
+
description: 'Reset configuration to defaults',
|
|
111
|
+
parameters: z.object({}),
|
|
112
|
+
handler: async () => {
|
|
117
113
|
const defaultConfig: Config = { baseUrl: 'http://localhost:3010' }
|
|
118
114
|
saveConfig(defaultConfig)
|
|
119
115
|
setBaseUrl(defaultConfig.baseUrl)
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
116
|
+
return ok(defaultConfig, ['Configuration reset to defaults'])
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
site.command('config-path', {
|
|
121
|
+
description: 'Show config file path',
|
|
122
|
+
parameters: z.object({}),
|
|
123
|
+
handler: async () => {
|
|
124
|
+
return ok({ path: CONFIG_FILE })
|
|
125
|
+
},
|
|
126
|
+
})
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
export { loadConfig, saveConfig, CONFIG_FILE }
|
|
@@ -1,12 +1,34 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Core } from '@dyyz1993/xcli-core'
|
|
2
2
|
import { registerTodoCommands } from './todo'
|
|
3
3
|
import { registerNotificationCommands } from './notification'
|
|
4
4
|
import { registerConfigCommands } from './config'
|
|
5
|
+
import { registerPluginCommands } from './plugin'
|
|
6
|
+
import { registerAuthCommands } from './auth'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Register all builtin CLI commands to xcli-core.
|
|
10
|
+
* Each register function receives a SiteInstance for command registration.
|
|
11
|
+
*/
|
|
12
|
+
export function registerBuiltinCommands(app: Core) {
|
|
13
|
+
const api = app.loader.getAPI()
|
|
14
|
+
|
|
15
|
+
// Create a builtin site representing the local server
|
|
16
|
+
const site = api.createSite({
|
|
17
|
+
name: 'local-server',
|
|
18
|
+
url: 'http://localhost:3010',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
registerTodoCommands(site)
|
|
22
|
+
registerNotificationCommands(site)
|
|
23
|
+
registerConfigCommands(site)
|
|
24
|
+
registerPluginCommands(site)
|
|
25
|
+
registerAuthCommands(site)
|
|
10
26
|
}
|
|
11
27
|
|
|
12
|
-
export {
|
|
28
|
+
export {
|
|
29
|
+
registerTodoCommands,
|
|
30
|
+
registerNotificationCommands,
|
|
31
|
+
registerConfigCommands,
|
|
32
|
+
registerPluginCommands,
|
|
33
|
+
registerAuthCommands,
|
|
34
|
+
}
|