mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
优化选择
This commit is contained in:
@@ -212,6 +212,11 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,11 +256,23 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled:hover {
|
||||
border-color: transparent;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&.outside {
|
||||
color: var(--text-quaternary);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.35;
|
||||
transform: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.in-range {
|
||||
background: rgba(var(--primary-rgb), 0.1);
|
||||
color: var(--primary);
|
||||
|
||||
@@ -25,6 +25,8 @@ interface ExportDateRangeDialogProps {
|
||||
open: boolean
|
||||
value: ExportDateRangeSelection
|
||||
title?: string
|
||||
minDate?: Date | null
|
||||
maxDate?: Date | null
|
||||
onClose: () => void
|
||||
onConfirm: (value: ExportDateRangeSelection) => void
|
||||
}
|
||||
@@ -35,19 +37,65 @@ interface ExportDateRangeDialogDraft extends ExportDateRangeSelection {
|
||||
panelMonth: Date
|
||||
}
|
||||
|
||||
const buildDialogDraft = (value: ExportDateRangeSelection): ExportDateRangeDialogDraft => ({
|
||||
...cloneExportDateRangeSelection(value),
|
||||
panelMonth: toMonthStart(value.dateRange.start)
|
||||
})
|
||||
const resolveBounds = (minDate?: Date | null, maxDate?: Date | null): { minDate: Date; maxDate: Date } | null => {
|
||||
if (!(minDate instanceof Date) || Number.isNaN(minDate.getTime())) return null
|
||||
if (!(maxDate instanceof Date) || Number.isNaN(maxDate.getTime())) return null
|
||||
const normalizedMin = startOfDay(minDate)
|
||||
const normalizedMax = endOfDay(maxDate)
|
||||
if (normalizedMin.getTime() > normalizedMax.getTime()) return null
|
||||
return {
|
||||
minDate: normalizedMin,
|
||||
maxDate: normalizedMax
|
||||
}
|
||||
}
|
||||
|
||||
const clampSelectionToBounds = (
|
||||
value: ExportDateRangeSelection,
|
||||
minDate?: Date | null,
|
||||
maxDate?: Date | null
|
||||
): ExportDateRangeSelection => {
|
||||
const bounds = resolveBounds(minDate, maxDate)
|
||||
if (!bounds) return cloneExportDateRangeSelection(value)
|
||||
|
||||
const rawStart = value.useAllTime ? bounds.minDate : startOfDay(value.dateRange.start)
|
||||
const rawEnd = value.useAllTime ? bounds.maxDate : endOfDay(value.dateRange.end)
|
||||
const nextStart = new Date(Math.min(Math.max(rawStart.getTime(), bounds.minDate.getTime()), bounds.maxDate.getTime()))
|
||||
const nextEndCandidate = new Date(Math.min(Math.max(rawEnd.getTime(), bounds.minDate.getTime()), bounds.maxDate.getTime()))
|
||||
const nextEnd = nextEndCandidate.getTime() < nextStart.getTime() ? endOfDay(nextStart) : nextEndCandidate
|
||||
const changed = nextStart.getTime() !== rawStart.getTime() || nextEnd.getTime() !== rawEnd.getTime()
|
||||
|
||||
return {
|
||||
preset: value.useAllTime ? value.preset : (changed ? 'custom' : value.preset),
|
||||
useAllTime: value.useAllTime,
|
||||
dateRange: {
|
||||
start: nextStart,
|
||||
end: nextEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildDialogDraft = (
|
||||
value: ExportDateRangeSelection,
|
||||
minDate?: Date | null,
|
||||
maxDate?: Date | null
|
||||
): ExportDateRangeDialogDraft => {
|
||||
const nextValue = clampSelectionToBounds(value, minDate, maxDate)
|
||||
return {
|
||||
...nextValue,
|
||||
panelMonth: toMonthStart(nextValue.dateRange.start)
|
||||
}
|
||||
}
|
||||
|
||||
export function ExportDateRangeDialog({
|
||||
open,
|
||||
value,
|
||||
title = '时间范围设置',
|
||||
minDate,
|
||||
maxDate,
|
||||
onClose,
|
||||
onConfirm
|
||||
}: ExportDateRangeDialogProps) {
|
||||
const [draft, setDraft] = useState<ExportDateRangeDialogDraft>(() => buildDialogDraft(value))
|
||||
const [draft, setDraft] = useState<ExportDateRangeDialogDraft>(() => buildDialogDraft(value, minDate, maxDate))
|
||||
const [activeBoundary, setActiveBoundary] = useState<ActiveBoundary>('start')
|
||||
const [dateInput, setDateInput] = useState({
|
||||
start: formatDateInputValue(value.dateRange.start),
|
||||
@@ -57,7 +105,7 @@ export function ExportDateRangeDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const nextDraft = buildDialogDraft(value)
|
||||
const nextDraft = buildDialogDraft(value, minDate, maxDate)
|
||||
setDraft(nextDraft)
|
||||
setActiveBoundary('start')
|
||||
setDateInput({
|
||||
@@ -65,7 +113,7 @@ export function ExportDateRangeDialog({
|
||||
end: formatDateInputValue(nextDraft.dateRange.end)
|
||||
})
|
||||
setDateInputError({ start: false, end: false })
|
||||
}, [open, value])
|
||||
}, [maxDate, minDate, open, value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
@@ -76,8 +124,24 @@ export function ExportDateRangeDialog({
|
||||
setDateInputError({ start: false, end: false })
|
||||
}, [draft.dateRange.end.getTime(), draft.dateRange.start.getTime(), open])
|
||||
|
||||
const setRangeStart = useCallback((targetDate: Date) => {
|
||||
const bounds = useMemo(() => resolveBounds(minDate, maxDate), [maxDate, minDate])
|
||||
const clampStartDate = useCallback((targetDate: Date) => {
|
||||
const start = startOfDay(targetDate)
|
||||
if (!bounds) return start
|
||||
if (start.getTime() < bounds.minDate.getTime()) return bounds.minDate
|
||||
if (start.getTime() > bounds.maxDate.getTime()) return startOfDay(bounds.maxDate)
|
||||
return start
|
||||
}, [bounds])
|
||||
const clampEndDate = useCallback((targetDate: Date) => {
|
||||
const end = endOfDay(targetDate)
|
||||
if (!bounds) return end
|
||||
if (end.getTime() < bounds.minDate.getTime()) return endOfDay(bounds.minDate)
|
||||
if (end.getTime() > bounds.maxDate.getTime()) return bounds.maxDate
|
||||
return end
|
||||
}, [bounds])
|
||||
|
||||
const setRangeStart = useCallback((targetDate: Date) => {
|
||||
const start = clampStartDate(targetDate)
|
||||
setDraft(prev => {
|
||||
const nextEnd = prev.dateRange.end < start ? endOfDay(start) : prev.dateRange.end
|
||||
return {
|
||||
@@ -91,12 +155,12 @@ export function ExportDateRangeDialog({
|
||||
panelMonth: toMonthStart(start)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
}, [clampStartDate])
|
||||
|
||||
const setRangeEnd = useCallback((targetDate: Date) => {
|
||||
const end = endOfDay(targetDate)
|
||||
const end = clampEndDate(targetDate)
|
||||
setDraft(prev => {
|
||||
const nextStart = prev.useAllTime ? startOfDay(targetDate) : prev.dateRange.start
|
||||
const nextStart = prev.useAllTime ? clampStartDate(targetDate) : prev.dateRange.start
|
||||
const nextEnd = end < nextStart ? endOfDay(nextStart) : end
|
||||
return {
|
||||
...prev,
|
||||
@@ -109,11 +173,13 @@ export function ExportDateRangeDialog({
|
||||
panelMonth: toMonthStart(targetDate)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
}, [clampEndDate, clampStartDate])
|
||||
|
||||
const applyPreset = useCallback((preset: Exclude<ExportDateRangePreset, 'custom'>) => {
|
||||
if (preset === 'all') {
|
||||
const previewRange = createDefaultDateRange()
|
||||
const previewRange = bounds
|
||||
? { start: bounds.minDate, end: bounds.maxDate }
|
||||
: createDefaultDateRange()
|
||||
setDraft(prev => ({
|
||||
...prev,
|
||||
preset,
|
||||
@@ -125,7 +191,11 @@ export function ExportDateRangeDialog({
|
||||
return
|
||||
}
|
||||
|
||||
const range = createDateRangeByPreset(preset)
|
||||
const range = clampSelectionToBounds({
|
||||
preset,
|
||||
useAllTime: false,
|
||||
dateRange: createDateRangeByPreset(preset)
|
||||
}, minDate, maxDate).dateRange
|
||||
setDraft(prev => ({
|
||||
...prev,
|
||||
preset,
|
||||
@@ -134,7 +204,7 @@ export function ExportDateRangeDialog({
|
||||
panelMonth: toMonthStart(range.start)
|
||||
}))
|
||||
setActiveBoundary('start')
|
||||
}, [])
|
||||
}, [bounds, maxDate, minDate])
|
||||
|
||||
const commitStartFromInput = useCallback(() => {
|
||||
const parsed = parseDateInputValue(dateInput.start)
|
||||
@@ -200,6 +270,10 @@ export function ExportDateRangeDialog({
|
||||
}, [draft])
|
||||
|
||||
const calendarCells = useMemo(() => buildCalendarCells(draft.panelMonth), [draft.panelMonth])
|
||||
const minPanelMonth = bounds ? toMonthStart(bounds.minDate) : null
|
||||
const maxPanelMonth = bounds ? toMonthStart(bounds.maxDate) : null
|
||||
const canShiftPrev = !minPanelMonth || draft.panelMonth.getTime() > minPanelMonth.getTime()
|
||||
const canShiftNext = !maxPanelMonth || draft.panelMonth.getTime() < maxPanelMonth.getTime()
|
||||
|
||||
const isStartSelected = useCallback((date: Date) => (
|
||||
!draft.useAllTime && isSameDay(date, draft.dateRange.start)
|
||||
@@ -215,6 +289,12 @@ export function ExportDateRangeDialog({
|
||||
startOfDay(date).getTime() <= startOfDay(draft.dateRange.end).getTime()
|
||||
), [draft])
|
||||
|
||||
const isDateSelectable = useCallback((date: Date) => {
|
||||
if (!bounds) return true
|
||||
const target = startOfDay(date).getTime()
|
||||
return target >= startOfDay(bounds.minDate).getTime() && target <= startOfDay(bounds.maxDate).getTime()
|
||||
}, [bounds])
|
||||
|
||||
const hintText = draft.useAllTime
|
||||
? '选择开始或结束日期后,会自动切换为自定义时间范围'
|
||||
: (activeBoundary === 'start' ? '下一次点击将设置开始日期' : '下一次点击将设置结束日期')
|
||||
@@ -323,10 +403,10 @@ export function ExportDateRangeDialog({
|
||||
<strong>{formatCalendarMonthTitle(draft.panelMonth)}</strong>
|
||||
</div>
|
||||
<div className="export-date-range-calendar-nav">
|
||||
<button type="button" onClick={() => shiftPanelMonth(-1)} aria-label="上个月">
|
||||
<button type="button" onClick={() => shiftPanelMonth(-1)} aria-label="上个月" disabled={!canShiftPrev}>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<button type="button" onClick={() => shiftPanelMonth(1)} aria-label="下个月">
|
||||
<button type="button" onClick={() => shiftPanelMonth(1)} aria-label="下个月" disabled={!canShiftNext}>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -341,13 +421,16 @@ export function ExportDateRangeDialog({
|
||||
const startSelected = isStartSelected(cell.date)
|
||||
const endSelected = isEndSelected(cell.date)
|
||||
const inRange = isDateInRange(cell.date)
|
||||
const selectable = isDateSelectable(cell.date)
|
||||
return (
|
||||
<button
|
||||
key={cell.date.getTime()}
|
||||
type="button"
|
||||
disabled={!selectable}
|
||||
className={[
|
||||
'export-date-range-calendar-day',
|
||||
cell.inCurrentMonth ? '' : 'outside',
|
||||
selectable ? '' : 'disabled',
|
||||
inRange ? 'in-range' : '',
|
||||
startSelected ? 'range-start' : '',
|
||||
endSelected ? 'range-end' : '',
|
||||
|
||||
@@ -52,10 +52,13 @@ import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '..
|
||||
import type { SnsPost } from '../types/sns'
|
||||
import {
|
||||
cloneExportDateRange,
|
||||
cloneExportDateRangeSelection,
|
||||
createDefaultDateRange,
|
||||
createDefaultExportDateRangeSelection,
|
||||
getExportDateRangeLabel,
|
||||
resolveExportDateRangeConfig,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
type ExportDateRangeSelection
|
||||
} from '../utils/exportDateRange'
|
||||
import './ExportPage.scss'
|
||||
@@ -830,6 +833,13 @@ interface SessionContentMetric {
|
||||
transferMessages?: number
|
||||
redPacketMessages?: number
|
||||
callMessages?: number
|
||||
firstTimestamp?: number
|
||||
lastTimestamp?: number
|
||||
}
|
||||
|
||||
interface TimeRangeBounds {
|
||||
minDate: Date
|
||||
maxDate: Date
|
||||
}
|
||||
|
||||
interface SessionExportCacheMeta {
|
||||
@@ -1049,27 +1059,74 @@ const normalizeMessageCount = (value: unknown): number | undefined => {
|
||||
return Math.floor(parsed)
|
||||
}
|
||||
|
||||
const normalizeTimestampSeconds = (value: unknown): number | undefined => {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return undefined
|
||||
return Math.floor(parsed)
|
||||
}
|
||||
|
||||
const clampExportSelectionToBounds = (
|
||||
selection: ExportDateRangeSelection,
|
||||
bounds: TimeRangeBounds | null
|
||||
): ExportDateRangeSelection => {
|
||||
if (!bounds) return cloneExportDateRangeSelection(selection)
|
||||
|
||||
const boundedStart = startOfDay(bounds.minDate)
|
||||
const boundedEnd = endOfDay(bounds.maxDate)
|
||||
const originalStart = selection.useAllTime ? boundedStart : startOfDay(selection.dateRange.start)
|
||||
const originalEnd = selection.useAllTime ? boundedEnd : endOfDay(selection.dateRange.end)
|
||||
const nextStart = new Date(Math.min(Math.max(originalStart.getTime(), boundedStart.getTime()), boundedEnd.getTime()))
|
||||
const nextEndCandidate = new Date(Math.min(Math.max(originalEnd.getTime(), boundedStart.getTime()), boundedEnd.getTime()))
|
||||
const nextEnd = nextEndCandidate.getTime() < nextStart.getTime() ? endOfDay(nextStart) : nextEndCandidate
|
||||
const rangeChanged = nextStart.getTime() !== originalStart.getTime() || nextEnd.getTime() !== originalEnd.getTime()
|
||||
|
||||
return {
|
||||
preset: selection.useAllTime ? selection.preset : (rangeChanged ? 'custom' : selection.preset),
|
||||
useAllTime: selection.useAllTime,
|
||||
dateRange: {
|
||||
start: nextStart,
|
||||
end: nextEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const areExportSelectionsEqual = (left: ExportDateRangeSelection, right: ExportDateRangeSelection): boolean => (
|
||||
left.preset === right.preset &&
|
||||
left.useAllTime === right.useAllTime &&
|
||||
left.dateRange.start.getTime() === right.dateRange.start.getTime() &&
|
||||
left.dateRange.end.getTime() === right.dateRange.end.getTime()
|
||||
)
|
||||
|
||||
const pickSessionMediaMetric = (
|
||||
metricRaw: SessionExportMetric | SessionContentMetric | undefined
|
||||
): SessionContentMetric | null => {
|
||||
if (!metricRaw) return null
|
||||
const totalMessages = normalizeMessageCount(metricRaw.totalMessages)
|
||||
const voiceMessages = normalizeMessageCount(metricRaw.voiceMessages)
|
||||
const imageMessages = normalizeMessageCount(metricRaw.imageMessages)
|
||||
const videoMessages = normalizeMessageCount(metricRaw.videoMessages)
|
||||
const emojiMessages = normalizeMessageCount(metricRaw.emojiMessages)
|
||||
const firstTimestamp = normalizeTimestampSeconds(metricRaw.firstTimestamp)
|
||||
const lastTimestamp = normalizeTimestampSeconds(metricRaw.lastTimestamp)
|
||||
if (
|
||||
typeof totalMessages !== 'number' &&
|
||||
typeof voiceMessages !== 'number' &&
|
||||
typeof imageMessages !== 'number' &&
|
||||
typeof videoMessages !== 'number' &&
|
||||
typeof emojiMessages !== 'number'
|
||||
typeof emojiMessages !== 'number' &&
|
||||
typeof firstTimestamp !== 'number' &&
|
||||
typeof lastTimestamp !== 'number'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
totalMessages,
|
||||
voiceMessages,
|
||||
imageMessages,
|
||||
videoMessages,
|
||||
emojiMessages
|
||||
emojiMessages,
|
||||
firstTimestamp,
|
||||
lastTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1520,6 +1577,8 @@ function ExportPage() {
|
||||
const [snsExportLivePhotos, setSnsExportLivePhotos] = useState(false)
|
||||
const [snsExportVideos, setSnsExportVideos] = useState(false)
|
||||
const [isTimeRangeDialogOpen, setIsTimeRangeDialogOpen] = useState(false)
|
||||
const [isResolvingTimeRangeBounds, setIsResolvingTimeRangeBounds] = useState(false)
|
||||
const [timeRangeBounds, setTimeRangeBounds] = useState<TimeRangeBounds | null>(null)
|
||||
const [isExportDefaultsModalOpen, setIsExportDefaultsModalOpen] = useState(false)
|
||||
const [timeRangeSelection, setTimeRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
||||
const [exportDefaultFormat, setExportDefaultFormat] = useState<TextExportFormat>('excel')
|
||||
@@ -2686,7 +2745,9 @@ function ExportPage() {
|
||||
typeof emojiMessages !== 'number' &&
|
||||
typeof transferMessages !== 'number' &&
|
||||
typeof redPacketMessages !== 'number' &&
|
||||
typeof callMessages !== 'number'
|
||||
typeof callMessages !== 'number' &&
|
||||
typeof normalizeTimestampSeconds(metricRaw.firstTimestamp) !== 'number' &&
|
||||
typeof normalizeTimestampSeconds(metricRaw.lastTimestamp) !== 'number'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -2699,7 +2760,9 @@ function ExportPage() {
|
||||
emojiMessages,
|
||||
transferMessages,
|
||||
redPacketMessages,
|
||||
callMessages
|
||||
callMessages,
|
||||
firstTimestamp: normalizeTimestampSeconds(metricRaw.firstTimestamp),
|
||||
lastTimestamp: normalizeTimestampSeconds(metricRaw.lastTimestamp)
|
||||
}
|
||||
if (typeof totalMessages === 'number') {
|
||||
nextMessageCounts[sessionId] = totalMessages
|
||||
@@ -2743,7 +2806,9 @@ function ExportPage() {
|
||||
previous.emojiMessages === nextMetric.emojiMessages &&
|
||||
previous.transferMessages === nextMetric.transferMessages &&
|
||||
previous.redPacketMessages === nextMetric.redPacketMessages &&
|
||||
previous.callMessages === nextMetric.callMessages
|
||||
previous.callMessages === nextMetric.callMessages &&
|
||||
previous.firstTimestamp === nextMetric.firstTimestamp &&
|
||||
previous.lastTimestamp === nextMetric.lastTimestamp
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -3898,6 +3963,7 @@ function ExportPage() {
|
||||
const openExportDialog = useCallback((payload: Omit<ExportDialogState, 'open'>) => {
|
||||
setExportDialog({ open: true, ...payload })
|
||||
setIsTimeRangeDialogOpen(false)
|
||||
setTimeRangeBounds(null)
|
||||
setTimeRangeSelection(exportDefaultDateRangeSelection)
|
||||
|
||||
setOptions(prev => {
|
||||
@@ -3960,11 +4026,108 @@ function ExportPage() {
|
||||
const closeExportDialog = useCallback(() => {
|
||||
setExportDialog(prev => ({ ...prev, open: false }))
|
||||
setIsTimeRangeDialogOpen(false)
|
||||
setTimeRangeBounds(null)
|
||||
}, [])
|
||||
|
||||
const resolveChatExportTimeRangeBounds = useCallback(async (sessionIds: string[]): Promise<TimeRangeBounds | null> => {
|
||||
const normalizedSessionIds = Array.from(new Set((sessionIds || []).map(id => String(id || '').trim()).filter(Boolean)))
|
||||
if (normalizedSessionIds.length === 0) return null
|
||||
|
||||
let minTimestamp: number | undefined
|
||||
let maxTimestamp: number | undefined
|
||||
const resolvedSessionIds = new Set<string>()
|
||||
|
||||
const absorbMetric = (sessionId: string, metric?: { firstTimestamp?: number; lastTimestamp?: number } | null) => {
|
||||
if (!metric) return
|
||||
const firstTimestamp = normalizeTimestampSeconds(metric.firstTimestamp)
|
||||
const lastTimestamp = normalizeTimestampSeconds(metric.lastTimestamp)
|
||||
if (typeof firstTimestamp !== 'number' || typeof lastTimestamp !== 'number') return
|
||||
resolvedSessionIds.add(sessionId)
|
||||
if (minTimestamp === undefined || firstTimestamp < minTimestamp) minTimestamp = firstTimestamp
|
||||
if (maxTimestamp === undefined || lastTimestamp > maxTimestamp) maxTimestamp = lastTimestamp
|
||||
}
|
||||
|
||||
for (const sessionId of normalizedSessionIds) {
|
||||
absorbMetric(sessionId, sessionContentMetrics[sessionId])
|
||||
if (sessionDetail?.wxid === sessionId) {
|
||||
absorbMetric(sessionId, {
|
||||
firstTimestamp: sessionDetail.firstMessageTime,
|
||||
lastTimestamp: sessionDetail.latestMessageTime
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const applyStatsResult = (result?: {
|
||||
success: boolean
|
||||
data?: Record<string, SessionExportMetric>
|
||||
} | null) => {
|
||||
if (!result?.success || !result.data) return
|
||||
applySessionMediaMetricsFromStats(result.data)
|
||||
for (const sessionId of normalizedSessionIds) {
|
||||
absorbMetric(sessionId, result.data[sessionId])
|
||||
}
|
||||
}
|
||||
|
||||
const missingSessionIds = () => normalizedSessionIds.filter(sessionId => !resolvedSessionIds.has(sessionId))
|
||||
|
||||
if (missingSessionIds().length > 0) {
|
||||
applyStatsResult(await window.electronAPI.chat.getExportSessionStats(
|
||||
missingSessionIds(),
|
||||
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
||||
))
|
||||
}
|
||||
|
||||
if (missingSessionIds().length > 0) {
|
||||
applyStatsResult(await window.electronAPI.chat.getExportSessionStats(
|
||||
missingSessionIds(),
|
||||
{ includeRelations: false, allowStaleCache: true }
|
||||
))
|
||||
}
|
||||
|
||||
if (resolvedSessionIds.size !== normalizedSessionIds.length) {
|
||||
return null
|
||||
}
|
||||
if (typeof minTimestamp !== 'number' || typeof maxTimestamp !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
minDate: new Date(minTimestamp * 1000),
|
||||
maxDate: new Date(maxTimestamp * 1000)
|
||||
}
|
||||
}, [applySessionMediaMetricsFromStats, sessionContentMetrics, sessionDetail])
|
||||
|
||||
const openTimeRangeDialog = useCallback(() => {
|
||||
void (async () => {
|
||||
if (isResolvingTimeRangeBounds) return
|
||||
setIsResolvingTimeRangeBounds(true)
|
||||
try {
|
||||
let nextBounds: TimeRangeBounds | null = null
|
||||
if (exportDialog.scope !== 'sns') {
|
||||
nextBounds = await resolveChatExportTimeRangeBounds(exportDialog.sessionIds)
|
||||
}
|
||||
setTimeRangeBounds(nextBounds)
|
||||
if (nextBounds) {
|
||||
const nextSelection = clampExportSelectionToBounds(timeRangeSelection, nextBounds)
|
||||
if (!areExportSelectionsEqual(nextSelection, timeRangeSelection)) {
|
||||
setTimeRangeSelection(nextSelection)
|
||||
setOptions(prev => ({
|
||||
...prev,
|
||||
useAllTime: nextSelection.useAllTime,
|
||||
dateRange: cloneExportDateRange(nextSelection.dateRange)
|
||||
}))
|
||||
}
|
||||
}
|
||||
setIsTimeRangeDialogOpen(true)
|
||||
}, [])
|
||||
} catch (error) {
|
||||
console.error('导出页解析时间范围边界失败', error)
|
||||
setTimeRangeBounds(null)
|
||||
setIsTimeRangeDialogOpen(true)
|
||||
} finally {
|
||||
setIsResolvingTimeRangeBounds(false)
|
||||
}
|
||||
})()
|
||||
}, [exportDialog.scope, exportDialog.sessionIds, isResolvingTimeRangeBounds, resolveChatExportTimeRangeBounds, timeRangeSelection])
|
||||
|
||||
const closeTimeRangeDialog = useCallback(() => {
|
||||
setIsTimeRangeDialogOpen(false)
|
||||
@@ -7753,8 +7916,9 @@ function ExportPage() {
|
||||
type="button"
|
||||
className="time-range-trigger"
|
||||
onClick={openTimeRangeDialog}
|
||||
disabled={isResolvingTimeRangeBounds}
|
||||
>
|
||||
<span>{timeRangeSummaryLabel}</span>
|
||||
<span>{isResolvingTimeRangeBounds ? '正在统计可选时间...' : timeRangeSummaryLabel}</span>
|
||||
<span className="time-range-arrow">></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -7840,6 +8004,8 @@ function ExportPage() {
|
||||
<ExportDateRangeDialog
|
||||
open={isTimeRangeDialogOpen}
|
||||
value={timeRangeSelection}
|
||||
minDate={timeRangeBounds?.minDate}
|
||||
maxDate={timeRangeBounds?.maxDate}
|
||||
onClose={closeTimeRangeDialog}
|
||||
onConfirm={(nextSelection) => {
|
||||
setTimeRangeSelection(nextSelection)
|
||||
|
||||
@@ -580,6 +580,8 @@ export interface ExportSessionContentMetricCacheEntry {
|
||||
imageMessages?: number
|
||||
videoMessages?: number
|
||||
emojiMessages?: number
|
||||
firstTimestamp?: number
|
||||
lastTimestamp?: number
|
||||
}
|
||||
|
||||
export interface ExportSessionContentMetricCacheItem {
|
||||
@@ -742,6 +744,12 @@ export async function getExportSessionContentMetricCache(scopeKey: string): Prom
|
||||
if (typeof source.emojiMessages === 'number' && Number.isFinite(source.emojiMessages) && source.emojiMessages >= 0) {
|
||||
metric.emojiMessages = Math.floor(source.emojiMessages)
|
||||
}
|
||||
if (typeof source.firstTimestamp === 'number' && Number.isFinite(source.firstTimestamp) && source.firstTimestamp > 0) {
|
||||
metric.firstTimestamp = Math.floor(source.firstTimestamp)
|
||||
}
|
||||
if (typeof source.lastTimestamp === 'number' && Number.isFinite(source.lastTimestamp) && source.lastTimestamp > 0) {
|
||||
metric.lastTimestamp = Math.floor(source.lastTimestamp)
|
||||
}
|
||||
if (Object.keys(metric).length === 0) continue
|
||||
metrics[sessionId] = metric
|
||||
}
|
||||
@@ -781,6 +789,12 @@ export async function setExportSessionContentMetricCache(
|
||||
if (typeof rawMetric.emojiMessages === 'number' && Number.isFinite(rawMetric.emojiMessages) && rawMetric.emojiMessages >= 0) {
|
||||
metric.emojiMessages = Math.floor(rawMetric.emojiMessages)
|
||||
}
|
||||
if (typeof rawMetric.firstTimestamp === 'number' && Number.isFinite(rawMetric.firstTimestamp) && rawMetric.firstTimestamp > 0) {
|
||||
metric.firstTimestamp = Math.floor(rawMetric.firstTimestamp)
|
||||
}
|
||||
if (typeof rawMetric.lastTimestamp === 'number' && Number.isFinite(rawMetric.lastTimestamp) && rawMetric.lastTimestamp > 0) {
|
||||
metric.lastTimestamp = Math.floor(rawMetric.lastTimestamp)
|
||||
}
|
||||
if (Object.keys(metric).length === 0) continue
|
||||
normalized[sessionId] = metric
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user