修复磁盘不能显示问题,ui增加仓库图标
This commit is contained in:
@@ -18,6 +18,9 @@ class ConfigManager {
|
||||
this.configDir = path.join(__dirname, 'user-config.json')
|
||||
}
|
||||
|
||||
// 确保配置目录存在
|
||||
this.ensureConfigDir()
|
||||
|
||||
this.defaultConfig = {
|
||||
downloadDir: "./downloads",
|
||||
fileStructure: "artist/artwork",
|
||||
@@ -30,6 +33,21 @@ class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保配置目录存在
|
||||
*/
|
||||
ensureConfigDir() {
|
||||
try {
|
||||
const configDirPath = path.dirname(this.configDir)
|
||||
if (!require('fs').existsSync(configDirPath)) {
|
||||
require('fs').mkdirSync(configDirPath, { recursive: true })
|
||||
console.log('配置目录创建成功:', configDirPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建配置目录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置文件
|
||||
* 如果配置文件不存在,则创建默认配置
|
||||
@@ -55,14 +73,27 @@ class ConfigManager {
|
||||
const configDirPath = path.dirname(this.configDir)
|
||||
await fs.mkdir(configDirPath, { recursive: true })
|
||||
|
||||
// 写入默认配置
|
||||
await fs.writeFile(
|
||||
this.configDir,
|
||||
JSON.stringify(this.defaultConfig, null, 2),
|
||||
'utf8'
|
||||
)
|
||||
// 检查目录是否创建成功
|
||||
try {
|
||||
await fs.access(configDirPath)
|
||||
console.log('配置目录确认存在:', configDirPath)
|
||||
} catch (accessError) {
|
||||
console.error('配置目录访问失败:', accessError)
|
||||
throw new Error(`无法访问配置目录: ${configDirPath}`)
|
||||
}
|
||||
|
||||
// 写入默认配置
|
||||
const configContent = JSON.stringify(this.defaultConfig, null, 2)
|
||||
await fs.writeFile(this.configDir, configContent, 'utf8')
|
||||
|
||||
// 验证文件是否写入成功
|
||||
try {
|
||||
await fs.access(this.configDir)
|
||||
console.log('默认配置文件创建成功:', this.configDir)
|
||||
} catch (verifyError) {
|
||||
console.error('配置文件验证失败:', verifyError)
|
||||
throw new Error('配置文件创建后无法访问')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建默认配置文件失败:', error)
|
||||
throw error
|
||||
@@ -74,12 +105,30 @@ class ConfigManager {
|
||||
*/
|
||||
async readConfig() {
|
||||
try {
|
||||
// 首先检查文件是否存在
|
||||
const exists = await this.configExists()
|
||||
if (!exists) {
|
||||
console.log('配置文件不存在,创建默认配置...')
|
||||
await this.createDefaultConfig()
|
||||
}
|
||||
|
||||
const configData = await fs.readFile(this.configDir, 'utf8')
|
||||
return JSON.parse(configData)
|
||||
const config = JSON.parse(configData)
|
||||
|
||||
// 合并默认配置,确保所有必要的字段都存在
|
||||
return { ...this.defaultConfig, ...config }
|
||||
} catch (error) {
|
||||
console.error('读取配置文件失败:', error)
|
||||
// 如果读取失败,返回默认配置
|
||||
console.log('使用默认配置...')
|
||||
// 如果读取失败,尝试创建默认配置
|
||||
try {
|
||||
await this.createDefaultConfig()
|
||||
return { ...this.defaultConfig }
|
||||
} catch (createError) {
|
||||
console.error('创建默认配置也失败:', createError)
|
||||
// 最后返回内存中的默认配置
|
||||
return { ...this.defaultConfig }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,24 @@ router.post('/config/reset', async (req, res) => {
|
||||
// 获取仓库统计信息
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = await repositoryService.getStats()
|
||||
const { forceRefresh } = req.query
|
||||
const stats = await repositoryService.getStats(forceRefresh === 'true')
|
||||
res.json(ResponseUtil.success(stats))
|
||||
} catch (error) {
|
||||
res.status(500).json(ResponseUtil.error(error.message))
|
||||
}
|
||||
})
|
||||
|
||||
// 清除磁盘使用情况缓存
|
||||
router.post('/stats/clear-cache', async (req, res) => {
|
||||
try {
|
||||
const result = await repositoryService.clearDiskUsageCache()
|
||||
res.json(ResponseUtil.success(result))
|
||||
} catch (error) {
|
||||
res.status(500).json(ResponseUtil.error(error.message))
|
||||
}
|
||||
})
|
||||
|
||||
// 获取所有作者列表
|
||||
router.get('/artists', async (req, res) => {
|
||||
try {
|
||||
|
||||
+317
-19
@@ -10,6 +10,16 @@ class RepositoryService {
|
||||
// 初始化配置管理器
|
||||
this.configManager = new ConfigManager()
|
||||
this.config = null
|
||||
|
||||
// 磁盘使用情况缓存
|
||||
this.diskUsageCache = {
|
||||
data: null,
|
||||
timestamp: 0,
|
||||
cacheDuration: 5 * 60 * 1000 // 5分钟缓存
|
||||
}
|
||||
|
||||
// 缓存文件路径
|
||||
this.cacheFilePath = null
|
||||
}
|
||||
|
||||
// 获取当前工作目录(基于配置)
|
||||
@@ -37,6 +47,12 @@ class RepositoryService {
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
await fs.mkdir(currentBaseDir, { recursive: true })
|
||||
|
||||
// 初始化缓存文件路径
|
||||
this.cacheFilePath = path.join(path.dirname(this.configManager.getConfigPath()), 'disk-usage-cache.json')
|
||||
|
||||
// 加载持久化缓存
|
||||
await this.loadPersistentCache()
|
||||
|
||||
return { success: true, message: '仓库初始化成功' }
|
||||
} catch (error) {
|
||||
throw new Error(`仓库初始化失败: ${error.message}`)
|
||||
@@ -49,8 +65,17 @@ class RepositoryService {
|
||||
this.config = await this.configManager.readConfig()
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
// 如果加载失败,使用默认配置
|
||||
this.config = await this.configManager.readConfig()
|
||||
// 如果加载失败,使用默认配置对象
|
||||
this.config = {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +112,14 @@ class RepositoryService {
|
||||
}
|
||||
|
||||
// 获取仓库统计信息
|
||||
async getStats() {
|
||||
async getStats(forceRefresh = false) {
|
||||
try {
|
||||
const stats = await this.scanRepository()
|
||||
return {
|
||||
totalArtworks: stats.artworks.length,
|
||||
totalArtists: stats.artists.length,
|
||||
totalSize: stats.totalSize,
|
||||
diskUsage: await this.getDiskUsage(),
|
||||
diskUsage: await this.getDiskUsage(forceRefresh),
|
||||
lastScan: new Date().toISOString()
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -219,11 +244,15 @@ class RepositoryService {
|
||||
}
|
||||
|
||||
// 获取磁盘使用情况
|
||||
async getDiskUsage() {
|
||||
async getDiskUsage(forceRefresh = false) {
|
||||
try {
|
||||
const currentBaseDir = this.getCurrentBaseDir()
|
||||
|
||||
// 尝试使用 fs.statfs (Node.js 内置方法)
|
||||
// 检查是否在打包环境中
|
||||
const isPkg = process.pkg !== undefined
|
||||
|
||||
// 尝试使用 fs.statfs (Node.js 内置方法) - 最快的方法
|
||||
if (!isPkg && typeof fs.statfs === 'function') {
|
||||
try {
|
||||
const stats = await fs.statfs(currentBaseDir)
|
||||
const total = stats.blocks * stats.bsize
|
||||
@@ -237,19 +266,64 @@ class RepositoryService {
|
||||
usagePercent: Math.round((used / total) * 100)
|
||||
}
|
||||
} catch (statfsError) {
|
||||
console.log('fs.statfs 不可用,尝试使用系统命令:', statfsError.message)
|
||||
console.log('fs.statfs 调用失败:', statfsError.message)
|
||||
}
|
||||
} else {
|
||||
console.log('fs.statfs 在打包环境中不可用,尝试使用系统命令')
|
||||
}
|
||||
|
||||
// 如果 fs.statfs 不可用,尝试使用系统命令
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 系统
|
||||
// Windows 系统 - 优先使用最快的命令
|
||||
const methods = [
|
||||
// 方法1: 使用 PowerShell (通常比 wmic 快)
|
||||
async () => {
|
||||
try {
|
||||
const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption')
|
||||
// 设置超时,避免长时间等待
|
||||
const { stdout } = await Promise.race([
|
||||
execAsync('powershell "Get-WmiObject -Class Win32_LogicalDisk | Select-Object Size,FreeSpace,Caption | ConvertTo-Json"'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('PowerShell 超时')), 3000))
|
||||
])
|
||||
|
||||
const disks = JSON.parse(stdout)
|
||||
const diskArray = Array.isArray(disks) ? disks : [disks]
|
||||
|
||||
for (const disk of diskArray) {
|
||||
const caption = disk.Caption
|
||||
const freeSpace = parseInt(disk.FreeSpace)
|
||||
const totalSize = parseInt(disk.Size)
|
||||
|
||||
// 检查当前目录是否在这个磁盘上
|
||||
if (currentBaseDir.toUpperCase().startsWith(caption.toUpperCase())) {
|
||||
const used = totalSize - freeSpace
|
||||
return {
|
||||
total: totalSize,
|
||||
used,
|
||||
free: freeSpace,
|
||||
usagePercent: Math.round((used / totalSize) * 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('PowerShell 方法失败:', error.message)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 方法2: 使用 wmic (备用方案)
|
||||
async () => {
|
||||
try {
|
||||
const { stdout } = await Promise.race([
|
||||
execAsync('wmic logicaldisk get size,freespace,caption /format:csv'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('wmic 超时')), 3000))
|
||||
])
|
||||
|
||||
const lines = stdout.trim().split('\n').slice(1) // 跳过标题行
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const parts = line.split(',')
|
||||
if (parts.length >= 3) {
|
||||
const caption = parts[0]
|
||||
const caption = parts[0].trim()
|
||||
const freeSpace = parseInt(parts[1])
|
||||
const totalSize = parseInt(parts[2])
|
||||
|
||||
@@ -265,13 +339,36 @@ class RepositoryService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (wmicError) {
|
||||
console.log('wmic 命令失败:', wmicError.message)
|
||||
} catch (error) {
|
||||
console.log('wmic 方法失败:', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 尝试所有方法,但限制总时间
|
||||
for (const method of methods) {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
method(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('磁盘信息获取超时')), 5000))
|
||||
])
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`磁盘使用情况获取方法失败:`, error.message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unix/Linux 系统
|
||||
try {
|
||||
const { stdout } = await execAsync(`df -B1 "${currentBaseDir}" | tail -1`)
|
||||
const { stdout } = await Promise.race([
|
||||
execAsync(`df -B1 "${currentBaseDir}" | tail -1`),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('df 命令超时')), 3000))
|
||||
])
|
||||
|
||||
const parts = stdout.trim().split(/\s+/)
|
||||
if (parts.length >= 4) {
|
||||
const total = parseInt(parts[1])
|
||||
@@ -290,13 +387,18 @@ class RepositoryService {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有方法都失败,返回默认值
|
||||
console.log('无法获取磁盘使用情况,返回默认值')
|
||||
return { total: 0, used: 0, free: 0, usagePercent: 0 }
|
||||
}
|
||||
// 如果系统命令都失败,返回基于缓存的估算值
|
||||
return await this.getCachedDiskUsage(currentBaseDir, forceRefresh)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取磁盘使用情况失败:', error)
|
||||
return { total: 0, used: 0, free: 0, usagePercent: 0 }
|
||||
return {
|
||||
total: 0,
|
||||
used: 0,
|
||||
free: 0,
|
||||
usagePercent: 0,
|
||||
note: '获取失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +727,202 @@ class RepositoryService {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载持久化缓存
|
||||
async loadPersistentCache() {
|
||||
try {
|
||||
if (!this.cacheFilePath) {
|
||||
return
|
||||
}
|
||||
|
||||
const cacheData = await fs.readFile(this.cacheFilePath, 'utf8')
|
||||
const cache = JSON.parse(cacheData)
|
||||
|
||||
// 检查缓存是否有效(24小时内)
|
||||
const now = Date.now()
|
||||
const cacheAge = now - cache.timestamp
|
||||
const maxCacheAge = 24 * 60 * 60 * 1000 // 24小时
|
||||
|
||||
if (cacheAge < maxCacheAge) {
|
||||
this.diskUsageCache.data = cache.data
|
||||
this.diskUsageCache.timestamp = cache.timestamp
|
||||
console.log('已加载持久化缓存,缓存年龄:', Math.round(cacheAge / 1000 / 60), '分钟')
|
||||
} else {
|
||||
console.log('持久化缓存已过期,将重新计算')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('加载持久化缓存失败,将使用内存缓存:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存持久化缓存
|
||||
async savePersistentCache() {
|
||||
try {
|
||||
if (!this.cacheFilePath || !this.diskUsageCache.data) {
|
||||
return
|
||||
}
|
||||
|
||||
const cacheData = {
|
||||
data: this.diskUsageCache.data,
|
||||
timestamp: this.diskUsageCache.timestamp,
|
||||
savedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
await fs.writeFile(this.cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf8')
|
||||
console.log('持久化缓存已保存')
|
||||
} catch (error) {
|
||||
console.error('保存持久化缓存失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除磁盘使用情况缓存
|
||||
async clearDiskUsageCache() {
|
||||
try {
|
||||
// 清除内存缓存
|
||||
this.diskUsageCache.data = null
|
||||
this.diskUsageCache.timestamp = 0
|
||||
|
||||
// 删除持久化缓存文件
|
||||
if (this.cacheFilePath) {
|
||||
try {
|
||||
await fs.unlink(this.cacheFilePath)
|
||||
console.log('持久化缓存文件已删除')
|
||||
} catch (error) {
|
||||
console.log('删除持久化缓存文件失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, message: '缓存已清除' }
|
||||
} catch (error) {
|
||||
throw new Error(`清除缓存失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存的磁盘使用情况
|
||||
async getCachedDiskUsage(currentBaseDir, forceRefresh = false) {
|
||||
const now = Date.now()
|
||||
|
||||
// 检查内存缓存是否有效(除非强制刷新)
|
||||
if (!forceRefresh && this.diskUsageCache.data &&
|
||||
(now - this.diskUsageCache.timestamp) < this.diskUsageCache.cacheDuration) {
|
||||
console.log('使用内存缓存的磁盘使用情况')
|
||||
return this.diskUsageCache.data
|
||||
}
|
||||
|
||||
// 缓存过期或不存在,计算新的值
|
||||
try {
|
||||
// 快速估算:只计算前几层目录
|
||||
const estimatedSize = await this.quickDirectorySizeEstimate(currentBaseDir)
|
||||
|
||||
// 基于估算值创建磁盘使用情况
|
||||
const estimatedTotal = Math.max(estimatedSize * 200, 100 * 1024 * 1024 * 1024) // 至少100GB
|
||||
const estimatedUsed = estimatedSize
|
||||
const estimatedFree = estimatedTotal - estimatedUsed
|
||||
|
||||
const result = {
|
||||
total: estimatedTotal,
|
||||
used: estimatedUsed,
|
||||
free: estimatedFree,
|
||||
usagePercent: Math.round((estimatedUsed / estimatedTotal) * 100),
|
||||
note: '基于目录估算(缓存5分钟)'
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
this.diskUsageCache.data = result
|
||||
this.diskUsageCache.timestamp = now
|
||||
|
||||
// 保存到持久化缓存
|
||||
await this.savePersistentCache()
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.log('快速估算失败,返回默认值:', error.message)
|
||||
return {
|
||||
total: 0,
|
||||
used: 0,
|
||||
free: 0,
|
||||
usagePercent: 0,
|
||||
note: '无法获取磁盘信息'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 快速目录大小估算(只计算前几层)
|
||||
async quickDirectorySizeEstimate(dirPath, maxDepth = 2, currentDepth = 0) {
|
||||
try {
|
||||
if (currentDepth >= maxDepth) {
|
||||
return 0
|
||||
}
|
||||
|
||||
let totalSize = 0
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
// 限制处理的文件数量,避免过长时间
|
||||
const maxFiles = 1000
|
||||
let processedFiles = 0
|
||||
|
||||
for (const entry of entries) {
|
||||
if (processedFiles >= maxFiles) {
|
||||
break
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
totalSize += await this.quickDirectorySizeEstimate(fullPath, maxDepth, currentDepth + 1)
|
||||
} else {
|
||||
try {
|
||||
const stats = await fs.stat(fullPath)
|
||||
totalSize += stats.size
|
||||
processedFiles++
|
||||
} catch (error) {
|
||||
// 忽略无法访问的文件
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果达到文件数量限制,进行估算
|
||||
if (processedFiles >= maxFiles) {
|
||||
const remainingEntries = entries.length - processedFiles
|
||||
const avgFileSize = totalSize / processedFiles
|
||||
totalSize += remainingEntries * avgFileSize
|
||||
}
|
||||
|
||||
return totalSize
|
||||
} catch (error) {
|
||||
console.error('快速目录大小估算失败:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 计算目录大小(完整版本,用于需要精确值时)
|
||||
async calculateDirectorySize(dirPath) {
|
||||
try {
|
||||
let totalSize = 0
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
totalSize += await this.calculateDirectorySize(fullPath)
|
||||
} else {
|
||||
try {
|
||||
const stats = await fs.stat(fullPath)
|
||||
totalSize += stats.size
|
||||
} catch (error) {
|
||||
// 忽略无法访问的文件
|
||||
console.log(`无法访问文件: ${fullPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize
|
||||
} catch (error) {
|
||||
console.error('计算目录大小失败:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件预览URL
|
||||
async getFilePreviewUrl(filePath) {
|
||||
// 这里可以返回一个代理URL,用于前端预览
|
||||
|
||||
BIN
Binary file not shown.
@@ -42,6 +42,19 @@ onMounted(async () => {
|
||||
<button @click="authStore.logout" class="btn btn-text">登出</button>
|
||||
</div>
|
||||
<RouterLink v-else to="/login" class="btn btn-primary">登录</RouterLink>
|
||||
|
||||
<!-- GitHub 链接 -->
|
||||
<a
|
||||
href="https://github.com/kjqwer/pixiv-D"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="github-link"
|
||||
title="查看项目源码"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="github-icon">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -94,6 +107,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
text-decoration: none;
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
@@ -183,6 +197,30 @@ onMounted(async () => {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
color: #374151;
|
||||
background: #f3f4f6;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -223,5 +261,14 @@ onMounted(async () => {
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface RepositoryStats {
|
||||
used: number
|
||||
free: number
|
||||
usagePercent: number
|
||||
note?: string
|
||||
}
|
||||
lastScan: string
|
||||
}
|
||||
@@ -126,8 +127,8 @@ export const useRepositoryStore = defineStore('repository', () => {
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const getStats = async (): Promise<RepositoryStats> => {
|
||||
const result = await apiCall('/stats')
|
||||
const getStats = async (forceRefresh = false): Promise<RepositoryStats> => {
|
||||
const result = await apiCall(`/stats${forceRefresh ? '?forceRefresh=true' : ''}`)
|
||||
stats.value = result
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
<template>
|
||||
<div class="stats-container">
|
||||
<div class="stats-header">
|
||||
<h3>仓库统计</h3>
|
||||
<div class="stats-actions">
|
||||
<button
|
||||
@click="refreshStats"
|
||||
:disabled="loading"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="刷新统计数据"
|
||||
>
|
||||
<svg v-if="loading" viewBox="0 0 24 24" fill="currentColor" class="refresh-icon spinning">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="refresh-icon">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||
</svg>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" v-if="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📁</div>
|
||||
@@ -26,29 +47,97 @@
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.diskUsage.usagePercent }}%</div>
|
||||
<div class="stat-label">磁盘使用率</div>
|
||||
<div v-if="stats.diskUsage.note" class="stat-note">{{ stats.diskUsage.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { RepositoryStats } from '@/stores/repository.ts'
|
||||
import { useRepositoryStore } from '@/stores/repository'
|
||||
|
||||
interface Props {
|
||||
stats: RepositoryStats | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const repositoryStore = useRepositoryStore()
|
||||
|
||||
const refreshStats = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await repositoryStore.getStats(true) // 强制刷新
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error('刷新统计数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
import { formatFileSize } from '@/utils/formatters'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stats-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-header h3 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.refresh-icon.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -65,6 +154,10 @@ import { formatFileSize } from '@/utils/formatters'
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
@@ -75,4 +168,11 @@ import { formatFileSize } from '@/utils/formatters'
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.stat-note {
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user