全新出发
This commit is contained in:
1 parent
b06c117b1e
commit
fa4edebed6
463 files changed
+181610
-125
No files matched your search
@@ -0,0 +1,454 @@
|
||||
.export-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: color-mix(in srgb, var(--bg-primary) 60%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
|
||||
.export-dialog-container {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 12px 48px color-mix(in srgb, var(--shadow-color) 40%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
overflow: hidden;
|
||||
|
||||
// Header
|
||||
.dialog-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.title-area {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-tertiary);
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Body
|
||||
.dialog-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.config-row-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: var(--bg-surface, var(--bg-primary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.row-label {
|
||||
width: 72px;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.row-value-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-main, var(--bg-secondary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 8px;
|
||||
|
||||
.icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--bg-surface, var(--bg-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.export-path-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.path-display-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
|
||||
.path-display-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--bg-tertiary, var(--bg-secondary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 5px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 0;
|
||||
|
||||
.path-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 4%, transparent);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.open-folder-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 4%, transparent);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.change-path-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
padding: 0 16px;
|
||||
background: var(--bg-surface, var(--bg-primary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-main, var(--bg-secondary));
|
||||
border-color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
|
||||
svg {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.format-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
.format-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.format-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.format-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color));
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 4%, transparent);
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
|
||||
.format-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-options, .advanced-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: var(--primary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
&:has(input:checked) {
|
||||
border-color: color-mix(in srgb, var(--primary) 50%, transparent);
|
||||
background: color-mix(in srgb, var(--primary) 4%, transparent);
|
||||
color: var(--primary);
|
||||
|
||||
svg {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-size-limit, .concurrency-control, .path-style-control {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
input, select {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.path-style-control {
|
||||
flex: 1 1 100%;
|
||||
margin-top: 4px;
|
||||
|
||||
> span {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.path-style-segmented {
|
||||
display: inline-flex;
|
||||
min-height: 34px;
|
||||
padding: 3px;
|
||||
gap: 3px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
|
||||
button {
|
||||
min-width: 82px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 28%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
.dialog-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
background: var(--bg-secondary);
|
||||
|
||||
button {
|
||||
padding: 8px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.cancel-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
background: var(--primary);
|
||||
border: 1px solid var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--primary) 90%, black);
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 40%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import React, { memo, useState, useEffect } from 'react'
|
||||
import {
|
||||
X,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
Video,
|
||||
Mic,
|
||||
FileBox,
|
||||
Smile,
|
||||
Calendar,
|
||||
Settings,
|
||||
HardDrive,
|
||||
ExternalLink,
|
||||
FolderOpen
|
||||
} from 'lucide-react'
|
||||
import { ExportDateRangeDialog } from '../../../../components/Export/ExportDateRangeDialog'
|
||||
import {
|
||||
getExportDateRangeLabel,
|
||||
resolveExportDateRangeConfig,
|
||||
serializeExportDateRangeConfig,
|
||||
type ExportDefaultDateRangeConfig
|
||||
} from '../../../../utils/exportDateRange'
|
||||
import type { ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
||||
import { formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
||||
import { formatPathBrief } from '../../utils/format'
|
||||
import './ExportDialog.scss'
|
||||
|
||||
interface ExportDialogProps {
|
||||
dialogState: ExportDialogState
|
||||
onClose: () => void
|
||||
options: ExportOptions
|
||||
onOptionsChange: (patch: Partial<ExportOptions>) => void
|
||||
exportPath: string
|
||||
onSelectPath: () => void
|
||||
rawDateRangeConfig: ExportDefaultDateRangeConfig | string | null
|
||||
onDateRangeConfigChange: (config: ExportDefaultDateRangeConfig | string | null) => void
|
||||
onConfirm: (finalOptions: ExportOptions) => void
|
||||
onAutomationCreate: (finalOptions: ExportOptions) => void
|
||||
}
|
||||
|
||||
const ExportDialog: React.FC<ExportDialogProps> = ({
|
||||
dialogState,
|
||||
onClose,
|
||||
options,
|
||||
onOptionsChange,
|
||||
exportPath,
|
||||
onSelectPath,
|
||||
rawDateRangeConfig,
|
||||
onDateRangeConfigChange,
|
||||
onConfirm,
|
||||
onAutomationCreate
|
||||
}) => {
|
||||
const [isDateRangeOpen, setIsDateRangeOpen] = useState(false)
|
||||
|
||||
const currentSelection = React.useMemo(() => {
|
||||
return resolveExportDateRangeConfig(rawDateRangeConfig)
|
||||
}, [rawDateRangeConfig])
|
||||
|
||||
const dateRangeLabel = React.useMemo(() => {
|
||||
return getExportDateRangeLabel(currentSelection)
|
||||
}, [currentSelection])
|
||||
|
||||
if (!dialogState.open) return null
|
||||
|
||||
// Local state for UI toggles that might not strictly map 1:1 to config immediately
|
||||
// But we'll just use the global options for now
|
||||
const { intent, scope, sessionNames, title } = dialogState
|
||||
const isAutomation = intent === 'automation-create'
|
||||
|
||||
const handleFormatSelect = (format: TextExportFormat) => {
|
||||
onOptionsChange({ format })
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (isAutomation) {
|
||||
onAutomationCreate(options)
|
||||
} else {
|
||||
onConfirm(options)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="export-dialog-overlay">
|
||||
<div className="export-dialog-container">
|
||||
{/* Header */}
|
||||
<div className="dialog-header">
|
||||
<div className="title-area">
|
||||
<h2 className="title">{title}</h2>
|
||||
<div className="subtitle">
|
||||
{scope === 'single'
|
||||
? `准备导出包含 ${sessionNames[0]} 的会话内容`
|
||||
: `已选择 ${sessionNames.length} 个会话`}
|
||||
{isAutomation && ' (自动化任务模板配置)'}
|
||||
</div>
|
||||
</div>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="dialog-body">
|
||||
{/* Export Path */}
|
||||
<div className="config-section">
|
||||
<div className="section-title">
|
||||
<HardDrive size={16} /> 导出配置
|
||||
</div>
|
||||
|
||||
<div className="config-row-group">
|
||||
<div className="config-row">
|
||||
<span className="row-label">时间范围</span>
|
||||
<button
|
||||
className="row-value-btn"
|
||||
onClick={() => setIsDateRangeOpen(true)}
|
||||
>
|
||||
<Calendar size={14} className="icon" />
|
||||
<span className="text">{dateRangeLabel}</span>
|
||||
<span className="arrow">></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="config-row">
|
||||
<span className="row-label">保存路径</span>
|
||||
<div className="export-path-row">
|
||||
<div className="path-display-group">
|
||||
<button
|
||||
className="path-display-btn"
|
||||
onClick={onSelectPath}
|
||||
title={exportPath}
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
<span className="path-text">
|
||||
{exportPath ? formatPathBrief(exportPath, 46) : '请选择导出目录...'}
|
||||
</span>
|
||||
</button>
|
||||
{exportPath && (
|
||||
<button
|
||||
className="open-folder-btn"
|
||||
title="在文件管理器中打开"
|
||||
onClick={() => window.electronAPI.shell.openPath(exportPath)}
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button className="change-path-btn" onClick={onSelectPath}>
|
||||
更改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!exportPath && <div className="error-text">请先选择一个保存路径</div>}
|
||||
</div>
|
||||
|
||||
{/* Format Selection */}
|
||||
<div className="config-section">
|
||||
<div className="section-title">
|
||||
<FileText size={16} /> 导出格式
|
||||
</div>
|
||||
<div className="format-grid">
|
||||
{formatOptions.map(fmt => (
|
||||
<button
|
||||
key={fmt.value}
|
||||
className={`format-card ${options.format === fmt.value ? 'active' : ''}`}
|
||||
onClick={() => handleFormatSelect(fmt.value)}
|
||||
>
|
||||
<div className="format-name">{fmt.label}</div>
|
||||
<div className="format-desc">{fmt.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Media Configuration */}
|
||||
<div className="config-section">
|
||||
<div className="section-title">
|
||||
<HardDrive size={16} /> 媒体与文件附件
|
||||
</div>
|
||||
<div className="media-options">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportImages}
|
||||
onChange={e => onOptionsChange({ exportImages: e.target.checked })}
|
||||
/>
|
||||
<ImageIcon size={16} /> 导出图片
|
||||
</label>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportVideos}
|
||||
onChange={e => onOptionsChange({ exportVideos: e.target.checked })}
|
||||
/>
|
||||
<Video size={16} /> 导出视频
|
||||
</label>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportVoices}
|
||||
onChange={e => onOptionsChange({ exportVoices: e.target.checked })}
|
||||
/>
|
||||
<Mic size={16} /> 导出语音
|
||||
</label>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportFiles}
|
||||
onChange={e => onOptionsChange({ exportFiles: e.target.checked })}
|
||||
/>
|
||||
<FileBox size={16} /> 导出文件
|
||||
</label>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportEmojis}
|
||||
onChange={e => onOptionsChange({ exportEmojis: e.target.checked })}
|
||||
/>
|
||||
<Smile size={16} /> 导出表情包
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{(options.exportVideos || options.exportFiles) && (
|
||||
<div className="file-size-limit">
|
||||
<span>最大文件限制 (MB):</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_EXPORT_FILE_SIZE_MB_LIMIT}
|
||||
value={options.maxFileSizeMb}
|
||||
onChange={e => onOptionsChange({ maxFileSizeMb: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<div className="config-section">
|
||||
<div className="section-title">
|
||||
<Settings size={16} /> 高级选项
|
||||
</div>
|
||||
<div className="advanced-options">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportAvatars}
|
||||
onChange={e => onOptionsChange({ exportAvatars: e.target.checked })}
|
||||
/>
|
||||
包含联系人头像
|
||||
</label>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.exportVoiceAsText}
|
||||
onChange={e => onOptionsChange({ exportVoiceAsText: e.target.checked })}
|
||||
/>
|
||||
语音转文字 (若已转换)
|
||||
</label>
|
||||
|
||||
<div className="path-style-control">
|
||||
<span>媒体路径:</span>
|
||||
<div className="path-style-segmented" title="控制 TXT/JSON/Excel/CSV 中写入的媒体相对路径分隔符">
|
||||
{[
|
||||
{ value: 'auto', label: '当前系统' },
|
||||
{ value: 'posix', label: 'macOS/Linux' },
|
||||
{ value: 'windows', label: 'Windows' }
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={options.exportPathStyle === item.value ? 'active' : ''}
|
||||
onClick={() => onOptionsChange({ exportPathStyle: item.value as ExportOptions['exportPathStyle'] })}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="concurrency-control">
|
||||
<span>导出并发数:</span>
|
||||
<select
|
||||
value={options.exportConcurrency}
|
||||
onChange={e => onOptionsChange({ exportConcurrency: Number(e.target.value) })}
|
||||
>
|
||||
<option value={1}>1 (最稳定)</option>
|
||||
<option value={3}>3 (推荐)</option>
|
||||
<option value={5}>5 (较快)</option>
|
||||
<option value={10}>10 (最快, 易卡顿)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="dialog-footer">
|
||||
<button className="cancel-btn" onClick={onClose}>取消</button>
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={handleConfirm}
|
||||
disabled={!exportPath}
|
||||
>
|
||||
{isAutomation ? '创建自动化任务' : '开始导出'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExportDateRangeDialog
|
||||
open={isDateRangeOpen}
|
||||
value={currentSelection}
|
||||
onClose={() => setIsDateRangeOpen(false)}
|
||||
onConfirm={(nextSelection) => {
|
||||
onDateRangeConfigChange(serializeExportDateRangeConfig(nextSelection))
|
||||
setIsDateRangeOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ExportDialog)
|
||||
Reference in new issue
Block a user