refactor: 重构设置、API、图库与存储页面前端结构。

将操作日志并入设置侧栏,补齐检查更新提示与版本接口刷新;拆分各模块组件并修复图库搜索分页变量。
This commit is contained in:
O96u committed 2026-09-01 17:15:02 +08:00
1 parent 270af4e707
commit ac65c1eb9d
51 files changed
+5119 -2059

No files matched your search

+2 -2
View File
@@ -32,9 +32,9 @@ export function isVersionNewer(candidate: string, current: string): boolean {
return false
}
export async function fetchLatestGitHubRelease(): Promise<LatestReleaseInfo | null> {
export async function fetchLatestGitHubRelease(force = false): Promise<LatestReleaseInfo | null> {
const now = Date.now()
if (cachedRelease && now - cachedRelease.fetchedAt < CACHE_TTL_MS) {
if (!force && cachedRelease && now - cachedRelease.fetchedAt < CACHE_TTL_MS) {
return cachedRelease.value
}
+27
View File
@@ -613,6 +613,15 @@ function countSince(action: LogAction, sinceIso: string): number {
return row?.total ?? 0
}
function countBetween(action: LogAction, startIso: string, endIso: string): number {
const row = getDb().prepare(`
SELECT COUNT(*) AS total
FROM activity_logs
WHERE action = ? AND created_at >= ? AND created_at < ?
`).get(action, startIso, endIso) as unknown as { total: number } | undefined
return row?.total ?? 0
}
function sumUploadBytes(): number {
const row = getDb().prepare(`
SELECT COALESCE(SUM(size), 0) AS total
@@ -674,9 +683,23 @@ function startOfMonthIsoInShanghai(now = new Date()): string {
return new Date(Date.UTC(y, m, 1) - 8 * 60 * 60 * 1000).toISOString()
}
function startOfYesterdayIsoInShanghai(now = new Date()): string {
const startOfDay = startOfDayIsoInShanghai(now)
return new Date(new Date(startOfDay).getTime() - 24 * 60 * 60 * 1000).toISOString()
}
function startOfLastMonthIsoInShanghai(now = new Date()): string {
const shifted = new Date(now.getTime() + 8 * 60 * 60 * 1000)
const y = shifted.getUTCFullYear()
const m = shifted.getUTCMonth()
return new Date(Date.UTC(y, m - 1, 1) - 8 * 60 * 60 * 1000).toISOString()
}
export function getActivityStats(): {
uploadToday: number
uploadYesterday: number
uploadMonth: number
uploadLastMonth: number
deleteToday: number
deleteMonth: number
uploadTotal: number
@@ -687,6 +710,8 @@ export function getActivityStats(): {
} {
const startOfDay = startOfDayIsoInShanghai()
const startOfMonth = startOfMonthIsoInShanghai()
const startOfYesterday = startOfYesterdayIsoInShanghai()
const startOfLastMonth = startOfLastMonthIsoInShanghai()
const uploadTotalRow = getDb().prepare(`
SELECT COUNT(*) AS total FROM activity_logs WHERE action = 'upload'
@@ -697,7 +722,9 @@ export function getActivityStats(): {
return {
uploadToday: countSince('upload', startOfDay),
uploadYesterday: countBetween('upload', startOfYesterday, startOfDay),
uploadMonth: countSince('upload', startOfMonth),
uploadLastMonth: countBetween('upload', startOfLastMonth, startOfMonth),
deleteToday: countSince('delete', startOfDay),
deleteMonth: countSince('delete', startOfMonth),
uploadTotal: uploadTotalRow.total,
+55 -3
View File
@@ -1,6 +1,6 @@
import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path'
import { getDb } from './db'
import { getDb, getUploadSourcesForKeys } from './db'
import { getDataDir } from './data-dir'
import { contentTypeFromKey } from './content-type'
import { DEFAULT_FOLDER, toCanonicalImageKey, validateImageKey } from './image-key'
@@ -402,11 +402,31 @@ function paginateSlice<T>(
return { items, total, page: safePage, pageSize, totalPages }
}
function filterImageRows(
rows: ImageIndexRow[],
options: {
contentType?: string
uploadSource?: 'web' | 'api'
}
): ImageIndexRow[] {
let filtered = rows
if (options.contentType) {
filtered = filtered.filter(row => row.content_type === options.contentType)
}
if (options.uploadSource) {
const sourceMap = getUploadSourcesForKeys(filtered.map(row => row.key))
filtered = filtered.filter(row => sourceMap.get(row.key) === options.uploadSource)
}
return filtered
}
export async function listImages(options: {
limit: number
page?: number
userFilter?: number | 'admin'
backendId?: string
contentType?: string
uploadSource?: 'web' | 'api'
}): Promise<PaginatedResult<StoredImage>> {
ensureStorageSchema()
const params: Array<string | number> = []
@@ -421,7 +441,11 @@ export async function listImages(options: {
ORDER BY uploaded_at DESC, key DESC
`).all(...params) as unknown as ImageIndexRow[]
const sliced = paginateSlice(rows, options.limit, options.page)
const filtered = filterImageRows(rows, {
contentType: options.contentType,
uploadSource: options.uploadSource
})
const sliced = paginateSlice(filtered, options.limit, options.page)
return {
...sliced,
items: sliced.items.map(rowToStoredImage)
@@ -434,6 +458,8 @@ export async function searchImages(options: {
page?: number
userFilter?: number | 'admin'
backendId?: string
contentType?: string
uploadSource?: 'web' | 'api'
}): Promise<PaginatedResult<StoredImage>> {
ensureStorageSchema()
const needle = options.query.trim().toLowerCase()
@@ -455,7 +481,11 @@ export async function searchImages(options: {
|| row.original_name.toLowerCase().includes(needle)
})
const sliced = paginateSlice(matches, options.limit, options.page)
const filtered = filterImageRows(matches, {
contentType: options.contentType,
uploadSource: options.uploadSource
})
const sliced = paginateSlice(filtered, options.limit, options.page)
return {
...sliced,
items: sliced.items.map(rowToStoredImage)
@@ -495,16 +525,32 @@ function startOfMonthIsoInShanghai(now = new Date()): string {
return new Date(Date.UTC(y, m, 1) - 8 * 60 * 60 * 1000).toISOString()
}
function startOfLastMonthIsoInShanghai(now = new Date()): string {
const shifted = new Date(now.getTime() + 8 * 60 * 60 * 1000)
const y = shifted.getUTCFullYear()
const m = shifted.getUTCMonth()
return new Date(Date.UTC(y, m - 1, 1) - 8 * 60 * 60 * 1000).toISOString()
}
function startOfYesterdayIsoInShanghai(now = new Date()): string {
const startOfDay = startOfDayIsoInShanghai(now)
return new Date(new Date(startOfDay).getTime() - 24 * 60 * 60 * 1000).toISOString()
}
export async function getUserScopedStorageStats(userId: number): Promise<{
storedCount: number
uploadBytesTotal: number
uploadToday: number
uploadYesterday: number
uploadMonth: number
uploadLastMonth: number
byFolder: Array<{ folder: string, count: number, bytes: number }>
}> {
ensureStorageSchema()
const startOfDay = startOfDayIsoInShanghai()
const startOfMonth = startOfMonthIsoInShanghai()
const startOfYesterday = startOfYesterdayIsoInShanghai()
const startOfLastMonth = startOfLastMonthIsoInShanghai()
const rows = getDb().prepare(`
SELECT folder, size, uploaded_at
@@ -515,12 +561,16 @@ export async function getUserScopedStorageStats(userId: number): Promise<{
const map = new Map<string, { count: number, bytes: number }>()
let uploadBytesTotal = 0
let uploadToday = 0
let uploadYesterday = 0
let uploadMonth = 0
let uploadLastMonth = 0
for (const row of rows) {
uploadBytesTotal += row.size
if (row.uploaded_at >= startOfDay) uploadToday += 1
if (row.uploaded_at >= startOfYesterday && row.uploaded_at < startOfDay) uploadYesterday += 1
if (row.uploaded_at >= startOfMonth) uploadMonth += 1
if (row.uploaded_at >= startOfLastMonth && row.uploaded_at < startOfMonth) uploadLastMonth += 1
const current = map.get(row.folder) ?? { count: 0, bytes: 0 }
current.count += 1
@@ -536,7 +586,9 @@ export async function getUserScopedStorageStats(userId: number): Promise<{
storedCount: rows.length,
uploadBytesTotal,
uploadToday,
uploadYesterday,
uploadMonth,
uploadLastMonth,
byFolder
}
}
+19
View File
@@ -1,4 +1,5 @@
import { getStorageBackendRow } from './storage-backends'
import { ALLOWED_MIME_TYPES } from './constants'
export function readBackendIdQuery(
query: Record<string, unknown>
@@ -7,3 +8,21 @@ export function readBackendIdQuery(
if (!raw || raw === 'all') return undefined
return getStorageBackendRow(raw)?.id ?? null
}
export function readContentTypeQuery(
query: Record<string, unknown>
): string | undefined | null {
const raw = typeof query.contentType === 'string' ? query.contentType.trim() : ''
if (!raw || raw === 'all') return undefined
if (!(ALLOWED_MIME_TYPES as readonly string[]).includes(raw)) return null
return raw
}
export function readUploadSourceQuery(
query: Record<string, unknown>
): 'web' | 'api' | undefined | null {
const raw = typeof query.uploadSource === 'string' ? query.uploadSource.trim() : ''
if (!raw || raw === 'all') return undefined
if (raw === 'web' || raw === 'api') return raw
return null
}
+6
View File
@@ -192,7 +192,9 @@ export async function getUserScopedStorageStats(userId: number): Promise<{
storedCount: number
uploadBytesTotal: number
uploadToday: number
uploadYesterday: number
uploadMonth: number
uploadLastMonth: number
byFolder: FolderStorageStat[]
}> {
return getUserScopedStorageStatsFromIndex(userId)
@@ -203,6 +205,8 @@ export async function listImages(options: {
page?: number
userFilter?: number | 'admin'
backendId?: string
contentType?: string
uploadSource?: 'web' | 'api'
}): Promise<PaginatedResult<StoredImage>> {
return listImagesFromIndex(options)
}
@@ -213,6 +217,8 @@ export async function searchImages(options: {
page?: number
userFilter?: number | 'admin'
backendId?: string
contentType?: string
uploadSource?: 'web' | 'api'
}): Promise<PaginatedResult<StoredImage>> {
return searchImagesFromIndex(options)
}