mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(export): move task center into top control row
This commit is contained in:
@@ -24,8 +24,9 @@
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(300px, 1fr) 320px;
|
grid-template-columns: minmax(320px, 1.45fr) minmax(220px, 0.8fr) minmax(260px, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -189,6 +190,54 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-center-control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-center-inline {
|
||||||
|
min-height: 40px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-collapse-btn {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-card-grid {
|
.content-card-grid {
|
||||||
@@ -276,51 +325,7 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.task-center-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-summary {
|
|
||||||
margin-left: auto;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-collapse-btn {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--primary);
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-empty {
|
.task-empty {
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -329,7 +334,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-list {
|
.task-list {
|
||||||
margin-top: 10px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-height: 190px;
|
max-height: 190px;
|
||||||
@@ -1099,6 +1103,12 @@
|
|||||||
.path-inline-row > .secondary-btn {
|
.path-inline-row > .secondary-btn {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-center-inline {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-card-grid {
|
.content-card-grid {
|
||||||
|
|||||||
@@ -242,11 +242,13 @@ const timestampOrDash = (timestamp?: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
const MESSAGE_COUNT_VIEWPORT_PREFETCH = 180
|
const MESSAGE_COUNT_VIEWPORT_PREFETCH = 90
|
||||||
const MESSAGE_COUNT_ACTIVE_TAB_WARMUP_LIMIT = 960
|
const MESSAGE_COUNT_ACTIVE_TAB_WARMUP_LIMIT = 240
|
||||||
const METRICS_VIEWPORT_PREFETCH = 90
|
const MESSAGE_COUNT_REQUEST_BATCH = 120
|
||||||
const METRICS_BACKGROUND_BATCH = 40
|
const METRICS_VIEWPORT_PREFETCH = 60
|
||||||
const METRICS_BACKGROUND_INTERVAL_MS = 220
|
const METRICS_REQUEST_BATCH = 24
|
||||||
|
const METRICS_BACKGROUND_BATCH = 20
|
||||||
|
const METRICS_BACKGROUND_INTERVAL_MS = 500
|
||||||
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
||||||
const EXPORT_SESSION_COUNT_CACHE_STALE_MS = 48 * 60 * 60 * 1000
|
const EXPORT_SESSION_COUNT_CACHE_STALE_MS = 48 * 60 * 60 * 1000
|
||||||
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
||||||
@@ -393,6 +395,11 @@ function ExportPage() {
|
|||||||
const sessionLoadTokenRef = useRef(0)
|
const sessionLoadTokenRef = useRef(0)
|
||||||
const loadingMessageCountsRef = useRef<Set<string>>(new Set())
|
const loadingMessageCountsRef = useRef<Set<string>>(new Set())
|
||||||
const loadingMetricsRef = useRef<Set<string>>(new Set())
|
const loadingMetricsRef = useRef<Set<string>>(new Set())
|
||||||
|
const pendingMessageCountsRef = useRef<Set<string>>(new Set())
|
||||||
|
const pendingMetricsRef = useRef<Set<string>>(new Set())
|
||||||
|
const messageCountPumpRunningRef = useRef(false)
|
||||||
|
const metricsPumpRunningRef = useRef(false)
|
||||||
|
const isExportRouteRef = useRef(isExportRoute)
|
||||||
const preselectAppliedRef = useRef(false)
|
const preselectAppliedRef = useRef(false)
|
||||||
const visibleSessionsRef = useRef<SessionRow[]>([])
|
const visibleSessionsRef = useRef<SessionRow[]>([])
|
||||||
const exportCacheScopeRef = useRef('default')
|
const exportCacheScopeRef = useRef('default')
|
||||||
@@ -415,6 +422,10 @@ function ExportPage() {
|
|||||||
sessionMetricsRef.current = sessionMetrics
|
sessionMetricsRef.current = sessionMetrics
|
||||||
}, [sessionMetrics])
|
}, [sessionMetrics])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isExportRouteRef.current = isExportRoute
|
||||||
|
}, [isExportRoute])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persistSessionCountTimerRef.current) {
|
if (persistSessionCountTimerRef.current) {
|
||||||
window.clearTimeout(persistSessionCountTimerRef.current)
|
window.clearTimeout(persistSessionCountTimerRef.current)
|
||||||
@@ -452,9 +463,10 @@ function ExportPage() {
|
|||||||
}, [location.state])
|
}, [location.state])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isExportRoute) return
|
||||||
const timer = setInterval(() => setNowTick(Date.now()), 60 * 1000)
|
const timer = setInterval(() => setNowTick(Date.now()), 60 * 1000)
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [])
|
}, [isExportRoute])
|
||||||
|
|
||||||
const loadBaseConfig = useCallback(async () => {
|
const loadBaseConfig = useCallback(async () => {
|
||||||
setIsBaseConfigLoading(true)
|
setIsBaseConfigLoading(true)
|
||||||
@@ -581,6 +593,8 @@ function ExportPage() {
|
|||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
loadingMessageCountsRef.current.clear()
|
loadingMessageCountsRef.current.clear()
|
||||||
loadingMetricsRef.current.clear()
|
loadingMetricsRef.current.clear()
|
||||||
|
pendingMessageCountsRef.current.clear()
|
||||||
|
pendingMetricsRef.current.clear()
|
||||||
sessionMetricsRef.current = {}
|
sessionMetricsRef.current = {}
|
||||||
setSessionMetrics({})
|
setSessionMetrics({})
|
||||||
|
|
||||||
@@ -632,6 +646,7 @@ function ExportPage() {
|
|||||||
setIsSessionEnriching(true)
|
setIsSessionEnriching(true)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
if (isStale()) return
|
||||||
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
@@ -650,6 +665,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
||||||
if (needsEnrichment.length > 0) {
|
if (needsEnrichment.length > 0) {
|
||||||
|
if (isStale()) return
|
||||||
const enrichResult = await withTimeout(
|
const enrichResult = await withTimeout(
|
||||||
window.electronAPI.chat.enrichSessionsContactInfo(needsEnrichment),
|
window.electronAPI.chat.enrichSessionsContactInfo(needsEnrichment),
|
||||||
CONTACT_ENRICH_TIMEOUT_MS
|
CONTACT_ENRICH_TIMEOUT_MS
|
||||||
@@ -714,6 +730,8 @@ function ExportPage() {
|
|||||||
sessionLoadTokenRef.current = Date.now()
|
sessionLoadTokenRef.current = Date.now()
|
||||||
loadingMessageCountsRef.current.clear()
|
loadingMessageCountsRef.current.clear()
|
||||||
loadingMetricsRef.current.clear()
|
loadingMetricsRef.current.clear()
|
||||||
|
pendingMessageCountsRef.current.clear()
|
||||||
|
pendingMetricsRef.current.clear()
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
}, [isExportRoute])
|
}, [isExportRoute])
|
||||||
|
|
||||||
@@ -769,38 +787,50 @@ function ExportPage() {
|
|||||||
}, [visibleSessions])
|
}, [visibleSessions])
|
||||||
|
|
||||||
const ensureSessionMessageCounts = useCallback(async (targetSessions: SessionRow[]) => {
|
const ensureSessionMessageCounts = useCallback(async (targetSessions: SessionRow[]) => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRouteRef.current) return
|
||||||
const loadTokenAtStart = sessionLoadTokenRef.current
|
|
||||||
const currentCounts = sessionMessageCountsRef.current
|
const currentCounts = sessionMessageCountsRef.current
|
||||||
const pending = targetSessions.filter(
|
for (const session of targetSessions) {
|
||||||
session => currentCounts[session.username] === undefined && !loadingMessageCountsRef.current.has(session.username)
|
if (currentCounts[session.username] !== undefined) continue
|
||||||
)
|
if (loadingMessageCountsRef.current.has(session.username)) continue
|
||||||
if (pending.length === 0) return
|
pendingMessageCountsRef.current.add(session.username)
|
||||||
for (const session of pending) {
|
|
||||||
loadingMessageCountsRef.current.add(session.username)
|
|
||||||
}
|
}
|
||||||
|
if (pendingMessageCountsRef.current.size === 0 || messageCountPumpRunningRef.current) return
|
||||||
|
|
||||||
|
messageCountPumpRunningRef.current = true
|
||||||
|
const loadTokenAtStart = sessionLoadTokenRef.current
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const batchSize = pending.length > 260 ? 260 : pending.length
|
while (isExportRouteRef.current && loadTokenAtStart === sessionLoadTokenRef.current) {
|
||||||
for (let i = 0; i < pending.length; i += batchSize) {
|
const ids = Array.from(pendingMessageCountsRef.current).slice(0, MESSAGE_COUNT_REQUEST_BATCH)
|
||||||
if (loadTokenAtStart !== sessionLoadTokenRef.current) return
|
if (ids.length === 0) break
|
||||||
const chunk = pending.slice(i, i + batchSize)
|
|
||||||
const ids = chunk.map(session => session.username)
|
for (const id of ids) {
|
||||||
|
pendingMessageCountsRef.current.delete(id)
|
||||||
|
loadingMessageCountsRef.current.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
const chunkUpdates: Record<string, number> = {}
|
const chunkUpdates: Record<string, number> = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await withTimeout(window.electronAPI.chat.getSessionMessageCounts(ids), 10000)
|
const result = await withTimeout(window.electronAPI.chat.getSessionMessageCounts(ids), 10000)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
continue
|
for (const id of ids) {
|
||||||
}
|
chunkUpdates[id] = 0
|
||||||
for (const session of chunk) {
|
}
|
||||||
const value = result?.success && result.counts ? result.counts[session.username] : undefined
|
} else {
|
||||||
chunkUpdates[session.username] = typeof value === 'number' ? value : 0
|
for (const id of ids) {
|
||||||
|
const value = result?.success && result.counts ? result.counts[id] : undefined
|
||||||
|
chunkUpdates[id] = typeof value === 'number' ? value : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载会话总消息数失败:', error)
|
console.error('加载会话总消息数失败:', error)
|
||||||
for (const session of chunk) {
|
for (const id of ids) {
|
||||||
chunkUpdates[session.username] = 0
|
chunkUpdates[id] = 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (const id of ids) {
|
||||||
|
loadingMessageCountsRef.current.delete(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,72 +839,95 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
for (const session of pending) {
|
messageCountPumpRunningRef.current = false
|
||||||
loadingMessageCountsRef.current.delete(session.username)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isExportRoute])
|
}, [])
|
||||||
|
|
||||||
const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => {
|
const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRouteRef.current) return
|
||||||
const loadTokenAtStart = sessionLoadTokenRef.current
|
|
||||||
const currentMetrics = sessionMetricsRef.current
|
const currentMetrics = sessionMetricsRef.current
|
||||||
const pending = targetSessions.filter(session => !currentMetrics[session.username] && !loadingMetricsRef.current.has(session.username))
|
for (const session of targetSessions) {
|
||||||
if (pending.length === 0) return
|
if (currentMetrics[session.username]) continue
|
||||||
|
if (loadingMetricsRef.current.has(session.username)) continue
|
||||||
const updates: Record<string, SessionMetrics> = {}
|
pendingMetricsRef.current.add(session.username)
|
||||||
for (const session of pending) {
|
|
||||||
loadingMetricsRef.current.add(session.username)
|
|
||||||
}
|
}
|
||||||
|
if (pendingMetricsRef.current.size === 0 || metricsPumpRunningRef.current) return
|
||||||
|
|
||||||
|
metricsPumpRunningRef.current = true
|
||||||
|
const loadTokenAtStart = sessionLoadTokenRef.current
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const batchSize = 80
|
while (isExportRouteRef.current && loadTokenAtStart === sessionLoadTokenRef.current) {
|
||||||
for (let i = 0; i < pending.length; i += batchSize) {
|
const ids = Array.from(pendingMetricsRef.current).slice(0, METRICS_REQUEST_BATCH)
|
||||||
if (loadTokenAtStart !== sessionLoadTokenRef.current) return
|
if (ids.length === 0) break
|
||||||
const chunk = pending.slice(i, i + batchSize)
|
|
||||||
const ids = chunk.map(session => session.username)
|
for (const id of ids) {
|
||||||
|
pendingMetricsRef.current.delete(id)
|
||||||
|
loadingMetricsRef.current.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Record<string, SessionMetrics> = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const statsResult = await window.electronAPI.chat.getExportSessionStats(ids)
|
const statsResult = await window.electronAPI.chat.getExportSessionStats(ids)
|
||||||
if (!statsResult.success || !statsResult.data) {
|
if (!statsResult.success || !statsResult.data) {
|
||||||
console.error('加载会话统计失败:', statsResult.error || '未知错误')
|
console.error('加载会话统计失败:', statsResult.error || '未知错误')
|
||||||
continue
|
for (const id of ids) {
|
||||||
}
|
updates[id] = {
|
||||||
|
totalMessages: 0,
|
||||||
for (const session of chunk) {
|
voiceMessages: 0,
|
||||||
const raw = statsResult.data[session.username]
|
imageMessages: 0,
|
||||||
// 成功响应但无明细时按 0 回填,避免该行反复重试导致滚动抖动。
|
videoMessages: 0,
|
||||||
updates[session.username] = {
|
emojiMessages: 0
|
||||||
totalMessages: raw?.totalMessages ?? 0,
|
}
|
||||||
voiceMessages: raw?.voiceMessages ?? 0,
|
}
|
||||||
imageMessages: raw?.imageMessages ?? 0,
|
} else {
|
||||||
videoMessages: raw?.videoMessages ?? 0,
|
for (const id of ids) {
|
||||||
emojiMessages: raw?.emojiMessages ?? 0,
|
const raw = statsResult.data[id]
|
||||||
privateMutualGroups: raw?.privateMutualGroups,
|
// 成功响应但无明细时按 0 回填,避免该行反复重试导致滚动抖动。
|
||||||
groupMemberCount: raw?.groupMemberCount,
|
updates[id] = {
|
||||||
groupMyMessages: raw?.groupMyMessages,
|
totalMessages: raw?.totalMessages ?? 0,
|
||||||
groupActiveSpeakers: raw?.groupActiveSpeakers,
|
voiceMessages: raw?.voiceMessages ?? 0,
|
||||||
groupMutualFriends: raw?.groupMutualFriends,
|
imageMessages: raw?.imageMessages ?? 0,
|
||||||
firstTimestamp: raw?.firstTimestamp,
|
videoMessages: raw?.videoMessages ?? 0,
|
||||||
lastTimestamp: raw?.lastTimestamp
|
emojiMessages: raw?.emojiMessages ?? 0,
|
||||||
|
privateMutualGroups: raw?.privateMutualGroups,
|
||||||
|
groupMemberCount: raw?.groupMemberCount,
|
||||||
|
groupMyMessages: raw?.groupMyMessages,
|
||||||
|
groupActiveSpeakers: raw?.groupActiveSpeakers,
|
||||||
|
groupMutualFriends: raw?.groupMutualFriends,
|
||||||
|
firstTimestamp: raw?.firstTimestamp,
|
||||||
|
lastTimestamp: raw?.lastTimestamp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载会话统计分批失败:', error)
|
console.error('加载会话统计分批失败:', error)
|
||||||
|
for (const id of ids) {
|
||||||
|
updates[id] = {
|
||||||
|
totalMessages: 0,
|
||||||
|
voiceMessages: 0,
|
||||||
|
imageMessages: 0,
|
||||||
|
videoMessages: 0,
|
||||||
|
emojiMessages: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (const id of ids) {
|
||||||
|
loadingMetricsRef.current.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(updates).length > 0) {
|
||||||
|
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载会话统计失败:', error)
|
console.error('加载会话统计失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
for (const session of pending) {
|
metricsPumpRunningRef.current = false
|
||||||
loadingMetricsRef.current.delete(session.username)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(updates).length > 0) {
|
|
||||||
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
|
||||||
}
|
|
||||||
}, [isExportRoute])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRoute) return
|
||||||
@@ -1660,9 +1713,72 @@ function ExportPage() {
|
|||||||
await configService.setExportWriteLayout(value)
|
await configService.setExportWriteLayout(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="task-center-control">
|
||||||
|
<span className="control-label">任务中心</span>
|
||||||
|
<div className="task-center-inline">
|
||||||
|
<div className="task-summary">
|
||||||
|
<span>进行中 {taskRunningCount}</span>
|
||||||
|
<span>排队 {taskQueuedCount}</span>
|
||||||
|
<span>总计 {tasks.length}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="task-collapse-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsTaskCenterExpanded(prev => !prev)}
|
||||||
|
>
|
||||||
|
{isTaskCenterExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
{isTaskCenterExpanded ? '收起' : '展开'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isTaskCenterExpanded && (
|
||||||
|
<div className="task-center expanded">
|
||||||
|
{tasks.length === 0 ? (
|
||||||
|
<div className="task-empty">暂无任务。点击会话导出或卡片导出后会在这里创建任务。</div>
|
||||||
|
) : (
|
||||||
|
<div className="task-list">
|
||||||
|
{tasks.map(task => (
|
||||||
|
<div key={task.id} className={`task-card ${task.status}`}>
|
||||||
|
<div className="task-main">
|
||||||
|
<div className="task-title">{task.title}</div>
|
||||||
|
<div className="task-meta">
|
||||||
|
<span className={`task-status ${task.status}`}>{task.status === 'queued' ? '排队中' : task.status === 'running' ? '进行中' : task.status === 'success' ? '已完成' : '失败'}</span>
|
||||||
|
<span>{new Date(task.createdAt).toLocaleString('zh-CN')}</span>
|
||||||
|
</div>
|
||||||
|
{task.status === 'running' && (
|
||||||
|
<>
|
||||||
|
<div className="task-progress-bar">
|
||||||
|
<div
|
||||||
|
className="task-progress-fill"
|
||||||
|
style={{ width: `${task.progress.total > 0 ? (task.progress.current / task.progress.total) * 100 : 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="task-progress-text">
|
||||||
|
{task.progress.total > 0
|
||||||
|
? `${task.progress.current} / ${task.progress.total}`
|
||||||
|
: '处理中'}
|
||||||
|
{task.progress.phaseLabel ? ` · ${task.progress.phaseLabel}` : ''}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{task.status === 'error' && <div className="task-error">{task.error || '任务失败'}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="task-actions">
|
||||||
|
<button className="secondary-btn" onClick={() => exportFolder && void window.electronAPI.shell.openPath(exportFolder)}>
|
||||||
|
<FolderOpen size={14} /> 目录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="content-card-grid">
|
<div className="content-card-grid">
|
||||||
{contentCards.map(card => {
|
{contentCards.map(card => {
|
||||||
const Icon = card.icon
|
const Icon = card.icon
|
||||||
@@ -1705,65 +1821,6 @@ function ExportPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`task-center ${isTaskCenterExpanded ? 'expanded' : 'collapsed'}`}>
|
|
||||||
<div className="task-center-header">
|
|
||||||
<div className="section-title">任务中心</div>
|
|
||||||
<div className="task-summary">
|
|
||||||
<span>进行中 {taskRunningCount}</span>
|
|
||||||
<span>排队 {taskQueuedCount}</span>
|
|
||||||
<span>总计 {tasks.length}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="task-collapse-btn"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsTaskCenterExpanded(prev => !prev)}
|
|
||||||
>
|
|
||||||
{isTaskCenterExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
||||||
{isTaskCenterExpanded ? '收起' : '展开'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isTaskCenterExpanded && (tasks.length === 0 ? (
|
|
||||||
<div className="task-empty">暂无任务。点击会话导出或卡片导出后会在这里创建任务。</div>
|
|
||||||
) : (
|
|
||||||
<div className="task-list">
|
|
||||||
{tasks.map(task => (
|
|
||||||
<div key={task.id} className={`task-card ${task.status}`}>
|
|
||||||
<div className="task-main">
|
|
||||||
<div className="task-title">{task.title}</div>
|
|
||||||
<div className="task-meta">
|
|
||||||
<span className={`task-status ${task.status}`}>{task.status === 'queued' ? '排队中' : task.status === 'running' ? '进行中' : task.status === 'success' ? '已完成' : '失败'}</span>
|
|
||||||
<span>{new Date(task.createdAt).toLocaleString('zh-CN')}</span>
|
|
||||||
</div>
|
|
||||||
{task.status === 'running' && (
|
|
||||||
<>
|
|
||||||
<div className="task-progress-bar">
|
|
||||||
<div
|
|
||||||
className="task-progress-fill"
|
|
||||||
style={{ width: `${task.progress.total > 0 ? (task.progress.current / task.progress.total) * 100 : 0}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="task-progress-text">
|
|
||||||
{task.progress.total > 0
|
|
||||||
? `${task.progress.current} / ${task.progress.total}`
|
|
||||||
: '处理中'}
|
|
||||||
{task.progress.phaseLabel ? ` · ${task.progress.phaseLabel}` : ''}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{task.status === 'error' && <div className="task-error">{task.error || '任务失败'}</div>}
|
|
||||||
</div>
|
|
||||||
<div className="task-actions">
|
|
||||||
<button className="secondary-btn" onClick={() => exportFolder && void window.electronAPI.shell.openPath(exportFolder)}>
|
|
||||||
<FolderOpen size={14} /> 目录
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="session-table-section">
|
<div className="session-table-section">
|
||||||
<div className="table-toolbar">
|
<div className="table-toolbar">
|
||||||
<div className="table-tabs" role="tablist" aria-label="会话类型">
|
<div className="table-tabs" role="tablist" aria-label="会话类型">
|
||||||
|
|||||||
Reference in New Issue
Block a user