feat(export): add more defaults modal

This commit is contained in:
aits2026
2026-03-06 13:36:25 +08:00
parent 3fa0b36426
commit 90b33ef444
5 changed files with 758 additions and 215 deletions

View File

@@ -261,14 +261,14 @@
}
.global-export-controls {
flex: 0 1 820px;
width: min(820px, 100%);
flex: 0 1 980px;
width: min(980px, 100%);
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px;
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 1fr);
grid-template-columns: minmax(0, 1.55fr) minmax(240px, 1fr) auto;
gap: 10px;
align-items: stretch;
@@ -359,6 +359,32 @@
z-index: 40;
}
.more-export-settings-control {
display: flex;
align-items: center;
justify-content: flex-end;
}
.more-export-settings-btn {
min-height: 38px;
border-radius: 10px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
padding: 0 14px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
&:hover {
border-color: var(--primary);
color: var(--primary);
background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary));
}
}
.layout-trigger {
width: 100%;
padding: 8px 10px;
@@ -551,6 +577,62 @@
}
}
.export-defaults-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.42);
display: flex;
align-items: center;
justify-content: center;
z-index: 2300;
padding: 20px;
}
.export-defaults-modal {
width: min(720px, 100%);
max-height: min(80vh, 860px);
overflow: hidden;
border-radius: 14px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
display: flex;
flex-direction: column;
}
.export-defaults-modal-header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
h3 {
margin: 0;
font-size: 15px;
color: var(--text-primary);
}
p {
margin: 4px 0 0;
font-size: 12px;
color: var(--text-tertiary);
}
}
.export-defaults-modal-body {
padding: 16px;
overflow: auto;
}
.export-defaults-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 0 16px 16px;
}
.task-center-card-label {
line-height: 1;
white-space: nowrap;
@@ -3285,9 +3367,9 @@
.global-export-controls {
padding: 10px;
gap: 8px;
flex-basis: 760px;
width: min(760px, 100%);
grid-template-columns: minmax(0, 1.5fr) minmax(240px, 1fr);
flex-basis: 920px;
width: min(920px, 100%);
grid-template-columns: minmax(0, 1.35fr) minmax(220px, 1fr) auto;
}
.format-grid {
@@ -3333,6 +3415,10 @@
width: 100%;
justify-content: space-between;
}
.export-defaults-modal {
width: min(92vw, 720px);
}
}
@media (max-width: 720px) {

View File

@@ -41,6 +41,7 @@ import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
import { SnsPostItem } from '../components/Sns/SnsPostItem'
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm'
import type { SnsPost } from '../types/sns'
import {
cloneExportDateRange,
@@ -1282,8 +1283,14 @@ function ExportPage() {
const [snsExportLivePhotos, setSnsExportLivePhotos] = useState(false)
const [snsExportVideos, setSnsExportVideos] = useState(false)
const [isTimeRangeDialogOpen, setIsTimeRangeDialogOpen] = useState(false)
const [isExportDefaultsModalOpen, setIsExportDefaultsModalOpen] = useState(false)
const [timeRangeSelection, setTimeRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
const [exportDefaultFormat, setExportDefaultFormat] = useState<TextExportFormat>('excel')
const [exportDefaultDateRangeSelection, setExportDefaultDateRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
const [options, setOptions] = useState<ExportOptions>({
format: 'json',
@@ -1785,8 +1792,9 @@ function ExportPage() {
setIsBaseConfigLoading(true)
let isReady = true
try {
const [savedPath, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, exportCacheScope] = await Promise.all([
const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, exportCacheScope] = await Promise.all([
configService.getExportPath(),
configService.getExportDefaultFormat(),
configService.getExportDefaultMedia(),
configService.getExportDefaultVoiceAsText(),
configService.getExportDefaultExcelCompactColumns(),
@@ -1817,10 +1825,14 @@ function ExportPage() {
setLastExportByContent(savedContentMap)
setExportRecordsBySession(savedSessionRecordMap)
setLastSnsExportPostCount(savedSnsPostCount)
setExportDefaultFormat((savedFormat as TextExportFormat) || 'excel')
setExportDefaultMedia(savedMedia ?? false)
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
setExportDefaultConcurrency(savedConcurrency ?? 2)
const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange)
setExportDefaultDateRangeSelection(resolvedDefaultDateRange)
setTimeRangeSelection(resolvedDefaultDateRange)
await configService.setExportDefaultFormat('json')
if (cachedSnsStats && Date.now() - cachedSnsStats.updatedAt <= EXPORT_SNS_STATS_CACHE_STALE_MS) {
setSnsStats({
@@ -1835,7 +1847,7 @@ function ExportPage() {
const txtColumns = savedTxtColumns && savedTxtColumns.length > 0 ? savedTxtColumns : defaultTxtColumns
setOptions(prev => ({
...prev,
format: 'json',
format: ((savedFormat as TextExportFormat) || 'excel'),
exportMedia: savedMedia ?? prev.exportMedia,
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
@@ -3192,8 +3204,13 @@ function ExportPage() {
const next: ExportOptions = {
...prev,
format: exportDefaultFormat,
useAllTime: exportDefaultDateRangeSelection.useAllTime,
dateRange: nextDateRange
dateRange: nextDateRange,
exportMedia: exportDefaultMedia,
exportVoiceAsText: exportDefaultVoiceAsText,
excelCompactColumns: exportDefaultExcelCompactColumns,
exportConcurrency: exportDefaultConcurrency
}
if (payload.scope === 'sns') {
@@ -3220,7 +3237,14 @@ function ExportPage() {
return next
})
}, [exportDefaultDateRangeSelection])
}, [
exportDefaultDateRangeSelection,
exportDefaultExcelCompactColumns,
exportDefaultFormat,
exportDefaultMedia,
exportDefaultVoiceAsText,
exportDefaultConcurrency
])
const closeExportDialog = useCallback(() => {
setExportDialog(prev => ({ ...prev, open: false }))
@@ -5147,6 +5171,27 @@ function ExportPage() {
}
}, [])
const handleExportDefaultsChanged = useCallback((patch: ExportDefaultsSettingsPatch) => {
if (patch.format) {
setExportDefaultFormat(patch.format as TextExportFormat)
}
if (patch.dateRange) {
setExportDefaultDateRangeSelection(patch.dateRange)
}
if (typeof patch.media === 'boolean') {
setExportDefaultMedia(patch.media)
}
if (typeof patch.voiceAsText === 'boolean') {
setExportDefaultVoiceAsText(patch.voiceAsText)
}
if (typeof patch.excelCompactColumns === 'boolean') {
setExportDefaultExcelCompactColumns(patch.excelCompactColumns)
}
if (typeof patch.concurrency === 'number') {
setExportDefaultConcurrency(patch.concurrency)
}
}, [])
return (
<div className="export-board-page">
<div className="export-top-panel">
@@ -5186,6 +5231,16 @@ function ExportPage() {
await configService.setExportSessionNamePrefixEnabled(enabled)
}}
/>
<div className="more-export-settings-control">
<button
className="more-export-settings-btn"
type="button"
onClick={() => setIsExportDefaultsModalOpen(true)}
>
</button>
</div>
</div>
<button
@@ -5212,6 +5267,48 @@ function ExportPage() {
onTogglePerfTask={toggleTaskPerfDetail}
/>
{isExportDefaultsModalOpen && (
<div
className="export-defaults-modal-overlay"
onClick={() => setIsExportDefaultsModalOpen(false)}
>
<div
className="export-defaults-modal"
role="dialog"
aria-modal="true"
aria-label="更多导出设置"
onClick={(event) => event.stopPropagation()}
>
<div className="export-defaults-modal-header">
<div>
<h3></h3>
<p></p>
</div>
<button
className="close-icon-btn"
type="button"
onClick={() => setIsExportDefaultsModalOpen(false)}
aria-label="关闭更多导出设置"
>
<X size={16} />
</button>
</div>
<div className="export-defaults-modal-body">
<ExportDefaultsSettingsForm onDefaultsChanged={handleExportDefaultsChanged} />
</div>
<div className="export-defaults-modal-actions">
<button
type="button"
className="secondary-btn"
onClick={() => setIsExportDefaultsModalOpen(false)}
>
</button>
</div>
</div>
</div>
)}
<div className="export-section-title-row">
<h3 className="export-section-title"></h3>
<SectionInfoTooltip

View File

@@ -14,6 +14,7 @@ import {
} from 'lucide-react'
import { Avatar } from '../components/Avatar'
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
import { ExportDefaultsSettingsForm } from '../components/Export/ExportDefaultsSettingsForm'
import {
createDefaultExportDateRangeSelection,
getExportDateRangeLabel,
@@ -1588,212 +1589,9 @@ function SettingsPage() {
}
const renderExportTab = () => {
const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full'
const exportFormatLabel = getOptionLabel(exportFormatOptions, exportDefaultFormat)
const exportDateRangeLabel = getExportDateRangeLabel(exportDefaultDateRange)
const exportExcelColumnsLabel = getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue)
const exportConcurrencyLabel = String(exportDefaultConcurrency)
return (
<div className="tab-content">
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<div className="select-field" ref={exportFormatDropdownRef}>
<button
type="button"
className={`select-trigger ${showExportFormatSelect ? 'open' : ''}`}
onClick={() => {
setShowExportFormatSelect(!showExportFormatSelect)
setIsExportDateRangeDialogOpen(false)
setShowExportExcelColumnsSelect(false)
setShowExportConcurrencySelect(false)
}}
>
<span className="select-value">{exportFormatLabel}</span>
<ChevronDown size={16} />
</button>
{showExportFormatSelect && (
<div className="select-dropdown">
{exportFormatOptions.map((option) => (
<button
key={option.value}
type="button"
className={`select-option ${exportDefaultFormat === option.value ? 'active' : ''}`}
onClick={async () => {
setExportDefaultFormat(option.value)
await configService.setExportDefaultFormat(option.value)
showMessage('已更新导出格式默认值', true)
setShowExportFormatSelect(false)
}}
>
<span className="option-label">{option.label}</span>
{option.desc && <span className="option-desc">{option.desc}</span>}
</button>
))}
</div>
)}
</div>
</div>
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<div className="settings-time-range-field">
<button
type="button"
className={`settings-time-range-trigger ${isExportDateRangeDialogOpen ? 'open' : ''}`}
onClick={() => {
setShowExportFormatSelect(false)
setShowExportExcelColumnsSelect(false)
setShowExportConcurrencySelect(false)
setIsExportDateRangeDialogOpen(true)
}}
>
<span className="settings-time-range-value">{exportDateRangeLabel}</span>
<span className="settings-time-range-arrow">&gt;</span>
</button>
</div>
</div>
<ExportDateRangeDialog
open={isExportDateRangeDialogOpen}
value={exportDefaultDateRange}
onClose={() => setIsExportDateRangeDialogOpen(false)}
onConfirm={async (nextSelection) => {
setExportDefaultDateRange(nextSelection)
await configService.setExportDefaultDateRange(serializeExportDateRangeConfig(nextSelection))
showMessage('已更新默认导出时间范围', true)
setIsExportDateRangeDialogOpen(false)
}}
/>
<div className="form-group">
<label></label>
<span className="form-hint">//</span>
<div className="log-toggle-line">
<span className="log-status">{exportDefaultMedia ? '已开启' : '已关闭'}</span>
<label className="switch" htmlFor="export-default-media">
<input
id="export-default-media"
className="switch-input"
type="checkbox"
checked={exportDefaultMedia}
onChange={async (e) => {
const enabled = e.target.checked
setExportDefaultMedia(enabled)
await configService.setExportDefaultMedia(enabled)
showMessage(enabled ? '已开启默认媒体导出' : '已关闭默认媒体导出', true)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<div className="log-toggle-line">
<span className="log-status">{exportDefaultVoiceAsText ? '已开启' : '已关闭'}</span>
<label className="switch" htmlFor="export-default-voice-as-text">
<input
id="export-default-voice-as-text"
className="switch-input"
type="checkbox"
checked={exportDefaultVoiceAsText}
onChange={async (e) => {
const enabled = e.target.checked
setExportDefaultVoiceAsText(enabled)
await configService.setExportDefaultVoiceAsText(enabled)
showMessage(enabled ? '已开启默认语音转文字' : '已关闭默认语音转文字', true)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="form-group">
<label>Excel </label>
<span className="form-hint"> Excel </span>
<div className="select-field" ref={exportExcelColumnsDropdownRef}>
<button
type="button"
className={`select-trigger ${showExportExcelColumnsSelect ? 'open' : ''}`}
onClick={() => {
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
setShowExportFormatSelect(false)
setIsExportDateRangeDialogOpen(false)
setShowExportConcurrencySelect(false)
}}
>
<span className="select-value">{exportExcelColumnsLabel}</span>
<ChevronDown size={16} />
</button>
{showExportExcelColumnsSelect && (
<div className="select-dropdown">
{exportExcelColumnOptions.map((option) => (
<button
key={option.value}
type="button"
className={`select-option ${exportExcelColumnsValue === option.value ? 'active' : ''}`}
onClick={async () => {
const compact = option.value === 'compact'
setExportDefaultExcelCompactColumns(compact)
await configService.setExportDefaultExcelCompactColumns(compact)
showMessage(compact ? '已启用精简列' : '已启用完整列', true)
setShowExportExcelColumnsSelect(false)
}}
>
<span className="option-label">{option.label}</span>
{option.desc && <span className="option-desc">{option.desc}</span>}
</button>
))}
</div>
)}
</div>
</div>
<div className="form-group">
<label></label>
<span className="form-hint">1~6</span>
<div className="select-field" ref={exportConcurrencyDropdownRef}>
<button
type="button"
className={`select-trigger ${showExportConcurrencySelect ? 'open' : ''}`}
onClick={() => {
setShowExportConcurrencySelect(!showExportConcurrencySelect)
setShowExportFormatSelect(false)
setIsExportDateRangeDialogOpen(false)
setShowExportExcelColumnsSelect(false)
}}
>
<span className="select-value">{exportConcurrencyLabel}</span>
<ChevronDown size={16} />
</button>
{showExportConcurrencySelect && (
<div className="select-dropdown">
{exportConcurrencyOptions.map((option) => (
<button
key={option.value}
type="button"
className={`select-option ${exportDefaultConcurrency === option.value ? 'active' : ''}`}
onClick={async () => {
setExportDefaultConcurrency(option.value)
await configService.setExportDefaultConcurrency(option.value)
showMessage(`已将导出并发数设为 ${option.value}`, true)
setShowExportConcurrencySelect(false)
}}
>
<span className="option-label">{option.label}</span>
</button>
))}
</div>
)}
</div>
</div>
<ExportDefaultsSettingsForm onNotify={showMessage} />
</div>
)
}