mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(chat-export): open single export dialog in chat with init feedback
This commit is contained in:
@@ -63,7 +63,6 @@ function App() {
|
|||||||
const isNotificationWindow = location.pathname === '/notification-window'
|
const isNotificationWindow = location.pathname === '/notification-window'
|
||||||
const isExportRoute = location.pathname === '/export'
|
const isExportRoute = location.pathname === '/export'
|
||||||
const [themeHydrated, setThemeHydrated] = useState(false)
|
const [themeHydrated, setThemeHydrated] = useState(false)
|
||||||
const [hasVisitedExport, setHasVisitedExport] = useState(isExportRoute)
|
|
||||||
|
|
||||||
// 锁定状态
|
// 锁定状态
|
||||||
// const [isLocked, setIsLocked] = useState(false) // Moved to store
|
// const [isLocked, setIsLocked] = useState(false) // Moved to store
|
||||||
@@ -101,12 +100,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [isOnboardingWindow])
|
}, [isOnboardingWindow])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isExportRoute) {
|
|
||||||
setHasVisitedExport(true)
|
|
||||||
}
|
|
||||||
}, [isExportRoute])
|
|
||||||
|
|
||||||
// 应用主题
|
// 应用主题
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
@@ -462,11 +455,9 @@ function App() {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="content">
|
<main className="content">
|
||||||
<RouteGuard>
|
<RouteGuard>
|
||||||
{hasVisitedExport && (
|
|
||||||
<div className={`export-keepalive-page ${isExportRoute ? 'active' : 'hidden'}`} aria-hidden={!isExportRoute}>
|
<div className={`export-keepalive-page ${isExportRoute ? 'active' : 'hidden'}`} aria-hidden={!isExportRoute}>
|
||||||
<ExportPage />
|
<ExportPage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
|||||||
@@ -534,6 +534,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-prepare-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--chat-pattern);
|
background: var(--chat-pattern);
|
||||||
@@ -1642,6 +1658,10 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1683,6 +1703,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-prepare-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import { LivePhotoIcon } from '../components/LivePhotoIcon'
|
|||||||
import { AnimatedStreamingText } from '../components/AnimatedStreamingText'
|
import { AnimatedStreamingText } from '../components/AnimatedStreamingText'
|
||||||
import JumpToDateDialog from '../components/JumpToDateDialog'
|
import JumpToDateDialog from '../components/JumpToDateDialog'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
|
import {
|
||||||
|
emitOpenSingleExport,
|
||||||
|
onExportSessionStatus,
|
||||||
|
onSingleExportDialogStatus,
|
||||||
|
requestExportSessionStatus
|
||||||
|
} from '../services/exportBridge'
|
||||||
import './ChatPage.scss'
|
import './ChatPage.scss'
|
||||||
|
|
||||||
// 系统消息类型常量
|
// 系统消息类型常量
|
||||||
@@ -385,6 +391,9 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
|
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
|
||||||
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
||||||
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
|
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
|
||||||
|
const [inProgressExportSessionIds, setInProgressExportSessionIds] = useState<Set<string>>(new Set())
|
||||||
|
const [isPreparingExportDialog, setIsPreparingExportDialog] = useState(false)
|
||||||
|
const [exportPrepareHint, setExportPrepareHint] = useState('')
|
||||||
|
|
||||||
// 消息右键菜单
|
// 消息右键菜单
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, message: Message } | null>(null)
|
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, message: Message } | null>(null)
|
||||||
@@ -454,6 +463,85 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const previewCacheRef = useRef<Record<string, SessionPreviewCacheEntry>>({})
|
const previewCacheRef = useRef<Record<string, SessionPreviewCacheEntry>>({})
|
||||||
const previewPersistTimerRef = useRef<number | null>(null)
|
const previewPersistTimerRef = useRef<number | null>(null)
|
||||||
const sessionListPersistTimerRef = useRef<number | null>(null)
|
const sessionListPersistTimerRef = useRef<number | null>(null)
|
||||||
|
const pendingExportRequestIdRef = useRef<string | null>(null)
|
||||||
|
const exportPrepareLongWaitTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
|
const clearExportPrepareState = useCallback(() => {
|
||||||
|
pendingExportRequestIdRef.current = null
|
||||||
|
setIsPreparingExportDialog(false)
|
||||||
|
setExportPrepareHint('')
|
||||||
|
if (exportPrepareLongWaitTimerRef.current) {
|
||||||
|
window.clearTimeout(exportPrepareLongWaitTimerRef.current)
|
||||||
|
exportPrepareLongWaitTimerRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = onExportSessionStatus((payload) => {
|
||||||
|
const ids = Array.isArray(payload?.inProgressSessionIds)
|
||||||
|
? payload.inProgressSessionIds
|
||||||
|
.filter((id): id is string => typeof id === 'string')
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: []
|
||||||
|
setInProgressExportSessionIds(new Set(ids))
|
||||||
|
})
|
||||||
|
|
||||||
|
requestExportSessionStatus()
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
requestExportSessionStatus()
|
||||||
|
}, 0)
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timer)
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = onSingleExportDialogStatus((payload) => {
|
||||||
|
const requestId = typeof payload?.requestId === 'string' ? payload.requestId.trim() : ''
|
||||||
|
if (!requestId || requestId !== pendingExportRequestIdRef.current) return
|
||||||
|
|
||||||
|
if (payload.status === 'initializing') {
|
||||||
|
setExportPrepareHint('正在准备导出模块(首次会稍慢,通常 1-3 秒)')
|
||||||
|
if (exportPrepareLongWaitTimerRef.current) {
|
||||||
|
window.clearTimeout(exportPrepareLongWaitTimerRef.current)
|
||||||
|
}
|
||||||
|
exportPrepareLongWaitTimerRef.current = window.setTimeout(() => {
|
||||||
|
if (pendingExportRequestIdRef.current !== requestId) return
|
||||||
|
setExportPrepareHint('仍在准备导出模块,请稍候...')
|
||||||
|
}, 8000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.status === 'opened') {
|
||||||
|
clearExportPrepareState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.status === 'failed') {
|
||||||
|
const message = (typeof payload.message === 'string' && payload.message.trim())
|
||||||
|
? payload.message.trim()
|
||||||
|
: '导出模块初始化失败,请重试'
|
||||||
|
clearExportPrepareState()
|
||||||
|
window.alert(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe()
|
||||||
|
if (exportPrepareLongWaitTimerRef.current) {
|
||||||
|
window.clearTimeout(exportPrepareLongWaitTimerRef.current)
|
||||||
|
exportPrepareLongWaitTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [clearExportPrepareState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPreparingExportDialog || !currentSessionId) return
|
||||||
|
if (!inProgressExportSessionIds.has(currentSessionId)) return
|
||||||
|
clearExportPrepareState()
|
||||||
|
}, [clearExportPrepareState, currentSessionId, inProgressExportSessionIds, isPreparingExportDialog])
|
||||||
|
|
||||||
// 加载当前用户头像
|
// 加载当前用户头像
|
||||||
const loadMyAvatar = useCallback(async () => {
|
const loadMyAvatar = useCallback(async () => {
|
||||||
@@ -1844,6 +1932,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
displayName: fallbackDisplayName || currentSessionId,
|
displayName: fallbackDisplayName || currentSessionId,
|
||||||
} as ChatSession
|
} as ChatSession
|
||||||
})()
|
})()
|
||||||
|
const isCurrentSessionExporting = Boolean(currentSessionId && inProgressExportSessionIds.has(currentSessionId))
|
||||||
|
const isExportActionBusy = isCurrentSessionExporting || isPreparingExportDialog
|
||||||
|
|
||||||
// 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称
|
// 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1960,12 +2050,23 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
const handleExportCurrentSession = useCallback(() => {
|
const handleExportCurrentSession = useCallback(() => {
|
||||||
if (!currentSessionId) return
|
if (!currentSessionId) return
|
||||||
navigate('/export', {
|
if (inProgressExportSessionIds.has(currentSessionId) || isPreparingExportDialog) return
|
||||||
state: {
|
|
||||||
preselectSessionIds: [currentSessionId]
|
const requestId = `chat-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
const sessionName = currentSession?.displayName || currentSession?.username || currentSessionId
|
||||||
|
pendingExportRequestIdRef.current = requestId
|
||||||
|
setIsPreparingExportDialog(true)
|
||||||
|
setExportPrepareHint('')
|
||||||
|
if (exportPrepareLongWaitTimerRef.current) {
|
||||||
|
window.clearTimeout(exportPrepareLongWaitTimerRef.current)
|
||||||
|
exportPrepareLongWaitTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
emitOpenSingleExport({
|
||||||
|
sessionId: currentSessionId,
|
||||||
|
sessionName,
|
||||||
|
requestId
|
||||||
})
|
})
|
||||||
}, [currentSessionId, navigate])
|
}, [currentSession, currentSessionId, inProgressExportSessionIds, isPreparingExportDialog])
|
||||||
|
|
||||||
const handleGroupAnalytics = useCallback(() => {
|
const handleGroupAnalytics = useCallback(() => {
|
||||||
if (!currentSessionId || !isGroupChat(currentSessionId)) return
|
if (!currentSessionId || !isGroupChat(currentSessionId)) return
|
||||||
@@ -2641,12 +2742,16 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="icon-btn export-session-btn"
|
className={`icon-btn export-session-btn${isExportActionBusy ? ' exporting' : ''}`}
|
||||||
onClick={handleExportCurrentSession}
|
onClick={handleExportCurrentSession}
|
||||||
disabled={!currentSessionId}
|
disabled={!currentSessionId || isExportActionBusy}
|
||||||
title="导出当前会话"
|
title={isCurrentSessionExporting ? '导出中' : isPreparingExportDialog ? '正在准备导出模块' : '导出当前会话'}
|
||||||
>
|
>
|
||||||
|
{isExportActionBusy ? (
|
||||||
|
<Loader2 size={18} className="spin" />
|
||||||
|
) : (
|
||||||
<Download size={18} />
|
<Download size={18} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||||
@@ -2751,6 +2856,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isPreparingExportDialog && exportPrepareHint && (
|
||||||
|
<div className="export-prepare-hint" role="status" aria-live="polite">
|
||||||
|
<Loader2 size={14} className="spin" />
|
||||||
|
<span>{exportPrepareHint}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'} ${isSessionSwitching ? 'switching' : ''}`}>
|
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'} ${isSessionSwitching ? 'switching' : ''}`}>
|
||||||
{isLoadingMessages && (!hasInitialMessages || isSessionSwitching) && (
|
{isLoadingMessages && (!hasInitialMessages || isSessionSwitching) && (
|
||||||
<div className="loading-messages loading-overlay">
|
<div className="loading-messages loading-overlay">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { TableVirtuoso } from 'react-virtuoso'
|
import { TableVirtuoso } from 'react-virtuoso'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
import {
|
import {
|
||||||
Aperture,
|
Aperture,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -29,6 +30,12 @@ import {
|
|||||||
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
||||||
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
|
import {
|
||||||
|
emitExportSessionStatus,
|
||||||
|
emitSingleExportDialogStatus,
|
||||||
|
onExportSessionStatusRequest,
|
||||||
|
onOpenSingleExport
|
||||||
|
} from '../services/exportBridge'
|
||||||
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||||
import './ExportPage.scss'
|
import './ExportPage.scss'
|
||||||
|
|
||||||
@@ -654,6 +661,8 @@ function ExportPage() {
|
|||||||
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
|
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
|
||||||
const contactsListRef = useRef<HTMLDivElement>(null)
|
const contactsListRef = useRef<HTMLDivElement>(null)
|
||||||
const detailRequestSeqRef = useRef(0)
|
const detailRequestSeqRef = useRef(0)
|
||||||
|
const inProgressSessionIdsRef = useRef<string[]>([])
|
||||||
|
const hasBaseConfigReadyRef = useRef(false)
|
||||||
|
|
||||||
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
||||||
if (exportCacheScopeReadyRef.current) {
|
if (exportCacheScopeReadyRef.current) {
|
||||||
@@ -1033,8 +1042,9 @@ function ExportPage() {
|
|||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [isExportRoute])
|
}, [isExportRoute])
|
||||||
|
|
||||||
const loadBaseConfig = useCallback(async () => {
|
const loadBaseConfig = useCallback(async (): Promise<boolean> => {
|
||||||
setIsBaseConfigLoading(true)
|
setIsBaseConfigLoading(true)
|
||||||
|
let isReady = true
|
||||||
try {
|
try {
|
||||||
const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedWriteLayout, savedSessionMap, savedContentMap, savedSnsPostCount, exportCacheScope] = await Promise.all([
|
const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedWriteLayout, savedSessionMap, savedContentMap, savedSnsPostCount, exportCacheScope] = await Promise.all([
|
||||||
configService.getExportPath(),
|
configService.getExportPath(),
|
||||||
@@ -1085,10 +1095,15 @@ function ExportPage() {
|
|||||||
exportConcurrency: savedConcurrency ?? prev.exportConcurrency
|
exportConcurrency: savedConcurrency ?? prev.exportConcurrency
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
isReady = false
|
||||||
console.error('加载导出配置失败:', error)
|
console.error('加载导出配置失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsBaseConfigLoading(false)
|
setIsBaseConfigLoading(false)
|
||||||
}
|
}
|
||||||
|
if (isReady) {
|
||||||
|
hasBaseConfigReadyRef.current = true
|
||||||
|
}
|
||||||
|
return isReady
|
||||||
}, [ensureExportCacheScope])
|
}, [ensureExportCacheScope])
|
||||||
|
|
||||||
const loadSnsStats = useCallback(async (options?: { full?: boolean; silent?: boolean }) => {
|
const loadSnsStats = useCallback(async (options?: { full?: boolean; silent?: boolean }) => {
|
||||||
@@ -1475,7 +1490,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
const clearSelection = () => setSelectedSessions(new Set())
|
const clearSelection = () => setSelectedSessions(new Set())
|
||||||
|
|
||||||
const openExportDialog = (payload: Omit<ExportDialogState, 'open'>) => {
|
const openExportDialog = useCallback((payload: Omit<ExportDialogState, 'open'>) => {
|
||||||
setExportDialog({ open: true, ...payload })
|
setExportDialog({ open: true, ...payload })
|
||||||
|
|
||||||
setOptions(prev => {
|
setOptions(prev => {
|
||||||
@@ -1516,11 +1531,63 @@ function ExportPage() {
|
|||||||
|
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const closeExportDialog = useCallback(() => {
|
||||||
|
setExportDialog(prev => ({ ...prev, open: false }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = onOpenSingleExport((payload) => {
|
||||||
|
void (async () => {
|
||||||
|
const sessionId = typeof payload?.sessionId === 'string'
|
||||||
|
? payload.sessionId.trim()
|
||||||
|
: ''
|
||||||
|
if (!sessionId) return
|
||||||
|
|
||||||
|
const sessionName = typeof payload?.sessionName === 'string'
|
||||||
|
? payload.sessionName.trim()
|
||||||
|
: ''
|
||||||
|
const displayName = sessionName || sessionId
|
||||||
|
const requestId = typeof payload?.requestId === 'string'
|
||||||
|
? payload.requestId.trim()
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const emitStatus = (
|
||||||
|
status: 'initializing' | 'opened' | 'failed',
|
||||||
|
message?: string
|
||||||
|
) => {
|
||||||
|
if (!requestId) return
|
||||||
|
emitSingleExportDialogStatus({ requestId, status, message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeExportDialog = () => {
|
try {
|
||||||
setExportDialog(prev => ({ ...prev, open: false }))
|
if (!hasBaseConfigReadyRef.current) {
|
||||||
|
emitStatus('initializing')
|
||||||
|
const ready = await loadBaseConfig()
|
||||||
|
if (!ready) {
|
||||||
|
emitStatus('failed', '导出模块初始化失败,请重试')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedSessions(new Set([sessionId]))
|
||||||
|
openExportDialog({
|
||||||
|
scope: 'single',
|
||||||
|
sessionIds: [sessionId],
|
||||||
|
sessionNames: [displayName],
|
||||||
|
title: `导出会话:${displayName}`
|
||||||
|
})
|
||||||
|
emitStatus('opened')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('聊天页唤起导出弹窗失败:', error)
|
||||||
|
emitStatus('failed', String(error))
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
|
}, [loadBaseConfig, openExportDialog])
|
||||||
|
|
||||||
const buildExportOptions = (scope: TaskScope, contentType?: ContentType): ElectronExportOptions => {
|
const buildExportOptions = (scope: TaskScope, contentType?: ContentType): ElectronExportOptions => {
|
||||||
const sessionLayout: SessionLayout = writeLayout === 'C' ? 'per-session' : 'shared'
|
const sessionLayout: SessionLayout = writeLayout === 'C' ? 'per-session' : 'shared'
|
||||||
@@ -2103,6 +2170,41 @@ function ExportPage() {
|
|||||||
return set
|
return set
|
||||||
}, [tasks])
|
}, [tasks])
|
||||||
|
|
||||||
|
const inProgressSessionIds = useMemo(() => {
|
||||||
|
const set = new Set<string>()
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.status !== 'running' && task.status !== 'queued') continue
|
||||||
|
for (const id of task.payload.sessionIds) {
|
||||||
|
set.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(set).sort()
|
||||||
|
}, [tasks])
|
||||||
|
|
||||||
|
const inProgressSessionIdsKey = useMemo(
|
||||||
|
() => inProgressSessionIds.join('||'),
|
||||||
|
[inProgressSessionIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inProgressSessionIdsRef.current = inProgressSessionIds
|
||||||
|
}, [inProgressSessionIds])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
emitExportSessionStatus({
|
||||||
|
inProgressSessionIds: inProgressSessionIdsRef.current
|
||||||
|
})
|
||||||
|
}, [inProgressSessionIdsKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = onExportSessionStatusRequest(() => {
|
||||||
|
emitExportSessionStatus({
|
||||||
|
inProgressSessionIds: inProgressSessionIdsRef.current
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return unsubscribe
|
||||||
|
}, [])
|
||||||
|
|
||||||
const runningCardTypes = useMemo(() => {
|
const runningCardTypes = useMemo(() => {
|
||||||
const set = new Set<ContentCardType>()
|
const set = new Set<ContentCardType>()
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
@@ -3223,7 +3325,7 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exportDialog.open && (
|
{exportDialog.open && createPortal(
|
||||||
<div className="export-dialog-overlay" onClick={closeExportDialog}>
|
<div className="export-dialog-overlay" onClick={closeExportDialog}>
|
||||||
<div className="export-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
|
<div className="export-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
|
||||||
<div className="dialog-header">
|
<div className="dialog-header">
|
||||||
@@ -3365,7 +3467,8 @@ function ExportPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
84
src/services/exportBridge.ts
Normal file
84
src/services/exportBridge.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export interface OpenSingleExportPayload {
|
||||||
|
sessionId: string
|
||||||
|
sessionName?: string
|
||||||
|
requestId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportSessionStatusPayload {
|
||||||
|
inProgressSessionIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleExportDialogStatusPayload {
|
||||||
|
requestId: string
|
||||||
|
status: 'initializing' | 'opened' | 'failed'
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPEN_SINGLE_EXPORT_EVENT = 'weflow:open-single-export'
|
||||||
|
const EXPORT_SESSION_STATUS_EVENT = 'weflow:export-session-status'
|
||||||
|
const EXPORT_SESSION_STATUS_REQUEST_EVENT = 'weflow:export-session-status-request'
|
||||||
|
const SINGLE_EXPORT_DIALOG_STATUS_EVENT = 'weflow:single-export-dialog-status'
|
||||||
|
|
||||||
|
export const emitOpenSingleExport = (payload: OpenSingleExportPayload) => {
|
||||||
|
window.dispatchEvent(new CustomEvent<OpenSingleExportPayload>(OPEN_SINGLE_EXPORT_EVENT, {
|
||||||
|
detail: payload
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onOpenSingleExport = (
|
||||||
|
listener: (payload: OpenSingleExportPayload) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<OpenSingleExportPayload>
|
||||||
|
listener(customEvent.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(OPEN_SINGLE_EXPORT_EVENT, handler as EventListener)
|
||||||
|
return () => window.removeEventListener(OPEN_SINGLE_EXPORT_EVENT, handler as EventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emitExportSessionStatus = (payload: ExportSessionStatusPayload) => {
|
||||||
|
window.dispatchEvent(new CustomEvent<ExportSessionStatusPayload>(EXPORT_SESSION_STATUS_EVENT, {
|
||||||
|
detail: payload
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onExportSessionStatus = (
|
||||||
|
listener: (payload: ExportSessionStatusPayload) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<ExportSessionStatusPayload>
|
||||||
|
listener(customEvent.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(EXPORT_SESSION_STATUS_EVENT, handler as EventListener)
|
||||||
|
return () => window.removeEventListener(EXPORT_SESSION_STATUS_EVENT, handler as EventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestExportSessionStatus = () => {
|
||||||
|
window.dispatchEvent(new CustomEvent(EXPORT_SESSION_STATUS_REQUEST_EVENT))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onExportSessionStatusRequest = (listener: () => void): (() => void) => {
|
||||||
|
const handler = () => listener()
|
||||||
|
window.addEventListener(EXPORT_SESSION_STATUS_REQUEST_EVENT, handler)
|
||||||
|
return () => window.removeEventListener(EXPORT_SESSION_STATUS_REQUEST_EVENT, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emitSingleExportDialogStatus = (payload: SingleExportDialogStatusPayload) => {
|
||||||
|
window.dispatchEvent(new CustomEvent<SingleExportDialogStatusPayload>(SINGLE_EXPORT_DIALOG_STATUS_EVENT, {
|
||||||
|
detail: payload
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onSingleExportDialogStatus = (
|
||||||
|
listener: (payload: SingleExportDialogStatusPayload) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<SingleExportDialogStatusPayload>
|
||||||
|
listener(customEvent.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(SINGLE_EXPORT_DIALOG_STATUS_EVENT, handler as EventListener)
|
||||||
|
return () => window.removeEventListener(SINGLE_EXPORT_DIALOG_STATUS_EVENT, handler as EventListener)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user