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)
195 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|