@samanhappy/mcphub 0.0.10 → 0.0.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.
Files changed (97) hide show
  1. package/package.json +8 -5
  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/bin/mcphub.js +0 -3
  23. package/doc/intro.md +0 -73
  24. package/doc/intro2.md +0 -232
  25. package/entrypoint.sh +0 -10
  26. package/frontend/favicon.ico +0 -0
  27. package/frontend/index.html +0 -13
  28. package/frontend/postcss.config.js +0 -6
  29. package/frontend/src/App.tsx +0 -44
  30. package/frontend/src/components/AddGroupForm.tsx +0 -132
  31. package/frontend/src/components/AddServerForm.tsx +0 -90
  32. package/frontend/src/components/ChangePasswordForm.tsx +0 -158
  33. package/frontend/src/components/EditGroupForm.tsx +0 -149
  34. package/frontend/src/components/EditServerForm.tsx +0 -76
  35. package/frontend/src/components/GroupCard.tsx +0 -143
  36. package/frontend/src/components/MarketServerCard.tsx +0 -153
  37. package/frontend/src/components/MarketServerDetail.tsx +0 -297
  38. package/frontend/src/components/ProtectedRoute.tsx +0 -27
  39. package/frontend/src/components/ServerCard.tsx +0 -230
  40. package/frontend/src/components/ServerForm.tsx +0 -276
  41. package/frontend/src/components/icons/LucideIcons.tsx +0 -14
  42. package/frontend/src/components/layout/Content.tsx +0 -17
  43. package/frontend/src/components/layout/Header.tsx +0 -61
  44. package/frontend/src/components/layout/Sidebar.tsx +0 -98
  45. package/frontend/src/components/ui/Badge.tsx +0 -33
  46. package/frontend/src/components/ui/Button.tsx +0 -0
  47. package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
  48. package/frontend/src/components/ui/Pagination.tsx +0 -128
  49. package/frontend/src/components/ui/Toast.tsx +0 -96
  50. package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
  51. package/frontend/src/components/ui/ToolCard.tsx +0 -38
  52. package/frontend/src/contexts/AuthContext.tsx +0 -159
  53. package/frontend/src/contexts/ToastContext.tsx +0 -60
  54. package/frontend/src/hooks/useGroupData.ts +0 -232
  55. package/frontend/src/hooks/useMarketData.ts +0 -410
  56. package/frontend/src/hooks/useServerData.ts +0 -306
  57. package/frontend/src/hooks/useSettingsData.ts +0 -131
  58. package/frontend/src/i18n.ts +0 -42
  59. package/frontend/src/index.css +0 -20
  60. package/frontend/src/layouts/MainLayout.tsx +0 -33
  61. package/frontend/src/locales/en.json +0 -214
  62. package/frontend/src/locales/zh.json +0 -214
  63. package/frontend/src/main.tsx +0 -12
  64. package/frontend/src/pages/Dashboard.tsx +0 -206
  65. package/frontend/src/pages/GroupsPage.tsx +0 -116
  66. package/frontend/src/pages/LoginPage.tsx +0 -104
  67. package/frontend/src/pages/MarketPage.tsx +0 -356
  68. package/frontend/src/pages/ServersPage.tsx +0 -144
  69. package/frontend/src/pages/SettingsPage.tsx +0 -149
  70. package/frontend/src/services/authService.ts +0 -141
  71. package/frontend/src/types/index.ts +0 -160
  72. package/frontend/src/utils/cn.ts +0 -10
  73. package/frontend/tsconfig.json +0 -31
  74. package/frontend/tsconfig.node.json +0 -10
  75. package/frontend/vite.config.ts +0 -26
  76. package/googled76ca578b6543fbc.html +0 -1
  77. package/jest.config.js +0 -10
  78. package/mcp_settings.json +0 -45
  79. package/servers.json +0 -74722
  80. package/src/config/index.ts +0 -46
  81. package/src/controllers/authController.ts +0 -179
  82. package/src/controllers/groupController.ts +0 -341
  83. package/src/controllers/marketController.ts +0 -154
  84. package/src/controllers/serverController.ts +0 -303
  85. package/src/index.ts +0 -17
  86. package/src/middlewares/auth.ts +0 -28
  87. package/src/middlewares/index.ts +0 -43
  88. package/src/models/User.ts +0 -103
  89. package/src/routes/index.ts +0 -96
  90. package/src/server.ts +0 -72
  91. package/src/services/groupService.ts +0 -232
  92. package/src/services/marketService.ts +0 -116
  93. package/src/services/mcpService.ts +0 -385
  94. package/src/services/sseService.ts +0 -119
  95. package/src/types/index.ts +0 -129
  96. package/src/utils/migration.ts +0 -52
  97. 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;