|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useRef } from 'react'; |
|
|
import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui'; |
|
|
import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { |
|
|
exportConfig, |
|
|
importConfig, |
|
|
clearConfig, |
|
|
hasStoredConfig, |
|
|
getConfigTimestamp, |
|
|
} from './configStorage'; |
|
|
|
|
|
const ConfigManager = ({ |
|
|
currentConfig, |
|
|
onConfigImport, |
|
|
onConfigReset, |
|
|
styleState, |
|
|
messages, |
|
|
}) => { |
|
|
const { t } = useTranslation(); |
|
|
const fileInputRef = useRef(null); |
|
|
|
|
|
const handleExport = () => { |
|
|
try { |
|
|
|
|
|
const configWithTimestamp = { |
|
|
...currentConfig, |
|
|
timestamp: new Date().toISOString(), |
|
|
}; |
|
|
localStorage.setItem( |
|
|
'playground_config', |
|
|
JSON.stringify(configWithTimestamp), |
|
|
); |
|
|
|
|
|
exportConfig(currentConfig, messages); |
|
|
Toast.success({ |
|
|
content: t('配置已导出到下载文件夹'), |
|
|
duration: 3, |
|
|
}); |
|
|
} catch (error) { |
|
|
Toast.error({ |
|
|
content: t('导出配置失败: ') + error.message, |
|
|
duration: 3, |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleImportClick = () => { |
|
|
fileInputRef.current?.click(); |
|
|
}; |
|
|
|
|
|
const handleFileChange = async (event) => { |
|
|
const file = event.target.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
try { |
|
|
const importedConfig = await importConfig(file); |
|
|
|
|
|
Modal.confirm({ |
|
|
title: t('确认导入配置'), |
|
|
content: t('导入的配置将覆盖当前设置,是否继续?'), |
|
|
okText: t('确定导入'), |
|
|
cancelText: t('取消'), |
|
|
onOk: () => { |
|
|
onConfigImport(importedConfig); |
|
|
Toast.success({ |
|
|
content: t('配置导入成功'), |
|
|
duration: 3, |
|
|
}); |
|
|
}, |
|
|
}); |
|
|
} catch (error) { |
|
|
Toast.error({ |
|
|
content: t('导入配置失败: ') + error.message, |
|
|
duration: 3, |
|
|
}); |
|
|
} finally { |
|
|
|
|
|
event.target.value = ''; |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleReset = () => { |
|
|
Modal.confirm({ |
|
|
title: t('重置配置'), |
|
|
content: t( |
|
|
'将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?', |
|
|
), |
|
|
okText: t('确定重置'), |
|
|
cancelText: t('取消'), |
|
|
okButtonProps: { |
|
|
type: 'danger', |
|
|
}, |
|
|
onOk: () => { |
|
|
|
|
|
Modal.confirm({ |
|
|
title: t('重置选项'), |
|
|
content: t( |
|
|
'是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。', |
|
|
), |
|
|
okText: t('同时重置消息'), |
|
|
cancelText: t('仅重置配置'), |
|
|
okButtonProps: { |
|
|
type: 'danger', |
|
|
}, |
|
|
onOk: () => { |
|
|
clearConfig(); |
|
|
onConfigReset({ resetMessages: true }); |
|
|
Toast.success({ |
|
|
content: t('配置和消息已全部重置'), |
|
|
duration: 3, |
|
|
}); |
|
|
}, |
|
|
onCancel: () => { |
|
|
clearConfig(); |
|
|
onConfigReset({ resetMessages: false }); |
|
|
Toast.success({ |
|
|
content: t('配置已重置,对话消息已保留'), |
|
|
duration: 3, |
|
|
}); |
|
|
}, |
|
|
}); |
|
|
}, |
|
|
}); |
|
|
}; |
|
|
|
|
|
const getConfigStatus = () => { |
|
|
if (hasStoredConfig()) { |
|
|
const timestamp = getConfigTimestamp(); |
|
|
if (timestamp) { |
|
|
const date = new Date(timestamp); |
|
|
return t('上次保存: ') + date.toLocaleString(); |
|
|
} |
|
|
return t('已有保存的配置'); |
|
|
} |
|
|
return t('暂无保存的配置'); |
|
|
}; |
|
|
|
|
|
const dropdownItems = [ |
|
|
{ |
|
|
node: 'item', |
|
|
name: 'export', |
|
|
onClick: handleExport, |
|
|
children: ( |
|
|
<div className='flex items-center gap-2'> |
|
|
<Download size={14} /> |
|
|
{t('导出配置')} |
|
|
</div> |
|
|
), |
|
|
}, |
|
|
{ |
|
|
node: 'item', |
|
|
name: 'import', |
|
|
onClick: handleImportClick, |
|
|
children: ( |
|
|
<div className='flex items-center gap-2'> |
|
|
<Upload size={14} /> |
|
|
{t('导入配置')} |
|
|
</div> |
|
|
), |
|
|
}, |
|
|
{ |
|
|
node: 'divider', |
|
|
}, |
|
|
{ |
|
|
node: 'item', |
|
|
name: 'reset', |
|
|
onClick: handleReset, |
|
|
children: ( |
|
|
<div className='flex items-center gap-2 text-red-600'> |
|
|
<RotateCcw size={14} /> |
|
|
{t('重置配置')} |
|
|
</div> |
|
|
), |
|
|
}, |
|
|
]; |
|
|
|
|
|
if (styleState.isMobile) { |
|
|
|
|
|
return ( |
|
|
<> |
|
|
<Dropdown |
|
|
trigger='click' |
|
|
position='bottomLeft' |
|
|
showTick |
|
|
menu={dropdownItems} |
|
|
> |
|
|
<Button |
|
|
icon={<Settings2 size={14} />} |
|
|
theme='borderless' |
|
|
type='tertiary' |
|
|
size='small' |
|
|
className='!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50' |
|
|
/> |
|
|
</Dropdown> |
|
|
|
|
|
<input |
|
|
ref={fileInputRef} |
|
|
type='file' |
|
|
accept='.json' |
|
|
onChange={handleFileChange} |
|
|
style={{ display: 'none' }} |
|
|
/> |
|
|
</> |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
return ( |
|
|
<div className='space-y-3'> |
|
|
{/* 配置状态信息和重置按钮 */} |
|
|
<div className='flex items-center justify-between'> |
|
|
<Typography.Text className='text-xs text-gray-500'> |
|
|
{getConfigStatus()} |
|
|
</Typography.Text> |
|
|
<Button |
|
|
icon={<RotateCcw size={12} />} |
|
|
size='small' |
|
|
theme='borderless' |
|
|
type='danger' |
|
|
onClick={handleReset} |
|
|
className='!rounded-full !text-xs !px-2' |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{/* 导出和导入按钮 */} |
|
|
<div className='flex gap-2'> |
|
|
<Button |
|
|
icon={<Download size={12} />} |
|
|
size='small' |
|
|
theme='solid' |
|
|
type='primary' |
|
|
onClick={handleExport} |
|
|
className='!rounded-lg flex-1 !text-xs !h-7' |
|
|
> |
|
|
{t('导出')} |
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
icon={<Upload size={12} />} |
|
|
size='small' |
|
|
theme='outline' |
|
|
type='primary' |
|
|
onClick={handleImportClick} |
|
|
className='!rounded-lg flex-1 !text-xs !h-7' |
|
|
> |
|
|
{t('导入')} |
|
|
</Button> |
|
|
</div> |
|
|
|
|
|
<input |
|
|
ref={fileInputRef} |
|
|
type='file' |
|
|
accept='.json' |
|
|
onChange={handleFileChange} |
|
|
style={{ display: 'none' }} |
|
|
/> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default ConfigManager; |
|
|
|