mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-08 15:08:44 +00:00
规范化资源文件;修复消息气泡宽度异常的问题;优化资源管理页面性能
This commit is contained in:
@@ -1965,6 +1965,10 @@
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// 对方发送的消息 - 左侧白色
|
||||
@@ -1974,6 +1978,10 @@
|
||||
color: var(--text-primary);
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.system {
|
||||
@@ -2038,6 +2046,12 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
// 让文字气泡按内容收缩,不被群昵称行宽度牵连
|
||||
.message-bubble:not(.system) .bubble-content {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 表情包消息
|
||||
.message-bubble.emoji {
|
||||
.bubble-content {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState, type HTMLAttributes } from 'react'
|
||||
import { Calendar, Image as ImageIcon, Loader2, PlayCircle, RefreshCw, Trash2, UserRound } from 'lucide-react'
|
||||
import { VirtuosoGrid } from 'react-virtuoso'
|
||||
import { finishBackgroundTask, registerBackgroundTask, updateBackgroundTask } from '../services/backgroundTaskMonitor'
|
||||
import './ResourcesPage.scss'
|
||||
|
||||
type MediaTab = 'image' | 'video'
|
||||
@@ -35,10 +36,14 @@ type DialogState = {
|
||||
onConfirm?: (() => void) | null
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 120
|
||||
const MAX_IMAGE_CACHE_RESOLVE_PER_TICK = 18
|
||||
const MAX_IMAGE_CACHE_PRELOAD_PER_TICK = 36
|
||||
const MAX_VIDEO_POSTER_RESOLVE_PER_TICK = 4
|
||||
const PAGE_SIZE = 96
|
||||
const MAX_IMAGE_CACHE_RESOLVE_PER_TICK = 12
|
||||
const MAX_IMAGE_CACHE_PRELOAD_PER_TICK = 24
|
||||
const MAX_VIDEO_POSTER_RESOLVE_PER_TICK = 3
|
||||
const INITIAL_IMAGE_PRELOAD_END = 48
|
||||
const INITIAL_IMAGE_RESOLVE_END = 12
|
||||
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
|
||||
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
|
||||
|
||||
const GridList = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function GridList(props, ref) {
|
||||
const { className = '', ...rest } = props
|
||||
@@ -409,7 +414,13 @@ function ResourcesPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
await window.electronAPI.chat.connect()
|
||||
if (reset) {
|
||||
const connectResult = await window.electronAPI.chat.connect()
|
||||
if (!connectResult.success) {
|
||||
setError(connectResult.error || '连接数据库失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
const requestOffset = reset ? 0 : nextOffset
|
||||
const streamResult = await window.electronAPI.chat.getMediaStream({
|
||||
sessionId: selectedContact === 'all' ? undefined : selectedContact,
|
||||
@@ -524,7 +535,6 @@ function ResourcesPage() {
|
||||
let cancelled = false
|
||||
const run = async () => {
|
||||
try {
|
||||
await window.electronAPI.chat.connect()
|
||||
const sessionResult = await window.electronAPI.chat.getSessions()
|
||||
if (!cancelled && sessionResult.success && Array.isArray(sessionResult.sessions)) {
|
||||
const initialNameMap: Record<string, string> = {}
|
||||
@@ -674,7 +684,10 @@ function ResourcesPage() {
|
||||
resolvingImageCacheBatchRef.current = true
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.image.resolveCacheBatch(payloads, { disableUpdateCheck: true })
|
||||
const result = await window.electronAPI.image.resolveCacheBatch(payloads, {
|
||||
disableUpdateCheck: true,
|
||||
allowCacheIndex: false
|
||||
})
|
||||
const rows = Array.isArray(result?.rows) ? result.rows : []
|
||||
const pathPatch: Record<string, string> = {}
|
||||
const updatePatch: Record<string, boolean> = {}
|
||||
@@ -741,7 +754,10 @@ function ResourcesPage() {
|
||||
if (payloads.length >= MAX_IMAGE_CACHE_PRELOAD_PER_TICK) break
|
||||
}
|
||||
if (payloads.length === 0) return
|
||||
void window.electronAPI.image.preload(payloads, { allowDecrypt: false })
|
||||
void window.electronAPI.image.preload(payloads, {
|
||||
allowDecrypt: false,
|
||||
allowCacheIndex: false
|
||||
})
|
||||
}, [displayItems])
|
||||
|
||||
const resolveItemVideoMd5 = useCallback(async (item: MediaStreamItem): Promise<string> => {
|
||||
@@ -813,14 +829,18 @@ function ResourcesPage() {
|
||||
if (!pending) return
|
||||
pendingRangeRef.current = null
|
||||
if (tab === 'image') {
|
||||
preloadImageCacheRange(pending.start - 8, pending.end + 32)
|
||||
resolveImageCacheRange(pending.start - 2, pending.end + 8)
|
||||
preloadImageCacheRange(pending.start - 4, pending.end + 20)
|
||||
resolveImageCacheRange(pending.start - 1, pending.end + 6)
|
||||
return
|
||||
}
|
||||
resolvePosterRange(pending.start, pending.end)
|
||||
}, [preloadImageCacheRange, resolveImageCacheRange, resolvePosterRange, tab])
|
||||
|
||||
const scheduleRangeResolve = useCallback((start: number, end: number) => {
|
||||
const previous = pendingRangeRef.current
|
||||
if (previous && start >= previous.start && end <= previous.end) {
|
||||
return
|
||||
}
|
||||
pendingRangeRef.current = { start, end }
|
||||
if (rangeTimerRef.current !== null) {
|
||||
window.clearTimeout(rangeTimerRef.current)
|
||||
@@ -832,8 +852,8 @@ function ResourcesPage() {
|
||||
useEffect(() => {
|
||||
if (displayItems.length === 0) return
|
||||
if (tab === 'image') {
|
||||
preloadImageCacheRange(0, Math.min(displayItems.length - 1, 80))
|
||||
resolveImageCacheRange(0, Math.min(displayItems.length - 1, 20))
|
||||
preloadImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_PRELOAD_END))
|
||||
resolveImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_RESOLVE_END))
|
||||
return
|
||||
}
|
||||
resolvePosterRange(0, Math.min(displayItems.length - 1, 12))
|
||||
@@ -1057,25 +1077,61 @@ function ResourcesPage() {
|
||||
|
||||
setBatchBusy(true)
|
||||
let success = 0
|
||||
let failed = 0
|
||||
const previewPatch: Record<string, string> = {}
|
||||
const updatePatch: Record<string, boolean> = {}
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'other',
|
||||
title: '资源页图片批量解密',
|
||||
detail: `正在解密图片(0/${imageItems.length})`,
|
||||
progressText: `0 / ${imageItems.length}`,
|
||||
cancelable: false
|
||||
})
|
||||
try {
|
||||
let completed = 0
|
||||
const progressStep = Math.max(1, Math.floor(imageItems.length / TASK_PROGRESS_UPDATE_MAX_STEPS))
|
||||
let lastProgressBucket = 0
|
||||
let lastProgressUpdateAt = Date.now()
|
||||
const updateTaskProgress = (force: boolean = false) => {
|
||||
const now = Date.now()
|
||||
const bucket = Math.floor(completed / progressStep)
|
||||
const crossedBucket = bucket !== lastProgressBucket
|
||||
const intervalReached = now - lastProgressUpdateAt >= TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS
|
||||
if (!force && !crossedBucket && !intervalReached) return
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: `正在解密图片(${completed}/${imageItems.length})`,
|
||||
progressText: `${completed} / ${imageItems.length}`
|
||||
})
|
||||
lastProgressBucket = bucket
|
||||
lastProgressUpdateAt = now
|
||||
}
|
||||
for (const item of imageItems) {
|
||||
if (!item.imageMd5 && !item.imageDatName) continue
|
||||
if (!item.imageMd5 && !item.imageDatName) {
|
||||
failed += 1
|
||||
completed += 1
|
||||
updateTaskProgress()
|
||||
continue
|
||||
}
|
||||
const result = await window.electronAPI.image.decrypt({
|
||||
sessionId: item.sessionId,
|
||||
imageMd5: item.imageMd5 || undefined,
|
||||
imageDatName: item.imageDatName || undefined,
|
||||
force: true
|
||||
})
|
||||
if (!result?.success) continue
|
||||
success += 1
|
||||
if (result.localPath) {
|
||||
const key = getItemKey(item)
|
||||
previewPatch[key] = result.localPath
|
||||
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
|
||||
if (!result?.success) {
|
||||
failed += 1
|
||||
} else {
|
||||
success += 1
|
||||
if (result.localPath) {
|
||||
const key = getItemKey(item)
|
||||
previewPatch[key] = result.localPath
|
||||
updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
|
||||
}
|
||||
}
|
||||
completed += 1
|
||||
updateTaskProgress()
|
||||
}
|
||||
updateTaskProgress(true)
|
||||
|
||||
if (Object.keys(previewPatch).length > 0) {
|
||||
setPreviewPathMap((prev) => ({ ...prev, ...previewPatch }))
|
||||
@@ -1083,8 +1139,17 @@ function ResourcesPage() {
|
||||
if (Object.keys(updatePatch).length > 0) {
|
||||
setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
|
||||
}
|
||||
setActionMessage(`批量解密完成:成功 ${success},失败 ${imageItems.length - success}`)
|
||||
showAlert(`批量解密完成:成功 ${success},失败 ${imageItems.length - success}`, '批量解密完成')
|
||||
setActionMessage(`批量解密完成:成功 ${success},失败 ${failed}`)
|
||||
showAlert(`批量解密完成:成功 ${success},失败 ${failed}`, '批量解密完成')
|
||||
finishBackgroundTask(taskId, success > 0 || failed === 0 ? 'completed' : 'failed', {
|
||||
detail: `资源页图片批量解密完成:成功 ${success},失败 ${failed}`,
|
||||
progressText: `成功 ${success} / 失败 ${failed}`
|
||||
})
|
||||
} catch (e) {
|
||||
finishBackgroundTask(taskId, 'failed', {
|
||||
detail: `资源页图片批量解密失败:${String(e)}`
|
||||
})
|
||||
showAlert(`批量解密失败:${String(e)}`, '批量解密失败')
|
||||
} finally {
|
||||
setBatchBusy(false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { create } from 'zustand'
|
||||
import {
|
||||
finishBackgroundTask,
|
||||
registerBackgroundTask,
|
||||
updateBackgroundTask
|
||||
} from '../services/backgroundTaskMonitor'
|
||||
import type { BackgroundTaskSourcePage } from '../types/backgroundTask'
|
||||
|
||||
export interface BatchImageDecryptState {
|
||||
isBatchDecrypting: boolean
|
||||
@@ -8,8 +14,9 @@ export interface BatchImageDecryptState {
|
||||
result: { success: number; fail: number }
|
||||
startTime: number
|
||||
sessionName: string
|
||||
taskId: string | null
|
||||
|
||||
startDecrypt: (total: number, sessionName: string) => void
|
||||
startDecrypt: (total: number, sessionName: string, sourcePage?: BackgroundTaskSourcePage) => void
|
||||
updateProgress: (current: number, total: number) => void
|
||||
finishDecrypt: (success: number, fail: number) => void
|
||||
setShowToast: (show: boolean) => void
|
||||
@@ -17,7 +24,26 @@ export interface BatchImageDecryptState {
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set) => ({
|
||||
const clampProgress = (current: number, total: number): { current: number; total: number } => {
|
||||
const normalizedTotal = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0
|
||||
const normalizedCurrentRaw = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : 0
|
||||
const normalizedCurrent = normalizedTotal > 0
|
||||
? Math.min(normalizedCurrentRaw, normalizedTotal)
|
||||
: normalizedCurrentRaw
|
||||
return { current: normalizedCurrent, total: normalizedTotal }
|
||||
}
|
||||
|
||||
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
|
||||
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
|
||||
|
||||
const taskProgressUpdateMeta = new Map<string, { lastAt: number; lastBucket: number; step: number }>()
|
||||
|
||||
const calcProgressStep = (total: number): number => {
|
||||
if (total <= 0) return 1
|
||||
return Math.max(1, Math.floor(total / TASK_PROGRESS_UPDATE_MAX_STEPS))
|
||||
}
|
||||
|
||||
export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set, get) => ({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
@@ -25,40 +51,127 @@ export const useBatchImageDecryptStore = create<BatchImageDecryptState>((set) =>
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: '',
|
||||
taskId: null,
|
||||
|
||||
startDecrypt: (total, sessionName) => set({
|
||||
isBatchDecrypting: true,
|
||||
progress: { current: 0, total },
|
||||
showToast: true,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: Date.now(),
|
||||
sessionName
|
||||
}),
|
||||
startDecrypt: (total, sessionName, sourcePage = 'chat') => {
|
||||
const previousTaskId = get().taskId
|
||||
if (previousTaskId) {
|
||||
taskProgressUpdateMeta.delete(previousTaskId)
|
||||
finishBackgroundTask(previousTaskId, 'canceled', {
|
||||
detail: '已被新的批量解密任务替换',
|
||||
progressText: '已替换'
|
||||
})
|
||||
}
|
||||
|
||||
updateProgress: (current, total) => set({
|
||||
progress: { current, total }
|
||||
}),
|
||||
const normalizedProgress = clampProgress(0, total)
|
||||
const normalizedSessionName = String(sessionName || '').trim()
|
||||
const title = normalizedSessionName
|
||||
? `图片批量解密(${normalizedSessionName})`
|
||||
: '图片批量解密'
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage,
|
||||
title,
|
||||
detail: `正在解密图片(${normalizedProgress.current}/${normalizedProgress.total})`,
|
||||
progressText: `${normalizedProgress.current} / ${normalizedProgress.total}`,
|
||||
cancelable: false
|
||||
})
|
||||
taskProgressUpdateMeta.set(taskId, {
|
||||
lastAt: Date.now(),
|
||||
lastBucket: 0,
|
||||
step: calcProgressStep(normalizedProgress.total)
|
||||
})
|
||||
|
||||
finishDecrypt: (success, fail) => set({
|
||||
isBatchDecrypting: false,
|
||||
showToast: false,
|
||||
showResultToast: true,
|
||||
result: { success, fail },
|
||||
startTime: 0
|
||||
}),
|
||||
set({
|
||||
isBatchDecrypting: true,
|
||||
progress: normalizedProgress,
|
||||
showToast: true,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: Date.now(),
|
||||
sessionName: normalizedSessionName,
|
||||
taskId
|
||||
})
|
||||
},
|
||||
|
||||
updateProgress: (current, total) => {
|
||||
const previousProgress = get().progress
|
||||
const normalizedProgress = clampProgress(current, total)
|
||||
const taskId = get().taskId
|
||||
if (taskId) {
|
||||
const now = Date.now()
|
||||
const meta = taskProgressUpdateMeta.get(taskId)
|
||||
const step = meta?.step || calcProgressStep(normalizedProgress.total)
|
||||
const bucket = Math.floor(normalizedProgress.current / step)
|
||||
const intervalReached = !meta || (now - meta.lastAt >= TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS)
|
||||
const crossedBucket = !meta || bucket !== meta.lastBucket
|
||||
const isFinal = normalizedProgress.total > 0 && normalizedProgress.current >= normalizedProgress.total
|
||||
if (crossedBucket || intervalReached || isFinal) {
|
||||
updateBackgroundTask(taskId, {
|
||||
detail: `正在解密图片(${normalizedProgress.current}/${normalizedProgress.total})`,
|
||||
progressText: `${normalizedProgress.current} / ${normalizedProgress.total}`
|
||||
})
|
||||
taskProgressUpdateMeta.set(taskId, {
|
||||
lastAt: now,
|
||||
lastBucket: bucket,
|
||||
step
|
||||
})
|
||||
}
|
||||
}
|
||||
if (
|
||||
previousProgress.current !== normalizedProgress.current ||
|
||||
previousProgress.total !== normalizedProgress.total
|
||||
) {
|
||||
set({
|
||||
progress: normalizedProgress
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
finishDecrypt: (success, fail) => {
|
||||
const taskId = get().taskId
|
||||
const normalizedSuccess = Number.isFinite(success) ? Math.max(0, Math.floor(success)) : 0
|
||||
const normalizedFail = Number.isFinite(fail) ? Math.max(0, Math.floor(fail)) : 0
|
||||
if (taskId) {
|
||||
taskProgressUpdateMeta.delete(taskId)
|
||||
const status = normalizedSuccess > 0 || normalizedFail === 0 ? 'completed' : 'failed'
|
||||
finishBackgroundTask(taskId, status, {
|
||||
detail: `图片批量解密完成:成功 ${normalizedSuccess},失败 ${normalizedFail}`,
|
||||
progressText: `成功 ${normalizedSuccess} / 失败 ${normalizedFail}`
|
||||
})
|
||||
}
|
||||
|
||||
set({
|
||||
isBatchDecrypting: false,
|
||||
showToast: false,
|
||||
showResultToast: true,
|
||||
result: { success: normalizedSuccess, fail: normalizedFail },
|
||||
startTime: 0,
|
||||
taskId: null
|
||||
})
|
||||
},
|
||||
|
||||
setShowToast: (show) => set({ showToast: show }),
|
||||
setShowResultToast: (show) => set({ showResultToast: show }),
|
||||
|
||||
reset: () => set({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: ''
|
||||
})
|
||||
}))
|
||||
reset: () => {
|
||||
const taskId = get().taskId
|
||||
if (taskId) {
|
||||
taskProgressUpdateMeta.delete(taskId)
|
||||
finishBackgroundTask(taskId, 'canceled', {
|
||||
detail: '批量解密任务已重置',
|
||||
progressText: '已停止'
|
||||
})
|
||||
}
|
||||
|
||||
set({
|
||||
isBatchDecrypting: false,
|
||||
progress: { current: 0, total: 0 },
|
||||
showToast: false,
|
||||
showResultToast: false,
|
||||
result: { success: 0, fail: 0 },
|
||||
startTime: 0,
|
||||
sessionName: '',
|
||||
taskId: null
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
12
src/types/electron.d.ts
vendored
12
src/types/electron.d.ts
vendored
@@ -403,10 +403,16 @@ export interface ElectronAPI {
|
||||
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||
resolveCache: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; disableUpdateCheck?: boolean }) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||
resolveCache: (payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
disableUpdateCheck?: boolean
|
||||
allowCacheIndex?: boolean
|
||||
}) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||
resolveCacheBatch: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { disableUpdateCheck?: boolean }
|
||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean }
|
||||
) => Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
||||
@@ -414,7 +420,7 @@ export interface ElectronAPI {
|
||||
}>
|
||||
preload: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
options?: { allowDecrypt?: boolean }
|
||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||
) => Promise<boolean>
|
||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
||||
onCacheResolved: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string; localPath: string }) => void) => () => void
|
||||
|
||||
Reference in New Issue
Block a user