mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(export): add more defaults modal
This commit is contained in:
230
src/components/Export/ExportDefaultsSettingsForm.scss
Normal file
230
src/components/Export/ExportDefaultsSettingsForm.scss
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
.export-defaults-settings-form {
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option.active .option-desc {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-time-range-field {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-time-range-trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(var(--primary-rgb), 0.45);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-time-range-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-time-range-arrow {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toggle-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 48px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&:checked + .switch-slider {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .switch-slider::before {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus + .switch-slider {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-slider {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
left: 3px;
|
||||||
|
top: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
332
src/components/Export/ExportDefaultsSettingsForm.tsx
Normal file
332
src/components/Export/ExportDefaultsSettingsForm.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { ChevronDown } from 'lucide-react'
|
||||||
|
import * as configService from '../../services/config'
|
||||||
|
import { ExportDateRangeDialog } from './ExportDateRangeDialog'
|
||||||
|
import {
|
||||||
|
createDefaultExportDateRangeSelection,
|
||||||
|
getExportDateRangeLabel,
|
||||||
|
resolveExportDateRangeConfig,
|
||||||
|
serializeExportDateRangeConfig,
|
||||||
|
type ExportDateRangeSelection
|
||||||
|
} from '../../utils/exportDateRange'
|
||||||
|
import './ExportDefaultsSettingsForm.scss'
|
||||||
|
|
||||||
|
export interface ExportDefaultsSettingsPatch {
|
||||||
|
format?: string
|
||||||
|
dateRange?: ExportDateRangeSelection
|
||||||
|
media?: boolean
|
||||||
|
voiceAsText?: boolean
|
||||||
|
excelCompactColumns?: boolean
|
||||||
|
concurrency?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExportDefaultsSettingsFormProps {
|
||||||
|
onNotify?: (text: string, success: boolean) => void
|
||||||
|
onDefaultsChanged?: (patch: ExportDefaultsSettingsPatch) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportFormatOptions = [
|
||||||
|
{ value: 'excel', label: 'Excel', desc: '电子表格,适合统计分析' },
|
||||||
|
{ value: 'json', label: 'JSON', desc: '详细格式,包含完整消息信息' },
|
||||||
|
{ value: 'html', label: 'HTML', desc: '网页格式,可直接浏览' },
|
||||||
|
{ value: 'txt', label: 'TXT', desc: '纯文本,通用格式' },
|
||||||
|
{ value: 'arkme-json', label: 'Arkme JSON', desc: '紧凑 JSON,支持 sender 去重与关系统计' },
|
||||||
|
{ value: 'chatlab', label: 'ChatLab', desc: '标准格式,支持其他软件导入' },
|
||||||
|
{ value: 'chatlab-jsonl', label: 'ChatLab JSONL', desc: '流式格式,适合大量消息' },
|
||||||
|
{ value: 'weclone', label: 'WeClone CSV', desc: 'WeClone 兼容字段格式(CSV)' },
|
||||||
|
{ value: 'sql', label: 'PostgreSQL', desc: '数据库脚本,便于导入到数据库' }
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const exportExcelColumnOptions = [
|
||||||
|
{ value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' },
|
||||||
|
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const
|
||||||
|
|
||||||
|
const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => {
|
||||||
|
return options.find((option) => option.value === value)?.label ?? value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportDefaultsSettingsForm({
|
||||||
|
onNotify,
|
||||||
|
onDefaultsChanged
|
||||||
|
}: ExportDefaultsSettingsFormProps) {
|
||||||
|
const [showExportFormatSelect, setShowExportFormatSelect] = useState(false)
|
||||||
|
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
|
||||||
|
const [showExportConcurrencySelect, setShowExportConcurrencySelect] = useState(false)
|
||||||
|
const [isExportDateRangeDialogOpen, setIsExportDateRangeDialogOpen] = useState(false)
|
||||||
|
const exportFormatDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const exportExcelColumnsDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const exportConcurrencyDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [exportDefaultFormat, setExportDefaultFormat] = useState('excel')
|
||||||
|
const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
||||||
|
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
|
||||||
|
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
|
||||||
|
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
||||||
|
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
void (async () => {
|
||||||
|
const [savedFormat, savedDateRange, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedConcurrency] = await Promise.all([
|
||||||
|
configService.getExportDefaultFormat(),
|
||||||
|
configService.getExportDefaultDateRange(),
|
||||||
|
configService.getExportDefaultMedia(),
|
||||||
|
configService.getExportDefaultVoiceAsText(),
|
||||||
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
|
configService.getExportDefaultConcurrency()
|
||||||
|
])
|
||||||
|
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
setExportDefaultFormat(savedFormat || 'excel')
|
||||||
|
setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange))
|
||||||
|
setExportDefaultMedia(savedMedia ?? false)
|
||||||
|
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
|
||||||
|
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
|
||||||
|
setExportDefaultConcurrency(savedConcurrency ?? 2)
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as Node
|
||||||
|
if (showExportFormatSelect && exportFormatDropdownRef.current && !exportFormatDropdownRef.current.contains(target)) {
|
||||||
|
setShowExportFormatSelect(false)
|
||||||
|
}
|
||||||
|
if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) {
|
||||||
|
setShowExportExcelColumnsSelect(false)
|
||||||
|
}
|
||||||
|
if (showExportConcurrencySelect && exportConcurrencyDropdownRef.current && !exportConcurrencyDropdownRef.current.contains(target)) {
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [showExportFormatSelect, showExportExcelColumnsSelect, showExportConcurrencySelect])
|
||||||
|
|
||||||
|
const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full'
|
||||||
|
const exportFormatLabel = useMemo(() => getOptionLabel(exportFormatOptions, exportDefaultFormat), [exportDefaultFormat])
|
||||||
|
const exportDateRangeLabel = useMemo(() => getExportDateRangeLabel(exportDefaultDateRange), [exportDefaultDateRange])
|
||||||
|
const exportExcelColumnsLabel = useMemo(() => getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue), [exportExcelColumnsValue])
|
||||||
|
const exportConcurrencyLabel = String(exportDefaultConcurrency)
|
||||||
|
|
||||||
|
const notify = (text: string, success = true) => {
|
||||||
|
onNotify?.(text, success)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="export-defaults-settings-form">
|
||||||
|
<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)
|
||||||
|
onDefaultsChanged?.({ format: option.value })
|
||||||
|
notify('已更新导出格式默认值', true)
|
||||||
|
setShowExportFormatSelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="option-label">{option.label}</span>
|
||||||
|
<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">></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExportDateRangeDialog
|
||||||
|
open={isExportDateRangeDialogOpen}
|
||||||
|
value={exportDefaultDateRange}
|
||||||
|
onClose={() => setIsExportDateRangeDialogOpen(false)}
|
||||||
|
onConfirm={async (nextSelection) => {
|
||||||
|
setExportDefaultDateRange(nextSelection)
|
||||||
|
await configService.setExportDefaultDateRange(serializeExportDateRangeConfig(nextSelection))
|
||||||
|
onDefaultsChanged?.({ dateRange: nextSelection })
|
||||||
|
notify('已更新默认导出时间范围', 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="shared-export-default-media">
|
||||||
|
<input
|
||||||
|
id="shared-export-default-media"
|
||||||
|
className="switch-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={exportDefaultMedia}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const enabled = e.target.checked
|
||||||
|
setExportDefaultMedia(enabled)
|
||||||
|
await configService.setExportDefaultMedia(enabled)
|
||||||
|
onDefaultsChanged?.({ media: enabled })
|
||||||
|
notify(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="shared-export-default-voice-as-text">
|
||||||
|
<input
|
||||||
|
id="shared-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)
|
||||||
|
onDefaultsChanged?.({ voiceAsText: enabled })
|
||||||
|
notify(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)
|
||||||
|
onDefaultsChanged?.({ excelCompactColumns: compact })
|
||||||
|
notify(compact ? '已启用精简列' : '已启用完整列', true)
|
||||||
|
setShowExportExcelColumnsSelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="option-label">{option.label}</span>
|
||||||
|
<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}
|
||||||
|
type="button"
|
||||||
|
className={`select-option ${exportDefaultConcurrency === option ? 'active' : ''}`}
|
||||||
|
onClick={async () => {
|
||||||
|
setExportDefaultConcurrency(option)
|
||||||
|
await configService.setExportDefaultConcurrency(option)
|
||||||
|
onDefaultsChanged?.({ concurrency: option })
|
||||||
|
notify(`已将导出并发数设为 ${option}`, true)
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="option-label">{option}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -261,14 +261,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.global-export-controls {
|
.global-export-controls {
|
||||||
flex: 0 1 820px;
|
flex: 0 1 980px;
|
||||||
width: min(820px, 100%);
|
width: min(980px, 100%);
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
display: grid;
|
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;
|
gap: 10px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
@@ -359,6 +359,32 @@
|
|||||||
z-index: 40;
|
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 {
|
.layout-trigger {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 10px;
|
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 {
|
.task-center-card-label {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -3285,9 +3367,9 @@
|
|||||||
.global-export-controls {
|
.global-export-controls {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-basis: 760px;
|
flex-basis: 920px;
|
||||||
width: min(760px, 100%);
|
width: min(920px, 100%);
|
||||||
grid-template-columns: minmax(0, 1.5fr) minmax(240px, 1fr);
|
grid-template-columns: minmax(0, 1.35fr) minmax(220px, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.format-grid {
|
.format-grid {
|
||||||
@@ -3333,6 +3415,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-defaults-modal {
|
||||||
|
width: min(92vw, 720px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
|||||||
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
||||||
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
|
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
|
||||||
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
|
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
|
||||||
|
import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm'
|
||||||
import type { SnsPost } from '../types/sns'
|
import type { SnsPost } from '../types/sns'
|
||||||
import {
|
import {
|
||||||
cloneExportDateRange,
|
cloneExportDateRange,
|
||||||
@@ -1282,8 +1283,14 @@ function ExportPage() {
|
|||||||
const [snsExportLivePhotos, setSnsExportLivePhotos] = useState(false)
|
const [snsExportLivePhotos, setSnsExportLivePhotos] = useState(false)
|
||||||
const [snsExportVideos, setSnsExportVideos] = useState(false)
|
const [snsExportVideos, setSnsExportVideos] = useState(false)
|
||||||
const [isTimeRangeDialogOpen, setIsTimeRangeDialogOpen] = useState(false)
|
const [isTimeRangeDialogOpen, setIsTimeRangeDialogOpen] = useState(false)
|
||||||
|
const [isExportDefaultsModalOpen, setIsExportDefaultsModalOpen] = useState(false)
|
||||||
const [timeRangeSelection, setTimeRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
const [timeRangeSelection, setTimeRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
||||||
|
const [exportDefaultFormat, setExportDefaultFormat] = useState<TextExportFormat>('excel')
|
||||||
const [exportDefaultDateRangeSelection, setExportDefaultDateRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
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>({
|
const [options, setOptions] = useState<ExportOptions>({
|
||||||
format: 'json',
|
format: 'json',
|
||||||
@@ -1785,8 +1792,9 @@ function ExportPage() {
|
|||||||
setIsBaseConfigLoading(true)
|
setIsBaseConfigLoading(true)
|
||||||
let isReady = true
|
let isReady = true
|
||||||
try {
|
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.getExportPath(),
|
||||||
|
configService.getExportDefaultFormat(),
|
||||||
configService.getExportDefaultMedia(),
|
configService.getExportDefaultMedia(),
|
||||||
configService.getExportDefaultVoiceAsText(),
|
configService.getExportDefaultVoiceAsText(),
|
||||||
configService.getExportDefaultExcelCompactColumns(),
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
@@ -1817,10 +1825,14 @@ function ExportPage() {
|
|||||||
setLastExportByContent(savedContentMap)
|
setLastExportByContent(savedContentMap)
|
||||||
setExportRecordsBySession(savedSessionRecordMap)
|
setExportRecordsBySession(savedSessionRecordMap)
|
||||||
setLastSnsExportPostCount(savedSnsPostCount)
|
setLastSnsExportPostCount(savedSnsPostCount)
|
||||||
|
setExportDefaultFormat((savedFormat as TextExportFormat) || 'excel')
|
||||||
|
setExportDefaultMedia(savedMedia ?? false)
|
||||||
|
setExportDefaultVoiceAsText(savedVoiceAsText ?? false)
|
||||||
|
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
|
||||||
|
setExportDefaultConcurrency(savedConcurrency ?? 2)
|
||||||
const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange)
|
const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange)
|
||||||
setExportDefaultDateRangeSelection(resolvedDefaultDateRange)
|
setExportDefaultDateRangeSelection(resolvedDefaultDateRange)
|
||||||
setTimeRangeSelection(resolvedDefaultDateRange)
|
setTimeRangeSelection(resolvedDefaultDateRange)
|
||||||
await configService.setExportDefaultFormat('json')
|
|
||||||
|
|
||||||
if (cachedSnsStats && Date.now() - cachedSnsStats.updatedAt <= EXPORT_SNS_STATS_CACHE_STALE_MS) {
|
if (cachedSnsStats && Date.now() - cachedSnsStats.updatedAt <= EXPORT_SNS_STATS_CACHE_STALE_MS) {
|
||||||
setSnsStats({
|
setSnsStats({
|
||||||
@@ -1835,7 +1847,7 @@ function ExportPage() {
|
|||||||
const txtColumns = savedTxtColumns && savedTxtColumns.length > 0 ? savedTxtColumns : defaultTxtColumns
|
const txtColumns = savedTxtColumns && savedTxtColumns.length > 0 ? savedTxtColumns : defaultTxtColumns
|
||||||
setOptions(prev => ({
|
setOptions(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
format: 'json',
|
format: ((savedFormat as TextExportFormat) || 'excel'),
|
||||||
exportMedia: savedMedia ?? prev.exportMedia,
|
exportMedia: savedMedia ?? prev.exportMedia,
|
||||||
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
|
exportVoiceAsText: savedVoiceAsText ?? prev.exportVoiceAsText,
|
||||||
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
|
excelCompactColumns: savedExcelCompactColumns ?? prev.excelCompactColumns,
|
||||||
@@ -3192,8 +3204,13 @@ function ExportPage() {
|
|||||||
|
|
||||||
const next: ExportOptions = {
|
const next: ExportOptions = {
|
||||||
...prev,
|
...prev,
|
||||||
|
format: exportDefaultFormat,
|
||||||
useAllTime: exportDefaultDateRangeSelection.useAllTime,
|
useAllTime: exportDefaultDateRangeSelection.useAllTime,
|
||||||
dateRange: nextDateRange
|
dateRange: nextDateRange,
|
||||||
|
exportMedia: exportDefaultMedia,
|
||||||
|
exportVoiceAsText: exportDefaultVoiceAsText,
|
||||||
|
excelCompactColumns: exportDefaultExcelCompactColumns,
|
||||||
|
exportConcurrency: exportDefaultConcurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.scope === 'sns') {
|
if (payload.scope === 'sns') {
|
||||||
@@ -3220,7 +3237,14 @@ function ExportPage() {
|
|||||||
|
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}, [exportDefaultDateRangeSelection])
|
}, [
|
||||||
|
exportDefaultDateRangeSelection,
|
||||||
|
exportDefaultExcelCompactColumns,
|
||||||
|
exportDefaultFormat,
|
||||||
|
exportDefaultMedia,
|
||||||
|
exportDefaultVoiceAsText,
|
||||||
|
exportDefaultConcurrency
|
||||||
|
])
|
||||||
|
|
||||||
const closeExportDialog = useCallback(() => {
|
const closeExportDialog = useCallback(() => {
|
||||||
setExportDialog(prev => ({ ...prev, open: false }))
|
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 (
|
return (
|
||||||
<div className="export-board-page">
|
<div className="export-board-page">
|
||||||
<div className="export-top-panel">
|
<div className="export-top-panel">
|
||||||
@@ -5186,6 +5231,16 @@ function ExportPage() {
|
|||||||
await configService.setExportSessionNamePrefixEnabled(enabled)
|
await configService.setExportSessionNamePrefixEnabled(enabled)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="more-export-settings-control">
|
||||||
|
<button
|
||||||
|
className="more-export-settings-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsExportDefaultsModalOpen(true)}
|
||||||
|
>
|
||||||
|
更多导出设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -5212,6 +5267,48 @@ function ExportPage() {
|
|||||||
onTogglePerfTask={toggleTaskPerfDetail}
|
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">
|
<div className="export-section-title-row">
|
||||||
<h3 className="export-section-title">按类型批量导出</h3>
|
<h3 className="export-section-title">按类型批量导出</h3>
|
||||||
<SectionInfoTooltip
|
<SectionInfoTooltip
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
|
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
|
||||||
|
import { ExportDefaultsSettingsForm } from '../components/Export/ExportDefaultsSettingsForm'
|
||||||
import {
|
import {
|
||||||
createDefaultExportDateRangeSelection,
|
createDefaultExportDateRangeSelection,
|
||||||
getExportDateRangeLabel,
|
getExportDateRangeLabel,
|
||||||
@@ -1588,212 +1589,9 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderExportTab = () => {
|
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 (
|
return (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
<div className="form-group">
|
<ExportDefaultsSettingsForm onNotify={showMessage} />
|
||||||
<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">></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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user