refactor: 重构设置、API、图库与存储页面前端结构。
将操作日志并入设置侧栏,补齐检查更新提示与版本接口刷新;拆分各模块组件并修复图库搜索分页变量。
This commit is contained in:
1 parent
270af4e707
commit
ac65c1eb9d
51 files changed
+5119
-2059
No files matched your search
@@ -9,7 +9,7 @@ import { createApiError } from '../../utils/api-error'
|
||||
import { getUploadSourcesForKeys, listUserIdUsernameMap } from '../../utils/db'
|
||||
import { mapStoredImageToItem } from '../../utils/image-response'
|
||||
import { listStorageBackendNameMap } from '../../utils/storage-backends'
|
||||
import { readBackendIdQuery } from '../../utils/image-query'
|
||||
import { readBackendIdQuery, readContentTypeQuery, readUploadSourceQuery } from '../../utils/image-query'
|
||||
import { listImages } from '../../utils/storage'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -31,9 +31,24 @@ export default defineEventHandler(async (event) => {
|
||||
if (backendId === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的存储后端', 400)
|
||||
}
|
||||
const contentType = readContentTypeQuery(query)
|
||||
if (contentType === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的图片类型', 400)
|
||||
}
|
||||
const uploadSource = readUploadSourceQuery(query)
|
||||
if (uploadSource === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的上传来源', 400)
|
||||
}
|
||||
const userFilter = await getImageUserFilter(event)
|
||||
|
||||
const listing = await listImages({ limit, page, userFilter, backendId })
|
||||
const listing = await listImages({
|
||||
limit,
|
||||
page,
|
||||
userFilter,
|
||||
backendId,
|
||||
contentType,
|
||||
uploadSource
|
||||
})
|
||||
|
||||
const user = await getCurrentUser(event)
|
||||
const ownerMap = user?.role === 'admin' ? listUserIdUsernameMap() : undefined
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createApiError } from '../../utils/api-error'
|
||||
import { getUploadSourcesForKeys, listUserIdUsernameMap } from '../../utils/db'
|
||||
import { mapStoredImageToItem } from '../../utils/image-response'
|
||||
import { listStorageBackendNameMap } from '../../utils/storage-backends'
|
||||
import { readBackendIdQuery } from '../../utils/image-query'
|
||||
import { readBackendIdQuery, readContentTypeQuery, readUploadSourceQuery } from '../../utils/image-query'
|
||||
import { searchImages } from '../../utils/storage'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -40,9 +40,25 @@ export default defineEventHandler(async (event) => {
|
||||
if (backendId === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的存储后端', 400)
|
||||
}
|
||||
const contentType = readContentTypeQuery(query)
|
||||
if (contentType === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的图片类型', 400)
|
||||
}
|
||||
const uploadSource = readUploadSourceQuery(query)
|
||||
if (uploadSource === null) {
|
||||
createApiError(event, 'INVALID_REQUEST', '无效的上传来源', 400)
|
||||
}
|
||||
const userFilter = await getImageUserFilter(event)
|
||||
|
||||
const result = await searchImages({ query: q, limit, page, userFilter, backendId })
|
||||
const result = await searchImages({
|
||||
query: q,
|
||||
limit,
|
||||
page,
|
||||
userFilter,
|
||||
backendId,
|
||||
contentType,
|
||||
uploadSource
|
||||
})
|
||||
|
||||
const user = await getCurrentUser(event)
|
||||
const ownerMap = user?.role === 'admin' ? listUserIdUsernameMap() : undefined
|
||||
|
||||
+39
-5
@@ -7,6 +7,34 @@ import {
|
||||
getFolderStorageStats,
|
||||
getUserScopedStorageStats
|
||||
} from '../utils/storage'
|
||||
import { buildBackendCapacity } from '../utils/storage-capacity'
|
||||
import { getActiveBackendRow } from '../utils/storage/resolver'
|
||||
|
||||
interface StorageUsageStat {
|
||||
usedBytes: number
|
||||
totalBytes: number | null
|
||||
percent: number | null
|
||||
}
|
||||
|
||||
async function resolveStorageUsage(usedBytes: number): Promise<StorageUsageStat> {
|
||||
const backend = getActiveBackendRow()
|
||||
if (!backend) {
|
||||
return { usedBytes, totalBytes: null, percent: null }
|
||||
}
|
||||
|
||||
const capacity = await buildBackendCapacity(
|
||||
backend.id,
|
||||
backend.type,
|
||||
usedBytes,
|
||||
backend.quota_bytes
|
||||
)
|
||||
|
||||
return {
|
||||
usedBytes,
|
||||
totalBytes: capacity.totalBytes,
|
||||
percent: capacity.percent
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await requireUserAuth(event)
|
||||
@@ -14,25 +42,30 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
try {
|
||||
if (user.role === 'admin' && userFilter === 'admin') {
|
||||
const [activity, storedCount, byFolder] = await Promise.all([
|
||||
Promise.resolve(getActivityStats()),
|
||||
const activity = getActivityStats()
|
||||
const [storedCount, byFolder, storageUsage] = await Promise.all([
|
||||
countImages(),
|
||||
getFolderStorageStats()
|
||||
getFolderStorageStats(),
|
||||
resolveStorageUsage(activity.uploadBytesTotal)
|
||||
])
|
||||
|
||||
return {
|
||||
...activity,
|
||||
storedCount,
|
||||
byFolder,
|
||||
userCount: countUsers()
|
||||
userCount: countUsers(),
|
||||
storageUsage
|
||||
}
|
||||
}
|
||||
|
||||
const scoped = await getUserScopedStorageStats(user.id)
|
||||
const storageUsage = await resolveStorageUsage(scoped.uploadBytesTotal)
|
||||
|
||||
return {
|
||||
uploadToday: scoped.uploadToday,
|
||||
uploadYesterday: scoped.uploadYesterday,
|
||||
uploadMonth: scoped.uploadMonth,
|
||||
uploadLastMonth: scoped.uploadLastMonth,
|
||||
deleteToday: 0,
|
||||
deleteMonth: 0,
|
||||
uploadTotal: scoped.storedCount,
|
||||
@@ -44,7 +77,8 @@ export default defineEventHandler(async (event) => {
|
||||
count: item.count
|
||||
})),
|
||||
storedCount: scoped.storedCount,
|
||||
byFolder: scoped.byFolder
|
||||
byFolder: scoped.byFolder,
|
||||
storageUsage
|
||||
}
|
||||
} catch (error) {
|
||||
logException('stats failed', error)
|
||||
|
||||
@@ -9,7 +9,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const config = useRuntimeConfig(event)
|
||||
const currentVersion = String(config.appVersion ?? '')
|
||||
const latest = await fetchLatestGitHubRelease()
|
||||
const query = getQuery(event)
|
||||
const force = query.refresh === '1' || query.refresh === 'true'
|
||||
const latest = await fetchLatestGitHubRelease(force)
|
||||
|
||||
if (!latest) {
|
||||
return {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user