更新静态文件处理逻辑,新增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"
}
// 构建文件路径
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 开头

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) {
// 规范化路径,处理尾斜杠问题
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)

View File

@ -26,7 +26,7 @@ export default function AdminPage() {
const createEndpoint = async (endpointData: Partial<APIEndpoint>) => {
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<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 (
<EndpointsTab
endpoints={endpoints}
onCreateEndpoint={createEndpoint}
onUpdateEndpoint={updateEndpoint}
onUpdateEndpoints={loadEndpoints}
/>
)

View File

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

View File

@ -34,13 +34,15 @@ import { GripVertical } from 'lucide-react'
interface EndpointsTabProps {
endpoints: APIEndpoint[]
onCreateEndpoint: (data: Partial<APIEndpoint>) => void
onUpdateEndpoint: (id: number, data: Partial<APIEndpoint>) => 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,6 +98,14 @@ function SortableTableRow({ endpoint, onManageDataSources }: {
{new Date(endpoint.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
onClick={() => onEditEndpoint(endpoint)}
variant="outline"
size="sm"
>
</Button>
<Button
onClick={() => onManageDataSources(endpoint)}
variant="outline"
@ -103,13 +113,16 @@ function SortableTableRow({ endpoint, onManageDataSources }: {
>
</Button>
</div>
</TableCell>
</TableRow>
)
}
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<APIEndpoint | null>(null)
const [selectedEndpoint, setSelectedEndpoint] = useState<APIEndpoint | null>(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
</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">
<DndContext
sensors={sensors}
@ -302,6 +409,7 @@ export default function EndpointsTab({ endpoints, onCreateEndpoint, onUpdateEndp
key={endpoint.id}
endpoint={endpoint}
onManageDataSources={handleManageDataSources}
onEditEndpoint={handleEditEndpoint}
/>
))}
</SortableContext>

View File

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