create-fullstack-scaffold 0.4.9 → 0.4.10
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 +499 -100
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/template/CLAUDE.md +18 -6
- package/template/drizzle/0004_add_merchants_products.sql +30 -0
- package/template/drizzle/meta/_journal.json +7 -0
- package/template/eslint-rules/module-boundary.js +6 -2
- package/template/eslint-rules/no-cross-module-service-import.js +105 -0
- package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
- package/template/eslint.config.js +9 -0
- package/template/lint-scripts/config/project.config.ts +21 -0
- package/template/lint-scripts/validate-all.ts +48 -12
- package/template/lint-scripts/validators/index.ts +29 -0
- package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
- package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
- package/template/modules.config.ts +2 -0
- package/template/package.json +2 -0
- package/template/playwright.config.ts +14 -0
- package/template/src/admin/App.tsx +40 -3
- package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
- package/template/src/admin/components/NotificationDrawer.tsx +108 -38
- package/template/src/admin/components/StatsCard.tsx +3 -1
- package/template/src/admin/components/ThemeToggle.tsx +28 -0
- package/template/src/admin/hooks/useRoles.ts +2 -2
- package/template/src/admin/i18n/index.ts +18 -0
- package/template/src/admin/i18n/locales/en-US.json +381 -0
- package/template/src/admin/i18n/locales/zh-CN.json +381 -0
- package/template/src/admin/i18n/useLanguage.ts +21 -0
- package/template/src/admin/layouts/Header.tsx +37 -16
- package/template/src/admin/layouts/Layout.tsx +7 -3
- package/template/src/admin/layouts/Sidebar.tsx +70 -68
- package/template/src/admin/main.tsx +1 -0
- package/template/src/admin/pages/ContentPage.tsx +68 -56
- package/template/src/admin/pages/DashboardPage.tsx +82 -76
- package/template/src/admin/pages/DisputesPage.tsx +61 -53
- package/template/src/admin/pages/LoginPage.tsx +41 -33
- package/template/src/admin/pages/OrdersPage.tsx +69 -64
- package/template/src/admin/pages/PermissionsPage.tsx +10 -8
- package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
- package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
- package/template/src/admin/pages/RegisterPage.tsx +18 -16
- package/template/src/admin/pages/RolesPage.tsx +51 -49
- package/template/src/admin/pages/SettingsPage.tsx +22 -22
- package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
- package/template/src/admin/pages/TicketsPage.tsx +67 -59
- package/template/src/admin/pages/UsersPage.tsx +63 -53
- package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
- package/template/src/admin/stores/themeStore.ts +32 -0
- package/template/src/client/index.css +73 -0
- package/template/src/merchant/App.tsx +2 -0
- package/template/src/merchant/components/MerchantGuard.tsx +2 -6
- package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
- package/template/src/merchant/layouts/Header.tsx +2 -2
- package/template/src/merchant/pages/DashboardPage.tsx +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +14 -10
- package/template/src/merchant/pages/LoginPage.tsx +49 -0
- package/template/src/merchant/pages/OrdersPage.tsx +17 -10
- package/template/src/merchant/pages/ProductsPage.tsx +31 -8
- package/template/src/merchant/pages/SettingsPage.tsx +3 -7
- package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
- package/template/src/merchant/stores/merchantStore.ts +24 -27
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/merchants.ts +26 -0
- package/template/src/server/db/schema/products.ts +25 -0
- package/template/src/server/db/test-setup.ts +237 -0
- package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
- package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
- package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
- package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
- package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
- package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
- package/template/src/server/module-admin/module.ts +4 -0
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
- package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
- package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
- package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
- package/template/src/server/module-auth/module.ts +2 -0
- package/template/src/server/module-content/module.ts +1 -0
- package/template/src/server/module-content/routes/content-routes.ts +2 -2
- package/template/src/server/module-dispute/module.ts +1 -0
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
- package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
- package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
- package/template/src/server/module-merchant/index.ts +10 -0
- package/template/src/server/module-merchant/module.ts +33 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
- package/template/src/server/module-notifications/index.ts +18 -0
- package/template/src/server/module-notifications/module.ts +2 -0
- package/template/src/server/module-order/module.ts +1 -0
- package/template/src/server/module-order/routes/order-routes.ts +2 -2
- package/template/src/server/module-permission/routes/role-routes.ts +3 -3
- package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
- package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
- package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
- package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
- package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
- package/template/src/server/module-plugin/module.ts +3 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
- package/template/src/server/module-tenant/module.ts +1 -0
- package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
- package/template/src/server/module-ticket/module.ts +1 -0
- package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
- package/template/src/server/module-todos/module.ts +3 -0
- package/template/src/server/route-registry.ts +4 -0
- package/template/src/server/utils/__tests__/date.test.ts +130 -0
- package/template/src/server/utils/__tests__/env.test.ts +25 -0
- package/template/src/server/utils/__tests__/generate.test.ts +82 -0
- package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
- package/template/src/server/utils/__tests__/json.test.ts +49 -0
- package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
- package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
- package/template/src/shared/core/module-manifest.ts +15 -0
- package/template/src/shared/modules/admin/schemas.ts +1 -1
- package/template/src/shared/modules/content/schemas.ts +2 -2
- package/template/src/shared/modules/dispute/schemas.ts +2 -2
- package/template/src/shared/modules/index.ts +181 -1
- package/template/src/shared/modules/merchant/index.ts +12 -0
- package/template/src/shared/modules/merchant/schemas.ts +63 -0
- package/template/src/shared/modules/order/schemas.ts +2 -2
- package/template/src/shared/modules/permission/index.ts +1 -1
- package/template/src/shared/modules/permission/schemas.ts +1 -1
- package/template/src/shared/modules/role/schemas.ts +2 -2
- package/template/src/shared/modules/ticket/schemas.ts +2 -2
- package/template/src/shared/schemas/index.ts +74 -2
|
@@ -7,80 +7,82 @@ import {
|
|
|
7
7
|
RocketOutlined,
|
|
8
8
|
} from '@ant-design/icons'
|
|
9
9
|
import { NavLink, useLocation } from 'react-router-dom'
|
|
10
|
+
import { useLanguage } from '../i18n/useLanguage'
|
|
10
11
|
|
|
11
12
|
interface SidebarProps {
|
|
12
13
|
collapsed: boolean
|
|
13
14
|
onCollapse: (collapsed: boolean) => void
|
|
14
15
|
}
|
|
15
16
|
|
|
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
|
-
]
|
|
80
|
-
|
|
81
17
|
export const Sidebar: React.FC<SidebarProps> = ({ collapsed, onCollapse }) => {
|
|
18
|
+
const { t } = useLanguage()
|
|
82
19
|
const location = useLocation()
|
|
83
20
|
|
|
21
|
+
const MENU_ITEMS = [
|
|
22
|
+
{
|
|
23
|
+
key: 'dashboard',
|
|
24
|
+
icon: <DashboardOutlined />,
|
|
25
|
+
label: <NavLink to="/dashboard">{t('sidebar.dashboard')}</NavLink>,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'content',
|
|
29
|
+
icon: <FileTextOutlined />,
|
|
30
|
+
label: t('sidebar.content'),
|
|
31
|
+
children: [
|
|
32
|
+
{
|
|
33
|
+
key: 'content-list',
|
|
34
|
+
label: <NavLink to="/content">{t('sidebar.contentList')}</NavLink>,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'categories',
|
|
38
|
+
label: <NavLink to="/categories">{t('sidebar.categories')}</NavLink>,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'users-orders',
|
|
44
|
+
icon: <TeamOutlined />,
|
|
45
|
+
label: t('sidebar.usersOrders'),
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
key: 'users',
|
|
49
|
+
label: <NavLink to="/users">{t('sidebar.users')}</NavLink>,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'orders',
|
|
53
|
+
label: <NavLink to="/orders">{t('sidebar.orders')}</NavLink>,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'tickets',
|
|
57
|
+
label: <NavLink to="/tickets">{t('sidebar.tickets')}</NavLink>,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'disputes',
|
|
61
|
+
label: <NavLink to="/disputes">{t('sidebar.disputes')}</NavLink>,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'system',
|
|
67
|
+
icon: <SettingOutlined />,
|
|
68
|
+
label: t('sidebar.system'),
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
key: 'roles',
|
|
72
|
+
label: <NavLink to="/system/roles">{t('sidebar.roles')}</NavLink>,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'system-settings',
|
|
76
|
+
label: <NavLink to="/system/settings">{t('sidebar.settings')}</NavLink>,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'logs',
|
|
80
|
+
label: <NavLink to="/system/logs">{t('sidebar.logs')}</NavLink>,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
|
|
84
86
|
const getSelectedKey = () => {
|
|
85
87
|
const path = location.pathname
|
|
86
88
|
if (path === '/dashboard') return 'dashboard'
|
|
@@ -90,9 +92,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ collapsed, onCollapse }) => {
|
|
|
90
92
|
if (path.startsWith('/orders')) return 'orders'
|
|
91
93
|
if (path.startsWith('/tickets')) return 'tickets'
|
|
92
94
|
if (path.startsWith('/disputes')) return 'disputes'
|
|
93
|
-
if (path.startsWith('/roles')) return 'roles'
|
|
94
|
-
if (path.startsWith('/
|
|
95
|
-
if (path.startsWith('/system
|
|
95
|
+
if (path.startsWith('/system/roles')) return 'roles'
|
|
96
|
+
if (path.startsWith('/system/settings')) return 'system-settings'
|
|
97
|
+
if (path.startsWith('/system/logs')) return 'logs'
|
|
96
98
|
return 'dashboard'
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -5,34 +5,42 @@ import { PermissionGuard } from '../components/PermissionGuard'
|
|
|
5
5
|
import { Permission } from '@shared/modules/permission'
|
|
6
6
|
import { apiClient } from '../services/apiClient'
|
|
7
7
|
import type { Content, CreateContentInput } from '@shared/modules/content'
|
|
8
|
+
import { useLanguage } from '../i18n/useLanguage'
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
article: '文章',
|
|
11
|
-
announcement: '公告',
|
|
12
|
-
tutorial: '教程',
|
|
13
|
-
news: '新闻',
|
|
14
|
-
policy: '政策',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const STATUS_COLORS = {
|
|
10
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
18
11
|
draft: 'default',
|
|
19
12
|
published: 'success',
|
|
20
13
|
archived: 'warning',
|
|
21
14
|
}
|
|
22
15
|
|
|
23
|
-
const STATUS_LABELS = {
|
|
24
|
-
draft: '草稿',
|
|
25
|
-
published: '已发布',
|
|
26
|
-
archived: '已归档',
|
|
27
|
-
}
|
|
28
|
-
|
|
29
16
|
export const ContentPage: React.FC = () => {
|
|
17
|
+
const { t, formatDate } = useLanguage()
|
|
30
18
|
const [contents, setContents] = useState<Content[]>([])
|
|
31
19
|
const [loading, setLoading] = useState(false)
|
|
32
20
|
const [modalVisible, setModalVisible] = useState(false)
|
|
33
21
|
const [editingContent, setEditingContent] = useState<Content | null>(null)
|
|
34
22
|
const [form] = Form.useForm<CreateContentInput>()
|
|
35
23
|
|
|
24
|
+
const getCategoryLabel = (category: string) => {
|
|
25
|
+
const map: Record<string, string> = {
|
|
26
|
+
article: t('content.catArticle'),
|
|
27
|
+
announcement: t('content.catAnnouncement'),
|
|
28
|
+
tutorial: t('content.catTutorial'),
|
|
29
|
+
news: t('content.catNews'),
|
|
30
|
+
policy: t('content.catPolicy'),
|
|
31
|
+
}
|
|
32
|
+
return map[category] || category
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const getStatusLabel = (status: string) => {
|
|
36
|
+
const map: Record<string, string> = {
|
|
37
|
+
draft: t('content.statusDraft'),
|
|
38
|
+
published: t('content.statusPublished'),
|
|
39
|
+
archived: t('content.statusArchived'),
|
|
40
|
+
}
|
|
41
|
+
return map[status] || status
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
useEffect(() => {
|
|
37
45
|
fetchContents()
|
|
38
46
|
}, [])
|
|
@@ -45,10 +53,10 @@ export const ContentPage: React.FC = () => {
|
|
|
45
53
|
if (result.success) {
|
|
46
54
|
setContents(result.data)
|
|
47
55
|
} else {
|
|
48
|
-
message.error('
|
|
56
|
+
message.error(t('content.loadFailed'))
|
|
49
57
|
}
|
|
50
58
|
} catch {
|
|
51
|
-
message.error('
|
|
59
|
+
message.error(t('content.loadFailed'))
|
|
52
60
|
} finally {
|
|
53
61
|
setLoading(false)
|
|
54
62
|
}
|
|
@@ -68,8 +76,8 @@ export const ContentPage: React.FC = () => {
|
|
|
68
76
|
|
|
69
77
|
const handleDelete = (id: string) => {
|
|
70
78
|
Modal.confirm({
|
|
71
|
-
title: '
|
|
72
|
-
content: '
|
|
79
|
+
title: t('content.confirmDelete'),
|
|
80
|
+
content: t('content.confirmDeleteContent'),
|
|
73
81
|
onOk: async () => {
|
|
74
82
|
try {
|
|
75
83
|
const response = await apiClient.api.contents[':id'].$delete({
|
|
@@ -77,11 +85,11 @@ export const ContentPage: React.FC = () => {
|
|
|
77
85
|
})
|
|
78
86
|
const result = await response.json()
|
|
79
87
|
if (result.success) {
|
|
80
|
-
message.success('
|
|
88
|
+
message.success(t('content.deleted'))
|
|
81
89
|
fetchContents()
|
|
82
90
|
}
|
|
83
91
|
} catch {
|
|
84
|
-
message.error('
|
|
92
|
+
message.error(t('content.deleteFailed'))
|
|
85
93
|
}
|
|
86
94
|
},
|
|
87
95
|
})
|
|
@@ -98,7 +106,7 @@ export const ContentPage: React.FC = () => {
|
|
|
98
106
|
})
|
|
99
107
|
const result = await response.json()
|
|
100
108
|
if (result.success) {
|
|
101
|
-
message.success('
|
|
109
|
+
message.success(t('content.updated'))
|
|
102
110
|
setModalVisible(false)
|
|
103
111
|
fetchContents()
|
|
104
112
|
}
|
|
@@ -108,43 +116,43 @@ export const ContentPage: React.FC = () => {
|
|
|
108
116
|
})
|
|
109
117
|
const result = await response.json()
|
|
110
118
|
if (result.success) {
|
|
111
|
-
message.success('
|
|
119
|
+
message.success(t('content.created'))
|
|
112
120
|
setModalVisible(false)
|
|
113
121
|
fetchContents()
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
} catch {
|
|
117
|
-
message.error('
|
|
125
|
+
message.error(t('common.error'))
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
const columns = [
|
|
122
130
|
{
|
|
123
|
-
title: '
|
|
131
|
+
title: t('content.titleCol'),
|
|
124
132
|
dataIndex: 'title',
|
|
125
133
|
key: 'title',
|
|
126
134
|
},
|
|
127
135
|
{
|
|
128
|
-
title: '
|
|
136
|
+
title: t('content.category'),
|
|
129
137
|
dataIndex: 'category',
|
|
130
138
|
key: 'category',
|
|
131
|
-
render: (category:
|
|
139
|
+
render: (category: string) => <Tag>{getCategoryLabel(category)}</Tag>,
|
|
132
140
|
},
|
|
133
141
|
{
|
|
134
|
-
title: '
|
|
142
|
+
title: t('common.status'),
|
|
135
143
|
dataIndex: 'status',
|
|
136
144
|
key: 'status',
|
|
137
|
-
render: (status:
|
|
138
|
-
<Tag color={STATUS_COLORS[status]}>{
|
|
145
|
+
render: (status: string) => (
|
|
146
|
+
<Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
|
|
139
147
|
),
|
|
140
148
|
},
|
|
141
149
|
{
|
|
142
|
-
title: '
|
|
150
|
+
title: t('content.author'),
|
|
143
151
|
dataIndex: 'author',
|
|
144
152
|
key: 'author',
|
|
145
153
|
},
|
|
146
154
|
{
|
|
147
|
-
title: '
|
|
155
|
+
title: t('content.viewsLikes'),
|
|
148
156
|
key: 'stats',
|
|
149
157
|
render: (_: unknown, record: Content) => (
|
|
150
158
|
<span>
|
|
@@ -153,13 +161,13 @@ export const ContentPage: React.FC = () => {
|
|
|
153
161
|
),
|
|
154
162
|
},
|
|
155
163
|
{
|
|
156
|
-
title: '
|
|
164
|
+
title: t('common.createdAt'),
|
|
157
165
|
dataIndex: 'createdAt',
|
|
158
166
|
key: 'createdAt',
|
|
159
|
-
render: (date: string) =>
|
|
167
|
+
render: (date: string) => formatDate(date),
|
|
160
168
|
},
|
|
161
169
|
{
|
|
162
|
-
title: '
|
|
170
|
+
title: t('common.actions'),
|
|
163
171
|
key: 'actions',
|
|
164
172
|
render: (_: unknown, record: Content) => (
|
|
165
173
|
<Space>
|
|
@@ -170,7 +178,7 @@ export const ContentPage: React.FC = () => {
|
|
|
170
178
|
icon={<Edit className="w-4 h-4" />}
|
|
171
179
|
onClick={() => handleEdit(record)}
|
|
172
180
|
>
|
|
173
|
-
|
|
181
|
+
{t('common.edit')}
|
|
174
182
|
</Button>
|
|
175
183
|
</PermissionGuard>
|
|
176
184
|
<PermissionGuard permission={Permission.CONTENT_DELETE}>
|
|
@@ -181,7 +189,7 @@ export const ContentPage: React.FC = () => {
|
|
|
181
189
|
icon={<Delete className="w-4 h-4" />}
|
|
182
190
|
onClick={() => handleDelete(record.id)}
|
|
183
191
|
>
|
|
184
|
-
|
|
192
|
+
{t('common.delete')}
|
|
185
193
|
</Button>
|
|
186
194
|
</PermissionGuard>
|
|
187
195
|
</Space>
|
|
@@ -193,12 +201,12 @@ export const ContentPage: React.FC = () => {
|
|
|
193
201
|
<div>
|
|
194
202
|
<div className="flex justify-between items-center mb-6">
|
|
195
203
|
<div>
|
|
196
|
-
<h1 className="text-2xl font-bold text-gray-900"
|
|
197
|
-
<p className="text-gray-600 mt-1"
|
|
204
|
+
<h1 className="text-2xl font-bold text-gray-900">{t('content.title')}</h1>
|
|
205
|
+
<p className="text-gray-600 mt-1">{t('content.subtitle')}</p>
|
|
198
206
|
</div>
|
|
199
207
|
<PermissionGuard permission={Permission.CONTENT_CREATE}>
|
|
200
208
|
<Button type="primary" icon={<Plus className="w-4 h-4" />} onClick={handleCreate}>
|
|
201
|
-
|
|
209
|
+
{t('content.createContent')}
|
|
202
210
|
</Button>
|
|
203
211
|
</PermissionGuard>
|
|
204
212
|
</div>
|
|
@@ -214,38 +222,42 @@ export const ContentPage: React.FC = () => {
|
|
|
214
222
|
</Card>
|
|
215
223
|
|
|
216
224
|
<Modal
|
|
217
|
-
title={editingContent ? '
|
|
225
|
+
title={editingContent ? t('content.editContent') : t('content.createContent')}
|
|
218
226
|
open={modalVisible}
|
|
219
227
|
onOk={handleSubmit}
|
|
220
228
|
onCancel={() => setModalVisible(false)}
|
|
221
229
|
width={600}
|
|
222
230
|
>
|
|
223
231
|
<Form form={form} layout="vertical">
|
|
224
|
-
<Form.Item
|
|
225
|
-
|
|
232
|
+
<Form.Item
|
|
233
|
+
name="title"
|
|
234
|
+
label={t('content.titleCol')}
|
|
235
|
+
rules={[{ required: true, message: t('content.titleRequired') }]}
|
|
236
|
+
>
|
|
237
|
+
<Input placeholder={t('content.titlePlaceholder')} />
|
|
226
238
|
</Form.Item>
|
|
227
239
|
<Form.Item
|
|
228
240
|
name="category"
|
|
229
|
-
label=
|
|
230
|
-
rules={[{ required: true, message: '
|
|
241
|
+
label={t('content.category')}
|
|
242
|
+
rules={[{ required: true, message: t('content.categoryRequired') }]}
|
|
231
243
|
>
|
|
232
|
-
<Select placeholder=
|
|
233
|
-
<Select.Option value="article"
|
|
234
|
-
<Select.Option value="announcement"
|
|
235
|
-
<Select.Option value="tutorial"
|
|
236
|
-
<Select.Option value="news"
|
|
237
|
-
<Select.Option value="policy"
|
|
244
|
+
<Select placeholder={t('content.categoryPlaceholder')}>
|
|
245
|
+
<Select.Option value="article">{t('content.catArticle')}</Select.Option>
|
|
246
|
+
<Select.Option value="announcement">{t('content.catAnnouncement')}</Select.Option>
|
|
247
|
+
<Select.Option value="tutorial">{t('content.catTutorial')}</Select.Option>
|
|
248
|
+
<Select.Option value="news">{t('content.catNews')}</Select.Option>
|
|
249
|
+
<Select.Option value="policy">{t('content.catPolicy')}</Select.Option>
|
|
238
250
|
</Select>
|
|
239
251
|
</Form.Item>
|
|
240
252
|
<Form.Item
|
|
241
253
|
name="content"
|
|
242
|
-
label=
|
|
243
|
-
rules={[{ required: true, message: '
|
|
254
|
+
label={t('content.contentLabel')}
|
|
255
|
+
rules={[{ required: true, message: t('content.contentRequired') }]}
|
|
244
256
|
>
|
|
245
|
-
<Input.TextArea rows={6} placeholder=
|
|
257
|
+
<Input.TextArea rows={6} placeholder={t('content.contentPlaceholder')} />
|
|
246
258
|
</Form.Item>
|
|
247
|
-
<Form.Item name="tags" label=
|
|
248
|
-
<Select mode="tags" placeholder=
|
|
259
|
+
<Form.Item name="tags" label={t('content.tags')}>
|
|
260
|
+
<Select mode="tags" placeholder={t('content.tagsPlaceholder')} />
|
|
249
261
|
</Form.Item>
|
|
250
262
|
</Form>
|
|
251
263
|
</Modal>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Button, Select, message, theme } from 'antd'
|
|
3
|
+
import { Activity, CheckCircle, Clock, TrendingUp, BellRing } from 'lucide-react'
|
|
2
4
|
import { apiClient } from '../services/apiClient'
|
|
5
|
+
import { useLanguage } from '../i18n/useLanguage'
|
|
3
6
|
import type { SystemStats } from '@shared/modules/admin'
|
|
4
7
|
import type { NotificationType } from '@shared/schemas'
|
|
5
|
-
import { Activity, CheckCircle, Clock, TrendingUp, Bell, BellRing } from 'lucide-react'
|
|
6
|
-
import { Button, Select, message } from 'antd'
|
|
7
8
|
|
|
8
9
|
export const DashboardPage: React.FC = () => {
|
|
10
|
+
const { t } = useLanguage()
|
|
11
|
+
const { token } = theme.useToken()
|
|
9
12
|
const [stats, setStats] = useState<SystemStats | null>(null)
|
|
10
13
|
const [loading, setLoading] = useState(true)
|
|
11
14
|
const [sendingNotification, setSendingNotification] = useState(false)
|
|
@@ -18,20 +21,40 @@ export const DashboardPage: React.FC = () => {
|
|
|
18
21
|
const result = await response.json()
|
|
19
22
|
if (result.success) {
|
|
20
23
|
setStats(result.data)
|
|
21
|
-
} else {
|
|
22
|
-
message.error(result.error || 'Failed to load stats')
|
|
23
24
|
}
|
|
24
|
-
} catch
|
|
25
|
-
|
|
25
|
+
} catch {
|
|
26
|
+
message.error(t('dashboard.loadFailed'))
|
|
26
27
|
} finally {
|
|
27
28
|
setLoading(false)
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
fetchStats()
|
|
32
|
-
}, [])
|
|
33
|
+
}, [t])
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
if (loading) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="p-5" style={{ color: token.colorText }}>
|
|
38
|
+
{t('common.loading')}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const statCards = [
|
|
44
|
+
{ title: t('dashboard.totalTodos'), value: stats?.totalTodos || 0, icon: CheckCircle, color: 'bg-blue-500' },
|
|
45
|
+
{ title: t('dashboard.pending'), value: stats?.pendingTodos || 0, icon: Clock, color: 'bg-yellow-500' },
|
|
46
|
+
{ title: t('dashboard.completed'), value: stats?.completedTodos || 0, icon: TrendingUp, color: 'bg-green-500' },
|
|
47
|
+
{ title: t('dashboard.lastUpdated'), value: stats?.lastUpdated || '-', icon: Activity, color: 'bg-purple-500' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const notificationTypeOptions: { value: NotificationType; label: string }[] = [
|
|
51
|
+
{ value: 'info', label: t('dashboard.notificationInfo') },
|
|
52
|
+
{ value: 'success', label: t('dashboard.notificationSuccess') },
|
|
53
|
+
{ value: 'warning', label: t('dashboard.notificationWarning') },
|
|
54
|
+
{ value: 'error', label: t('dashboard.notificationError') },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const handleSendTest = async () => {
|
|
35
58
|
setSendingNotification(true)
|
|
36
59
|
try {
|
|
37
60
|
const response = await apiClient.api.admin.notifications.test.$post({
|
|
@@ -39,102 +62,85 @@ export const DashboardPage: React.FC = () => {
|
|
|
39
62
|
})
|
|
40
63
|
const result = await response.json()
|
|
41
64
|
if (result.success) {
|
|
42
|
-
message.success(
|
|
65
|
+
message.success(t('dashboard.sent', { type: notificationType }))
|
|
43
66
|
} else {
|
|
44
|
-
message.error('
|
|
67
|
+
message.error(t('dashboard.sendFailed'))
|
|
45
68
|
}
|
|
46
|
-
} catch
|
|
47
|
-
|
|
48
|
-
message.error('发送失败')
|
|
69
|
+
} catch {
|
|
70
|
+
message.error(t('dashboard.sendFailed'))
|
|
49
71
|
} finally {
|
|
50
72
|
setSendingNotification(false)
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
|
|
54
|
-
if (loading) {
|
|
55
|
-
return (
|
|
56
|
-
<div className="flex items-center justify-center h-64">
|
|
57
|
-
<div className="text-gray-500">Loading...</div>
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const statCards = [
|
|
63
|
-
{
|
|
64
|
-
title: 'Total Todos',
|
|
65
|
-
value: stats?.totalTodos || 0,
|
|
66
|
-
icon: CheckCircle,
|
|
67
|
-
color: 'bg-blue-500',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
title: 'Pending',
|
|
71
|
-
value: stats?.pendingTodos || 0,
|
|
72
|
-
icon: Clock,
|
|
73
|
-
color: 'bg-yellow-500',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
title: 'Completed',
|
|
77
|
-
value: stats?.completedTodos || 0,
|
|
78
|
-
icon: TrendingUp,
|
|
79
|
-
color: 'bg-green-500',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
title: 'Last Updated',
|
|
83
|
-
value: stats?.lastUpdated || '-',
|
|
84
|
-
icon: Activity,
|
|
85
|
-
color: 'bg-purple-500',
|
|
86
|
-
},
|
|
87
|
-
]
|
|
88
|
-
|
|
89
76
|
return (
|
|
90
77
|
<div>
|
|
91
|
-
<h1
|
|
78
|
+
<h1
|
|
79
|
+
className="text-2xl font-bold mb-4"
|
|
80
|
+
style={{ color: token.colorText }}
|
|
81
|
+
>
|
|
82
|
+
{t('dashboard.title')}
|
|
83
|
+
</h1>
|
|
92
84
|
|
|
93
|
-
<div className="grid grid-cols-
|
|
85
|
+
<div className="grid grid-cols-4 gap-4 mb-6">
|
|
94
86
|
{statCards.map((card, index) => {
|
|
95
87
|
const Icon = card.icon
|
|
96
88
|
return (
|
|
97
|
-
<div
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
<div
|
|
90
|
+
key={index}
|
|
91
|
+
className="rounded-lg p-6"
|
|
92
|
+
style={{
|
|
93
|
+
backgroundColor: token.colorBgContainer,
|
|
94
|
+
boxShadow: `0 1px 3px ${token.colorBorderSecondary}`,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<div className="flex items-center justify-between mb-2">
|
|
98
|
+
<div className={`p-3 rounded-md ${card.color} w-10 h-10 flex items-center justify-center`}>
|
|
100
99
|
<Icon className="w-6 h-6 text-white" />
|
|
101
100
|
</div>
|
|
102
101
|
</div>
|
|
103
|
-
<h3
|
|
104
|
-
|
|
102
|
+
<h3
|
|
103
|
+
className="text-sm font-medium"
|
|
104
|
+
style={{ color: token.colorTextSecondary }}
|
|
105
|
+
>
|
|
106
|
+
{card.title}
|
|
107
|
+
</h3>
|
|
108
|
+
<p
|
|
109
|
+
className="text-2xl font-bold mt-1"
|
|
110
|
+
style={{ color: token.colorText }}
|
|
111
|
+
>
|
|
112
|
+
{card.value}
|
|
113
|
+
</p>
|
|
105
114
|
</div>
|
|
106
115
|
)
|
|
107
116
|
})}
|
|
108
117
|
</div>
|
|
109
118
|
|
|
110
|
-
<div
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
<div
|
|
120
|
+
className="rounded-lg p-6"
|
|
121
|
+
style={{
|
|
122
|
+
backgroundColor: token.colorBgContainer,
|
|
123
|
+
boxShadow: `0 1px 3px ${token.colorBorderSecondary}`,
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<div className="flex items-center gap-2 mb-4">
|
|
127
|
+
<BellRing className="w-5 h-5" style={{ color: token.colorTextSecondary }} />
|
|
128
|
+
<h2 className="text-base font-semibold" style={{ color: token.colorText }}>
|
|
129
|
+
{t('dashboard.testNotification')}
|
|
130
|
+
</h2>
|
|
114
131
|
</div>
|
|
115
|
-
<p className="text-sm
|
|
116
|
-
|
|
117
|
-
notification,info 和 success 类型只会出现在铃铛列表中。
|
|
132
|
+
<p className="text-sm mb-4" style={{ color: token.colorTextSecondary }}>
|
|
133
|
+
{t('dashboard.testNotificationDesc')}
|
|
118
134
|
</p>
|
|
119
|
-
<div className="flex
|
|
135
|
+
<div className="flex gap-2">
|
|
120
136
|
<Select
|
|
121
137
|
value={notificationType}
|
|
122
138
|
onChange={setNotificationType}
|
|
123
139
|
style={{ width: 120 }}
|
|
124
|
-
options={
|
|
125
|
-
{ value: 'info', label: 'ℹ️ Info' },
|
|
126
|
-
{ value: 'success', label: '✅ Success' },
|
|
127
|
-
{ value: 'warning', label: '⚠️ Warning' },
|
|
128
|
-
{ value: 'error', label: '❌ Error' },
|
|
129
|
-
]}
|
|
140
|
+
options={notificationTypeOptions}
|
|
130
141
|
/>
|
|
131
|
-
<Button
|
|
132
|
-
|
|
133
|
-
icon={<Bell className="w-4 h-4" />}
|
|
134
|
-
loading={sendingNotification}
|
|
135
|
-
onClick={handleSendTestNotification}
|
|
136
|
-
>
|
|
137
|
-
发送测试通知
|
|
142
|
+
<Button type="primary" loading={sendingNotification} onClick={handleSendTest}>
|
|
143
|
+
{t('dashboard.sendTest')}
|
|
138
144
|
</Button>
|
|
139
145
|
</div>
|
|
140
146
|
</div>
|