mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +00:00
@@ -154,6 +154,21 @@ function hasRenderableChatRecordName(value?: string): boolean {
|
||||
return value !== undefined && value !== null && String(value).length > 0
|
||||
}
|
||||
|
||||
function toRenderableImageSrc(path?: string): string | undefined {
|
||||
const raw = String(path || '').trim()
|
||||
if (!raw) return undefined
|
||||
if (/^(data:|blob:|https?:|file:)/i.test(raw)) return raw
|
||||
|
||||
const normalized = raw.replace(/\\/g, '/')
|
||||
if (/^[a-zA-Z]:\//.test(normalized)) {
|
||||
return encodeURI(`file:///${normalized}`)
|
||||
}
|
||||
if (normalized.startsWith('/')) {
|
||||
return encodeURI(`file://${normalized}`)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
function getChatRecordPreviewText(item: ChatRecordItem): string {
|
||||
const text = normalizeChatRecordText(item.datadesc) || normalizeChatRecordText(item.datatitle)
|
||||
if (item.datatype === 17) {
|
||||
@@ -4853,7 +4868,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
const candidates = [...head, ...tail]
|
||||
const queued = preloadImageKeysRef.current
|
||||
const seen = new Set<string>()
|
||||
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }> = []
|
||||
const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
|
||||
for (const msg of candidates) {
|
||||
if (payloads.length >= maxPreload) break
|
||||
if (msg.localType !== 3) continue
|
||||
@@ -4867,11 +4882,14 @@ function ChatPage(props: ChatPageProps) {
|
||||
payloads.push({
|
||||
sessionId: currentSessionId,
|
||||
imageMd5: msg.imageMd5 || undefined,
|
||||
imageDatName: msg.imageDatName
|
||||
imageDatName: msg.imageDatName,
|
||||
createTime: msg.createTime
|
||||
})
|
||||
}
|
||||
if (payloads.length > 0) {
|
||||
window.electronAPI.image.preload(payloads).catch(() => { })
|
||||
window.electronAPI.image.preload(payloads, {
|
||||
allowCacheIndex: false
|
||||
}).catch(() => { })
|
||||
}
|
||||
}, [currentSessionId, messages])
|
||||
|
||||
@@ -5840,7 +5858,10 @@ function ChatPage(props: ChatPageProps) {
|
||||
sessionId: session.username,
|
||||
imageMd5: img.imageMd5,
|
||||
imageDatName: img.imageDatName,
|
||||
force: true
|
||||
createTime: img.createTime,
|
||||
force: true,
|
||||
preferFilePath: true,
|
||||
hardlinkOnly: true
|
||||
})
|
||||
if (r?.success) successCount++
|
||||
else failCount++
|
||||
@@ -7882,7 +7903,7 @@ function MessageBubble({
|
||||
)
|
||||
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
|
||||
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
|
||||
() => imageDataUrlCache.get(imageCacheKey)
|
||||
() => toRenderableImageSrc(imageDataUrlCache.get(imageCacheKey))
|
||||
)
|
||||
const voiceIdentityKey = buildVoiceCacheIdentity(session.username, message)
|
||||
const voiceCacheKey = `voice:${voiceIdentityKey}`
|
||||
@@ -7904,6 +7925,7 @@ function MessageBubble({
|
||||
const imageUpdateCheckedRef = useRef<string | null>(null)
|
||||
const imageClickTimerRef = useRef<number | null>(null)
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const imageElementRef = useRef<HTMLImageElement | null>(null)
|
||||
const emojiContainerRef = useRef<HTMLDivElement>(null)
|
||||
const imageResizeBaselineRef = useRef<number | null>(null)
|
||||
const emojiResizeBaselineRef = useRef<number | null>(null)
|
||||
@@ -8260,19 +8282,27 @@ function MessageBubble({
|
||||
sessionId: session.username,
|
||||
imageMd5: message.imageMd5 || undefined,
|
||||
imageDatName: message.imageDatName,
|
||||
force: forceUpdate
|
||||
createTime: message.createTime,
|
||||
force: forceUpdate,
|
||||
preferFilePath: true,
|
||||
hardlinkOnly: true
|
||||
}) as SharedImageDecryptResult
|
||||
})
|
||||
if (result.success && result.localPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
||||
if (imageLocalPath !== result.localPath) {
|
||||
const renderPath = toRenderableImageSrc(result.localPath)
|
||||
if (!renderPath) {
|
||||
if (!silent) setImageError(true)
|
||||
return { success: false }
|
||||
}
|
||||
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||
if (imageLocalPath !== renderPath) {
|
||||
captureImageResizeBaseline()
|
||||
lockImageStageHeight()
|
||||
}
|
||||
setImageLocalPath(result.localPath)
|
||||
setImageLocalPath(renderPath)
|
||||
setImageHasUpdate(false)
|
||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||
return result
|
||||
return { ...result, localPath: renderPath }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8297,7 +8327,7 @@ function MessageBubble({
|
||||
imageDecryptPendingRef.current = false
|
||||
}
|
||||
return { success: false }
|
||||
}, [isImage, message.imageMd5, message.imageDatName, message.localId, session.username, imageCacheKey, detectImageMimeFromBase64, imageLocalPath, captureImageResizeBaseline, lockImageStageHeight])
|
||||
}, [isImage, message.imageMd5, message.imageDatName, message.createTime, message.localId, session.username, imageCacheKey, detectImageMimeFromBase64, imageLocalPath, captureImageResizeBaseline, lockImageStageHeight])
|
||||
|
||||
const triggerForceHd = useCallback(() => {
|
||||
if (!message.imageMd5 && !message.imageDatName) return
|
||||
@@ -8352,24 +8382,29 @@ function MessageBubble({
|
||||
const resolved = await window.electronAPI.image.resolveCache({
|
||||
sessionId: session.username,
|
||||
imageMd5: message.imageMd5 || undefined,
|
||||
imageDatName: message.imageDatName
|
||||
imageDatName: message.imageDatName,
|
||||
createTime: message.createTime,
|
||||
preferFilePath: true,
|
||||
hardlinkOnly: true
|
||||
})
|
||||
if (resolved?.success && resolved.localPath) {
|
||||
finalImagePath = resolved.localPath
|
||||
const renderPath = toRenderableImageSrc(resolved.localPath)
|
||||
if (!renderPath) return
|
||||
finalImagePath = renderPath
|
||||
finalLiveVideoPath = resolved.liveVideoPath || finalLiveVideoPath
|
||||
imageDataUrlCache.set(imageCacheKey, resolved.localPath)
|
||||
if (imageLocalPath !== resolved.localPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||
if (imageLocalPath !== renderPath) {
|
||||
captureImageResizeBaseline()
|
||||
lockImageStageHeight()
|
||||
}
|
||||
setImageLocalPath(resolved.localPath)
|
||||
setImageLocalPath(renderPath)
|
||||
if (resolved.liveVideoPath) setImageLiveVideoPath(resolved.liveVideoPath)
|
||||
setImageHasUpdate(Boolean(resolved.hasUpdate))
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
void window.electronAPI.window.openImageViewerWindow(finalImagePath, finalLiveVideoPath)
|
||||
void window.electronAPI.window.openImageViewerWindow(toRenderableImageSrc(finalImagePath) || finalImagePath, finalLiveVideoPath)
|
||||
}, [
|
||||
imageLiveVideoPath,
|
||||
imageLocalPath,
|
||||
@@ -8378,6 +8413,7 @@ function MessageBubble({
|
||||
lockImageStageHeight,
|
||||
message.imageDatName,
|
||||
message.imageMd5,
|
||||
message.createTime,
|
||||
requestImageDecrypt,
|
||||
session.username
|
||||
])
|
||||
@@ -8391,8 +8427,19 @@ function MessageBubble({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setImageLoaded(false)
|
||||
}, [imageLocalPath])
|
||||
if (!isImage) return
|
||||
if (!imageLocalPath) {
|
||||
setImageLoaded(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 某些 file:// 缓存图在 src 切换时可能不会稳定触发 onLoad,
|
||||
// 这里用 complete/naturalWidth 做一次兜底,避免图片进入 pending 隐身态。
|
||||
const img = imageElementRef.current
|
||||
if (img && img.complete && img.naturalWidth > 0) {
|
||||
setImageLoaded(true)
|
||||
}
|
||||
}, [isImage, imageLocalPath])
|
||||
|
||||
useEffect(() => {
|
||||
if (imageLoading) return
|
||||
@@ -8401,7 +8448,7 @@ function MessageBubble({
|
||||
}, [imageError, imageLoading, imageLocalPath])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isImage || imageLoading) return
|
||||
if (!isImage || imageLoading || !imageInView) return
|
||||
if (!message.imageMd5 && !message.imageDatName) return
|
||||
if (imageUpdateCheckedRef.current === imageCacheKey) return
|
||||
imageUpdateCheckedRef.current = imageCacheKey
|
||||
@@ -8409,15 +8456,21 @@ function MessageBubble({
|
||||
window.electronAPI.image.resolveCache({
|
||||
sessionId: session.username,
|
||||
imageMd5: message.imageMd5 || undefined,
|
||||
imageDatName: message.imageDatName
|
||||
imageDatName: message.imageDatName,
|
||||
createTime: message.createTime,
|
||||
preferFilePath: true,
|
||||
hardlinkOnly: true,
|
||||
allowCacheIndex: false
|
||||
}).then((result: { success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }) => {
|
||||
if (cancelled) return
|
||||
if (result.success && result.localPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
||||
if (!imageLocalPath || imageLocalPath !== result.localPath) {
|
||||
const renderPath = toRenderableImageSrc(result.localPath)
|
||||
if (!renderPath) return
|
||||
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||
if (!imageLocalPath || imageLocalPath !== renderPath) {
|
||||
captureImageResizeBaseline()
|
||||
lockImageStageHeight()
|
||||
setImageLocalPath(result.localPath)
|
||||
setImageLocalPath(renderPath)
|
||||
setImageError(false)
|
||||
}
|
||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||
@@ -8427,7 +8480,7 @@ function MessageBubble({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isImage, imageLocalPath, imageLoading, message.imageMd5, message.imageDatName, imageCacheKey, session.username, captureImageResizeBaseline, lockImageStageHeight])
|
||||
}, [isImage, imageInView, imageLocalPath, imageLoading, message.imageMd5, message.imageDatName, message.createTime, imageCacheKey, session.username, captureImageResizeBaseline, lockImageStageHeight])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isImage) return
|
||||
@@ -8455,15 +8508,17 @@ function MessageBubble({
|
||||
(payload.imageMd5 && payload.imageMd5 === message.imageMd5) ||
|
||||
(payload.imageDatName && payload.imageDatName === message.imageDatName)
|
||||
if (matchesCacheKey) {
|
||||
const renderPath = toRenderableImageSrc(payload.localPath)
|
||||
if (!renderPath) return
|
||||
const cachedPath = imageDataUrlCache.get(imageCacheKey)
|
||||
if (cachedPath !== payload.localPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, payload.localPath)
|
||||
if (cachedPath !== renderPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, renderPath)
|
||||
}
|
||||
if (imageLocalPath !== payload.localPath) {
|
||||
if (imageLocalPath !== renderPath) {
|
||||
captureImageResizeBaseline()
|
||||
lockImageStageHeight()
|
||||
}
|
||||
setImageLocalPath((prev) => (prev === payload.localPath ? prev : payload.localPath))
|
||||
setImageLocalPath((prev) => (prev === renderPath ? prev : renderPath))
|
||||
setImageError(false)
|
||||
}
|
||||
})
|
||||
@@ -9093,6 +9148,7 @@ function MessageBubble({
|
||||
<>
|
||||
<div className="image-message-wrapper">
|
||||
<img
|
||||
ref={imageElementRef}
|
||||
src={imageLocalPath}
|
||||
alt="图片"
|
||||
className={`image-message ${imageLoaded ? 'ready' : 'pending'}`}
|
||||
|
||||
@@ -105,7 +105,6 @@ interface ExportOptions {
|
||||
txtColumns: string[]
|
||||
displayNamePreference: DisplayNamePreference
|
||||
exportConcurrency: number
|
||||
imageDeepSearchOnMiss: boolean
|
||||
}
|
||||
|
||||
interface SessionRow extends AppChatSession {
|
||||
@@ -336,6 +335,15 @@ const isTextBatchTask = (task: ExportTask): boolean => (
|
||||
task.payload.scope === 'content' && task.payload.contentType === 'text'
|
||||
)
|
||||
|
||||
const isImageExportTask = (task: ExportTask): boolean => {
|
||||
if (task.payload.scope === 'sns') {
|
||||
return Boolean(task.payload.snsOptions?.exportImages)
|
||||
}
|
||||
if (task.payload.scope !== 'content') return false
|
||||
if (task.payload.contentType === 'image') return true
|
||||
return Boolean(task.payload.options?.exportImages)
|
||||
}
|
||||
|
||||
const resolvePerfStageByPhase = (phase?: ExportProgress['phase']): TaskPerfStage => {
|
||||
if (phase === 'preparing') return 'collect'
|
||||
if (phase === 'writing') return 'write'
|
||||
@@ -1705,6 +1713,24 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
||||
const currentSessionRatio = task.progress.phaseTotal > 0
|
||||
? Math.max(0, Math.min(1, task.progress.phaseProgress / task.progress.phaseTotal))
|
||||
: null
|
||||
const imageTask = isImageExportTask(task)
|
||||
const imageTimingElapsedMs = imageTask
|
||||
? Math.max(0, (
|
||||
typeof task.finishedAt === 'number'
|
||||
? task.finishedAt
|
||||
: nowTick
|
||||
) - (task.startedAt || task.createdAt))
|
||||
: 0
|
||||
const imageTimingAvgMs = imageTask && mediaDoneFiles > 0
|
||||
? Math.floor(imageTimingElapsedMs / Math.max(1, mediaDoneFiles))
|
||||
: 0
|
||||
const imageTimingLabel = imageTask
|
||||
? (
|
||||
mediaDoneFiles > 0
|
||||
? `图片耗时 ${formatDurationMs(imageTimingElapsedMs)} · 平均 ${imageTimingAvgMs}ms/张`
|
||||
: `图片耗时 ${formatDurationMs(imageTimingElapsedMs)}`
|
||||
)
|
||||
: ''
|
||||
return (
|
||||
<div key={task.id} className={`task-card ${task.status}`}>
|
||||
<div className="task-main">
|
||||
@@ -1734,6 +1760,11 @@ const TaskCenterModal = memo(function TaskCenterModal({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{imageTimingLabel && task.status !== 'queued' && (
|
||||
<div className="task-perf-summary">
|
||||
<span>{imageTimingLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
{canShowPerfDetail && stageTotals && (
|
||||
<div className="task-perf-summary">
|
||||
<span>累计耗时 {formatDurationMs(stageTotalMs)}</span>
|
||||
@@ -1903,7 +1934,6 @@ function ExportPage() {
|
||||
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
|
||||
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
||||
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
|
||||
const [exportDefaultImageDeepSearchOnMiss, setExportDefaultImageDeepSearchOnMiss] = useState(true)
|
||||
|
||||
const [options, setOptions] = useState<ExportOptions>({
|
||||
format: 'json',
|
||||
@@ -1924,8 +1954,7 @@ function ExportPage() {
|
||||
excelCompactColumns: true,
|
||||
txtColumns: defaultTxtColumns,
|
||||
displayNamePreference: 'remark',
|
||||
exportConcurrency: 2,
|
||||
imageDeepSearchOnMiss: true
|
||||
exportConcurrency: 2
|
||||
})
|
||||
|
||||
const [exportDialog, setExportDialog] = useState<ExportDialogState>({
|
||||
@@ -2622,7 +2651,7 @@ function ExportPage() {
|
||||
automationTasksReadyRef.current = false
|
||||
let isReady = true
|
||||
try {
|
||||
const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedImageDeepSearchOnMiss, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([
|
||||
const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([
|
||||
configService.getExportPath(),
|
||||
configService.getExportDefaultFormat(),
|
||||
configService.getExportDefaultAvatars(),
|
||||
@@ -2631,7 +2660,6 @@ function ExportPage() {
|
||||
configService.getExportDefaultExcelCompactColumns(),
|
||||
configService.getExportDefaultTxtColumns(),
|
||||
configService.getExportDefaultConcurrency(),
|
||||
configService.getExportDefaultImageDeepSearchOnMiss(),
|
||||
configService.getExportLastSessionRunMap(),
|
||||
configService.getExportLastContentRunMap(),
|
||||
configService.getExportSessionRecordMap(),
|
||||
@@ -2671,7 +2699,6 @@ function ExportPage() {
|
||||
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
|
||||
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
|
||||
setExportDefaultConcurrency(savedConcurrency ?? 2)
|
||||
setExportDefaultImageDeepSearchOnMiss(savedImageDeepSearchOnMiss ?? true)
|
||||
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
||||
setAutomationTasks(automationTaskItem?.tasks || [])
|
||||
automationTasksReadyRef.current = true
|
||||
@@ -2709,8 +2736,7 @@ function ExportPage() {
|
||||
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
|
||||
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
|
||||
txtColumns,
|
||||
exportConcurrency: savedConcurrency ?? prev.exportConcurrency,
|
||||
imageDeepSearchOnMiss: savedImageDeepSearchOnMiss ?? prev.imageDeepSearchOnMiss
|
||||
exportConcurrency: savedConcurrency ?? prev.exportConcurrency
|
||||
}))
|
||||
} catch (error) {
|
||||
isReady = false
|
||||
@@ -4491,8 +4517,7 @@ function ExportPage() {
|
||||
maxFileSizeMb: prev.maxFileSizeMb,
|
||||
exportVoiceAsText: exportDefaultVoiceAsText,
|
||||
excelCompactColumns: exportDefaultExcelCompactColumns,
|
||||
exportConcurrency: exportDefaultConcurrency,
|
||||
imageDeepSearchOnMiss: exportDefaultImageDeepSearchOnMiss
|
||||
exportConcurrency: exportDefaultConcurrency
|
||||
}
|
||||
|
||||
if (payload.scope === 'sns') {
|
||||
@@ -4527,8 +4552,7 @@ function ExportPage() {
|
||||
exportDefaultAvatars,
|
||||
exportDefaultMedia,
|
||||
exportDefaultVoiceAsText,
|
||||
exportDefaultConcurrency,
|
||||
exportDefaultImageDeepSearchOnMiss
|
||||
exportDefaultConcurrency
|
||||
])
|
||||
|
||||
const closeExportDialog = useCallback(() => {
|
||||
@@ -4755,7 +4779,6 @@ function ExportPage() {
|
||||
txtColumns: options.txtColumns,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
exportConcurrency: options.exportConcurrency,
|
||||
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
|
||||
fileNamingMode: exportDefaultFileNamingMode,
|
||||
sessionLayout,
|
||||
sessionNameWithTypePrefix,
|
||||
@@ -5691,8 +5714,6 @@ function ExportPage() {
|
||||
await configService.setExportDefaultExcelCompactColumns(options.excelCompactColumns)
|
||||
await configService.setExportDefaultTxtColumns(options.txtColumns)
|
||||
await configService.setExportDefaultConcurrency(options.exportConcurrency)
|
||||
await configService.setExportDefaultImageDeepSearchOnMiss(options.imageDeepSearchOnMiss)
|
||||
setExportDefaultImageDeepSearchOnMiss(options.imageDeepSearchOnMiss)
|
||||
}
|
||||
|
||||
const openSingleExport = useCallback((session: SessionRow) => {
|
||||
@@ -7393,14 +7414,6 @@ function ExportPage() {
|
||||
const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog
|
||||
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
||||
const shouldShowMediaSection = !isContentScopeDialog
|
||||
const shouldRenderImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
||||
isSessionScopeDialog ||
|
||||
(isContentScopeDialog && exportDialog.contentType === 'image')
|
||||
)
|
||||
const shouldShowImageDeepSearchToggle = exportDialog.scope !== 'sns' && (
|
||||
(isSessionScopeDialog && options.exportImages) ||
|
||||
(isContentScopeDialog && exportDialog.contentType === 'image')
|
||||
)
|
||||
const avatarExportStatusLabel = options.exportAvatars ? '已开启聊天消息导出带头像' : '已关闭聊天消息导出带头像'
|
||||
const contentTextDialogSummary = '此模式只导出聊天文本,不包含图片语音视频表情包等多媒体文件。'
|
||||
const activeDialogFormatLabel = exportDialog.scope === 'sns'
|
||||
@@ -9710,30 +9723,6 @@ function ExportPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldRenderImageDeepSearchToggle && (
|
||||
<div className={`dialog-collapse-slot ${shouldShowImageDeepSearchToggle ? 'open' : ''}`} aria-hidden={!shouldShowImageDeepSearchToggle}>
|
||||
<div className="dialog-collapse-inner">
|
||||
<div className="dialog-section">
|
||||
<div className="dialog-switch-row">
|
||||
<div className="dialog-switch-copy">
|
||||
<h4>缺图时深度搜索</h4>
|
||||
<div className="format-note">关闭后仅尝试 hardlink 命中,未命中将直接显示占位符,导出速度更快。</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`dialog-switch ${options.imageDeepSearchOnMiss ? 'on' : ''}`}
|
||||
aria-pressed={options.imageDeepSearchOnMiss}
|
||||
aria-label="切换缺图时深度搜索"
|
||||
onClick={() => setOptions(prev => ({ ...prev, imageDeepSearchOnMiss: !prev.imageDeepSearchOnMiss }))}
|
||||
>
|
||||
<span className="dialog-switch-thumb" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSessionScopeDialog && (
|
||||
<div className="dialog-section">
|
||||
<div className="dialog-switch-row">
|
||||
|
||||
@@ -37,7 +37,6 @@ export const CONFIG_KEYS = {
|
||||
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
||||
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
||||
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
||||
EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS: 'exportDefaultImageDeepSearchOnMiss',
|
||||
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
||||
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
||||
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
||||
@@ -548,18 +547,6 @@ export async function setExportDefaultConcurrency(concurrency: number): Promise<
|
||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
|
||||
}
|
||||
|
||||
// 获取缺图时是否深度搜索(默认导出行为)
|
||||
export async function getExportDefaultImageDeepSearchOnMiss(): Promise<boolean | null> {
|
||||
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS)
|
||||
if (typeof value === 'boolean') return value
|
||||
return null
|
||||
}
|
||||
|
||||
// 设置缺图时是否深度搜索(默认导出行为)
|
||||
export async function setExportDefaultImageDeepSearchOnMiss(enabled: boolean): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS, enabled)
|
||||
}
|
||||
|
||||
export type ExportWriteLayout = 'A' | 'B' | 'C'
|
||||
|
||||
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
||||
|
||||
20
src/types/electron.d.ts
vendored
20
src/types/electron.d.ts
vendored
@@ -491,24 +491,35 @@ export interface ElectronAPI {
|
||||
}
|
||||
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||
decrypt: (payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
createTime?: number
|
||||
force?: boolean
|
||||
preferFilePath?: boolean
|
||||
hardlinkOnly?: boolean
|
||||
}) => Promise<{ success: boolean; localPath?: string; liveVideoPath?: string; error?: string }>
|
||||
resolveCache: (payload: {
|
||||
sessionId?: string
|
||||
imageMd5?: string
|
||||
imageDatName?: string
|
||||
createTime?: number
|
||||
preferFilePath?: boolean
|
||||
hardlinkOnly?: boolean
|
||||
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; allowCacheIndex?: boolean }
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number; preferFilePath?: boolean; hardlinkOnly?: boolean }>,
|
||||
options?: { disableUpdateCheck?: boolean; allowCacheIndex?: boolean; preferFilePath?: boolean; hardlinkOnly?: boolean }
|
||||
) => Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
||||
error?: string
|
||||
}>
|
||||
preload: (
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>,
|
||||
payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }>,
|
||||
options?: { allowDecrypt?: boolean; allowCacheIndex?: boolean }
|
||||
) => Promise<boolean>
|
||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
||||
@@ -1117,7 +1128,6 @@ export interface ExportOptions {
|
||||
sessionNameWithTypePrefix?: boolean
|
||||
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
||||
exportConcurrency?: number
|
||||
imageDeepSearchOnMiss?: boolean
|
||||
}
|
||||
|
||||
export interface ExportProgress {
|
||||
|
||||
Reference in New Issue
Block a user