移动端样式优化,修复当下载文件太多时,下载注册扫描前端显示超时问题
This commit is contained in:
+117
-18
@@ -972,6 +972,28 @@ router.get('/stats', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取下载注册表统计信息
|
||||
* GET /api/download/registry/stats
|
||||
*/
|
||||
router.get('/registry/stats', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const stats = await downloadService.downloadRegistry.getStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取下载注册表统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出下载注册表
|
||||
* GET /api/download/registry/export
|
||||
@@ -1032,14 +1054,63 @@ router.post('/registry/rebuild', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const fileManager = downloadService.fileManager;
|
||||
const result = await downloadService.downloadRegistry.rebuildFromFileSystem(fileManager);
|
||||
|
||||
// 生成任务ID
|
||||
const taskId = `registry-rebuild-${Date.now()}`;
|
||||
|
||||
// 立即返回任务ID,不等待完成
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
data: {
|
||||
taskId,
|
||||
status: 'started',
|
||||
message: '注册表重建任务已启动'
|
||||
}
|
||||
});
|
||||
|
||||
// 异步执行重建任务
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
// 设置任务状态为进行中
|
||||
global.registryRebuildTasks = global.registryRebuildTasks || new Map();
|
||||
global.registryRebuildTasks.set(taskId, {
|
||||
status: 'running',
|
||||
startTime: Date.now(),
|
||||
progress: {
|
||||
scannedArtists: 0,
|
||||
scannedArtworks: 0,
|
||||
addedArtworks: 0,
|
||||
skippedArtworks: 0,
|
||||
currentArtist: null
|
||||
}
|
||||
});
|
||||
|
||||
const result = await downloadService.downloadRegistry.rebuildFromFileSystem(fileManager, taskId);
|
||||
|
||||
// 更新任务状态为完成
|
||||
global.registryRebuildTasks.set(taskId, {
|
||||
status: 'completed',
|
||||
startTime: global.registryRebuildTasks.get(taskId).startTime,
|
||||
endTime: Date.now(),
|
||||
result: result
|
||||
});
|
||||
|
||||
logger.info(`注册表重建任务完成: ${taskId}`, result);
|
||||
} catch (error) {
|
||||
logger.error('重建下载注册表失败:', error);
|
||||
logger.error(`注册表重建任务失败: ${taskId}`, error);
|
||||
|
||||
// 更新任务状态为失败
|
||||
global.registryRebuildTasks.set(taskId, {
|
||||
status: 'failed',
|
||||
startTime: global.registryRebuildTasks.get(taskId).startTime,
|
||||
endTime: Date.now(),
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('启动注册表重建任务失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
@@ -1048,20 +1119,29 @@ router.post('/registry/rebuild', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取下载注册表统计信息
|
||||
* GET /api/download/registry/stats
|
||||
* 获取注册表重建任务状态
|
||||
* GET /api/download/registry/rebuild/status/:taskId
|
||||
*/
|
||||
router.get('/registry/stats', async (req, res) => {
|
||||
router.get('/registry/rebuild/status/:taskId', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const stats = await downloadService.downloadRegistry.getStats();
|
||||
const { taskId } = req.params;
|
||||
|
||||
global.registryRebuildTasks = global.registryRebuildTasks || new Map();
|
||||
const task = global.registryRebuildTasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '任务不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
data: task
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取下载注册表统计信息失败:', error);
|
||||
logger.error('获取注册表重建任务状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
@@ -1070,21 +1150,40 @@ router.get('/registry/stats', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 清理下载注册表
|
||||
* POST /api/download/registry/cleanup
|
||||
* 取消注册表重建任务
|
||||
* DELETE /api/download/registry/rebuild/:taskId
|
||||
*/
|
||||
router.post('/registry/cleanup', async (req, res) => {
|
||||
router.delete('/registry/rebuild/:taskId', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const fileManager = downloadService.fileManager;
|
||||
const result = await downloadService.downloadRegistry.cleanupRegistry(fileManager);
|
||||
const { taskId } = req.params;
|
||||
|
||||
global.registryRebuildTasks = global.registryRebuildTasks || new Map();
|
||||
const task = global.registryRebuildTasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '任务不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (task.status === 'running') {
|
||||
// 标记任务为已取消
|
||||
global.registryRebuildTasks.set(taskId, {
|
||||
...task,
|
||||
status: 'cancelled',
|
||||
endTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
data: {
|
||||
message: '任务已取消'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('清理下载注册表失败:', error);
|
||||
logger.error('取消注册表重建任务失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
|
||||
@@ -329,118 +329,95 @@ class DownloadRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件系统扫描并重建注册表
|
||||
* @param {Object} fileManager - 文件管理器实例
|
||||
* @returns {Object} 扫描结果统计
|
||||
* 从文件系统重建注册表
|
||||
* @param {FileManager} fileManager
|
||||
* @param {string} taskId - 任务ID,用于更新进度
|
||||
* @returns {Promise<{scannedArtists: number, scannedArtworks: number, addedArtworks: number, skippedArtworks: number}>}
|
||||
*/
|
||||
async rebuildFromFileSystem(fileManager) {
|
||||
try {
|
||||
logger.info('开始从文件系统扫描并添加新作品到注册表...');
|
||||
async rebuildFromFileSystem(fileManager, taskId = null) {
|
||||
logger.info('开始从文件系统重建下载注册表...');
|
||||
|
||||
if (!this.loaded) {
|
||||
await this.loadRegistry();
|
||||
}
|
||||
|
||||
let scannedArtists = 0;
|
||||
let scannedArtworks = 0;
|
||||
let addedArtworks = 0;
|
||||
let skippedArtworks = 0;
|
||||
|
||||
const downloadPath = await fileManager.getDownloadPath();
|
||||
logger.debug(`扫描下载路径: ${downloadPath}`);
|
||||
|
||||
const artists = await fileManager.listDirectory(downloadPath);
|
||||
logger.debug(`找到 ${artists.length} 个作者目录`);
|
||||
|
||||
for (const artist of artists) {
|
||||
try {
|
||||
const artistPath = path.join(downloadPath, artist);
|
||||
const artistStat = await fileManager.getFileInfo(artistPath);
|
||||
|
||||
if (artistStat.exists && artistStat.isDirectory) {
|
||||
scannedArtists++;
|
||||
logger.debug(`扫描作者: ${artist}`);
|
||||
|
||||
const artworks = await fileManager.listDirectory(artistPath);
|
||||
|
||||
for (const artwork of artworks) {
|
||||
try {
|
||||
const artworkPath = path.join(artistPath, artwork);
|
||||
const artworkStat = await fileManager.getFileInfo(artworkPath);
|
||||
|
||||
if (artworkStat.exists && artworkStat.isDirectory) {
|
||||
scannedArtworks++;
|
||||
|
||||
// 检查是否是作品目录(包含数字ID)
|
||||
const artworkMatch = artwork.match(/^(\d+)_(.+)$/);
|
||||
if (artworkMatch) {
|
||||
const artworkId = parseInt(artworkMatch[1]);
|
||||
|
||||
// 检查作品是否已经在注册表中
|
||||
const isAlreadyRegistered = await this.isArtworkDownloaded(artworkId);
|
||||
if (isAlreadyRegistered) {
|
||||
skippedArtworks++;
|
||||
continue; // 跳过已注册的作品
|
||||
}
|
||||
|
||||
// 检查作品信息文件和图片文件
|
||||
const infoPath = path.join(artworkPath, 'artwork_info.json');
|
||||
let artworkInfo;
|
||||
try {
|
||||
const infoContent = await fs.readFile(infoPath, 'utf8');
|
||||
artworkInfo = JSON.parse(infoContent);
|
||||
} catch (error) {
|
||||
logger.debug(`读取作品信息文件失败: ${infoPath}`, error);
|
||||
continue; // 跳过没有信息文件的目录
|
||||
}
|
||||
|
||||
// 检查是否有图片文件
|
||||
const files = await fileManager.listDirectory(artworkPath);
|
||||
const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
// 检查图片数量是否与artwork_info.json中记录的一致
|
||||
const expectedImageCount = artworkInfo.page_count || 1;
|
||||
if (imageFiles.length >= expectedImageCount) {
|
||||
// 添加到注册表(只添加新的)
|
||||
await this.addArtwork(artist, artworkId);
|
||||
addedArtworks++;
|
||||
logger.debug(`添加作品到注册表: ${artist} - ${artworkId}`);
|
||||
} else {
|
||||
logger.debug(`作品图片数量不足: ${artist} - ${artworkId}, 期望: ${expectedImageCount}, 实际: ${imageFiles.length}`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`作品目录无图片文件: ${artworkPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`处理作品目录 ${artwork} 时出错:`, error);
|
||||
continue; // 跳过有问题的作品目录
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`处理作者目录 ${artist} 时出错:`, error);
|
||||
continue; // 跳过有问题的作者目录
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
scannedArtists,
|
||||
scannedArtworks,
|
||||
addedArtworks,
|
||||
skippedArtworks,
|
||||
totalRegisteredArtists: Object.keys(this.registry.artists).length,
|
||||
totalRegisteredArtworks: this.getTotalArtworkCount()
|
||||
const stats = {
|
||||
scannedArtists: 0,
|
||||
scannedArtworks: 0,
|
||||
addedArtworks: 0,
|
||||
skippedArtworks: 0
|
||||
};
|
||||
|
||||
logger.info('注册表扫描完成', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('注册表扫描失败:', error);
|
||||
throw error;
|
||||
// 获取所有艺术家目录
|
||||
const artistDirs = await fileManager.getArtistDirectories();
|
||||
logger.info(`发现 ${artistDirs.length} 个艺术家目录`);
|
||||
|
||||
// 更新进度的辅助函数
|
||||
const updateProgress = (currentArtist = null) => {
|
||||
if (taskId && global.registryRebuildTasks) {
|
||||
const task = global.registryRebuildTasks.get(taskId);
|
||||
if (task && task.status === 'running') {
|
||||
global.registryRebuildTasks.set(taskId, {
|
||||
...task,
|
||||
progress: {
|
||||
...stats,
|
||||
currentArtist
|
||||
}
|
||||
});
|
||||
}
|
||||
// 检查是否被取消
|
||||
if (task && task.status === 'cancelled') {
|
||||
throw new Error('任务已被取消');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const artistDir of artistDirs) {
|
||||
try {
|
||||
stats.scannedArtists++;
|
||||
updateProgress(artistDir);
|
||||
|
||||
logger.info(`扫描艺术家目录: ${artistDir}`);
|
||||
|
||||
// 获取艺术家目录下的所有作品目录
|
||||
const artworkDirs = await fileManager.getArtworkDirectories(artistDir);
|
||||
|
||||
for (const artworkDir of artworkDirs) {
|
||||
try {
|
||||
stats.scannedArtworks++;
|
||||
updateProgress(artistDir);
|
||||
|
||||
// 检查作品是否已在注册表中
|
||||
const isRegistered = await this.isArtworkRegistered(artistDir, artworkDir);
|
||||
|
||||
if (!isRegistered) {
|
||||
// 获取作品信息并添加到注册表
|
||||
const artworkInfo = await fileManager.getArtworkInfo(artistDir, artworkDir);
|
||||
if (artworkInfo) {
|
||||
await this.addArtwork(artistDir, artworkDir, artworkInfo);
|
||||
stats.addedArtworks++;
|
||||
logger.debug(`添加作品到注册表: ${artistDir}/${artworkDir}`);
|
||||
}
|
||||
} else {
|
||||
stats.skippedArtworks++;
|
||||
}
|
||||
|
||||
// 每处理10个作品更新一次进度
|
||||
if (stats.scannedArtworks % 10 === 0) {
|
||||
updateProgress(artistDir);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.warn(`处理作品目录失败 ${artistDir}/${artworkDir}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.warn(`处理艺术家目录失败 ${artistDir}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 最终更新进度
|
||||
updateProgress(null);
|
||||
|
||||
logger.info('从文件系统重建下载注册表完成', stats);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -639,6 +639,64 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有艺术家目录
|
||||
*/
|
||||
async getArtistDirectories() {
|
||||
try {
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
if (!await this.directoryExists(downloadPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await this.listDirectory(downloadPath);
|
||||
const artistDirs = [];
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(downloadPath, item);
|
||||
const stat = await fs.stat(itemPath);
|
||||
if (stat.isDirectory()) {
|
||||
artistDirs.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return artistDirs;
|
||||
} catch (error) {
|
||||
logger.error('获取艺术家目录失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定艺术家目录下的所有作品目录
|
||||
*/
|
||||
async getArtworkDirectories(artistName) {
|
||||
try {
|
||||
const downloadPath = await this.getDownloadPath();
|
||||
const artistPath = path.join(downloadPath, artistName);
|
||||
|
||||
if (!await this.directoryExists(artistPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await this.listDirectory(artistPath);
|
||||
const artworkDirs = [];
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(artistPath, item);
|
||||
const stat = await fs.stat(itemPath);
|
||||
if (stat.isDirectory()) {
|
||||
artworkDirs.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return artworkDirs;
|
||||
} catch (error) {
|
||||
logger.error(`获取艺术家 ${artistName} 的作品目录失败:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目录是否存在
|
||||
*/
|
||||
|
||||
+185
-17
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDownloadStore } from '@/stores/download'
|
||||
@@ -19,11 +19,24 @@ const updateStore = useUpdateStore()
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
const username = computed(() => authStore.username)
|
||||
|
||||
// 移动端菜单状态
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
// 在下载管理页面隐藏下载进度小组件
|
||||
const showDownloadWidget = computed(() => {
|
||||
return isLoggedIn.value && route.path !== '/downloads'
|
||||
})
|
||||
|
||||
// 切换移动端菜单
|
||||
const toggleMobileMenu = () => {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
// 关闭移动端菜单
|
||||
const closeMobileMenu = () => {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.fetchLoginStatus()
|
||||
|
||||
@@ -47,13 +60,19 @@ onMounted(async () => {
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<RouterLink to="/" class="brand-link">
|
||||
<RouterLink to="/" class="brand-link" @click="closeMobileMenu">
|
||||
<SvgIcon name="home2" class="brand-icon" />
|
||||
<span class="brand-text">Pixiv Manager</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<!-- 移动端菜单切换按钮 -->
|
||||
<button class="mobile-nav-toggle" @click="toggleMobileMenu" :class="{ active: isMobileMenuOpen }">
|
||||
<SvgIcon name="menu" class="menu-icon" />
|
||||
</button>
|
||||
|
||||
<!-- 桌面端导航菜单 -->
|
||||
<div class="nav-menu desktop-nav">
|
||||
<RouterLink to="/" class="nav-link">首页</RouterLink>
|
||||
<RouterLink to="/search" class="nav-link" v-if="isLoggedIn">搜索</RouterLink>
|
||||
<RouterLink to="/ranking" class="nav-link" v-if="isLoggedIn">排行榜</RouterLink>
|
||||
@@ -63,7 +82,8 @@ onMounted(async () => {
|
||||
<RouterLink to="/bookmarks" class="nav-link" v-if="isLoggedIn">我的收藏</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-auth">
|
||||
<!-- 桌面端用户信息 -->
|
||||
<div class="nav-auth desktop-nav">
|
||||
<div v-if="isLoggedIn" class="user-info">
|
||||
<span class="username">{{ username }}</span>
|
||||
<button @click="authStore.logout" class="btn btn-text">登出</button>
|
||||
@@ -80,6 +100,31 @@ onMounted(async () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div class="mobile-nav-menu" :class="{ active: isMobileMenuOpen }">
|
||||
<div class="mobile-nav-content">
|
||||
<!-- 移动端导航链接 -->
|
||||
<div class="mobile-nav-links">
|
||||
<RouterLink to="/" class="mobile-nav-item" @click="closeMobileMenu">首页</RouterLink>
|
||||
<RouterLink to="/search" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">搜索</RouterLink>
|
||||
<RouterLink to="/ranking" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">排行榜</RouterLink>
|
||||
<RouterLink to="/downloads" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">下载管理</RouterLink>
|
||||
<RouterLink to="/artists" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">作者管理</RouterLink>
|
||||
<RouterLink to="/repository" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">仓库管理</RouterLink>
|
||||
<RouterLink to="/bookmarks" class="mobile-nav-item" v-if="isLoggedIn" @click="closeMobileMenu">我的收藏</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- 移动端用户信息 -->
|
||||
<div class="mobile-user-section">
|
||||
<div v-if="isLoggedIn" class="mobile-user-info">
|
||||
<div class="mobile-username">{{ username }}</div>
|
||||
<button @click="authStore.logout(); closeMobileMenu()" class="btn-mobile">登出</button>
|
||||
</div>
|
||||
<RouterLink v-else to="/login" class="btn-mobile btn-mobile-primary" @click="closeMobileMenu">登录</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
@@ -120,7 +165,7 @@ onMounted(async () => {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
z-index: 1002; /* 提高导航栏z-index,确保在所有元素之上 */
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
@@ -131,18 +176,19 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 4rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1003;
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
text-decoration: none;
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
@@ -159,8 +205,34 @@ onMounted(async () => {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
/* 移动端菜单切换按钮 */
|
||||
.mobile-nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.2s;
|
||||
z-index: 1003; /* 确保汉堡菜单按钮在最上层 */
|
||||
}
|
||||
|
||||
.mobile-nav-toggle:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 桌面端导航 */
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@@ -183,7 +255,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.nav-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -199,6 +270,76 @@ onMounted(async () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 移动端菜单 */
|
||||
.mobile-nav-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1001; /* 提高z-index,确保在小组件之上 */
|
||||
}
|
||||
|
||||
.mobile-nav-menu.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.mobile-nav-content {
|
||||
padding: 1rem 2rem 2rem;
|
||||
}
|
||||
|
||||
.mobile-nav-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
text-decoration: none;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mobile-nav-item:hover {
|
||||
background: #f3f4f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mobile-nav-item.router-link-active {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mobile-user-section {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -281,30 +422,57 @@ onMounted(async () => {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
padding: 0 1rem;
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
gap: 1rem;
|
||||
/* 隐藏桌面端导航 */
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 显示移动端菜单切换按钮 */
|
||||
.mobile-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
.mobile-nav-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
margin-left: 0.25rem;
|
||||
.footer-container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.nav-container {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
.brand-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.mobile-nav-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const navigationIcons = {
|
||||
'bookmark-empty': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
|
||||
'eye': 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z',
|
||||
'folder': 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z',
|
||||
'settings': 'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z',
|
||||
'settings': 'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.82,11.69,4.82,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z',
|
||||
'update': 'M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z',
|
||||
'update-spinning': 'M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z',
|
||||
'info': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
|
||||
@@ -17,4 +17,5 @@ export const navigationIcons = {
|
||||
'cleanup-history': 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
|
||||
'cleanup-history2': 'M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z',
|
||||
'loading': 'M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z',
|
||||
'menu': 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z',
|
||||
}
|
||||
@@ -75,6 +75,19 @@ html, body {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 移动端按钮样式 */
|
||||
.btn-mobile {
|
||||
padding: 0.875rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
min-height: 44px; /* 符合移动端触摸标准 */
|
||||
}
|
||||
|
||||
.btn-mobile-sm {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* 全局容器样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
@@ -82,11 +95,201 @@ html, body {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* 移动端导航相关样式 */
|
||||
.mobile-nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-nav-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.mobile-nav-menu.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: block;
|
||||
padding: 1rem 2rem;
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mobile-nav-item:hover {
|
||||
background: #f8fafc;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mobile-nav-item.router-link-active {
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
/* 移动端用户信息 */
|
||||
.mobile-user-section {
|
||||
padding: 1.5rem 2rem;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.mobile-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.mobile-auth-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* 显示移动端导航切换按钮 */
|
||||
.mobile-nav-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 隐藏桌面端导航菜单 */
|
||||
.desktop-nav-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 移动端特定的间距调整 */
|
||||
.mobile-spacing {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.mobile-spacing-sm {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-spacing-lg {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* 移动端文本大小调整 */
|
||||
.mobile-text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mobile-text-base {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mobile-text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* 移动端布局工具类 */
|
||||
.mobile-stack {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.mobile-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-show {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 移动端卡片样式 */
|
||||
.mobile-card {
|
||||
margin: 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 移动端表单样式 */
|
||||
.mobile-form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile-input {
|
||||
width: 100%;
|
||||
padding: 0.875rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* 移动端网格布局 */
|
||||
.mobile-grid-1 {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.mobile-grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
|
||||
.mobile-user-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn-mobile {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
|
||||
@@ -120,21 +120,21 @@
|
||||
<div class="action-group">
|
||||
<div class="action-group-title">基础操作</div>
|
||||
<div class="action-buttons basic-actions">
|
||||
<button @click="refreshStats" class="btn btn-enhanced btn-secondary" :disabled="loading">
|
||||
<button @click="refreshStats" class="btn btn-enhanced btn-secondary" :disabled="loading || isRebuildingRegistry">
|
||||
<SvgIcon name="refresh" class="btn-icon" />
|
||||
刷新统计
|
||||
</button>
|
||||
|
||||
<button @click="exportRegistry" class="btn btn-enhanced btn-primary" :disabled="loading">
|
||||
<button @click="exportRegistry" class="btn btn-enhanced btn-primary" :disabled="loading || isRebuildingRegistry">
|
||||
<SvgIcon name="download" class="btn-icon" />
|
||||
导出注册表
|
||||
</button>
|
||||
|
||||
<label class="btn btn-enhanced btn-primary" :class="{ disabled: loading }">
|
||||
<label class="btn btn-enhanced btn-primary" :class="{ disabled: loading || isRebuildingRegistry }">
|
||||
<SvgIcon name="upload" class="btn-icon" />
|
||||
导入注册表
|
||||
<input type="file" @change="handleFileImport" accept=".json" style="display: none;"
|
||||
:disabled="loading" />
|
||||
:disabled="loading || isRebuildingRegistry" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,17 +143,65 @@
|
||||
<div class="action-group">
|
||||
<div class="action-group-title">高级操作</div>
|
||||
<div class="action-buttons advanced-actions">
|
||||
<button @click="rebuildRegistry" class="btn btn-enhanced btn-warning" :disabled="loading">
|
||||
<button @click="rebuildRegistry" class="btn btn-enhanced btn-warning" :disabled="loading || isRebuildingRegistry">
|
||||
<SvgIcon name="rebuild" class="btn-icon" />
|
||||
同步文件系统
|
||||
{{ isRebuildingRegistry ? '同步中...' : '同步文件系统' }}
|
||||
</button>
|
||||
|
||||
<button @click="cleanupRegistry" class="btn btn-enhanced btn-danger" :disabled="loading">
|
||||
<button @click="cleanupRegistry" class="btn btn-enhanced btn-danger" :disabled="loading || isRebuildingRegistry">
|
||||
<SvgIcon name="clean" class="btn-icon" />
|
||||
清理注册表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册表重建进度显示 -->
|
||||
<div v-if="isRebuildingRegistry" class="rebuild-progress">
|
||||
<div class="progress-header">
|
||||
<h4>文件系统同步进度</h4>
|
||||
<button @click="cancelRebuild" class="btn btn-small btn-danger" title="取消同步">
|
||||
<SvgIcon name="close" class="btn-icon" />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-content">
|
||||
<div class="progress-stats">
|
||||
<div class="progress-stat">
|
||||
<span class="stat-label">已扫描艺术家:</span>
|
||||
<span class="stat-value">{{ rebuildProgress.scannedArtists || 0 }}</span>
|
||||
</div>
|
||||
<div class="progress-stat">
|
||||
<span class="stat-label">已扫描作品:</span>
|
||||
<span class="stat-value">{{ rebuildProgress.scannedArtworks || 0 }}</span>
|
||||
</div>
|
||||
<div class="progress-stat">
|
||||
<span class="stat-label">新增作品:</span>
|
||||
<span class="stat-value">{{ rebuildProgress.addedArtworks || 0 }}</span>
|
||||
</div>
|
||||
<div class="progress-stat">
|
||||
<span class="stat-label">跳过作品:</span>
|
||||
<span class="stat-value">{{ rebuildProgress.skippedArtworks || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rebuildProgress.currentArtist" class="current-status">
|
||||
<span class="status-label">当前处理:</span>
|
||||
<span class="status-value">{{ rebuildProgress.currentArtist }}</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ progressPercentage.toFixed(1) }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="time-info">
|
||||
<span class="elapsed-time">已用时: {{ formatElapsedTime(rebuildStartTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,9 +209,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { ref, onMounted, watch, computed, onUnmounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRegistryStore } from '@/stores/registry';
|
||||
import downloadService from '@/services/download';
|
||||
import SvgIcon from './SvgIcon.vue';
|
||||
import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import ErrorMessage from './ErrorMessage.vue';
|
||||
@@ -178,6 +227,27 @@ const { stats, loading, error, config } = storeToRefs(registryStore);
|
||||
// 检测方法选择 - 不设置默认值,等待从后端配置初始化
|
||||
const detectionMethod = ref<'registry' | 'scan' | 'hybrid'>();
|
||||
|
||||
// 重建进度相关状态
|
||||
const isRebuildingRegistry = ref(false);
|
||||
const rebuildTaskId = ref<string | null>(null);
|
||||
const rebuildProgress = ref({
|
||||
scannedArtists: 0,
|
||||
scannedArtworks: 0,
|
||||
addedArtworks: 0,
|
||||
skippedArtworks: 0,
|
||||
currentArtist: ''
|
||||
});
|
||||
const rebuildStartTime = ref<number>(0);
|
||||
const progressPollingInterval = ref<number | null>(null);
|
||||
|
||||
// 计算进度百分比
|
||||
const progressPercentage = computed(() => {
|
||||
const total = rebuildProgress.value.scannedArtworks;
|
||||
const processed = rebuildProgress.value.addedArtworks + rebuildProgress.value.skippedArtworks;
|
||||
if (total === 0) return 0;
|
||||
return Math.min((processed / total) * 100, 100);
|
||||
});
|
||||
|
||||
// 切换面板显示
|
||||
const togglePanel = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
@@ -214,16 +284,138 @@ const handleFileImport = async (event: Event) => {
|
||||
target.value = '';
|
||||
};
|
||||
|
||||
// 重建注册表
|
||||
// 重建注册表 - 使用新的异步API
|
||||
const rebuildRegistry = async () => {
|
||||
if (!confirm('确定要同步文件系统到注册表吗?这将扫描整个下载目录并添加新发现的作品,可能需要一些时间。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await registryStore.rebuildRegistry();
|
||||
if (result.success) {
|
||||
showSuccess(`文件系统同步完成,新增 ${result.data?.addedArtworks || 0} 个作品,跳过 ${result.data?.skippedArtworks || 0} 个已存在作品`);
|
||||
try {
|
||||
// 启动异步重建任务
|
||||
const result = await downloadService.rebuildRegistry();
|
||||
if (result.success && result.data?.taskId) {
|
||||
rebuildTaskId.value = result.data.taskId;
|
||||
isRebuildingRegistry.value = true;
|
||||
rebuildStartTime.value = Date.now();
|
||||
|
||||
// 重置进度
|
||||
rebuildProgress.value = {
|
||||
scannedArtists: 0,
|
||||
scannedArtworks: 0,
|
||||
addedArtworks: 0,
|
||||
skippedArtworks: 0,
|
||||
currentArtist: ''
|
||||
};
|
||||
|
||||
// 开始轮询进度
|
||||
startProgressPolling();
|
||||
showSuccess('文件系统同步已开始,请等待完成...');
|
||||
} else {
|
||||
throw new Error(result.error || '启动同步任务失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动重建任务失败:', error);
|
||||
showError('启动文件系统同步失败: ' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始轮询进度
|
||||
const startProgressPolling = () => {
|
||||
if (progressPollingInterval.value) {
|
||||
clearInterval(progressPollingInterval.value);
|
||||
}
|
||||
|
||||
progressPollingInterval.value = setInterval(async () => {
|
||||
if (!rebuildTaskId.value) return;
|
||||
|
||||
try {
|
||||
const statusResult = await downloadService.getRegistryRebuildStatus(rebuildTaskId.value);
|
||||
if (statusResult.success && statusResult.data) {
|
||||
const status = statusResult.data;
|
||||
|
||||
// 更新进度信息
|
||||
if (status.progress) {
|
||||
rebuildProgress.value = {
|
||||
scannedArtists: status.progress.scannedArtists || 0,
|
||||
scannedArtworks: status.progress.scannedArtworks || 0,
|
||||
addedArtworks: status.progress.addedArtworks || 0,
|
||||
skippedArtworks: status.progress.skippedArtworks || 0,
|
||||
currentArtist: status.progress.currentArtist || ''
|
||||
};
|
||||
}
|
||||
|
||||
// 检查任务状态
|
||||
if (status.status === 'completed') {
|
||||
stopProgressPolling();
|
||||
isRebuildingRegistry.value = false;
|
||||
rebuildTaskId.value = null;
|
||||
|
||||
const addedCount = rebuildProgress.value.addedArtworks;
|
||||
const skippedCount = rebuildProgress.value.skippedArtworks;
|
||||
showSuccess(`文件系统同步完成!新增 ${addedCount} 个作品,跳过 ${skippedCount} 个已存在作品`);
|
||||
|
||||
// 刷新统计信息
|
||||
refreshStats();
|
||||
} else if (status.status === 'failed') {
|
||||
stopProgressPolling();
|
||||
isRebuildingRegistry.value = false;
|
||||
rebuildTaskId.value = null;
|
||||
showError('文件系统同步失败: ' + (status.error || '未知错误'));
|
||||
} else if (status.status === 'cancelled') {
|
||||
stopProgressPolling();
|
||||
isRebuildingRegistry.value = false;
|
||||
rebuildTaskId.value = null;
|
||||
showSuccess('文件系统同步已取消');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取重建进度失败:', error);
|
||||
// 不立即停止轮询,可能是临时网络问题
|
||||
}
|
||||
}, 2000); // 每2秒轮询一次
|
||||
};
|
||||
|
||||
// 停止轮询进度
|
||||
const stopProgressPolling = () => {
|
||||
if (progressPollingInterval.value) {
|
||||
clearInterval(progressPollingInterval.value);
|
||||
progressPollingInterval.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消重建任务
|
||||
const cancelRebuild = async () => {
|
||||
if (!rebuildTaskId.value) return;
|
||||
|
||||
if (!confirm('确定要取消文件系统同步吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await downloadService.cancelRegistryRebuild(rebuildTaskId.value);
|
||||
if (result.success) {
|
||||
stopProgressPolling();
|
||||
isRebuildingRegistry.value = false;
|
||||
rebuildTaskId.value = null;
|
||||
showSuccess('文件系统同步已取消');
|
||||
} else {
|
||||
showError('取消同步失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消重建任务失败:', error);
|
||||
showError('取消同步失败: ' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化已用时间
|
||||
const formatElapsedTime = (startTime: number): string => {
|
||||
if (!startTime) return '00:00';
|
||||
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 清理注册表
|
||||
@@ -293,6 +485,11 @@ const showSuccess = (message: string) => {
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 显示错误消息
|
||||
const showError = (message: string) => {
|
||||
registryStore.error = message;
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString?: string): string => {
|
||||
if (!dateString) return '未知';
|
||||
@@ -326,6 +523,11 @@ onMounted(async () => {
|
||||
refreshStats();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
stopProgressPolling();
|
||||
});
|
||||
|
||||
// 监听配置变化,自动更新检测方法
|
||||
watch(config, () => {
|
||||
initDetectionMethod();
|
||||
@@ -1241,4 +1443,322 @@ watch(config, () => {
|
||||
color: var(--color-text-tertiary, #9ca3af);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 重建进度样式 */
|
||||
.rebuild-progress {
|
||||
background: var(--color-bg-secondary, #f8fafc);
|
||||
border: 2px solid var(--color-warning-border, #fcd34d);
|
||||
border-radius: var(--radius-lg, 0.5rem);
|
||||
padding: var(--spacing-lg, 1rem);
|
||||
margin-top: var(--spacing-lg, 1rem);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.rebuild-progress::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--color-warning), var(--color-info), var(--color-success));
|
||||
animation: progressGlow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes progressGlow {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg, 1rem);
|
||||
padding-bottom: var(--spacing-sm, 0.5rem);
|
||||
border-bottom: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.progress-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary, #374151);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.progress-header h4::before {
|
||||
content: '⚡';
|
||||
font-size: 1.125rem;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
|
||||
font-size: 0.75rem;
|
||||
border-radius: var(--radius-sm, 0.25rem);
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md, 0.75rem);
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
background: var(--color-bg-primary, white);
|
||||
padding: var(--spacing-md, 0.75rem);
|
||||
border-radius: var(--radius-md, 0.375rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.progress-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-xs, 0.25rem);
|
||||
}
|
||||
|
||||
.progress-stat .stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-text-secondary, #6b7280);
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--spacing-xs, 0.25rem);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.progress-stat .stat-value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary, #3b82f6);
|
||||
font-family: var(--font-mono, 'Courier New', monospace);
|
||||
background: linear-gradient(135deg, var(--color-primary-light, #eff6ff), transparent);
|
||||
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
|
||||
border-radius: var(--radius-sm, 0.25rem);
|
||||
border: 1px solid var(--color-primary-light, #dbeafe);
|
||||
min-width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-status {
|
||||
background: var(--color-bg-primary, white);
|
||||
padding: var(--spacing-md, 0.75rem);
|
||||
border-radius: var(--radius-md, 0.375rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary, #6b7280);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary, #374151);
|
||||
font-family: var(--font-mono, 'Courier New', monospace);
|
||||
background: var(--color-bg-tertiary, #f8fafc);
|
||||
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
|
||||
border-radius: var(--radius-sm, 0.25rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md, 0.75rem);
|
||||
background: var(--color-bg-primary, white);
|
||||
padding: var(--spacing-md, 0.75rem);
|
||||
border-radius: var(--radius-md, 0.375rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 1rem;
|
||||
background: var(--color-bg-tertiary, #f1f5f9);
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.progress-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0.1) 100%);
|
||||
animation: shimmer 2s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
var(--color-success, #16a34a) 0%,
|
||||
var(--color-info, #06b6d4) 50%,
|
||||
var(--color-warning, #f59e0b) 100%);
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
transition: width 0.5s ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
animation: progressShine 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progressShine {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary, #374151);
|
||||
font-family: var(--font-mono, 'Courier New', monospace);
|
||||
min-width: 3rem;
|
||||
text-align: right;
|
||||
background: var(--color-bg-tertiary, #f8fafc);
|
||||
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
|
||||
border-radius: var(--radius-sm, 0.25rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg-primary, white);
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 0.75rem);
|
||||
border-radius: var(--radius-md, 0.375rem);
|
||||
border: 1px solid var(--color-border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary, #6b7280);
|
||||
font-family: var(--font-mono, 'Courier New', monospace);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs, 0.25rem);
|
||||
}
|
||||
|
||||
.elapsed-time::before {
|
||||
content: '⏱️';
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 进度显示 */
|
||||
@media (max-width: 768px) {
|
||||
.progress-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-xs, 0.25rem);
|
||||
}
|
||||
|
||||
.progress-stat {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.progress-stat .stat-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.progress-stat .stat-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.current-status {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs, 0.25rem);
|
||||
}
|
||||
|
||||
.status-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.rebuild-progress {
|
||||
padding: var(--spacing-md, 0.75rem);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm, 0.5rem);
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -276,12 +276,26 @@ class DownloadService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建下载注册表
|
||||
* 重建下载注册表(异步任务)
|
||||
*/
|
||||
async rebuildRegistry() {
|
||||
return apiService.post('/api/download/registry/rebuild');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表重建任务状态
|
||||
*/
|
||||
async getRegistryRebuildStatus(taskId: string) {
|
||||
return apiService.get(`/api/download/registry/rebuild/status/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册表重建任务
|
||||
*/
|
||||
async cancelRegistryRebuild(taskId: string) {
|
||||
return apiService.delete(`/api/download/registry/rebuild/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理下载注册表
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user