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"
|
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 开头
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user