main
parent
cfe3db8de4
commit
dbfa8dcff0
@ -1,104 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/tls" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
func downloadImages(images map[string]string, dir string) (map[string]bool, map[string]error) { |
|
||||||
var mu sync.Mutex |
|
||||||
var wg sync.WaitGroup |
|
||||||
|
|
||||||
success := make(map[string]bool) |
|
||||||
errors := make(map[string]error) |
|
||||||
|
|
||||||
// 并发控制:最多同时下载5张图片
|
|
||||||
semaphore := make(chan struct{}, 5) |
|
||||||
|
|
||||||
for key, url := range images { |
|
||||||
wg.Add(1) |
|
||||||
|
|
||||||
go func(key, url string) { |
|
||||||
defer wg.Done() |
|
||||||
|
|
||||||
// 获取信号量
|
|
||||||
semaphore <- struct{}{} |
|
||||||
defer func() { <-semaphore }() |
|
||||||
|
|
||||||
// 下载图片
|
|
||||||
err := downloadImage(url, dir, key) |
|
||||||
|
|
||||||
mu.Lock() |
|
||||||
if err != nil { |
|
||||||
errors[key] = err |
|
||||||
} else { |
|
||||||
success[key] = true |
|
||||||
} |
|
||||||
mu.Unlock() |
|
||||||
}(key, url) |
|
||||||
} |
|
||||||
|
|
||||||
wg.Wait() |
|
||||||
return success, errors |
|
||||||
} |
|
||||||
|
|
||||||
func downloadImage(url, dir, filename string) error { |
|
||||||
// 创建HTTP客户端
|
|
||||||
client := &http.Client{ |
|
||||||
Timeout: 30 * time.Second, |
|
||||||
Transport: &http.Transport{ |
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
// 创建请求
|
|
||||||
req, err := http.NewRequest("GET", url, nil) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("创建请求失败: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// 设置请求头(模拟浏览器)
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") |
|
||||||
req.Header.Set("Accept", "image/webp,image/apng,image/*,*/*;q=0.8") |
|
||||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") |
|
||||||
req.Header.Set("Referer", "https://hd4k.com/") |
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
resp, err := client.Do(req) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("请求失败: %v", err) |
|
||||||
} |
|
||||||
defer resp.Body.Close() |
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK { |
|
||||||
return fmt.Errorf("HTTP错误: %s", resp.Status) |
|
||||||
} |
|
||||||
|
|
||||||
// 获取文件扩展名
|
|
||||||
ext := getFileExtension(url) |
|
||||||
fullFilename := filename + ext |
|
||||||
filePath := filepath.Join(dir, fullFilename) |
|
||||||
|
|
||||||
// 创建文件
|
|
||||||
file, err := os.Create(filePath) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("创建文件失败: %v", err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
// 下载并保存
|
|
||||||
_, err = io.Copy(file, resp.Body) |
|
||||||
if err != nil { |
|
||||||
// 删除可能已损坏的文件
|
|
||||||
os.Remove(filePath) |
|
||||||
return fmt.Errorf("保存文件失败: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,237 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
// 请求结构
|
|
||||||
type SaveImagesRequest struct { |
|
||||||
Title string `json:"title"` |
|
||||||
Imgs map[string]string `json:"imgs"` |
|
||||||
} |
|
||||||
|
|
||||||
// 下载详情
|
|
||||||
type DownloadDetail struct { |
|
||||||
Key string `json:"key"` |
|
||||||
URL string `json:"url"` |
|
||||||
Status string `json:"status"` // success, skipped, failed
|
|
||||||
Message string `json:"message"` |
|
||||||
SavedAs string `json:"saved_as,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
// 响应结构
|
|
||||||
type SaveImagesResponse struct { |
|
||||||
Success bool `json:"success"` |
|
||||||
Message string `json:"message"` |
|
||||||
Title string `json:"title"` |
|
||||||
Folder string `json:"folder"` |
|
||||||
JsonPath string `json:"json_path"` |
|
||||||
Total int `json:"total"` |
|
||||||
Saved int `json:"saved"` |
|
||||||
Skipped int `json:"skipped"` |
|
||||||
Failed int `json:"failed"` |
|
||||||
Details []DownloadDetail `json:"details"` |
|
||||||
} |
|
||||||
|
|
||||||
func saveImagesHandler(w http.ResponseWriter, r *http.Request) { |
|
||||||
if r.Method != http.MethodPost { |
|
||||||
http.Error(w, "只支持POST请求", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 解析请求
|
|
||||||
var req SaveImagesRequest |
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
||||||
http.Error(w, "请求格式错误: "+err.Error(), http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 验证参数
|
|
||||||
if req.Title == "" { |
|
||||||
http.Error(w, "标题不能为空", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
if len(req.Imgs) == 0 { |
|
||||||
http.Error(w, "图片列表不能为空", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 处理下载
|
|
||||||
resp, err := processDownload(&req) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "处理失败: "+err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 返回响应
|
|
||||||
w.Header().Set("Content-Type", "application/json") |
|
||||||
json.NewEncoder(w).Encode(resp) |
|
||||||
} |
|
||||||
|
|
||||||
func processDownload(req *SaveImagesRequest) (*SaveImagesResponse, error) { |
|
||||||
// 清理标题(移除非法字符)
|
|
||||||
cleanTitle := cleanFilename(req.Title) |
|
||||||
|
|
||||||
// 创建目录
|
|
||||||
comicDir := filepath.Join(*downloadDir, cleanTitle) |
|
||||||
if err := os.MkdirAll(comicDir, 0755); err != nil { |
|
||||||
return nil, fmt.Errorf("创建目录失败: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// JSON文件路径
|
|
||||||
jsonFilename := cleanTitle + ".json" |
|
||||||
jsonPath := filepath.Join(comicDir, jsonFilename) |
|
||||||
|
|
||||||
// 检查现有文件
|
|
||||||
existingFiles := make(map[string]bool) |
|
||||||
files, _ := os.ReadDir(comicDir) |
|
||||||
for _, file := range files { |
|
||||||
if !file.IsDir() { |
|
||||||
filename := file.Name() |
|
||||||
if strings.HasSuffix(filename, ".json") { |
|
||||||
continue |
|
||||||
} |
|
||||||
// 提取序号(去掉扩展名)
|
|
||||||
ext := filepath.Ext(filename) |
|
||||||
key := strings.TrimSuffix(filename, ext) |
|
||||||
existingFiles[key] = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 准备下载
|
|
||||||
details := make([]DownloadDetail, 0, len(req.Imgs)) |
|
||||||
needDownload := make(map[string]string) |
|
||||||
|
|
||||||
for key, url := range req.Imgs { |
|
||||||
// 获取文件扩展名
|
|
||||||
ext := getFileExtension(url) |
|
||||||
filename := key + ext |
|
||||||
|
|
||||||
if existingFiles[key] { |
|
||||||
// 文件已存在,跳过
|
|
||||||
details = append(details, DownloadDetail{ |
|
||||||
Key: key, |
|
||||||
URL: url, |
|
||||||
Status: "skipped", |
|
||||||
Message: "文件已存在", |
|
||||||
SavedAs: filename, |
|
||||||
}) |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// 需要下载
|
|
||||||
needDownload[key] = url |
|
||||||
details = append(details, DownloadDetail{ |
|
||||||
Key: key, |
|
||||||
URL: url, |
|
||||||
Status: "pending", |
|
||||||
Message: "等待下载", |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 下载图片
|
|
||||||
var saved, failed int |
|
||||||
if len(needDownload) > 0 { |
|
||||||
successMap, errors := downloadImages(needDownload, comicDir) |
|
||||||
|
|
||||||
// 使用 successMap 来检查哪些下载成功
|
|
||||||
// 虽然我们不直接使用 successMap,但它用于 downloadImages 函数的返回值
|
|
||||||
_ = successMap // 明确标记为使用(避免未使用警告)
|
|
||||||
|
|
||||||
// 更新下载详情
|
|
||||||
for i, detail := range details { |
|
||||||
if detail.Status == "pending" { |
|
||||||
if err, ok := errors[detail.Key]; ok { |
|
||||||
details[i].Status = "failed" |
|
||||||
details[i].Message = err.Error() |
|
||||||
failed++ |
|
||||||
} else { |
|
||||||
ext := getFileExtension(detail.URL) |
|
||||||
filename := detail.Key + ext |
|
||||||
details[i].Status = "success" |
|
||||||
details[i].Message = "下载成功" |
|
||||||
details[i].SavedAs = filename |
|
||||||
saved++ |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 保存JSON文件
|
|
||||||
skipped := len(req.Imgs) - saved - failed |
|
||||||
finalImgs := make(map[string]string) |
|
||||||
for key, url := range req.Imgs { |
|
||||||
finalImgs[key] = url |
|
||||||
} |
|
||||||
|
|
||||||
saveJSON(comicDir, jsonFilename, req.Title, finalImgs) |
|
||||||
|
|
||||||
// 构建响应
|
|
||||||
return &SaveImagesResponse{ |
|
||||||
Success: failed == 0, |
|
||||||
Message: fmt.Sprintf("下载完成: 新增%d张, 跳过%d张, 失败%d张", saved, skipped, failed), |
|
||||||
Title: req.Title, |
|
||||||
Folder: comicDir, |
|
||||||
JsonPath: jsonPath, |
|
||||||
Total: len(req.Imgs), |
|
||||||
Saved: saved, |
|
||||||
Skipped: skipped, |
|
||||||
Failed: failed, |
|
||||||
Details: details, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func saveJSON(dir, filename, title string, imgs map[string]string) error { |
|
||||||
data := map[string]interface{}{ |
|
||||||
"title": title, |
|
||||||
"imgs": imgs, |
|
||||||
"created_at": time.Now().Format("2006-01-02 15:04:05"), |
|
||||||
"updated_at": time.Now().Format("2006-01-02 15:04:05"), |
|
||||||
} |
|
||||||
|
|
||||||
jsonData, err := json.MarshalIndent(data, "", " ") |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
path := filepath.Join(dir, filename) |
|
||||||
return os.WriteFile(path, jsonData, 0644) |
|
||||||
} |
|
||||||
|
|
||||||
func cleanFilename(filename string) string { |
|
||||||
illegalChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "\n", "\r", "\t"} |
|
||||||
result := filename |
|
||||||
for _, char := range illegalChars { |
|
||||||
result = strings.ReplaceAll(result, char, "_") |
|
||||||
} |
|
||||||
if len(result) > 200 { |
|
||||||
result = result[:200] |
|
||||||
} |
|
||||||
return strings.TrimSpace(result) |
|
||||||
} |
|
||||||
|
|
||||||
func getFileExtension(url string) string { |
|
||||||
// 默认扩展名
|
|
||||||
ext := ".jpg" |
|
||||||
|
|
||||||
// 从URL提取扩展名
|
|
||||||
if idx := strings.LastIndex(url, "."); idx != -1 { |
|
||||||
fileExt := strings.ToLower(url[idx:]) |
|
||||||
// 检查是否为常见图片格式
|
|
||||||
validExts := []string{".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff"} |
|
||||||
for _, validExt := range validExts { |
|
||||||
if strings.HasPrefix(fileExt, validExt) { |
|
||||||
ext = validExt |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ext |
|
||||||
} |
|
||||||
@ -0,0 +1,446 @@ |
|||||||
|
* { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||||
|
min-height: 100vh; |
||||||
|
padding: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0 auto; |
||||||
|
background: white; |
||||||
|
border-radius: 20px; |
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
header { |
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); |
||||||
|
color: white; |
||||||
|
padding: 40px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
header h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
margin-bottom: 10px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
gap: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
header .subtitle { |
||||||
|
font-size: 1.1rem; |
||||||
|
opacity: 0.9; |
||||||
|
} |
||||||
|
|
||||||
|
.control-panel { |
||||||
|
padding: 30px; |
||||||
|
background: #f8fafc; |
||||||
|
border-bottom: 1px solid #e2e8f0; |
||||||
|
display: flex; |
||||||
|
gap: 15px; |
||||||
|
align-items: center; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.btn { |
||||||
|
padding: 12px 24px; |
||||||
|
border: none; |
||||||
|
border-radius: 10px; |
||||||
|
font-size: 1rem; |
||||||
|
font-weight: 600; |
||||||
|
cursor: pointer; |
||||||
|
display: inline-flex; |
||||||
|
align-items: center; |
||||||
|
gap: 8px; |
||||||
|
transition: all 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary { |
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary:hover { |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 10px 20px rgba(16, 185, 129, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
.btn-warning { |
||||||
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-warning:hover { |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 10px 20px rgba(245, 158, 11, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
.btn-secondary { |
||||||
|
background: #64748b; |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-secondary:hover { |
||||||
|
background: #475569; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-small { |
||||||
|
padding: 8px 16px; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.status { |
||||||
|
margin-left: auto; |
||||||
|
padding: 10px 20px; |
||||||
|
background: white; |
||||||
|
border-radius: 8px; |
||||||
|
border-left: 4px solid #10b981; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.folders-container { |
||||||
|
padding: 30px; |
||||||
|
} |
||||||
|
|
||||||
|
.folders-container h2 { |
||||||
|
color: #334155; |
||||||
|
margin-bottom: 20px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.folders-list { |
||||||
|
display: grid; |
||||||
|
gap: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-item { |
||||||
|
background: white; |
||||||
|
border: 2px solid #e2e8f0; |
||||||
|
border-radius: 12px; |
||||||
|
padding: 20px; |
||||||
|
transition: all 0.3s ease; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-item:hover { |
||||||
|
border-color: #94a3b8; |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
||||||
|
} |
||||||
|
|
||||||
|
.folder-info { |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-title { |
||||||
|
font-size: 1.2rem; |
||||||
|
font-weight: 600; |
||||||
|
color: #1e293b; |
||||||
|
margin-bottom: 8px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-stats { |
||||||
|
display: flex; |
||||||
|
gap: 20px; |
||||||
|
color: #64748b; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-container { |
||||||
|
width: 200px; |
||||||
|
margin: 10px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-wrapper { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.progress { |
||||||
|
flex: 1; |
||||||
|
height: 8px; |
||||||
|
background: #e2e8f0; |
||||||
|
border-radius: 4px; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-fill { |
||||||
|
height: 100%; |
||||||
|
background: linear-gradient(90deg, #10b981 0%, #34d399 100%); |
||||||
|
border-radius: 4px; |
||||||
|
transition: width 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-text { |
||||||
|
font-weight: 600; |
||||||
|
color: #1e293b; |
||||||
|
min-width: 50px; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-actions { |
||||||
|
display: flex; |
||||||
|
gap: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.no-folders { |
||||||
|
text-align: center; |
||||||
|
padding: 60px 20px; |
||||||
|
color: #64748b; |
||||||
|
} |
||||||
|
|
||||||
|
.no-folders i { |
||||||
|
font-size: 4rem; |
||||||
|
color: #10b981; |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.no-folders p { |
||||||
|
font-size: 1.2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.api-info { |
||||||
|
padding: 30px; |
||||||
|
background: #f1f5f9; |
||||||
|
border-top: 1px solid #e2e8f0; |
||||||
|
} |
||||||
|
|
||||||
|
.api-info h3 { |
||||||
|
color: #334155; |
||||||
|
margin-bottom: 15px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.api-info code { |
||||||
|
background: #e2e8f0; |
||||||
|
padding: 4px 8px; |
||||||
|
border-radius: 4px; |
||||||
|
font-family: 'Courier New', monospace; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.api-info p { |
||||||
|
margin: 10px 0; |
||||||
|
color: #475569; |
||||||
|
} |
||||||
|
|
||||||
|
/* 模态框样式 */ |
||||||
|
.modal { |
||||||
|
display: none; |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background: rgba(0, 0, 0, 0.5); |
||||||
|
z-index: 1000; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-content { |
||||||
|
background: white; |
||||||
|
border-radius: 15px; |
||||||
|
width: 90%; |
||||||
|
max-width: 600px; |
||||||
|
max-height: 80vh; |
||||||
|
overflow: hidden; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-header { |
||||||
|
padding: 20px; |
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); |
||||||
|
color: white; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-header h3 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.close-btn { |
||||||
|
background: none; |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
font-size: 1.8rem; |
||||||
|
cursor: pointer; |
||||||
|
padding: 0; |
||||||
|
width: 30px; |
||||||
|
height: 30px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-body { |
||||||
|
padding: 20px; |
||||||
|
flex: 1; |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-container { |
||||||
|
background: #f8fafc; |
||||||
|
border-radius: 10px; |
||||||
|
padding: 20px; |
||||||
|
margin-bottom: 20px; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-bar { |
||||||
|
height: 10px; |
||||||
|
background: linear-gradient(90deg, #10b981 0%, #34d399 100%); |
||||||
|
border-radius: 5px; |
||||||
|
width: 0%; |
||||||
|
transition: width 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.progress-text { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
right: 20px; |
||||||
|
transform: translateY(-50%); |
||||||
|
font-weight: 600; |
||||||
|
color: #1e293b; |
||||||
|
} |
||||||
|
|
||||||
|
.download-stats { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(3, 1fr); |
||||||
|
gap: 15px; |
||||||
|
margin: 20px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-item { |
||||||
|
background: #f8fafc; |
||||||
|
padding: 15px; |
||||||
|
border-radius: 8px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-label { |
||||||
|
display: block; |
||||||
|
color: #64748b; |
||||||
|
font-size: 0.9rem; |
||||||
|
margin-bottom: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-value { |
||||||
|
display: block; |
||||||
|
font-size: 1.5rem; |
||||||
|
font-weight: 600; |
||||||
|
color: #1e293b; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-value.success { |
||||||
|
color: #10b981; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-value.pending { |
||||||
|
color: #f59e0b; |
||||||
|
} |
||||||
|
|
||||||
|
.log-container { |
||||||
|
background: #f8fafc; |
||||||
|
border-radius: 10px; |
||||||
|
padding: 20px; |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.log-container h4 { |
||||||
|
color: #334155; |
||||||
|
margin-bottom: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.log-content { |
||||||
|
max-height: 200px; |
||||||
|
overflow-y: auto; |
||||||
|
font-family: 'Courier New', monospace; |
||||||
|
font-size: 0.9rem; |
||||||
|
line-height: 1.4; |
||||||
|
color: #475569; |
||||||
|
} |
||||||
|
|
||||||
|
.log-entry { |
||||||
|
padding: 5px 0; |
||||||
|
border-bottom: 1px solid #e2e8f0; |
||||||
|
} |
||||||
|
|
||||||
|
.log-entry.success { |
||||||
|
color: #10b981; |
||||||
|
} |
||||||
|
|
||||||
|
.log-entry.error { |
||||||
|
color: #ef4444; |
||||||
|
} |
||||||
|
|
||||||
|
.log-entry.info { |
||||||
|
color: #3b82f6; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-footer { |
||||||
|
padding: 20px; |
||||||
|
background: #f8fafc; |
||||||
|
border-top: 1px solid #e2e8f0; |
||||||
|
text-align: right; |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 768px) { |
||||||
|
.container { |
||||||
|
border-radius: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
header { |
||||||
|
padding: 30px 20px; |
||||||
|
} |
||||||
|
|
||||||
|
header h1 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.control-panel { |
||||||
|
flex-direction: column; |
||||||
|
align-items: stretch; |
||||||
|
} |
||||||
|
|
||||||
|
.status { |
||||||
|
margin-left: 0; |
||||||
|
margin-top: 10px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-item { |
||||||
|
flex-direction: column; |
||||||
|
align-items: stretch; |
||||||
|
gap: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
.folder-actions { |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.download-stats { |
||||||
|
grid-template-columns: 1fr; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,181 +1,84 @@ |
|||||||
<!DOCTYPE html> |
<!DOCTYPE html> |
||||||
<html> |
<html lang="zh-CN"> |
||||||
<head> |
<head> |
||||||
<title>HD4K下载测试</title> |
<meta charset="UTF-8"> |
||||||
<style> |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
body { |
<title>HD4K 下载管理</title> |
||||||
font-family: Arial, sans-serif; |
<link rel="stylesheet" href="index.css"> |
||||||
max-width: 800px; |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
||||||
margin: 0 auto; |
|
||||||
padding: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
.form-group { |
|
||||||
margin-bottom: 15px; |
|
||||||
} |
|
||||||
|
|
||||||
label { |
|
||||||
display: block; |
|
||||||
margin-bottom: 5px; |
|
||||||
font-weight: bold; |
|
||||||
} |
|
||||||
|
|
||||||
input, textarea { |
|
||||||
width: 100%; |
|
||||||
padding: 8px; |
|
||||||
border: 1px solid #ddd; |
|
||||||
border-radius: 4px; |
|
||||||
} |
|
||||||
|
|
||||||
textarea { |
|
||||||
height: 200px; |
|
||||||
font-family: monospace; |
|
||||||
} |
|
||||||
|
|
||||||
button { |
|
||||||
background-color: #007bff; |
|
||||||
color: white; |
|
||||||
border: none; |
|
||||||
padding: 10px 20px; |
|
||||||
border-radius: 4px; |
|
||||||
cursor: pointer; |
|
||||||
font-size: 16px; |
|
||||||
} |
|
||||||
|
|
||||||
button:hover { |
|
||||||
background-color: #0056b3; |
|
||||||
} |
|
||||||
|
|
||||||
.result { |
|
||||||
margin-top: 20px; |
|
||||||
padding: 15px; |
|
||||||
border-radius: 4px; |
|
||||||
background-color: #f8f9fa; |
|
||||||
} |
|
||||||
|
|
||||||
.success { |
|
||||||
color: #28a745; |
|
||||||
} |
|
||||||
|
|
||||||
.error { |
|
||||||
color: #dc3545; |
|
||||||
} |
|
||||||
|
|
||||||
.status-item { |
|
||||||
margin: 5px 0; |
|
||||||
padding: 5px; |
|
||||||
border-left: 3px solid #ddd; |
|
||||||
} |
|
||||||
|
|
||||||
.status-success { |
|
||||||
border-left-color: #28a745; |
|
||||||
} |
|
||||||
|
|
||||||
.status-skipped { |
|
||||||
border-left-color: #ffc107; |
|
||||||
} |
|
||||||
|
|
||||||
.status-failed { |
|
||||||
border-left-color: #dc3545; |
|
||||||
} |
|
||||||
</style> |
|
||||||
</head> |
</head> |
||||||
<body> |
<body> |
||||||
<h1>HD4K下载测试</h1> |
<div class="container"> |
||||||
|
<header> |
||||||
<div class="form-group"> |
<h1><i class="fas fa-download"></i> HD4K 下载管理</h1> |
||||||
<label for="title">标题:</label> |
<p class="subtitle">管理未完成的下载任务</p> |
||||||
<input type="text" id="title" value="测试"> |
</header> |
||||||
|
|
||||||
|
<div class="control-panel"> |
||||||
|
<button id="reloadBtn" class="btn btn-primary"> |
||||||
|
<i class="fas fa-sync-alt"></i> 重载下载文件夹 |
||||||
|
</button> |
||||||
|
<button id="cleanupBtn" class="btn btn-warning"> |
||||||
|
<i class="fas fa-trash-alt"></i> 清理JSON文件 |
||||||
|
</button> |
||||||
|
<div class="status" id="statusMessage"></div> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="form-group"> |
<div class="folders-container"> |
||||||
<label for="jsonData">JSON数据:</label> |
<h2><i class="fas fa-folder-open"></i> 未完成的任务</h2> |
||||||
<textarea id="jsonData">{ |
<div id="foldersList" class="folders-list"> |
||||||
"title": "测试", |
<!-- 文件夹列表将在这里动态生成 --> |
||||||
"imgs": { |
</div> |
||||||
"0001": "https://i.imgur.com/3dV1KnX1.jpg", |
<div id="noFolders" class="no-folders"> |
||||||
"0002": "https://i.imgur.com/3dV1KnX2.jpg", |
<i class="fas fa-check-circle"></i> |
||||||
"0003": "https://i.imgur.com/3dV1KnX3.jpg", |
<p>所有下载任务已完成!</p> |
||||||
"0004": "https://i.imgur.com/3dV1KnX4.jpg" |
</div> |
||||||
} |
|
||||||
}</textarea> |
|
||||||
</div> |
</div> |
||||||
|
|
||||||
<button onclick="downloadComic()">开始下载</button> |
<div class="api-info"> |
||||||
|
<h3><i class="fas fa-info-circle"></i> API 信息</h3> |
||||||
<div id="result" class="result" style="display: none;"></div> |
<p>图片下载API: <code>POST http://127.0.0.1:<span id="port">55830</span>/api/save_imgs</code></p> |
||||||
|
<p>请求格式: <code>{"title": "漫画标题", "imgs": {"001": "url1", "002": "url2"}}</code></p> |
||||||
<script> |
</div> |
||||||
async function downloadComic() { |
</div> |
||||||
const title = document.getElementById('title').value; |
|
||||||
const jsonData = document.getElementById('jsonData').value; |
|
||||||
|
|
||||||
let imgs; |
|
||||||
try { |
|
||||||
imgs = JSON.parse(jsonData); |
|
||||||
} catch (e) { |
|
||||||
showResult('error', 'JSON格式错误: ' + e.message); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const data = { |
|
||||||
title: title, |
|
||||||
imgs: imgs.imgs || imgs |
|
||||||
}; |
|
||||||
|
|
||||||
try { |
|
||||||
const response = await fetch('http://127.0.0.1:8888/api/save_imgs', { |
|
||||||
method: 'POST', |
|
||||||
headers: { |
|
||||||
'Content-Type': 'application/json', |
|
||||||
}, |
|
||||||
body: JSON.stringify(data) |
|
||||||
}); |
|
||||||
|
|
||||||
const result = await response.json(); |
|
||||||
showResult('success', result); |
|
||||||
} catch (error) { |
|
||||||
showResult('error', '请求失败: ' + error.message); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function showResult(type, result) { |
|
||||||
const resultDiv = document.getElementById('result'); |
|
||||||
resultDiv.style.display = 'block'; |
|
||||||
|
|
||||||
if (type === 'error') { |
|
||||||
resultDiv.innerHTML = `<div class="error"><strong>错误:</strong> ${result}</div>`; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
let html = `<div class="success"><strong>下载完成!</strong></div>`; |
|
||||||
html += `<p><strong>标题:</strong> ${result.title}</p>`; |
|
||||||
html += `<p><strong>文件夹:</strong> ${result.folder}</p>`; |
|
||||||
html += `<p><strong>总计:</strong> ${result.total} 张</p>`; |
|
||||||
html += `<p><strong>新增:</strong> ${result.saved} 张</p>`; |
|
||||||
html += `<p><strong>跳过:</strong> ${result.skipped} 张</p>`; |
|
||||||
html += `<p><strong>失败:</strong> ${result.failed} 张</p>`; |
|
||||||
html += `<p><strong>消息:</strong> ${result.message}</p>`; |
|
||||||
|
|
||||||
if (result.details && result.details.length > 0) { |
|
||||||
html += `<h3>下载详情:</h3>`; |
|
||||||
result.details.forEach(detail => { |
|
||||||
let statusClass = ''; |
|
||||||
if (detail.status === 'success') statusClass = 'status-success'; |
|
||||||
else if (detail.status === 'skipped') statusClass = 'status-skipped'; |
|
||||||
else if (detail.status === 'failed') statusClass = 'status-failed'; |
|
||||||
|
|
||||||
html += ` |
<!-- 下载进度模态框 --> |
||||||
<div class="status-item ${statusClass}"> |
<div id="downloadModal" class="modal"> |
||||||
<strong>${detail.key}:</strong> ${detail.status} - ${detail.message} |
<div class="modal-content"> |
||||||
${detail.saved_as ? ` (保存为: ${detail.saved_as})` : ''} |
<div class="modal-header"> |
||||||
</div> |
<h3 id="modalTitle">下载进度</h3> |
||||||
`; |
<button class="close-btn">×</button> |
||||||
}); |
</div> |
||||||
} |
<div class="modal-body"> |
||||||
|
<div class="progress-container"> |
||||||
|
<div class="progress-bar" id="progressBar"></div> |
||||||
|
<div class="progress-text" id="progressText">0%</div> |
||||||
|
</div> |
||||||
|
<div class="download-stats"> |
||||||
|
<div class="stat-item"> |
||||||
|
<span class="stat-label">总文件数:</span> |
||||||
|
<span class="stat-value" id="totalFiles">0</span> |
||||||
|
</div> |
||||||
|
<div class="stat-item"> |
||||||
|
<span class="stat-label">已下载:</span> |
||||||
|
<span class="stat-value success" id="downloadedFiles">0</span> |
||||||
|
</div> |
||||||
|
<div class="stat-item"> |
||||||
|
<span class="stat-label">待下载:</span> |
||||||
|
<span class="stat-value pending" id="pendingFiles">0</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="log-container"> |
||||||
|
<h4>下载日志</h4> |
||||||
|
<div class="log-content" id="downloadLog"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button id="closeModalBtn" class="btn btn-secondary">关闭</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
resultDiv.innerHTML = html; |
<script src="index.js"></script> |
||||||
} |
|
||||||
</script> |
|
||||||
</body> |
</body> |
||||||
</html> |
</html> |
||||||
@ -0,0 +1,255 @@ |
|||||||
|
class DownloadManager { |
||||||
|
constructor() { |
||||||
|
this.port = window.location.port || '55830'; |
||||||
|
this.baseUrl = `http://127.0.0.1:${this.port}/api`; |
||||||
|
this.currentDownload = null; |
||||||
|
|
||||||
|
this.init(); |
||||||
|
} |
||||||
|
|
||||||
|
init() { |
||||||
|
document.getElementById('port').textContent = this.port; |
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
document.getElementById('reloadBtn').addEventListener('click', () => this.reloadFolders()); |
||||||
|
document.getElementById('cleanupBtn').addEventListener('click', () => this.cleanupJson()); |
||||||
|
document.getElementById('closeModalBtn').addEventListener('click', () => this.hideModal()); |
||||||
|
document.querySelector('.close-btn').addEventListener('click', () => this.hideModal()); |
||||||
|
|
||||||
|
// 点击模态框外部关闭
|
||||||
|
document.getElementById('downloadModal').addEventListener('click', (e) => { |
||||||
|
if (e.target === document.getElementById('downloadModal')) { |
||||||
|
this.hideModal(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
this.reloadFolders(); |
||||||
|
} |
||||||
|
|
||||||
|
showStatus(message, type = 'info') { |
||||||
|
const statusEl = document.getElementById('statusMessage'); |
||||||
|
statusEl.textContent = message; |
||||||
|
statusEl.style.borderLeftColor = type === 'error' ? '#ef4444' : |
||||||
|
type === 'warning' ? '#f59e0b' : '#10b981'; |
||||||
|
} |
||||||
|
|
||||||
|
showModal(title) { |
||||||
|
document.getElementById('modalTitle').textContent = title; |
||||||
|
document.getElementById('downloadModal').style.display = 'flex'; |
||||||
|
this.resetModal(); |
||||||
|
} |
||||||
|
|
||||||
|
hideModal() { |
||||||
|
document.getElementById('downloadModal').style.display = 'none'; |
||||||
|
if (this.currentDownload && this.currentDownload.abort) { |
||||||
|
this.currentDownload.abort(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
resetModal() { |
||||||
|
document.getElementById('progressBar').style.width = '0%'; |
||||||
|
document.getElementById('progressText').textContent = '0%'; |
||||||
|
document.getElementById('totalFiles').textContent = '0'; |
||||||
|
document.getElementById('downloadedFiles').textContent = '0'; |
||||||
|
document.getElementById('pendingFiles').textContent = '0'; |
||||||
|
document.getElementById('downloadLog').innerHTML = ''; |
||||||
|
} |
||||||
|
|
||||||
|
updateProgress(progress, stats) { |
||||||
|
document.getElementById('progressBar').style.width = `${progress}%`; |
||||||
|
document.getElementById('progressText').textContent = `${Math.round(progress)}%`; |
||||||
|
|
||||||
|
if (stats) { |
||||||
|
document.getElementById('totalFiles').textContent = stats.total || 0; |
||||||
|
document.getElementById('downloadedFiles').textContent = stats.downloaded || 0; |
||||||
|
document.getElementById('pendingFiles').textContent = stats.pending || 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
addLogEntry(message, type = 'info') { |
||||||
|
const logEl = document.getElementById('downloadLog'); |
||||||
|
const entry = document.createElement('div'); |
||||||
|
entry.className = `log-entry ${type}`; |
||||||
|
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; |
||||||
|
logEl.appendChild(entry); |
||||||
|
logEl.scrollTop = logEl.scrollHeight; |
||||||
|
} |
||||||
|
|
||||||
|
async reloadFolders() { |
||||||
|
try { |
||||||
|
this.showStatus('正在加载文件夹...', 'info'); |
||||||
|
document.getElementById('reloadBtn').disabled = true; |
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/reload_folders`); |
||||||
|
const data = await response.json(); |
||||||
|
|
||||||
|
if (data.success) { |
||||||
|
this.displayFolders(data.folders); |
||||||
|
this.showStatus(`已加载 ${data.folders.length} 个未完成的任务`, 'success'); |
||||||
|
} else { |
||||||
|
throw new Error(data.message || '加载失败'); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('加载失败:', error); |
||||||
|
this.showStatus(`加载失败: ${error.message}`, 'error'); |
||||||
|
this.displayFolders([]); |
||||||
|
} finally { |
||||||
|
document.getElementById('reloadBtn').disabled = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
displayFolders(folders) { |
||||||
|
const foldersList = document.getElementById('foldersList'); |
||||||
|
const noFolders = document.getElementById('noFolders'); |
||||||
|
|
||||||
|
// 过滤掉进度为100%的文件夹
|
||||||
|
const incompleteFolders = folders.filter(folder => folder.progress < 100); |
||||||
|
|
||||||
|
if (incompleteFolders.length === 0) { |
||||||
|
foldersList.innerHTML = ''; |
||||||
|
noFolders.style.display = 'block'; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
noFolders.style.display = 'none'; |
||||||
|
|
||||||
|
foldersList.innerHTML = incompleteFolders.map(folder => ` |
||||||
|
<div class="folder-item"> |
||||||
|
<div class="folder-info"> |
||||||
|
<div class="folder-title"> |
||||||
|
<i class="fas fa-book"></i> |
||||||
|
${this.escapeHtml(folder.title)} |
||||||
|
</div> |
||||||
|
<div class="folder-stats"> |
||||||
|
<span>总文件: ${folder.total}</span> |
||||||
|
<span>已下载: ${folder.downloaded}</span> |
||||||
|
<span>缺失: ${folder.total - folder.downloaded}</span> |
||||||
|
</div> |
||||||
|
<div class="progress-container"> |
||||||
|
<div class="progress-wrapper"> |
||||||
|
<div class="progress"> |
||||||
|
<div class="progress-fill" style="width: ${folder.progress}%"></div> |
||||||
|
</div> |
||||||
|
<div class="progress-text">${folder.progress.toFixed(1)}%</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="folder-actions"> |
||||||
|
<button class="btn btn-primary btn-small" onclick="downloadManager.downloadFolder('${this.escapeAttr(folder.folder)}', '${this.escapeAttr(folder.title)}')"> |
||||||
|
<i class="fas fa-download"></i> 下载缺失文件 (${folder.total - folder.downloaded}) |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`).join('');
|
||||||
|
} |
||||||
|
|
||||||
|
async downloadFolder(folderPath, title) { |
||||||
|
try { |
||||||
|
this.showModal(`正在下载: ${title}`); |
||||||
|
this.addLogEntry('开始下载缺失文件...', 'info'); |
||||||
|
|
||||||
|
console.log('发送请求:', { folder: folderPath, title: title }); |
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/download_missing`, { |
||||||
|
method: 'POST', |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/json', |
||||||
|
}, |
||||||
|
body: JSON.stringify({ |
||||||
|
folder: folderPath, |
||||||
|
title: title |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (!response.ok) { |
||||||
|
const errorText = await response.text(); |
||||||
|
console.error('HTTP错误:', response.status, errorText); |
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`); |
||||||
|
} |
||||||
|
|
||||||
|
const data = await response.json(); |
||||||
|
console.log('收到响应:', data); |
||||||
|
|
||||||
|
if (data.success) { |
||||||
|
// 更新进度
|
||||||
|
const totalSaved = (data.saved || 0) + (data.skipped || 0); |
||||||
|
const total = data.total || 1; |
||||||
|
const progress = totalSaved / total * 100; |
||||||
|
|
||||||
|
this.updateProgress(progress, { |
||||||
|
total: total, |
||||||
|
downloaded: totalSaved, |
||||||
|
pending: data.failed || 0 |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
if (data.details) { |
||||||
|
data.details.forEach(detail => { |
||||||
|
if (detail.status === 'success') { |
||||||
|
this.addLogEntry(`成功下载: ${detail.key}`, 'success'); |
||||||
|
} else if (detail.status === 'failed') { |
||||||
|
this.addLogEntry(`下载失败 ${detail.key}: ${detail.message}`, 'error'); |
||||||
|
} else if (detail.status === 'skipped') { |
||||||
|
this.addLogEntry(`跳过: ${detail.key} (${detail.message})`, 'info'); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
this.addLogEntry(`下载完成: ${data.message}`, 'success'); |
||||||
|
|
||||||
|
// 刷新文件夹列表
|
||||||
|
setTimeout(() => this.reloadFolders(), 1000); |
||||||
|
} else { |
||||||
|
this.addLogEntry(`下载失败: ${data.message}`, 'error'); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('下载失败:', error); |
||||||
|
this.addLogEntry(`下载失败: ${error.message}`, 'error'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async cleanupJson() { |
||||||
|
if (!confirm('确定要删除所有JSON文件吗?此操作不可恢复。')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
this.showStatus('正在清理JSON文件...', 'info'); |
||||||
|
document.getElementById('cleanupBtn').disabled = true; |
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/cleanup_json`, { |
||||||
|
method: 'POST' |
||||||
|
}); |
||||||
|
|
||||||
|
const data = await response.json(); |
||||||
|
|
||||||
|
if (data.success) { |
||||||
|
this.showStatus(data.message, 'success'); |
||||||
|
// 刷新文件夹列表
|
||||||
|
setTimeout(() => this.reloadFolders(), 500); |
||||||
|
} else { |
||||||
|
throw new Error(data.message || '清理失败'); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('清理失败:', error); |
||||||
|
this.showStatus(`清理失败: ${error.message}`, 'error'); |
||||||
|
} finally { |
||||||
|
document.getElementById('cleanupBtn').disabled = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
escapeHtml(text) { |
||||||
|
const div = document.createElement('div'); |
||||||
|
div.textContent = text; |
||||||
|
return div.innerHTML; |
||||||
|
} |
||||||
|
|
||||||
|
escapeAttr(text) { |
||||||
|
return this.escapeHtml(text).replace(/"/g, '"'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
const downloadManager = new DownloadManager(); |
||||||
Loading…
Reference in new issue