luoqian 2c20f9bb54 feat: 视频缩略图生成、最近文件面板与前端视图重构
- 新增 VideoThumbnailService,基于 ffmpeg 截取视频缩略图,ffprobe 提取时长
  - 新增 ManagedThumbnailMap 模型及多数据库迁移,存储缩略图元数据
  - 新增 /api/thumbnails/{id} 缩略图流端点
  - 新增最近添加/最近播放 API 与前端面板,支持列表/网格双视图切换
  - FileRecordDto 扩展 thumbnailUrl、videoDuration、lastPlayedAt 字段
  - 前端新增文件库 Tab 导航、卡片网格视图、视频海报与时长信息栏
  - 添加文件库目录不再同步全量扫描,改为后台异步自动扫描
2026-05-22 17:01:49 +08:00

111 lines
3.5 KiB
TypeScript

import { apiUrl, request } from './http'
export type MediaType = 'all' | 'text' | 'video' | 'audio'
export interface DriveDto {
name: string
displayName: string
rootDirectory: string
driveType: string
totalSize: number | null
availableFreeSpace: number | null
isReady: boolean
}
export interface DirectoryDto {
name: string
fullPath: string
}
export interface LibraryRootDto {
id: number
path: string
displayName: string
isEnabled: boolean
isAvailable: boolean
scanIntervalMinutes: number
lastScanStartedAt: string | null
lastScanCompletedAt: string | null
lastScanError: string | null
fileCount: number
}
export interface FileRecordDto {
id: number
libraryRootId: number
fileName: string
relativePath: string
extension: string
sizeBytes: number
lastWriteTimeUtc: string
mediaType: 'text' | 'video' | 'audio'
contentType: string
streamUrl: string
textUrl: string | null
browserPlayable: boolean
thumbnailUrl: string | null
videoDuration: number | null
lastPlayedAt: string | null
}
export interface BrowseDirectoryResponse {
currentPath: string
subdirectories: string[]
files: FileRecordDto[]
}
export interface TextPreviewDto {
id: number
fileName: string
content: string
truncated: boolean
}
export interface PagedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}
const qs = (params: Record<string, string | number | undefined | null>) => {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== '') search.set(key, String(value))
}
const value = search.toString()
return value ? `?${value}` : ''
}
// 业务接口定义,新增接口在此处添加一行即可
export const api = {
getUser: () => request('getUser'),
processData: (input: string) => request('processData', { method: 'POST', body: { input } }),
wData: (input: string) => request('wData', { method: 'POST', body: { input } }),
getDrives: () => request<DriveDto[]>('library/drives'),
getDirectories: (path: string) => request<DirectoryDto[]>(`library/directories${qs({ path })}`),
getRoots: () => request<LibraryRootDto[]>('library/roots'),
addRoot: (body: { path: string; displayName?: string; scanIntervalMinutes?: number }) =>
request<LibraryRootDto>('library/roots', { method: 'POST', body }),
setRootEnabled: (id: number, isEnabled: boolean) =>
request<LibraryRootDto>('library/roots/enabled', { method: 'POST', body: { id, isEnabled } }),
deleteRoot: (id: number) =>
request('library/roots/delete', { method: 'POST', body: { id } }),
scanRoot: (id: number) =>
request<LibraryRootDto>('library/roots/scan', { method: 'POST', body: { id } }),
searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number }) =>
request<PagedResponse<FileRecordDto>>(`files${qs(params)}`),
browseDirectory: (rootId: number, path: string) =>
request<BrowseDirectoryResponse>(`files/browse${qs({ rootId, path })}`),
getTextPreview: (id: number) =>
request<TextPreviewDto>(`files/text${qs({ id })}`),
mediaUrl: (path: string) => apiUrl(path),
thumbnailUrl: (path: string) => apiUrl(path),
getRecentFiles: (type: string, count = 12) =>
request<FileRecordDto[]>(`files/recent${qs({ type, count })}`),
markFilePlayed: (id: number) =>
request('files/played', { method: 'POST', body: { id } }),
qrCode: () => request<{ url: string; qrCodeBase64: string }>('qrcode'),
}