♻️ refactor(handlers): status 与 review 页面模板化
- HTML 提取至 templates/status.html、templates/review.html(go:embed 内嵌) - status.go 1022→271 行、review.go 1281→438 行,动态数据全部经 html/template 按上下文自动转义 - 评论循环/作者徽章/贴纸/图片条件渲染翻译为模板 range/if;emojiMap 以 template.JS 注入 - 与旧版渲染输出实测对比:22 条评论结构完全一致;差异仅为 JS 上下文安全转义(& 等)与 html/template 剥除 CSS 注释,均无功能影响 - 其余文件为 gofmt 顺手格式化
This commit is contained in:
1 parent
ad6d28fd85
commit
5bcc8a962e
29 files changed
+1267
-2893
No files matched your search
@@ -103,4 +103,3 @@ func HandleMultiDetail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Fprint(w, result)
|
||||
}
|
||||
|
||||
@@ -41,4 +41,3 @@ func HandleCategoryLanding(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Fprint(w, result)
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ func HandleContent(w http.ResponseWriter, r *http.Request) {
|
||||
itemID := params["item_id"]
|
||||
|
||||
// 获取URL参数(支持动态覆盖配置)
|
||||
textMode := r.URL.Query().Get("text_mode") // 1=纯文本模式
|
||||
imageMode := r.URL.Query().Get("image_mode") // 0=移除图片, 1=保留图片
|
||||
reviewParam := r.URL.Query().Get("review") // 1=启用段评, 0=禁用段评
|
||||
textMode := r.URL.Query().Get("text_mode") // 1=纯文本模式
|
||||
imageMode := r.URL.Query().Get("image_mode") // 0=移除图片, 1=保留图片
|
||||
reviewParam := r.URL.Query().Get("review") // 1=启用段评, 0=禁用段评
|
||||
|
||||
result, err := fqapi.HandleGetContent(itemID, textMode, imageMode)
|
||||
if err != nil {
|
||||
@@ -103,19 +103,19 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
|
||||
if err := fqapi.ParseJSON([]byte(jsonResult), &resultData); err != nil {
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
|
||||
// 获取 data 字段
|
||||
data, ok := resultData["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
|
||||
// 获取 content 字段
|
||||
content, ok := data["content"].(string)
|
||||
if !ok || content == "" {
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
|
||||
// 从响应数据中提取 book_id (data.novel_data.book_id)
|
||||
bookID := ""
|
||||
if novelData, ok := data["novel_data"].(map[string]interface{}); ok {
|
||||
@@ -125,7 +125,7 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
|
||||
bookID = fmt.Sprintf("%.0f", bookIDVal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取基础 URL
|
||||
cfg := config.GetConfig()
|
||||
baseURL := ""
|
||||
@@ -136,7 +136,7 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
|
||||
}
|
||||
port := cfg.Server.Port
|
||||
scheme := "http"
|
||||
|
||||
|
||||
// 检查是否来自外部请求
|
||||
if r.Host != "" {
|
||||
baseURL = scheme + "://" + r.Host
|
||||
@@ -144,24 +144,23 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
|
||||
baseURL = scheme + "://" + host + ":" + port
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 生成带段评标记的内容
|
||||
newContent, err := fqapi.GenerateParagraphsWithComments(itemID, bookID, content, baseURL)
|
||||
if err != nil {
|
||||
// 如果失败,返回原始内容
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
|
||||
// 替换 content 字段
|
||||
data["content"] = newContent
|
||||
resultData["data"] = data
|
||||
|
||||
|
||||
// 重新序列化为 JSON
|
||||
newJSON, err := fqapi.ToJSON(resultData)
|
||||
if err != nil {
|
||||
return jsonResult
|
||||
}
|
||||
|
||||
|
||||
return newJSON
|
||||
}
|
||||
|
||||
@@ -13,32 +13,31 @@ func HandleMangaImageProxy(w http.ResponseWriter, r *http.Request) {
|
||||
// 从URL路径提取参数: /manga/image/{itemID}/{index}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/manga/image/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, "Invalid URL format. Expected: /manga/image/{itemID}/{index}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
itemID := parts[0]
|
||||
index, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid index parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取解密后的图片数据
|
||||
imageData, mimeType, err := fqapi.HandleMangaImageProxy(itemID, index)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to get image: %s", err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 设置响应头
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600") // 缓存1小时
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(imageData)))
|
||||
|
||||
|
||||
// 返回图片数据
|
||||
w.Write(imageData)
|
||||
}
|
||||
|
||||
+81
-925
File diff suppressed because it is too large.
Load diff
+54
-806
@@ -1,11 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fanqie/internal/algos"
|
||||
"fanqie/internal/config"
|
||||
"fanqie/internal/fqapi"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@@ -13,7 +15,7 @@ import (
|
||||
// HandleVacuum 处理数据库压缩请求
|
||||
func HandleVacuum(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
|
||||
// 只允许 POST 请求
|
||||
if r.Method != http.MethodPost {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
@@ -22,7 +24,7 @@ func HandleVacuum(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 执行 VACUUM
|
||||
err := fqapi.VacuumDatabase()
|
||||
if err != nil {
|
||||
@@ -32,18 +34,25 @@ func HandleVacuum(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "数据库压缩完成",
|
||||
})
|
||||
}
|
||||
|
||||
// 状态页模板(embed 内嵌,动态数据由 html/template 自动转义)
|
||||
//
|
||||
//go:embed templates/status.html
|
||||
var statusHTML string
|
||||
|
||||
var statusTmpl = template.Must(template.New("status").Parse(statusHTML))
|
||||
|
||||
// HandleStatus 状态页面处理
|
||||
func HandleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
uptime := time.Since(startTime)
|
||||
|
||||
|
||||
// 格式化运行时间
|
||||
days := int(uptime.Hours() / 24)
|
||||
hours := int(uptime.Hours()) % 24
|
||||
@@ -59,10 +68,10 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
uptimeStr = fmt.Sprintf("%d秒", seconds)
|
||||
}
|
||||
|
||||
|
||||
// 获取签名信息
|
||||
currentSign := algos.GlobalSignatureSwitcher.GetCurrentMethod()
|
||||
|
||||
|
||||
// 生成签名详情显示
|
||||
var signatureDetails string
|
||||
if currentSign == "sign2" {
|
||||
@@ -70,800 +79,40 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos"
|
||||
}
|
||||
|
||||
|
||||
// 获取统计数据
|
||||
stats := getStatsSnapshot()
|
||||
|
||||
|
||||
// 获取设备密钥池状态
|
||||
poolStatus := getDeviceKeyPoolStatus()
|
||||
|
||||
html := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>番茄小说API - 运行状态</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 8px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.header p {
|
||||
font-size: 1em;
|
||||
color: #666;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
.stat-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.stat-value {
|
||||
color: #1a1a1a;
|
||||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.api-docs {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.api-section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.api-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.api-section-title {
|
||||
color: #1a1a1a;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.api-section-title::before {
|
||||
content: "▸";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #666;
|
||||
}
|
||||
.api-endpoint {
|
||||
background: #fafafa;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.api-endpoint:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.api-method {
|
||||
display: inline-block;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.api-path {
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 0.9em;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.api-desc {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
margin-top: 6px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.api-params {
|
||||
margin-top: 8px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.param-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
margin: 4px 0;
|
||||
color: #555;
|
||||
}
|
||||
.param-name {
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
color: #e91e63;
|
||||
font-weight: 500;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.param-required {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.75em;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.param-optional {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.75em;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.param-desc {
|
||||
color: #777;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.badge-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.badge-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
.badge-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.badge-clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.badge-clickable:hover {
|
||||
background: #ffc107;
|
||||
color: #000;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.badge-clickable:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
max-width: 300px;
|
||||
}
|
||||
.toast.success {
|
||||
background: #4caf50;
|
||||
}
|
||||
.toast.error {
|
||||
background: #f44336;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
let autoRefreshInterval;
|
||||
|
||||
window.onload = function() {
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
autoRefreshInterval = setInterval(function() {
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newBody = doc.querySelector('body');
|
||||
document.body.innerHTML = newBody.innerHTML;
|
||||
})
|
||||
.catch(err => console.error('刷新失败:', err));
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast ' + type;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function performVacuum() {
|
||||
stopAutoRefresh();
|
||||
|
||||
const badge = event.target;
|
||||
const originalText = badge.textContent;
|
||||
badge.textContent = '压缩中...';
|
||||
badge.style.cursor = 'wait';
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const key = urlParams.get('key');
|
||||
const vacuumUrl = key ? '/vacuum?key=' + encodeURIComponent(key) : '/vacuum';
|
||||
|
||||
fetch(vacuumUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
badge.textContent = originalText;
|
||||
badge.style.cursor = 'pointer';
|
||||
startAutoRefresh();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('压缩失败: ' + err.message, 'error');
|
||||
badge.textContent = originalText;
|
||||
badge.style.cursor = 'pointer';
|
||||
startAutoRefresh();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>番茄小说API</h1>
|
||||
<p>运行状态监控面板</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-title">● 运行状态</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">服务器时间</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">运行时长</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">启动时间</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item" style="flex-direction: column; align-items: flex-start;">
|
||||
<span class="stat-label" style="margin-bottom: 8px;">签名方法</span>
|
||||
<span class="stat-value" style="font-size: 13px; line-height: 1.6;">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">数据库大小</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已用空间</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">空闲空间</span>
|
||||
<span class="stat-value">%s <span class="badge badge-warning badge-clickable" onclick="performVacuum()" title="点击立即压缩数据库">立即压缩</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">◉ 设备密钥池</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">池状态</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总设备数</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">有效设备</span>
|
||||
<span class="stat-value">%d <span class="badge badge-success">可用</span></span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">无效设备</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">当前索引</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">■ 请求统计</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总请求数</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">搜索请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">详情请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">目录请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">内容请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">漫画请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">音频请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">视频请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">分类请求</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">◆ 缓存统计</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">正文缓存数</span>
|
||||
<span class="stat-value">%d</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">正文缓存</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">自动清理</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">清理间隔</span>
|
||||
<span class="stat-value">%s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-docs">
|
||||
<div class="card-title">◈ API 接口文档</div>
|
||||
|
||||
<!-- 书籍相关 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">书籍相关</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/search</span></div>
|
||||
<div class="api-desc">◆ 搜索书籍</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">query</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">搜索关键字</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">offset</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">偏移量,默认0</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">tab_type</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">类型,默认3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/detail</span></div>
|
||||
<div class="api-desc">◆ 获取书籍详情</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/catalog</span></div>
|
||||
<div class="api-desc">◆ 获取书籍目录</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/multi-detail</span></div>
|
||||
<div class="api-desc">◆ 批量获取书籍详情</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID列表,逗号分隔</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容相关 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">内容相关</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/content</span></div>
|
||||
<div class="api-desc">◆ 获取章节正文内容</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID,支持路径参数或查询参数</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">text_mode</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">image_mode</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">review</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">段评功能:1=启用,0=禁用</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/multi-content</span></div>
|
||||
<div class="api-desc">◆ 批量获取多个章节正文</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_ids</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID列表,多个ID用逗号分隔,如:123,456,789</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">text_mode</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">image_mode</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">ignore_failed</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">忽略失败章节:1/true=忽略,0/false=不忽略</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/item-summary</span></div>
|
||||
<div class="api-desc">◆ 获取章节摘要信息</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_ids</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID列表,逗号分隔</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 段评相关 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">段评相关</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/review</span></div>
|
||||
<div class="api-desc">◆ 获取段落评论、回复</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">para</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">段落索引(从0开始)</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">comment_id</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">评论ID(获取回复时需要)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/review_html</span></div>
|
||||
<div class="api-desc">◆ 获取段落评论HTML页面</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">book_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">书籍ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">para</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">段落索引(从0开始)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多媒体 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">多媒体</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/manga</span></div>
|
||||
<div class="api-desc">◆ 获取漫画内容</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_ids</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">output</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">输出格式:url/proxy/data_uri,默认url</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/audio</span></div>
|
||||
<div class="api-desc">◆ 获取音频信息</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_ids</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">tone_id</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">音色ID,默认1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/video</span></div>
|
||||
<div class="api-desc">◆ 获取视频信息</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">item_ids</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">章节ID</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">分类</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/category-front</span></div>
|
||||
<div class="api-desc">◆ 获取分类首页</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">tab</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">标签页,默认0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/category-landing</span></div>
|
||||
<div class="api-desc">◆ 获取分类详情页</div>
|
||||
<div class="api-params">
|
||||
<div class="param-item">
|
||||
<span class="param-name">category_id</span>
|
||||
<span class="param-required">必需</span>
|
||||
<span class="param-desc">分类ID</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">offset</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">偏移量,默认0</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-name">limit</span>
|
||||
<span class="param-optional">可选</span>
|
||||
<span class="param-desc">每页数量,默认20</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统 -->
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">系统</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/status</span></div>
|
||||
<div class="api-desc">◆ 系统状态页面(当前页面)</div>
|
||||
</div>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<div><span class="api-method">GET</span><span class="api-path">/self-info</span></div>
|
||||
<div class="api-desc">◆ 设备信息(包含敏感数据)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
uptimeStr,
|
||||
startTime.Format("2006-01-02 15:04:05"),
|
||||
signatureDetails,
|
||||
getDBTotalSize(),
|
||||
getDBUsedSize(),
|
||||
getDBFreeSize(),
|
||||
poolStatus["status"],
|
||||
poolStatus["total"],
|
||||
poolStatus["valid"],
|
||||
poolStatus["invalid"],
|
||||
poolStatus["current_index"],
|
||||
stats["total"],
|
||||
stats["search"],
|
||||
stats["detail"],
|
||||
stats["catalog"],
|
||||
stats["content"],
|
||||
stats["manga"],
|
||||
stats["audio"],
|
||||
stats["video"],
|
||||
stats["category"],
|
||||
getDBCacheCount(),
|
||||
getCacheStatus(),
|
||||
getCleanupStatus(),
|
||||
getCleanupInterval(),
|
||||
)
|
||||
fmt.Fprint(w, html)
|
||||
|
||||
statusTmpl.Execute(w, map[string]interface{}{
|
||||
"NowTime": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"Uptime": uptimeStr,
|
||||
"StartTime": startTime.Format("2006-01-02 15:04:05"),
|
||||
"Signature": signatureDetails,
|
||||
"DBTotal": getDBTotalSize(),
|
||||
"DBUsed": getDBUsedSize(),
|
||||
"DBFree": getDBFreeSize(),
|
||||
"PoolStatus": poolStatus["status"],
|
||||
"PoolTotal": poolStatus["total"],
|
||||
"PoolValid": poolStatus["valid"],
|
||||
"PoolInvalid": poolStatus["invalid"],
|
||||
"PoolIndex": poolStatus["current_index"],
|
||||
"StatTotal": stats["total"],
|
||||
"StatSearch": stats["search"],
|
||||
"StatDetail": stats["detail"],
|
||||
"StatCatalog": stats["catalog"],
|
||||
"StatContent": stats["content"],
|
||||
"StatManga": stats["manga"],
|
||||
"StatAudio": stats["audio"],
|
||||
"StatVideo": stats["video"],
|
||||
"StatCategory": stats["category"],
|
||||
"CacheCount": getDBCacheCount(),
|
||||
"CacheStatus": getCacheStatus(),
|
||||
"CleanupStatus": getCleanupStatus(),
|
||||
"CleanupInterval": getCleanupInterval(),
|
||||
})
|
||||
}
|
||||
|
||||
// getCacheStatus 获取缓存状态
|
||||
@@ -960,23 +209,23 @@ func getDBFreeSize() string {
|
||||
}
|
||||
freeSize, okFree := stats["free_size_bytes"].(int64)
|
||||
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
||||
|
||||
|
||||
if !okFree {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
|
||||
if freeSize == 0 {
|
||||
return "0 B"
|
||||
}
|
||||
|
||||
|
||||
freeSizeStr := formatBytes(freeSize)
|
||||
|
||||
|
||||
// 如果有总大小,计算并显示百分比
|
||||
if okTotal && totalSize > 0 {
|
||||
percentage := float64(freeSize) * 100 / float64(totalSize)
|
||||
return fmt.Sprintf("%s (%.1f%%)", freeSizeStr, percentage)
|
||||
}
|
||||
|
||||
|
||||
return freeSizeStr
|
||||
}
|
||||
|
||||
@@ -988,18 +237,18 @@ func getDBUsagePercentage() float64 {
|
||||
}
|
||||
usedSize, okUsed := stats["used_size_bytes"].(int64)
|
||||
totalSize, okTotal := stats["total_size_bytes"].(int64)
|
||||
|
||||
|
||||
if !okUsed || !okTotal || totalSize == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
return float64(usedSize) * 100 / float64(totalSize)
|
||||
}
|
||||
|
||||
// getDeviceKeyPoolStatus 获取设备密钥池状态
|
||||
func getDeviceKeyPoolStatus() map[string]interface{} {
|
||||
pool := fqapi.GetDeviceKeyPool()
|
||||
|
||||
|
||||
if !pool.IsEnabled() {
|
||||
return map[string]interface{}{
|
||||
"status": "未启用",
|
||||
@@ -1009,7 +258,7 @@ func getDeviceKeyPoolStatus() map[string]interface{} {
|
||||
"current_index": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stats := pool.GetStats()
|
||||
return map[string]interface{}{
|
||||
"status": "已启用",
|
||||
@@ -1019,4 +268,3 @@ func getDeviceKeyPoolStatus() map[string]interface{} {
|
||||
"current_index": stats["current_index"],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user