删除固定路径代理配置, 因为普通代理好像已经支持了

This commit is contained in:
wood chen 2025-03-03 06:47:52 +08:00
parent f614692f33
commit 4af1592021
9 changed files with 24 additions and 542 deletions

View File

@ -76,7 +76,6 @@ func (c *configImpl) Update(newConfig *Config) {
// 更新配置
c.MAP = newConfig.MAP
c.Compression = newConfig.Compression
c.FixedPaths = newConfig.FixedPaths
// 触发回调
for _, callback := range c.onConfigUpdate {

View File

@ -8,7 +8,6 @@ import (
type Config struct {
MAP map[string]PathConfig `json:"MAP"` // 改为使用PathConfig
Compression CompressionConfig `json:"Compression"`
FixedPaths []FixedPathConfig `json:"FixedPaths"`
}
type PathConfig struct {
@ -30,19 +29,12 @@ type CompressorConfig struct {
Level int `json:"Level"`
}
type FixedPathConfig struct {
Path string `json:"Path"`
TargetHost string `json:"TargetHost"`
TargetURL string `json:"TargetURL"`
}
// 添加一个辅助方法来处理字符串到 PathConfig 的转换
func (c *Config) UnmarshalJSON(data []byte) error {
// 创建一个临时结构来解析原始JSON
type TempConfig struct {
MAP map[string]json.RawMessage `json:"MAP"`
Compression CompressionConfig `json:"Compression"`
FixedPaths []FixedPathConfig `json:"FixedPaths"`
}
var temp TempConfig
@ -77,7 +69,6 @@ func (c *Config) UnmarshalJSON(data []byte) error {
// 复制其他字段
c.Compression = temp.Compression
c.FixedPaths = temp.FixedPaths
return nil
}

View File

@ -9,14 +9,12 @@ import (
type CacheAdminHandler struct {
proxyCache *cache.CacheManager
mirrorCache *cache.CacheManager
fixedPathCache *cache.CacheManager
}
func NewCacheAdminHandler(proxyCache, mirrorCache, fixedPathCache *cache.CacheManager) *CacheAdminHandler {
func NewCacheAdminHandler(proxyCache, mirrorCache *cache.CacheManager) *CacheAdminHandler {
return &CacheAdminHandler{
proxyCache: proxyCache,
mirrorCache: mirrorCache,
fixedPathCache: fixedPathCache,
}
}
@ -37,7 +35,6 @@ func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request
stats := map[string]cache.CacheStats{
"proxy": h.proxyCache.GetStats(),
"mirror": h.mirrorCache.GetStats(),
"fixedPath": h.fixedPathCache.GetStats(),
}
w.Header().Set("Content-Type", "application/json")
@ -54,7 +51,6 @@ func (h *CacheAdminHandler) GetCacheConfig(w http.ResponseWriter, r *http.Reques
configs := map[string]cache.CacheConfig{
"proxy": h.proxyCache.GetConfig(),
"mirror": h.mirrorCache.GetConfig(),
"fixedPath": h.fixedPathCache.GetConfig(),
}
w.Header().Set("Content-Type", "application/json")
@ -69,7 +65,7 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
}
var req struct {
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
Type string `json:"type"` // "proxy", "mirror"
Config CacheConfig `json:"config"` // 新的配置
}
@ -84,8 +80,6 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
targetCache = h.proxyCache
case "mirror":
targetCache = h.mirrorCache
case "fixedPath":
targetCache = h.fixedPathCache
default:
http.Error(w, "Invalid cache type", http.StatusBadRequest)
return
@ -107,7 +101,7 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
}
var req struct {
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
Type string `json:"type"` // "proxy", "mirror"
Enabled bool `json:"enabled"` // true 或 false
}
@ -121,8 +115,6 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
h.proxyCache.SetEnabled(req.Enabled)
case "mirror":
h.mirrorCache.SetEnabled(req.Enabled)
case "fixedPath":
h.fixedPathCache.SetEnabled(req.Enabled)
default:
http.Error(w, "Invalid cache type", http.StatusBadRequest)
return
@ -139,7 +131,7 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
}
var req struct {
Type string `json:"type"` // "proxy", "mirror", "fixedPath" 或 "all"
Type string `json:"type"` // "proxy", "mirror" 或 "all"
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -153,16 +145,11 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
err = h.proxyCache.ClearCache()
case "mirror":
err = h.mirrorCache.ClearCache()
case "fixedPath":
err = h.fixedPathCache.ClearCache()
case "all":
err = h.proxyCache.ClearCache()
if err == nil {
err = h.mirrorCache.ClearCache()
}
if err == nil {
err = h.fixedPathCache.ClearCache()
}
default:
http.Error(w, "Invalid cache type", http.StatusBadRequest)
return

View File

@ -118,18 +118,5 @@ func (h *ConfigHandler) validateConfig(cfg *config.Config) error {
}
}
// 验证FixedPaths配置
for _, fp := range cfg.FixedPaths {
if fp.Path == "" {
return fmt.Errorf("固定路径不能为空")
}
if fp.TargetURL == "" {
return fmt.Errorf("固定路径 %s 的目标URL不能为空", fp.Path)
}
if _, err := url.Parse(fp.TargetURL); err != nil {
return fmt.Errorf("固定路径 %s 的目标URL无效: %v", fp.Path, err)
}
}
return nil
}

View File

@ -40,10 +40,6 @@ var hopHeadersBase = map[string]bool{
"Upgrade": true,
}
func init() {
// 移除旧的初始化代码,因为我们直接在 map 字面量中定义了所有值
}
// ErrorHandler 定义错误处理函数类型
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)

View File

@ -1,165 +0,0 @@
package middleware
import (
"errors"
"io"
"log"
"net/http"
"proxy-go/internal/cache"
"proxy-go/internal/config"
"proxy-go/internal/metrics"
"proxy-go/internal/utils"
"strings"
"syscall"
"time"
)
type FixedPathConfig struct {
Path string `json:"Path"`
TargetHost string `json:"TargetHost"`
TargetURL string `json:"TargetURL"`
}
var fixedPathCache *cache.CacheManager
func init() {
var err error
fixedPathCache, err = cache.NewCacheManager("data/fixed_path_cache")
if err != nil {
log.Printf("[Cache] Failed to initialize fixed path cache manager: %v", err)
}
}
func FixedPathProxyMiddleware(configs []config.FixedPathConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
collector := metrics.GetCollector()
collector.BeginRequest()
defer collector.EndRequest()
// 检查是否匹配任何固定路径
for _, cfg := range configs {
if strings.HasPrefix(r.URL.Path, cfg.Path) {
// 创建新的请求
targetPath := strings.TrimPrefix(r.URL.Path, cfg.Path)
targetURL := cfg.TargetURL + targetPath
// 检查是否可以使用缓存
if r.Method == http.MethodGet && fixedPathCache != nil {
cacheKey := fixedPathCache.GenerateCacheKey(r)
if item, hit, notModified := fixedPathCache.Get(cacheKey, r); hit {
// 从缓存提供响应
w.Header().Set("Content-Type", item.ContentType)
if item.ContentEncoding != "" {
w.Header().Set("Content-Encoding", item.ContentEncoding)
}
w.Header().Set("Proxy-Go-Cache", "HIT")
if notModified {
w.WriteHeader(http.StatusNotModified)
return
}
http.ServeFile(w, r, item.FilePath)
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, utils.GetClientIP(r), r)
return
}
}
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, "Error creating proxy request", http.StatusInternalServerError)
log.Printf("[Fixed] ERR %s %s -> 500 (%s) create request error from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
return
}
// 复制原始请求的 header
for key, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(key, value)
}
}
// 设置必要的头部
proxyReq.Host = cfg.TargetHost
proxyReq.Header.Set("Host", cfg.TargetHost)
proxyReq.Header.Set("X-Real-IP", utils.GetClientIP(r))
proxyReq.Header.Set("X-Scheme", r.URL.Scheme)
// 发送代理请求
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, "Error forwarding request", http.StatusBadGateway)
log.Printf("[Fixed] ERR %s %s -> 502 (%s) proxy error from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
return
}
defer resp.Body.Close()
// 复制响应头
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.Header().Set("Proxy-Go-Cache", "MISS")
// 设置响应状态码
w.WriteHeader(resp.StatusCode)
var written int64
// 如果是GET请求且响应成功使用TeeReader同时写入缓存
if r.Method == http.MethodGet && resp.StatusCode == http.StatusOK && fixedPathCache != nil {
cacheKey := fixedPathCache.GenerateCacheKey(r)
if cacheFile, err := fixedPathCache.CreateTemp(cacheKey, resp); err == nil {
defer cacheFile.Close()
teeReader := io.TeeReader(resp.Body, cacheFile)
written, err = io.Copy(w, teeReader)
if err == nil {
fixedPathCache.Commit(cacheKey, cacheFile.Name(), resp, written)
}
} else {
written, err = io.Copy(w, resp.Body)
if err != nil && !isConnectionClosed(err) {
log.Printf("[Fixed] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
}
}
} else {
written, err = io.Copy(w, resp.Body)
if err != nil && !isConnectionClosed(err) {
log.Printf("[Fixed] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
}
}
// 记录统计信息
collector.RecordRequest(r.URL.Path, resp.StatusCode, time.Since(startTime), written, utils.GetClientIP(r), r)
return
}
}
// 如果没有匹配的固定路径,继续下一个处理器
next.ServeHTTP(w, r)
})
}
}
func isConnectionClosed(err error) bool {
if err == nil {
return false
}
// 忽略常见的连接关闭错误
if errors.Is(err, syscall.EPIPE) || // broken pipe
errors.Is(err, syscall.ECONNRESET) || // connection reset by peer
strings.Contains(err.Error(), "broken pipe") ||
strings.Contains(err.Error(), "connection reset by peer") {
return true
}
return false
}
// GetFixedPathCache 获取固定路径缓存管理器
func GetFixedPathCache() *cache.CacheManager {
return fixedPathCache
}

23
main.go
View File

@ -40,7 +40,6 @@ func main() {
// 创建代理处理器
mirrorHandler := handler.NewMirrorProxyHandler()
proxyHandler := handler.NewProxyHandler(cfg)
fixedPathCache := middleware.GetFixedPathCache()
// 创建处理器链
handlers := []struct {
@ -88,16 +87,16 @@ func main() {
case "/admin/api/config/save":
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
case "/admin/api/cache/stats":
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheStats)(w, r)
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheStats)(w, r)
case "/admin/api/cache/enable":
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).SetCacheEnabled)(w, r)
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).SetCacheEnabled)(w, r)
case "/admin/api/cache/clear":
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).ClearCache)(w, r)
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).ClearCache)(w, r)
case "/admin/api/cache/config":
if r.Method == http.MethodGet {
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheConfig)(w, r)
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheConfig)(w, r)
} else if r.Method == http.MethodPost {
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).UpdateCacheConfig)(w, r)
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).UpdateCacheConfig)(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
@ -129,18 +128,6 @@ func main() {
},
handler: mirrorHandler,
},
// 固定路径处理器
{
matcher: func(r *http.Request) bool {
for _, fp := range cfg.FixedPaths {
if strings.HasPrefix(r.URL.Path, fp.Path) {
return true
}
}
return false
},
handler: middleware.FixedPathProxyMiddleware(cfg.FixedPaths)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})),
},
// 默认代理处理器
{
matcher: func(r *http.Request) bool {

View File

@ -28,13 +28,11 @@ interface CacheConfig {
interface CacheData {
proxy: CacheStats
mirror: CacheStats
fixedPath: CacheStats
}
interface CacheConfigs {
proxy: CacheConfig
mirror: CacheConfig
fixedPath: CacheConfig
}
function formatBytes(bytes: number) {
@ -133,7 +131,7 @@ export default function CachePage() {
return () => clearInterval(interval)
}, [fetchStats, fetchConfigs])
const handleToggleCache = async (type: "proxy" | "mirror" | "fixedPath", enabled: boolean) => {
const handleToggleCache = async (type: "proxy" | "mirror", enabled: boolean) => {
try {
const token = localStorage.getItem("token")
if (!token) {
@ -160,7 +158,7 @@ export default function CachePage() {
toast({
title: "成功",
description: `${type === "proxy" ? "代理" : type === "mirror" ? "镜像" : "固定路径"}缓存已${enabled ? "启用" : "禁用"}`,
description: `${type === "proxy" ? "代理" : "镜像"}缓存已${enabled ? "启用" : "禁用"}`,
})
fetchStats()
@ -173,7 +171,7 @@ export default function CachePage() {
}
}
const handleUpdateConfig = async (type: "proxy" | "mirror" | "fixedPath", config: CacheConfig) => {
const handleUpdateConfig = async (type: "proxy" | "mirror", config: CacheConfig) => {
try {
const token = localStorage.getItem("token")
if (!token) {
@ -213,7 +211,7 @@ export default function CachePage() {
}
}
const handleClearCache = async (type: "proxy" | "mirror" | "fixedPath" | "all") => {
const handleClearCache = async (type: "proxy" | "mirror" | "all") => {
try {
const token = localStorage.getItem("token")
if (!token) {
@ -253,7 +251,7 @@ export default function CachePage() {
}
}
const renderCacheConfig = (type: "proxy" | "mirror" | "fixedPath") => {
const renderCacheConfig = (type: "proxy" | "mirror" ) => {
if (!configs) return null
const config = configs[type]
@ -425,55 +423,6 @@ export default function CachePage() {
{renderCacheConfig("mirror")}
</CardContent>
</Card>
{/* 固定路径缓存 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle></CardTitle>
<div className="flex items-center space-x-2">
<Switch
checked={stats?.fixedPath.enabled ?? false}
onCheckedChange={(checked) => handleToggleCache("fixedPath", checked)}
/>
<Button
variant="outline"
size="sm"
onClick={() => handleClearCache("fixedPath")}
>
</Button>
</div>
</CardHeader>
<CardContent>
<dl className="space-y-2">
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{stats?.fixedPath.total_items ?? 0}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.total_size ?? 0)}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{stats?.fixedPath.hit_count ?? 0}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{stats?.fixedPath.miss_count ?? 0}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{(stats?.fixedPath.hit_rate ?? 0).toFixed(2)}%</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.bytes_saved ?? 0)}</dd>
</div>
</dl>
{renderCacheConfig("fixedPath")}
</CardContent>
</Card>
</div>
</div>
)

View File

@ -44,12 +44,6 @@ interface PathMapping {
MaxSize?: number // 最大文件大小阈值
}
interface FixedPath {
Path: string
TargetHost: string
TargetURL: string
}
interface CompressionConfig {
Enabled: boolean
Level: number
@ -61,7 +55,6 @@ interface Config {
Gzip: CompressionConfig
Brotli: CompressionConfig
}
FixedPaths: FixedPath[]
}
export default function ConfigPage() {
@ -85,13 +78,7 @@ export default function ConfigPage() {
sizeThresholdUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
maxSizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
})
const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false)
const [editingFixedPath, setEditingFixedPath] = useState<FixedPath | null>(null)
const [newFixedPath, setNewFixedPath] = useState<FixedPath>({
Path: "",
TargetHost: "",
TargetURL: "",
})
const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false)
const [editingPath, setEditingPath] = useState<string | null>(null)
const [editingExtension, setEditingExtension] = useState<{ext: string, target: string} | null>(null)
@ -107,7 +94,6 @@ export default function ConfigPage() {
} | null>(null);
const [deletingPath, setDeletingPath] = useState<string | null>(null)
const [deletingFixedPath, setDeletingFixedPath] = useState<FixedPath | null>(null)
const [deletingExtension, setDeletingExtension] = useState<{path: string, ext: string} | null>(null)
const fetchConfig = useCallback(async () => {
@ -231,20 +217,6 @@ export default function ConfigPage() {
})
}, [handleDialogOpenChange])
const handleFixedPathDialogOpenChange = useCallback((open: boolean) => {
handleDialogOpenChange(open, (isOpen) => {
setFixedPathDialogOpen(isOpen)
if (!isOpen) {
setEditingFixedPath(null)
setNewFixedPath({
Path: "",
TargetHost: "",
TargetURL: "",
})
}
})
}, [handleDialogOpenChange])
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
handleDialogOpenChange(open, (isOpen) => {
setExtensionMapDialogOpen(isOpen)
@ -453,95 +425,8 @@ export default function ConfigPage() {
})
}
const addFixedPath = () => {
if (!config) return
const { Path, TargetHost, TargetURL } = newFixedPath
// 验证输入
if (!Path.trim() || !TargetHost.trim() || !TargetURL.trim()) {
toast({
title: "错误",
description: "所有字段都不能为空",
variant: "destructive",
})
return
}
// 验证路径格式
if (!Path.startsWith('/')) {
toast({
title: "错误",
description: "路径必须以/开头",
variant: "destructive",
})
return
}
// 验证URL格式
try {
new URL(TargetURL)
} catch {
toast({
title: "错误",
description: "目标URL格式不正确",
variant: "destructive",
})
return
}
// 验证主机名格式
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/.test(TargetHost)) {
toast({
title: "错误",
description: "目标主机格式不正确",
variant: "destructive",
})
return
}
const newConfig = { ...config }
if (editingFixedPath) {
const index = newConfig.FixedPaths.findIndex(p => p.Path === editingFixedPath.Path)
if (index !== -1) {
newConfig.FixedPaths[index] = newFixedPath
}
} else {
// 检查路径是否已存在
if (newConfig.FixedPaths.some(p => p.Path === Path)) {
toast({
title: "错误",
description: "该路径已存在",
variant: "destructive",
})
return
}
newConfig.FixedPaths.push(newFixedPath)
}
setConfig(newConfig)
setFixedPathDialogOpen(false)
setEditingFixedPath(null)
setNewFixedPath({
Path: "",
TargetHost: "",
TargetURL: "",
})
toast({
title: "成功",
description: "固定路径已更新",
})
}
const editFixedPath = (path: FixedPath) => {
setEditingFixedPath(path)
setNewFixedPath({
Path: path.Path,
TargetHost: path.TargetHost,
TargetURL: path.TargetURL,
})
setFixedPathDialogOpen(true)
}
const openAddPathDialog = () => {
setEditingPathData(null)
@ -557,32 +442,6 @@ export default function ConfigPage() {
setPathDialogOpen(true)
}
const openAddFixedPathDialog = () => {
setEditingFixedPath(null)
setNewFixedPath({
Path: "",
TargetHost: "",
TargetURL: "",
})
setFixedPathDialogOpen(true)
}
const deleteFixedPath = (path: FixedPath) => {
setDeletingFixedPath(path)
}
const confirmDeleteFixedPath = () => {
if (!config || !deletingFixedPath) return
const newConfig = { ...config }
newConfig.FixedPaths = newConfig.FixedPaths.filter(p => p.Path !== deletingFixedPath.Path)
setConfig(newConfig)
setDeletingFixedPath(null)
toast({
title: "成功",
description: "固定路径已删除",
})
}
const exportConfig = () => {
if (!config) return
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
@ -618,10 +477,6 @@ export default function ConfigPage() {
throw new Error('配置文件压缩设置格式不正确')
}
if (!Array.isArray(newConfig.FixedPaths)) {
throw new Error('配置文件固定路径格式不正确')
}
// 验证路径映射
for (const [path, target] of Object.entries(newConfig.MAP)) {
if (!path.startsWith('/')) {
@ -1059,92 +914,6 @@ export default function ConfigPage() {
</CardContent>
</Card>
</TabsContent>
<TabsContent value="fixed-paths">
<div className="flex justify-end mb-4">
<Button onClick={openAddFixedPathDialog}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> URL</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{config?.FixedPaths.map((path, index) => (
<TableRow key={index}>
<TableCell>{path.Path}</TableCell>
<TableCell>{path.TargetHost}</TableCell>
<TableCell>{path.TargetURL}</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
variant="ghost"
size="icon"
onClick={() => editFixedPath(path)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteFixedPath(path)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Dialog open={fixedPathDialogOpen} onOpenChange={handleFixedPathDialogOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingFixedPath ? "编辑固定路径" : "添加固定路径"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
value={editingFixedPath ? editingFixedPath.Path : newFixedPath.Path}
onChange={(e) => setNewFixedPath({ ...newFixedPath, Path: e.target.value })}
placeholder="/example"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={editingFixedPath ? editingFixedPath.TargetHost : newFixedPath.TargetHost}
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetHost: e.target.value })}
placeholder="example.com"
/>
</div>
<div className="space-y-2">
<Label> URL</Label>
<Input
value={editingFixedPath ? editingFixedPath.TargetURL : newFixedPath.TargetURL}
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetURL: e.target.value })}
placeholder="https://example.com"
/>
</div>
<Button onClick={addFixedPath}>
{editingFixedPath ? "保存" : "添加"}
</Button>
</div>
</DialogContent>
</Dialog>
</TabsContent>
</Tabs>
</CardContent>
</Card>
@ -1204,24 +973,6 @@ export default function ConfigPage() {
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={!!deletingFixedPath}
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingFixedPath)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
&ldquo;{deletingFixedPath?.Path}&rdquo;
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDeleteFixedPath}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={!!deletingExtension}
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}