feat(export): refine format selector layouts

This commit is contained in:
aits2026
2026-03-06 14:15:18 +08:00
parent 450e5f7e61
commit 6253def76c
4 changed files with 159 additions and 23 deletions

View File

@@ -359,8 +359,19 @@
margin-bottom: 0;
}
.format-setting-group {
grid-template-columns: 1fr;
gap: 10px;
align-items: stretch;
}
.format-setting-group .form-control {
justify-content: flex-start;
}
.format-grid {
max-width: 360px;
max-width: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-bottom: 0;
}
}
@@ -384,5 +395,9 @@
.format-grid {
max-width: none;
}
.format-grid {
grid-template-columns: repeat(auto-fit, minmax(156px, 1fr));
}
}
}

View File

@@ -145,7 +145,7 @@ export function ExportDefaultsSettingsForm({
</div>
</div>
<div className="form-group">
<div className="form-group format-setting-group">
<div className="form-copy">
<label></label>
<span className="form-hint"></span>

View File

@@ -2858,6 +2858,66 @@
}
}
.dialog-format-select {
position: relative;
min-width: 220px;
}
.dialog-format-dropdown {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: min(360px, calc(100vw - 64px));
background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary));
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 6px;
box-shadow: var(--shadow-md);
z-index: 120;
max-height: 320px;
overflow-y: auto;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.dialog-format-option {
width: 100%;
text-align: left;
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border: none;
border-radius: 10px;
background: transparent;
cursor: pointer;
transition: all 0.15s;
color: var(--text-primary);
font-size: 14px;
&:hover {
background: var(--bg-tertiary);
}
&.active {
background: color-mix(in srgb, var(--primary) 12%, transparent);
color: var(--primary);
}
.option-label {
font-weight: 500;
}
.option-desc {
font-size: 12px;
color: var(--text-tertiary);
}
&.active .option-desc {
color: var(--primary);
}
}
.scope-tag-row {
display: flex;
align-items: center;

View File

@@ -1320,6 +1320,7 @@ function ExportPage() {
sessionNames: [],
title: ''
})
const [showSessionFormatSelect, setShowSessionFormatSelect] = useState(false)
const [tasks, setTasks] = useState<ExportTask[]>([])
const [lastExportBySession, setLastExportBySession] = useState<Record<string, number>>({})
@@ -1354,6 +1355,7 @@ function ExportPage() {
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
const contactsVirtuosoRef = useRef<VirtuosoHandle | null>(null)
const sessionTableSectionRef = useRef<HTMLDivElement | null>(null)
const sessionFormatDropdownRef = useRef<HTMLDivElement | null>(null)
const detailRequestSeqRef = useRef(0)
const sessionsRef = useRef<SessionRow[]>([])
const sessionContentMetricsRef = useRef<Record<string, SessionContentMetric>>({})
@@ -4801,6 +4803,24 @@ function ExportPage() {
return () => window.removeEventListener('keydown', handleKeyDown)
}, [closeSessionSnsTimeline, sessionSnsTimelineTarget])
useEffect(() => {
if (!showSessionFormatSelect) return
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node
if (sessionFormatDropdownRef.current && !sessionFormatDropdownRef.current.contains(target)) {
setShowSessionFormatSelect(false)
}
}
document.addEventListener('mousedown', handlePointerDown)
return () => document.removeEventListener('mousedown', handlePointerDown)
}, [showSessionFormatSelect])
useEffect(() => {
if (!exportDialog.open) {
setShowSessionFormatSelect(false)
}
}, [exportDialog.open])
const handleCopyDetailField = useCallback(async (text: string, field: string) => {
try {
await navigator.clipboard.writeText(text)
@@ -4894,14 +4914,19 @@ function ExportPage() {
const formatCandidateOptions = exportDialog.scope === 'sns'
? snsFormatOptions
: formatOptions
const isSessionScopeDialog = exportDialog.scope === 'single' || exportDialog.scope === 'multi'
const isContentScopeDialog = exportDialog.scope === 'content'
const isContentTextDialog = isContentScopeDialog && exportDialog.contentType === 'text'
const useCollapsedSessionFormatSelector = isSessionScopeDialog
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
const shouldShowMediaSection = !isContentScopeDialog
const avatarExportStatusLabel = options.exportAvatars ? '已开启聊天消息导出带头像' : '已关闭聊天消息导出带头像'
const textContentFormatNote = options.exportAvatars
? '此模式包含用户头像,不导出图片语音视频表情包等多媒体内容'
: '此模式不包含用户头像,不导出图片语音视频表情包等多媒体内容'
const activeDialogFormatLabel = exportDialog.scope === 'sns'
? (snsFormatOptions.find(option => option.value === snsExportFormat)?.label ?? snsExportFormat)
: (formatOptions.find(option => option.value === options.format)?.label ?? options.format)
const shouldShowDisplayNameSection = !(
exportDialog.scope === 'sns' ||
(
@@ -6057,33 +6082,69 @@ function ExportPage() {
{shouldShowFormatSection && (
<div className="dialog-section">
<h4>{exportDialog.scope === 'sns' ? '朋友圈导出格式选择' : '对话文本导出格式选择'}</h4>
{useCollapsedSessionFormatSelector ? (
<div className="section-header-action">
<h4></h4>
<div className="dialog-format-select" ref={sessionFormatDropdownRef}>
<button
type="button"
className={`time-range-trigger ${showSessionFormatSelect ? 'open' : ''}`}
onClick={() => setShowSessionFormatSelect(prev => !prev)}
>
<span>{activeDialogFormatLabel}</span>
<span className="time-range-arrow">&gt;</span>
</button>
{showSessionFormatSelect && (
<div className="dialog-format-dropdown">
{formatOptions.map(option => (
<button
key={option.value}
type="button"
className={`dialog-format-option ${options.format === option.value ? 'active' : ''}`}
onClick={() => {
setOptions(prev => ({ ...prev, format: option.value as TextExportFormat }))
setShowSessionFormatSelect(false)
}}
>
<span className="option-label">{option.label}</span>
<span className="option-desc">{option.desc}</span>
</button>
))}
</div>
)}
</div>
</div>
) : (
<h4>{exportDialog.scope === 'sns' ? '朋友圈导出格式选择' : '对话文本导出格式选择'}</h4>
)}
{!isContentScopeDialog && exportDialog.scope !== 'sns' && (
<div className="format-note">{avatarExportStatusLabel}</div>
)}
{isContentTextDialog && (
<div className="format-note">{textContentFormatNote}</div>
)}
<div className="format-grid">
{formatCandidateOptions.map(option => (
<button
key={option.value}
className={`format-card ${exportDialog.scope === 'sns'
? (snsExportFormat === option.value ? 'active' : '')
: (options.format === option.value ? 'active' : '')}`}
onClick={() => {
if (exportDialog.scope === 'sns') {
setSnsExportFormat(option.value as SnsTimelineExportFormat)
} else {
setOptions(prev => ({ ...prev, format: option.value as TextExportFormat }))
}
}}
>
<div className="format-label">{option.label}</div>
<div className="format-desc">{option.desc}</div>
</button>
))}
</div>
{!useCollapsedSessionFormatSelector && (
<div className="format-grid">
{formatCandidateOptions.map(option => (
<button
key={option.value}
className={`format-card ${exportDialog.scope === 'sns'
? (snsExportFormat === option.value ? 'active' : '')
: (options.format === option.value ? 'active' : '')}`}
onClick={() => {
if (exportDialog.scope === 'sns') {
setSnsExportFormat(option.value as SnsTimelineExportFormat)
} else {
setOptions(prev => ({ ...prev, format: option.value as TextExportFormat }))
}
}}
>
<div className="format-label">{option.label}</div>
<div className="format-desc">{option.desc}</div>
</button>
))}
</div>
)}
</div>
)}