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
@@ -1,120 +1,137 @@
1
- import { useState } from 'react'
2
- import { NavLink } from 'react-router-dom'
1
+ import { Layout, Menu } from 'antd'
3
2
  import {
4
- LayoutDashboard,
5
- Users,
6
- ShoppingCart,
7
- Headphones,
8
- FileText,
9
- AlertTriangle,
10
- Settings,
11
- Shield,
12
- UserCog,
13
- Activity,
14
- ChevronDown,
15
- ChevronRight,
16
- type LucideIcon,
17
- } from 'lucide-react'
18
- import { usePermissions } from '../hooks/usePermissions'
19
- import type { MenuItem } from '@shared/modules/permission'
20
-
21
- const ICON_MAP: Record<string, LucideIcon> = {
22
- LayoutDashboard,
23
- Users,
24
- ShoppingCart,
25
- Headphones,
26
- FileText,
27
- AlertTriangle,
28
- Settings,
29
- Shield,
30
- UserCog,
31
- Activity,
32
- }
3
+ DashboardOutlined,
4
+ FileTextOutlined,
5
+ TeamOutlined,
6
+ SettingOutlined,
7
+ RocketOutlined,
8
+ } from '@ant-design/icons'
9
+ import { NavLink, useLocation } from 'react-router-dom'
33
10
 
34
11
  interface SidebarProps {
35
- isOpen: boolean
12
+ collapsed: boolean
13
+ onCollapse: (collapsed: boolean) => void
36
14
  }
37
15
 
38
- interface MenuItemComponentProps {
39
- item: MenuItem
40
- level?: number
41
- }
16
+ const MENU_ITEMS = [
17
+ {
18
+ key: 'dashboard',
19
+ icon: <DashboardOutlined />,
20
+ label: <NavLink to="/dashboard">Dashboard</NavLink>,
21
+ },
22
+ {
23
+ key: 'content',
24
+ icon: <FileTextOutlined />,
25
+ label: 'Content Management',
26
+ children: [
27
+ {
28
+ key: 'content-list',
29
+ label: <NavLink to="/content">Content List</NavLink>,
30
+ },
31
+ {
32
+ key: 'categories',
33
+ label: <NavLink to="/categories">Categories</NavLink>,
34
+ },
35
+ ],
36
+ },
37
+ {
38
+ key: 'users-orders',
39
+ icon: <TeamOutlined />,
40
+ label: 'Users & Orders',
41
+ children: [
42
+ {
43
+ key: 'users',
44
+ label: <NavLink to="/users">Users</NavLink>,
45
+ },
46
+ {
47
+ key: 'orders',
48
+ label: <NavLink to="/orders">Orders</NavLink>,
49
+ },
50
+ {
51
+ key: 'tickets',
52
+ label: <NavLink to="/tickets">Tickets</NavLink>,
53
+ },
54
+ {
55
+ key: 'disputes',
56
+ label: <NavLink to="/disputes">Disputes</NavLink>,
57
+ },
58
+ ],
59
+ },
60
+ {
61
+ key: 'system',
62
+ icon: <SettingOutlined />,
63
+ label: 'System',
64
+ children: [
65
+ {
66
+ key: 'roles',
67
+ label: <NavLink to="/roles">Roles & Permissions</NavLink>,
68
+ },
69
+ {
70
+ key: 'system-settings',
71
+ label: <NavLink to="/settings">Settings</NavLink>,
72
+ },
73
+ {
74
+ key: 'logs',
75
+ label: <NavLink to="/system-logs">System Logs</NavLink>,
76
+ },
77
+ ],
78
+ },
79
+ ]
42
80
 
43
- const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, level = 0 }) => {
44
- const [expanded, setExpanded] = useState(false)
45
- const Icon = ICON_MAP[item.icon] || LayoutDashboard
46
- const hasChildren = item.children && item.children.length > 0
81
+ export const Sidebar: React.FC<SidebarProps> = ({ collapsed, onCollapse }) => {
82
+ const location = useLocation()
47
83
 
48
- if (hasChildren) {
49
- return (
50
- <div>
51
- <button
52
- onClick={() => setExpanded(!expanded)}
53
- className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg mb-2 transition-colors text-gray-300 hover:bg-gray-800 ${
54
- level > 0 ? 'pl-8' : ''
55
- }`}
56
- >
57
- <Icon className="w-5 h-5" />
58
- <span className="flex-1 text-left">{item.label}</span>
59
- {expanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
60
- </button>
61
- {expanded && (
62
- <div className="ml-4">
63
- {item.children!.map(child => (
64
- <MenuItemComponent key={child.path} item={child} level={level + 1} />
65
- ))}
66
- </div>
67
- )}
68
- </div>
69
- )
84
+ const getSelectedKey = () => {
85
+ const path = location.pathname
86
+ if (path === '/dashboard') return 'dashboard'
87
+ if (path.startsWith('/content')) return 'content-list'
88
+ if (path.startsWith('/categories')) return 'categories'
89
+ if (path.startsWith('/users')) return 'users'
90
+ if (path.startsWith('/orders')) return 'orders'
91
+ if (path.startsWith('/tickets')) return 'tickets'
92
+ if (path.startsWith('/disputes')) return 'disputes'
93
+ if (path.startsWith('/roles')) return 'roles'
94
+ if (path.startsWith('/settings') || path === '/settings') return 'system-settings'
95
+ if (path.startsWith('/system-logs')) return 'logs'
96
+ return 'dashboard'
70
97
  }
71
98
 
72
99
  return (
73
- <NavLink
74
- to={item.path}
75
- className={({ isActive }) =>
76
- `flex items-center gap-3 px-4 py-3 rounded-lg mb-2 transition-colors ${
77
- isActive ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'
78
- } ${level > 0 ? 'pl-8 text-sm' : ''}`
79
- }
80
- >
81
- <Icon className="w-5 h-5" />
82
- <span>{item.label}</span>
83
- </NavLink>
84
- )
85
- }
86
-
87
- export const Sidebar: React.FC<SidebarProps> = ({ isOpen }) => {
88
- const { menuConfig, loading, initialized } = usePermissions()
89
-
90
- if (loading || !initialized) {
91
- return (
92
- <aside className="bg-gray-900 text-white w-64 overflow-hidden" data-testid="admin-sidebar">
93
- <div className="h-16 flex items-center justify-between px-4 border-b border-gray-800">
94
- <h1 className="text-lg font-bold">Admin Panel</h1>
95
- </div>
96
- <nav className="p-4">
97
- <div className="text-gray-400">加载中...</div>
98
- </nav>
99
- </aside>
100
- )
101
- }
102
-
103
- return (
104
- <aside
105
- className={`bg-gray-900 text-white transition-all duration-300 ${
106
- isOpen ? 'w-64' : 'w-0'
107
- } overflow-hidden`}
100
+ <Layout.Sider
101
+ collapsible
102
+ collapsed={collapsed}
103
+ onCollapse={onCollapse}
104
+ trigger={null}
105
+ width={256}
106
+ collapsedWidth={80}
107
+ className="!bg-[#001529]"
108
108
  data-testid="admin-sidebar"
109
109
  >
110
- <div className="h-16 flex items-center justify-between px-4 border-b border-gray-800">
111
- <h1 className="text-lg font-bold">Admin Panel</h1>
110
+ <div
111
+ className="h-16 flex items-center justify-center border-b border-gray-700 overflow-hidden transition-all duration-300"
112
+ data-testid="admin-sidebar-logo"
113
+ >
114
+ <div className="flex items-center gap-2">
115
+ <div className="w-8 h-8 rounded-lg bg-indigo-500 flex items-center justify-center flex-shrink-0">
116
+ <RocketOutlined className="text-white text-base" />
117
+ </div>
118
+ {!collapsed && (
119
+ <span className="text-white font-semibold text-base whitespace-nowrap tracking-tight">
120
+ Biomimic
121
+ </span>
122
+ )}
123
+ </div>
112
124
  </div>
113
- <nav className="p-4">
114
- {menuConfig.map((item: MenuItem) => (
115
- <MenuItemComponent key={item.path} item={item} />
116
- ))}
117
- </nav>
118
- </aside>
125
+
126
+ <Menu
127
+ mode="inline"
128
+ selectedKeys={[getSelectedKey()]}
129
+ defaultOpenKeys={['content', 'users-orders', 'system']}
130
+ inlineCollapsed={false}
131
+ items={MENU_ITEMS}
132
+ className="!border-r-0 !bg-transparent mt-2"
133
+ style={{ background: 'transparent' }}
134
+ />
135
+ </Layout.Sider>
119
136
  )
120
137
  }
@@ -0,0 +1,241 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import {
3
+ Table,
4
+ Card,
5
+ Button,
6
+ Space,
7
+ Modal,
8
+ Form,
9
+ Input,
10
+ InputNumber,
11
+ message,
12
+ Popconfirm,
13
+ } from 'antd'
14
+ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
15
+ import type { ColumnsType } from 'antd/es/table'
16
+ import { apiClient, api } from '../services/apiClient'
17
+ import type { Category } from '@shared/modules/plugins'
18
+
19
+ type CategoryFormValues = Pick<Category, 'name' | 'slug'> & {
20
+ description?: string
21
+ icon?: string
22
+ sortOrder?: number
23
+ }
24
+
25
+ export const CategoryManagementPage: React.FC = () => {
26
+ const [categories, setCategories] = useState<Category[]>([])
27
+ const [loading, setLoading] = useState(false)
28
+ const [modalVisible, setModalVisible] = useState(false)
29
+ const [editingCategory, setEditingCategory] = useState<Category | null>(null)
30
+ const [form] = Form.useForm<CategoryFormValues>()
31
+
32
+ const fetchCategories = useCallback(async () => {
33
+ setLoading(true)
34
+ try {
35
+ const data = await api(apiClient.api.categories.$get()).withLoading('加载分类...').json()
36
+ if (Array.isArray(data)) {
37
+ setCategories(data)
38
+ }
39
+ } catch {
40
+ // 错误已由 api-request 自动处理
41
+ } finally {
42
+ setLoading(false)
43
+ }
44
+ }, [])
45
+
46
+ useEffect(() => {
47
+ fetchCategories()
48
+ }, [fetchCategories])
49
+
50
+ const handleCreate = () => {
51
+ setEditingCategory(null)
52
+ form.resetFields()
53
+ form.setFieldsValue({ sortOrder: 0 })
54
+ setModalVisible(true)
55
+ }
56
+
57
+ const handleEdit = (category: Category) => {
58
+ setEditingCategory(category)
59
+ form.setFieldsValue({
60
+ name: category.name,
61
+ slug: category.slug,
62
+ description: category.description ?? undefined,
63
+ icon: category.icon ?? undefined,
64
+ sortOrder: category.sortOrder,
65
+ })
66
+ setModalVisible(true)
67
+ }
68
+
69
+ const handleSubmit = async () => {
70
+ try {
71
+ const values = await form.validateFields()
72
+
73
+ if (editingCategory) {
74
+ await api(
75
+ apiClient.api.categories[':id'].$put({
76
+ param: { id: editingCategory.id },
77
+ json: values,
78
+ })
79
+ )
80
+ .withLoading('更新中...')
81
+ .json()
82
+ message.success('分类已更新')
83
+ } else {
84
+ await api(apiClient.api.categories.$post({ json: values }))
85
+ .withLoading('创建中...')
86
+ .json()
87
+ message.success('分类已创建')
88
+ }
89
+
90
+ setModalVisible(false)
91
+ fetchCategories()
92
+ } catch {
93
+ // 错误已由 api-request 自动处理
94
+ }
95
+ }
96
+
97
+ const handleDelete = async (id: string) => {
98
+ try {
99
+ await api(apiClient.api.categories[':id'].$delete({ param: { id } }))
100
+ .withLoading('删除中...')
101
+ .json()
102
+ message.success('分类已删除')
103
+ fetchCategories()
104
+ } catch {
105
+ // 错误已由 api-request 自动处理
106
+ }
107
+ }
108
+
109
+ const columns: ColumnsType<Category> = [
110
+ {
111
+ title: '名称',
112
+ dataIndex: 'name',
113
+ key: 'name',
114
+ render: (name: string, record: Category) => (
115
+ <div>
116
+ <span className="font-medium">{name}</span>
117
+ {record.icon && <span className="ml-2 text-gray-400">{record.icon}</span>}
118
+ </div>
119
+ ),
120
+ },
121
+ {
122
+ title: 'Slug',
123
+ dataIndex: 'slug',
124
+ key: 'slug',
125
+ render: (slug: string) => (
126
+ <code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{slug}</code>
127
+ ),
128
+ },
129
+ {
130
+ title: '描述',
131
+ dataIndex: 'description',
132
+ key: 'description',
133
+ ellipsis: true,
134
+ render: (desc?: string) => desc || <span className="text-gray-300">-</span>,
135
+ },
136
+ {
137
+ title: '图标',
138
+ dataIndex: 'icon',
139
+ key: 'icon',
140
+ render: (icon?: string) => icon || <span className="text-gray-300">-</span>,
141
+ },
142
+ {
143
+ title: '排序',
144
+ dataIndex: 'sortOrder',
145
+ key: 'sortOrder',
146
+ sorter: (a: Category, b: Category) => a.sortOrder - b.sortOrder,
147
+ width: 100,
148
+ },
149
+ {
150
+ title: '操作',
151
+ key: 'actions',
152
+ width: 180,
153
+ render: (_: unknown, record: Category) => (
154
+ <Space>
155
+ <Button
156
+ type="link"
157
+ size="small"
158
+ icon={<EditOutlined />}
159
+ onClick={() => handleEdit(record)}
160
+ >
161
+ 编辑
162
+ </Button>
163
+ <Popconfirm
164
+ title="确定要删除此分类吗?"
165
+ description="删除后关联的插件将失去分类标记"
166
+ onConfirm={() => handleDelete(record.id)}
167
+ okText="删除"
168
+ cancelText="取消"
169
+ okButtonProps={{ danger: true }}
170
+ >
171
+ <Button type="link" size="small" danger icon={<DeleteOutlined />}>
172
+ 删除
173
+ </Button>
174
+ </Popconfirm>
175
+ </Space>
176
+ ),
177
+ },
178
+ ]
179
+
180
+ return (
181
+ <div>
182
+ <div className="flex justify-between items-center mb-6">
183
+ <div>
184
+ <h1 className="text-2xl font-bold text-gray-900">分类管理</h1>
185
+ <p className="text-gray-600 mt-1">管理插件市场的分类</p>
186
+ </div>
187
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
188
+ 创建分类
189
+ </Button>
190
+ </div>
191
+
192
+ <Card className="shadow-sm">
193
+ <Table
194
+ columns={columns}
195
+ dataSource={categories}
196
+ rowKey="id"
197
+ loading={loading}
198
+ pagination={{ pageSize: 20, showTotal: t => `共 ${t} 个分类` }}
199
+ />
200
+ </Card>
201
+
202
+ <Modal
203
+ title={editingCategory ? '编辑分类' : '创建分类'}
204
+ open={modalVisible}
205
+ onOk={handleSubmit}
206
+ onCancel={() => setModalVisible(false)}
207
+ okText={editingCategory ? '保存' : '创建'}
208
+ width={520}
209
+ >
210
+ <Form form={form} layout="vertical" className="mt-4">
211
+ <Form.Item
212
+ name="name"
213
+ label="分类名称"
214
+ rules={[{ required: true, message: '请输入分类名称' }]}
215
+ >
216
+ <Input placeholder="例如:AI 工具" />
217
+ </Form.Item>
218
+ <Form.Item
219
+ name="slug"
220
+ label="Slug"
221
+ rules={[
222
+ { required: true, message: '请输入 Slug' },
223
+ { pattern: /^[a-z0-9-]+$/, message: '只能包含小写字母、数字和连字符' },
224
+ ]}
225
+ >
226
+ <Input placeholder="例如:ai-tools" disabled={!!editingCategory} />
227
+ </Form.Item>
228
+ <Form.Item name="description" label="描述">
229
+ <Input.TextArea rows={3} placeholder="分类描述(可选)" maxLength={500} showCount />
230
+ </Form.Item>
231
+ <Form.Item name="icon" label="图标">
232
+ <Input placeholder="图标名称或 Emoji(可选)" />
233
+ </Form.Item>
234
+ <Form.Item name="sortOrder" label="排序" initialValue={0}>
235
+ <InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" />
236
+ </Form.Item>
237
+ </Form>
238
+ </Modal>
239
+ </div>
240
+ )
241
+ }
@@ -44,6 +44,8 @@ export const ContentPage: React.FC = () => {
44
44
  const result = await response.json()
45
45
  if (result.success) {
46
46
  setContents(result.data)
47
+ } else {
48
+ message.error('Failed to load content')
47
49
  }
48
50
  } catch {
49
51
  message.error('获取内容列表失败')
@@ -18,6 +18,8 @@ export const DashboardPage: React.FC = () => {
18
18
  const result = await response.json()
19
19
  if (result.success) {
20
20
  setStats(result.data)
21
+ } else {
22
+ message.error(result.error || 'Failed to load stats')
21
23
  }
22
24
  } catch (error) {
23
25
  console.error('Failed to fetch stats:', error)
@@ -45,6 +45,8 @@ export const DisputesPage: React.FC = () => {
45
45
  const result = await response.json()
46
46
  if (result.success) {
47
47
  setDisputes(result.data)
48
+ } else {
49
+ message.error(result.error || 'Failed to load disputes')
48
50
  }
49
51
  } catch {
50
52
  message.error('获取争议列表失败')
@@ -194,7 +194,7 @@ export const MediaTestPage: React.FC = () => {
194
194
  setDirectSpeed(`${speed} KB/s`)
195
195
  }
196
196
 
197
- const blob = new Blob(chunks)
197
+ const blob = new Blob(chunks as BlobPart[])
198
198
  const url = URL.createObjectURL(blob)
199
199
  const a = document.createElement('a')
200
200
  a.href = url
@@ -41,6 +41,8 @@ export const OrdersPage: React.FC = () => {
41
41
  const result = await response.json()
42
42
  if (result.success) {
43
43
  setOrders(result.data)
44
+ } else {
45
+ message.error(result.error || 'Failed to load orders')
44
46
  }
45
47
  } catch {
46
48
  message.error('获取订单列表失败')