create-fullstack-scaffold 0.4.11 → 0.4.12
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.map +1 -1
- package/package.json +1 -1
- package/template/src/admin/App.tsx +4 -1
- package/template/src/admin/components/NotificationDrawer.tsx +13 -42
- package/template/src/admin/components/ThemeToggle.tsx +2 -2
- package/template/src/admin/layouts/Header.tsx +12 -9
- package/template/src/admin/pages/ContentPage.tsx +8 -10
- package/template/src/admin/pages/DashboardPage.tsx +30 -17
- package/template/src/admin/pages/DisputesPage.tsx +20 -14
- package/template/src/admin/pages/LoginPage.tsx +14 -3
- package/template/src/admin/pages/OrdersPage.tsx +10 -6
- package/template/src/admin/pages/PermissionsPage.tsx +1 -3
- package/template/src/admin/pages/RegisterPage.tsx +6 -4
- package/template/src/admin/pages/RolesPage.tsx +10 -3
- package/template/src/admin/pages/SystemLogsPage.tsx +12 -4
- package/template/src/admin/pages/TicketsPage.tsx +15 -15
- package/template/src/admin/pages/UsersPage.tsx +10 -6
- package/template/src/admin/stores/themeStore.ts +1 -1
- package/template/src/cli/modules/admin/index.ts +8 -16
- package/template/src/cli/modules/content/index.ts +10 -2
- package/template/src/cli/modules/dispute/index.ts +8 -2
- package/template/src/cli/modules/file/index.ts +6 -1
- package/template/src/cli/modules/order/index.ts +16 -3
- package/template/src/cli/modules/permission/index.ts +7 -1
- package/template/src/cli/modules/ticket/index.ts +14 -3
- package/template/src/client/index.css +22 -24
package/package.json
CHANGED
|
@@ -80,7 +80,10 @@ export const App: React.FC<{ basePath?: string }> = ({ basePath = '/admin' }) =>
|
|
|
80
80
|
<Route path="/content" element={<ContentPage />} />
|
|
81
81
|
<Route path="/system/settings" element={<SettingsPage />} />
|
|
82
82
|
<Route path="/system/logs" element={<SystemLogsPage />} />
|
|
83
|
-
<Route
|
|
83
|
+
<Route
|
|
84
|
+
path="/system/monitor"
|
|
85
|
+
element={<ComingSoonPage title="System Monitor" />}
|
|
86
|
+
/>
|
|
84
87
|
<Route path="/system/permissions" element={<PermissionsPage />} />
|
|
85
88
|
<Route path="/system/roles" element={<RolesPage />} />
|
|
86
89
|
</Routes>
|
|
@@ -17,11 +17,7 @@ const typeConfig: Record<
|
|
|
17
17
|
NotificationType,
|
|
18
18
|
{
|
|
19
19
|
colorToken: 'colorInfo' | 'colorWarning' | 'colorError' | 'colorSuccess'
|
|
20
|
-
bgToken:
|
|
21
|
-
| 'colorInfoBg'
|
|
22
|
-
| 'colorWarningBg'
|
|
23
|
-
| 'colorErrorBg'
|
|
24
|
-
| 'colorSuccessBg'
|
|
20
|
+
bgToken: 'colorInfoBg' | 'colorWarningBg' | 'colorErrorBg' | 'colorSuccessBg'
|
|
25
21
|
icon: React.ReactNode
|
|
26
22
|
}
|
|
27
23
|
> = {
|
|
@@ -50,7 +46,7 @@ const typeConfig: Record<
|
|
|
50
46
|
function formatTime(
|
|
51
47
|
dateString: string,
|
|
52
48
|
locale: string,
|
|
53
|
-
t: (key: string, options?: Record<string, unknown>) => string
|
|
49
|
+
t: (key: string, options?: Record<string, unknown>) => string
|
|
54
50
|
): string {
|
|
55
51
|
const date = new Date(dateString)
|
|
56
52
|
const now = new Date()
|
|
@@ -90,15 +86,12 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
90
86
|
className="p-4 cursor-pointer transition-colors"
|
|
91
87
|
style={{
|
|
92
88
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
|
93
|
-
backgroundColor: !notif.read ? token.colorInfoBg : undefined,
|
|
89
|
+
backgroundColor: !notif.read ? (token.colorInfoBg ?? 'transparent') : undefined,
|
|
94
90
|
}}
|
|
95
91
|
onClick={() => !notif.read && onMarkAsRead(notif.id)}
|
|
96
|
-
onMouseEnter={e =>
|
|
97
|
-
(e.currentTarget.style.backgroundColor = token.colorBgTextHover)
|
|
98
|
-
}
|
|
92
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = token.colorBgTextHover ?? 'transparent')}
|
|
99
93
|
onMouseLeave={e =>
|
|
100
|
-
(e.currentTarget.style.backgroundColor =
|
|
101
|
-
!notif.read ? token.colorInfoBg : undefined)
|
|
94
|
+
(e.currentTarget.style.backgroundColor = !notif.read ? (token.colorInfoBg ?? 'transparent') : 'transparent')
|
|
102
95
|
}
|
|
103
96
|
>
|
|
104
97
|
<div className="flex items-start gap-3">
|
|
@@ -110,10 +103,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
110
103
|
</div>
|
|
111
104
|
<div className="flex-1 min-w-0">
|
|
112
105
|
<div className="flex items-center gap-2">
|
|
113
|
-
<span
|
|
114
|
-
className="font-medium truncate"
|
|
115
|
-
style={{ color: token.colorText }}
|
|
116
|
-
>
|
|
106
|
+
<span className="font-medium truncate" style={{ color: token.colorText }}>
|
|
117
107
|
{notif.title}
|
|
118
108
|
</span>
|
|
119
109
|
{!notif.read && (
|
|
@@ -123,16 +113,10 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
123
113
|
/>
|
|
124
114
|
)}
|
|
125
115
|
</div>
|
|
126
|
-
<p
|
|
127
|
-
className="text-sm mt-1 line-clamp-2"
|
|
128
|
-
style={{ color: token.colorTextSecondary }}
|
|
129
|
-
>
|
|
116
|
+
<p className="text-sm mt-1 line-clamp-2" style={{ color: token.colorTextSecondary }}>
|
|
130
117
|
{notif.message}
|
|
131
118
|
</p>
|
|
132
|
-
<span
|
|
133
|
-
className="text-xs mt-1 block"
|
|
134
|
-
style={{ color: token.colorTextTertiary }}
|
|
135
|
-
>
|
|
119
|
+
<span className="text-xs mt-1 block" style={{ color: token.colorTextTertiary }}>
|
|
136
120
|
{formatTime(notif.createdAt, currentLanguage, t)}
|
|
137
121
|
</span>
|
|
138
122
|
</div>
|
|
@@ -149,10 +133,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
149
133
|
key: 'all',
|
|
150
134
|
label: `${t('notification.all')} (${notifications.length})`,
|
|
151
135
|
children: (
|
|
152
|
-
<div
|
|
153
|
-
className="overflow-y-auto"
|
|
154
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
155
|
-
>
|
|
136
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
156
137
|
{loading ? (
|
|
157
138
|
<div className="flex items-center justify-center py-12">
|
|
158
139
|
<Spin />
|
|
@@ -173,10 +154,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
173
154
|
key: 'unread',
|
|
174
155
|
label: `${t('notification.unread')} (${unreadNotifications.length})`,
|
|
175
156
|
children: (
|
|
176
|
-
<div
|
|
177
|
-
className="overflow-y-auto"
|
|
178
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
179
|
-
>
|
|
157
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
180
158
|
{unreadNotifications.length === 0 ? (
|
|
181
159
|
<Empty
|
|
182
160
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
@@ -193,10 +171,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
193
171
|
key: 'read',
|
|
194
172
|
label: `${t('notification.read')} (${readNotifications.length})`,
|
|
195
173
|
children: (
|
|
196
|
-
<div
|
|
197
|
-
className="overflow-y-auto"
|
|
198
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
199
|
-
>
|
|
174
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
200
175
|
{readNotifications.length === 0 ? (
|
|
201
176
|
<Empty
|
|
202
177
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
@@ -255,12 +230,8 @@ export const NotificationBell: React.FC<{
|
|
|
255
230
|
className="p-2 rounded-lg transition-colors relative"
|
|
256
231
|
style={{ color: token.colorTextSecondary }}
|
|
257
232
|
onClick={onClick}
|
|
258
|
-
onMouseEnter={e =>
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
onMouseLeave={e =>
|
|
262
|
-
(e.currentTarget.style.backgroundColor = 'transparent')
|
|
263
|
-
}
|
|
233
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = token.colorBgTextHover)}
|
|
234
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
264
235
|
>
|
|
265
236
|
<Bell className="w-5 h-5" />
|
|
266
237
|
{unreadCount > 0 && (
|
|
@@ -13,10 +13,10 @@ export const ThemeToggle: React.FC = () => {
|
|
|
13
13
|
onClick={toggleTheme}
|
|
14
14
|
className="p-2 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
|
|
15
15
|
style={{ color: token.colorText }}
|
|
16
|
-
onMouseEnter={
|
|
16
|
+
onMouseEnter={e => {
|
|
17
17
|
e.currentTarget.style.backgroundColor = token.colorFillQuaternary
|
|
18
18
|
}}
|
|
19
|
-
onMouseLeave={
|
|
19
|
+
onMouseLeave={e => {
|
|
20
20
|
e.currentTarget.style.backgroundColor = 'transparent'
|
|
21
21
|
}}
|
|
22
22
|
title={mode === 'light' ? t('theme.toggleDark') : t('theme.toggleLight')}
|
|
@@ -37,11 +37,14 @@ export const Header: React.FC<HeaderProps> = ({ collapsed, onToggle }) => {
|
|
|
37
37
|
navigate('/login')
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const ROLE_OPTIONS = useMemo(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const ROLE_OPTIONS = useMemo(
|
|
41
|
+
() => [
|
|
42
|
+
{ label: t('header.superAdmin'), value: 'super_admin' },
|
|
43
|
+
{ label: t('header.moderator'), value: 'moderator' },
|
|
44
|
+
{ label: t('header.operator'), value: 'operator' },
|
|
45
|
+
],
|
|
46
|
+
[t]
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
const userMenuItems: MenuProps['items'] = [
|
|
47
50
|
{
|
|
@@ -83,10 +86,10 @@ export const Header: React.FC<HeaderProps> = ({ collapsed, onToggle }) => {
|
|
|
83
86
|
onClick={onToggle}
|
|
84
87
|
className="p-2 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
|
|
85
88
|
style={{ color: token.colorText }}
|
|
86
|
-
onMouseEnter={
|
|
89
|
+
onMouseEnter={e => {
|
|
87
90
|
e.currentTarget.style.backgroundColor = token.colorFillQuaternary
|
|
88
91
|
}}
|
|
89
|
-
onMouseLeave={
|
|
92
|
+
onMouseLeave={e => {
|
|
90
93
|
e.currentTarget.style.backgroundColor = 'transparent'
|
|
91
94
|
}}
|
|
92
95
|
data-testid="toggle-sidebar-button"
|
|
@@ -117,10 +120,10 @@ export const Header: React.FC<HeaderProps> = ({ collapsed, onToggle }) => {
|
|
|
117
120
|
<button
|
|
118
121
|
className="flex items-center gap-2 p-1.5 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
|
|
119
122
|
style={{ color: token.colorText }}
|
|
120
|
-
onMouseEnter={
|
|
123
|
+
onMouseEnter={e => {
|
|
121
124
|
e.currentTarget.style.backgroundColor = token.colorFillQuaternary
|
|
122
125
|
}}
|
|
123
|
-
onMouseLeave={
|
|
126
|
+
onMouseLeave={e => {
|
|
124
127
|
e.currentTarget.style.backgroundColor = 'transparent'
|
|
125
128
|
}}
|
|
126
129
|
>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
import { Table, Card, Tag, Button, Space, Modal, Form, Input, Select, message } from 'antd'
|
|
3
3
|
import { Plus, Edit, Delete } from 'lucide-react'
|
|
4
4
|
import { PermissionGuard } from '../components/PermissionGuard'
|
|
@@ -41,11 +41,7 @@ export const ContentPage: React.FC = () => {
|
|
|
41
41
|
return map[status] || status
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
fetchContents()
|
|
46
|
-
}, [])
|
|
47
|
-
|
|
48
|
-
const fetchContents = async () => {
|
|
44
|
+
const fetchContents = useCallback(async () => {
|
|
49
45
|
setLoading(true)
|
|
50
46
|
try {
|
|
51
47
|
const response = await apiClient.api.contents.$get({ query: {} })
|
|
@@ -60,7 +56,11 @@ export const ContentPage: React.FC = () => {
|
|
|
60
56
|
} finally {
|
|
61
57
|
setLoading(false)
|
|
62
58
|
}
|
|
63
|
-
}
|
|
59
|
+
}, [t])
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
fetchContents()
|
|
63
|
+
}, [fetchContents])
|
|
64
64
|
|
|
65
65
|
const handleCreate = () => {
|
|
66
66
|
setEditingContent(null)
|
|
@@ -142,9 +142,7 @@ export const ContentPage: React.FC = () => {
|
|
|
142
142
|
title: t('common.status'),
|
|
143
143
|
dataIndex: 'status',
|
|
144
144
|
key: 'status',
|
|
145
|
-
render: (status: string) => (
|
|
146
|
-
<Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
|
|
147
|
-
),
|
|
145
|
+
render: (status: string) => <Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>,
|
|
148
146
|
},
|
|
149
147
|
{
|
|
150
148
|
title: t('content.author'),
|
|
@@ -41,10 +41,30 @@ export const DashboardPage: React.FC = () => {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const statCards = [
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
{
|
|
45
|
+
title: t('dashboard.totalTodos'),
|
|
46
|
+
value: stats?.totalTodos || 0,
|
|
47
|
+
icon: CheckCircle,
|
|
48
|
+
color: 'bg-blue-500',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: t('dashboard.pending'),
|
|
52
|
+
value: stats?.pendingTodos || 0,
|
|
53
|
+
icon: Clock,
|
|
54
|
+
color: 'bg-yellow-500',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: t('dashboard.completed'),
|
|
58
|
+
value: stats?.completedTodos || 0,
|
|
59
|
+
icon: TrendingUp,
|
|
60
|
+
color: 'bg-green-500',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: t('dashboard.lastUpdated'),
|
|
64
|
+
value: stats?.lastUpdated || '-',
|
|
65
|
+
icon: Activity,
|
|
66
|
+
color: 'bg-purple-500',
|
|
67
|
+
},
|
|
48
68
|
]
|
|
49
69
|
|
|
50
70
|
const notificationTypeOptions: { value: NotificationType; label: string }[] = [
|
|
@@ -75,10 +95,7 @@ export const DashboardPage: React.FC = () => {
|
|
|
75
95
|
|
|
76
96
|
return (
|
|
77
97
|
<div>
|
|
78
|
-
<h1
|
|
79
|
-
className="text-2xl font-bold mb-4"
|
|
80
|
-
style={{ color: token.colorText }}
|
|
81
|
-
>
|
|
98
|
+
<h1 className="text-2xl font-bold mb-4" style={{ color: token.colorText }}>
|
|
82
99
|
{t('dashboard.title')}
|
|
83
100
|
</h1>
|
|
84
101
|
|
|
@@ -95,20 +112,16 @@ export const DashboardPage: React.FC = () => {
|
|
|
95
112
|
}}
|
|
96
113
|
>
|
|
97
114
|
<div className="flex items-center justify-between mb-2">
|
|
98
|
-
<div
|
|
115
|
+
<div
|
|
116
|
+
className={`p-3 rounded-md ${card.color} w-10 h-10 flex items-center justify-center`}
|
|
117
|
+
>
|
|
99
118
|
<Icon className="w-6 h-6 text-white" />
|
|
100
119
|
</div>
|
|
101
120
|
</div>
|
|
102
|
-
<h3
|
|
103
|
-
className="text-sm font-medium"
|
|
104
|
-
style={{ color: token.colorTextSecondary }}
|
|
105
|
-
>
|
|
121
|
+
<h3 className="text-sm font-medium" style={{ color: token.colorTextSecondary }}>
|
|
106
122
|
{card.title}
|
|
107
123
|
</h3>
|
|
108
|
-
<p
|
|
109
|
-
className="text-2xl font-bold mt-1"
|
|
110
|
-
style={{ color: token.colorText }}
|
|
111
|
-
>
|
|
124
|
+
<p className="text-2xl font-bold mt-1" style={{ color: token.colorText }}>
|
|
112
125
|
{card.value}
|
|
113
126
|
</p>
|
|
114
127
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
import { Table, Card, Tag, Button, Space, Modal, message, Descriptions } from 'antd'
|
|
3
3
|
import { Eye, AlertTriangle, CheckCircle } from 'lucide-react'
|
|
4
4
|
import { PermissionGuard } from '../components/PermissionGuard'
|
|
@@ -42,11 +42,7 @@ export const DisputesPage: React.FC = () => {
|
|
|
42
42
|
return map[status] || status
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
fetchDisputes()
|
|
47
|
-
}, [])
|
|
48
|
-
|
|
49
|
-
const fetchDisputes = async () => {
|
|
45
|
+
const fetchDisputes = useCallback(async () => {
|
|
50
46
|
setLoading(true)
|
|
51
47
|
try {
|
|
52
48
|
const response = await apiClient.api.disputes.$get({ query: {} })
|
|
@@ -61,7 +57,11 @@ export const DisputesPage: React.FC = () => {
|
|
|
61
57
|
} finally {
|
|
62
58
|
setLoading(false)
|
|
63
59
|
}
|
|
64
|
-
}
|
|
60
|
+
}, [t])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
fetchDisputes()
|
|
64
|
+
}, [fetchDisputes])
|
|
65
65
|
|
|
66
66
|
const handleResolve = (dispute: Dispute) => {
|
|
67
67
|
Modal.confirm({
|
|
@@ -138,9 +138,7 @@ export const DisputesPage: React.FC = () => {
|
|
|
138
138
|
title: t('common.status'),
|
|
139
139
|
dataIndex: 'status',
|
|
140
140
|
key: 'status',
|
|
141
|
-
render: (status: string) => (
|
|
142
|
-
<Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
|
|
143
|
-
),
|
|
141
|
+
render: (status: string) => <Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>,
|
|
144
142
|
},
|
|
145
143
|
{
|
|
146
144
|
title: t('common.createdAt'),
|
|
@@ -244,11 +242,15 @@ export const DisputesPage: React.FC = () => {
|
|
|
244
242
|
<Descriptions.Item label={t('disputes.disputeNo')}>
|
|
245
243
|
<code>{selectedDispute.disputeNo}</code>
|
|
246
244
|
</Descriptions.Item>
|
|
247
|
-
<Descriptions.Item label={t('disputes.orderNo')}>
|
|
245
|
+
<Descriptions.Item label={t('disputes.orderNo')}>
|
|
246
|
+
{selectedDispute.orderNo}
|
|
247
|
+
</Descriptions.Item>
|
|
248
248
|
<Descriptions.Item label={t('disputes.customer')}>
|
|
249
249
|
{selectedDispute.customerName} ({selectedDispute.customerEmail})
|
|
250
250
|
</Descriptions.Item>
|
|
251
|
-
<Descriptions.Item label={t('disputes.type')}>
|
|
251
|
+
<Descriptions.Item label={t('disputes.type')}>
|
|
252
|
+
{getTypeLabel(selectedDispute.type)}
|
|
253
|
+
</Descriptions.Item>
|
|
252
254
|
<Descriptions.Item label={t('disputes.amount')}>
|
|
253
255
|
<span className="text-lg font-bold text-red-600">
|
|
254
256
|
¥{selectedDispute.amount.toFixed(2)}
|
|
@@ -259,9 +261,13 @@ export const DisputesPage: React.FC = () => {
|
|
|
259
261
|
{getStatusLabel(selectedDispute.status)}
|
|
260
262
|
</Tag>
|
|
261
263
|
</Descriptions.Item>
|
|
262
|
-
<Descriptions.Item label={t('tickets.description')}>
|
|
264
|
+
<Descriptions.Item label={t('tickets.description')}>
|
|
265
|
+
{selectedDispute.description}
|
|
266
|
+
</Descriptions.Item>
|
|
263
267
|
{selectedDispute.resolution && (
|
|
264
|
-
<Descriptions.Item label={t('disputes.resolution')}>
|
|
268
|
+
<Descriptions.Item label={t('disputes.resolution')}>
|
|
269
|
+
{selectedDispute.resolution}
|
|
270
|
+
</Descriptions.Item>
|
|
265
271
|
)}
|
|
266
272
|
</Descriptions>
|
|
267
273
|
)}
|
|
@@ -68,8 +68,16 @@ export const LoginPage: React.FC = () => {
|
|
|
68
68
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
|
69
69
|
<Card className="w-full max-w-md" title={t('login.title')}>
|
|
70
70
|
<Spin spinning={loading}>
|
|
71
|
-
<Form
|
|
72
|
-
|
|
71
|
+
<Form
|
|
72
|
+
form={form}
|
|
73
|
+
onFinish={handleSubmit}
|
|
74
|
+
layout="vertical"
|
|
75
|
+
data-testid="admin-login-form"
|
|
76
|
+
>
|
|
77
|
+
<Form.Item
|
|
78
|
+
name="username"
|
|
79
|
+
rules={[{ required: true, message: t('login.usernameRequired') }]}
|
|
80
|
+
>
|
|
73
81
|
<Input
|
|
74
82
|
prefix={<UserOutlined />}
|
|
75
83
|
placeholder={t('login.username')}
|
|
@@ -77,7 +85,10 @@ export const LoginPage: React.FC = () => {
|
|
|
77
85
|
data-testid="admin-login-username"
|
|
78
86
|
/>
|
|
79
87
|
</Form.Item>
|
|
80
|
-
<Form.Item
|
|
88
|
+
<Form.Item
|
|
89
|
+
name="password"
|
|
90
|
+
rules={[{ required: true, message: t('login.passwordRequired') }]}
|
|
91
|
+
>
|
|
81
92
|
<Input.Password
|
|
82
93
|
prefix={<LockOutlined />}
|
|
83
94
|
placeholder={t('login.password')}
|
|
@@ -135,9 +135,7 @@ export const OrdersPage: React.FC = () => {
|
|
|
135
135
|
title: t('orders.status'),
|
|
136
136
|
dataIndex: 'status',
|
|
137
137
|
key: 'status',
|
|
138
|
-
render: (status: string) => (
|
|
139
|
-
<Tag color={STATUS_COLORS[status]}>{getLabel(status)}</Tag>
|
|
140
|
-
),
|
|
138
|
+
render: (status: string) => <Tag color={STATUS_COLORS[status]}>{getLabel(status)}</Tag>,
|
|
141
139
|
},
|
|
142
140
|
{
|
|
143
141
|
title: t('common.createdAt'),
|
|
@@ -282,9 +280,15 @@ export const OrdersPage: React.FC = () => {
|
|
|
282
280
|
<Descriptions.Item label={t('orders.orderNo')}>
|
|
283
281
|
<code>{selectedOrder.orderNo}</code>
|
|
284
282
|
</Descriptions.Item>
|
|
285
|
-
<Descriptions.Item label={t('orders.customerName')}>
|
|
286
|
-
|
|
287
|
-
|
|
283
|
+
<Descriptions.Item label={t('orders.customerName')}>
|
|
284
|
+
{selectedOrder.customerName}
|
|
285
|
+
</Descriptions.Item>
|
|
286
|
+
<Descriptions.Item label={t('orders.customerEmail')}>
|
|
287
|
+
{selectedOrder.customerEmail}
|
|
288
|
+
</Descriptions.Item>
|
|
289
|
+
<Descriptions.Item label={t('orders.productName')}>
|
|
290
|
+
{selectedOrder.productName}
|
|
291
|
+
</Descriptions.Item>
|
|
288
292
|
<Descriptions.Item label={t('orders.orderAmount')}>
|
|
289
293
|
<span className="text-lg font-bold text-green-600">
|
|
290
294
|
¥{selectedOrder.amount.toFixed(2)}
|
|
@@ -64,9 +64,7 @@ export const PermissionsPage: React.FC = () => {
|
|
|
64
64
|
<div className="grid grid-cols-3 gap-4">
|
|
65
65
|
{roles.map(role => (
|
|
66
66
|
<Card key={role.role} size="small">
|
|
67
|
-
<div className="text-lg font-semibold mb-2">
|
|
68
|
-
{roleLabels[role.role] || role.role}
|
|
69
|
-
</div>
|
|
67
|
+
<div className="text-lg font-semibold mb-2">{roleLabels[role.role] || role.role}</div>
|
|
70
68
|
<div className="text-sm text-gray-500">
|
|
71
69
|
{t('permissions.permissionCount', { count: role.permissions.length })}
|
|
72
70
|
</div>
|
|
@@ -59,11 +59,13 @@ export const RegisterPage: React.FC = () => {
|
|
|
59
59
|
</Form.Item>
|
|
60
60
|
<Form.Item
|
|
61
61
|
name="password"
|
|
62
|
-
rules={[
|
|
63
|
-
{ required: true, min: 6, message: t('register.passwordMin') },
|
|
64
|
-
]}
|
|
62
|
+
rules={[{ required: true, min: 6, message: t('register.passwordMin') }]}
|
|
65
63
|
>
|
|
66
|
-
<Input.Password
|
|
64
|
+
<Input.Password
|
|
65
|
+
prefix={<LockOutlined />}
|
|
66
|
+
placeholder={t('register.password')}
|
|
67
|
+
size="large"
|
|
68
|
+
/>
|
|
67
69
|
</Form.Item>
|
|
68
70
|
<Form.Item
|
|
69
71
|
name="confirmPassword"
|
|
@@ -175,7 +175,9 @@ export const RolesPage: React.FC = () => {
|
|
|
175
175
|
dataIndex: 'isSystem',
|
|
176
176
|
key: 'isSystem',
|
|
177
177
|
render: (isSystem: boolean) => (
|
|
178
|
-
<Tag color={isSystem ? 'blue' : 'default'}>
|
|
178
|
+
<Tag color={isSystem ? 'blue' : 'default'}>
|
|
179
|
+
{isSystem ? t('common.yes') : t('common.no')}
|
|
180
|
+
</Tag>
|
|
179
181
|
),
|
|
180
182
|
},
|
|
181
183
|
{
|
|
@@ -183,7 +185,9 @@ export const RolesPage: React.FC = () => {
|
|
|
183
185
|
dataIndex: 'isActive',
|
|
184
186
|
key: 'isActive',
|
|
185
187
|
render: (isActive: boolean) => (
|
|
186
|
-
<Tag color={isActive ? 'green' : 'red'}>
|
|
188
|
+
<Tag color={isActive ? 'green' : 'red'}>
|
|
189
|
+
{isActive ? t('common.enabled') : t('common.disabled')}
|
|
190
|
+
</Tag>
|
|
187
191
|
),
|
|
188
192
|
},
|
|
189
193
|
{
|
|
@@ -270,7 +274,10 @@ export const RolesPage: React.FC = () => {
|
|
|
270
274
|
|
|
271
275
|
{editingRole && (
|
|
272
276
|
<Form.Item name="isActive" label={t('common.status')} valuePropName="checked">
|
|
273
|
-
<Switch
|
|
277
|
+
<Switch
|
|
278
|
+
checkedChildren={t('common.enabled')}
|
|
279
|
+
unCheckedChildren={t('common.disabled')}
|
|
280
|
+
/>
|
|
274
281
|
</Form.Item>
|
|
275
282
|
)}
|
|
276
283
|
</Form>
|
|
@@ -192,7 +192,9 @@ export const SystemLogsPage: React.FC = () => {
|
|
|
192
192
|
{selectedLog && (
|
|
193
193
|
<Descriptions column={1} bordered>
|
|
194
194
|
<Descriptions.Item label={t('systemLogs.logId')}>{selectedLog.id}</Descriptions.Item>
|
|
195
|
-
<Descriptions.Item label={t('systemLogs.userId')}>
|
|
195
|
+
<Descriptions.Item label={t('systemLogs.userId')}>
|
|
196
|
+
{selectedLog.userId}
|
|
197
|
+
</Descriptions.Item>
|
|
196
198
|
<Descriptions.Item label={t('systemLogs.action')}>
|
|
197
199
|
<Tag color={ACTION_COLORS[selectedLog.action] || 'default'}>
|
|
198
200
|
{ACTION_LABELS[selectedLog.action] || selectedLog.action}
|
|
@@ -201,9 +203,15 @@ export const SystemLogsPage: React.FC = () => {
|
|
|
201
203
|
<Descriptions.Item label={t('systemLogs.resourceType')}>
|
|
202
204
|
{RESOURCE_LABELS[selectedLog.resourceType] || selectedLog.resourceType}
|
|
203
205
|
</Descriptions.Item>
|
|
204
|
-
<Descriptions.Item label={t('systemLogs.resourceId')}>
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
<Descriptions.Item label={t('systemLogs.resourceId')}>
|
|
207
|
+
{selectedLog.resourceId || '-'}
|
|
208
|
+
</Descriptions.Item>
|
|
209
|
+
<Descriptions.Item label={t('systemLogs.ipAddress')}>
|
|
210
|
+
{selectedLog.ipAddress || '-'}
|
|
211
|
+
</Descriptions.Item>
|
|
212
|
+
<Descriptions.Item label={t('systemLogs.userAgent')}>
|
|
213
|
+
{selectedLog.userAgent || '-'}
|
|
214
|
+
</Descriptions.Item>
|
|
207
215
|
<Descriptions.Item label={t('systemLogs.time')}>
|
|
208
216
|
{formatDate(selectedLog.createdAt)}
|
|
209
217
|
</Descriptions.Item>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
import { Table, Card, Tag, Button, Space, Modal, message, Descriptions } from 'antd'
|
|
3
3
|
import { Eye, MessageCircle, CheckCircle, AlertCircle, Clock } from 'lucide-react'
|
|
4
4
|
import { PermissionGuard } from '../components/PermissionGuard'
|
|
@@ -50,11 +50,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
50
50
|
return map[status] || status
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
fetchTickets()
|
|
55
|
-
}, [])
|
|
56
|
-
|
|
57
|
-
const fetchTickets = async () => {
|
|
53
|
+
const fetchTickets = useCallback(async () => {
|
|
58
54
|
setLoading(true)
|
|
59
55
|
try {
|
|
60
56
|
const response = await apiClient.api.tickets.$get({ query: {} })
|
|
@@ -69,7 +65,11 @@ export const TicketsPage: React.FC = () => {
|
|
|
69
65
|
} finally {
|
|
70
66
|
setLoading(false)
|
|
71
67
|
}
|
|
72
|
-
}
|
|
68
|
+
}, [t])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
fetchTickets()
|
|
72
|
+
}, [fetchTickets])
|
|
73
73
|
|
|
74
74
|
const handleClose = async (ticketId: string) => {
|
|
75
75
|
Modal.confirm({
|
|
@@ -131,9 +131,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
131
131
|
title: t('common.status'),
|
|
132
132
|
dataIndex: 'status',
|
|
133
133
|
key: 'status',
|
|
134
|
-
render: (status: string) => (
|
|
135
|
-
<Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>
|
|
136
|
-
),
|
|
134
|
+
render: (status: string) => <Tag color={STATUS_COLORS[status]}>{getStatusLabel(status)}</Tag>,
|
|
137
135
|
},
|
|
138
136
|
{
|
|
139
137
|
title: t('common.createdAt'),
|
|
@@ -257,8 +255,12 @@ export const TicketsPage: React.FC = () => {
|
|
|
257
255
|
{getStatusLabel(selectedTicket.status)}
|
|
258
256
|
</Tag>
|
|
259
257
|
</Descriptions.Item>
|
|
260
|
-
<Descriptions.Item label={t('tickets.customerName')}>
|
|
261
|
-
|
|
258
|
+
<Descriptions.Item label={t('tickets.customerName')}>
|
|
259
|
+
{selectedTicket.customerName}
|
|
260
|
+
</Descriptions.Item>
|
|
261
|
+
<Descriptions.Item label={t('tickets.customerEmail')}>
|
|
262
|
+
{selectedTicket.customerEmail}
|
|
263
|
+
</Descriptions.Item>
|
|
262
264
|
<Descriptions.Item label={t('tickets.priority')}>
|
|
263
265
|
<Tag color={PRIORITY_COLORS[selectedTicket.priority]}>
|
|
264
266
|
{getPriorityLabel(selectedTicket.priority)}
|
|
@@ -285,9 +287,7 @@ export const TicketsPage: React.FC = () => {
|
|
|
285
287
|
>
|
|
286
288
|
<div className="flex justify-between items-center mb-1">
|
|
287
289
|
<span className="font-medium">{reply.author}</span>
|
|
288
|
-
<span className="text-sm text-gray-500">
|
|
289
|
-
{formatDate(reply.createdAt)}
|
|
290
|
-
</span>
|
|
290
|
+
<span className="text-sm text-gray-500">{formatDate(reply.createdAt)}</span>
|
|
291
291
|
</div>
|
|
292
292
|
<div className="text-gray-700">{reply.content}</div>
|
|
293
293
|
</div>
|