diff --git a/handlers/static_handler.go b/handlers/static_handler.go index dd26aa0..621a1cb 100644 --- a/handlers/static_handler.go +++ b/handlers/static_handler.go @@ -27,8 +27,8 @@ func (s *StaticHandler) ServeStatic(w http.ResponseWriter, r *http.Request) { path = "/index.html" } - // 构建文件路径 - filePath := filepath.Join(s.staticDir, path) + // 处理 Next.js 静态导出的路由问题 + filePath := s.resolveFilePath(path) // 检查文件是否存在 if _, err := os.Stat(filePath); os.IsNotExist(err) { @@ -50,6 +50,47 @@ func (s *StaticHandler) ServeStatic(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filePath) } +// resolveFilePath 解析文件路径,处理 Next.js 静态导出的路由问题 +func (s *StaticHandler) resolveFilePath(path string) string { + // 移除查询参数和锚点 + if idx := strings.Index(path, "?"); idx != -1 { + path = path[:idx] + } + if idx := strings.Index(path, "#"); idx != -1 { + path = path[:idx] + } + + // 构建初始文件路径 + filePath := filepath.Join(s.staticDir, path) + + // 如果路径以斜杠结尾,尝试查找 index.html + if strings.HasSuffix(path, "/") { + indexPath := filepath.Join(filePath, "index.html") + if _, err := os.Stat(indexPath); err == nil { + return indexPath + } + } else { + // 如果路径不以斜杠结尾,先检查是否存在对应的文件 + if _, err := os.Stat(filePath); err == nil { + return filePath + } + + // 如果文件不存在,尝试查找对应目录下的 index.html + indexPath := filepath.Join(filePath, "index.html") + if _, err := os.Stat(indexPath); err == nil { + return indexPath + } + + // 尝试添加 .html 扩展名 + htmlPath := filePath + ".html" + if _, err := os.Stat(htmlPath); err == nil { + return htmlPath + } + } + + return filePath +} + // isFrontendRoute 判断是否是前端路由 func (s *StaticHandler) isFrontendRoute(path string) bool { // 前端路由通常以 /admin 开头 diff --git a/router/router.go b/router/router.go index a1433f3..5c9476b 100644 --- a/router/router.go +++ b/router/router.go @@ -143,6 +143,20 @@ func (r *Router) HandleFunc(pattern string, handler func(http.ResponseWriter, *h } func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // 规范化路径,处理尾斜杠问题 + path := req.URL.Path + + // 对于前端路由,统一处理尾斜杠 + if strings.HasPrefix(path, "/admin") && path != "/admin" && strings.HasSuffix(path, "/") { + // 移除尾斜杠并重定向 + newPath := strings.TrimSuffix(path, "/") + if req.URL.RawQuery != "" { + newPath += "?" + req.URL.RawQuery + } + http.Redirect(w, req, newPath, http.StatusMovedPermanently) + return + } + // 首先检查是否是静态文件请求或前端路由 if r.staticHandler != nil && r.shouldServeStatic(req.URL.Path) { r.staticHandler.ServeStatic(w, req) diff --git a/web/app/admin/page.tsx b/web/app/admin/page.tsx index 86b9b69..cfd1fc6 100644 --- a/web/app/admin/page.tsx +++ b/web/app/admin/page.tsx @@ -26,7 +26,7 @@ export default function AdminPage() { const createEndpoint = async (endpointData: Partial) => { try { - const response = await authenticatedFetch('/api/admin/endpoints/', { + const response = await authenticatedFetch('/api/admin/endpoints', { method: 'POST', body: JSON.stringify(endpointData), }) @@ -42,10 +42,29 @@ export default function AdminPage() { } } + const updateEndpoint = async (id: number, endpointData: Partial) => { + try { + const response = await authenticatedFetch(`/api/admin/endpoints/${id}`, { + method: 'PUT', + body: JSON.stringify(endpointData), + }) + + if (response.ok) { + loadEndpoints() // 重新加载数据 + } else { + alert('更新端点失败') + } + } catch (error) { + console.error('Failed to update endpoint:', error) + alert('更新端点失败') + } + } + return ( ) diff --git a/web/app/page.tsx b/web/app/page.tsx index bdac866..d158928 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -216,7 +216,7 @@ export default function Home() {
) => void + onUpdateEndpoint: (id: number, data: Partial) => void onUpdateEndpoints: () => void } // 可拖拽的表格行组件 -function SortableTableRow({ endpoint, onManageDataSources }: { +function SortableTableRow({ endpoint, onManageDataSources, onEditEndpoint }: { endpoint: APIEndpoint onManageDataSources: (endpoint: APIEndpoint) => void + onEditEndpoint: (endpoint: APIEndpoint) => void }) { const { attributes, @@ -96,20 +98,31 @@ function SortableTableRow({ endpoint, onManageDataSources }: { {new Date(endpoint.created_at).toLocaleDateString()} - +
+ + +
) } -export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndpoints }: EndpointsTabProps) { +export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndpoint, onUpdateEndpoints }: EndpointsTabProps) { const [showCreateForm, setShowCreateForm] = useState(false) + const [showEditForm, setShowEditForm] = useState(false) + const [editingEndpoint, setEditingEndpoint] = useState(null) const [selectedEndpoint, setSelectedEndpoint] = useState(null) const [formData, setFormData] = useState({ name: '', @@ -133,6 +146,28 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp setShowCreateForm(false) } + const handleEditSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (editingEndpoint) { + onUpdateEndpoint(editingEndpoint.id, formData) + setFormData({ name: '', url: '', description: '', is_active: true, show_on_homepage: true }) + setShowEditForm(false) + setEditingEndpoint(null) + } + } + + const handleEditEndpoint = (endpoint: APIEndpoint) => { + setEditingEndpoint(endpoint) + setFormData({ + name: endpoint.name, + url: endpoint.url, + description: endpoint.description, + is_active: endpoint.is_active, + show_on_homepage: endpoint.show_on_homepage + }) + setShowEditForm(true) + } + const loadEndpointDataSources = async (endpointId: number) => { try { const response = await authenticatedFetch(`/api/admin/endpoints/${endpointId}/data-sources`) @@ -274,6 +309,78 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp
)} + {showEditForm && editingEndpoint && ( +
+

编辑端点

+
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, url: e.target.value })} + placeholder="例如: pic/anime" + required + /> +
+
+ +