create-fullstack-scaffold 0.4.11 → 0.4.13
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 +15 -40
- 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 +28 -30
- 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/index.ts +3 -0
- package/template/src/cli/modules/merchant/index.ts +123 -0
- 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/template/vite.config.ts +5 -0
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,16 @@ 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
92
|
onMouseEnter={e =>
|
|
97
|
-
(e.currentTarget.style.backgroundColor = token.colorBgTextHover)
|
|
93
|
+
(e.currentTarget.style.backgroundColor = token.colorBgTextHover ?? 'transparent')
|
|
98
94
|
}
|
|
99
95
|
onMouseLeave={e =>
|
|
100
|
-
(e.currentTarget.style.backgroundColor =
|
|
101
|
-
|
|
96
|
+
(e.currentTarget.style.backgroundColor = !notif.read
|
|
97
|
+
? token.colorInfoBg ?? 'transparent'
|
|
98
|
+
: 'transparent')
|
|
102
99
|
}
|
|
103
100
|
>
|
|
104
101
|
<div className="flex items-start gap-3">
|
|
@@ -110,10 +107,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
110
107
|
</div>
|
|
111
108
|
<div className="flex-1 min-w-0">
|
|
112
109
|
<div className="flex items-center gap-2">
|
|
113
|
-
<span
|
|
114
|
-
className="font-medium truncate"
|
|
115
|
-
style={{ color: token.colorText }}
|
|
116
|
-
>
|
|
110
|
+
<span className="font-medium truncate" style={{ color: token.colorText }}>
|
|
117
111
|
{notif.title}
|
|
118
112
|
</span>
|
|
119
113
|
{!notif.read && (
|
|
@@ -123,16 +117,10 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
123
117
|
/>
|
|
124
118
|
)}
|
|
125
119
|
</div>
|
|
126
|
-
<p
|
|
127
|
-
className="text-sm mt-1 line-clamp-2"
|
|
128
|
-
style={{ color: token.colorTextSecondary }}
|
|
129
|
-
>
|
|
120
|
+
<p className="text-sm mt-1 line-clamp-2" style={{ color: token.colorTextSecondary }}>
|
|
130
121
|
{notif.message}
|
|
131
122
|
</p>
|
|
132
|
-
<span
|
|
133
|
-
className="text-xs mt-1 block"
|
|
134
|
-
style={{ color: token.colorTextTertiary }}
|
|
135
|
-
>
|
|
123
|
+
<span className="text-xs mt-1 block" style={{ color: token.colorTextTertiary }}>
|
|
136
124
|
{formatTime(notif.createdAt, currentLanguage, t)}
|
|
137
125
|
</span>
|
|
138
126
|
</div>
|
|
@@ -149,10 +137,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
149
137
|
key: 'all',
|
|
150
138
|
label: `${t('notification.all')} (${notifications.length})`,
|
|
151
139
|
children: (
|
|
152
|
-
<div
|
|
153
|
-
className="overflow-y-auto"
|
|
154
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
155
|
-
>
|
|
140
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
156
141
|
{loading ? (
|
|
157
142
|
<div className="flex items-center justify-center py-12">
|
|
158
143
|
<Spin />
|
|
@@ -173,10 +158,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
173
158
|
key: 'unread',
|
|
174
159
|
label: `${t('notification.unread')} (${unreadNotifications.length})`,
|
|
175
160
|
children: (
|
|
176
|
-
<div
|
|
177
|
-
className="overflow-y-auto"
|
|
178
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
179
|
-
>
|
|
161
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
180
162
|
{unreadNotifications.length === 0 ? (
|
|
181
163
|
<Empty
|
|
182
164
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
@@ -193,10 +175,7 @@ export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
|
193
175
|
key: 'read',
|
|
194
176
|
label: `${t('notification.read')} (${readNotifications.length})`,
|
|
195
177
|
children: (
|
|
196
|
-
<div
|
|
197
|
-
className="overflow-y-auto"
|
|
198
|
-
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
199
|
-
>
|
|
178
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
200
179
|
{readNotifications.length === 0 ? (
|
|
201
180
|
<Empty
|
|
202
181
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
@@ -255,12 +234,8 @@ export const NotificationBell: React.FC<{
|
|
|
255
234
|
className="p-2 rounded-lg transition-colors relative"
|
|
256
235
|
style={{ color: token.colorTextSecondary }}
|
|
257
236
|
onClick={onClick}
|
|
258
|
-
onMouseEnter={e =>
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
onMouseLeave={e =>
|
|
262
|
-
(e.currentTarget.style.backgroundColor = 'transparent')
|
|
263
|
-
}
|
|
237
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = token.colorBgTextHover)}
|
|
238
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
264
239
|
>
|
|
265
240
|
<Bell className="w-5 h-5" />
|
|
266
241
|
{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>
|