优化TMDB搜索,支持编辑后一键导入

This commit is contained in:
www.xueximeng.com
2025-06-15 12:44:09 +08:00
parent 70f5aeb754
commit 5a336de503
7 changed files with 2560 additions and 439 deletions

302
README.md
View File

@@ -137,299 +137,6 @@ docker run -d --name dongman \
---
# 美漫资源共建平台部署
本教程将指导您如何将前端和后端代码部署到 `/home/work/dongman` 目录并使用nginx配置同一个域名 `https://dm.xueximeng.com`。
## 一、目录结构
首先创建所需的目录结构:
```bash
/home/work/dongman/
├── assets/ # 资源文件目录(用户上传的图片)
│ ├── imgs/
│ ├── public/ # 用户上传的favicon.ico
│ └── uploads/
├── gobackend/ # 后端代码目录
└── frontend/ # 前端代码目录
```
## 二、前端部署
### 构建前端代码
#### 进入前端代码目录
```
cd /home/work/dongman/frontend
```
#### 安装依赖
```
npm install
```
#### 修改`.env.production`文件
```
BASE_URL=https://dm.xueximeng.com // 生成sitemap会用不指定则访问 'http://localhost:80000'
ASSETS_PATH=../assets // 使用express代理访问本地图片路径不指定默认为 '../assets'
```
#### 编译
```
npm i && npm run build
```
构建完成后,将在 `/home/work/dongman/frontend/dist` 目录中生成静态文件。
## 三、后端部署
### 安装后端依赖
#### 进入后端代码目录
```
cd /home/work/dongman/gobackend
```
#### 安装依赖
```
go mod tidy
```
#### 编译二进制
linux机器直接编译二进制
```
go build -ldflags="-w -s" -o app
```
mac上交叉编译linux二进制
```
sudo chown -R $(whoami):admin /usr/local/Homebrew
chmod u+w /usr/local/Homebrew
brew install x86_64-linux-gnu-binutils
brew tap messense/macos-cross-toolchains
brew install x86_64-unknown-linux-gnu
CC=/usr/local/Cellar/x86_64-unknown-linux-gnu/13.3.0.reinstall/bin/x86_64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "sqlite_static" -ldflags="-w -s" -o app cmd/api/main.go
```
## 四、Supervisor 配置
配置 Supervisor 来管理后端应用:
#### 创建 supervisor 配置文件
```
sudo nano /etc/supervisor/conf.d/dongman.conf
```
填入以下内容:
```ini
[program:dm]
environment=ASSETS_PATH="/home/work/dongman/assets",DB_PATH="/home/work/dongman/gobackend/resource_hub.db" ;支持环境变量指定assets和数据库路径(默认assets和frontend、gobackend同级数据库默认在gobackend/resource_hub.db)
command=/home/work/dongman/gobackend/app
directory=/home/work/dongman/gobackend ; 项目的文件夹路径
autostart=true ; 是否在 Supervisor 启动时自动启动该程序
autorestart=true ; 程序退出后是否自动重启
startsecs=5 ; 程序启动需要的秒数
startretries=3 ; 启动失败后的重试次数
exitcodes=0 ; 程序正常退出的退出码
stopwaitsecs=10 ; 程序停止等待的秒数
stopasgroup=true ; 是否向进程组发送停止信号
killasgroup=true ; 是否向进程组发送杀死信号
redirect_stderr=true ; 是否将 stderr 重定向到 stdout
stdout_logfile=/home/work/logs/dongman.log
stdout_logfile_maxbytes=50MB ; 标准输出日志文件的最大字节数
stdout_logfile_backups=10 ; 保留的日志文件备份数量
```
重新加载 supervisor 配置:
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start dm
```
## 五、Nginx 配置
### 1. 安装 Nginx
```bash
sudo apt update
sudo apt install nginx
```
### 2. 配置 Nginx
```bash
sudo vi /etc/nginx/conf.d/dongman.conf
```
填入以下配置:
```nginx
server {
listen 80;
server_name dm.xueximeng.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name dm.xueximeng.com;
# 基础路径
set $base_path /home/work/dongman;
# SSL配置
ssl_certificate /etc/letsencrypt/live/xueximeng.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xueximeng.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# 安全头部
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
# CORS代理端点 - 注意这与前端请求的路径完全匹配 /proxy
location = /proxy {
# 重要:这里没有以 /api 开头,与前端代码中的路径保持一致
proxy_pass http://127.0.0.1:8000/proxy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 处理大型响应
proxy_buffer_size 16k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
# 超时设置
proxy_connect_timeout 15s;
proxy_read_timeout 45s;
proxy_send_timeout 15s;
proxy_ssl_server_name on;
# 日志设置
access_log /var/log/nginx/proxy_access.log;
error_log /var/log/nginx/proxy_error.log;
}
# API请求 - 代理到Go后端
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
# 静态资源
location /static/ {
alias $base_path/frontend/dist/;
expires 30d;
}
location /assets/ {
alias $base_path/assets/; # 如果修改了默认的assets路径这里也要一同修改
expires 30d;
}
# 特定文件
location = /favicon.ico {
alias $base_path/frontend/dist/favicon.ico;
}
location = /robots.txt {
alias $base_path/frontend/dist/robots.txt;
}
location = /sitemap.xml {
alias $base_path/frontend/dist/sitemap.xml;
}
# 前端应用
location / {
root $base_path/frontend/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 限制上传大小
client_max_body_size 50M;
# 日志
access_log /var/log/nginx/dongman.access.log;
error_log /var/log/nginx/dongman.error.log;
}
```
### 3. 启用站点配置
```bash
# 验证配置是否正确
sudo nginx -t
# 如果配置正确,重新加载 Nginx
sudo systemctl reload nginx
```
## 六、SSL 证书配置
使用 Let's Encrypt 获取免费 SSL 证书:
```bash
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d dm.xueximeng.com
# 配置自动续期
sudo systemctl status certbot.timer
```
## 七、权限配置
确保文件权限正确:
```bash
# 设置资源目录权限
sudo chmod 755 -R /home/work/dongman/
```
## 八、防火墙配置
```bash
# 允许 HTTP 和 HTTPS 流量
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
## 九、配置检查和测试
### 1. 检查 Supervisor 状态
```bash
sudo supervisorctl status dm
```
### 2. 检查 Nginx 状态
```bash
sudo systemctl status nginx
```
### 附录(梳理了一些采集站)
黑木耳 https://www.heimuer.tv/
魔都 https://moduzy2.com/
@@ -541,6 +248,11 @@ U酷 https://www.ukuzy.com/
- Goof Troop高飞家族
- Disney's House of Mouse米老鼠群星会
- Bee and PuppyCat蜂妹与狗狗猫
- Ben 10少年骇客
- YOLO乐活姐妹怪诞冒险
- Super Robot Monkey Team Hyperforce Go!(超级战猴)✅
- Fanboy and Chum Chum小幻与冲冲
- The Loud House喧闹一家亲
- Bob's Burgers开心汉堡店
- SpongeBob SquarePants海绵宝宝
- Harley Quinn哈莉·奎茵
@@ -579,7 +291,6 @@ U酷 https://www.ukuzy.com/
- Disenchantment幻灭
- Bless the Harts福是全家福的福
- My Adventures with Superman我亲爱的怪物伙伴
- Ben 10少年骇客
- She-Ra and the Princesses of Power神勇战士
- Central Park中央公园
- The Age of the Chip and the Amazing Animals奇波和神奇动物的时代
@@ -605,6 +316,9 @@ U酷 https://www.ukuzy.com/
- Space Ghost Coast to Coast太空幽灵海岸到海岸
# 更新日志
-202506151233
✅ TMDB搜索后支持直接编辑然后再一键导入
✅ 调整TMDB搜索预览界面和实际资源详情页保持风格一致
-202506141634
✅ 去掉点播时的质量设置,提升加载速度
✅ 播放器界面切换数据源时,不再自动搜索

292
docs/手动部署文档.md Normal file
View File

@@ -0,0 +1,292 @@
# 美漫资源共建平台部署
本教程将指导您如何将前端和后端代码部署到 `/home/work/dongman` 目录并使用nginx配置同一个域名 `https://dm.xueximeng.com`
## 一、目录结构
首先创建所需的目录结构:
```bash
/home/work/dongman/
├── assets/ # 资源文件目录(用户上传的图片)
│ ├── imgs/
│ ├── public/ # 用户上传的favicon.ico
│ └── uploads/
├── gobackend/ # 后端代码目录
└── frontend/ # 前端代码目录
```
## 二、前端部署
### 构建前端代码
#### 进入前端代码目录
```
cd /home/work/dongman/frontend
```
#### 安装依赖
```
npm install
```
#### 修改`.env.production`文件
```
BASE_URL=https://dm.xueximeng.com // 生成sitemap会用不指定则访问 'http://localhost:80000'
ASSETS_PATH=../assets // 使用express代理访问本地图片路径不指定默认为 '../assets'
```
#### 编译
```
npm i && npm run build
```
构建完成后,将在 `/home/work/dongman/frontend/dist` 目录中生成静态文件。
## 三、后端部署
### 安装后端依赖
#### 进入后端代码目录
```
cd /home/work/dongman/gobackend
```
#### 安装依赖
```
go mod tidy
```
#### 编译二进制
linux机器直接编译二进制
```
go build -ldflags="-w -s" -o app
```
mac上交叉编译linux二进制
```
sudo chown -R $(whoami):admin /usr/local/Homebrew
chmod u+w /usr/local/Homebrew
brew install x86_64-linux-gnu-binutils
brew tap messense/macos-cross-toolchains
brew install x86_64-unknown-linux-gnu
CC=/usr/local/Cellar/x86_64-unknown-linux-gnu/13.3.0.reinstall/bin/x86_64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "sqlite_static" -ldflags="-w -s" -o app cmd/api/main.go
```
## 四、Supervisor 配置
配置 Supervisor 来管理后端应用:
#### 创建 supervisor 配置文件
```
sudo nano /etc/supervisor/conf.d/dongman.conf
```
填入以下内容:
```ini
[program:dm]
environment=ASSETS_PATH="/home/work/dongman/assets",DB_PATH="/home/work/dongman/gobackend/resource_hub.db" ;支持环境变量指定assets和数据库路径(默认assets和frontend、gobackend同级数据库默认在gobackend/resource_hub.db)
command=/home/work/dongman/gobackend/app
directory=/home/work/dongman/gobackend ; 项目的文件夹路径
autostart=true ; 是否在 Supervisor 启动时自动启动该程序
autorestart=true ; 程序退出后是否自动重启
startsecs=5 ; 程序启动需要的秒数
startretries=3 ; 启动失败后的重试次数
exitcodes=0 ; 程序正常退出的退出码
stopwaitsecs=10 ; 程序停止等待的秒数
stopasgroup=true ; 是否向进程组发送停止信号
killasgroup=true ; 是否向进程组发送杀死信号
redirect_stderr=true ; 是否将 stderr 重定向到 stdout
stdout_logfile=/home/work/logs/dongman.log
stdout_logfile_maxbytes=50MB ; 标准输出日志文件的最大字节数
stdout_logfile_backups=10 ; 保留的日志文件备份数量
```
重新加载 supervisor 配置:
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start dm
```
## 五、Nginx 配置
### 1. 安装 Nginx
```bash
sudo apt update
sudo apt install nginx
```
### 2. 配置 Nginx
```bash
sudo vi /etc/nginx/conf.d/dongman.conf
```
填入以下配置:
```nginx
server {
listen 80;
server_name dm.xueximeng.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name dm.xueximeng.com;
# 基础路径
set $base_path /home/work/dongman;
# SSL配置
ssl_certificate /etc/letsencrypt/live/xueximeng.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xueximeng.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# 安全头部
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
# CORS代理端点 - 注意这与前端请求的路径完全匹配 /proxy
location = /proxy {
# 重要:这里没有以 /api 开头,与前端代码中的路径保持一致
proxy_pass http://127.0.0.1:8000/proxy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 处理大型响应
proxy_buffer_size 16k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
# 超时设置
proxy_connect_timeout 15s;
proxy_read_timeout 45s;
proxy_send_timeout 15s;
proxy_ssl_server_name on;
# 日志设置
access_log /var/log/nginx/proxy_access.log;
error_log /var/log/nginx/proxy_error.log;
}
# API请求 - 代理到Go后端
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
# 静态资源
location /static/ {
alias $base_path/frontend/dist/;
expires 30d;
}
location /assets/ {
alias $base_path/assets/; # 如果修改了默认的assets路径这里也要一同修改
expires 30d;
}
# 特定文件
location = /favicon.ico {
alias $base_path/frontend/dist/favicon.ico;
}
location = /robots.txt {
alias $base_path/frontend/dist/robots.txt;
}
location = /sitemap.xml {
alias $base_path/frontend/dist/sitemap.xml;
}
# 前端应用
location / {
root $base_path/frontend/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 限制上传大小
client_max_body_size 50M;
# 日志
access_log /var/log/nginx/dongman.access.log;
error_log /var/log/nginx/dongman.error.log;
}
```
### 3. 启用站点配置
```bash
# 验证配置是否正确
sudo nginx -t
# 如果配置正确,重新加载 Nginx
sudo systemctl reload nginx
```
## 六、SSL 证书配置
使用 Let's Encrypt 获取免费 SSL 证书:
```bash
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d dm.xueximeng.com
# 配置自动续期
sudo systemctl status certbot.timer
```
## 七、权限配置
确保文件权限正确:
```bash
# 设置资源目录权限
sudo chmod 755 -R /home/work/dongman/
```
## 八、防火墙配置
```bash
# 允许 HTTP 和 HTTPS 流量
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
## 九、配置检查和测试
### 1. 检查 Supervisor 状态
```bash
sudo supervisorctl status dm
```
### 2. 检查 Nginx 状态
```bash
sudo systemctl status nginx
```

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-14</lastmod>
<lastmod>2025-06-15</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/submit</loc>
<lastmod>2025-06-14</lastmod>
<lastmod>2025-06-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/about</loc>
<lastmod>2025-06-14</lastmod>
<lastmod>2025-06-15</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
@@ -68,7 +68,13 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/34</loc>
<lastmod>2025-06-05</lastmod>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/82</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -110,13 +116,13 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/37</loc>
<lastmod>2025-06-05</lastmod>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/82</loc>
<lastmod>2025-06-14</lastmod>
<loc>https://dm.xueximeng.com/resource/6</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -186,12 +192,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/6</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/7</loc>
<lastmod>2025-05-29</lastmod>
@@ -259,7 +259,7 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/78</loc>
<loc>https://dm.xueximeng.com/resource/95</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
@@ -338,7 +338,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/44</loc>
<lastmod>2025-06-06</lastmod>
<lastmod>2025-06-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -446,13 +446,19 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/74</loc>
<lastmod>2025-06-12</lastmod>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/76</loc>
<lastmod>2025-06-12</lastmod>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/78</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -470,7 +476,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/81</loc>
<lastmod>2025-06-13</lastmod>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -505,7 +511,31 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/92</loc>
<loc>https://dm.xueximeng.com/resource/94</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/96</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/97</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/98</loc>
<lastmod>2025-06-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/99</loc>
<lastmod>2025-06-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>

View File

@@ -1698,6 +1698,27 @@
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
max-height: 400px;
overflow-y: auto;
padding-right: 8px;
}
.links-list::-webkit-scrollbar {
width: 6px;
}
.links-list::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
}
.links-list::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.3);
border-radius: 10px;
}
.links-list::-webkit-scrollbar-thumb:hover {
background: rgba(124, 58, 237, 0.5);
}
.link-item {
@@ -1720,7 +1741,7 @@
.link-inputs {
display: grid;
grid-template-columns: 1fr 120px 120px;
grid-template-columns: 2fr 0.5fr 2fr;
gap: 1rem;
width: 100%;
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,82 +39,415 @@
<div v-else-if="importSuccess" class="success-message">
<i class="bi bi-check-circle-fill"></i>
<p>资源已成功导入</p>
</div>
<!-- 搜索结果和预览 -->
<div v-else-if="tmdbResource" class="preview-container">
<div v-else-if="tmdbResource">
<div class="resource-detail">
<!-- 预览界面重新组织结构 -->
<div class="resource-content">
<!-- 左侧图片区域 -->
<div class="media-section">
<!-- 大图展示区 -->
<div class="main-image-container" @click="previewImage(currentImage)">
<img
:src="currentImage"
class="resource-poster"
:alt="tmdbResource.title || tmdbResource.title_en"
>
<!-- 编辑模式 -->
<div v-if="isEditing" class="edit-form-container">
<form @submit.prevent="saveChanges" class="edit-card">
<div class="edit-card-header">
<h3>编辑TMDB资源</h3>
<button type="button" class="btn-close" @click="cancelEdit"></button>
</div>
<!-- 缩略图滚动区 -->
<div class="thumbnails-container" v-if="tmdbResource.images && tmdbResource.images.length > 1">
<div class="thumbnails-scroll">
<div
v-for="(image, index) in tmdbResource.images"
:key="index"
class="thumbnail"
:class="{ active: currentImage === image }"
@click="selectImage(image)"
>
<img
:src="image"
class="thumbnail-img"
:alt="`缩略图${index+1}`"
<div class="edit-card-body">
<div class="form-group">
<label for="title" class="form-label">中文标题</label>
<input type="text" class="form-control custom-input" id="title" v-model="editForm.title" required>
</div>
<div class="form-group">
<label for="title_en" class="form-label">英文标题</label>
<input type="text" class="form-control custom-input" id="title_en" v-model="editForm.title_en">
</div>
<div class="form-group">
<label class="form-label">资源类型</label>
<div class="resource-type-options">
<div
v-for="type in resourceTypes"
:key="type"
class="resource-type-option"
:class="{'selected': isTypeSelected(type)}"
@click="selectResourceType(type)"
>
<span class="option-text">{{ type }}</span>
<i v-if="isTypeSelected(type)" class="bi bi-check-circle-fill check-icon"></i>
</div>
</div>
<div class="selected-types-preview">
<span>已选类型</span>
<span v-if="editForm.resource_type" class="selected-type-text">{{ editForm.resource_type }}</span>
<span v-else class="text-muted">未选择</span>
</div>
</div>
<div class="form-group">
<label for="description" class="form-label">简介</label>
<textarea class="form-control custom-textarea" id="description" rows="6" v-model="editForm.description" required></textarea>
</div>
<!-- 图片管理部分 -->
<div class="form-group">
<label class="form-label">图片管理</label>
<!-- 现有图片展示和管理 -->
<div class="image-management-section">
<h6 class="section-subtitle">已有图片 ({{ editForm.images.length }})</h6>
<div class="image-grid">
<div v-for="(image, index) in editForm.images" :key="index" class="image-item" :class="{'is-poster': image === editForm.poster_image}">
<div class="image-preview-container">
<img
:src="image"
class="image-preview"
alt="资源图片"
@click="previewEditImage(image)"
>
<div class="image-overlay">
<button
type="button"
class="image-action-btn set-poster-btn"
@click.stop="setPosterImage(image)"
:disabled="image === editForm.poster_image"
>
<i class="bi bi-star-fill me-1"></i>
{{ image === editForm.poster_image ? '当前海报' : '设为海报' }}
</button>
<button
type="button"
class="image-action-btn remove-btn"
@click.stop="removeImage(index)"
:disabled="editForm.images.length <= 1"
>
<i class="bi bi-trash me-1"></i>删除
</button>
</div>
</div>
<div class="poster-badge" v-if="image === editForm.poster_image">
<i class="bi bi-star-fill"></i> 海报图片
</div>
</div>
</div>
</div>
<!-- 添加新图片 -->
<div class="upload-section">
<h6 class="section-subtitle">添加新图片</h6>
<div
class="dropzone-container"
:class="{'active-dropzone': isDragging}"
@dragenter.prevent="isDragging = true"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleFileDrop"
>
<div class="dropzone-content">
<i class="bi bi-cloud-arrow-up-fill dropzone-icon"></i>
<p>拖拽图片文件到此处</p>
<label for="file-upload" class="btn-custom btn-outline file-upload-btn">
<i class="bi bi-image me-2"></i><span class="file-btn-text">选择文件</span>
</label>
<input
type="file"
id="file-upload"
@change="handleFilesSelection"
multiple
accept="image/*"
class="d-none"
>
</div>
</div>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<div class="progress-info">
<div class="spinner"></div>
<div>正在上传图片 {{ currentUploadIndex }}/{{ totalUploadCount }}请稍等...</div>
</div>
<div class="progress-bar-container">
<div
class="progress-bar-inner"
:style="{width: `${uploadProgress}%`}"
>{{ uploadProgress }}%</div>
</div>
</div>
</div>
</div>
<!-- 链接管理部分 -->
<div class="form-group">
<label class="form-label">资源链接管理</label>
<div class="links-edit-card">
<p class="link-info-text">
您可以管理网盘链接或在线观看地址方便用户获取资源每种类型可添加多个链接
</p>
<!-- 链接类型选项卡 -->
<div class="links-tabs">
<button
v-for="(category, categoryIndex) in Object.keys(editLinks)"
:key="categoryIndex"
class="tab-btn"
:class="{ active: editActiveCategory === category }"
@click.prevent="editActiveCategory = category"
>
{{ getCategoryDisplayName(category) }}
<span v-if="editLinks[category].length > 0" class="tab-badge">{{ editLinks[category].length }}</span>
</button>
</div>
<!-- 链接输入区域 -->
<div class="links-content">
<div v-for="(category, categoryIndex) in Object.keys(editLinks)" :key="categoryIndex"
v-show="editActiveCategory === category"
class="link-category-content">
<div v-if="editLinks[category].length === 0" class="empty-links">
<i class="bi bi-link-45deg empty-icon"></i>
<p>暂无{{ getCategoryDisplayName(category) }}链接点击下方按钮添加</p>
</div>
<!-- 已添加的链接 -->
<div class="links-list">
<div class="link-item" v-for="(link, index) in editLinks[category]" :key="index">
<div class="link-inputs">
<div class="input-group">
<div class="input-prefix">
<i class="bi bi-link-45deg"></i>
<span>链接</span>
</div>
<input
type="text"
class="form-control custom-input"
v-model="link.url"
placeholder="输入链接地址"
>
</div>
<div class="input-group">
<div class="input-prefix">
<i class="bi bi-key"></i>
<span>密码</span>
</div>
<input
type="text"
class="form-control custom-input"
v-model="link.password"
placeholder="提取码(可选)"
>
</div>
<div class="input-group">
<div class="input-prefix">
<i class="bi bi-info-circle"></i>
<span>备注</span>
</div>
<input
type="text"
class="form-control custom-input"
v-model="link.note"
placeholder="如:第1季"
>
</div>
</div>
<button
type="button"
class="remove-link-btn"
@click="removeEditLink(category, index)"
title="删除链接"
>
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<!-- 添加链接按钮 -->
<div class="add-link-container">
<button
type="button"
class="btn-custom btn-outline add-link-btn"
@click="addEditLink(category)"
>
<i class="bi bi-plus-circle me-2"></i> 添加{{ getCategoryDisplayName(category) }}链接
</button>
</div>
</div>
</div>
</div>
</div>
<div v-if="saveError" class="save-error">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{{ saveError }}
</div>
<div class="form-actions">
<button type="submit" class="btn-custom btn-primary save-btn" :disabled="saving">
<span v-if="saving" class="spinner small-spinner me-1"></span>
<i v-else class="bi bi-check-circle me-2"></i>
保存
</button>
</div>
</div>
</form>
</div>
<!-- 预览界面 -->
<div v-else>
<!-- 顶部横幅 -->
<div class="preview-banner">
<div class="banner-content">
<!-- 左侧标题区域 -->
<div class="banner-title-area">
<div class="title-text">
<h1 class="title">{{ tmdbResource.title }}</h1>
<h2 class="subtitle">{{ tmdbResource.title_en }}</h2>
</div>
<!-- 分类标签移到这里在移动端会显示在标题右侧 -->
<div class="mobile-badge-container">
<div class="resource-type-badge">
{{ tmdbResource.resource_type }}
</div>
</div>
</div>
<!-- 右侧分类和操作按钮 -->
<div class="banner-action-area">
<!-- 桌面端分类标签 -->
<div class="desktop-badge-container">
<div class="resource-type-badge">
{{ tmdbResource.resource_type }}
</div>
</div>
<div class="action-buttons">
<button
@click="startEdit"
class="btn-custom btn-outline me-2"
>
<i class="bi bi-pencil-square me-1"></i>
<span class="btn-text">编辑</span>
</button>
<button
@click="importResource"
class="btn-custom btn-primary import-button"
:disabled="importing"
>
<i class="bi bi-cloud-download me-1"></i>
<span class="import-text">{{ importing ? '导入中...' : '一键导入' }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="info-section">
<!-- 右上标题区域 -->
<div class="resource-header">
<div class="header-content">
<!-- 标题区域重新组织 -->
<div class="title-area">
<!-- 标题和分类/导入按钮 -->
<div class="title-main">
<h1 class="title">{{ tmdbResource.title }}</h1>
<h2 class="subtitle">{{ tmdbResource.title_en }}</h2>
</div>
<!-- 右侧操作区分类和导入按钮 -->
<div class="action-area">
<div class="resource-type-badge">
{{ tmdbResource.resource_type }}
</div>
<button
@click="importResource"
class="btn-custom btn-primary import-button-top"
:disabled="importing"
<!-- 内容区域容器 -->
<div class="content-container">
<!-- 左侧图片区域 -->
<div class="media-section">
<!-- 大图展示区 -->
<div class="main-image-container" @click="previewImage(currentImage)">
<img
:src="currentImage"
class="resource-poster"
:alt="tmdbResource.title || tmdbResource.title_en"
>
</div>
<!-- 缩略图滚动区 -->
<div class="thumbnails-container" v-if="tmdbResource.images && tmdbResource.images.length > 1">
<div class="thumbnails-scroll">
<div
v-for="(image, index) in tmdbResource.images"
:key="index"
class="thumbnail"
:class="{ active: currentImage === image }"
@click="selectImage(image)"
>
<img
:src="image"
class="thumbnail-img"
:alt="`缩略图${index+1}`"
>
<i class="bi bi-cloud-download me-1"></i>
<span class="import-text">{{ importing ? '导入中...' : '一键导入' }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 简介 -->
<div class="description-card">
<div class="card-header">
<h3>简介</h3>
<!-- 信息区域 -->
<div class="info-section">
<!-- 简介 -->
<div class="description-card">
<div class="card-header">
<h3>简介</h3>
</div>
<div class="card-body">
<p>{{ tmdbResource.description }}</p>
</div>
</div>
<div class="card-body">
<p>{{ tmdbResource.description }}</p>
<!-- 资源链接预览 -->
<div v-if="hasResourceLinks" class="links-preview-card">
<div class="card-header">
<h3>资源链接</h3>
</div>
<div class="card-body">
<div class="links-tabs">
<button
v-for="(links, category) in tmdbResource.links"
:key="category"
class="tab-btn"
:class="{ active: activeCategory === category }"
@click.prevent="activeCategory = category"
v-show="links.length > 0"
>
{{ getCategoryDisplayName(category) }}
<span class="tab-badge">{{ links.length }}</span>
</button>
</div>
<div class="links-content">
<div v-for="(links, category) in tmdbResource.links" :key="`content-${category}`"
class="tab-pane"
:class="{ active: activeCategory === category }"
v-show="links.length > 0 && activeCategory === category">
<div class="links-table">
<div class="table-header">
<div class="col-link">链接</div>
<div class="col-password">提取码</div>
<div class="col-note">备注</div>
</div>
<div class="table-body">
<div class="table-row" v-for="(link, index) in links" :key="index">
<div class="col-link" data-label="链接">
<a
:href="link.url"
target="_blank"
rel="noopener noreferrer"
class="link-url"
>
<i class="bi bi-link-45deg"></i>
<span class="url-text">{{ link.url }}</span>
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="col-password" data-label="提取码">
<div v-if="link.password" class="password-text">
{{ link.password }}
</div>
<span v-else class="no-password">-</span>
</div>
<div class="col-note" data-label="备注">
<span class="note-text">{{ link.note || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -157,12 +490,75 @@ export default {
importing: false,
// 图片预览相关
showImagePreview: false,
previewImageUrl: ''
previewImageUrl: '',
// 编辑模式相关
isEditing: false,
_resourceWasEdited: false, // 内部标记,用于跟踪资源是否被编辑过
editForm: {
title: '',
title_en: '',
description: '',
resource_type: '',
poster_image: '',
images: []
},
// 链接编辑相关数据
editLinks: {
"magnet": [],
"ed2k": [],
"uc": [],
"mobile": [],
"tianyi": [],
"quark": [],
"115": [],
"aliyun": [],
"pikpak": [],
"baidu": [],
"123": [],
"xunlei": [],
"online": [],
"others": []
},
editActiveCategory: "magnet",
// 上传图片相关
isDragging: false,
uploading: false,
uploadProgress: 0,
currentUploadIndex: 0,
totalUploadCount: 0,
// 保存相关
saving: false,
saveError: null,
// 资源类型选项
resourceTypes: ['动画', '电影', '纪录片', '综艺', '其他'],
activeCategory: null
};
},
computed: {
isLoggedIn() {
return isAuthenticated();
},
// 判断资源是否已被编辑
hasBeenEdited() {
// 如果tmdbResource为空则返回false
if (!this.tmdbResource) return false;
// 检查是否有自定义链接
const hasCustomLinks = this.tmdbResource.links && Object.keys(this.tmdbResource.links).length > 0;
// 检查是否上传了自定义图片
// 原始TMDB资源的图片URL通常包含tmdb.org域名
const hasCustomImages = this.tmdbResource.images &&
this.tmdbResource.images.some(img => !img.includes('tmdb.org'));
return hasCustomLinks || hasCustomImages || this._resourceWasEdited;
},
hasResourceLinks() {
return this.tmdbResource && this.tmdbResource.links && Object.keys(this.tmdbResource.links).length > 0;
}
},
methods: {
@@ -185,6 +581,18 @@ export default {
if (this.tmdbResource && this.tmdbResource.images && this.tmdbResource.images.length > 0) {
this.currentImage = this.tmdbResource.poster_image || this.tmdbResource.images[0];
}
// 设置默认的活动链接类别
if (this.tmdbResource && this.tmdbResource.links) {
const categories = Object.keys(this.tmdbResource.links);
for (const category of categories) {
if (this.tmdbResource.links[category] && this.tmdbResource.links[category].length > 0) {
this.activeCategory = category;
break;
}
}
}
this.hasSearched = true;
} catch (error) {
console.error('TMDB搜索失败:', error);
@@ -205,19 +613,38 @@ export default {
document.body.style.overflow = 'hidden'; // 防止背景滚动
},
previewEditImage(image) {
this.previewImageUrl = image;
this.showImagePreview = true;
document.body.style.overflow = 'hidden';
},
closeImagePreview() {
this.showImagePreview = false;
document.body.style.overflow = ''; // 恢复滚动
},
async importResource() {
this.importing = true;
try {
const response = await axios.post('/api/tmdb/create', {
query: this.searchQuery.trim()
});
// 构建要提交的数据
const submitData = {
query: this.searchQuery.trim(),
// 如果资源已被编辑,则添加自定义字段
title: this.tmdbResource.title,
title_en: this.tmdbResource.title_en,
description: this.tmdbResource.description,
resource_type: this.tmdbResource.resource_type,
poster_image: this.tmdbResource.poster_image,
images: this.tmdbResource.images,
links: this.tmdbResource.links,
// 检查资源是否已被编辑过
is_custom: this.hasBeenEdited
};
// 调用API创建资源
const response = await axios.post('/api/tmdb/create', submitData);
this.importSuccess = true;
this.importedResourceId = response.data.id;
@@ -236,11 +663,288 @@ export default {
this.hasSearched = false;
this.importSuccess = false;
this.importedResourceId = null;
this.isEditing = false;
this.activeCategory = null;
},
resetSearch() {
this.resetState();
this.searchQuery = '';
},
// 编辑模式相关方法
startEdit() {
if (!this.tmdbResource) return;
// 复制当前资源数据到表单
this.editForm.title = this.tmdbResource.title || '';
this.editForm.title_en = this.tmdbResource.title_en || '';
this.editForm.description = this.tmdbResource.description || '';
this.editForm.resource_type = this.tmdbResource.resource_type || '';
this.editForm.poster_image = this.tmdbResource.poster_image || '';
this.editForm.images = [...(this.tmdbResource.images || [])];
// 初始化编辑链接
for (const category in this.editLinks) {
this.editLinks[category] = [];
}
// 加载已有的链接数据
if (this.tmdbResource.links) {
for (const category in this.tmdbResource.links) {
if (this.editLinks.hasOwnProperty(category) && this.tmdbResource.links[category]) {
// 深拷贝链接数据,避免直接引用
this.editLinks[category] = JSON.parse(JSON.stringify(this.tmdbResource.links[category]));
}
}
// 设置默认的编辑链接类别为第一个有链接的类别
let foundActiveCategory = false;
for (const category in this.editLinks) {
if (this.editLinks[category] && this.editLinks[category].length > 0) {
this.editActiveCategory = category;
foundActiveCategory = true;
break;
}
}
// 如果没有找到有链接的类别,则使用默认值
if (!foundActiveCategory) {
this.editActiveCategory = "magnet";
}
}
this.isEditing = true;
},
cancelEdit() {
this.isEditing = false;
this.saveError = null;
},
isTypeSelected(type) {
return this.editForm.resource_type === type;
},
selectResourceType(type) {
this.editForm.resource_type = type;
},
setPosterImage(image) {
this.editForm.poster_image = image;
},
removeImage(index) {
// 如果删除的是海报图片,则清空海报字段
if (this.editForm.images[index] === this.editForm.poster_image) {
this.editForm.poster_image = '';
}
this.editForm.images.splice(index, 1);
},
addEditLink(category) {
this.editLinks[category].push({
url: '',
password: '',
note: ''
});
},
removeEditLink(category, index) {
this.editLinks[category].splice(index, 1);
},
getCategoryDisplayName(category) {
const displayNames = {
"magnet": "磁力链接",
"ed2k": "电驴链接",
"uc": "UC网盘",
"mobile": "移动云盘",
"tianyi": "天翼云盘",
"quark": "夸克网盘",
"115": "115网盘",
"aliyun": "阿里云盘",
"pikpak": "PikPak",
"baidu": "百度网盘",
"123": "123网盘",
"xunlei": "迅雷云盘",
"online": "在线播放",
"others": "其他链接"
};
return displayNames[category] || category;
},
// 文件上传相关方法
handleFileDrop(event) {
this.isDragging = false;
const files = [...event.dataTransfer.files];
if (files.length > 0) {
this.uploadFiles(files);
}
},
handleFilesSelection(event) {
const files = [...event.target.files];
if (files.length > 0) {
this.uploadFiles(files);
}
},
async uploadFiles(files) {
// 过滤非图片文件
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) return;
this.uploading = true;
this.saveError = null;
this.uploadProgress = 0;
this.currentUploadIndex = 0;
this.totalUploadCount = imageFiles.length;
try {
for (let i = 0; i < imageFiles.length; i++) {
this.currentUploadIndex = i + 1;
const file = imageFiles[i];
const formData = new FormData();
formData.append('file', file);
const response = await axios.post('/api/resources/upload-images/', formData);
// 添加图片URL到已上传列表
this.editForm.images.push(response.data.filename);
// 更新进度
this.uploadProgress = Math.round(((i + 1) / imageFiles.length) * 100);
}
// 清除选择的文件
document.getElementById('file-upload').value = '';
} catch (err) {
console.error('上传图片失败:', err);
this.saveError = '上传图片失败,请稍后重试';
} finally {
this.uploading = false;
}
},
// 保存并导入资源
async saveChanges() {
// 移除登录检查
this.saving = true;
this.saveError = null;
try {
// 处理资源链接
const linksToSubmit = {};
let hasLinks = false;
for (const category in this.editLinks) {
// 过滤掉空链接
const validLinks = this.editLinks[category].filter(link => link.url.trim() !== '');
if (validLinks.length > 0) {
linksToSubmit[category] = validLinks;
hasLinks = true;
}
}
// 更新本地数据,而不是提交到服务器
this.tmdbResource = {
...this.tmdbResource,
title: this.editForm.title,
title_en: this.editForm.title_en,
description: this.editForm.description,
resource_type: this.editForm.resource_type,
poster_image: this.editForm.poster_image,
images: [...this.editForm.images],
links: linksToSubmit
};
// 标记资源已被编辑
this._resourceWasEdited = true;
// 确保当前图片设置正确
if (this.tmdbResource.images && this.tmdbResource.images.length > 0) {
this.currentImage = this.tmdbResource.poster_image || this.tmdbResource.images[0];
}
// 退出编辑模式
this.isEditing = false;
} catch (err) {
console.error('保存资源失败:', err);
this.saveError = err.message || '保存失败,请稍后重试';
} finally {
this.saving = false;
}
},
formatLinkUrl(url) {
// 格式化链接URL使其更友好显示
if (!url) return '';
try {
// 移除协议前缀
let formattedUrl = url.replace(/^(https?:\/\/)?(www\.)?/i, '');
// 如果链接太长,截断显示
if (formattedUrl.length > 40) {
formattedUrl = formattedUrl.substring(0, 37) + '...';
}
return formattedUrl;
} catch (e) {
return url;
}
},
copyToClipboard(text) {
if (!text) return;
// 创建一个临时文本区域元素
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
try {
// 选中并复制文本
textarea.select();
document.execCommand('copy');
// 创建并显示一个临时提示元素
const toast = document.createElement('div');
toast.textContent = '已复制到剪贴板';
toast.style.position = 'fixed';
toast.style.top = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.padding = '10px 20px';
toast.style.backgroundColor = '#28a745';
toast.style.color = '#fff';
toast.style.borderRadius = '4px';
toast.style.zIndex = '9999';
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s ease';
document.body.appendChild(toast);
// 显示提示
setTimeout(() => { toast.style.opacity = '1'; }, 10);
// 删除提示
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 2000);
} catch (err) {
console.error('复制失败:', err);
alert('复制失败,请手动复制');
} finally {
// 移除临时元素
document.body.removeChild(textarea);
}
}
},
beforeDestroy() {

View File

@@ -14,7 +14,16 @@ import (
// TMDBSearchRequest TMDB搜索请求
type TMDBSearchRequest struct {
Query string `json:"query" binding:"required"`
Query string `json:"query" binding:"required"`
// 添加自定义字段,支持自定义资源创建
Title string `json:"title"`
TitleEn string `json:"title_en"`
Description string `json:"description"`
ResourceType string `json:"resource_type"`
PosterImage string `json:"poster_image"`
Images []string `json:"images"`
Links map[string][]map[string]string `json:"links"`
IsCustom bool `json:"is_custom"` // 标识是否为自定义资源
}
// SearchTMDB 搜索TMDB API
@@ -51,7 +60,7 @@ func SearchTMDB(c *gin.Context) {
// CreateResourceFromTMDB 从TMDB搜索结果创建资源
// @Summary 从TMDB创建资源
// @Description 根据TMDB搜索结果创建新的资源
// @Description 根据TMDB搜索结果或用户自定义内容创建新的资源
// @Tags TMDB
// @Accept json
// @Produce json
@@ -61,7 +70,6 @@ func SearchTMDB(c *gin.Context) {
// @Failure 500 {object} gin.H
// @Router /api/tmdb/create [post]
func CreateResourceFromTMDB(c *gin.Context) {
var req TMDBSearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("解析请求失败: %v", err)
@@ -75,33 +83,71 @@ func CreateResourceFromTMDB(c *gin.Context) {
return
}
// 使用TMDB工具搜索
tmdbResource, err := utils.SearchTMDB(req.Query)
if err != nil {
log.Printf("TMDB搜索失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 转换为需要插入数据库的资源
now := time.Now()
defaultStatus := models.ResourceStatusPending
var resource *models.Resource
// 确保PosterImage不为空
posterImage := tmdbResource.PosterImage
// 创建资源对象
resource := &models.Resource{
Title: tmdbResource.Title,
TitleEn: tmdbResource.TitleEn,
Description: tmdbResource.Description,
ResourceType: tmdbResource.ResourceType,
PosterImage: &posterImage,
Images: tmdbResource.Images,
Links: models.JsonMap(tmdbResource.Links),
Status: defaultStatus,
CreatedAt: now,
UpdatedAt: now,
// 根据是否为自定义资源处理不同的逻辑
if req.IsCustom {
// 自定义资源处理逻辑
if strings.TrimSpace(req.Title) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "自定义资源的标题不能为空"})
return
}
// 确保PosterImage不为空
var posterImage *string
if req.PosterImage != "" {
posterImage = &req.PosterImage
} else if len(req.Images) > 0 {
posterImage = &req.Images[0]
}
// 将 req.Links 转换为 models.JsonMap 类型
linksMap := make(models.JsonMap)
for key, value := range req.Links {
linksMap[key] = value
}
// 创建自定义资源对象
resource = &models.Resource{
Title: req.Title,
TitleEn: req.TitleEn,
Description: req.Description,
ResourceType: req.ResourceType,
PosterImage: posterImage,
Images: req.Images,
Links: linksMap,
Status: defaultStatus,
CreatedAt: now,
UpdatedAt: now,
}
} else {
// 标准TMDB资源处理逻辑
tmdbResource, err := utils.SearchTMDB(req.Query)
if err != nil {
log.Printf("TMDB搜索失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 确保PosterImage不为空
posterImage := tmdbResource.PosterImage
// 创建资源对象
resource = &models.Resource{
Title: tmdbResource.Title,
TitleEn: tmdbResource.TitleEn,
Description: tmdbResource.Description,
ResourceType: tmdbResource.ResourceType,
PosterImage: &posterImage,
Images: tmdbResource.Images,
Links: models.JsonMap(tmdbResource.Links),
Status: defaultStatus,
CreatedAt: now,
UpdatedAt: now,
}
}
// 执行SQL插入