This commit is contained in:
www.xueximeng.com
2025-06-07 19:19:58 +08:00
parent 59834ab759
commit f7bf87022f
26 changed files with 3690 additions and 183 deletions

View File

@@ -99,6 +99,9 @@
目前内置了8个解析采集
![image|690x397](docs/27.jpg)
## 全面支持管理后台设置网站信息
![image|690x397](docs/28.gif)
---
# 美漫资源共建平台部署
@@ -593,6 +596,9 @@ sudo systemctl status nginx
- Space Ghost Coast to Coast太空幽灵海岸到海岸
# 更新日志
-202506071917
✅ 全面支持管理后台设置网站信息
- 202506061607
✅ 优化悬浮按钮样式问题
✅ 修复最近播放恢复播放失败问题

BIN
docs/28.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,2 +1,3 @@
BASE_URL=https://dm.xueximeng.com
ASSETS_PATH=../assets
ASSETS_PATH=../assets
# ASSETS_PATH=../data/assets

View File

@@ -14,12 +14,63 @@
<meta property="og:type" content="website">
<meta property="og:url" content="https://dm.xueximeng.com/">
<meta property="og:image" content="https://dm.xueximeng.com/favicon.ico">
<!-- 添加favicon -->
<link rel="icon" href="/favicon.ico">
<!-- 添加动态favicon -->
<link rel="icon" href="/favicon.ico" id="dynamic-favicon">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<!-- 外部样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<!-- 动态检测favicon -->
<script>
// 添加会话级缓存变量,避免重复检测
let faviconChecked = false;
let customFaviconExists = false;
// 检查是否存在用户上传的favicon
function checkFavicon() {
// 如果已经检测过,直接使用缓存结果
if (faviconChecked) {
const dynamicFavicon = document.getElementById('dynamic-favicon');
if (customFaviconExists) {
dynamicFavicon.href = '/assets/public/favicon.ico';
} else {
dynamicFavicon.href = '/favicon.ico';
}
return;
}
const dynamicFavicon = document.getElementById('dynamic-favicon');
// 只在首次检测时使用时间戳
const timestamp = new Date().getTime();
// 检测自定义favicon是否存在
fetch('/assets/public/favicon.ico?' + timestamp, { method: 'HEAD' })
.then(response => {
faviconChecked = true; // 标记为已检测
if (response.ok) {
// 如果自定义favicon存在则使用它不带时间戳
customFaviconExists = true;
dynamicFavicon.href = '/assets/public/favicon.ico';
console.log('使用用户上传的favicon');
} else {
// 如果不存在使用默认favicon不带时间戳
customFaviconExists = false;
dynamicFavicon.href = '/favicon.ico';
console.log('使用默认favicon');
}
})
.catch(error => {
// 出错时使用默认favicon
faviconChecked = true;
customFaviconExists = false;
console.error('检测favicon失败:', error);
dynamicFavicon.href = '/favicon.ico';
});
}
// 页面加载完成后检测favicon
window.addEventListener('load', checkFavicon);
</script>
</head>
<body>
<div id="app"></div>

View File

@@ -16,7 +16,8 @@
"express": "^5.1.0",
"video.js": "^8.22.0",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
"vue-router": "^4.2.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
@@ -2002,6 +2003,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2214,6 +2220,17 @@
"vue": "^3.2.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -22,7 +22,8 @@
"express": "^5.1.0",
"video.js": "^8.22.0",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
"vue-router": "^4.2.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",

View File

@@ -0,0 +1,5 @@
<head>
<!-- ... existing tags ... -->
<link rel="icon" href="/public/favicon.ico">
<!-- ... existing tags ... -->
</head>

View File

@@ -2,19 +2,19 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://dm.xueximeng.com/</loc>
<lastmod>2025-06-04</lastmod>
<lastmod>2025-06-07</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/submit</loc>
<lastmod>2025-06-04</lastmod>
<lastmod>2025-06-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/about</loc>
<lastmod>2025-06-04</lastmod>
<lastmod>2025-06-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
@@ -24,36 +24,78 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/24</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/5</loc>
<lastmod>2025-05-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/11</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/12</loc>
<lastmod>2025-05-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/13</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/34</loc>
<lastmod>2025-06-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/11</loc>
<lastmod>2025-06-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/46</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/12</loc>
<lastmod>2025-05-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/18</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/26</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/36</loc>
<lastmod>2025-06-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/1</loc>
<lastmod>2025-05-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/2</loc>
<lastmod>2025-05-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/6</loc>
<lastmod>2025-05-30</lastmod>
@@ -72,18 +114,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/18</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/26</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/29</loc>
<lastmod>2025-06-03</lastmod>
@@ -91,14 +121,56 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/34</loc>
<loc>https://dm.xueximeng.com/resource/31</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/32</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/2</loc>
<lastmod>2025-05-24</lastmod>
<loc>https://dm.xueximeng.com/resource/37</loc>
<lastmod>2025-06-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/39</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/40</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/41</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/42</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/48</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/50</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -174,12 +246,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/24</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/25</loc>
<lastmod>2025-05-31</lastmod>
@@ -204,22 +270,52 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/31</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/32</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/33</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/35</loc>
<lastmod>2025-06-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/38</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/43</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/44</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/45</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/47</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/49</loc>
<lastmod>2025-06-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>

View File

@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试设置API</title>
<style>
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
margin-bottom: 10px;
font-family: monospace;
resize: vertical;
}
button {
background: #4f46e5;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background: #4338ca;
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
background-color: #f8f8f8;
border: 1px solid #ddd;
max-height: 300px;
overflow: auto;
}
.success {
background-color: #d1fae5;
border-color: #10b981;
}
.error {
background-color: #fee2e2;
border-color: #ef4444;
}
</style>
</head>
<body>
<h1>设置API测试工具</h1>
<div class="card">
<h2>测试页脚设置API</h2>
<p>这个工具可以帮助您测试页脚设置API发送PUT请求到 <code>/api/settings/footer</code> 端点。</p>
<div>
<label for="token"><strong>认证令牌 (Bearer Token)</strong></label>
<input type="text" id="token" style="width: 100%; padding: 8px; margin: 5px 0 15px;" placeholder="粘贴您的认证令牌">
</div>
<div>
<label for="payload"><strong>请求数据 (JSON)</strong></label>
<textarea id="payload" placeholder="输入JSON格式的请求数据">{
"setting_value": {
"links": [
{ "text": "关于我们", "url": "/about", "type": "internal" },
{ "text": "GitHub", "url": "https://github.com/fish2018/GoComicMosaic", "icon": "bi bi-github", "type": "external", "title": "查看GitHub源码" }
],
"copyright": "© 2025 美漫资源共建. 保留所有权利",
"show_visitor_count": true
}
}</textarea>
</div>
<div>
<button id="sendRequest">发送PUT请求</button>
<button id="checkToken" style="background: #059669;">检查令牌</button>
</div>
<div id="result" class="result" style="display: none;">
待发送请求...
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 从localStorage中获取token
try {
const savedToken = localStorage.getItem('accessToken');
if (savedToken) {
document.getElementById('token').value = savedToken;
console.log('已从localStorage加载令牌');
}
} catch (e) {
console.error('读取localStorage失败:', e);
}
// 发送请求按钮
document.getElementById('sendRequest').addEventListener('click', async function() {
const token = document.getElementById('token').value.trim();
const payload = document.getElementById('payload').value.trim();
const resultElement = document.getElementById('result');
resultElement.style.display = 'block';
resultElement.className = 'result';
resultElement.textContent = '发送请求中...';
if (!token) {
resultElement.textContent = '错误: 请提供认证令牌';
resultElement.className = 'result error';
return;
}
try {
let payloadObj = JSON.parse(payload);
// 创建一个XHR请求
const xhr = new XMLHttpRequest();
xhr.open('PUT', '/api/settings/footer', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.withCredentials = true;
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resultElement.textContent = `请求成功 (${xhr.status} ${xhr.statusText}):\n\n${xhr.responseText}`;
resultElement.className = 'result success';
} else {
resultElement.textContent = `请求失败 (${xhr.status} ${xhr.statusText}):\n\n${xhr.responseText}`;
resultElement.className = 'result error';
}
};
xhr.onerror = function() {
resultElement.textContent = '网络错误,请检查控制台';
resultElement.className = 'result error';
};
const requestStart = new Date();
xhr.send(payload);
console.log('已发送请求:', payload);
console.log('请求头:', {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token.substring(0, 10)}...` // 只显示部分令牌
});
} catch (e) {
resultElement.textContent = `请求错误: ${e.message}`;
resultElement.className = 'result error';
}
});
// 检查令牌按钮
document.getElementById('checkToken').addEventListener('click', function() {
const token = document.getElementById('token').value.trim();
const resultElement = document.getElementById('result');
resultElement.style.display = 'block';
if (!token) {
resultElement.textContent = '错误: 请提供认证令牌';
resultElement.className = 'result error';
return;
}
try {
// 尝试解码令牌
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
resultElement.textContent = '无效的JWT令牌格式';
resultElement.className = 'result error';
return;
}
// 解码payload部分
const payload = JSON.parse(atob(tokenParts[1]));
const expDate = new Date(payload.exp * 1000);
const now = new Date();
const isExpired = expDate < now;
resultElement.textContent = `令牌信息:\n\n`;
resultElement.textContent += `有效期至: ${expDate.toLocaleString()}\n`;
resultElement.textContent += `当前时间: ${now.toLocaleString()}\n`;
resultElement.textContent += `是否过期: ${isExpired ? '已过期' : '有效'}\n\n`;
resultElement.textContent += `载荷数据:\n${JSON.stringify(payload, null, 2)}`;
resultElement.className = isExpired ? 'result error' : 'result success';
} catch (e) {
resultElement.textContent = `令牌解析错误: ${e.message}\n可能不是有效的JWT格式`;
resultElement.className = 'result error';
}
});
});
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<div class="brand">
<router-link to="/" class="brand-link">
<i class="bi bi-collection-play brand-icon"></i>
<span class="brand-text">美漫资源共建</span>
<span class="brand-text">{{ siteInfo.logoText }}</span>
</router-link>
</div>
@@ -62,25 +62,44 @@
<div class="container footer-inner">
<!-- 页脚布局 -->
<div class="footer-row">
<router-link to="/about" class="footer-link">关于我们</router-link>
<a href="https://t.me/xueximeng" target="_blank" class="footer-link" title="加入Telegram群组">
<i class="bi bi-telegram"></i>
</a>
<a href="https://github.com/fish2018/GoComicMosaic" target="_blank" class="footer-link" title="查看GitHub源码">
<i class="bi bi-github"></i>
</a>
<a href="/streams" target="_blank" class="footer-link">在线点播</a>
<a href="https://mdsub.top/" target="_blank" class="footer-link">漫迪小站</a>
<a href="https://www.kangfuzhongx.in/" target="_blank" class="footer-link">三次元成瘾者康复中心</a>
<span class="footer-link">总访问量 <span id="busuanzi_value_site_pv">0</span></span>
<template v-if="footerSettings">
<!-- 动态生成链接 -->
<template v-for="(link, index) in footerSettings.links" :key="index">
<!-- 内部链接 -->
<router-link v-if="link.type === 'internal'" :to="link.url" class="footer-link" :title="link.title">
<i v-if="link.icon" :class="link.icon"></i>
<span>{{ link.text }}</span>
</router-link>
<!-- 外部链接 -->
<a v-else :href="link.url" target="_blank" class="footer-link" :title="link.title">
<i v-if="link.icon" :class="link.icon"></i>
<span v-if="!link.icon">{{ link.text }}</span>
</a>
</template>
<!-- 访问统计 -->
<span class="footer-link" v-if="footerSettings.show_visitor_count">总访问量 <span id="busuanzi_value_site_pv">0</span></span>
</template>
<!-- 在设置加载前的默认链接或加载失败时的回退链接 -->
<template v-else>
<router-link to="/about" class="footer-link">关于我们</router-link>
<a href="https://t.me/xueximeng" target="_blank" class="footer-link" title="加入Telegram群组">
<i class="bi bi-telegram"></i>
</a>
<a href="https://github.com/fish2018/GoComicMosaic" target="_blank" class="footer-link" title="查看GitHub源码">
<i class="bi bi-github"></i>
</a>
</template>
</div>
<!-- 分隔线 -->
<div class="footer-divider"></div>
<!-- 版权信息 -->
<div class="copyright">
<p>&copy; 2025 美漫资源共建. 保留所有权利</p>
<p>{{ footerSettings?.copyright || '&copy; 2025 美漫资源共建. 保留所有权利' }}</p>
</div>
</div>
</footer>
@@ -106,12 +125,20 @@ import { isAuthenticated, getCurrentUser, logout, setupAxiosInterceptors } from
import { useRoute, useRouter } from 'vue-router'
import LocalSearch from './components/LocalSearch.vue'
import axios from 'axios'
import { getSiteSettings } from './utils/api'
const route = useRoute()
const router = useRouter()
const isLoggedIn = ref(false)
const currentUser = ref({})
const footerPreloaded = ref(false)
const footerSettings = ref(null)
const siteInfo = ref({
title: '美漫资源共建',
logoText: '美漫资源共建',
description: '美漫共建平台是一个开源的美漫资源共享网站,用户可以自由提交动漫信息,像马赛克一样,由多方贡献拼凑成完整资源。',
keywords: '美漫, 动漫资源, 资源共享, 开源平台, 美漫共建'
})
// 计算当前是否在管理员页面
const isAdminPage = computed(() => {
@@ -130,6 +157,47 @@ const handleLogout = () => {
checkAuthState()
}
// 获取页脚设置
const loadFooterSettings = async () => {
try {
// 使用InfoManager获取缓存的信息
const infoManager = (await import('./utils/InfoManager')).default;
footerSettings.value = await infoManager.getFooterInfo();
console.log('页脚设置加载成功:', footerSettings.value);
} catch (error) {
console.error('获取页脚设置失败:', error);
// 使用默认设置
footerSettings.value = {
links: [
{ text: "关于我们", url: "/about", type: "internal" },
{ text: "Telegram", url: "https://t.me/xueximeng", icon: "bi bi-telegram", type: "external", title: "加入Telegram群组" },
{ text: "GitHub", url: "https://github.com/fish2018/GoComicMosaic", icon: "bi bi-github", type: "external", title: "查看GitHub源码" },
{ text: "在线点播", url: "/streams", type: "internal" },
{ text: "漫迪小站", url: "https://mdsub.top/", type: "external" },
{ text: "三次元成瘾者康复中心", url: "https://www.kangfuzhongx.in/", type: "external" },
],
copyright: "© 2025 美漫资源共建. 保留所有权利",
show_visitor_count: true
};
}
}
// 加载网站基本信息
const loadSiteInfo = async () => {
try {
const infoManager = (await import('./utils/InfoManager')).default;
const info = await infoManager.getSiteBasicInfo();
siteInfo.value = info;
console.log('网站基本信息加载成功:', siteInfo.value);
// 更新页面标题和meta信息
updateMetaInfo(route);
} catch (error) {
console.error('获取网站基本信息失败:', error);
// 默认值已在siteInfo的ref初始化中设置
}
}
// 滚动到页面顶部
const scrollToTop = () => {
window.scrollTo({
@@ -210,39 +278,44 @@ const handleScroll = () => {
// 更新页面标题和meta信息的函数
const updateMetaInfo = (to) => {
// 设置默认值
const defaultTitle = '美漫资源共建 - 动漫爱好者共同贡献的美漫资源库'
const defaultDescription = '美漫共建平台是一个开源的美漫资源共享网站,用户可以自由提交动漫信息,像马赛克一样,由多方贡献拼凑成完整资源。'
const defaultKeywords = '美漫, 动漫资源, 资源共享, 开源平台, 美漫共建'
const defaultTitle = siteInfo.value.title;
const defaultDescription = siteInfo.value.description;
const defaultKeywords = siteInfo.value.keywords;
// 获取路由的meta信息
const title = to.meta.title || defaultTitle
const description = to.meta.description || defaultDescription
const keywords = to.meta.keywords || defaultKeywords
const title = to.meta.title || defaultTitle;
const description = to.meta.description || defaultDescription;
const keywords = to.meta.keywords || defaultKeywords;
// 更新页面标题
document.title = title
document.title = title;
// 更新meta描述
let metaDescription = document.querySelector('meta[name="description"]')
let metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
metaDescription.setAttribute('content', description)
metaDescription.setAttribute('content', description);
}
// 更新meta关键词
let metaKeywords = document.querySelector('meta[name="keywords"]')
let metaKeywords = document.querySelector('meta[name="keywords"]');
if (metaKeywords) {
metaKeywords.setAttribute('content', keywords)
metaKeywords.setAttribute('content', keywords);
}
// 更新Open Graph标签
let ogTitle = document.querySelector('meta[property="og:title"]')
let ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) {
ogTitle.setAttribute('content', title)
ogTitle.setAttribute('content', title);
}
let ogDescription = document.querySelector('meta[property="og:description"]')
let ogDescription = document.querySelector('meta[property="og:description"]');
if (ogDescription) {
ogDescription.setAttribute('content', description)
ogDescription.setAttribute('content', description);
}
// 检查并更新favicon
if (typeof window.checkFavicon === 'function') {
window.checkFavicon();
}
}
@@ -251,36 +324,40 @@ onMounted(() => {
// 设置路由afterEach钩子
router.afterEach((to) => {
// 更新meta信息
updateMetaInfo(to)
updateMetaInfo(to);
// 回到页面顶部(可选)
// window.scrollTo(0, 0)
})
});
checkAuthState()
checkAuthState();
// 初始加载时设置meta信息
updateMetaInfo(route)
updateMetaInfo(route);
// 设置axios拦截器
setupAxiosInterceptors(() => {
logout()
isLoggedIn.value = false
})
logout();
isLoggedIn.value = false;
});
// 加载页脚设置和网站基本信息
loadFooterSettings();
loadSiteInfo();
// 监听滚动事件
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('scroll', handleScroll, { passive: true });
// 优化滚动性能
optimizeScrollPerformance()
optimizeScrollPerformance();
// 确保初始渲染后预加载底部元素
nextTick(() => {
setTimeout(preloadFooterContent, 1000);
})
});
// 添加beforeunload事件监听器
window.addEventListener('beforeunload', clearPaginationStorage)
window.addEventListener('beforeunload', clearPaginationStorage);
// 添加不蒜子访问统计脚本
const bszScript = document.createElement('script');
@@ -291,9 +368,9 @@ onMounted(() => {
// 页面卸载时移除事件监听器
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('beforeunload', clearPaginationStorage)
})
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('beforeunload', clearPaginationStorage);
});
</script>
<style>
@@ -914,7 +991,7 @@ body {
}
}
@media (min-width: 993px) and (max-width: 1200px) {
@media(max-width: 1200px) {
.btn-custom {
padding: 0.6rem 1.2rem;
}
@@ -929,7 +1006,7 @@ body {
}
}
@media (max-width: 992px) {
@media (max-width: 1200px) {
.header-inner {
display: flex;
flex-wrap: wrap;

View File

@@ -1,6 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import router, { getDynamicRouter } from './router'
import axios from 'axios'
import { setupAxiosInterceptors } from './utils/auth'
@@ -10,6 +10,28 @@ axios.defaults.baseURL = '/api'
// 设置请求拦截器
setupAxiosInterceptors()
// 创建Vue应用但暂不挂载
const app = createApp(App)
app.use(router)
app.mount('#app')
// 异步初始化应用
async function initApp() {
try {
// 获取动态配置的路由
const dynamicRouter = await getDynamicRouter();
// 使用动态路由而不是默认路由
app.use(dynamicRouter);
console.log('已应用动态路由配置');
// 挂载应用
app.mount('#app');
} catch (error) {
console.error('初始化动态路由失败,使用默认路由:', error);
// 出错时使用默认路由
app.use(router);
app.mount('#app');
}
}
// 执行初始化
initApp();

View File

@@ -1,16 +1,18 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { isAuthenticated } from '../utils/auth'
import infoManager from '../utils/InfoManager'
const routes = [
// 基础路由配置
const baseRoutes = [
{
path: '/',
name: 'Home',
component: Home,
meta: {
title: '美漫资源共建 - 动漫爱好者共同贡献的资源平台',
description: '美漫共建平台是一个开源的美漫资源共享网站,用户可以自由提交动漫信息,像马赛克一样,由多方贡献拼凑成完整资源。',
keywords: '美漫, 动漫资源, 资源共享, 开源平台, 美漫共建'
title: 'home_title', // 使用键名,后续会被替换为实际内容
description: 'home_description',
keywords: 'home_keywords'
}
},
{
@@ -18,9 +20,9 @@ const routes = [
name: 'ResourceDetail',
component: () => import('../views/ResourceDetail.vue'),
meta: {
title: '资源详情 - 美漫资源共建平台',
description: '查看详细的动漫资源信息,包括简介、图片、下载链接等。在这里您可以浏览由社区贡献的美漫资源详情。',
keywords: '美漫资源, 动漫详情, 资源下载, 美漫共建'
title: 'resource_detail_title',
description: 'resource_detail_description',
keywords: 'resource_detail_keywords'
}
},
{
@@ -28,9 +30,9 @@ const routes = [
name: 'SubmitResource',
component: () => import('../views/SubmitResource.vue'),
meta: {
title: '提交资源 - 美漫资源共建平台',
description: '在这里提交您收集的美漫资源,包括标题、简介、链接等信息,与社区共同构建完整的资源库。',
keywords: '提交资源, 分享美漫, 资源贡献, 美漫共建'
title: 'submit_resource_title',
description: 'submit_resource_description',
keywords: 'submit_resource_keywords'
}
},
{
@@ -38,9 +40,9 @@ const routes = [
name: 'Login',
component: () => import('../views/Login.vue'),
meta: {
title: '用户登录 - 美漫资源共建平台',
description: '登录美漫资源共建平台,管理您的资源贡献并参与社区建设。',
keywords: '用户登录, 账号登录, 美漫共建'
title: 'login_title',
description: 'login_description',
keywords: 'login_keywords'
}
},
{
@@ -49,9 +51,9 @@ const routes = [
component: () => import('../views/Admin.vue'),
meta: {
requiresAuth: true,
title: '管理后台 - 美漫资源共建平台',
description: '美漫资源共建平台管理后台,用于管理用户提交的资源和维护网站内容。',
keywords: '管理后台, 资源审核, 美漫共建'
title: 'admin_title',
description: 'admin_description',
keywords: 'admin_keywords'
}
},
{
@@ -60,9 +62,9 @@ const routes = [
component: () => import('../views/ResourceReview.vue'),
meta: {
requiresAuth: true,
title: '资源审核 - 美漫资源共建平台',
description: '审核用户提交的美漫资源,确保内容质量和合规性。',
keywords: '资源审核, 内容审核, 美漫共建'
title: 'resource_review_title',
description: 'resource_review_description',
keywords: 'resource_review_keywords'
}
},
{
@@ -70,9 +72,9 @@ const routes = [
name: 'About',
component: () => import('../views/About.vue'),
meta: {
title: '关于我们 - 美漫资源共建平台',
description: '了解美漫资源共建平台的宗旨、团队和发展历程。我们致力于为动漫爱好者提供优质的资源共享环境。',
keywords: '关于我们, 平台介绍, 团队介绍, 美漫共建'
title: 'about_title',
description: 'about_description',
keywords: 'about_keywords'
}
},
{
@@ -84,32 +86,210 @@ const routes = [
direct_url: route.query.direct_url
}),
meta: {
title: '流媒体内容 - 美漫资源共建平台',
description: '浏览和观看各种高质量的动漫流媒体内容,包括动画、电影和连续剧。',
keywords: '流媒体内容, 动漫视频, 在线观看, 美漫共建'
title: 'streams_title',
description: 'streams_description',
keywords: 'streams_keywords'
}
}
]
// 默认的路由元信息,当配置中没有对应值时使用
const defaultMetaInfo = {
// 首页
home_title: '美漫资源共建 - 动漫爱好者共同贡献的资源平台',
home_description: '美漫共建平台是一个开源的美漫资源共享网站,用户可以自由提交动漫信息,像马赛克一样,由多方贡献拼凑成完整资源。',
home_keywords: '美漫, 动漫资源, 资源共享, 开源平台, 美漫共建',
// 资源详情页
resource_detail_title: '资源详情 - 美漫资源共建平台',
resource_detail_description: '查看详细的动漫资源信息,包括简介、图片、下载链接等。在这里您可以浏览由社区贡献的美漫资源详情。',
resource_detail_keywords: '美漫资源, 动漫详情, 资源下载, 美漫共建',
// 提交资源页
submit_resource_title: '提交资源 - 美漫资源共建平台',
submit_resource_description: '在这里提交您收集的美漫资源,包括标题、简介、链接等信息,与社区共同构建完整的资源库。',
submit_resource_keywords: '提交资源, 分享美漫, 资源贡献, 美漫共建',
// 登录页
login_title: '用户登录 - 美漫资源共建平台',
login_description: '登录美漫资源共建平台,管理您的资源贡献并参与社区建设。',
login_keywords: '用户登录, 账号登录, 美漫共建',
// 管理后台页
admin_title: '管理后台 - 美漫资源共建平台',
admin_description: '美漫资源共建平台管理后台,用于管理用户提交的资源和维护网站内容。',
admin_keywords: '管理后台, 资源审核, 美漫共建',
// 资源审核页
resource_review_title: '资源审核 - 美漫资源共建平台',
resource_review_description: '审核用户提交的美漫资源,确保内容质量和合规性。',
resource_review_keywords: '资源审核, 内容审核, 美漫共建',
// 关于我们页
about_title: '关于我们 - 美漫资源共建平台',
about_description: '了解美漫资源共建平台的宗旨、团队和发展历程。我们致力于为动漫爱好者提供优质的资源共享环境。',
about_keywords: '关于我们, 平台介绍, 团队介绍, 美漫共建',
// 流媒体内容页
streams_title: '流媒体内容 - 美漫资源共建平台',
streams_description: '浏览和观看各种高质量的动漫流媒体内容,包括动画、电影和连续剧。',
streams_keywords: '流媒体内容, 动漫视频, 在线观看, 美漫共建'
}
// 异步函数,创建路由并应用动态配置
async function createDynamicRouter() {
// 尝试获取动态配置
let routeMetaInfo = { ...defaultMetaInfo };
try {
console.log('开始获取网站配置信息...');
// 获取网站信息配置
const siteInfo = await infoManager.getSiteBasicInfo();
console.log('成功获取网站基本信息:', siteInfo.title);
// 获取路由meta配置如果存在的话
if (siteInfo.routeMeta) {
console.log('找到路由Meta配置');
routeMetaInfo = { ...defaultMetaInfo, ...siteInfo.routeMeta };
} else {
console.log('未找到路由Meta配置使用默认配置');
}
// 如果没有特定页面的配置,使用基本网站标题生成
const siteName = siteInfo.title || '美漫资源共建平台';
console.log('使用网站名称:', siteName);
// 为所有没有具体配置的页面设置默认值
Object.keys(defaultMetaInfo).forEach(key => {
if (!routeMetaInfo[key] && key.endsWith('_title')) {
const pageName = key.replace('_title', '');
let pageTitle = '';
// 根据页面标识生成合理的标题
switch(pageName) {
case 'home':
pageTitle = siteName;
break;
case 'resource_detail':
pageTitle = `资源详情 - ${siteName}`;
break;
case 'submit_resource':
pageTitle = `提交资源 - ${siteName}`;
break;
case 'login':
pageTitle = `用户登录 - ${siteName}`;
break;
case 'admin':
pageTitle = `管理后台 - ${siteName}`;
break;
case 'resource_review':
pageTitle = `资源审核 - ${siteName}`;
break;
case 'about':
pageTitle = `关于我们 - ${siteName}`;
break;
case 'streams':
pageTitle = `流媒体内容 - ${siteName}`;
break;
default:
pageTitle = siteName;
}
routeMetaInfo[key] = pageTitle;
}
});
console.log('动态路由配置已加载');
} catch (error) {
console.error('加载动态路由配置失败,使用默认值:', error);
}
// 应用配置到路由
console.log('开始应用配置到路由...');
const routes = baseRoutes.map(route => {
const newRoute = { ...route };
// 替换meta信息中的占位符为实际内容
if (newRoute.meta) {
const meta = { ...newRoute.meta };
if (meta.title && routeMetaInfo[meta.title]) {
console.log(`替换路由[${newRoute.name}]标题: ${meta.title} => ${routeMetaInfo[meta.title]}`);
meta.title = routeMetaInfo[meta.title];
} else if (meta.title) {
console.log(`警告: 未找到路由[${newRoute.name}]的标题配置: ${meta.title}`);
}
if (meta.description && routeMetaInfo[meta.description]) {
meta.description = routeMetaInfo[meta.description];
}
if (meta.keywords && routeMetaInfo[meta.keywords]) {
meta.keywords = routeMetaInfo[meta.keywords];
}
newRoute.meta = meta;
}
return newRoute;
});
console.log('创建带有动态配置的路由器');
const router = createRouter({
history: createWebHistory(),
routes
});
// 导航守卫,检查是否需要登录
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated()) {
next({
path: '/login',
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
return router;
}
// 创建一个简单的路由器作为默认导出
// 实际应用中会被替换为动态配置的路由器
const router = createRouter({
history: createWebHistory(),
routes
})
routes: baseRoutes
});
// 导航守卫,检查是否需要登录
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next()
// 初始化动态路由Promise
let dynamicRouterPromise = null;
// 导出获取动态路由器的函数
export const getDynamicRouter = async () => {
// 如果已经有Promise直接返回
if (dynamicRouterPromise) {
return dynamicRouterPromise;
}
})
// 否则创建新的Promise并返回
dynamicRouterPromise = createDynamicRouter();
try {
// 等待路由创建完成并返回
const dynamicRouter = await dynamicRouterPromise;
console.log('动态路由器创建成功');
return dynamicRouter;
} catch (error) {
console.error('动态路由器创建失败:', error);
// 出错时返回默认路由
return router;
}
};
export default router
// 导出默认路由器
export default router;

View File

@@ -0,0 +1,227 @@
import { getSiteSettings, updateSiteSettings } from './api';
// 缓存过期时间(毫秒)- 默认5分钟
const CACHE_EXPIRATION = 5 * 60 * 1000;
// 存储在LocalStorage中的键名
const STORAGE_KEY = 'site_info_cache';
const VERSION_KEY = 'site_info_version';
class InfoManager {
constructor() {
// 单例模式
if (InfoManager.instance) {
return InfoManager.instance;
}
InfoManager.instance = this;
// 初始化
this.cache = null;
this.lastFetchTime = 0;
this.version = this.getStoredVersion() || 1;
this.isLoading = false;
// 从本地存储加载缓存
this.loadFromStorage();
}
/**
* 从localStorage中加载缓存的网站信息
*/
loadFromStorage() {
try {
const cachedData = localStorage.getItem(STORAGE_KEY);
if (cachedData) {
const parsed = JSON.parse(cachedData);
this.cache = parsed.data;
this.lastFetchTime = parsed.timestamp;
console.log('已从本地存储加载网站信息缓存');
}
} catch (error) {
console.error('加载缓存的网站信息失败:', error);
// 清除可能损坏的缓存
localStorage.removeItem(STORAGE_KEY);
}
}
/**
* 保存数据到localStorage
*/
saveToStorage(data) {
try {
const cacheObject = {
timestamp: Date.now(),
data: data
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(cacheObject));
console.log('网站信息缓存已保存到本地存储');
} catch (error) {
console.error('保存网站信息缓存失败:', error);
}
}
/**
* 获取存储的版本号
*/
getStoredVersion() {
const version = localStorage.getItem(VERSION_KEY);
return version ? parseInt(version, 10) : null;
}
/**
* 保存版本号
*/
saveVersion(version) {
localStorage.setItem(VERSION_KEY, version.toString());
this.version = version;
}
/**
* 检查缓存是否过期
*/
isCacheExpired() {
// 如果没有缓存,视为过期
if (!this.cache) return true;
// 检查缓存是否超过指定的过期时间
return Date.now() - this.lastFetchTime > CACHE_EXPIRATION;
}
/**
* 获取网站信息,优先使用缓存
*/
async getInfo() {
// 如果有缓存且未过期,直接返回缓存
if (this.cache && !this.isCacheExpired()) {
console.log('使用缓存的网站信息');
return this.cache;
}
// 防止并发请求
if (this.isLoading) {
console.log('正在获取网站信息,等待...');
// 等待当前请求完成
return new Promise(resolve => {
const checkCache = () => {
if (!this.isLoading) {
resolve(this.cache);
} else {
setTimeout(checkCache, 100);
}
};
checkCache();
});
}
// 从服务器获取最新数据
this.isLoading = true;
try {
console.log('从服务器获取最新网站信息');
const response = await getSiteSettings('info');
this.cache = response.setting_value;
this.lastFetchTime = Date.now();
// 保存到本地存储
this.saveToStorage(this.cache);
// 检查并更新favicon
if (typeof window.checkFavicon === 'function') {
window.checkFavicon();
}
return this.cache;
} catch (error) {
console.error('获取网站信息失败:', error);
// 如果有缓存,返回过期的缓存作为降级
if (this.cache) {
console.log('使用过期的缓存作为降级');
return this.cache;
}
// 如果没有缓存,抛出错误
throw error;
} finally {
this.isLoading = false;
}
}
/**
* 获取页脚信息(这是为了兼容性而保留的方法)
*/
async getFooterInfo() {
const info = await this.getInfo();
return info;
}
/**
* 获取网站基本信息标题、meta信息等
*/
async getSiteBasicInfo() {
const info = await this.getInfo();
// 确保基本信息字段存在
return {
title: info.title || '美漫资源共建',
logoText: info.logoText || '美漫资源共建',
description: info.description || '美漫共建平台是一个开源的美漫资源共享网站,用户可以自由提交动漫信息,像马赛克一样,由多方贡献拼凑成完整资源。',
keywords: info.keywords || '美漫, 动漫资源, 资源共享, 开源平台, 美漫共建',
...info
};
}
/**
* 更新网站信息
*/
async updateInfo(infoData) {
try {
// 调用API更新信息
const response = await updateSiteSettings('info', infoData);
// 更新缓存
this.cache = infoData;
this.lastFetchTime = Date.now();
// 更新版本号
this.saveVersion(this.version + 1);
// 保存到本地存储
this.saveToStorage(this.cache);
// 检查并更新favicon
if (typeof window.checkFavicon === 'function') {
window.checkFavicon();
}
return response;
} catch (error) {
console.error('更新网站信息失败:', error);
throw error;
}
}
/**
* 强制刷新缓存
*/
async refreshCache() {
// 清除现有缓存
this.cache = null;
this.lastFetchTime = 0;
localStorage.removeItem(STORAGE_KEY);
// 重新获取数据
return await this.getInfo();
}
/**
* 清除缓存
*/
clearCache() {
this.cache = null;
this.lastFetchTime = 0;
localStorage.removeItem(STORAGE_KEY);
console.log('网站信息缓存已清除');
}
}
// 创建并导出单例实例
const infoManager = new InfoManager();
export default infoManager;

View File

@@ -1,4 +1,5 @@
import { getDataSourceManager } from './dataSourceManager';
import axios from 'axios';
// 是否启用离线模式当API不可用时
const OFFLINE_MODE = false; // 设置为false使用在线API
@@ -149,4 +150,68 @@ export const parseEpisodes = (playUrl) => {
}
return episodesArray;
};
// 获取指定key的网站设置 (key可以是'info'等)
export const getSiteSettings = async (key) => {
try {
const response = await axios.get(`/api/settings/${key}`);
return response.data;
} catch (error) {
console.error(`获取网站设置 [${key}] 失败:`, error);
throw error;
}
};
// 获取所有网站设置
export const getAllSiteSettings = async () => {
try {
const response = await axios.get('/api/settings/');
return response.data;
} catch (error) {
console.error('获取所有网站设置失败:', error);
throw error;
}
};
// 更新网站设置 (需要管理员权限key可以是'info'等)
export const updateSiteSettings = async (key, settingValue) => {
try {
const token = localStorage.getItem('accessToken');
if (!token) {
throw new Error('未登录或认证令牌已过期');
}
// 详细打印调试信息
console.log(`准备更新设置 [${key}], 提交的数据:`, JSON.stringify(settingValue, null, 2));
console.log(`认证令牌前10位: ${token.substring(0, 10)}...`);
// 构造请求数据
const requestData = { setting_value: settingValue };
console.log(`完整请求数据对象:`, requestData);
// 使用单/api前缀
const response = await axios.put(`/api/settings/${key}`, requestData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log(`设置更新成功: 状态码=${response.status}, 响应数据:`, response.data);
return response.data;
} catch (error) {
console.error(`更新网站设置 [${key}] 失败:`, error);
// 打印详细错误信息
if (error.response) {
console.error(`错误状态码: ${error.response.status}`);
console.error(`错误响应数据:`, error.response.data);
console.error(`请求URL: ${error.config.url}`);
console.error(`请求头:`, error.config.headers);
console.error(`请求数据:`, error.config.data);
}
throw error;
}
};

View File

@@ -50,10 +50,24 @@ export const setupAxiosInterceptors = () => {
// 标准化URL移除可能的尾部斜杠
const normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return (
normalizedUrl.includes('/api/auth') ||
normalizedUrl.includes('/api/resources')
);
// 打印调试信息
console.log(`检查URL是否需要保护: ${normalizedUrl}`);
// 确保settings路径被正确识别
const isSettingsUrl = normalizedUrl.includes('/api/settings') ||
normalizedUrl.includes('/settings/');
const isAuthUrl = normalizedUrl.includes('/api/auth') ||
normalizedUrl.includes('/auth/');
const isResourcesUrl = normalizedUrl.includes('/api/resources') ||
normalizedUrl.includes('/resources/');
const isProtected = isSettingsUrl || isAuthUrl || isResourcesUrl;
console.log(`URL ${normalizedUrl} 需要保护: ${isProtected}`);
return isProtected;
}
axios.interceptors.request.use(
@@ -68,7 +82,7 @@ export const setupAxiosInterceptors = () => {
if (token) {
config.headers['Authorization'] = `${tokenType} ${token}`
console.log(`Added auth headers to: ${config.url}`)
console.log(`Added auth headers to: ${config.url}`, config.headers)
} else {
console.log(`No token available for: ${config.url}`)
}

File diff suppressed because it is too large Load Diff

View File

@@ -190,14 +190,12 @@ const sortBy = ref('created_at') // 默认按创建时间排序
const currentPage = ref(1)
const pageSize = ref(12) // 默认每页12条
const totalItems = ref(0)
const initialLoadDone = ref(false) // 使用ref管理初始加载状态
// 添加用于自定义每页显示数量的变量
const showCustomPageSize = ref(false)
const customPageSize = ref(12)
// 添加一个reactive变量来跟踪初始加载状态
const initialLoadDone = ref(false)
// 检测是否为移动设备
const isMobile = computed(() => {
return window.innerWidth < 768
@@ -293,7 +291,7 @@ const fetchResources = async () => {
}
console.log(`Fetched resources: ${resources.value.length} items, page ${currentPage.value}/${totalPages.value}`)
initialLoadDone.value = true // 标记初始加载已完成
initialLoadDone.value = true // 标记已完成初始加载
} catch (err) {
console.error('获取资源失败:', err)
error.value = '获取资源列表失败,请稍后重试'
@@ -442,7 +440,10 @@ onMounted(() => {
currentPage.value = parseInt(savedCurrentPage, 10)
}
fetchResources()
// 只在组件首次挂载且未加载数据时获取资源
if (!initialLoadDone.value) {
fetchResources()
}
// 检查URL查询参数显示删除成功提示
if (route.query.deleted === 'success') {

View File

@@ -54,6 +54,9 @@
>
<div class="history-thumbnail" :style="{ backgroundImage: `url(${item.poster || 'https://via.placeholder.com/120x80.png?text=视频'})` }">
<div class="history-play-icon"></div>
<div v-if="item.episodeIndex !== undefined" class="episode-badge">
{{ item.episodeIndex + 1 }}
</div>
</div>
<div class="history-info">
<h3>{{ item.title }}</h3>
@@ -495,9 +498,13 @@ export default {
// 添加到播放历史
const addToPlayHistory = (item) => {
// 首先检查是否已存在相同内容
// 获取当前标题,用于识别同一部影视
const currentTitle = item.title || '自定义流媒体';
// 首先检查是否已存在相同影视剧(基于标题匹配)
const existingItemIndex = playHistory.value.findIndex(
h => (h.id && h.id === item.id) ||
h => (h.title && h.title === currentTitle) ||
(h.id && h.id === item.id) ||
(h.src && item.src && h.src === item.src)
);
@@ -509,14 +516,15 @@ export default {
// 获取当前数据源ID
const currentDataSourceId = selectedDataSource.value || '';
// 添加到开头
// 添加到开头 - 增加集数信息记录
playHistory.value.unshift({
id: item.id,
title: item.title || '自定义流媒体',
title: currentTitle,
src: item.src,
poster: item.poster || '',
timestamp: new Date().getTime(),
dataSourceId: currentDataSourceId // 保存数据源ID
dataSourceId: currentDataSourceId, // 保存数据源ID
episodeIndex: item.episodeIndex !== undefined ? item.episodeIndex : (streamInfo.value?.currentEpisode || 0) // 记录当前播放的集数
});
// 限制历史记录数量
@@ -549,8 +557,8 @@ export default {
}
if (item.id) {
// 这是预设的视频
loadStreamById(item.id);
// 这是预设的视频 - 传递集数信息
loadStreamById(item.id, item.episodeIndex);
} else if (item.src) {
// 这是自定义URL
customStreamUrl.value = item.src;
@@ -564,7 +572,7 @@ export default {
};
// 从API加载流媒体详情
const loadStreamFromApi = async (streamId) => {
const loadStreamFromApi = async (streamId, targetEpisodeIndex = 0) => {
isLoading.value = true; // API请求加载显示全屏遮罩
playerError.value = null;
@@ -582,14 +590,20 @@ export default {
throw new Error('没有可用的播放链接');
}
// 默认播放第一集
const firstEpisode = episodesList[0];
// 确保目标集数在有效范围内
const episodeIndex = targetEpisodeIndex >= 0 && targetEpisodeIndex < episodesList.length
? targetEpisodeIndex
: 0;
// 获取要播放的集数
const targetEpisode = episodesList[episodeIndex];
// 准备流媒体信息
const mediaInfo = {
title: movieDetail.vod_name,
description: movieDetail.vod_blurb || movieDetail.vod_content || '',
episodes: episodesList,
currentEpisode: 0,
currentEpisode: episodeIndex, // 使用目标集数
apiData: movieDetail,
// 增加更多详情信息
actor: movieDetail.vod_actor || '',
@@ -611,7 +625,7 @@ export default {
// 设置媒体信息和播放源
streamInfo.value = mediaInfo;
currentStreamSources.value = [{
src: firstEpisode.url,
src: targetEpisode.url, // 使用目标集数的URL
type: 'application/x-mpegURL' // 默认为HLS格式
}];
currentPoster.value = posterUrl;
@@ -620,11 +634,12 @@ export default {
isPlaying.value = true;
showingSearchResults.value = false;
// 添加到播放历史
// 添加到播放历史,包括集数信息
addToPlayHistory({
id: streamId,
title: movieDetail.vod_name,
poster: posterUrl
poster: posterUrl,
episodeIndex: episodeIndex // 保存当前播放的集数
});
// 延迟结束加载状态
@@ -675,7 +690,18 @@ export default {
// 5. 确保播放器始终显示
isPlaying.value = true;
// 6. 充分延迟后结束加载状态
// 6. 更新播放历史中的集数信息
// 确保有有效的当前播放信息
if (streamInfo.value && streamInfo.value.apiData) {
addToPlayHistory({
id: streamInfo.value.apiData.vod_id,
title: streamInfo.value.title,
poster: currentPoster.value,
episodeIndex: index
});
}
// 7. 充分延迟后结束加载状态
setTimeout(() => {
console.log('剧集切换完成');
isVideoLoading.value = false; // 使用视频加载状态而非全局加载状态
@@ -689,7 +715,7 @@ export default {
};
// 修改从URL参数中加载流媒体的方法
const loadStreamById = async (streamId) => {
const loadStreamById = async (streamId, targetEpisodeIndex = 0) => {
isLoading.value = true;
try {
@@ -702,7 +728,7 @@ export default {
isLoading.value = false; // 本地数据加载完成后关闭加载状态
} else {
// 使用API加载
await loadStreamFromApi(streamId);
await loadStreamFromApi(streamId, targetEpisodeIndex);
}
} catch (error) {
console.error('加载流媒体信息失败:', error);
@@ -1071,4 +1097,180 @@ export default {
};
}
}
</script>
</script>
<style scoped>
/* 历史记录项样式 */
.history-section {
margin: 1.5rem 0;
background: linear-gradient(to right, rgba(124, 58, 237, 0.03), rgba(241, 239, 254, 0.08));
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden;
}
.history-section::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(to bottom, #7c3aed, #8b5cf6);
border-radius: 4px 0 0 4px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
}
.history-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
}
.history-header h2::before {
content: "⏱";
margin-right: 8px;
font-size: 1.1rem;
}
.history-items {
display: flex;
overflow-x: auto;
gap: 1rem;
padding-bottom: 0.5rem;
scroll-behavior: smooth;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: thin; /* Firefox */
}
.history-items::-webkit-scrollbar {
height: 6px;
}
.history-items::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.03);
border-radius: 3px;
}
.history-items::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
.history-items::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.history-item {
display: flex;
flex-direction: column;
min-width: 180px;
max-width: 200px;
border-radius: 12px;
overflow: hidden;
background-color: #fff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.history-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
.history-thumbnail {
height: 110px;
background-size: cover;
background-position: center;
position: relative;
width: 100%;
}
.history-play-icon {
position: absolute;
bottom: 10px;
right: 10px;
width: 28px;
height: 28px;
background-color: rgba(124, 58, 237, 0.9);
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
transform: scale(0.9);
}
.history-item:hover .history-play-icon {
opacity: 1;
transform: scale(1);
}
.history-info {
padding: 12px;
}
.history-info h3 {
margin: 0 0 5px 0;
font-size: 0.95rem;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
}
.history-info p {
margin: 0;
font-size: 0.8rem;
color: #888;
}
/* 集数徽章样式 */
.episode-badge {
position: absolute;
bottom: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
z-index: 2;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
/* 响应式调整 */
@media (max-width: 768px) {
.history-items {
padding-bottom: 12px;
}
.history-item {
min-width: 150px;
max-width: 170px;
}
.history-thumbnail {
height: 100px;
}
}
</style>

View File

@@ -38,6 +38,7 @@ export default defineConfig(({ command, mode }) => {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
// 恢复原始重写,去掉/api前缀
rewrite: (path) => path.replace(/^\/api/, ''),
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
@@ -48,6 +49,18 @@ export default defineConfig(({ command, mode }) => {
});
}
},
'/proxy': {
target: 'http://localhost:8000',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('Proxy error:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('请求代理:', req.method, req.url, '->', options.target + proxyReq.path);
});
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ import (
"dongman/internal/handlers"
"dongman/internal/models"
"dongman/internal/config"
)
func main() {
@@ -29,6 +30,11 @@ func main() {
log.Printf("创建初始管理员账号失败: %v", err)
}
// 初始化网站设置
if err := models.InitSiteSettings(); err != nil {
log.Printf("初始化网站设置失败: %v", err)
}
// 创建Gin应用
router := gin.Default()
@@ -41,15 +47,9 @@ func main() {
AllowCredentials: true,
}))
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
log.Fatalf("获取工作目录失败: %v", err)
}
// 配置静态文件服务
assetsDir := filepath.Join(workDir, "..", "assets")
router.Static("/assets", assetsDir)
router.Static("/assets", config.AssetPath)
router.Static("/public", filepath.Join(config.AssetPath, "public"))
// 设置路由
handlers.SetupRoutes(router)

View File

@@ -0,0 +1,43 @@
package config
import (
"log"
"os"
"path/filepath"
)
var AssetPath string
func init() {
// 优先读取环境变量指定的资源目录
if envPath := os.Getenv("ASSETS_PATH"); envPath != "" {
AssetPath = envPath
log.Printf("使用环境变量指定的资源目录: %s", AssetPath)
} else {
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
log.Printf("获取工作目录失败: %v使用默认路径", err)
workDir = "."
}
// 使用默认资源目录路径
AssetPath = filepath.Join(workDir, "..", "assets")
log.Printf("使用默认资源目录: %s", AssetPath)
}
// 如果目录不存在,尝试创建
if _, err := os.Stat(AssetPath); os.IsNotExist(err) {
if err := os.MkdirAll(AssetPath, 0755); err != nil {
log.Printf("无法创建资源目录: %v", err)
}
}
// 确保public子目录存在
publicDir := filepath.Join(AssetPath, "public")
if _, err := os.Stat(publicDir); os.IsNotExist(err) {
if err := os.MkdirAll(publicDir, 0755); err != nil {
log.Printf("无法创建public目录: %v", err)
}
}
}

View File

@@ -23,6 +23,24 @@ func SetupRoutes(router *gin.Engine) {
auth.POST("/change-password", JWTAuthMiddleware(), UpdatePassword)
}
// 网站设置路由
settings := api.Group("/settings")
{
// 获取设置 - 公开API
settings.GET("/:key", GetSiteSettings)
settings.GET("/", GetAllSiteSettings)
// 更新设置 - 需要管理员权限
settings.PUT("/:key", JWTAuthMiddleware(), AdminAuthMiddleware(), UpdateSiteSettings)
}
// 管理员路由
admin := api.Group("/admin", JWTAuthMiddleware(), AdminAuthMiddleware())
{
// 网站图标上传
admin.POST("/upload/favicon", UploadFavicon)
}
// 资源路由 - 需要认证
resources := api.Group("/resources")
{

View File

@@ -0,0 +1,206 @@
package handlers
import (
"net/http"
"time"
"log"
"github.com/gin-gonic/gin"
"dongman/internal/models"
"io"
"bytes"
"os"
"path/filepath"
"dongman/internal/config"
)
// GetSiteSettings 获取指定key的网站设置
func GetSiteSettings(c *gin.Context) {
settingKey := c.Param("key")
if settingKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少setting_key参数"})
return
}
var settings models.SiteSettings
err := models.GetDB().Get(&settings, "SELECT * FROM site_settings WHERE setting_key = ?", settingKey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "未找到指定设置",
"key": settingKey,
})
return
}
c.JSON(http.StatusOK, settings)
}
// GetAllSiteSettings 获取所有网站设置
func GetAllSiteSettings(c *gin.Context) {
var settings []models.SiteSettings
err := models.GetDB().Select(&settings, "SELECT * FROM site_settings")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取设置失败"})
return
}
if len(settings) == 0 {
c.JSON(http.StatusOK, []models.SiteSettings{})
return
}
c.JSON(http.StatusOK, settings)
}
// UpdateSiteSettings 更新网站设置
func UpdateSiteSettings(c *gin.Context) {
// 打印请求头信息
log.Printf("===== 更新设置请求 =====")
log.Printf("请求方法: %s", c.Request.Method)
log.Printf("请求路径: %s", c.Request.URL.Path)
log.Printf("认证头: %s", c.GetHeader("Authorization"))
log.Printf("Content-Type: %s", c.GetHeader("Content-Type"))
// 获取原始请求体
data, err := c.GetRawData()
if err != nil {
log.Printf("获取请求体失败: %v", err)
} else {
log.Printf("原始请求数据: %s", string(data))
}
// 重新设置请求体否则后续ShouldBindJSON会读取失败
c.Request.Body = originalBody(data)
settingKey := c.Param("key")
log.Printf("设置键名: %v", settingKey)
if settingKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少setting_key参数"})
return
}
var update models.SiteSettingsUpdate
if err := c.ShouldBindJSON(&update); err != nil {
log.Printf("解析JSON数据失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据", "details": err.Error()})
return
}
// 打印解析后的数据
settingValue, err := update.SettingValue.Value()
if err != nil {
log.Printf("获取设置值失败: %v", err)
} else {
// 根据实际类型进行处理
switch v := settingValue.(type) {
case []byte:
log.Printf("设置值([]byte): %v", string(v))
case string:
log.Printf("设置值(string): %v", v)
default:
log.Printf("设置值(其他类型): %v", v)
}
}
db := models.GetDB()
// 检查设置是否存在
var settingExists int
err = db.Get(&settingExists, "SELECT COUNT(*) FROM site_settings WHERE setting_key = ?", settingKey)
if err != nil {
log.Printf("查询设置是否存在失败: %v", err)
}
log.Printf("设置是否存在: %v (count=%d)", settingExists > 0, settingExists)
if err != nil || settingExists == 0 {
// 创建新设置
settingValue, err := update.SettingValue.Value()
if err != nil {
log.Printf("序列化设置值失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化设置值失败"})
return
}
result, err := db.Exec(
"INSERT INTO site_settings (setting_key, setting_value, created_at, updated_at) VALUES (?, ?, ?, ?)",
settingKey, settingValue, time.Now(), time.Now(),
)
if err != nil {
log.Printf("保存设置失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败", "details": err.Error()})
return
}
id, _ := result.LastInsertId()
log.Printf("创建新设置成功ID: %d", id)
} else {
// 更新现有设置
settingValue, err := update.SettingValue.Value()
if err != nil {
log.Printf("序列化设置值失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化设置值失败"})
return
}
result, err := db.Exec(
"UPDATE site_settings SET setting_value = ?, updated_at = ? WHERE setting_key = ?",
settingValue, time.Now(), settingKey,
)
if err != nil {
log.Printf("更新设置失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新设置失败", "details": err.Error()})
return
}
rows, _ := result.RowsAffected()
log.Printf("更新设置成功,影响行数: %d", rows)
}
// 返回更新后的设置
var settings models.SiteSettings
err = models.GetDB().Get(&settings, "SELECT * FROM site_settings WHERE setting_key = ?", settingKey)
if err != nil {
log.Printf("读取更新后的设置失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取更新后的设置失败"})
return
}
log.Printf("更新设置完成,返回结果: %+v", settings)
c.JSON(http.StatusOK, settings)
}
// originalBody 创建一个可重复读取的请求体
func originalBody(data []byte) io.ReadCloser {
return io.NopCloser(bytes.NewBuffer(data))
}
// UploadFavicon 处理网站图标上传
func UploadFavicon(c *gin.Context) {
// 获取上传的文件
file, err := c.FormFile("favicon")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件上传"})
return
}
// 确保目录存在
publicDir := filepath.Join(config.AssetPath, "public")
if err := os.MkdirAll(publicDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
// 保存favicon.ico
faviconPath := filepath.Join(publicDir, "favicon.ico")
// 保存上传的文件
if err := c.SaveUploadedFile(file, faviconPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
// 增加日志输出
log.Printf("网站图标已更新,保存路径: %s", faviconPath)
// 确保返回正确的路径
c.JSON(http.StatusOK, gin.H{
"message": "网站图标已更新",
"faviconPath": "/assets/public/favicon.ico",
})
}

View File

@@ -71,6 +71,16 @@ CREATE TABLE IF NOT EXISTS users (
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE TABLE IF NOT EXISTS site_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT NOT NULL UNIQUE,
setting_value JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_site_settings_key ON site_settings(setting_key);
`
// InitDB 初始化数据库连接
@@ -309,4 +319,43 @@ func ConvertJsonFieldsToText() error {
log.Printf("JSON字段修复完成: 总共%d条记录, 成功修复%d条", len(resources), fixed)
return nil
}
// InitSiteSettings 初始化网站设置
func InitSiteSettings() error {
// 默认的页脚设置
footerSettings := JsonMap{
"links": []map[string]interface{}{
{"text": "关于我们", "url": "/about", "type": "internal"},
{"text": "Telegram", "url": "https://t.me/xueximeng", "icon": "bi-telegram", "type": "external"},
{"text": "GitHub", "url": "https://github.com/fish2018/GoComicMosaic", "icon": "bi-github", "type": "external"},
{"text": "在线点播", "url": "/streams", "type": "internal"},
{"text": "漫迪小站", "url": "https://mdsub.top/", "type": "external"},
{"text": "三次元成瘾者康复中心", "url": "https://www.kangfuzhongx.in/", "type": "external"},
},
"copyright": "© 2025 美漫资源共建. 保留所有权利",
"show_visitor_count": true,
}
// 将设置转为JSON
footerJSON, err := json.Marshal(footerSettings)
if err != nil {
return fmt.Errorf("序列化页脚设置失败: %w", err)
}
// 插入或更新页脚设置
_, err = DB.Exec(`
INSERT INTO site_settings (setting_key, setting_value, created_at, updated_at)
VALUES ('footer', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT(setting_key) DO UPDATE SET
setting_value = ?,
updated_at = CURRENT_TIMESTAMP
`, string(footerJSON), string(footerJSON))
if err != nil {
return fmt.Errorf("保存页脚设置失败: %w", err)
}
log.Printf("网站设置初始化完成")
return nil
}

View File

@@ -282,4 +282,18 @@ type SupplementCreate struct {
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
}
// SiteSettings 网站设置模型
type SiteSettings struct {
ID int `db:"id" json:"id"`
SettingKey string `db:"setting_key" json:"setting_key"`
SettingValue JsonMap `db:"setting_value" json:"setting_value"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// SiteSettingsUpdate 网站设置更新请求
type SiteSettingsUpdate struct {
SettingValue JsonMap `json:"setting_value" binding:"required"`
}

View File

@@ -0,0 +1,8 @@
// 网站设置相关路由
siteSettings := api.Group("/site-settings")
{
siteSettings.GET("/:key", handlers.GetSiteSetting)
siteSettings.GET("", handlers.GetAllSiteSettings)
siteSettings.PUT("/:key", auth.AdminAuthMiddleware(), handlers.UpdateSiteSetting)
siteSettings.POST("/favicon", auth.AdminAuthMiddleware(), handlers.UploadFavicon) // 新增图标上传路由
}