Files
ele-HeiXiu/src/pages/settings/sections/StoragePathSetting.tsx
YoungestSongMo 0e6d996718 refactor: 重组 components/ 和 pages/ 文件结构,按职责分层
components/ 拆为三层:
  - providers/   — 状态管理(App/Theme/SettingsProvider)
  - shell/       — 应用外壳(Layout/NavBar/PageDispatcher)
  - ui/          — 通用 UI 原子(FloatingPanel/LegalTextModal)

pages/ 重命名 + 重组:
  - headers/ → panels/        NavBar 触发的面板页
  - login/   → auth/          认证模块(含注册/忘记密码)
  - settings/blocks/ → sections/
  - home/components/ → panels/ features/ controls/ hooks/
    按职责分:panels(布局壳) + features(功能面板) + controls(表单控件) + hooks

其他清理:
  - 删除空占位 Test3.tsx
  - HomeContext 从 HomeContent 拆出独立文件
  - uploadValidation 内联到 ImageInput
  - 修复过时注释(useUI → controls)
2026-06-15 11:19:06 +08:00

195 lines
5.9 KiB
TypeScript

// ============================================================
// StoragePathSetting — 存储路径设置区块
//
// 个人版:仅"大模型产物存储仓库路径"
// 企业版:额外"团队创作存储仓库路径"
// ============================================================
import { useState, useCallback } from 'react';
import { Card, Button, Input, Typography, Space, App } from 'antd';
import { FolderOpenOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import type { Edition } from '@shared/types';
import { useSettings } from '@/components/providers/SettingsProvider';
import { hasIpc, safeIpcInvoke } from '@/utils/ipc';
import { BIDIRECTIONAL } from '@shared/constants/ipc-channels';
const { Text, Paragraph } = Typography;
interface StoragePathSettingProps {
edition: Edition;
}
/** 单行路径选择器 */
function PathRow({
label,
path,
onChange,
onClear,
}: {
label: string;
path: string;
onChange: (path: string) => void;
onClear: () => void;
}) {
const [editing, setEditing] = useState(false);
const [inputValue, setInputValue] = useState(path);
const { message } = App.useApp();
const handleSelectFolder = useCallback(async () => {
if (!hasIpc()) {
// 非 Electron 环境 → 切换为手动输入模式
setInputValue(path);
setEditing(true);
return;
}
try {
const result = (await safeIpcInvoke(BIDIRECTIONAL.FILE_DIALOG, {
title: label,
defaultPath: path || undefined,
properties: ['openDirectory'],
})) as { canceled: boolean; filePaths: string[] } | undefined;
if (!result || result.canceled || result.filePaths.length === 0) return;
const selectedPath = result.filePaths[0];
onChange(selectedPath);
message.success('路径已更新');
} catch {
message.error('选择文件夹失败');
}
}, [label, path, onChange, message]);
const handleSaveInput = useCallback(() => {
onChange(inputValue.trim());
setEditing(false);
if (inputValue.trim()) {
message.success('路径已更新');
}
}, [inputValue, onChange, message]);
const handleCancelEdit = useCallback(() => {
setInputValue(path);
setEditing(false);
}, [path]);
return (
<div className="flex flex-col gap-1.5">
<Text type="secondary" className="text-xs">
{label}
</Text>
{editing ? (
<Space.Compact className="w-full">
<Input
size="small"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onPressEnter={handleSaveInput}
placeholder="请输入或粘贴路径"
className="flex-1"
/>
<Button size="small" type="primary" onClick={handleSaveInput}>
</Button>
<Button size="small" onClick={handleCancelEdit}>
</Button>
</Space.Compact>
) : path ? (
<div className="flex items-center gap-2">
<Paragraph
className="mb-0! flex-1 min-w-0"
style={{ maxWidth: 240 }}
ellipsis={{ rows: 1, tooltip: path }}
>
<Text className="text-xs" style={{ fontFamily: 'monospace' }}>
{path}
</Text>
</Paragraph>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => {
setInputValue(path);
setEditing(true);
}}
title="手动编辑"
/>
<Button
size="small"
type="text"
danger
icon={<DeleteOutlined />}
onClick={onClear}
title="清除路径"
/>
</div>
) : (
<div className="flex items-center gap-2">
<Text type="secondary" className="text-xs italic flex-1">
</Text>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => {
setInputValue('');
setEditing(true);
}}
title="手动输入"
/>
</div>
)}
{!editing && (
<Button
size="small"
icon={<FolderOpenOutlined />}
onClick={handleSelectFolder}
className="self-start mt-1"
>
{hasIpc() ? '选择文件夹' : '手动输入路径'}
</Button>
)}
</div>
);
}
export function StoragePathSetting({ edition }: StoragePathSettingProps) {
const {
outputPath,
teamRepoPath,
setOutputPath,
setTeamRepoPath,
clearOutputPath,
clearTeamRepoPath,
} = useSettings();
const isEnterprise = edition === 'enterprise';
return (
<Card size="small" title="存储路径设置" className="rounded-md!">
<div className="flex flex-col gap-4">
<PathRow
label="大模型产物存储仓库路径"
path={outputPath}
onChange={setOutputPath}
onClear={clearOutputPath}
/>
{isEnterprise && (
<PathRow
label="团队创作存储仓库路径"
path={teamRepoPath}
onChange={setTeamRepoPath}
onClear={clearTeamRepoPath}
/>
)}
</div>
</Card>
);
}