diff --git a/backend/README.md b/backend/README.md
index e1372e7..67e95f8 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -19,11 +19,13 @@ backend/
│ ├── auth.js # 认证路由
│ ├── artwork.js # 作品路由
│ ├── artist.js # 作者路由
-│ └── download.js # 下载路由
+│ ├── download.js # 下载路由
+│ └── repository.js # 仓库管理路由
├── services/ # 服务层
│ ├── artwork.js # 作品服务
│ ├── artist.js # 作者服务
-│ └── download.js # 下载服务
+│ ├── download.js # 下载服务
+│ └── repository.js # 仓库管理服务
└── utils/ # 工具类
└── response.js # 响应工具
```
@@ -79,6 +81,29 @@ backend/
- `GET /api/proxy/image` - 图片代理服务
- 参数: `url` (图片URL)
+### 仓库管理相关
+
+- `POST /api/repository/initialize` - 初始化仓库
+- `GET /api/repository/config` - 获取仓库配置
+- `PUT /api/repository/config` - 更新仓库配置
+- `GET /api/repository/stats` - 获取仓库统计信息
+- `GET /api/repository/artists` - 获取作者列表
+ - 参数: `offset`, `limit`
+- `GET /api/repository/artists/:artistName/artworks` - 获取作者作品列表
+ - 参数: `offset`, `limit`
+- `GET /api/repository/search` - 搜索作品
+ - 参数: `q`, `offset`, `limit`
+- `GET /api/repository/artworks/:artworkId` - 获取作品详情
+- `DELETE /api/repository/artworks/:artworkId` - 删除作品
+- `POST /api/repository/migrate` - 自动迁移旧项目
+ - 参数: `sourceDir` (源目录路径)
+- `GET /api/repository/preview` - 文件预览代理
+ - 参数: `path` (文件路径)
+- `GET /api/repository/file-info` - 获取文件信息
+ - 参数: `path` (文件路径)
+- `GET /api/repository/directory` - 获取目录结构
+ - 参数: `path` (目录路径)
+
## 🔧 配置说明
### 代理配置
@@ -110,6 +135,7 @@ backend/
- **artwork.js**: 作品相关路由
- **artist.js**: 作者相关路由
- **download.js**: 下载相关路由
+- **repository.js**: 仓库管理路由
- **proxy.js**: 代理服务路由
### 服务层
@@ -117,6 +143,7 @@ backend/
- **artwork.js**: 作品服务,处理作品API调用
- **artist.js**: 作者服务,处理作者API调用
- **download.js**: 下载服务,处理文件下载
+- **repository.js**: 仓库管理服务,处理文件管理和配置
### 工具类
@@ -148,6 +175,15 @@ backend/
- 自动刷新令牌
- 登录状态管理
+### 5. 仓库管理
+- 文件存储配置管理
+- 作品文件浏览和搜索
+- 按作者分类浏览
+- 文件预览和下载
+- 自动迁移旧项目
+- 磁盘使用情况监控
+- 作品删除管理
+
## 🔒 安全特性
- 统一的错误处理
diff --git a/backend/routes/repository.js b/backend/routes/repository.js
new file mode 100644
index 0000000..f6b982d
--- /dev/null
+++ b/backend/routes/repository.js
@@ -0,0 +1,280 @@
+const express = require('express')
+const path = require('path')
+const fs = require('fs').promises
+const RepositoryService = require('../services/repository')
+const ResponseUtil = require('../utils/response')
+
+const router = express.Router()
+const repositoryService = new RepositoryService()
+
+// 初始化仓库
+router.post('/initialize', async (req, res) => {
+ try {
+ const result = await repositoryService.initialize()
+ res.json(ResponseUtil.success(result))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取仓库配置
+router.get('/config', async (req, res) => {
+ try {
+ const config = await repositoryService.getConfig()
+ res.json(ResponseUtil.success(config))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 更新仓库配置
+router.put('/config', async (req, res) => {
+ try {
+ const result = await repositoryService.updateConfig(req.body)
+ res.json(ResponseUtil.success(result))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取仓库统计信息
+router.get('/stats', async (req, res) => {
+ try {
+ const stats = await repositoryService.getStats()
+ res.json(ResponseUtil.success(stats))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取所有作者列表
+router.get('/artists', async (req, res) => {
+ try {
+ const { offset = 0, limit = 50 } = req.query
+ const artists = await repositoryService.getArtists(parseInt(offset), parseInt(limit))
+ res.json(ResponseUtil.success(artists))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取作者作品列表
+router.get('/artists/:artistName/artworks', async (req, res) => {
+ try {
+ const { artistName } = req.params
+ const { offset = 0, limit = 20 } = req.query
+ const artworks = await repositoryService.getArtworksByArtist(
+ artistName,
+ parseInt(offset),
+ parseInt(limit)
+ )
+ res.json(ResponseUtil.success(artworks))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 搜索作品
+router.get('/search', async (req, res) => {
+ try {
+ const { q, offset = 0, limit = 20 } = req.query
+ if (!q) {
+ return res.status(400).json(ResponseUtil.error('搜索关键词不能为空'))
+ }
+
+ const results = await repositoryService.searchArtworks(q, parseInt(offset), parseInt(limit))
+ res.json(ResponseUtil.success(results))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取作品详情
+router.get('/artworks/:artworkId', async (req, res) => {
+ try {
+ const { artworkId } = req.params
+ const artwork = await repositoryService.findArtworkById(artworkId)
+
+ if (!artwork) {
+ return res.status(404).json(ResponseUtil.error('作品不存在'))
+ }
+
+ res.json(ResponseUtil.success(artwork))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 删除作品
+router.delete('/artworks/:artworkId', async (req, res) => {
+ try {
+ const { artworkId } = req.params
+ const result = await repositoryService.deleteArtwork(artworkId)
+ res.json(ResponseUtil.success(result))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 自动迁移旧项目
+router.post('/migrate', async (req, res) => {
+ try {
+ const { sourceDir } = req.body
+ if (!sourceDir) {
+ return res.status(400).json(ResponseUtil.error('源目录不能为空'))
+ }
+
+ const result = await repositoryService.migrateOldProjects(sourceDir)
+ res.json(ResponseUtil.success(result))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 文件预览代理
+router.get('/preview', async (req, res) => {
+ try {
+ const { path: filePath } = req.query
+ if (!filePath) {
+ return res.status(400).json(ResponseUtil.error('文件路径不能为空'))
+ }
+
+ const fullPath = path.join(repositoryService.baseDir, filePath)
+
+ // 安全检查:确保文件在仓库目录内
+ const relativePath = path.relative(repositoryService.baseDir, fullPath)
+
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+ return res.status(403).json(ResponseUtil.error('访问被拒绝'))
+ }
+
+ // 检查文件是否存在
+ try {
+ await fs.access(fullPath)
+ } catch (error) {
+ return res.status(404).json(ResponseUtil.error('文件不存在'))
+ }
+
+ // 获取文件信息
+ const stats = await fs.stat(fullPath)
+ const ext = path.extname(fullPath).toLowerCase()
+
+ // 设置响应头
+ res.setHeader('Content-Type', getContentType(ext))
+ res.setHeader('Content-Length', stats.size)
+ res.setHeader('Cache-Control', 'public, max-age=3600')
+
+ // 流式传输文件
+ const fileStream = require('fs').createReadStream(fullPath)
+ fileStream.pipe(res)
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取文件信息
+router.get('/file-info', async (req, res) => {
+ try {
+ const { path: filePath } = req.query
+ if (!filePath) {
+ return res.status(400).json(ResponseUtil.error('文件路径不能为空'))
+ }
+
+ const fullPath = path.join(repositoryService.baseDir, filePath)
+
+ // 安全检查
+ const relativePath = path.relative(repositoryService.baseDir, fullPath)
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+ return res.status(403).json(ResponseUtil.error('访问被拒绝'))
+ }
+
+ const stats = await fs.stat(fullPath)
+ const ext = path.extname(fullPath).toLowerCase()
+
+ res.json(ResponseUtil.success({
+ name: path.basename(fullPath),
+ path: filePath,
+ size: stats.size,
+ extension: ext,
+ modifiedAt: stats.mtime,
+ createdAt: stats.birthtime,
+ contentType: getContentType(ext),
+ previewUrl: `/api/repository/preview?path=${encodeURIComponent(filePath)}`
+ }))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取目录结构
+router.get('/directory', async (req, res) => {
+ try {
+ const { path: dirPath = '' } = req.query
+ const fullPath = path.join(repositoryService.baseDir, dirPath)
+
+ // 安全检查
+ const relativePath = path.relative(repositoryService.baseDir, fullPath)
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+ return res.status(403).json(ResponseUtil.error('访问被拒绝'))
+ }
+
+ const entries = await fs.readdir(fullPath, { withFileTypes: true })
+ const items = []
+
+ for (const entry of entries) {
+ const itemPath = path.join(dirPath, entry.name)
+ const fullItemPath = path.join(fullPath, entry.name)
+
+ if (entry.isDirectory()) {
+ items.push({
+ type: 'directory',
+ name: entry.name,
+ path: itemPath
+ })
+ } else {
+ const ext = path.extname(entry.name).toLowerCase()
+ if (repositoryService.config.allowedExtensions.includes(ext)) {
+ const stats = await fs.stat(fullItemPath)
+ items.push({
+ type: 'file',
+ name: entry.name,
+ path: itemPath,
+ size: stats.size,
+ extension: ext,
+ modifiedAt: stats.mtime
+ })
+ }
+ }
+ }
+
+ res.json(ResponseUtil.success({
+ path: dirPath,
+ items: items.sort((a, b) => {
+ // 目录在前,文件在后
+ if (a.type !== b.type) {
+ return a.type === 'directory' ? -1 : 1
+ }
+ return a.name.localeCompare(b.name)
+ })
+ }))
+ } catch (error) {
+ res.status(500).json(ResponseUtil.error(error.message))
+ }
+})
+
+// 获取内容类型
+function getContentType(extension) {
+ const contentTypes = {
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ '.gif': 'image/gif',
+ '.webp': 'image/webp',
+ '.bmp': 'image/bmp',
+ '.svg': 'image/svg+xml'
+ }
+
+ return contentTypes[extension.toLowerCase()] || 'application/octet-stream'
+}
+
+module.exports = router
\ No newline at end of file
diff --git a/backend/scripts/migrate-downloads.js b/backend/scripts/migrate-downloads.js
new file mode 100644
index 0000000..42cdfb0
--- /dev/null
+++ b/backend/scripts/migrate-downloads.js
@@ -0,0 +1,88 @@
+const fs = require('fs').promises
+const path = require('path')
+const fsExtra = require('fs-extra')
+
+/**
+ * 转换现有的下载格式为仓库管理格式
+ * 从: {artistName}_{artworkId}/{artworkTitle}/
+ * 到: {artistName}/{artworkId}_{artworkTitle}/
+ */
+async function migrateDownloads() {
+ const downloadsPath = path.join(__dirname, '../../downloads')
+
+ try {
+ console.log('开始转换下载格式...')
+
+ // 读取downloads目录
+ const entries = await fs.readdir(downloadsPath, { withFileTypes: true })
+
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue
+
+ const oldDirName = entry.name
+ console.log(`处理目录: ${oldDirName}`)
+
+ // 解析目录名: {artistName}_{artworkId}
+ const match = oldDirName.match(/^(.+)_(\d+)$/)
+ if (!match) {
+ console.log(`跳过不符合格式的目录: ${oldDirName}`)
+ continue
+ }
+
+ const [, artistName, artworkId] = match
+ const oldDirPath = path.join(downloadsPath, oldDirName)
+
+ // 读取作品目录
+ const artworkEntries = await fs.readdir(oldDirPath, { withFileTypes: true })
+
+ for (const artworkEntry of artworkEntries) {
+ if (!artworkEntry.isDirectory()) continue
+
+ const artworkTitle = artworkEntry.name
+ const oldArtworkPath = path.join(oldDirPath, artworkTitle)
+
+ // 新的目录结构
+ const newArtistDir = path.join(downloadsPath, artistName)
+ const newArtworkDirName = `${artworkId}_${artworkTitle}`
+ const newArtworkPath = path.join(newArtistDir, newArtworkDirName)
+
+ console.log(`转换: ${oldArtworkPath} -> ${newArtworkPath}`)
+
+ try {
+ // 创建新的作者目录
+ await fsExtra.ensureDir(newArtistDir)
+
+ // 移动作品目录
+ await fsExtra.move(oldArtworkPath, newArtworkPath)
+
+ console.log(`✓ 成功转换: ${artworkTitle}`)
+ } catch (error) {
+ console.error(`✗ 转换失败: ${artworkTitle}`, error.message)
+ }
+ }
+
+ // 检查原目录是否为空,如果为空则删除
+ try {
+ const remainingEntries = await fs.readdir(oldDirPath)
+ if (remainingEntries.length === 0) {
+ await fsExtra.remove(oldDirPath)
+ console.log(`删除空目录: ${oldDirPath}`)
+ }
+ } catch (error) {
+ console.error(`删除目录失败: ${oldDirPath}`, error.message)
+ }
+ }
+
+ console.log('转换完成!')
+
+ } catch (error) {
+ console.error('转换过程中发生错误:', error)
+ }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+ migrateDownloads()
+}
+
+module.exports = { migrateDownloads }
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index 8669f04..161b140 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -9,6 +9,7 @@ const artworkRoutes = require('./routes/artwork');
const artistRoutes = require('./routes/artist');
const downloadRoutes = require('./routes/download');
const proxyRoutes = require('./routes/proxy');
+const repositoryRoutes = require('./routes/repository');
// 导入中间件 - 临时注释掉来定位问题
const { errorHandler } = require('./middleware/errorHandler');
@@ -101,6 +102,7 @@ class PixivServer {
this.app.use('/api/artwork', authMiddleware, artworkRoutes);
this.app.use('/api/artist', authMiddleware, artistRoutes);
this.app.use('/api/download', authMiddleware, downloadRoutes);
+ this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
// 404 处理
diff --git a/backend/services/download.js b/backend/services/download.js
index 17168cf..afee057 100644
--- a/backend/services/download.js
+++ b/backend/services/download.js
@@ -374,8 +374,10 @@ class DownloadService {
const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_');
const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_');
- // 创建作品目录
- const artworkDir = path.join(this.downloadPath, `${artistName}_${artworkId}`, artworkTitle);
+ // 创建作品目录 - 使用仓库管理格式
+ const artistDir = path.join(this.downloadPath, artistName);
+ const artworkDirName = `${artworkId}_${artworkTitle}`;
+ const artworkDir = path.join(artistDir, artworkDirName);
await fs.ensureDir(artworkDir);
// 获取图片URL
diff --git a/backend/services/repository.js b/backend/services/repository.js
new file mode 100644
index 0000000..f43d7d8
--- /dev/null
+++ b/backend/services/repository.js
@@ -0,0 +1,414 @@
+const fs = require('fs').promises
+const path = require('path')
+const { promisify } = require('util')
+const { exec } = require('child_process')
+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')
+ }
+
+ // 初始化仓库
+ async initialize() {
+ try {
+ await fs.mkdir(this.baseDir, { recursive: true })
+ await this.loadConfig()
+ return { success: true, message: '仓库初始化成功' }
+ } catch (error) {
+ throw new Error(`仓库初始化失败: ${error.message}`)
+ }
+ }
+
+ // 加载配置
+ async loadConfig() {
+ try {
+ const configData = await fs.readFile(this.configFile, 'utf8')
+ this.config = JSON.parse(configData)
+ } 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()
+ }
+ }
+
+ // 保存配置
+ async saveConfig() {
+ try {
+ await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2))
+ } catch (error) {
+ throw new Error(`保存配置失败: ${error.message}`)
+ }
+ }
+
+ // 获取仓库配置
+ async getConfig() {
+ await this.loadConfig()
+ return this.config
+ }
+
+ // 更新仓库配置
+ async updateConfig(newConfig) {
+ this.config = { ...this.config, ...newConfig }
+ await this.saveConfig()
+ return { success: true, message: '配置更新成功' }
+ }
+
+ // 获取仓库统计信息
+ async getStats() {
+ try {
+ const stats = await this.scanRepository()
+ return {
+ totalArtworks: stats.artworks.length,
+ totalArtists: stats.artists.length,
+ totalSize: stats.totalSize,
+ diskUsage: await this.getDiskUsage(),
+ lastScan: new Date().toISOString()
+ }
+ } catch (error) {
+ throw new Error(`获取统计信息失败: ${error.message}`)
+ }
+ }
+
+ // 扫描仓库
+ async scanRepository() {
+ const artworks = []
+ const artists = new Set()
+ let totalSize = 0
+
+ try {
+ // 确保配置已加载
+ await this.loadConfig()
+
+ // 扫描作者目录
+ const artistEntries = await fs.readdir(this.baseDir, { withFileTypes: true })
+
+ for (const artistEntry of artistEntries) {
+ if (!artistEntry.isDirectory()) continue
+
+ // 跳过配置文件和隐藏文件
+ if (artistEntry.name.startsWith('.') || artistEntry.name === '.repository-config.json') {
+ continue
+ }
+
+ const artistName = artistEntry.name
+ const artistPath = path.join(this.baseDir, artistName)
+
+ // 扫描作者下的作品目录
+ const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true })
+
+ for (const artworkEntry of artworkEntries) {
+ if (!artworkEntry.isDirectory()) continue
+
+ const fullPath = path.join(artistPath, artworkEntry.name)
+
+ // 检查是否是作品目录(包含数字ID)
+ const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/)
+ if (artworkMatch) {
+ const artworkId = artworkMatch[1]
+ const title = artworkMatch[2]
+
+ // 扫描作品文件
+ const files = await this.scanArtworkFiles(fullPath)
+
+ if (files.length > 0) {
+ artworks.push({
+ id: artworkId,
+ title: title,
+ artist: artistName,
+ artistPath: artistPath,
+ path: fullPath,
+ files: files,
+ size: files.reduce((sum, file) => sum + file.size, 0),
+ createdAt: await this.getFileCreationTime(fullPath)
+ })
+ artists.add(artistName)
+ totalSize += files.reduce((sum, file) => sum + file.size, 0)
+ }
+ }
+ }
+ }
+
+ return {
+ artworks,
+ artists: Array.from(artists),
+ totalSize
+ }
+ } catch (error) {
+ throw new Error(`扫描仓库失败: ${error.message}`)
+ }
+ }
+
+ // 扫描作品文件
+ async scanArtworkFiles(artworkPath) {
+ try {
+ // 确保配置已加载
+ await this.loadConfig()
+
+ const files = []
+ const entries = await fs.readdir(artworkPath, { withFileTypes: true })
+
+ for (const entry of entries) {
+ if (entry.isFile()) {
+ const filePath = path.join(artworkPath, entry.name)
+ const ext = path.extname(entry.name).toLowerCase()
+
+ if (this.config.allowedExtensions.includes(ext)) {
+ const stats = await fs.stat(filePath)
+ files.push({
+ name: entry.name,
+ path: path.relative(this.baseDir, filePath),
+ size: stats.size,
+ extension: ext,
+ modifiedAt: stats.mtime
+ })
+ }
+ }
+ }
+
+ return files
+ } catch (error) {
+ return []
+ }
+ }
+
+ // 获取文件创建时间
+ async getFileCreationTime(filePath) {
+ try {
+ const stats = await fs.stat(filePath)
+ return stats.birthtime
+ } catch (error) {
+ return new Date()
+ }
+ }
+
+ // 获取磁盘使用情况
+ async getDiskUsage() {
+ try {
+ const stats = await fs.statfs(this.baseDir)
+ const total = stats.blocks * stats.bsize
+ const free = stats.bavail * stats.bsize
+ const used = total - free
+
+ return {
+ total,
+ used,
+ free,
+ usagePercent: Math.round((used / total) * 100)
+ }
+ } catch (error) {
+ return { total: 0, used: 0, free: 0, usagePercent: 0 }
+ }
+ }
+
+ // 按作者浏览作品
+ async getArtworksByArtist(artistName, offset = 0, limit = 20) {
+ try {
+ const stats = await this.scanRepository()
+ const artistArtworks = stats.artworks.filter(artwork =>
+ artwork.artist === artistName
+ )
+
+ return {
+ artworks: artistArtworks.slice(offset, offset + limit),
+ total: artistArtworks.length,
+ offset,
+ limit
+ }
+ } catch (error) {
+ throw new Error(`获取作者作品失败: ${error.message}`)
+ }
+ }
+
+ // 按作品ID查找
+ async findArtworkById(artworkId) {
+ try {
+ const stats = await this.scanRepository()
+ return stats.artworks.find(artwork => artwork.id === artworkId)
+ } catch (error) {
+ throw new Error(`查找作品失败: ${error.message}`)
+ }
+ }
+
+ // 搜索作品
+ async searchArtworks(query, offset = 0, limit = 20) {
+ try {
+ const stats = await this.scanRepository()
+ const filtered = stats.artworks.filter(artwork =>
+ artwork.title.toLowerCase().includes(query.toLowerCase()) ||
+ artwork.artist.toLowerCase().includes(query.toLowerCase()) ||
+ artwork.id.includes(query)
+ )
+
+ return {
+ artworks: filtered.slice(offset, offset + limit),
+ total: filtered.length,
+ offset,
+ limit
+ }
+ } catch (error) {
+ throw new Error(`搜索作品失败: ${error.message}`)
+ }
+ }
+
+ // 获取所有作者列表
+ async getArtists(offset = 0, limit = 50) {
+ try {
+ const stats = await this.scanRepository()
+ const artists = stats.artists.slice(offset, offset + limit)
+
+ // 获取每个作者的统计信息
+ const artistsWithStats = artists.map(artistName => {
+ const artistArtworks = stats.artworks.filter(artwork => artwork.artist === artistName)
+ const totalSize = artistArtworks.reduce((sum, artwork) => sum + artwork.size, 0)
+
+ return {
+ name: artistName,
+ artworkCount: artistArtworks.length,
+ totalSize,
+ lastUpdated: artistArtworks.length > 0
+ ? Math.max(...artistArtworks.map(a => new Date(a.createdAt).getTime()))
+ : null
+ }
+ })
+
+ return {
+ artists: artistsWithStats,
+ total: stats.artists.length,
+ offset,
+ limit
+ }
+ } catch (error) {
+ throw new Error(`获取作者列表失败: ${error.message}`)
+ }
+ }
+
+ // 自动迁移旧项目
+ async migrateOldProjects(sourceDir) {
+ try {
+ const migrationLog = []
+
+ // 扫描源目录
+ const scanSource = async (dirPath, relativePath = '') => {
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
+
+ for (const entry of entries) {
+ const fullPath = path.join(dirPath, entry.name)
+ const newRelativePath = path.join(relativePath, entry.name)
+
+ 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)
+ }
+ }
+ }
+ }
+
+ await scanSource(sourceDir)
+
+ return {
+ success: true,
+ message: '迁移完成',
+ log: migrationLog,
+ totalMigrated: migrationLog.filter(item => item.status === 'success').length
+ }
+ } catch (error) {
+ throw new Error(`迁移失败: ${error.message}`)
+ }
+ }
+
+ // 复制目录
+ async copyDirectory(source, target) {
+ try {
+ await fs.mkdir(target, { recursive: true })
+ const entries = await fs.readdir(source, { withFileTypes: true })
+
+ for (const entry of entries) {
+ const sourcePath = path.join(source, entry.name)
+ const targetPath = path.join(target, entry.name)
+
+ if (entry.isDirectory()) {
+ await this.copyDirectory(sourcePath, targetPath)
+ } else {
+ await fs.copyFile(sourcePath, targetPath)
+ }
+ }
+ } catch (error) {
+ throw new Error(`复制目录失败: ${error.message}`)
+ }
+ }
+
+ // 删除作品
+ async deleteArtwork(artworkId) {
+ try {
+ const artwork = await this.findArtworkById(artworkId)
+ if (!artwork) {
+ throw new Error('作品不存在')
+ }
+
+ await fs.rm(artwork.path, { recursive: true, force: true })
+
+ // 检查作者目录是否为空,如果为空则删除
+ const artistDir = artwork.artistPath
+ const artistArtworks = await this.getArtworksByArtist(artwork.artist)
+ if (artistArtworks.artworks.length === 0) {
+ await fs.rmdir(artistDir)
+ }
+
+ return { success: true, message: '作品删除成功' }
+ } catch (error) {
+ throw new Error(`删除作品失败: ${error.message}`)
+ }
+ }
+
+ // 获取文件预览URL
+ async getFilePreviewUrl(filePath) {
+ // 这里可以返回一个代理URL,用于前端预览
+ const relativePath = path.relative(this.baseDir, filePath)
+ return `/api/repository/preview?path=${encodeURIComponent(relativePath)}`
+ }
+}
+
+module.exports = RepositoryService
\ No newline at end of file
diff --git a/ui/src/App.vue b/ui/src/App.vue
index 2af4342..0294082 100644
--- a/ui/src/App.vue
+++ b/ui/src/App.vue
@@ -32,6 +32,7 @@ onMounted(async () => {