统一端口设置,可以设置自义定端口
This commit is contained in:
@@ -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(默认端口,可修改)
|
||||||
|
|
||||||
## 🌐 代理配置
|
## 🌐 代理配置
|
||||||
|
|
||||||
|
|||||||
+82
-62
@@ -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';
|
||||||
@@ -44,17 +45,17 @@ function customLogger(req, res, next) {
|
|||||||
// 只记录API请求和重要请求,排除静态资源和图片代理
|
// 只记录API请求和重要请求,排除静态资源和图片代理
|
||||||
if (!isStaticResource && !isImageProxy) {
|
if (!isStaticResource && !isImageProxy) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
// 原始响应结束方法
|
// 原始响应结束方法
|
||||||
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;
|
||||||
const url = req.originalUrl;
|
const url = req.originalUrl;
|
||||||
|
|
||||||
// 根据状态码选择颜色和图标
|
// 根据状态码选择颜色和图标
|
||||||
let statusIcon, statusColor;
|
let statusIcon, statusColor;
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
@@ -70,38 +71,49 @@ function customLogger(req, res, next) {
|
|||||||
statusIcon = '❌';
|
statusIcon = '❌';
|
||||||
statusColor = '\x1b[31m'; // 红色
|
statusColor = '\x1b[31m'; // 红色
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据请求类型选择图标
|
// 根据请求类型选择图标
|
||||||
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 = '❓';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeStr = now.toLocaleTimeString('zh-CN', {
|
const timeStr = now.toLocaleTimeString('zh-CN', {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 构建日志消息
|
// 构建日志消息
|
||||||
const logMessage = `${statusColor}${statusIcon} ${methodIcon} ${method} ${url} ${statusCode} ${duration}ms\x1b[0m`;
|
const logMessage = `${statusColor}${statusIcon} ${methodIcon} ${method} ${url} ${statusCode} ${duration}ms\x1b[0m`;
|
||||||
|
|
||||||
// 输出日志
|
// 输出日志
|
||||||
console.log(`[${timeStr}] ${logMessage}`);
|
console.log(`[${timeStr}] ${logMessage}`);
|
||||||
|
|
||||||
// 调用原始的end方法
|
// 调用原始的end方法
|
||||||
originalEnd.call(this, chunk, encoding);
|
originalEnd.call(this, chunk, encoding);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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时重新设置
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,23 +129,26 @@ 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();
|
||||||
|
|
||||||
// 初始化 Pixiv 后端
|
// 初始化 Pixiv 后端
|
||||||
this.backend = new PixivBackend();
|
this.backend = new PixivBackend();
|
||||||
await this.backend.init();
|
await this.backend.init();
|
||||||
|
|
||||||
// 配置中间件
|
// 配置中间件
|
||||||
this.setupMiddleware();
|
this.setupMiddleware();
|
||||||
|
|
||||||
// 配置路由
|
// 配置路由
|
||||||
this.setupRoutes();
|
this.setupRoutes();
|
||||||
|
|
||||||
// 配置错误处理 - 临时注释掉
|
// 配置错误处理 - 临时注释掉
|
||||||
this.setupErrorHandling();
|
this.setupErrorHandling();
|
||||||
|
|
||||||
console.log('\x1b[32m✅ 服务器初始化完成\x1b[0m');
|
console.log('\x1b[32m✅ 服务器初始化完成\x1b[0m');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,23 +158,25 @@ class PixivServer {
|
|||||||
setupMiddleware() {
|
setupMiddleware() {
|
||||||
// 自定义日志中间件(替换morgan)
|
// 自定义日志中间件(替换morgan)
|
||||||
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' }));
|
||||||
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
this.app.use('/downloads', express.static(path.join(__dirname, '../downloads')));
|
this.app.use('/downloads', express.static(path.join(__dirname, '../downloads')));
|
||||||
|
|
||||||
// 前端静态文件服务
|
// 前端静态文件服务
|
||||||
this.app.use(express.static(path.join(__dirname, '../ui/dist')));
|
this.app.use(express.static(path.join(__dirname, '../ui/dist')));
|
||||||
|
|
||||||
// 将后端实例注入到请求对象中
|
// 将后端实例注入到请求对象中
|
||||||
this.app.use((req, res, next) => {
|
this.app.use((req, res, next) => {
|
||||||
req.backend = this.backend;
|
req.backend = this.backend;
|
||||||
@@ -173,13 +190,13 @@ class PixivServer {
|
|||||||
setupRoutes() {
|
setupRoutes() {
|
||||||
// 健康检查
|
// 健康检查
|
||||||
this.app.get('/health', (req, res) => {
|
this.app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
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,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,17 +208,17 @@ class PixivServer {
|
|||||||
this.app.use('/api/ranking', authMiddleware, rankingRoutes);
|
this.app.use('/api/ranking', authMiddleware, rankingRoutes);
|
||||||
this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证
|
this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证
|
||||||
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
|
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
|
||||||
|
|
||||||
// 404 处理
|
// 404 处理
|
||||||
this.app.use((req, res) => {
|
this.app.use((req, res) => {
|
||||||
// 如果是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路由支持)
|
||||||
res.sendFile(path.join(__dirname, '../ui/dist/index.html'));
|
res.sendFile(path.join(__dirname, '../ui/dist/index.html'));
|
||||||
});
|
});
|
||||||
@@ -245,13 +262,16 @@ class PixivServer {
|
|||||||
// 如果直接运行此文件
|
// 如果直接运行此文件
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const server = new PixivServer();
|
const server = new PixivServer();
|
||||||
|
|
||||||
// 处理进程信号
|
// 处理进程信号
|
||||||
process.on('SIGINT', () => server.shutdown());
|
process.on('SIGINT', () => server.shutdown());
|
||||||
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;
|
||||||
|
|||||||
+38
-8
@@ -10,15 +10,38 @@ const PixivServer = require('./server');
|
|||||||
function parseArguments() {
|
function parseArguments() {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const options = {};
|
const options = {};
|
||||||
|
|
||||||
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++; // 跳过下一个参数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 首次运行可能需要几秒钟启动时间
|
- 首次运行可能需要几秒钟启动时间
|
||||||
|
|||||||
@@ -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 ⏹️ 服务器已停止
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ pnpm lint
|
|||||||
VITE_API_BASE_URL=http://localhost:3000
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **注意**: 在生产环境中,前端会自动使用当前域名和端口,无需手动设置此环境变量。
|
||||||
|
|
||||||
### 构建部署
|
### 构建部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="artist-actions-bottom">
|
<div class="artist-actions-bottom">
|
||||||
<router-link :to="`/artist/${artist.id}`" class="btn btn-primary btn-small">
|
<router-link :to="`/artist/${artist.id}`" class="btn btn-primary btn-small">
|
||||||
查看作品
|
查看作品
|
||||||
@@ -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 {
|
||||||
@@ -228,14 +209,14 @@ const getImageUrl = (originalUrl: string) => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist-actions-bottom {
|
.artist-actions-bottom {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist-avatar {
|
.artist-avatar {
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -16,45 +10,39 @@
|
|||||||
<span>图片加载失败</span>
|
<span>图片加载失败</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="artwork-info">
|
<div class="artwork-info">
|
||||||
<h3 class="artwork-title" :title="artwork.title">
|
<h3 class="artwork-title" :title="artwork.title">
|
||||||
{{ artwork.title }}
|
{{ artwork.title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
@@ -239,4 +218,4 @@ const getImageUrl = (originalUrl: string) => {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+19
-3
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
+74
-116
@@ -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';
|
||||||
@@ -231,13 +199,13 @@ const navigationLoading = ref(false);
|
|||||||
// 计算属性
|
// 计算属性
|
||||||
const currentImageUrl = computed(() => {
|
const currentImageUrl = computed(() => {
|
||||||
if (!artwork.value) return '';
|
if (!artwork.value) return '';
|
||||||
|
|
||||||
if (artwork.value.page_count === 1) {
|
if (artwork.value.page_count === 1) {
|
||||||
return artwork.value.image_urls.large;
|
return artwork.value.image_urls.large;
|
||||||
} else if (artwork.value.meta_pages && artwork.value.meta_pages[currentPage.value]) {
|
} else if (artwork.value.meta_pages && artwork.value.meta_pages[currentPage.value]) {
|
||||||
return artwork.value.meta_pages[currentPage.value].image_urls.large;
|
return artwork.value.meta_pages[currentPage.value].image_urls.large;
|
||||||
}
|
}
|
||||||
|
|
||||||
return artwork.value.image_urls.large;
|
return artwork.value.image_urls.large;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -275,13 +243,13 @@ const fetchArtworkDetail = async () => {
|
|||||||
imageLoaded.value = false;
|
imageLoaded.value = false;
|
||||||
imageError.value = false;
|
imageError.value = false;
|
||||||
currentPage.value = 0;
|
currentPage.value = 0;
|
||||||
|
|
||||||
// 清理之前的任务状态
|
// 清理之前的任务状态
|
||||||
currentTask.value = null;
|
currentTask.value = null;
|
||||||
stopTaskStreaming();
|
stopTaskStreaming();
|
||||||
|
|
||||||
const response = await artworkService.getArtworkDetail(artworkId);
|
const response = await artworkService.getArtworkDetail(artworkId);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
artwork.value = response.data;
|
artwork.value = response.data;
|
||||||
// 检查下载状态
|
// 检查下载状态
|
||||||
@@ -302,9 +270,9 @@ const checkDownloadStatus = async (artworkId: number) => {
|
|||||||
try {
|
try {
|
||||||
checkingDownloadStatus.value = true;
|
checkingDownloadStatus.value = true;
|
||||||
const response = await repositoryStore.checkArtworkDownloaded(artworkId);
|
const response = await repositoryStore.checkArtworkDownloaded(artworkId);
|
||||||
|
|
||||||
console.log('下载状态检查响应:', response);
|
console.log('下载状态检查响应:', response);
|
||||||
|
|
||||||
// repository store的apiCall返回的是data.data,所以response直接是数据对象
|
// repository store的apiCall返回的是data.data,所以response直接是数据对象
|
||||||
if (response && typeof response === 'object') {
|
if (response && typeof response === 'object') {
|
||||||
isDownloaded.value = response.is_downloaded || false;
|
isDownloaded.value = response.is_downloaded || false;
|
||||||
@@ -329,10 +297,10 @@ const handleDownload = async () => {
|
|||||||
const response = await downloadService.downloadArtwork(artwork.value.id, {
|
const response = await downloadService.downloadArtwork(artwork.value.id, {
|
||||||
skipExisting
|
skipExisting
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log('下载响应:', response.data);
|
console.log('下载响应:', response.data);
|
||||||
|
|
||||||
// 检查是否跳过下载
|
// 检查是否跳过下载
|
||||||
if (response.data.skipped) {
|
if (response.data.skipped) {
|
||||||
console.log('作品已存在,跳过下载');
|
console.log('作品已存在,跳过下载');
|
||||||
@@ -340,7 +308,7 @@ const handleDownload = async () => {
|
|||||||
await checkDownloadStatus(artwork.value.id);
|
await checkDownloadStatus(artwork.value.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是新任务,开始监听进度
|
// 如果是新任务,开始监听进度
|
||||||
if (response.data.task_id) {
|
if (response.data.task_id) {
|
||||||
currentTask.value = {
|
currentTask.value = {
|
||||||
@@ -354,7 +322,7 @@ const handleDownload = async () => {
|
|||||||
artwork_id: artwork.value.id,
|
artwork_id: artwork.value.id,
|
||||||
start_time: new Date().toISOString()
|
start_time: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始SSE监听任务进度
|
// 开始SSE监听任务进度
|
||||||
startTaskStreaming(response.data.task_id);
|
startTaskStreaming(response.data.task_id);
|
||||||
}
|
}
|
||||||
@@ -375,9 +343,9 @@ const startTaskStreaming = (taskId: string) => {
|
|||||||
if (sseConnection.value) {
|
if (sseConnection.value) {
|
||||||
sseConnection.value();
|
sseConnection.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('开始SSE监听任务进度:', taskId);
|
console.log('开始SSE监听任务进度:', taskId);
|
||||||
|
|
||||||
// 建立SSE连接
|
// 建立SSE连接
|
||||||
sseConnection.value = downloadService.streamTaskProgress(
|
sseConnection.value = downloadService.streamTaskProgress(
|
||||||
taskId,
|
taskId,
|
||||||
@@ -389,14 +357,14 @@ const startTaskStreaming = (taskId: string) => {
|
|||||||
completed: task.completed_files,
|
completed: task.completed_files,
|
||||||
total: task.total_files
|
total: task.total_files
|
||||||
});
|
});
|
||||||
|
|
||||||
currentTask.value = task;
|
currentTask.value = task;
|
||||||
|
|
||||||
// 如果任务完成,清理连接并检查下载状态
|
// 如果任务完成,清理连接并检查下载状态
|
||||||
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
|
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
|
||||||
console.log('任务完成,关闭SSE连接');
|
console.log('任务完成,关闭SSE连接');
|
||||||
stopTaskStreaming();
|
stopTaskStreaming();
|
||||||
|
|
||||||
// 延迟检查下载状态,确保文件写入完成
|
// 延迟检查下载状态,确保文件写入完成
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await checkDownloadStatus(artwork.value!.id);
|
await checkDownloadStatus(artwork.value!.id);
|
||||||
@@ -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 = () => {
|
||||||
@@ -468,23 +426,23 @@ const clearError = () => {
|
|||||||
const fetchArtistArtworks = async () => {
|
const fetchArtistArtworks = async () => {
|
||||||
const artistId = route.query.artistId;
|
const artistId = route.query.artistId;
|
||||||
const artworkType = route.query.artworkType;
|
const artworkType = route.query.artworkType;
|
||||||
|
|
||||||
if (!artistId || !artworkType) return;
|
if (!artistId || !artworkType) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigationLoading.value = true;
|
navigationLoading.value = true;
|
||||||
|
|
||||||
// 获取当前页面信息
|
// 获取当前页面信息
|
||||||
const currentPage = parseInt(route.query.page as string) || 1;
|
const currentPage = parseInt(route.query.page as string) || 1;
|
||||||
const pageSize = 30;
|
const pageSize = 30;
|
||||||
const offset = (currentPage - 1) * pageSize;
|
const offset = (currentPage - 1) * pageSize;
|
||||||
|
|
||||||
const response = await artistService.getArtistArtworks(parseInt(artistId as string), {
|
const response = await artistService.getArtistArtworks(parseInt(artistId as string), {
|
||||||
type: artworkType as 'art' | 'manga' | 'novel',
|
type: artworkType as 'art' | 'manga' | 'novel',
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: pageSize
|
limit: pageSize
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
artistArtworks.value = response.data.artworks;
|
artistArtworks.value = response.data.artworks;
|
||||||
// 找到当前作品在列表中的位置
|
// 找到当前作品在列表中的位置
|
||||||
@@ -545,18 +503,18 @@ const isCtrlPressed = ref(false);
|
|||||||
const handleTagClick = (event: MouseEvent, tagName: string) => {
|
const handleTagClick = (event: MouseEvent, tagName: string) => {
|
||||||
// 阻止默认行为
|
// 阻止默认行为
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
console.log('标签点击事件:', {
|
console.log('标签点击事件:', {
|
||||||
tagName,
|
tagName,
|
||||||
ctrlKey: event.ctrlKey,
|
ctrlKey: event.ctrlKey,
|
||||||
metaKey: event.metaKey,
|
metaKey: event.metaKey,
|
||||||
isCtrlPressed: isCtrlPressed.value
|
isCtrlPressed: isCtrlPressed.value
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果按住Ctrl键,则添加到选中状态(不跳转)
|
// 如果按住Ctrl键,则添加到选中状态(不跳转)
|
||||||
if (event.ctrlKey || event.metaKey || isCtrlPressed.value) {
|
if (event.ctrlKey || event.metaKey || isCtrlPressed.value) {
|
||||||
console.log('检测到Ctrl键,添加到选中状态');
|
console.log('检测到Ctrl键,添加到选中状态');
|
||||||
|
|
||||||
// 切换标签的选中状态
|
// 切换标签的选中状态
|
||||||
const index = selectedTags.value.indexOf(tagName);
|
const index = selectedTags.value.indexOf(tagName);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -566,19 +524,19 @@ const handleTagClick = (event: MouseEvent, tagName: string) => {
|
|||||||
// 如果未选中,则添加到选中列表
|
// 如果未选中,则添加到选中列表
|
||||||
selectedTags.value.push(tagName);
|
selectedTags.value.push(tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('当前选中的标签:', selectedTags.value);
|
console.log('当前选中的标签:', selectedTags.value);
|
||||||
|
|
||||||
// 保存到sessionStorage
|
// 保存到sessionStorage
|
||||||
sessionStorage.setItem('currentSearchTags', JSON.stringify(selectedTags.value));
|
sessionStorage.setItem('currentSearchTags', JSON.stringify(selectedTags.value));
|
||||||
|
|
||||||
// 不跳转,只是更新选中状态
|
// 不跳转,只是更新选中状态
|
||||||
} else {
|
} else {
|
||||||
console.log('普通点击,执行单标签搜索');
|
console.log('普通点击,执行单标签搜索');
|
||||||
// 普通点击,只搜索当前标签,清除之前的多标签选择
|
// 普通点击,只搜索当前标签,清除之前的多标签选择
|
||||||
selectedTags.value = [];
|
selectedTags.value = [];
|
||||||
sessionStorage.removeItem('currentSearchTags');
|
sessionStorage.removeItem('currentSearchTags');
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
path: '/search',
|
path: '/search',
|
||||||
query: {
|
query: {
|
||||||
@@ -599,11 +557,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|||||||
const handleKeyUp = (event: KeyboardEvent) => {
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Control' || event.key === 'Meta') {
|
if (event.key === 'Control' || event.key === 'Meta') {
|
||||||
isCtrlPressed.value = false;
|
isCtrlPressed.value = false;
|
||||||
|
|
||||||
// 当松开Ctrl键时,如果有选中的标签,则跳转到搜索页面
|
// 当松开Ctrl键时,如果有选中的标签,则跳转到搜索页面
|
||||||
if (selectedTags.value.length > 0) {
|
if (selectedTags.value.length > 0) {
|
||||||
console.log('松开Ctrl键,跳转到搜索页面,标签:', selectedTags.value);
|
console.log('松开Ctrl键,跳转到搜索页面,标签:', selectedTags.value);
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
path: '/search',
|
path: '/search',
|
||||||
query: {
|
query: {
|
||||||
@@ -611,7 +569,7 @@ const handleKeyUp = (event: KeyboardEvent) => {
|
|||||||
tags: selectedTags.value
|
tags: selectedTags.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空选中状态
|
// 清空选中状态
|
||||||
selectedTags.value = [];
|
selectedTags.value = [];
|
||||||
sessionStorage.removeItem('currentSearchTags');
|
sessionStorage.removeItem('currentSearchTags');
|
||||||
@@ -624,10 +582,10 @@ watch(() => route.params.id, () => {
|
|||||||
// 清理之前的任务状态
|
// 清理之前的任务状态
|
||||||
currentTask.value = null;
|
currentTask.value = null;
|
||||||
stopTaskStreaming();
|
stopTaskStreaming();
|
||||||
|
|
||||||
// 重新获取作品详情
|
// 重新获取作品详情
|
||||||
fetchArtworkDetail();
|
fetchArtworkDetail();
|
||||||
|
|
||||||
// 如果是从作者页面来的,重新获取导航数据
|
// 如果是从作者页面来的,重新获取导航数据
|
||||||
if (showNavigation.value) {
|
if (showNavigation.value) {
|
||||||
fetchArtistArtworks();
|
fetchArtistArtworks();
|
||||||
@@ -637,7 +595,7 @@ watch(() => route.params.id, () => {
|
|||||||
// 键盘快捷键支持
|
// 键盘快捷键支持
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (!showNavigation.value) return;
|
if (!showNavigation.value) return;
|
||||||
|
|
||||||
if (event.key === 'ArrowLeft' && previousArtwork.value) {
|
if (event.key === 'ArrowLeft' && previousArtwork.value) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigateToPrevious();
|
navigateToPrevious();
|
||||||
@@ -655,7 +613,7 @@ onMounted(() => {
|
|||||||
if (showNavigation.value) {
|
if (showNavigation.value) {
|
||||||
fetchArtistArtworks();
|
fetchArtistArtworks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加键盘事件监听
|
// 添加键盘事件监听
|
||||||
document.addEventListener('keydown', handleKeydown);
|
document.addEventListener('keydown', handleKeydown);
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
@@ -1089,43 +1047,43 @@ onUnmounted(() => {
|
|||||||
.container {
|
.container {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-actions {
|
.artwork-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-stats {
|
.artwork-stats {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnails {
|
.thumbnails {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-navigation {
|
.artwork-navigation {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-back {
|
.nav-back {
|
||||||
order: -1;
|
order: -1;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-prev,
|
.nav-prev,
|
||||||
.nav-next {
|
.nav-next {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user