统一端口设置,可以设置自义定端口

This commit is contained in:
2025-08-24 20:38:28 +08:00
parent a35e82731d
commit 275a3672d2
12 changed files with 284 additions and 291 deletions
+2 -1
View File
@@ -63,10 +63,11 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功
4. **启动应用** 4. **启动应用**
- 修改代理端口,请用记事本打开 `start.bat` 文件,修改PROXY_PORT端口号 - 修改代理端口,请用记事本打开 `start.bat` 文件,修改PROXY_PORT端口号
- 修改服务器端口,请用记事本打开 `start.bat` 文件,修改SERVER_PORT端口号(默认3000
- 双击 `start.bat` 文件启动 - 双击 `start.bat` 文件启动
5. **访问应用** 5. **访问应用**
- 打开浏览器访问:http://localhost:3000 - 打开浏览器访问:http://localhost:3000(默认端口,可修改)
## 🌐 代理配置 ## 🌐 代理配置
+53 -33
View File
@@ -23,20 +23,21 @@ const proxyConfig = require('./config');
// 自定义日志中间件 // 自定义日志中间件
function customLogger(req, res, next) { function customLogger(req, res, next) {
// 过滤掉静态资源请求和图片代理请求 // 过滤掉静态资源请求和图片代理请求
const isStaticResource = req.path.startsWith('/assets/') || const isStaticResource =
req.path.startsWith('/downloads/') || req.path.startsWith('/assets/') ||
req.path.includes('.js') || req.path.startsWith('/downloads/') ||
req.path.includes('.css') || req.path.includes('.js') ||
req.path.includes('.ico') || req.path.includes('.css') ||
req.path.includes('.png') || req.path.includes('.ico') ||
req.path.includes('.jpg') || req.path.includes('.png') ||
req.path.includes('.jpeg') || req.path.includes('.jpg') ||
req.path.includes('.gif') || req.path.includes('.jpeg') ||
req.path.includes('.svg') || req.path.includes('.gif') ||
req.path.includes('.woff') || req.path.includes('.svg') ||
req.path.includes('.woff2') || req.path.includes('.woff') ||
req.path.includes('.ttf') || req.path.includes('.woff2') ||
req.path.includes('.eot'); req.path.includes('.ttf') ||
req.path.includes('.eot');
// 过滤掉图片代理请求 // 过滤掉图片代理请求
const isImageProxy = req.path === '/api/proxy/image'; const isImageProxy = req.path === '/api/proxy/image';
@@ -49,7 +50,7 @@ function customLogger(req, res, next) {
const originalEnd = res.end; const originalEnd = res.end;
// 重写响应结束方法以获取响应时间 // 重写响应结束方法以获取响应时间
res.end = function(chunk, encoding) { res.end = function (chunk, encoding) {
const duration = Date.now() - start; const duration = Date.now() - start;
const statusCode = res.statusCode; const statusCode = res.statusCode;
const method = req.method; const method = req.method;
@@ -74,12 +75,23 @@ function customLogger(req, res, next) {
// 根据请求类型选择图标 // 根据请求类型选择图标
let methodIcon; let methodIcon;
switch (method) { switch (method) {
case 'GET': methodIcon = '📥'; break; case 'GET':
case 'POST': methodIcon = '📤'; break; methodIcon = '📥';
case 'PUT': methodIcon = '🔄'; break; break;
case 'DELETE': methodIcon = '🗑️'; break; case 'POST':
case 'PATCH': methodIcon = '🔧'; break; methodIcon = '📤';
default: methodIcon = '❓'; break;
case 'PUT':
methodIcon = '🔄';
break;
case 'DELETE':
methodIcon = '🗑️';
break;
case 'PATCH':
methodIcon = '🔧';
break;
default:
methodIcon = '❓';
} }
// 格式化时间 // 格式化时间
@@ -88,7 +100,7 @@ function customLogger(req, res, next) {
hour12: false, hour12: false,
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit' second: '2-digit',
}); });
// 构建日志消息 // 构建日志消息
@@ -109,7 +121,7 @@ class PixivServer {
constructor() { constructor() {
this.app = express(); this.app = express();
this.backend = null; this.backend = null;
this.port = process.env.PORT || 3000; this.port = 3000; // 默认端口,会在init时重新设置
} }
/** /**
@@ -118,6 +130,9 @@ class PixivServer {
async init() { async init() {
console.log('\x1b[34m🔧 正在初始化 Pixiv 后端服务器...\x1b[0m'); console.log('\x1b[34m🔧 正在初始化 Pixiv 后端服务器...\x1b[0m');
// 重新设置端口(从环境变量获取)
this.port = process.env.PORT || 3000;
// 设置代理 // 设置代理
proxyConfig.setEnvironmentVariables(); proxyConfig.setEnvironmentVariables();
@@ -145,10 +160,12 @@ class PixivServer {
this.app.use(customLogger); this.app.use(customLogger);
// CORS 中间件 // CORS 中间件
this.app.use(cors({ this.app.use(
origin: process.env.FRONTEND_URL || 'http://localhost:5173', cors({
credentials: true origin: process.env.FRONTEND_URL || true, // 允许所有来源,或者通过环境变量指定
})); credentials: true,
})
);
// JSON 解析中间件 // JSON 解析中间件
this.app.use(express.json({ limit: '10mb' })); this.app.use(express.json({ limit: '10mb' }));
@@ -178,8 +195,8 @@ class PixivServer {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
backend: { backend: {
isLoggedIn: req.backend.isLoggedIn, isLoggedIn: req.backend.isLoggedIn,
user: req.backend.config.user?.account user: req.backend.config.user?.account,
} },
}); });
}); });
@@ -197,9 +214,9 @@ class PixivServer {
// 如果是API请求,返回JSON格式的404 // 如果是API请求,返回JSON格式的404
if (req.path.startsWith('/api/')) { if (req.path.startsWith('/api/')) {
return res.status(404).json({ return res.status(404).json({
error: 'Not Found', error: 'Not Found',
message: `Route ${req.originalUrl} not found` message: `Route ${req.originalUrl} not found`,
}); });
} }
// 否则返回前端页面(SPA路由支持) // 否则返回前端页面(SPA路由支持)
@@ -251,7 +268,10 @@ if (require.main === module) {
process.on('SIGTERM', () => server.shutdown()); process.on('SIGTERM', () => server.shutdown());
// 启动服务器 // 启动服务器
server.init().then(() => server.start()).catch(console.error); server
.init()
.then(() => server.start())
.catch(console.error);
} }
module.exports = PixivServer; module.exports = PixivServer;
+35 -5
View File
@@ -13,8 +13,31 @@ function parseArguments() {
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]; const arg = args[i];
if (arg === '--proxy-port' && i + 1 < args.length) {
options.proxyPort = parseInt(args[i + 1]); // 处理 --key=value 格式
if (arg.startsWith('--proxy-port=')) {
const port = parseInt(arg.split('=')[1]);
if (!isNaN(port)) {
options.proxyPort = port;
}
} else if (arg.startsWith('--server-port=')) {
const port = parseInt(arg.split('=')[1]);
if (!isNaN(port)) {
options.serverPort = port;
}
}
// 处理 --key value 格式(向后兼容)
else if (arg === '--proxy-port' && i + 1 < args.length) {
const port = parseInt(args[i + 1]);
if (!isNaN(port)) {
options.proxyPort = port;
}
i++; // 跳过下一个参数
} else if (arg === '--server-port' && i + 1 < args.length) {
const port = parseInt(args[i + 1]);
if (!isNaN(port)) {
options.serverPort = port;
}
i++; // 跳过下一个参数 i++; // 跳过下一个参数
} }
} }
@@ -34,6 +57,12 @@ if (cliOptions.proxyPort) {
console.log(`\x1b[36m📡 代理端口已设置为: ${cliOptions.proxyPort}\x1b[0m`); console.log(`\x1b[36m📡 代理端口已设置为: ${cliOptions.proxyPort}\x1b[0m`);
} }
// 如果提供了服务器端口,设置环境变量
if (cliOptions.serverPort) {
process.env.PORT = cliOptions.serverPort.toString();
console.log(`\x1b[36m🌐 服务器端口已设置为: ${cliOptions.serverPort}\x1b[0m`);
}
console.log('\x1b[35m🚀 启动 Pixiv 后端服务器...\x1b[0m'); console.log('\x1b[35m🚀 启动 Pixiv 后端服务器...\x1b[0m');
// 创建服务器实例 // 创建服务器实例
@@ -51,7 +80,7 @@ process.on('SIGTERM', async () => {
}); });
// 处理未捕获的异常 // 处理未捕获的异常
process.on('uncaughtException', (error) => { process.on('uncaughtException', error => {
console.error('\x1b[31m❌ 未捕获的异常:\x1b[0m', error); console.error('\x1b[31m❌ 未捕获的异常:\x1b[0m', error);
process.exit(1); process.exit(1);
}); });
@@ -62,9 +91,10 @@ process.on('unhandledRejection', (reason, promise) => {
}); });
// 启动服务器 // 启动服务器
server.init() server
.init()
.then(() => server.start()) .then(() => server.start())
.catch((error) => { .catch(error => {
console.error('\x1b[31m❌ 服务器启动失败:\x1b[0m', error); console.error('\x1b[31m❌ 服务器启动失败:\x1b[0m', error);
process.exit(1); process.exit(1);
}); });
+16 -11
View File
@@ -19,14 +19,11 @@ async function createPortable() {
// 创建启动脚本 // 创建启动脚本
const startScript = `@echo off const startScript = `@echo off
chcp 65001 >nul
title Pixiv Manager title Pixiv Manager
:: ========================================
:: 代理配置 - 请根据你的代理软件修改端口号
:: 常见端口: Clash=7890, V2Ray=10809, Shadowsocks=1080
:: ========================================
set PROXY_PORT=7890 set PROXY_PORT=7890
set SERVER_PORT=3000
echo. echo.
echo ======================================== echo ========================================
@@ -37,17 +34,19 @@ echo.
cd /d "%~dp0" cd /d "%~dp0"
echo Current proxy port: %PROXY_PORT% echo Current proxy port: %PROXY_PORT%
echo To change proxy port, edit this file and modify line 6 echo Current server port: %SERVER_PORT%
echo To change proxy port, edit PROXY_PORT=xxxx in this file
echo To change server port, edit SERVER_PORT=xxxx in this file
echo. echo.
echo Starting backend server... echo Starting backend server...
echo Access URL: http://localhost:3000 echo Access URL: http://localhost:%SERVER_PORT%
echo. echo.
echo Tip: Press Ctrl+C to stop server echo Tip: Press Ctrl+C to stop server
echo. echo.
:: Start server and pass proxy port :: Start server and pass proxy port and server port
pixiv-backend.exe --proxy-port=%PROXY_PORT% pixiv-backend.exe --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT%
echo. echo.
echo Server stopped echo Server stopped
@@ -65,13 +64,19 @@ pause
2. 在浏览器中访问 http://localhost:3000 2. 在浏览器中访问 http://localhost:3000
3. 按 Ctrl+C 停止服务器 3. 按 Ctrl+C 停止服务器
## 代理设置(重要) ## 配置设置
如需使用代理,请用记事本编辑 \`start.bat\` 文件,修改(PROXY_PORT=xxxx)的端口号 如需修改配置,请用记事本编辑 \`start.bat\` 文件:
### 代理设置(重要)
修改(PROXY_PORT=xxxx)的端口号:
- Clash: 7890 - Clash: 7890
- V2Ray: 10809 - V2Ray: 10809
- Shadowsocks: 1080 - Shadowsocks: 1080
### 服务器端口设置
修改(SERVER_PORT=xxxx)的端口号,默认为3000
## 注意事项 ## 注意事项
- 首次运行可能需要几秒钟启动时间 - 首次运行可能需要几秒钟启动时间
+14 -4
View File
@@ -7,6 +7,11 @@ chcp 65001 >nul
:: ======================================== :: ========================================
set PROXY_PORT= set PROXY_PORT=
:: ========================================
:: 服务器端口配置 - 默认3000
:: ========================================
set SERVER_PORT=3000
echo. echo.
echo 🚀 Pixiv Manager 启动中... echo 🚀 Pixiv Manager 启动中...
echo. echo.
@@ -14,17 +19,22 @@ echo.
cd /d "%~dp0" cd /d "%~dp0"
echo 📡 当前代理端口: %PROXY_PORT% echo 📡 当前代理端口: %PROXY_PORT%
echo 💡 如需修改代理端口,请用记事本打开此文件,修改第6行的端口号 echo 🌐 当前服务器端口: %SERVER_PORT%
echo 💡 如需修改端口,请用记事本打开此文件,修改对应的端口号
echo. echo.
echo 📊 启动后端服务器... echo 📊 启动后端服务器...
echo 🌐 访问地址: http://localhost:3000 echo 🌐 访问地址: http://localhost:%SERVER_PORT%
echo. echo.
echo 💡 提示: 按 Ctrl+C 停止服务器 echo 💡 提示: 按 Ctrl+C 停止服务器
echo. echo.
:: 启动服务器并传递代理端口 :: 启动服务器并传递代理端口和服务器端口
node backend/start.js --proxy-port=%PROXY_PORT% if "%PROXY_PORT%"=="" (
node backend/start.js --server-port=%SERVER_PORT%
) else (
node backend/start.js --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT%
)
echo. echo.
echo ⏹️ 服务器已停止 echo ⏹️ 服务器已停止
+2
View File
@@ -120,6 +120,8 @@ pnpm lint
VITE_API_BASE_URL=http://localhost:3000 VITE_API_BASE_URL=http://localhost:3000
``` ```
> **注意**: 在生产环境中,前端会自动使用当前域名和端口,无需手动设置此环境变量。
### 构建部署 ### 构建部署
```bash ```bash
+11 -30
View File
@@ -1,30 +1,18 @@
<template> <template>
<div class="artist-card"> <div class="artist-card">
<div class="artist-header"> <div class="artist-header">
<img <img :src="getImageUrl(artist.profile_image_urls.medium)" :alt="artist.name" class="artist-avatar"
:src="getImageUrl(artist.profile_image_urls.medium)" crossorigin="anonymous" />
:alt="artist.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-info"> <div class="artist-info">
<h3 class="artist-name">{{ artist.name }}</h3> <h3 class="artist-name">{{ artist.name }}</h3>
<p class="artist-account">@{{ artist.account }}</p> <p class="artist-account">@{{ artist.account }}</p>
</div> </div>
<div class="artist-actions"> <div class="artist-actions">
<button <button v-if="showFollowButton" @click="handleFollowClick" class="btn btn-primary btn-small"
v-if="showFollowButton" :disabled="artist.is_followed">
@click="handleFollowClick"
class="btn btn-primary btn-small"
:disabled="artist.is_followed"
>
{{ artist.is_followed ? '已关注' : '关注' }} {{ artist.is_followed ? '已关注' : '关注' }}
</button> </button>
<button <button v-if="showUnfollowButton" @click="handleUnfollowClick" class="btn btn-danger btn-small">
v-if="showUnfollowButton"
@click="handleUnfollowClick"
class="btn btn-danger btn-small"
>
取消关注 取消关注
</button> </button>
</div> </div>
@@ -42,6 +30,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getImageProxyUrl } from '@/services/api';
interface Artist { interface Artist {
id: number id: number
name: string name: string
@@ -86,18 +76,8 @@ const handleDownloadClick = () => {
emit('download', props.artist) emit('download', props.artist)
} }
// 处理图片URL,通过后端代理 // 使用统一的图片代理函数
const getImageUrl = (originalUrl: string) => { const getImageUrl = getImageProxyUrl
if (!originalUrl) return ''
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl)
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`
}
return originalUrl
}
</script> </script>
<style scoped> <style scoped>
@@ -135,7 +115,8 @@ const getImageUrl = (originalUrl: string) => {
.artist-info { .artist-info {
flex: 1; flex: 1;
min-width: 0; /* 防止文本溢出 */ min-width: 0;
/* 防止文本溢出 */
} }
.artist-name { .artist-name {
+12 -33
View File
@@ -1,14 +1,8 @@
<template> <template>
<div class="artwork-card" @click="handleClick"> <div class="artwork-card" @click="handleClick">
<div class="artwork-image"> <div class="artwork-image">
<img <img :src="getImageUrl(artwork.image_urls.medium)" :alt="artwork.title" @load="imageLoaded = true"
:src="getImageUrl(artwork.image_urls.medium)" @error="imageError = true" :class="{ loaded: imageLoaded, error: imageError }" crossorigin="anonymous" />
:alt="artwork.title"
@load="imageLoaded = true"
@error="imageError = true"
:class="{ loaded: imageLoaded, error: imageError }"
crossorigin="anonymous"
/>
<div v-if="!imageLoaded && !imageError" class="image-placeholder"> <div v-if="!imageLoaded && !imageError" class="image-placeholder">
<LoadingSpinner text="加载中..." /> <LoadingSpinner text="加载中..." />
</div> </div>
@@ -24,25 +18,23 @@
<div class="artwork-meta"> <div class="artwork-meta">
<div class="artist-info"> <div class="artist-info">
<img <img :src="getImageUrl(artwork.user.profile_image_urls.medium)" :alt="artwork.user.name" class="artist-avatar"
:src="getImageUrl(artwork.user.profile_image_urls.medium)" crossorigin="anonymous" />
:alt="artwork.user.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<span class="artist-name">{{ artwork.user.name }}</span> <span class="artist-name">{{ artwork.user.name }}</span>
</div> </div>
<div class="artwork-stats"> <div class="artwork-stats">
<span class="stat"> <span class="stat">
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/> <path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg> </svg>
{{ artwork.total_bookmarks }} {{ artwork.total_bookmarks }}
</span> </span>
<span class="stat"> <span class="stat">
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/> <path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg> </svg>
{{ artwork.total_view }} {{ artwork.total_view }}
</span> </span>
@@ -50,11 +42,7 @@
</div> </div>
<div class="artwork-tags"> <div class="artwork-tags">
<span <span v-for="tag in artwork.tags.slice(0, 3)" :key="tag.name" class="tag">
v-for="tag in artwork.tags.slice(0, 3)"
:key="tag.name"
class="tag"
>
{{ tag.name }} {{ tag.name }}
</span> </span>
<span v-if="artwork.tags.length > 3" class="tag-more"> <span v-if="artwork.tags.length > 3" class="tag-more">
@@ -68,6 +56,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import { getImageProxyUrl } from '@/services/api';
import type { Artwork } from '@/types'; import type { Artwork } from '@/types';
interface Props { interface Props {
@@ -88,18 +77,8 @@ const handleClick = () => {
emit('click', props.artwork); emit('click', props.artwork);
}; };
// 处理图片URL,通过后端代理 // 使用统一的图片代理函数
const getImageUrl = (originalUrl: string) => { const getImageUrl = getImageProxyUrl;
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
</script> </script>
<style scoped> <style scoped>
+19 -3
View File
@@ -1,15 +1,31 @@
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'; import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
import type { ApiResponse } from '@/types'; import type { ApiResponse } from '@/types';
// API配置 // API配置 - 使用相对路径,这样就不需要硬编码端口
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
// 导出API基础URL,供其他组件使用
export const getApiBaseUrl = () => API_BASE_URL || window.location.origin;
// 获取图片代理URL的工具函数
export const getImageProxyUrl = (originalUrl: string) => {
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `${getApiBaseUrl()}/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
class ApiService { class ApiService {
private client: AxiosInstance; private client: AxiosInstance;
constructor() { constructor() {
this.client = axios.create({ this.client = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL || window.location.origin,
timeout: 60000, // 增加到60秒 timeout: 60000, // 增加到60秒
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+2 -2
View File
@@ -1,4 +1,4 @@
import apiService from './api'; import apiService, { getApiBaseUrl } from './api';
import type { DownloadTask } from '@/types'; import type { DownloadTask } from '@/types';
class DownloadService { class DownloadService {
@@ -132,7 +132,7 @@ class DownloadService {
* 使用SSE监听下载进度 * 使用SSE监听下载进度
*/ */
streamTaskProgress(taskId: string, onProgress: (task: DownloadTask) => void, onComplete?: () => void) { streamTaskProgress(taskId: string, onProgress: (task: DownloadTask) => void, onComplete?: () => void) {
const eventSource = new EventSource(`http://localhost:3000/api/download/stream/${taskId}`); const eventSource = new EventSource(`${getApiBaseUrl()}/api/download/stream/${taskId}`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
+3 -12
View File
@@ -124,6 +124,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import artistService from '@/services/artist'; import artistService from '@/services/artist';
import downloadService from '@/services/download'; import downloadService from '@/services/download';
import { getImageProxyUrl } from '@/services/api';
import type { Artist, Artwork } from '@/types'; import type { Artist, Artwork } from '@/types';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import ErrorMessage from '@/components/common/ErrorMessage.vue'; import ErrorMessage from '@/components/common/ErrorMessage.vue';
@@ -385,18 +386,8 @@ const handleDownloadAll = async () => {
} }
}; };
// 处理图片URL,通过后端代理 // 使用统一的图片代理函数
const getImageUrl = (originalUrl: string) => { const getImageUrl = getImageProxyUrl;
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
// 点击作品 // 点击作品
const handleArtworkClick = (artwork: Artwork) => { const handleArtworkClick = (artwork: Artwork) => {
+31 -73
View File
@@ -13,14 +13,8 @@
<!-- 作品图片 --> <!-- 作品图片 -->
<div class="artwork-gallery"> <div class="artwork-gallery">
<div class="main-image"> <div class="main-image">
<img <img :src="getImageUrl(currentImageUrl)" :alt="artwork.title" @load="imageLoaded = true"
:src="getImageUrl(currentImageUrl)" @error="imageError = true" :class="{ loaded: imageLoaded, error: imageError }" crossorigin="anonymous" />
:alt="artwork.title"
@load="imageLoaded = true"
@error="imageError = true"
:class="{ loaded: imageLoaded, error: imageError }"
crossorigin="anonymous"
/>
<div v-if="!imageLoaded && !imageError" class="image-placeholder"> <div v-if="!imageLoaded && !imageError" class="image-placeholder">
<LoadingSpinner text="加载中..." /> <LoadingSpinner text="加载中..." />
</div> </div>
@@ -31,14 +25,10 @@
<!-- 多页作品缩略图 --> <!-- 多页作品缩略图 -->
<div v-if="artwork.page_count > 1" class="thumbnails"> <div v-if="artwork.page_count > 1" class="thumbnails">
<button <button v-for="(page, index) in artwork.meta_pages" :key="index" @click="currentPage = index"
v-for="(page, index) in artwork.meta_pages" class="thumbnail" :class="{ active: currentPage === index }">
:key="index" <img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`"
@click="currentPage = index" crossorigin="anonymous" />
class="thumbnail"
:class="{ active: currentPage === index }"
>
<img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`" crossorigin="anonymous" />
</button> </button>
</div> </div>
</div> </div>
@@ -63,30 +53,21 @@
<div v-if="isDownloaded && !currentTask" class="download-status"> <div v-if="isDownloaded && !currentTask" class="download-status">
<div class="status-indicator"> <div class="status-indicator">
<svg viewBox="0 0 24 24" fill="currentColor" class="status-icon"> <svg viewBox="0 0 24 24" fill="currentColor" class="status-icon">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg> </svg>
<span>已下载到本地</span> <span>已下载到本地</span>
</div> </div>
</div> </div>
<!-- 下载进度 --> <!-- 下载进度 -->
<DownloadProgress <DownloadProgress v-if="currentTask" :task="currentTask" :loading="downloading" @update="updateTask"
v-if="currentTask" @remove="removeTask" />
:task="currentTask"
:loading="downloading"
@update="updateTask"
@remove="removeTask"
/>
</div> </div>
<!-- 作者信息 --> <!-- 作者信息 -->
<div class="artist-info"> <div class="artist-info">
<img <img :src="getImageUrl(artwork.user.profile_image_urls.medium)" :alt="artwork.user.name"
:src="getImageUrl(artwork.user.profile_image_urls.medium)" class="artist-avatar" crossorigin="anonymous" />
:alt="artwork.user.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-details"> <div class="artist-details">
<h3 class="artist-name">{{ artwork.user.name }}</h3> <h3 class="artist-name">{{ artwork.user.name }}</h3>
<p class="artist-account">@{{ artwork.user.account }}</p> <p class="artist-account">@{{ artwork.user.account }}</p>
@@ -98,36 +79,24 @@
<!-- 作品导航 --> <!-- 作品导航 -->
<div v-if="showNavigation" class="artwork-navigation"> <div v-if="showNavigation" class="artwork-navigation">
<button <button @click="goBackToArtist" class="nav-btn nav-back" title="返回作者页面">
@click="goBackToArtist"
class="nav-btn nav-back"
title="返回作者页面"
>
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/> <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg> </svg>
<span>返回</span> <span>返回</span>
</button> </button>
<button <button @click="navigateToPrevious" class="nav-btn nav-prev" :disabled="!previousArtwork"
@click="navigateToPrevious" :title="previousArtwork ? `上一个: ${previousArtwork.title}` : '没有上一个作品'">
class="nav-btn nav-prev"
:disabled="!previousArtwork"
:title="previousArtwork ? `上一个: ${previousArtwork.title}` : '没有上一个作品'"
>
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg> </svg>
<span>上一个</span> <span>上一个</span>
</button> </button>
<button <button @click="navigateToNext" class="nav-btn nav-next" :disabled="!nextArtwork"
@click="navigateToNext" :title="nextArtwork ? `下一个: ${nextArtwork.title}` : '没有下一个作品'">
class="nav-btn nav-next"
:disabled="!nextArtwork"
:title="nextArtwork ? `下一个: ${nextArtwork.title}` : '没有下一个作品'"
>
<span>下一个</span> <span>下一个</span>
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/> <path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -136,19 +105,22 @@
<div class="artwork-stats"> <div class="artwork-stats">
<div class="stat"> <div class="stat">
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/> <path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg> </svg>
<span>{{ artwork.total_bookmarks }}</span> <span>{{ artwork.total_bookmarks }}</span>
</div> </div>
<div class="stat"> <div class="stat">
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/> <path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg> </svg>
<span>{{ artwork.total_view }}</span> <span>{{ artwork.total_view }}</span>
</div> </div>
<div class="stat"> <div class="stat">
<svg viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/> <path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z" />
</svg> </svg>
<span>{{ artwork.width }} × {{ artwork.height }}</span> <span>{{ artwork.width }} × {{ artwork.height }}</span>
</div> </div>
@@ -158,14 +130,9 @@
<div class="artwork-tags"> <div class="artwork-tags">
<h3>标签</h3> <h3>标签</h3>
<div class="tags-list"> <div class="tags-list">
<button <button v-for="tag in artwork.tags" :key="tag.name" @click="handleTagClick($event, tag.name)"
v-for="tag in artwork.tags" class="tag tag-clickable" :class="{ 'tag-selected': selectedTags.includes(tag.name) }"
:key="tag.name" :title="`搜索标签: ${tag.name} (按住Ctrl键点击选择多个标签,松开Ctrl键搜索)`">
@click="handleTagClick($event, tag.name)"
class="tag tag-clickable"
:class="{ 'tag-selected': selectedTags.includes(tag.name) }"
:title="`搜索标签: ${tag.name} (按住Ctrl键点击选择多个标签,松开Ctrl键搜索)`"
>
{{ tag.name }} {{ tag.name }}
</button> </button>
</div> </div>
@@ -198,6 +165,7 @@ import artworkService from '@/services/artwork';
import artistService from '@/services/artist'; import artistService from '@/services/artist';
import downloadService from '@/services/download'; import downloadService from '@/services/download';
import { useRepositoryStore } from '@/stores/repository'; import { useRepositoryStore } from '@/stores/repository';
import { getImageProxyUrl } from '@/services/api';
import type { Artwork, DownloadTask } from '@/types'; import type { Artwork, DownloadTask } from '@/types';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import ErrorMessage from '@/components/common/ErrorMessage.vue'; import ErrorMessage from '@/components/common/ErrorMessage.vue';
@@ -446,18 +414,8 @@ const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN'); return new Date(dateString).toLocaleDateString('zh-CN');
}; };
// 处理图片URL,通过后端代理 // 使用统一的图片代理函数
const getImageUrl = (originalUrl: string) => { const getImageUrl = getImageProxyUrl;
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
// 清除错误 // 清除错误
const clearError = () => { const clearError = () => {