ConfigManager.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import React, { useRef } from 'react';
  2. import {
  3. Button,
  4. Typography,
  5. Toast,
  6. Modal,
  7. Dropdown,
  8. } from '@douyinfe/semi-ui';
  9. import {
  10. Download,
  11. Upload,
  12. RotateCcw,
  13. Settings2,
  14. } from 'lucide-react';
  15. import { useTranslation } from 'react-i18next';
  16. import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage';
  17. const ConfigManager = ({
  18. currentConfig,
  19. onConfigImport,
  20. onConfigReset,
  21. styleState,
  22. messages,
  23. }) => {
  24. const { t } = useTranslation();
  25. const fileInputRef = useRef(null);
  26. const handleExport = () => {
  27. try {
  28. // 在导出前先保存当前配置,确保导出的是最新内容
  29. const configWithTimestamp = {
  30. ...currentConfig,
  31. timestamp: new Date().toISOString(),
  32. };
  33. localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp));
  34. exportConfig(currentConfig, messages);
  35. Toast.success({
  36. content: t('配置已导出到下载文件夹'),
  37. duration: 3,
  38. });
  39. } catch (error) {
  40. Toast.error({
  41. content: t('导出配置失败: ') + error.message,
  42. duration: 3,
  43. });
  44. }
  45. };
  46. const handleImportClick = () => {
  47. fileInputRef.current?.click();
  48. };
  49. const handleFileChange = async (event) => {
  50. const file = event.target.files[0];
  51. if (!file) return;
  52. try {
  53. const importedConfig = await importConfig(file);
  54. Modal.confirm({
  55. title: t('确认导入配置'),
  56. content: t('导入的配置将覆盖当前设置,是否继续?'),
  57. okText: t('确定导入'),
  58. cancelText: t('取消'),
  59. onOk: () => {
  60. onConfigImport(importedConfig);
  61. Toast.success({
  62. content: t('配置导入成功'),
  63. duration: 3,
  64. });
  65. },
  66. });
  67. } catch (error) {
  68. Toast.error({
  69. content: t('导入配置失败: ') + error.message,
  70. duration: 3,
  71. });
  72. } finally {
  73. // 重置文件输入,允许重复选择同一文件
  74. event.target.value = '';
  75. }
  76. };
  77. const handleReset = () => {
  78. Modal.confirm({
  79. title: t('重置配置'),
  80. content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'),
  81. okText: t('确定重置'),
  82. cancelText: t('取消'),
  83. okButtonProps: {
  84. type: 'danger',
  85. },
  86. onOk: () => {
  87. // 询问是否同时重置消息
  88. Modal.confirm({
  89. title: t('重置选项'),
  90. content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'),
  91. okText: t('同时重置消息'),
  92. cancelText: t('仅重置配置'),
  93. okButtonProps: {
  94. type: 'danger',
  95. },
  96. onOk: () => {
  97. clearConfig();
  98. onConfigReset({ resetMessages: true });
  99. Toast.success({
  100. content: t('配置和消息已全部重置'),
  101. duration: 3,
  102. });
  103. },
  104. onCancel: () => {
  105. clearConfig();
  106. onConfigReset({ resetMessages: false });
  107. Toast.success({
  108. content: t('配置已重置,对话消息已保留'),
  109. duration: 3,
  110. });
  111. },
  112. });
  113. },
  114. });
  115. };
  116. const getConfigStatus = () => {
  117. if (hasStoredConfig()) {
  118. const timestamp = getConfigTimestamp();
  119. if (timestamp) {
  120. const date = new Date(timestamp);
  121. return t('上次保存: ') + date.toLocaleString();
  122. }
  123. return t('已有保存的配置');
  124. }
  125. return t('暂无保存的配置');
  126. };
  127. const dropdownItems = [
  128. {
  129. node: 'item',
  130. name: 'export',
  131. onClick: handleExport,
  132. children: (
  133. <div className="flex items-center gap-2">
  134. <Download size={14} />
  135. {t('导出配置')}
  136. </div>
  137. ),
  138. },
  139. {
  140. node: 'item',
  141. name: 'import',
  142. onClick: handleImportClick,
  143. children: (
  144. <div className="flex items-center gap-2">
  145. <Upload size={14} />
  146. {t('导入配置')}
  147. </div>
  148. ),
  149. },
  150. {
  151. node: 'divider',
  152. },
  153. {
  154. node: 'item',
  155. name: 'reset',
  156. onClick: handleReset,
  157. children: (
  158. <div className="flex items-center gap-2 text-red-600">
  159. <RotateCcw size={14} />
  160. {t('重置配置')}
  161. </div>
  162. ),
  163. },
  164. ];
  165. if (styleState.isMobile) {
  166. // 移动端显示简化的下拉菜单
  167. return (
  168. <>
  169. <Dropdown
  170. trigger="click"
  171. position="bottomLeft"
  172. showTick
  173. menu={dropdownItems}
  174. >
  175. <Button
  176. icon={<Settings2 size={14} />}
  177. theme="borderless"
  178. type="tertiary"
  179. size="small"
  180. className="!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50"
  181. />
  182. </Dropdown>
  183. <input
  184. ref={fileInputRef}
  185. type="file"
  186. accept=".json"
  187. onChange={handleFileChange}
  188. style={{ display: 'none' }}
  189. />
  190. </>
  191. );
  192. }
  193. // 桌面端显示紧凑的按钮组
  194. return (
  195. <div className="space-y-3">
  196. {/* 配置状态信息和重置按钮 */}
  197. <div className="flex items-center justify-between">
  198. <Typography.Text className="text-xs text-gray-500">
  199. {getConfigStatus()}
  200. </Typography.Text>
  201. <Button
  202. icon={<RotateCcw size={12} />}
  203. size="small"
  204. theme="borderless"
  205. type="danger"
  206. onClick={handleReset}
  207. className="!rounded-full !text-xs !px-2"
  208. />
  209. </div>
  210. {/* 导出和导入按钮 */}
  211. <div className="flex gap-2">
  212. <Button
  213. icon={<Download size={12} />}
  214. size="small"
  215. theme="solid"
  216. type="primary"
  217. onClick={handleExport}
  218. className="!rounded-lg flex-1 !text-xs !h-7"
  219. >
  220. {t('导出')}
  221. </Button>
  222. <Button
  223. icon={<Upload size={12} />}
  224. size="small"
  225. theme="outline"
  226. type="primary"
  227. onClick={handleImportClick}
  228. className="!rounded-lg flex-1 !text-xs !h-7"
  229. >
  230. {t('导入')}
  231. </Button>
  232. </div>
  233. <input
  234. ref={fileInputRef}
  235. type="file"
  236. accept=".json"
  237. onChange={handleFileChange}
  238. style={{ display: 'none' }}
  239. />
  240. </div>
  241. );
  242. };
  243. export default ConfigManager;