更新静态文件处理逻辑,新增resolveFilePath方法以处理Next.js静态导出的路由问题;在路由处理器中规范化路径以解决尾斜杠问题;禁用尾斜杠以避免路由冲突;在管理端点页面中添加编辑功能,支持更新端点信息。

This commit is contained in:
wood chen 2025-06-14 19:03:58 +08:00
parent 2c0073f266
commit 2394ef7f15
6 changed files with 196 additions and 14 deletions

View File

@ -27,8 +27,8 @@ func (s *StaticHandler) ServeStatic(w http.ResponseWriter, r *http.Request) {
path = "/index.html" path = "/index.html"
} }
// 构建文件路径 // 处理 Next.js 静态导出的路由问题
filePath := filepath.Join(s.staticDir, path) filePath := s.resolveFilePath(path)
// 检查文件是否存在 // 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) { 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) 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 判断是否是前端路由 // isFrontendRoute 判断是否是前端路由
func (s *StaticHandler) isFrontendRoute(path string) bool { func (s *StaticHandler) isFrontendRoute(path string) bool {
// 前端路由通常以 /admin 开头 // 前端路由通常以 /admin 开头

View File

@ -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) { 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) { if r.staticHandler != nil && r.shouldServeStatic(req.URL.Path) {
r.staticHandler.ServeStatic(w, req) r.staticHandler.ServeStatic(w, req)

View File

@ -26,7 +26,7 @@ export default function AdminPage() {
const createEndpoint = async (endpointData: Partial<APIEndpoint>) => { const createEndpoint = async (endpointData: Partial<APIEndpoint>) => {
try { try {
const response = await authenticatedFetch('/api/admin/endpoints/', { const response = await authenticatedFetch('/api/admin/endpoints', {
method: 'POST', method: 'POST',
body: JSON.stringify(endpointData), body: JSON.stringify(endpointData),
}) })
@ -42,10 +42,29 @@ export default function AdminPage() {
} }
} }
const updateEndpoint = async (id: number, endpointData: Partial<APIEndpoint>) => {
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 ( return (
<EndpointsTab <EndpointsTab
endpoints={endpoints} endpoints={endpoints}
onCreateEndpoint={createEndpoint} onCreateEndpoint={createEndpoint}
onUpdateEndpoint={updateEndpoint}
onUpdateEndpoints={loadEndpoints} onUpdateEndpoints={loadEndpoints}
/> />
) )

View File

@ -216,7 +216,7 @@ export default function Home() {
<div <div
className="min-h-screen bg-gray-100 dark:bg-gray-900 relative" className="min-h-screen bg-gray-100 dark:bg-gray-900 relative"
style={{ style={{
backgroundImage: 'url(http://localhost:5003/pic/all)', backgroundImage: 'url(/pic/all)',
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundAttachment: 'fixed' backgroundAttachment: 'fixed'

View File

@ -34,13 +34,15 @@ import { GripVertical } from 'lucide-react'
interface EndpointsTabProps { interface EndpointsTabProps {
endpoints: APIEndpoint[] endpoints: APIEndpoint[]
onCreateEndpoint: (data: Partial<APIEndpoint>) => void onCreateEndpoint: (data: Partial<APIEndpoint>) => void
onUpdateEndpoint: (id: number, data: Partial<APIEndpoint>) => void
onUpdateEndpoints: () => void onUpdateEndpoints: () => void
} }
// 可拖拽的表格行组件 // 可拖拽的表格行组件
function SortableTableRow({ endpoint, onManageDataSources }: { function SortableTableRow({ endpoint, onManageDataSources, onEditEndpoint }: {
endpoint: APIEndpoint endpoint: APIEndpoint
onManageDataSources: (endpoint: APIEndpoint) => void onManageDataSources: (endpoint: APIEndpoint) => void
onEditEndpoint: (endpoint: APIEndpoint) => void
}) { }) {
const { const {
attributes, attributes,
@ -96,20 +98,31 @@ function SortableTableRow({ endpoint, onManageDataSources }: {
{new Date(endpoint.created_at).toLocaleDateString()} {new Date(endpoint.created_at).toLocaleDateString()}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <div className="flex space-x-2">
onClick={() => onManageDataSources(endpoint)} <Button
variant="outline" onClick={() => onEditEndpoint(endpoint)}
size="sm" variant="outline"
> size="sm"
>
</Button>
</Button>
<Button
onClick={() => onManageDataSources(endpoint)}
variant="outline"
size="sm"
>
</Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) )
} }
export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndpoints }: EndpointsTabProps) { export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndpoint, onUpdateEndpoints }: EndpointsTabProps) {
const [showCreateForm, setShowCreateForm] = useState(false) const [showCreateForm, setShowCreateForm] = useState(false)
const [showEditForm, setShowEditForm] = useState(false)
const [editingEndpoint, setEditingEndpoint] = useState<APIEndpoint | null>(null)
const [selectedEndpoint, setSelectedEndpoint] = useState<APIEndpoint | null>(null) const [selectedEndpoint, setSelectedEndpoint] = useState<APIEndpoint | null>(null)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@ -133,6 +146,28 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp
setShowCreateForm(false) 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) => { const loadEndpointDataSources = async (endpointId: number) => {
try { try {
const response = await authenticatedFetch(`/api/admin/endpoints/${endpointId}/data-sources`) const response = await authenticatedFetch(`/api/admin/endpoints/${endpointId}/data-sources`)
@ -274,6 +309,78 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp
</div> </div>
)} )}
{showEditForm && editingEndpoint && (
<div className="bg-card rounded-lg border p-6 mb-6">
<h3 className="text-lg font-medium mb-4"></h3>
<form onSubmit={handleEditSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-name"></Label>
<Input
id="edit-name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-url">URL路径</Label>
<Input
id="edit-url"
type="text"
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="例如: pic/anime"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description"></Label>
<Textarea
id="edit-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
<div className="flex space-x-6">
<div className="flex items-center space-x-2">
<Switch
id="edit-is_active"
checked={formData.is_active}
onCheckedChange={(checked) => setFormData({ ...formData, is_active: checked })}
/>
<Label htmlFor="edit-is_active"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="edit-show_on_homepage"
checked={formData.show_on_homepage}
onCheckedChange={(checked) => setFormData({ ...formData, show_on_homepage: checked })}
/>
<Label htmlFor="edit-show_on_homepage"></Label>
</div>
</div>
<div className="flex space-x-3">
<Button type="submit">
</Button>
<Button
type="button"
onClick={() => {
setShowEditForm(false)
setEditingEndpoint(null)
setFormData({ name: '', url: '', description: '', is_active: true, show_on_homepage: true })
}}
variant="outline"
>
</Button>
</div>
</form>
</div>
)}
<div className="rounded-md border"> <div className="rounded-md border">
<DndContext <DndContext
sensors={sensors} sensors={sensors}
@ -302,6 +409,7 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp
key={endpoint.id} key={endpoint.id}
endpoint={endpoint} endpoint={endpoint}
onManageDataSources={handleManageDataSources} onManageDataSources={handleManageDataSources}
onEditEndpoint={handleEditEndpoint}
/> />
))} ))}
</SortableContext> </SortableContext>

View File

@ -3,7 +3,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
output: 'export', output: 'export',
trailingSlash: true, trailingSlash: false, // 禁用尾斜杠,避免路由问题
images: { images: {
unoptimized: true unoptimized: true
}, },