mirror of
https://github.com/fish2018/GoComicMosaic.git
synced 2025-11-25 11:29:33 +08:00
update
This commit is contained in:
@@ -99,6 +99,9 @@
|
||||
目前内置了8个解析采集
|
||||

|
||||
|
||||
## 全面支持管理后台设置网站信息
|
||||

|
||||
|
||||
---
|
||||
|
||||
# 美漫资源共建平台部署
|
||||
@@ -593,6 +596,9 @@ sudo systemctl status nginx
|
||||
- Space Ghost Coast to Coast(太空幽灵海岸到海岸)
|
||||
|
||||
# 更新日志
|
||||
-202506071917
|
||||
✅ 全面支持管理后台设置网站信息
|
||||
|
||||
- 202506061607
|
||||
✅ 优化悬浮按钮样式问题
|
||||
✅ 修复最近播放恢复播放失败问题
|
||||
|
||||
BIN
docs/28.gif
Normal file
BIN
docs/28.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -1,2 +1,3 @@
|
||||
BASE_URL=https://dm.xueximeng.com
|
||||
ASSETS_PATH=../assets
|
||||
ASSETS_PATH=../assets
|
||||
# ASSETS_PATH=../data/assets
|
||||
@@ -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>
|
||||
|
||||
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
5
frontend/public/index.html
Normal file
5
frontend/public/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<head>
|
||||
<!-- ... existing tags ... -->
|
||||
<link rel="icon" href="/public/favicon.ico">
|
||||
<!-- ... existing tags ... -->
|
||||
</head>
|
||||
@@ -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>
|
||||
216
frontend/public/test-settings-api.html
Normal file
216
frontend/public/test-settings-api.html
Normal 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>
|
||||
@@ -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>© 2025 美漫资源共建. 保留所有权利</p>
|
||||
<p>{{ footerSettings?.copyright || '© 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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
227
frontend/src/utils/InfoManager.js
Normal file
227
frontend/src/utils/InfoManager.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
43
gobackend/internal/config/config.go
Normal file
43
gobackend/internal/config/config.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
206
gobackend/internal/handlers/site_settings_handlers.go
Normal file
206
gobackend/internal/handlers/site_settings_handlers.go
Normal 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",
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
8
gobackend/internal/routes/routes.go
Normal file
8
gobackend/internal/routes/routes.go
Normal 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) // 新增图标上传路由
|
||||
}
|
||||
Reference in New Issue
Block a user