mirror of
https://github.com/woodchen-ink/random-api-go.git
synced 2025-07-18 05:42:01 +08:00
更新静态文件处理逻辑,新增resolveFilePath方法以处理Next.js静态导出的路由问题;在路由处理器中规范化路径以解决尾斜杠问题;禁用尾斜杠以避免路由冲突;在管理端点页面中添加编辑功能,支持更新端点信息。
This commit is contained in:
parent
2c0073f266
commit
2394ef7f15
@ -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 开头
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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,20 +98,31 @@ function SortableTableRow({ endpoint, onManageDataSources }: {
|
||||
{new Date(endpoint.created_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
onClick={() => onManageDataSources(endpoint)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
管理数据源
|
||||
</Button>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={() => onEditEndpoint(endpoint)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onManageDataSources(endpoint)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
管理数据源
|
||||
</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>
|
||||
|
@ -3,7 +3,7 @@ import type { NextConfig } from "next";
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
trailingSlash: false, // 禁用尾斜杠,避免路由问题
|
||||
images: {
|
||||
unoptimized: true
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user