diff --git a/.gitignore b/.gitignore index d751d31..dbe0134 100644 --- a/.gitignore +++ b/.gitignore @@ -67,5 +67,8 @@ typings/ downloads/ data/ +# 用户配置文件(每个用户不同) +backend/config/user-config.json + # 自己的启动文件 start_me.bat diff --git a/backend/README.md b/backend/README.md index 67e95f8..04036a7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -103,6 +103,11 @@ backend/ - 参数: `path` (文件路径) - `GET /api/repository/directory` - 获取目录结构 - 参数: `path` (目录路径) +- `GET /api/repository/check-downloaded/:artworkId` - 检查作品是否已下载 +- `GET /api/repository/check-directory` - 检查目录是否存在 + - 参数: `path` (目录路径) +- `POST /api/repository/migrate-old-to-new` - 从旧目录迁移到新目录 + - 参数: `oldDir` (旧目录路径), `newDir` (新目录路径) ## 🔧 配置说明 diff --git a/backend/config/README.md b/backend/config/README.md new file mode 100644 index 0000000..b0ac3ea --- /dev/null +++ b/backend/config/README.md @@ -0,0 +1,64 @@ +# 用户配置系统 + +## 概述 + +本系统使用自动生成的用户配置文件来存储用户的下载目录路径和其他设置。配置文件会在后端初始化时自动创建,无需手动配置。 + +## 配置文件位置 + +- **配置文件**: `backend/config/user-config.json` +- **默认下载目录**: `./downloads` (相对于项目根目录) + +## 配置项说明 + +```json +{ + "downloadDir": "./downloads", // 下载目录路径 + "fileStructure": "artist/artwork", // 文件结构模式 + "namingPattern": "{artist_name}/{artwork_id}_{title}", // 命名模式 + "maxFileSize": 0, // 最大文件大小 (0=无限制) + "allowedExtensions": [".jpg", ".png", ".gif", ".webp"], // 允许的文件扩展名 + "autoMigration": false, // 是否启用自动迁移 + "migrationRules": [], // 迁移规则 + "lastUpdated": "2024-01-01T00:00:00.000Z" // 最后更新时间 +} +``` + +## 自动初始化 + +1. 当后端启动时,系统会自动检查配置文件是否存在 +2. 如果配置文件不存在,会自动创建默认配置文件 +3. 默认下载目录为项目根目录下的 `downloads` 文件夹 + +## 配置管理 + +### 前端界面 +- 在仓库管理页面可以修改配置 +- 支持重置为默认配置 +- 支持自动迁移功能 + +### API接口 +- `GET /api/repository/config` - 获取配置 +- `PUT /api/repository/config` - 更新配置 +- `POST /api/repository/config/reset` - 重置配置 + +## 注意事项 + +1. **配置文件已加入 .gitignore**:每个用户的配置文件不同,不会被提交到版本控制 +2. **路径支持**: + - 相对路径:`./downloads` (相对于项目根目录) + - 绝对路径:`D:\downloads` 或 `/home/user/downloads` +3. **迁移功能**:支持将旧目录中的文件移动到新配置的下载目录 + +## 故障排除 + +### 配置文件损坏 +如果配置文件损坏或无法读取,系统会自动使用默认配置并重新创建配置文件。 + +### 权限问题 +确保后端有权限读取和写入配置文件目录。 + +### 路径问题 +- 确保路径格式正确 +- Windows路径使用反斜杠:`D:\downloads` +- Unix路径使用正斜杠:`/home/user/downloads` \ No newline at end of file diff --git a/backend/config/config-manager.js b/backend/config/config-manager.js new file mode 100644 index 0000000..6810672 --- /dev/null +++ b/backend/config/config-manager.js @@ -0,0 +1,150 @@ +const fs = require('fs').promises +const path = require('path') + +/** + * 配置管理器 + * 负责自动生成和管理用户配置文件 + */ +class ConfigManager { + constructor() { + this.configDir = path.join(__dirname, 'user-config.json') + this.defaultConfig = { + downloadDir: "./downloads", + fileStructure: "artist/artwork", + namingPattern: "{artist_name}/{artwork_id}_{title}", + maxFileSize: 0, + allowedExtensions: [".jpg", ".png", ".gif", ".webp"], + autoMigration: false, + migrationRules: [], + lastUpdated: new Date().toISOString() + } + } + + /** + * 初始化配置文件 + * 如果配置文件不存在,则创建默认配置 + */ + async initialize() { + try { + // 检查配置文件是否存在 + await fs.access(this.configDir) + console.log('用户配置文件已存在') + } catch (error) { + // 配置文件不存在,创建默认配置 + console.log('创建默认用户配置文件...') + await this.createDefaultConfig() + } + } + + /** + * 创建默认配置文件 + */ + async createDefaultConfig() { + try { + // 确保配置目录存在 + const configDirPath = path.dirname(this.configDir) + await fs.mkdir(configDirPath, { recursive: true }) + + // 写入默认配置 + await fs.writeFile( + this.configDir, + JSON.stringify(this.defaultConfig, null, 2), + 'utf8' + ) + + console.log('默认配置文件创建成功:', this.configDir) + } catch (error) { + console.error('创建默认配置文件失败:', error) + throw error + } + } + + /** + * 读取配置文件 + */ + async readConfig() { + try { + const configData = await fs.readFile(this.configDir, 'utf8') + return JSON.parse(configData) + } catch (error) { + console.error('读取配置文件失败:', error) + // 如果读取失败,返回默认配置 + return { ...this.defaultConfig } + } + } + + /** + * 保存配置文件 + */ + async saveConfig(config) { + try { + // 添加更新时间 + const configToSave = { + ...config, + lastUpdated: new Date().toISOString() + } + + await fs.writeFile( + this.configDir, + JSON.stringify(configToSave, null, 2), + 'utf8' + ) + + console.log('配置文件保存成功') + return true + } catch (error) { + console.error('保存配置文件失败:', error) + throw error + } + } + + /** + * 更新配置 + */ + async updateConfig(updates) { + try { + const currentConfig = await this.readConfig() + const newConfig = { ...currentConfig, ...updates } + await this.saveConfig(newConfig) + return newConfig + } catch (error) { + console.error('更新配置失败:', error) + throw error + } + } + + /** + * 重置为默认配置 + */ + async resetToDefault() { + try { + await this.saveConfig(this.defaultConfig) + console.log('配置已重置为默认值') + return this.defaultConfig + } catch (error) { + console.error('重置配置失败:', error) + throw error + } + } + + /** + * 获取配置文件路径 + */ + getConfigPath() { + return this.configDir + } + + /** + * 检查配置文件是否存在 + */ + async configExists() { + try { + await fs.access(this.configDir) + return true + } catch (error) { + return false + } + } +} + +module.exports = ConfigManager \ No newline at end of file diff --git a/backend/routes/repository.js b/backend/routes/repository.js index f6b982d..c66b60f 100644 --- a/backend/routes/repository.js +++ b/backend/routes/repository.js @@ -37,6 +37,16 @@ router.put('/config', async (req, res) => { } }) +// 重置仓库配置为默认值 +router.post('/config/reset', async (req, res) => { + try { + const result = await repositoryService.resetConfig() + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + // 获取仓库统计信息 router.get('/stats', async (req, res) => { try { @@ -139,10 +149,11 @@ router.get('/preview', async (req, res) => { return res.status(400).json(ResponseUtil.error('文件路径不能为空')) } - const fullPath = path.join(repositoryService.baseDir, filePath) + const currentBaseDir = repositoryService.getCurrentBaseDir() + const fullPath = path.join(currentBaseDir, filePath) // 安全检查:确保文件在仓库目录内 - const relativePath = path.relative(repositoryService.baseDir, fullPath) + const relativePath = path.relative(currentBaseDir, fullPath) if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return res.status(403).json(ResponseUtil.error('访问被拒绝')) @@ -180,10 +191,11 @@ router.get('/file-info', async (req, res) => { return res.status(400).json(ResponseUtil.error('文件路径不能为空')) } - const fullPath = path.join(repositoryService.baseDir, filePath) + const currentBaseDir = repositoryService.getCurrentBaseDir() + const fullPath = path.join(currentBaseDir, filePath) // 安全检查 - const relativePath = path.relative(repositoryService.baseDir, fullPath) + const relativePath = path.relative(currentBaseDir, fullPath) if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return res.status(403).json(ResponseUtil.error('访问被拒绝')) } @@ -206,14 +218,18 @@ router.get('/file-info', async (req, res) => { } }) -// 获取目录结构 +/** + * 获取目录结构 + * GET /api/repository/directory + */ router.get('/directory', async (req, res) => { try { const { path: dirPath = '' } = req.query - const fullPath = path.join(repositoryService.baseDir, dirPath) + const currentBaseDir = repositoryService.getCurrentBaseDir() + const fullPath = path.join(currentBaseDir, dirPath) // 安全检查 - const relativePath = path.relative(repositoryService.baseDir, fullPath) + const relativePath = path.relative(currentBaseDir, fullPath) if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return res.status(403).json(ResponseUtil.error('访问被拒绝')) } @@ -262,7 +278,72 @@ router.get('/directory', async (req, res) => { } }) -// 获取内容类型 +/** + * 检查作品是否已下载 + * GET /api/repository/check-downloaded/:artworkId + */ +router.get('/check-downloaded/:artworkId', async (req, res) => { + try { + const { artworkId } = req.params + + if (!artworkId || isNaN(parseInt(artworkId))) { + return res.status(400).json(ResponseUtil.error('无效的作品ID')) + } + + const isDownloaded = await repositoryService.isArtworkDownloaded(parseInt(artworkId)) + + res.json(ResponseUtil.success({ + artwork_id: parseInt(artworkId), + is_downloaded: isDownloaded + })) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +/** + * 检查目录是否存在 + * GET /api/repository/check-directory + */ +router.get('/check-directory', async (req, res) => { + try { + const { path: dirPath } = req.query + + if (!dirPath) { + return res.status(400).json(ResponseUtil.error('目录路径不能为空')) + } + + const exists = await repositoryService.checkDirectoryExists(dirPath) + + res.json(ResponseUtil.success({ + path: dirPath, + exists: exists + })) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +/** + * 从旧目录迁移到新目录 + * POST /api/repository/migrate-old-to-new + */ +router.post('/migrate-old-to-new', async (req, res) => { + try { + const { oldDir, newDir } = req.body + + if (!oldDir || !newDir) { + return res.status(400).json(ResponseUtil.error('旧目录和新目录路径都不能为空')) + } + + const result = await repositoryService.migrateFromOldToNew(oldDir, newDir) + res.json(ResponseUtil.success(result)) + } catch (error) { + res.status(500).json(ResponseUtil.error(error.message)) + } +}) + +// 获取文件信息 function getContentType(extension) { const contentTypes = { '.jpg': 'image/jpeg', diff --git a/backend/services/download.js b/backend/services/download.js index afee057..ff723c6 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -4,13 +4,14 @@ const path = require('path'); const { v4: uuidv4 } = require('uuid'); const ArtworkService = require('./artwork'); const ArtistService = require('./artist'); +const ConfigManager = require('../config/config-manager'); class DownloadService { constructor(auth) { this.auth = auth; this.artworkService = new ArtworkService(auth); this.artistService = new ArtistService(auth); - this.downloadPath = path.join(__dirname, '../../downloads'); + this.configManager = new ConfigManager(); this.dataPath = path.join(__dirname, '../../data'); this.tasksFile = path.join(this.dataPath, 'download_tasks.json'); this.historyFile = path.join(this.dataPath, 'download_history.json'); @@ -20,13 +21,33 @@ class DownloadService { this.initialized = false; } + /** + * 获取当前下载路径 + */ + async getDownloadPath() { + try { + const config = await this.configManager.readConfig(); + const downloadDir = config.downloadDir || './downloads'; + + // 如果是相对路径,转换为绝对路径 + return path.isAbsolute(downloadDir) + ? downloadDir + : path.resolve(process.cwd(), downloadDir); + } catch (error) { + console.error('获取下载路径失败:', error); + // 返回默认路径 + return path.resolve(process.cwd(), 'downloads'); + } + } + /** * 初始化服务 */ async init() { try { // 确保目录存在 - await fs.ensureDir(this.downloadPath); + const downloadPath = await this.getDownloadPath(); + await fs.ensureDir(downloadPath); await fs.ensureDir(this.dataPath); // 加载历史记录 @@ -36,7 +57,7 @@ class DownloadService { await this.loadTasks(); this.initialized = true; - console.log('下载服务初始化完成'); + console.log('下载服务初始化完成,下载路径:', downloadPath); } catch (error) { console.error('下载服务初始化失败:', error); this.initialized = false; @@ -152,10 +173,11 @@ class DownloadService { async getDownloadedFiles() { try { const files = []; - const artists = await fs.readdir(this.downloadPath); + const downloadPath = await this.getDownloadPath(); + const artists = await fs.readdir(downloadPath); for (const artist of artists) { - const artistPath = path.join(this.downloadPath, artist); + const artistPath = path.join(downloadPath, artist); const artistStat = await fs.stat(artistPath); if (artistStat.isDirectory()) { @@ -198,20 +220,39 @@ class DownloadService { */ async isArtworkDownloaded(artworkId) { try { - // 从历史记录中查找 - const historyItem = this.history.find(item => - item.artwork_id === artworkId && item.status === 'completed' - ); + const downloadPath = await this.getDownloadPath(); - if (historyItem) { - // 检查文件是否还存在 - const exists = await fs.pathExists(historyItem.download_path); - if (exists) { - const files = await fs.readdir(historyItem.download_path); - const imageFiles = files.filter(file => - /\.(jpg|jpeg|png|gif|webp)$/i.test(file) - ); - return imageFiles.length > 0; + // 扫描下载目录查找作品 + const artists = await fs.readdir(downloadPath); + + for (const artist of artists) { + const artistPath = path.join(downloadPath, artist); + const artistStat = await fs.stat(artistPath); + + if (artistStat.isDirectory()) { + const artworks = await fs.readdir(artistPath); + + for (const artwork of artworks) { + // 检查是否是作品目录(包含数字ID) + const artworkMatch = artwork.match(/^(\d+)_(.+)$/); + if (artworkMatch) { + const foundArtworkId = artworkMatch[1]; + + if (parseInt(foundArtworkId) === parseInt(artworkId)) { + // 找到作品目录,检查是否包含图片文件 + const artworkPath = path.join(artistPath, artwork); + const artworkStat = await fs.stat(artworkPath); + + if (artworkStat.isDirectory()) { + const files = await fs.readdir(artworkPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) + ); + return imageFiles.length > 0; + } + } + } + } } } @@ -228,18 +269,37 @@ class DownloadService { async getDownloadedArtworkIds() { try { const downloadedIds = new Set(); + const downloadPath = await this.getDownloadPath(); - // 从历史记录中获取 - for (const item of this.history) { - if (item.artwork_id && item.status === 'completed') { - const exists = await fs.pathExists(item.download_path); - if (exists) { - const files = await fs.readdir(item.download_path); - const imageFiles = files.filter(file => - /\.(jpg|jpeg|png|gif|webp)$/i.test(file) - ); - if (imageFiles.length > 0) { - downloadedIds.add(item.artwork_id); + // 扫描下载目录获取所有已下载的作品ID + const artists = await fs.readdir(downloadPath); + + for (const artist of artists) { + const artistPath = path.join(downloadPath, artist); + const artistStat = await fs.stat(artistPath); + + if (artistStat.isDirectory()) { + const artworks = await fs.readdir(artistPath); + + for (const artwork of artworks) { + // 检查是否是作品目录(包含数字ID) + const artworkMatch = artwork.match(/^(\d+)_(.+)$/); + if (artworkMatch) { + const artworkId = artworkMatch[1]; + + // 检查作品目录是否包含图片文件 + const artworkPath = path.join(artistPath, artwork); + const artworkStat = await fs.stat(artworkPath); + + if (artworkStat.isDirectory()) { + const files = await fs.readdir(artworkPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) + ); + if (imageFiles.length > 0) { + downloadedIds.add(parseInt(artworkId)); + } + } } } } @@ -279,7 +339,8 @@ class DownloadService { */ async deleteDownloadedFiles(artist, artwork) { try { - const targetPath = path.join(this.downloadPath, artist, artwork); + const downloadPath = await this.getDownloadPath(); + const targetPath = path.join(downloadPath, artist, artwork); if (await fs.pathExists(targetPath)) { await fs.remove(targetPath); @@ -375,7 +436,8 @@ class DownloadService { const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_'); // 创建作品目录 - 使用仓库管理格式 - const artistDir = path.join(this.downloadPath, artistName); + const downloadPath = await this.getDownloadPath(); + const artistDir = path.join(downloadPath, artistName); const artworkDirName = `${artworkId}_${artworkTitle}`; const artworkDir = path.join(artistDir, artworkDirName); await fs.ensureDir(artworkDir); diff --git a/backend/services/repository.js b/backend/services/repository.js index f43d7d8..24a3989 100644 --- a/backend/services/repository.js +++ b/backend/services/repository.js @@ -2,19 +2,41 @@ const fs = require('fs').promises const path = require('path') const { promisify } = require('util') const { exec } = require('child_process') +const ConfigManager = require('../config/config-manager') const execAsync = promisify(exec) class RepositoryService { constructor() { - this.baseDir = process.env.DOWNLOAD_DIR || path.join(process.cwd(), 'downloads') - this.configFile = path.join(this.baseDir, '.repository-config.json') + // 初始化配置管理器 + this.configManager = new ConfigManager() + this.config = null + } + + // 获取当前工作目录(基于配置) + getCurrentBaseDir() { + if (this.config && this.config.downloadDir) { + // 如果是相对路径,转换为绝对路径 + return path.isAbsolute(this.config.downloadDir) + ? this.config.downloadDir + : path.resolve(process.cwd(), this.config.downloadDir) + } + // 默认返回项目根目录下的downloads文件夹 + return path.resolve(process.cwd(), 'downloads') } // 初始化仓库 async initialize() { try { - await fs.mkdir(this.baseDir, { recursive: true }) + // 初始化配置管理器 + await this.configManager.initialize() + + // 加载配置 await this.loadConfig() + + // 确保下载目录存在 + const currentBaseDir = this.getCurrentBaseDir() + await fs.mkdir(currentBaseDir, { recursive: true }) + return { success: true, message: '仓库初始化成功' } } catch (error) { throw new Error(`仓库初始化失败: ${error.message}`) @@ -24,30 +46,21 @@ class RepositoryService { // 加载配置 async loadConfig() { try { - const configData = await fs.readFile(this.configFile, 'utf8') - this.config = JSON.parse(configData) + this.config = await this.configManager.readConfig() } catch (error) { - // 如果配置文件不存在,创建默认配置 - this.config = { - downloadDir: this.baseDir, - autoMigration: false, - migrationRules: [], - fileStructure: 'artist/artwork', // artist/artwork, artwork, flat - namingPattern: '{artist_name}/{artwork_id}_{title}', - maxFileSize: 0, // 0表示无限制 - allowedExtensions: ['.jpg', '.png', '.gif', '.webp'] - } - await this.saveConfig() + console.error('加载配置失败:', error) + // 如果加载失败,使用默认配置 + this.config = await this.configManager.readConfig() } } // 保存配置 async saveConfig() { try { - await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2)) + await this.configManager.saveConfig(this.config) } catch (error) { throw new Error(`保存配置失败: ${error.message}`) - } + } } // 获取仓库配置 @@ -63,6 +76,16 @@ class RepositoryService { return { success: true, message: '配置更新成功' } } + // 重置仓库配置为默认值 + async resetConfig() { + try { + this.config = await this.configManager.resetToDefault() + return { success: true, message: '配置已重置为默认值' } + } catch (error) { + throw new Error(`重置配置失败: ${error.message}`) + } + } + // 获取仓库统计信息 async getStats() { try { @@ -89,8 +112,11 @@ class RepositoryService { // 确保配置已加载 await this.loadConfig() + // 使用当前配置的目录 + const currentBaseDir = this.getCurrentBaseDir() + // 扫描作者目录 - const artistEntries = await fs.readdir(this.baseDir, { withFileTypes: true }) + const artistEntries = await fs.readdir(currentBaseDir, { withFileTypes: true }) for (const artistEntry of artistEntries) { if (!artistEntry.isDirectory()) continue @@ -101,7 +127,7 @@ class RepositoryService { } const artistName = artistEntry.name - const artistPath = path.join(this.baseDir, artistName) + const artistPath = path.join(currentBaseDir, artistName) // 扫描作者下的作品目录 const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) @@ -164,9 +190,10 @@ class RepositoryService { if (this.config.allowedExtensions.includes(ext)) { const stats = await fs.stat(filePath) + const currentBaseDir = this.getCurrentBaseDir() files.push({ name: entry.name, - path: path.relative(this.baseDir, filePath), + path: path.relative(currentBaseDir, filePath), size: stats.size, extension: ext, modifiedAt: stats.mtime @@ -194,7 +221,8 @@ class RepositoryService { // 获取磁盘使用情况 async getDiskUsage() { try { - const stats = await fs.statfs(this.baseDir) + const currentBaseDir = this.getCurrentBaseDir() + const stats = await fs.statfs(currentBaseDir) const total = stats.blocks * stats.bsize const free = stats.bavail * stats.bsize const used = total - free @@ -239,6 +267,65 @@ class RepositoryService { } } + // 检查作品是否已下载 + async isArtworkDownloaded(artworkId) { + try { + // 确保配置已加载 + await this.loadConfig() + + // 使用当前配置的目录 + const currentBaseDir = this.getCurrentBaseDir() + + // 扫描所有作者目录 + const artistEntries = await fs.readdir(currentBaseDir, { withFileTypes: true }) + + for (const artistEntry of artistEntries) { + if (!artistEntry.isDirectory()) continue + + // 跳过配置文件和隐藏文件 + if (artistEntry.name.startsWith('.') || artistEntry.name === '.repository-config.json') { + continue + } + + const artistPath = path.join(currentBaseDir, artistEntry.name) + + // 扫描作者下的作品目录 + const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) + + for (const artworkEntry of artworkEntries) { + if (!artworkEntry.isDirectory()) continue + + // 检查是否是目标作品目录(包含数字ID) + const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) + if (artworkMatch && artworkMatch[1] === artworkId.toString()) { + // 检查作品目录中是否有图片文件 + const artworkPath = path.join(artistPath, artworkEntry.name) + const files = await this.scanArtworkFiles(artworkPath) + return files.length > 0 + } + } + } + + return false + } catch (error) { + console.error('检查作品下载状态失败:', error) + return false + } + } + + // 检查目录是否存在 + async checkDirectoryExists(dirPath) { + try { + // 如果是相对路径,转换为绝对路径 + const fullPath = path.isAbsolute(dirPath) ? dirPath : path.resolve(process.cwd(), dirPath) + + const stats = await fs.stat(fullPath) + return stats.isDirectory() + } catch (error) { + return false + } + } + // 搜索作品 async searchArtworks(query, offset = 0, limit = 20) { try { @@ -295,65 +382,138 @@ class RepositoryService { // 自动迁移旧项目 async migrateOldProjects(sourceDir) { try { - const migrationLog = [] + // 确保配置已加载 + await this.loadConfig() + + const currentBaseDir = this.getCurrentBaseDir() + const result = { + success: true, + message: '迁移完成', + log: [], + totalMigrated: 0 + } + + // 确保目标目录存在 + await fs.mkdir(currentBaseDir, { recursive: true }) // 扫描源目录 - const scanSource = async (dirPath, relativePath = '') => { - const entries = await fs.readdir(dirPath, { withFileTypes: true }) + const sourceEntries = await fs.readdir(sourceDir, { withFileTypes: true }) + + for (const entry of sourceEntries) { + if (!entry.isDirectory()) continue - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name) - const newRelativePath = path.join(relativePath, entry.name) + const oldDirPath = path.join(sourceDir, entry.name) + const newDirPath = path.join(currentBaseDir, entry.name) + + // 检查是否已存在 + try { + await fs.access(newDirPath) + result.log.push({ + id: entry.name, + title: entry.name, + status: 'skipped', + reason: '目录已存在' + }) + continue + } catch (error) { + // 目录不存在,可以迁移 + } + + try { + // 直接移动整个目录 + await fs.rename(oldDirPath, newDirPath) - if (entry.isDirectory()) { - // 检查是否是作品目录 - const artworkMatch = entry.name.match(/^(\d+)_(.+)$/) - if (artworkMatch) { - const artworkId = artworkMatch[1] - const title = artworkMatch[2] - - // 检查是否已存在 - const existingArtwork = await this.findArtworkById(artworkId) - if (!existingArtwork) { - // 迁移作品 - const targetPath = path.join(this.baseDir, newRelativePath) - await fs.mkdir(path.dirname(targetPath), { recursive: true }) - await this.copyDirectory(fullPath, targetPath) - - migrationLog.push({ - type: 'artwork', - id: artworkId, - title: title, - source: fullPath, - target: targetPath, - status: 'success' - }) - } else { - migrationLog.push({ - type: 'artwork', - id: artworkId, - title: title, - source: fullPath, - status: 'skipped', - reason: '已存在' - }) - } - } else { - // 递归扫描子目录 - await scanSource(fullPath, newRelativePath) - } - } + result.log.push({ + id: entry.name, + title: entry.name, + status: 'success' + }) + result.totalMigrated++ + + } catch (error) { + result.log.push({ + id: entry.name, + title: entry.name, + status: 'error', + reason: error.message + }) } } - await scanSource(sourceDir) - - return { + return result + } catch (error) { + throw new Error(`迁移失败: ${error.message}`) + } + } + + // 从旧目录迁移到新目录 + async migrateFromOldToNew(oldDir, newDir) { + try { + const result = { success: true, message: '迁移完成', - log: migrationLog, - totalMigrated: migrationLog.filter(item => item.status === 'success').length + log: [], + totalMigrated: 0 } + + // 检查旧目录是否存在 + try { + await fs.access(oldDir) + } catch (error) { + return { + ...result, + message: '旧目录不存在,无需迁移' + } + } + + // 确保新目录存在 + await fs.mkdir(newDir, { recursive: true }) + + // 扫描旧目录 + const oldEntries = await fs.readdir(oldDir, { withFileTypes: true }) + + for (const entry of oldEntries) { + if (!entry.isDirectory()) continue + + const oldEntryPath = path.join(oldDir, entry.name) + const newEntryPath = path.join(newDir, entry.name) + + // 检查是否已存在 + try { + await fs.access(newEntryPath) + result.log.push({ + id: entry.name, + title: entry.name, + status: 'skipped', + reason: '目录已存在' + }) + continue + } catch (error) { + // 目录不存在,可以迁移 + } + + try { + // 直接移动整个目录 + await fs.rename(oldEntryPath, newEntryPath) + + result.log.push({ + id: entry.name, + title: entry.name, + status: 'success' + }) + result.totalMigrated++ + + } catch (error) { + result.log.push({ + id: entry.name, + title: entry.name, + status: 'error', + reason: error.message + }) + } + } + + return result } catch (error) { throw new Error(`迁移失败: ${error.message}`) } diff --git a/ui/src/stores/repository.ts b/ui/src/stores/repository.ts index f496653..ea6d18b 100644 --- a/ui/src/stores/repository.ts +++ b/ui/src/stores/repository.ts @@ -115,6 +115,16 @@ export const useRepositoryStore = defineStore('repository', () => { return result } + // 重置配置 + const resetConfig = async (): Promise => { + const result = await apiCall('/config/reset', { + method: 'POST', + }) + // 重新加载配置 + config.value = await getConfig() + return result + } + // 获取统计信息 const getStats = async (): Promise => { const result = await apiCall('/stats') @@ -165,6 +175,24 @@ export const useRepositoryStore = defineStore('repository', () => { return await apiCall(`/directory?path=${encodeURIComponent(dirPath)}`) } + // 检查作品是否已下载 + const checkArtworkDownloaded = async (artworkId: number) => { + return await apiCall(`/check-downloaded/${artworkId}`) + } + + // 检查目录是否存在 + const checkDirectoryExists = async (dirPath: string) => { + return await apiCall(`/check-directory?path=${encodeURIComponent(dirPath)}`) + } + + // 从旧目录迁移到新目录 + const migrateFromOldToNew = async (oldDir: string, newDir: string) => { + return await apiCall('/migrate-old-to-new', { + method: 'POST', + body: JSON.stringify({ oldDir, newDir }), + }) + } + return { // 状态 config, @@ -174,6 +202,7 @@ export const useRepositoryStore = defineStore('repository', () => { initialize, getConfig, updateConfig, + resetConfig, getStats, getArtists, getArtworksByArtist, @@ -183,5 +212,8 @@ export const useRepositoryStore = defineStore('repository', () => { migrateOldProjects, getFileInfo, getDirectory, + checkArtworkDownloaded, + checkDirectoryExists, + migrateFromOldToNew, } }) \ No newline at end of file diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index b066bd3..8339e0e 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -49,12 +49,21 @@

{{ artwork.title }}

+ +
+
+ + + + 已下载到本地 +
+
@@ -162,6 +171,7 @@ import { useAuthStore } from '@/stores/auth'; import artworkService from '@/services/artwork'; import artistService from '@/services/artist'; import downloadService from '@/services/download'; +import { useRepositoryStore } from '@/stores/repository'; import type { Artwork } from '@/types'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import ErrorMessage from '@/components/common/ErrorMessage.vue'; @@ -169,6 +179,7 @@ import ErrorMessage from '@/components/common/ErrorMessage.vue'; const route = useRoute(); const router = useRouter(); const authStore = useAuthStore(); +const repositoryStore = useRepositoryStore(); // 状态 const artwork = ref(null); @@ -178,6 +189,8 @@ const currentPage = ref(0); const imageLoaded = ref(false); const imageError = ref(false); const downloading = ref(false); +const isDownloaded = ref(false); +const checkingDownloadStatus = ref(false); // 导航相关状态 const artistArtworks = ref([]); @@ -236,6 +249,8 @@ const fetchArtworkDetail = async () => { if (response.success && response.data) { artwork.value = response.data; + // 检查下载状态 + await checkDownloadStatus(artworkId); } else { throw new Error(response.error || '获取作品详情失败'); } @@ -247,6 +262,27 @@ const fetchArtworkDetail = async () => { } }; +// 检查下载状态 +const checkDownloadStatus = async (artworkId: number) => { + try { + checkingDownloadStatus.value = true; + const response = await repositoryStore.checkArtworkDownloaded(artworkId); + + console.log('下载状态检查响应:', response); + + // repository store的apiCall返回的是data.data,所以response直接是数据对象 + if (response && typeof response === 'object') { + isDownloaded.value = response.is_downloaded || false; + console.log('作品下载状态:', isDownloaded.value); + } + } catch (err) { + console.error('检查下载状态失败:', err); + isDownloaded.value = false; + } finally { + checkingDownloadStatus.value = false; + } +}; + // 下载作品 const handleDownload = async () => { if (!artwork.value) return; @@ -258,6 +294,10 @@ const handleDownload = async () => { if (response.success) { // 可以显示下载成功提示 console.log('下载任务已创建:', response.data); + // 下载完成后重新检查下载状态 + setTimeout(() => { + checkDownloadStatus(artwork.value!.id); + }, 2000); // 等待2秒让下载完成 } else { throw new Error(response.error || '下载失败'); } @@ -723,6 +763,32 @@ onUnmounted(() => { justify-content: flex-end; } +.download-status { + margin-top: 1rem; + padding: 0.75rem 1rem; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: #0369a1; + font-size: 0.875rem; + font-weight: 500; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status-icon { + width: 1.25rem; + height: 1.25rem; + color: #059669; +} + @media (max-width: 1024px) { .artwork-content { grid-template-columns: 1fr; diff --git a/ui/src/views/RepositoryView.vue b/ui/src/views/RepositoryView.vue index 99b022f..d2c0836 100644 --- a/ui/src/views/RepositoryView.vue +++ b/ui/src/views/RepositoryView.vue @@ -55,13 +55,6 @@ > 文件浏览 - @@ -75,15 +68,73 @@ + + + + 路径示例:
+ • 相对路径:./downloads(相对于项目根目录)
+ • 绝对路径:D:\downloads/home/user/downloads
+ • 当前目录:../ +
+ + + +
+ + + 启用后,保存配置时会自动将旧下载目录中的文件移动到新目录 + +
+ + +
+
+

正在迁移文件...

+ {{ migrationProgress }} +
+
+
+
+
+ + +
+

迁移完成

+
+

✅ 成功迁移: {{ migrationResult.totalMigrated }} 个作品

+

⏭️ 跳过: {{ migrationResult.log.filter((item: any) => item.status === 'skipped').length }} 个作品

+
+
+
详细日志
+
+ {{ (item as any).status === 'success' ? '✅' : '⏭️' }} + {{ (item as any).title }} (ID: {{ (item as any).id }}) + {{ (item as any).reason }} +
+
+ 还有 {{ migrationResult.log.length - 10 }} 个文件... +
- 默认路径: ./downloads
@@ -125,6 +176,9 @@ +
@@ -236,14 +290,22 @@ + + + 迁移说明:
+ • 选择要迁移的源目录,系统会将整个目录结构移动到目标位置
+ • 如果目标位置已存在同名目录,将跳过迁移
+ • 迁移完成后,源文件会被移动到新位置(移动操作) +