@samanhappy/mcphub 0.0.7 → 0.0.9

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 (96) hide show
  1. package/package.json +6 -3
  2. package/.env.example +0 -2
  3. package/.eslintrc.json +0 -25
  4. package/.github/workflows/build.yml +0 -51
  5. package/.github/workflows/release.yml +0 -19
  6. package/.prettierrc +0 -7
  7. package/Dockerfile +0 -51
  8. package/assets/amap-edit.png +0 -0
  9. package/assets/amap-result.png +0 -0
  10. package/assets/cherry-mcp.png +0 -0
  11. package/assets/cursor-mcp.png +0 -0
  12. package/assets/cursor-query.png +0 -0
  13. package/assets/cursor-tools.png +0 -0
  14. package/assets/dashboard.png +0 -0
  15. package/assets/dashboard.zh.png +0 -0
  16. package/assets/group.png +0 -0
  17. package/assets/group.zh.png +0 -0
  18. package/assets/market.zh.png +0 -0
  19. package/assets/wegroup.jpg +0 -0
  20. package/assets/wegroup.png +0 -0
  21. package/assets/wexin.png +0 -0
  22. package/doc/intro.md +0 -73
  23. package/doc/intro2.md +0 -232
  24. package/entrypoint.sh +0 -10
  25. package/frontend/favicon.ico +0 -0
  26. package/frontend/index.html +0 -13
  27. package/frontend/postcss.config.js +0 -6
  28. package/frontend/src/App.tsx +0 -44
  29. package/frontend/src/components/AddGroupForm.tsx +0 -132
  30. package/frontend/src/components/AddServerForm.tsx +0 -90
  31. package/frontend/src/components/ChangePasswordForm.tsx +0 -158
  32. package/frontend/src/components/EditGroupForm.tsx +0 -149
  33. package/frontend/src/components/EditServerForm.tsx +0 -76
  34. package/frontend/src/components/GroupCard.tsx +0 -143
  35. package/frontend/src/components/MarketServerCard.tsx +0 -153
  36. package/frontend/src/components/MarketServerDetail.tsx +0 -297
  37. package/frontend/src/components/ProtectedRoute.tsx +0 -27
  38. package/frontend/src/components/ServerCard.tsx +0 -230
  39. package/frontend/src/components/ServerForm.tsx +0 -276
  40. package/frontend/src/components/icons/LucideIcons.tsx +0 -14
  41. package/frontend/src/components/layout/Content.tsx +0 -17
  42. package/frontend/src/components/layout/Header.tsx +0 -61
  43. package/frontend/src/components/layout/Sidebar.tsx +0 -98
  44. package/frontend/src/components/ui/Badge.tsx +0 -33
  45. package/frontend/src/components/ui/Button.tsx +0 -0
  46. package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
  47. package/frontend/src/components/ui/Pagination.tsx +0 -128
  48. package/frontend/src/components/ui/Toast.tsx +0 -96
  49. package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
  50. package/frontend/src/components/ui/ToolCard.tsx +0 -38
  51. package/frontend/src/contexts/AuthContext.tsx +0 -159
  52. package/frontend/src/contexts/ToastContext.tsx +0 -60
  53. package/frontend/src/hooks/useGroupData.ts +0 -232
  54. package/frontend/src/hooks/useMarketData.ts +0 -410
  55. package/frontend/src/hooks/useServerData.ts +0 -306
  56. package/frontend/src/hooks/useSettingsData.ts +0 -131
  57. package/frontend/src/i18n.ts +0 -42
  58. package/frontend/src/index.css +0 -20
  59. package/frontend/src/layouts/MainLayout.tsx +0 -33
  60. package/frontend/src/locales/en.json +0 -214
  61. package/frontend/src/locales/zh.json +0 -214
  62. package/frontend/src/main.tsx +0 -12
  63. package/frontend/src/pages/Dashboard.tsx +0 -206
  64. package/frontend/src/pages/GroupsPage.tsx +0 -116
  65. package/frontend/src/pages/LoginPage.tsx +0 -104
  66. package/frontend/src/pages/MarketPage.tsx +0 -356
  67. package/frontend/src/pages/ServersPage.tsx +0 -144
  68. package/frontend/src/pages/SettingsPage.tsx +0 -149
  69. package/frontend/src/services/authService.ts +0 -141
  70. package/frontend/src/types/index.ts +0 -160
  71. package/frontend/src/utils/cn.ts +0 -10
  72. package/frontend/tsconfig.json +0 -31
  73. package/frontend/tsconfig.node.json +0 -10
  74. package/frontend/vite.config.ts +0 -26
  75. package/googled76ca578b6543fbc.html +0 -1
  76. package/jest.config.js +0 -10
  77. package/mcp_settings.json +0 -45
  78. package/servers.json +0 -74722
  79. package/src/config/index.ts +0 -46
  80. package/src/controllers/authController.ts +0 -179
  81. package/src/controllers/groupController.ts +0 -341
  82. package/src/controllers/marketController.ts +0 -154
  83. package/src/controllers/serverController.ts +0 -303
  84. package/src/index.ts +0 -18
  85. package/src/middlewares/auth.ts +0 -28
  86. package/src/middlewares/index.ts +0 -43
  87. package/src/models/User.ts +0 -103
  88. package/src/routes/index.ts +0 -96
  89. package/src/server.ts +0 -72
  90. package/src/services/groupService.ts +0 -232
  91. package/src/services/marketService.ts +0 -116
  92. package/src/services/mcpService.ts +0 -385
  93. package/src/services/sseService.ts +0 -119
  94. package/src/types/index.ts +0 -129
  95. package/src/utils/migration.ts +0 -52
  96. package/tsconfig.json +0 -17
@@ -1,230 +0,0 @@
1
- import { useState, useRef, useEffect } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { Server } from '@/types'
4
- import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
5
- import Badge from '@/components/ui/Badge'
6
- import ToolCard from '@/components/ui/ToolCard'
7
- import DeleteDialog from '@/components/ui/DeleteDialog'
8
- import { useToast } from '@/contexts/ToastContext'
9
-
10
- interface ServerCardProps {
11
- server: Server
12
- onRemove: (serverName: string) => void
13
- onEdit: (server: Server) => void
14
- onToggle?: (server: Server, enabled: boolean) => void
15
- }
16
-
17
- const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {
18
- const { t } = useTranslation()
19
- const { showToast } = useToast()
20
- const [isExpanded, setIsExpanded] = useState(false)
21
- const [showDeleteDialog, setShowDeleteDialog] = useState(false)
22
- const [isToggling, setIsToggling] = useState(false)
23
- const [showErrorPopover, setShowErrorPopover] = useState(false)
24
- const [copied, setCopied] = useState(false)
25
- const errorPopoverRef = useRef<HTMLDivElement>(null)
26
-
27
- useEffect(() => {
28
- const handleClickOutside = (event: MouseEvent) => {
29
- if (errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node)) {
30
- setShowErrorPopover(false)
31
- }
32
- }
33
-
34
- document.addEventListener('mousedown', handleClickOutside)
35
- return () => {
36
- document.removeEventListener('mousedown', handleClickOutside)
37
- }
38
- }, [])
39
-
40
- const handleRemove = (e: React.MouseEvent) => {
41
- e.stopPropagation()
42
- setShowDeleteDialog(true)
43
- }
44
-
45
- const handleEdit = (e: React.MouseEvent) => {
46
- e.stopPropagation()
47
- onEdit(server)
48
- }
49
-
50
- const handleToggle = async (e: React.MouseEvent) => {
51
- e.stopPropagation()
52
- if (isToggling || !onToggle) return
53
-
54
- setIsToggling(true)
55
- try {
56
- await onToggle(server, !(server.enabled !== false))
57
- } finally {
58
- setIsToggling(false)
59
- }
60
- }
61
-
62
- const handleErrorIconClick = (e: React.MouseEvent) => {
63
- e.stopPropagation()
64
- setShowErrorPopover(!showErrorPopover)
65
- }
66
-
67
- const copyToClipboard = (e: React.MouseEvent) => {
68
- e.stopPropagation()
69
- if (!server.error) return
70
-
71
- if (navigator.clipboard && window.isSecureContext) {
72
- navigator.clipboard.writeText(server.error).then(() => {
73
- setCopied(true)
74
- showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
75
- setTimeout(() => setCopied(false), 2000)
76
- })
77
- } else {
78
- // Fallback for HTTP or unsupported clipboard API
79
- const textArea = document.createElement('textarea')
80
- textArea.value = server.error
81
- // Avoid scrolling to bottom
82
- textArea.style.position = 'fixed'
83
- textArea.style.left = '-9999px'
84
- document.body.appendChild(textArea)
85
- textArea.focus()
86
- textArea.select()
87
- try {
88
- document.execCommand('copy')
89
- setCopied(true)
90
- showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
91
- setTimeout(() => setCopied(false), 2000)
92
- } catch (err) {
93
- showToast(t('common.copyFailed') || 'Copy failed', 'error')
94
- console.error('Copy to clipboard failed:', err)
95
- }
96
- document.body.removeChild(textArea)
97
- }
98
- }
99
-
100
- const handleConfirmDelete = () => {
101
- onRemove(server.name)
102
- setShowDeleteDialog(false)
103
- }
104
-
105
- return (
106
- <>
107
- <div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
108
- <div
109
- className="flex justify-between items-center cursor-pointer"
110
- onClick={() => setIsExpanded(!isExpanded)}
111
- >
112
- <div className="flex items-center space-x-3">
113
- <h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
114
- <Badge status={server.status} />
115
-
116
- {server.error && (
117
- <div className="relative">
118
- <div
119
- className="cursor-pointer"
120
- onClick={handleErrorIconClick}
121
- aria-label={t('server.viewErrorDetails')}
122
- >
123
- <AlertCircle className="text-red-500 hover:text-red-600" size={18} />
124
- </div>
125
-
126
- {showErrorPopover && (
127
- <div
128
- ref={errorPopoverRef}
129
- className="absolute z-10 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-0 w-120"
130
- style={{
131
- left: '-231px',
132
- top: '24px',
133
- maxHeight: '300px',
134
- overflowY: 'auto',
135
- width: '480px',
136
- transform: 'translateX(50%)'
137
- }}
138
- onClick={(e) => e.stopPropagation()}
139
- >
140
- <div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
141
- <div className="flex items-center space-x-2">
142
- <h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
143
- <button
144
- onClick={copyToClipboard}
145
- className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
146
- title={t('common.copy')}
147
- >
148
- {copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
149
- </button>
150
- </div>
151
- <button
152
- onClick={(e) => {
153
- e.stopPropagation()
154
- setShowErrorPopover(false)
155
- }}
156
- className="text-gray-400 hover:text-gray-600"
157
- >
158
-
159
- </button>
160
- </div>
161
- <div className="p-4 pt-2">
162
- <pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">{server.error}</pre>
163
- </div>
164
- </div>
165
- )}
166
- </div>
167
- )}
168
- </div>
169
- <div className="flex space-x-2">
170
- <button
171
- onClick={handleEdit}
172
- className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
173
- >
174
- {t('server.edit')}
175
- </button>
176
- <div className="flex items-center">
177
- <button
178
- onClick={handleToggle}
179
- className={`px-3 py-1 text-sm rounded transition-colors ${
180
- isToggling
181
- ? 'bg-gray-200 text-gray-500'
182
- : server.enabled !== false
183
- ? 'bg-green-100 text-green-800 hover:bg-green-200'
184
- : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
185
- }`}
186
- disabled={isToggling}
187
- >
188
- {isToggling
189
- ? t('common.processing')
190
- : server.enabled !== false
191
- ? t('server.disable')
192
- : t('server.enable')
193
- }
194
- </button>
195
- </div>
196
- <button
197
- onClick={handleRemove}
198
- className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
199
- >
200
- {t('server.delete')}
201
- </button>
202
- <button className="text-gray-400 hover:text-gray-600">
203
- {isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
204
- </button>
205
- </div>
206
- </div>
207
-
208
- {isExpanded && server.tools && (
209
- <div className="mt-6">
210
- <h3 className={`text-lg font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h3>
211
- <div className="space-y-4">
212
- {server.tools.map((tool, index) => (
213
- <ToolCard key={index} tool={tool} />
214
- ))}
215
- </div>
216
- </div>
217
- )}
218
- </div>
219
-
220
- <DeleteDialog
221
- isOpen={showDeleteDialog}
222
- onClose={() => setShowDeleteDialog(false)}
223
- onConfirm={handleConfirmDelete}
224
- serverName={server.name}
225
- />
226
- </>
227
- )
228
- }
229
-
230
- export default ServerCard
@@ -1,276 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { Server, EnvVar, ServerFormData } from '@/types'
4
-
5
- interface ServerFormProps {
6
- onSubmit: (payload: any) => void
7
- onCancel: () => void
8
- initialData?: Server | null
9
- modalTitle: string
10
- formError?: string | null
11
- }
12
-
13
- const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
14
- const { t } = useTranslation()
15
- const [serverType, setServerType] = useState<'sse' | 'stdio'>(
16
- initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
17
- )
18
-
19
- const [formData, setFormData] = useState<ServerFormData>({
20
- name: (initialData && initialData.name) || '',
21
- url: (initialData && initialData.config && initialData.config.url) || '',
22
- command: (initialData && initialData.config && initialData.config.command) || '',
23
- arguments:
24
- initialData && initialData.config && initialData.config.args
25
- ? Array.isArray(initialData.config.args)
26
- ? initialData.config.args.join(' ')
27
- : String(initialData.config.args)
28
- : '',
29
- args: (initialData && initialData.config && initialData.config.args) || [],
30
- })
31
-
32
- const [envVars, setEnvVars] = useState<EnvVar[]>(
33
- initialData && initialData.config && initialData.config.env
34
- ? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
35
- : [],
36
- )
37
-
38
- const [error, setError] = useState<string | null>(null)
39
- const isEdit = !!initialData
40
-
41
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
42
- const { name, value } = e.target
43
- setFormData({ ...formData, [name]: value })
44
- }
45
-
46
- // Transform space-separated arguments string into array
47
- const handleArgsChange = (value: string) => {
48
- let args = value.split(' ').filter((arg) => arg.trim() !== '')
49
- setFormData({ ...formData, arguments: value, args })
50
- }
51
-
52
- const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
53
- const newEnvVars = [...envVars]
54
- newEnvVars[index][field] = value
55
- setEnvVars(newEnvVars)
56
- }
57
-
58
- const addEnvVar = () => {
59
- setEnvVars([...envVars, { key: '', value: '' }])
60
- }
61
-
62
- const removeEnvVar = (index: number) => {
63
- const newEnvVars = [...envVars]
64
- newEnvVars.splice(index, 1)
65
- setEnvVars(newEnvVars)
66
- }
67
-
68
- // Submit handler for server configuration
69
- const handleSubmit = async (e: React.FormEvent) => {
70
- e.preventDefault()
71
- setError(null)
72
-
73
- try {
74
- const env: Record<string, string> = {}
75
- envVars.forEach(({ key, value }) => {
76
- if (key.trim()) {
77
- env[key.trim()] = value
78
- }
79
- })
80
-
81
- const payload = {
82
- name: formData.name,
83
- config:
84
- serverType === 'sse'
85
- ? { url: formData.url }
86
- : {
87
- command: formData.command,
88
- args: formData.args,
89
- env: Object.keys(env).length > 0 ? env : undefined,
90
- },
91
- }
92
-
93
- onSubmit(payload)
94
- } catch (err) {
95
- setError(`Error: ${err instanceof Error ? err.message : String(err)}`)
96
- }
97
- }
98
-
99
- return (
100
- <div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
101
- <div className="flex justify-between items-center mb-4">
102
- <h2 className="text-xl font-semibold text-gray-900">{modalTitle}</h2>
103
- <button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
104
-
105
- </button>
106
- </div>
107
-
108
- {(error || formError) && (
109
- <div className="bg-red-50 text-red-700 p-3 rounded mb-4">
110
- {formError || error}
111
- </div>
112
- )}
113
-
114
- <form onSubmit={handleSubmit}>
115
- <div className="mb-4">
116
- <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
117
- {t('server.name')}
118
- </label>
119
- <input
120
- type="text"
121
- name="name"
122
- id="name"
123
- value={formData.name}
124
- onChange={handleInputChange}
125
- className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
126
- placeholder="e.g.: time-mcp"
127
- required
128
- disabled={isEdit}
129
- />
130
- </div>
131
-
132
- <div className="mb-4">
133
- <label className="block text-gray-700 text-sm font-bold mb-2">{t('server.type')}</label>
134
- <div className="flex space-x-4">
135
- <div>
136
- <input
137
- type="radio"
138
- id="command"
139
- name="serverType"
140
- value="command"
141
- checked={serverType === 'stdio'}
142
- onChange={() => setServerType('stdio')}
143
- className="mr-1"
144
- />
145
- <label htmlFor="command">stdio</label>
146
- </div>
147
- <div>
148
- <input
149
- type="radio"
150
- id="url"
151
- name="serverType"
152
- value="url"
153
- checked={serverType === 'sse'}
154
- onChange={() => setServerType('sse')}
155
- className="mr-1"
156
- />
157
- <label htmlFor="url">sse</label>
158
- </div>
159
- </div>
160
- </div>
161
-
162
- {serverType === 'sse' ? (
163
- <div className="mb-4">
164
- <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
165
- {t('server.url')}
166
- </label>
167
- <input
168
- type="url"
169
- name="url"
170
- id="url"
171
- value={formData.url}
172
- onChange={handleInputChange}
173
- className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
174
- placeholder="e.g.: http://localhost:3000/sse"
175
- required={serverType === 'sse'}
176
- />
177
- </div>
178
- ) : (
179
- <>
180
- <div className="mb-4">
181
- <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="command">
182
- {t('server.command')}
183
- </label>
184
- <input
185
- type="text"
186
- name="command"
187
- id="command"
188
- value={formData.command}
189
- onChange={handleInputChange}
190
- className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
191
- placeholder="e.g.: npx"
192
- required={serverType === 'stdio'}
193
- />
194
- </div>
195
- <div className="mb-4">
196
- <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="arguments">
197
- {t('server.arguments')}
198
- </label>
199
- <input
200
- type="text"
201
- name="arguments"
202
- id="arguments"
203
- value={formData.arguments}
204
- onChange={(e) => handleArgsChange(e.target.value)}
205
- className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
206
- placeholder="e.g.: -y time-mcp"
207
- required={serverType === 'stdio'}
208
- />
209
- </div>
210
-
211
- <div className="mb-4">
212
- <div className="flex justify-between items-center mb-2">
213
- <label className="block text-gray-700 text-sm font-bold">
214
- {t('server.envVars')}
215
- </label>
216
- <button
217
- type="button"
218
- onClick={addEnvVar}
219
- className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
220
- >
221
- + {t('server.add')}
222
- </button>
223
- </div>
224
- {envVars.map((envVar, index) => (
225
- <div key={index} className="flex items-center mb-2">
226
- <div className="flex items-center space-x-2 flex-grow">
227
- <input
228
- type="text"
229
- value={envVar.key}
230
- onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
231
- className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
232
- placeholder={t('server.key')}
233
- />
234
- <span className="flex items-center">:</span>
235
- <input
236
- type="text"
237
- value={envVar.value}
238
- onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
239
- className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
240
- placeholder={t('server.value')}
241
- />
242
- </div>
243
- <button
244
- type="button"
245
- onClick={() => removeEnvVar(index)}
246
- className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
247
- >
248
- - {t('server.remove')}
249
- </button>
250
- </div>
251
- ))}
252
- </div>
253
- </>
254
- )}
255
-
256
- <div className="flex justify-end mt-6">
257
- <button
258
- type="button"
259
- onClick={onCancel}
260
- className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
261
- >
262
- {t('server.cancel')}
263
- </button>
264
- <button
265
- type="submit"
266
- className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
267
- >
268
- {isEdit ? t('server.save') : t('server.add')}
269
- </button>
270
- </div>
271
- </form>
272
- </div>
273
- )
274
- }
275
-
276
- export default ServerForm
@@ -1,14 +0,0 @@
1
- import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check } from 'lucide-react'
2
-
3
- export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check }
4
-
5
- const LucideIcons = {
6
- ChevronDown,
7
- ChevronRight,
8
- Edit,
9
- Trash,
10
- Copy,
11
- Check
12
- }
13
-
14
- export default LucideIcons
@@ -1,17 +0,0 @@
1
- import React, { ReactNode } from 'react';
2
-
3
- interface ContentProps {
4
- children: ReactNode;
5
- }
6
-
7
- const Content: React.FC<ContentProps> = ({ children }) => {
8
- return (
9
- <main className="flex-1 p-6 overflow-auto">
10
- <div className="max-w-5xl mx-auto">
11
- {children}
12
- </div>
13
- </main>
14
- );
15
- };
16
-
17
- export default Content;
@@ -1,61 +0,0 @@
1
- import React from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { useNavigate } from 'react-router-dom';
4
- import { useAuth } from '@/contexts/AuthContext';
5
-
6
- interface HeaderProps {
7
- onToggleSidebar: () => void;
8
- }
9
-
10
- const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
11
- const { t } = useTranslation();
12
- const navigate = useNavigate();
13
- const { auth, logout } = useAuth();
14
-
15
- const handleLogout = () => {
16
- logout();
17
- navigate('/login');
18
- };
19
-
20
- return (
21
- <header className="bg-white shadow-sm z-10">
22
- <div className="flex justify-between items-center px-4 py-3">
23
- <div className="flex items-center">
24
- {/* 侧边栏切换按钮 */}
25
- <button
26
- onClick={onToggleSidebar}
27
- className="p-2 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-100 focus:outline-none"
28
- aria-label={t('app.toggleSidebar')}
29
- >
30
- <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
32
- </svg>
33
- </button>
34
-
35
- {/* 应用标题 */}
36
- <h1 className="ml-4 text-xl font-bold text-gray-900">{t('app.title')}</h1>
37
- </div>
38
-
39
- {/* 用户信息和操作 */}
40
- <div className="flex items-center space-x-4">
41
- {auth.user && (
42
- <span className="text-sm text-gray-700">
43
- {t('app.welcomeUser', { username: auth.user.username })}
44
- </span>
45
- )}
46
-
47
- <div className="flex space-x-2">
48
- <button
49
- onClick={handleLogout}
50
- className="px-3 py-1.5 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
51
- >
52
- {t('app.logout')}
53
- </button>
54
- </div>
55
- </div>
56
- </div>
57
- </header>
58
- );
59
- };
60
-
61
- export default Header;