文件迁移配置优化,在浏览界面增加识别已经下载过的作品

This commit is contained in:
2025-08-21 15:58:43 +08:00
parent 84b712246d
commit c6783febc1
10 changed files with 1031 additions and 164 deletions
+3
View File
@@ -67,5 +67,8 @@ typings/
downloads/ downloads/
data/ data/
# 用户配置文件(每个用户不同)
backend/config/user-config.json
# 自己的启动文件 # 自己的启动文件
start_me.bat start_me.bat
+5
View File
@@ -103,6 +103,11 @@ backend/
- 参数: `path` (文件路径) - 参数: `path` (文件路径)
- `GET /api/repository/directory` - 获取目录结构 - `GET /api/repository/directory` - 获取目录结构
- 参数: `path` (目录路径) - 参数: `path` (目录路径)
- `GET /api/repository/check-downloaded/:artworkId` - 检查作品是否已下载
- `GET /api/repository/check-directory` - 检查目录是否存在
- 参数: `path` (目录路径)
- `POST /api/repository/migrate-old-to-new` - 从旧目录迁移到新目录
- 参数: `oldDir` (旧目录路径), `newDir` (新目录路径)
## 🔧 配置说明 ## 🔧 配置说明
+64
View File
@@ -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`
+150
View File
@@ -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
+89 -8
View File
@@ -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) => { router.get('/stats', async (req, res) => {
try { try {
@@ -139,10 +149,11 @@ router.get('/preview', async (req, res) => {
return res.status(400).json(ResponseUtil.error('文件路径不能为空')) 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)) { if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return res.status(403).json(ResponseUtil.error('访问被拒绝')) 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('文件路径不能为空')) 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)) { if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return res.status(403).json(ResponseUtil.error('访问被拒绝')) 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) => { router.get('/directory', async (req, res) => {
try { try {
const { path: dirPath = '' } = req.query 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)) { if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return res.status(403).json(ResponseUtil.error('访问被拒绝')) 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) { function getContentType(extension) {
const contentTypes = { const contentTypes = {
'.jpg': 'image/jpeg', '.jpg': 'image/jpeg',
+93 -31
View File
@@ -4,13 +4,14 @@ const path = require('path');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const ArtworkService = require('./artwork'); const ArtworkService = require('./artwork');
const ArtistService = require('./artist'); const ArtistService = require('./artist');
const ConfigManager = require('../config/config-manager');
class DownloadService { class DownloadService {
constructor(auth) { constructor(auth) {
this.auth = auth; this.auth = auth;
this.artworkService = new ArtworkService(auth); this.artworkService = new ArtworkService(auth);
this.artistService = new ArtistService(auth); this.artistService = new ArtistService(auth);
this.downloadPath = path.join(__dirname, '../../downloads'); this.configManager = new ConfigManager();
this.dataPath = path.join(__dirname, '../../data'); this.dataPath = path.join(__dirname, '../../data');
this.tasksFile = path.join(this.dataPath, 'download_tasks.json'); this.tasksFile = path.join(this.dataPath, 'download_tasks.json');
this.historyFile = path.join(this.dataPath, 'download_history.json'); this.historyFile = path.join(this.dataPath, 'download_history.json');
@@ -20,13 +21,33 @@ class DownloadService {
this.initialized = false; 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() { async init() {
try { try {
// 确保目录存在 // 确保目录存在
await fs.ensureDir(this.downloadPath); const downloadPath = await this.getDownloadPath();
await fs.ensureDir(downloadPath);
await fs.ensureDir(this.dataPath); await fs.ensureDir(this.dataPath);
// 加载历史记录 // 加载历史记录
@@ -36,7 +57,7 @@ class DownloadService {
await this.loadTasks(); await this.loadTasks();
this.initialized = true; this.initialized = true;
console.log('下载服务初始化完成'); console.log('下载服务初始化完成,下载路径:', downloadPath);
} catch (error) { } catch (error) {
console.error('下载服务初始化失败:', error); console.error('下载服务初始化失败:', error);
this.initialized = false; this.initialized = false;
@@ -152,10 +173,11 @@ class DownloadService {
async getDownloadedFiles() { async getDownloadedFiles() {
try { try {
const files = []; 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) { for (const artist of artists) {
const artistPath = path.join(this.downloadPath, artist); const artistPath = path.join(downloadPath, artist);
const artistStat = await fs.stat(artistPath); const artistStat = await fs.stat(artistPath);
if (artistStat.isDirectory()) { if (artistStat.isDirectory()) {
@@ -198,20 +220,39 @@ class DownloadService {
*/ */
async isArtworkDownloaded(artworkId) { async isArtworkDownloaded(artworkId) {
try { try {
// 从历史记录中查找 const downloadPath = await this.getDownloadPath();
const historyItem = this.history.find(item =>
item.artwork_id === artworkId && item.status === 'completed'
);
if (historyItem) { // 扫描下载目录查找作品
// 检查文件是否还存在 const artists = await fs.readdir(downloadPath);
const exists = await fs.pathExists(historyItem.download_path);
if (exists) { for (const artist of artists) {
const files = await fs.readdir(historyItem.download_path); const artistPath = path.join(downloadPath, artist);
const imageFiles = files.filter(file => const artistStat = await fs.stat(artistPath);
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
); if (artistStat.isDirectory()) {
return imageFiles.length > 0; 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() { async getDownloadedArtworkIds() {
try { try {
const downloadedIds = new Set(); const downloadedIds = new Set();
const downloadPath = await this.getDownloadPath();
// 从历史记录中获取 // 扫描下载目录获取所有已下载的作品ID
for (const item of this.history) { const artists = await fs.readdir(downloadPath);
if (item.artwork_id && item.status === 'completed') {
const exists = await fs.pathExists(item.download_path); for (const artist of artists) {
if (exists) { const artistPath = path.join(downloadPath, artist);
const files = await fs.readdir(item.download_path); const artistStat = await fs.stat(artistPath);
const imageFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file) if (artistStat.isDirectory()) {
); const artworks = await fs.readdir(artistPath);
if (imageFiles.length > 0) {
downloadedIds.add(item.artwork_id); 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) { async deleteDownloadedFiles(artist, artwork) {
try { 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)) { if (await fs.pathExists(targetPath)) {
await fs.remove(targetPath); await fs.remove(targetPath);
@@ -375,7 +436,8 @@ class DownloadService {
const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_'); 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 artworkDirName = `${artworkId}_${artworkTitle}`;
const artworkDir = path.join(artistDir, artworkDirName); const artworkDir = path.join(artistDir, artworkDirName);
await fs.ensureDir(artworkDir); await fs.ensureDir(artworkDir);
+231 -71
View File
@@ -2,19 +2,41 @@ const fs = require('fs').promises
const path = require('path') const path = require('path')
const { promisify } = require('util') const { promisify } = require('util')
const { exec } = require('child_process') const { exec } = require('child_process')
const ConfigManager = require('../config/config-manager')
const execAsync = promisify(exec) const execAsync = promisify(exec)
class RepositoryService { class RepositoryService {
constructor() { 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() { async initialize() {
try { try {
await fs.mkdir(this.baseDir, { recursive: true }) // 初始化配置管理器
await this.configManager.initialize()
// 加载配置
await this.loadConfig() await this.loadConfig()
// 确保下载目录存在
const currentBaseDir = this.getCurrentBaseDir()
await fs.mkdir(currentBaseDir, { recursive: true })
return { success: true, message: '仓库初始化成功' } return { success: true, message: '仓库初始化成功' }
} catch (error) { } catch (error) {
throw new Error(`仓库初始化失败: ${error.message}`) throw new Error(`仓库初始化失败: ${error.message}`)
@@ -24,30 +46,21 @@ class RepositoryService {
// 加载配置 // 加载配置
async loadConfig() { async loadConfig() {
try { try {
const configData = await fs.readFile(this.configFile, 'utf8') this.config = await this.configManager.readConfig()
this.config = JSON.parse(configData)
} catch (error) { } catch (error) {
// 如果配置文件不存在,创建默认配置 console.error('加载配置失败:', error)
this.config = { // 如果加载失败,使用默认配置
downloadDir: this.baseDir, this.config = await this.configManager.readConfig()
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()
} }
} }
// 保存配置 // 保存配置
async saveConfig() { async saveConfig() {
try { try {
await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2)) await this.configManager.saveConfig(this.config)
} catch (error) { } catch (error) {
throw new Error(`保存配置失败: ${error.message}`) throw new Error(`保存配置失败: ${error.message}`)
} }
} }
// 获取仓库配置 // 获取仓库配置
@@ -63,6 +76,16 @@ class RepositoryService {
return { success: true, message: '配置更新成功' } 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() { async getStats() {
try { try {
@@ -89,8 +112,11 @@ class RepositoryService {
// 确保配置已加载 // 确保配置已加载
await this.loadConfig() 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) { for (const artistEntry of artistEntries) {
if (!artistEntry.isDirectory()) continue if (!artistEntry.isDirectory()) continue
@@ -101,7 +127,7 @@ class RepositoryService {
} }
const artistName = artistEntry.name 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 }) const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true })
@@ -164,9 +190,10 @@ class RepositoryService {
if (this.config.allowedExtensions.includes(ext)) { if (this.config.allowedExtensions.includes(ext)) {
const stats = await fs.stat(filePath) const stats = await fs.stat(filePath)
const currentBaseDir = this.getCurrentBaseDir()
files.push({ files.push({
name: entry.name, name: entry.name,
path: path.relative(this.baseDir, filePath), path: path.relative(currentBaseDir, filePath),
size: stats.size, size: stats.size,
extension: ext, extension: ext,
modifiedAt: stats.mtime modifiedAt: stats.mtime
@@ -194,7 +221,8 @@ class RepositoryService {
// 获取磁盘使用情况 // 获取磁盘使用情况
async getDiskUsage() { async getDiskUsage() {
try { 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 total = stats.blocks * stats.bsize
const free = stats.bavail * stats.bsize const free = stats.bavail * stats.bsize
const used = total - free 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) { async searchArtworks(query, offset = 0, limit = 20) {
try { try {
@@ -295,65 +382,138 @@ class RepositoryService {
// 自动迁移旧项目 // 自动迁移旧项目
async migrateOldProjects(sourceDir) { async migrateOldProjects(sourceDir) {
try { 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 sourceEntries = await fs.readdir(sourceDir, { withFileTypes: true })
const entries = await fs.readdir(dirPath, { withFileTypes: true })
for (const entry of sourceEntries) {
if (!entry.isDirectory()) continue
for (const entry of entries) { const oldDirPath = path.join(sourceDir, entry.name)
const fullPath = path.join(dirPath, entry.name) const newDirPath = path.join(currentBaseDir, entry.name)
const newRelativePath = path.join(relativePath, 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()) { result.log.push({
// 检查是否是作品目录 id: entry.name,
const artworkMatch = entry.name.match(/^(\d+)_(.+)$/) title: entry.name,
if (artworkMatch) { status: 'success'
const artworkId = artworkMatch[1] })
const title = artworkMatch[2] result.totalMigrated++
// 检查是否已存在 } catch (error) {
const existingArtwork = await this.findArtworkById(artworkId) result.log.push({
if (!existingArtwork) { id: entry.name,
// 迁移作品 title: entry.name,
const targetPath = path.join(this.baseDir, newRelativePath) status: 'error',
await fs.mkdir(path.dirname(targetPath), { recursive: true }) reason: error.message
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)
}
}
} }
} }
await scanSource(sourceDir) return result
} catch (error) {
return { throw new Error(`迁移失败: ${error.message}`)
}
}
// 从旧目录迁移到新目录
async migrateFromOldToNew(oldDir, newDir) {
try {
const result = {
success: true, success: true,
message: '迁移完成', message: '迁移完成',
log: migrationLog, log: [],
totalMigrated: migrationLog.filter(item => item.status === 'success').length 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) { } catch (error) {
throw new Error(`迁移失败: ${error.message}`) throw new Error(`迁移失败: ${error.message}`)
} }
+32
View File
@@ -115,6 +115,16 @@ export const useRepositoryStore = defineStore('repository', () => {
return result return result
} }
// 重置配置
const resetConfig = async (): Promise<any> => {
const result = await apiCall('/config/reset', {
method: 'POST',
})
// 重新加载配置
config.value = await getConfig()
return result
}
// 获取统计信息 // 获取统计信息
const getStats = async (): Promise<RepositoryStats> => { const getStats = async (): Promise<RepositoryStats> => {
const result = await apiCall('/stats') const result = await apiCall('/stats')
@@ -165,6 +175,24 @@ export const useRepositoryStore = defineStore('repository', () => {
return await apiCall(`/directory?path=${encodeURIComponent(dirPath)}`) 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 { return {
// 状态 // 状态
config, config,
@@ -174,6 +202,7 @@ export const useRepositoryStore = defineStore('repository', () => {
initialize, initialize,
getConfig, getConfig,
updateConfig, updateConfig,
resetConfig,
getStats, getStats,
getArtists, getArtists,
getArtworksByArtist, getArtworksByArtist,
@@ -183,5 +212,8 @@ export const useRepositoryStore = defineStore('repository', () => {
migrateOldProjects, migrateOldProjects,
getFileInfo, getFileInfo,
getDirectory, getDirectory,
checkArtworkDownloaded,
checkDirectoryExists,
migrateFromOldToNew,
} }
}) })
+67 -1
View File
@@ -49,12 +49,21 @@
<h1 class="artwork-title">{{ artwork.title }}</h1> <h1 class="artwork-title">{{ artwork.title }}</h1>
<div class="artwork-actions"> <div class="artwork-actions">
<button @click="handleDownload" class="btn btn-primary" :disabled="downloading"> <button @click="handleDownload" class="btn btn-primary" :disabled="downloading">
{{ downloading ? '下载中...' : '下载' }} {{ downloading ? '下载中...' : (isDownloaded ? '重新下载' : '下载') }}
</button> </button>
<button @click="handleBookmark" class="btn btn-secondary"> <button @click="handleBookmark" class="btn btn-secondary">
{{ artwork.is_bookmarked ? '取消收藏' : '收藏' }} {{ artwork.is_bookmarked ? '取消收藏' : '收藏' }}
</button> </button>
</div> </div>
<!-- 下载状态提示 -->
<div v-if="isDownloaded" class="download-status">
<div class="status-indicator">
<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"/>
</svg>
<span>已下载到本地</span>
</div>
</div>
</div> </div>
<!-- 作者信息 --> <!-- 作者信息 -->
@@ -162,6 +171,7 @@ import { useAuthStore } from '@/stores/auth';
import artworkService from '@/services/artwork'; 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 type { Artwork } from '@/types'; import type { 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';
@@ -169,6 +179,7 @@ import ErrorMessage from '@/components/common/ErrorMessage.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuthStore();
const repositoryStore = useRepositoryStore();
// 状态 // 状态
const artwork = ref<Artwork | null>(null); const artwork = ref<Artwork | null>(null);
@@ -178,6 +189,8 @@ const currentPage = ref(0);
const imageLoaded = ref(false); const imageLoaded = ref(false);
const imageError = ref(false); const imageError = ref(false);
const downloading = ref(false); const downloading = ref(false);
const isDownloaded = ref(false);
const checkingDownloadStatus = ref(false);
// 导航相关状态 // 导航相关状态
const artistArtworks = ref<Artwork[]>([]); const artistArtworks = ref<Artwork[]>([]);
@@ -236,6 +249,8 @@ const fetchArtworkDetail = async () => {
if (response.success && response.data) { if (response.success && response.data) {
artwork.value = response.data; artwork.value = response.data;
// 检查下载状态
await checkDownloadStatus(artworkId);
} else { } else {
throw new Error(response.error || '获取作品详情失败'); 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 () => { const handleDownload = async () => {
if (!artwork.value) return; if (!artwork.value) return;
@@ -258,6 +294,10 @@ const handleDownload = async () => {
if (response.success) { if (response.success) {
// 可以显示下载成功提示 // 可以显示下载成功提示
console.log('下载任务已创建:', response.data); console.log('下载任务已创建:', response.data);
// 下载完成后重新检查下载状态
setTimeout(() => {
checkDownloadStatus(artwork.value!.id);
}, 2000); // 等待2秒让下载完成
} else { } else {
throw new Error(response.error || '下载失败'); throw new Error(response.error || '下载失败');
} }
@@ -723,6 +763,32 @@ onUnmounted(() => {
justify-content: flex-end; 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) { @media (max-width: 1024px) {
.artwork-content { .artwork-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
+297 -53
View File
@@ -55,13 +55,6 @@
> >
文件浏览 文件浏览
</button> </button>
<button
class="tab-button"
:class="{ active: activeTab === 'migrate' }"
@click="activeTab = 'migrate'"
>
数据迁移
</button>
</div> </div>
<!-- 配置管理 --> <!-- 配置管理 -->
@@ -75,15 +68,73 @@
<input <input
v-model="config.downloadDir" v-model="config.downloadDir"
type="text" type="text"
placeholder="设置下载目录路径" placeholder="设置下载目录路径,例如: ./downloads 或 D:\downloads"
class="form-input" class="form-input"
readonly
/> />
<button type="button" @click="selectDownloadDir" class="btn btn-secondary"> <button type="button" @click="selectDownloadDir" class="btn btn-secondary">
选择目录 选择目录
</button> </button>
<button type="button" @click="testDownloadDir" class="btn btn-outline">
测试
</button>
</div>
<small class="form-help">
<strong>路径示例</strong><br>
相对路径<code>./downloads</code>相对于项目根目录<br>
绝对路径<code>D:\downloads</code> <code>/home/user/downloads</code><br>
当前目录<code>.</code> <code>./</code>
</small>
</div>
<!-- 自动迁移选项 -->
<div class="form-group">
<label class="checkbox-label">
<input
v-model="config.autoMigration"
type="checkbox"
class="form-checkbox"
/>
<span>自动迁移旧下载文件</span>
</label>
<small class="form-help">
启用后保存配置时会自动将旧下载目录中的文件移动到新目录
</small>
</div>
<!-- 迁移进度显示 -->
<div v-if="migrating" class="migration-progress">
<div class="progress-header">
<h4>正在迁移文件...</h4>
<span class="progress-text">{{ migrationProgress }}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: migrationPercent + '%' }"></div>
</div>
</div>
<!-- 迁移结果 -->
<div v-if="migrationResult" class="migration-result">
<h4>迁移完成</h4>
<div class="result-stats">
<p> 成功迁移: {{ migrationResult.totalMigrated }} 个作品</p>
<p> 跳过: {{ migrationResult.log.filter((item: any) => item.status === 'skipped').length }} 个作品</p>
</div>
<div class="migration-log">
<h5>详细日志</h5>
<div
v-for="(item, index) in migrationResult.log.slice(0, 10)"
:key="index"
class="log-item"
:class="(item as any).status"
>
<span class="log-status">{{ (item as any).status === 'success' ? '✅' : '⏭️' }}</span>
<span class="log-text">{{ (item as any).title }} (ID: {{ (item as any).id }})</span>
<span v-if="(item as any).reason" class="log-reason">{{ (item as any).reason }}</span>
</div>
<div v-if="migrationResult.log.length > 10" class="log-more">
还有 {{ migrationResult.log.length - 10 }} 个文件...
</div>
</div> </div>
<small class="form-help">默认路径: ./downloads</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>文件结构</label> <label>文件结构</label>
@@ -125,6 +176,9 @@
<button @click="saveConfig" class="btn btn-primary" :disabled="saving"> <button @click="saveConfig" class="btn btn-primary" :disabled="saving">
{{ saving ? '保存中...' : '保存配置' }} {{ saving ? '保存中...' : '保存配置' }}
</button> </button>
<button @click="resetConfig" class="btn btn-outline" :disabled="saving">
重置为默认
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -236,14 +290,22 @@
<input <input
v-model="migrateSourceDir" v-model="migrateSourceDir"
type="text" type="text"
placeholder="选择要迁移的目录路径" placeholder="选择要迁移的目录路径,例如: D:\old-downloads"
class="form-input" class="form-input"
readonly
/> />
<button type="button" @click="selectMigrateDir" class="btn btn-secondary"> <button type="button" @click="selectMigrateDir" class="btn btn-secondary">
选择目录 选择目录
</button> </button>
<button type="button" @click="testMigrateDir" class="btn btn-outline">
测试
</button>
</div> </div>
<small class="form-help">
<strong>迁移说明</strong><br>
选择要迁移的源目录系统会将整个目录结构移动到目标位置<br>
如果目标位置已存在同名目录将跳过迁移<br>
迁移完成后源文件会被移动到新位置移动操作
</small>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button <button
@@ -361,6 +423,8 @@ const totalPages = computed(() => Math.ceil(artworks.value.length / pageSize))
const migrateSourceDir = ref('') const migrateSourceDir = ref('')
const migrating = ref(false) const migrating = ref(false)
const migrationResult = ref<any>(null) const migrationResult = ref<any>(null)
const migrationProgress = ref('')
const migrationPercent = ref(0)
// 模态框 // 模态框
const selectedArtwork = ref<Artwork | null>(null) const selectedArtwork = ref<Artwork | null>(null)
@@ -404,7 +468,18 @@ const saveConfig = async () => {
.filter((ext: string) => ext) .filter((ext: string) => ext)
} }
// 获取当前配置(旧配置)
const oldConfig = await repositoryStore.getConfig()
const oldDownloadDir = oldConfig.downloadDir
// 保存新配置
await repositoryStore.updateConfig(config.value) await repositoryStore.updateConfig(config.value)
// 如果启用了自动迁移,且下载目录发生了变化,执行迁移
if (config.value.autoMigration && oldDownloadDir !== config.value.downloadDir) {
await performAutoMigration(oldDownloadDir)
}
alert('配置保存成功') alert('配置保存成功')
} catch (error: any) { } catch (error: any) {
console.error('保存配置失败:', error) console.error('保存配置失败:', error)
@@ -414,6 +489,61 @@ const saveConfig = async () => {
} }
} }
// 重置配置
const resetConfig = async () => {
if (!confirm('确定要重置配置为默认值吗?此操作不可恢复。')) {
return
}
saving.value = true
try {
await repositoryStore.resetConfig()
// 重新加载配置
await loadConfig()
alert('配置已重置为默认值')
} catch (error: any) {
console.error('重置配置失败:', error)
alert('重置配置失败: ' + error.message)
} finally {
saving.value = false
}
}
// 执行自动迁移
const performAutoMigration = async (oldDownloadDir: string) => {
try {
migrating.value = true
migrationProgress.value = '正在准备迁移...'
migrationPercent.value = 10
console.log('开始自动迁移:', { oldDir: oldDownloadDir, newDir: config.value.downloadDir })
migrationProgress.value = `正在从 ${oldDownloadDir} 迁移到 ${config.value.downloadDir}...`
migrationPercent.value = 30
// 执行迁移:从旧目录到新目录
const result = await repositoryStore.migrateFromOldToNew(oldDownloadDir, config.value.downloadDir)
migrationResult.value = result
migrationPercent.value = 100
console.log('迁移完成:', result)
// 显示迁移结果
if (result.totalMigrated > 0) {
alert(`迁移完成!成功移动 ${result.totalMigrated} 个目录`)
} else {
alert('迁移完成,但没有找到需要迁移的文件')
}
} catch (error: any) {
console.error('自动迁移失败:', error)
migrationProgress.value = '迁移失败: ' + error.message
alert('自动迁移失败: ' + error.message)
} finally {
migrating.value = false
}
}
// 加载作者列表 // 加载作者列表
const loadArtists = async () => { const loadArtists = async () => {
try { try {
@@ -491,8 +621,18 @@ const startMigration = async () => {
migrating.value = true migrating.value = true
try { try {
console.log('开始手动迁移:', { sourceDir: migrateSourceDir.value })
migrationResult.value = await repositoryStore.migrateOldProjects(migrateSourceDir.value) migrationResult.value = await repositoryStore.migrateOldProjects(migrateSourceDir.value)
alert('迁移完成')
console.log('手动迁移完成:', migrationResult.value)
if (migrationResult.value.totalMigrated > 0) {
alert(`迁移完成!成功移动 ${migrationResult.value.totalMigrated} 个目录`)
} else {
alert('迁移完成,但没有找到需要迁移的文件')
}
await loadStats() await loadStats()
await loadArtists() await loadArtists()
} catch (error: any) { } catch (error: any) {
@@ -503,6 +643,20 @@ const startMigration = async () => {
} }
} }
// 选择迁移目录
const selectMigrateDir = async () => {
try {
// 使用简单的prompt方式,让用户输入完整路径
const dir = prompt('请输入要迁移的源目录完整路径:', migrateSourceDir.value || '')
if (dir && dir.trim()) {
migrateSourceDir.value = dir.trim()
}
} catch (error: any) {
console.error('选择目录失败:', error)
alert('选择目录失败,请手动输入路径')
}
}
// 分页 // 分页
const changePage = (page: number) => { const changePage = (page: number) => {
if (page >= 1 && page <= totalPages.value) { if (page >= 1 && page <= totalPages.value) {
@@ -513,56 +667,55 @@ const changePage = (page: number) => {
// 选择下载目录 // 选择下载目录
const selectDownloadDir = async () => { const selectDownloadDir = async () => {
try { try {
// 使用HTML5文件选择器选择目录 // 使用简单的prompt方式,让用户输入完整路径
const input = document.createElement('input') const dir = prompt('请输入下载目录的完整路径:', config.value.downloadDir || './downloads')
input.type = 'file' if (dir && dir.trim()) {
input.webkitdirectory = true config.value.downloadDir = dir.trim()
input.multiple = false
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) {
// 获取选择的目录路径
const path = files[0].webkitRelativePath.split('/')[0]
config.value.downloadDir = path
}
} }
input.click()
} catch (error: any) { } catch (error: any) {
console.error('选择目录失败:', error) console.error('选择目录失败:', error)
// 降级到prompt alert('选择目录失败,请手动输入路径')
const dir = prompt('请输入下载目录路径:', './downloads')
if (dir) {
config.value.downloadDir = dir
}
} }
} }
// 选择迁移目录 // 验证目录路径
const selectMigrateDir = async () => { const validateDirectory = async (path: string) => {
try { try {
const input = document.createElement('input') // 这里可以添加后端API调用来验证目录是否存在
input.type = 'file' // 暂时使用简单的客户端验证
input.webkitdirectory = true if (!path || path.trim() === '') {
input.multiple = false return { valid: false, message: '路径不能为空' }
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) {
const path = files[0].webkitRelativePath.split('/')[0]
migrateSourceDir.value = path
}
} }
input.click() // 检查路径格式
const trimmedPath = path.trim()
if (trimmedPath.includes('..') || trimmedPath.includes('//')) {
return { valid: false, message: '路径格式不正确' }
}
return { valid: true, message: '路径格式正确' }
} catch (error: any) { } catch (error: any) {
console.error('选择目录失败:', error) return { valid: false, message: '验证失败: ' + error.message }
// 降级到prompt }
const dir = prompt('请选择要迁移的源目录路径:') }
if (dir) {
migrateSourceDir.value = dir // 测试下载目录
} const testDownloadDir = async () => {
const validation = await validateDirectory(config.value.downloadDir)
if (validation.valid) {
alert('路径格式正确!保存配置后系统会验证目录是否存在。')
} else {
alert('路径验证失败: ' + validation.message)
}
}
// 测试迁移目录
const testMigrateDir = async () => {
const validation = await validateDirectory(migrateSourceDir.value)
if (validation.valid) {
alert('路径格式正确!开始迁移时会验证目录是否存在。')
} else {
alert('路径验证失败: ' + validation.message)
} }
} }
@@ -718,12 +871,43 @@ const getPreviewUrl = (filePath: string) => {
.path-input-group .btn { .path-input-group .btn {
white-space: nowrap; white-space: nowrap;
min-width: 80px;
} }
.form-help { .form-help {
color: #6b7280; color: #6b7280;
font-size: 0.75rem; font-size: 0.75rem;
margin-top: 0.25rem; margin-top: 0.25rem;
line-height: 1.4;
}
.form-help strong {
color: #374151;
font-weight: 600;
}
.form-help code {
background: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: 'Courier New', monospace;
font-size: 0.7rem;
color: #1f2937;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: 500;
color: #374151;
}
.form-checkbox {
width: 1rem;
height: 1rem;
accent-color: #3b82f6;
} }
.form-actions { .form-actions {
@@ -753,6 +937,21 @@ const getPreviewUrl = (filePath: string) => {
color: white; color: white;
} }
.btn-secondary:hover {
background: #4b5563;
}
.btn-outline {
background: white;
color: #3b82f6;
border: 1px solid #3b82f6;
}
.btn-outline:hover {
background: #3b82f6;
color: white;
}
.btn-danger { .btn-danger {
background: #ef4444; background: #ef4444;
color: white; color: white;
@@ -899,6 +1098,45 @@ const getPreviewUrl = (filePath: string) => {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.migration-progress {
margin-top: 1.5rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.progress-header h4 {
margin: 0;
color: #1f2937;
}
.progress-text {
font-size: 0.875rem;
color: #6b7280;
}
.progress-bar {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 4px;
transition: width 0.3s ease-in-out;
}
.migration-result { .migration-result {
margin-top: 2rem; margin-top: 2rem;
padding: 1.5rem; padding: 1.5rem;
@@ -932,6 +1170,12 @@ const getPreviewUrl = (filePath: string) => {
font-size: 0.875rem; font-size: 0.875rem;
} }
.log-more {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.5rem;
}
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;