Feat: 将编辑器状态存储在 Store 里面,调整头部连接组件逻辑
This commit is contained in:
parent
bf1a7dde48
commit
b214fa944b
|
|
@ -2,6 +2,8 @@ import {
|
|||
disconnectLanguage,
|
||||
useFileConnections,
|
||||
} from "@/store/file-connection";
|
||||
import { useProjectSourcesStore } from "@/store/sources-store";
|
||||
import { isTauriEnv } from "@/lib/is-tauri";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { Button } from "../ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -11,6 +13,99 @@ type Props = {
|
|||
};
|
||||
|
||||
export function HeaderConnectionIndicator({ projectId }: Props) {
|
||||
const isTauri = isTauriEnv();
|
||||
|
||||
if (isTauri) {
|
||||
return <TauriConnectionIndicator />;
|
||||
}
|
||||
|
||||
return <BrowserConnectionIndicator projectId={projectId} />;
|
||||
}
|
||||
|
||||
/** Tauri 模式:展示目前链接的所有语言文件地址 */
|
||||
function TauriConnectionIndicator() {
|
||||
const { languages, baseDir } = useProjectSourcesStore();
|
||||
|
||||
const hasAny = languages.length > 0;
|
||||
const allExist = languages.every((lang) => lang.exists);
|
||||
const missingCount = languages.filter((lang) => !lang.exists).length;
|
||||
|
||||
// 状态颜色:全部存在=绿色,部分缺失=黄色,无文件=红色
|
||||
const statusColor = !hasAny
|
||||
? "bg-red-500"
|
||||
: allExist
|
||||
? "bg-green-500"
|
||||
: "bg-yellow-500";
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
title={hasAny ? "已连接文件" : "未配置任何语言文件"}
|
||||
>
|
||||
<span className={cn("size-2 rounded-full", statusColor)}></span>
|
||||
{hasAny
|
||||
? `已连接 ${languages.length}${missingCount > 0 ? ` (${missingCount} 缺失)` : ""}`
|
||||
: "未配置"}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="p-4 max-w-md" align="end">
|
||||
<div className="text-sm font-medium mb-2">连接状态 (Tauri)</div>
|
||||
{languages.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
暂无语言文件配置。请通过项目设置配置语言源目录。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{baseDir && (
|
||||
<div className="text-xs text-muted-foreground mb-2 break-all">
|
||||
基础目录:{baseDir}
|
||||
</div>
|
||||
)}
|
||||
{languages.map((lang) => (
|
||||
<div
|
||||
key={lang.language}
|
||||
className="flex items-start justify-between gap-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 rounded-full shrink-0",
|
||||
lang.exists ? "bg-green-500" : "bg-red-500"
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium truncate">
|
||||
{lang.language}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs text-muted-foreground truncate ml-3.5"
|
||||
title={lang.path}
|
||||
>
|
||||
{lang.path}
|
||||
</div>
|
||||
{!lang.exists && (
|
||||
<div className="text-xs text-red-500 ml-3.5">
|
||||
文件不存在
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs text-muted-foreground pt-4 mt-4 border-t border-border/30">
|
||||
Tauri 模式下文件自动连接,无需手动操作。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/** 浏览器模式:使用 FileSystemFileHandle 管理连接 */
|
||||
function BrowserConnectionIndicator({ projectId }: { projectId: string }) {
|
||||
const snap = useFileConnections(projectId);
|
||||
const list = Object.values(snap.connections);
|
||||
const hasAny = list.length > 0;
|
||||
|
|
@ -31,11 +126,11 @@ export function HeaderConnectionIndicator({ projectId }: Props) {
|
|||
{hasAny ? `已连线 ${list.length}` : "未连线"}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="p-4">
|
||||
<TooltipContent className="p-4" align="end">
|
||||
<div className="text-sm font-medium mb-2">连线状态</div>
|
||||
{list.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
暂无连线。通过“导入 JSON”选择文件后将建立连线。
|
||||
暂无连线。通过"导入 JSON"选择文件后将建立连线。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function ProjectSettingsModalImpl({ open, onOpenChange, project, onSave, onDelet
|
|||
</div>
|
||||
{err && <div className="text-sm text-red-600">{err}</div>}
|
||||
<DialogFooter>
|
||||
<Button className="mr-auto" variant="destructive" onClick={handleDelete} disabled={saving}>删除项目</Button>
|
||||
<Button className="mr-auto" type="button" variant="destructive" onClick={handleDelete} disabled={saving}>删除项目</Button>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>取消</Button>
|
||||
<Button type="submit" disabled={saving}>保存</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
||||
import {
|
||||
createEditorProjectStore,
|
||||
createEditorDataStore,
|
||||
createEditorUIStore,
|
||||
createEditorModalStore,
|
||||
useStore,
|
||||
type EditorProjectStore,
|
||||
type EditorDataStore,
|
||||
type EditorUIStore,
|
||||
type EditorModalStore,
|
||||
} from '@/store/editor-store';
|
||||
|
||||
/**
|
||||
* Editor 的所有 Store 实例
|
||||
* 每个 Editor 组件实例都会创建独立的 stores
|
||||
*/
|
||||
interface EditorStores {
|
||||
projectStore: EditorProjectStore;
|
||||
dataStore: EditorDataStore;
|
||||
uiStore: EditorUIStore;
|
||||
modalStore: EditorModalStore;
|
||||
}
|
||||
|
||||
const EditorContext = createContext<EditorStores | null>(null);
|
||||
|
||||
/**
|
||||
* EditorProvider
|
||||
* 为 Editor 组件提供局部化的状态管理
|
||||
*
|
||||
* 优势:
|
||||
* 1. 每个 Editor 实例都有独立的状态(支持多个 Editor 同时存在)
|
||||
* 2. 组件卸载时自动清理,无需手动重置
|
||||
* 3. 测试更容易,每个测试可以创建独立的 Provider
|
||||
* 4. 更符合 React 的组件化思想
|
||||
*/
|
||||
export function EditorProvider({ children }: { children: ReactNode }) {
|
||||
// 使用 useMemo 确保 stores 只创建一次
|
||||
const stores = useMemo<EditorStores>(
|
||||
() => ({
|
||||
projectStore: createEditorProjectStore(),
|
||||
dataStore: createEditorDataStore(),
|
||||
uiStore: createEditorUIStore(),
|
||||
modalStore: createEditorModalStore(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return <EditorContext.Provider value={stores}>{children}</EditorContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 EditorContext,如果未在 EditorProvider 内使用会抛出错误
|
||||
*/
|
||||
function useEditorContext() {
|
||||
const context = useContext(EditorContext);
|
||||
if (!context) {
|
||||
throw new Error('Editor hooks must be used within EditorProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用项目元数据 Store
|
||||
*
|
||||
* @example
|
||||
* // 获取整个 store
|
||||
* const projectStore = useEditorProjectStore();
|
||||
*
|
||||
* // 使用 selector 获取特定字段(推荐,避免不必要的重渲染)
|
||||
* const structure = useEditorProjectStore((state) => state.structure);
|
||||
* const setStructure = useEditorProjectStore((state) => state.setStructure);
|
||||
*/
|
||||
export function useEditorProjectStore(): EditorProjectStore;
|
||||
export function useEditorProjectStore<T>(selector: (state: ReturnType<EditorProjectStore['getState']>) => T): T;
|
||||
export function useEditorProjectStore<T>(selector?: (state: ReturnType<EditorProjectStore['getState']>) => T) {
|
||||
const { projectStore } = useEditorContext();
|
||||
return useStore(projectStore, selector!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用翻译数据 Store
|
||||
*
|
||||
* @example
|
||||
* // 获取某个语言的所有翻译
|
||||
* const enValues = useEditorDataStore((state) => state.valuesByLang['en']);
|
||||
*
|
||||
* // 获取单个单元格的值(超细粒度,性能最优)
|
||||
* const cellValue = useEditorDataStore((state) => state.valuesByLang[lang]?.[path] ?? '');
|
||||
*
|
||||
* // 获取更新函数
|
||||
* const updateSingleValue = useEditorDataStore((state) => state.updateSingleValue);
|
||||
*/
|
||||
export function useEditorDataStore(): EditorDataStore;
|
||||
export function useEditorDataStore<T>(selector: (state: ReturnType<EditorDataStore['getState']>) => T): T;
|
||||
export function useEditorDataStore<T>(selector?: (state: ReturnType<EditorDataStore['getState']>) => T) {
|
||||
const { dataStore } = useEditorContext();
|
||||
return useStore(dataStore, selector!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 UI 状态 Store
|
||||
*
|
||||
* @example
|
||||
* // 只订阅 loading 状态
|
||||
* const loading = useEditorUIStore((state) => state.loading);
|
||||
* const setLoading = useEditorUIStore((state) => state.setLoading);
|
||||
*
|
||||
* // 订阅选中状态
|
||||
* const selected = useEditorUIStore((state) => state.selected);
|
||||
* const toggleSelected = useEditorUIStore((state) => state.toggleSelected);
|
||||
*/
|
||||
export function useEditorUIStore(): EditorUIStore;
|
||||
export function useEditorUIStore<T>(selector: (state: ReturnType<EditorUIStore['getState']>) => T): T;
|
||||
export function useEditorUIStore<T>(selector?: (state: ReturnType<EditorUIStore['getState']>) => T) {
|
||||
const { uiStore } = useEditorContext();
|
||||
return useStore(uiStore, selector!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用模态框状态 Store
|
||||
*
|
||||
* @example
|
||||
* const importOpen = useEditorModalStore((state) => state.importOpen);
|
||||
* const setImportOpen = useEditorModalStore((state) => state.setImportOpen);
|
||||
*/
|
||||
export function useEditorModalStore(): EditorModalStore;
|
||||
export function useEditorModalStore<T>(selector: (state: ReturnType<EditorModalStore['getState']>) => T): T;
|
||||
export function useEditorModalStore<T>(selector?: (state: ReturnType<EditorModalStore['getState']>) => T) {
|
||||
const { modalStore } = useEditorContext();
|
||||
return useStore(modalStore, selector!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 stores 的实例(用于在回调中访问最新状态)
|
||||
*
|
||||
* @example
|
||||
* const stores = useEditorStores();
|
||||
*
|
||||
* const handleSave = useCallback(() => {
|
||||
* // 在回调中直接获取最新状态,不需要通过依赖
|
||||
* const { valuesByLang } = stores.dataStore.getState();
|
||||
* const { structure } = stores.projectStore.getState();
|
||||
* // 执行保存逻辑...
|
||||
* }, []); // 不需要任何依赖!
|
||||
*/
|
||||
export function useEditorStores() {
|
||||
return useEditorContext();
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getProject,
|
||||
getStructure,
|
||||
listLanguages,
|
||||
type Project,
|
||||
type ProjectStructure,
|
||||
getLanguageTranslations,
|
||||
upsertLanguageTranslations,
|
||||
upsertStructure,
|
||||
|
|
@ -36,37 +34,52 @@ import { useLanguageAdapter } from "@/hooks/use-language-adapter";
|
|||
import { isTauriEnv } from "@/lib/is-tauri";
|
||||
import EditorTable from "@/components/biz/editor/table";
|
||||
import { useEditorKeyboardShortcuts } from "@/hooks/biz/use-editor-keyboard-shortcuts";
|
||||
import { EditorProvider, useEditorProjectStore, useEditorDataStore, useEditorUIStore, useEditorModalStore, useEditorStores } from "@/contexts/editor-context";
|
||||
|
||||
type EditorProps = {
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
// 新的 Editor 组件:包裹 EditorProvider
|
||||
export default function Editor({ projectId }: EditorProps) {
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [structure, setStructure] = useState<ProjectStructure | null>(null);
|
||||
const [languages, setLanguages] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pageError, setPageError] = useState<string | null>(null);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [sourcesWizardOpen, setSourcesWizardOpen] = useState(false);
|
||||
const [addModal, setAddModal] = useState<{ open: boolean; path: string; position: "above" | "below" } | null>(null);
|
||||
const [renameModal, setRenameModal] = useState<{ open: boolean; path: string } | null>(null);
|
||||
return (
|
||||
<EditorProvider>
|
||||
<EditorContent projectId={projectId} />
|
||||
</EditorProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const [aiModal, setAiModal] = useState<{ open: boolean; path: string } | null>(null);
|
||||
const [aiBulkOpen, setAiBulkOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
// 原来的 Editor 逻辑移到这里
|
||||
function EditorContent({ projectId }: EditorProps) {
|
||||
// 从 Context Store 获取状态(只订阅需要渲染的状态)
|
||||
const stores = useEditorStores();
|
||||
const project = useEditorProjectStore((state) => state.project);
|
||||
const structure = useEditorProjectStore((state) => state.structure);
|
||||
const languages = useEditorProjectStore((state) => state.languages);
|
||||
|
||||
const [savingAll, setSavingAll] = useState(false);
|
||||
const valuesByLang = useEditorDataStore((state) => state.valuesByLang);
|
||||
const setValuesByLang = useEditorDataStore((state) => state.setValuesByLang);
|
||||
|
||||
const [valuesByLang, setValuesByLang] = useState<Record<string, Record<string, string>>>({});
|
||||
const loading = useEditorUIStore((state) => state.loading);
|
||||
const pageError = useEditorUIStore((state) => state.pageError);
|
||||
const selected = useEditorUIStore((state) => state.selected);
|
||||
const setSelected = useEditorUIStore((state) => state.setSelected);
|
||||
const savingAll = useEditorUIStore((state) => state.savingAll);
|
||||
|
||||
const importOpen = useEditorModalStore((state) => state.importOpen);
|
||||
const exportOpen = useEditorModalStore((state) => state.exportOpen);
|
||||
const sourcesWizardOpen = useEditorModalStore((state) => state.sourcesWizardOpen);
|
||||
const settingsOpen = useEditorModalStore((state) => state.settingsOpen);
|
||||
const aiBulkOpen = useEditorModalStore((state) => state.aiBulkOpen);
|
||||
const addModal = useEditorModalStore((state) => state.addModal);
|
||||
const renameModal = useEditorModalStore((state) => state.renameModal);
|
||||
const aiModal = useEditorModalStore((state) => state.aiModal);
|
||||
|
||||
const inlineEdit = useTranslationInlineEdit({
|
||||
projectId,
|
||||
valuesByLang,
|
||||
setValuesByLang,
|
||||
onError: (message) => setPageError(message),
|
||||
onError: (message) => stores.uiStore.getState().setPageError(message),
|
||||
});
|
||||
|
||||
const connSnap = useFileConnections(projectId ?? "");
|
||||
|
|
@ -99,35 +112,32 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
nextValues[file.language] = flattened;
|
||||
await upsertLanguageTranslations(projectId, file.language, flattened);
|
||||
if (loaded.default_language && file.language === loaded.default_language) {
|
||||
// 使用函数式更新来检查当前 structure 状态,避免依赖它
|
||||
setStructure((currentStructure) => {
|
||||
if (!currentStructure) {
|
||||
try {
|
||||
const root = buildStructureFromObject(parsed);
|
||||
void upsertStructure({ projectId, root });
|
||||
return { projectId, root };
|
||||
} catch (err) {
|
||||
console.error("生成结构失败", err);
|
||||
return currentStructure;
|
||||
}
|
||||
// 获取当前 structure 状态检查
|
||||
const currentStructure = stores.projectStore.getState().structure;
|
||||
if (!currentStructure) {
|
||||
try {
|
||||
const root = buildStructureFromObject(parsed);
|
||||
await upsertStructure({ projectId, root });
|
||||
stores.projectStore.getState().setStructure({ projectId, root });
|
||||
} catch (err) {
|
||||
console.error("生成结构失败", err);
|
||||
}
|
||||
return currentStructure;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const loadedLanguages = Object.keys(nextValues);
|
||||
if (loadedLanguages.length === 0) {
|
||||
return false;
|
||||
}
|
||||
setValuesByLang((old) => ({ ...old, ...nextValues }));
|
||||
setLanguages(loadedLanguages);
|
||||
stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...nextValues }));
|
||||
stores.projectStore.getState().setLanguages(loadedLanguages);
|
||||
if (!opts?.silent) {
|
||||
toast.success("翻译内容已同步", {
|
||||
description: loaded.base_dir ? `来源:${loaded.base_dir}` : undefined,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}, [projectId, setSourcesMeta]);
|
||||
}, [projectId, setSourcesMeta, stores]);
|
||||
|
||||
const syncFromAdapter = useCallback(
|
||||
async (opts?: { silent?: boolean }) => {
|
||||
|
|
@ -182,11 +192,11 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
if (!projectId) return;
|
||||
const [nextProject, nextStructure, nextLanguages] = await Promise.all([getProject(projectId), getStructure(projectId), listLanguages(projectId)]);
|
||||
if (!nextProject) throw new Error("项目不存在");
|
||||
setProject(nextProject);
|
||||
stores.projectStore.getState().setProject(nextProject);
|
||||
upsertProjectMeta(nextProject);
|
||||
setStructure(nextStructure ?? null);
|
||||
setLanguages(nextLanguages);
|
||||
}, [projectId, upsertProjectMeta]);
|
||||
stores.projectStore.getState().setStructure(nextStructure ?? null);
|
||||
stores.projectStore.getState().setLanguages(nextLanguages);
|
||||
}, [projectId, stores, upsertProjectMeta]);
|
||||
|
||||
const bootstrapSources = useCallback(async (): Promise<LoadedProjectSources | null> => {
|
||||
if (!projectId) return null;
|
||||
|
|
@ -228,7 +238,8 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 获取默认语言的翻译内容
|
||||
// 从 store 获取最新的翻译内容
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
const translations = valuesByLang[defaultLang];
|
||||
if (!translations || Object.keys(translations).length === 0) {
|
||||
if (!opts?.silent) {
|
||||
|
|
@ -244,7 +255,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
const root = buildStructureFromObject(nestedObj);
|
||||
// 保存到数据库
|
||||
await upsertStructure({ projectId, root });
|
||||
setStructure({ projectId, root });
|
||||
stores.projectStore.getState().setStructure({ projectId, root });
|
||||
|
||||
if (!opts?.silent) {
|
||||
toast.success("翻译结构已更新");
|
||||
|
|
@ -259,7 +270,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, sourcesState.defaultLanguage, valuesByLang]
|
||||
[projectId, sourcesState.defaultLanguage, stores]
|
||||
);
|
||||
|
||||
const syncExternalSources = useCallback(
|
||||
|
|
@ -288,18 +299,18 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
const refresh = useCallback(
|
||||
async (opts?: { silent?: boolean }) => {
|
||||
if (!projectId) return;
|
||||
setLoading(true);
|
||||
setPageError(null);
|
||||
stores.uiStore.getState().setLoading(true);
|
||||
stores.uiStore.getState().setPageError(null);
|
||||
try {
|
||||
await bootstrapProject();
|
||||
await syncExternalSources({ silent: opts?.silent ?? true, reloadConfig: true });
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "加载失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
stores.uiStore.getState().setLoading(false);
|
||||
}
|
||||
},
|
||||
[bootstrapProject, projectId, syncExternalSources]
|
||||
[bootstrapProject, projectId, stores, syncExternalSources]
|
||||
);
|
||||
|
||||
const handleWizardSourcesLoaded = useCallback(
|
||||
|
|
@ -323,23 +334,23 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
// 只在 projectId 变化时触发,避免因 refresh 引用变化导致的循环
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
setLoading(true);
|
||||
setPageError(null);
|
||||
stores.uiStore.getState().setLoading(true);
|
||||
stores.uiStore.getState().setPageError(null);
|
||||
|
||||
const initialize = async () => {
|
||||
try {
|
||||
await bootstrapProject();
|
||||
await syncExternalSources({ silent: true, reloadConfig: true });
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "加载失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
stores.uiStore.getState().setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void initialize();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]); // 只依赖 projectId
|
||||
}, [projectId, stores]); // 依赖 projectId 和 stores
|
||||
|
||||
const entries: FlatEntry[] = useMemo(() => {
|
||||
if (!structure) return [];
|
||||
|
|
@ -347,7 +358,13 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
}, [structure]);
|
||||
|
||||
const handleSaveAllConnected = useCallback(async () => {
|
||||
if (!projectId || !structure) return;
|
||||
if (!projectId) return;
|
||||
|
||||
// 在执行时获取最新状态,减少依赖
|
||||
const { structure } = stores.projectStore.getState();
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
|
||||
if (!structure) return;
|
||||
if (!adapter) {
|
||||
toast.error("当前项目未配置可写入的语言文件");
|
||||
return;
|
||||
|
|
@ -366,7 +383,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
setSavingAll(true);
|
||||
stores.uiStore.getState().setSavingAll(true);
|
||||
try {
|
||||
const orderedPaths = flattenEntries(structure.root).map((e) => e.path);
|
||||
const payloads = targetLanguages.map((lang) => ({
|
||||
|
|
@ -385,11 +402,14 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
} catch (err) {
|
||||
toast.error((err as Error)?.message ?? "保存失败");
|
||||
} finally {
|
||||
setSavingAll(false);
|
||||
stores.uiStore.getState().setSavingAll(false);
|
||||
}
|
||||
}, [adapter, connSnap.connections, projectId, structure, valuesByLang]);
|
||||
}, [adapter, connSnap.connections, projectId, stores]);
|
||||
|
||||
const computeSuggestedLanguages = useCallback((pathsInput: string[]): string[] => {
|
||||
const { languages } = stores.projectStore.getState();
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
|
||||
function computeSuggestedLanguages(pathsInput: string[]): string[] {
|
||||
if (languages.length === 0 || pathsInput.length === 0) return [];
|
||||
|
||||
const missing = new Set<string>();
|
||||
|
|
@ -412,9 +432,12 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
const arr = Array.from(missing);
|
||||
|
||||
return arr.length > 0 ? arr : languages; // 如无缺失则默认全选
|
||||
}
|
||||
}, [stores]);
|
||||
|
||||
const getExistingByPath = useCallback((path: string) => {
|
||||
const { languages } = stores.projectStore.getState();
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const lang of languages) {
|
||||
const text = valuesByLang[lang]?.[path];
|
||||
|
|
@ -424,60 +447,64 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
}
|
||||
|
||||
return result;
|
||||
}, [languages, valuesByLang]);
|
||||
}, [stores]);
|
||||
|
||||
const handleOpenAddEntry = useCallback((path: string, position: "above" | "below") => {
|
||||
setAddModal({ open: true, path, position });
|
||||
}, []);
|
||||
stores.modalStore.getState().setAddModal({ open: true, path, position });
|
||||
}, [stores]);
|
||||
|
||||
const handleOpenAiTranslate = useCallback((path: string) => {
|
||||
setAiModal({ open: true, path });
|
||||
}, []);
|
||||
stores.modalStore.getState().setAiModal({ open: true, path });
|
||||
}, [stores]);
|
||||
|
||||
const handleOpenRenameEntry = useCallback((path: string) => {
|
||||
setRenameModal({ open: true, path });
|
||||
}, []);
|
||||
stores.modalStore.getState().setRenameModal({ open: true, path });
|
||||
}, [stores]);
|
||||
|
||||
const handleMoveEntry = useCallback(async (path: string, offset: number) => {
|
||||
if (!projectId || !structure || offset === 0) return;
|
||||
if (!projectId || offset === 0) return;
|
||||
|
||||
const { structure } = stores.projectStore.getState();
|
||||
if (!structure) return;
|
||||
|
||||
try {
|
||||
const nextRoot = moveEntryByOffset(structure.root, path, offset);
|
||||
await upsertStructure({ projectId, root: nextRoot });
|
||||
setStructure({ projectId, root: nextRoot });
|
||||
stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "移动失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "移动失败");
|
||||
}
|
||||
}, [projectId, structure]);
|
||||
}, [projectId, stores]);
|
||||
|
||||
const handleDeleteEntry = useCallback(async (path: string) => {
|
||||
if (!projectId || !structure) return;
|
||||
if (!projectId) return;
|
||||
|
||||
const { structure } = stores.projectStore.getState();
|
||||
if (!structure) return;
|
||||
|
||||
try {
|
||||
const nextRoot = removeEntryAtPath(structure.root, path);
|
||||
await upsertStructure({ projectId, root: nextRoot });
|
||||
await deleteEntryFromAllLanguages(projectId, path);
|
||||
setStructure({ projectId, root: nextRoot });
|
||||
setValuesByLang((old) => {
|
||||
const copy: typeof old = {};
|
||||
for (const [langKey, vals] of Object.entries(old)) {
|
||||
const next = { ...vals } as Record<string, string>;
|
||||
delete next[path];
|
||||
copy[langKey] = next;
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
setSelected((prev) => {
|
||||
stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
|
||||
stores.dataStore.getState().deleteEntry(path);
|
||||
stores.uiStore.getState().setSelected((prev) => {
|
||||
if (!prev.has(path)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(path);
|
||||
return next;
|
||||
});
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "删除失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "删除失败");
|
||||
}
|
||||
}, [projectId, setSelected, setValuesByLang, structure]);
|
||||
}, [projectId, stores]);
|
||||
|
||||
const handleDeleteSelectedEntries = useCallback(async (paths: string[]) => {
|
||||
if (!projectId || !structure || paths.length === 0) return;
|
||||
if (!projectId || paths.length === 0) return;
|
||||
|
||||
const { structure } = stores.projectStore.getState();
|
||||
if (!structure) return;
|
||||
|
||||
try {
|
||||
let nextRoot = structure.root;
|
||||
for (const p of paths) {
|
||||
|
|
@ -485,25 +512,21 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
}
|
||||
await upsertStructure({ projectId, root: nextRoot });
|
||||
await Promise.all(paths.map((p) => deleteEntryFromAllLanguages(projectId, p)));
|
||||
setStructure({ projectId, root: nextRoot });
|
||||
setValuesByLang((old) => {
|
||||
const copy: typeof old = {};
|
||||
for (const [langKey, vals] of Object.entries(old)) {
|
||||
const next = { ...vals } as Record<string, string>;
|
||||
for (const p of paths) delete next[p];
|
||||
copy[langKey] = next;
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
setSelected(new Set());
|
||||
stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
|
||||
|
||||
// 使用 store 的批量删除
|
||||
for (const p of paths) {
|
||||
stores.dataStore.getState().deleteEntry(p);
|
||||
}
|
||||
stores.uiStore.getState().setSelected(new Set());
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "批量删除失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "批量删除失败");
|
||||
}
|
||||
}, [projectId, setSelected, setValuesByLang, structure]);
|
||||
}, [projectId, stores]);
|
||||
|
||||
const handleOpenBulkAiTranslate = useCallback(() => {
|
||||
setAiBulkOpen(true);
|
||||
}, []);
|
||||
stores.modalStore.getState().setAiBulkOpen(true);
|
||||
}, [stores]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId || languages.length === 0) return;
|
||||
|
|
@ -514,12 +537,12 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
const rec = await getLanguageTranslations(projectId, lang);
|
||||
all[lang] = rec?.values ?? {};
|
||||
}
|
||||
if (!cancelled) setValuesByLang(all);
|
||||
if (!cancelled) stores.dataStore.getState().setValuesByLang(all);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId, languages]);
|
||||
}, [projectId, languages, stores]);
|
||||
|
||||
const disabledItems = useMemo(() => {
|
||||
return {
|
||||
|
|
@ -530,7 +553,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
"import-with-files": false,
|
||||
export: languages.length === 0,
|
||||
};
|
||||
}, [adapter, structure, languages.length]);
|
||||
}, [adapter, structure, languages.length, savingAll, connSnap.connections]);
|
||||
|
||||
const onClickItem = useCallback((item: CommonMenubarItem) => {
|
||||
switch (item) {
|
||||
|
|
@ -544,16 +567,16 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
void handleSaveAllConnected();
|
||||
break;
|
||||
case "import-with-directory":
|
||||
setSourcesWizardOpen(true);
|
||||
stores.modalStore.getState().setSourcesWizardOpen(true);
|
||||
break;
|
||||
case "import-with-files":
|
||||
setImportOpen(true);
|
||||
stores.modalStore.getState().setImportOpen(true);
|
||||
break;
|
||||
case "export":
|
||||
setExportOpen(true);
|
||||
stores.modalStore.getState().setExportOpen(true);
|
||||
break;
|
||||
}
|
||||
}, [handleSaveAllConnected, setSourcesWizardOpen, setImportOpen, setExportOpen, syncExternalSources]);
|
||||
}, [handleSaveAllConnected, syncExternalSources, stores]);
|
||||
|
||||
// 注册键盘快捷方式
|
||||
useEditorKeyboardShortcuts({
|
||||
|
|
@ -571,7 +594,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
<div className="flex items-center justify-end gap-2 text-foreground">
|
||||
<HeaderConnectionIndicator projectId={projectId ?? ""} />
|
||||
{project && (
|
||||
<Button variant="ghost" onClick={() => setSettingsOpen(true)}>
|
||||
<Button variant="ghost" onClick={() => stores.modalStore.getState().setSettingsOpen(true)}>
|
||||
<Settings />
|
||||
<span className="sr-only">设置</span>
|
||||
</Button>
|
||||
|
|
@ -592,7 +615,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
该项目还没有翻译结构。请导入一份 JSON 文件来构建结构,并同时录入该语言的翻译内容。
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button onClick={() => { setSourcesWizardOpen(true); }}>
|
||||
<Button onClick={() => { stores.modalStore.getState().setSourcesWizardOpen(true); }}>
|
||||
<FolderOpen />
|
||||
通过目录导入
|
||||
</Button>
|
||||
|
|
@ -619,7 +642,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<ProjectSourcesWizard
|
||||
open={sourcesWizardOpen}
|
||||
onOpenChange={setSourcesWizardOpen}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setSourcesWizardOpen(v)}
|
||||
projectId={projectId ?? ""}
|
||||
onCompleted={refresh}
|
||||
onSourcesLoaded={handleWizardSourcesLoaded}
|
||||
|
|
@ -627,7 +650,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<ImportLanguageModal
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setImportOpen(v)}
|
||||
projectId={projectId ?? ""}
|
||||
hasStructure={!!structure}
|
||||
onImported={refresh}
|
||||
|
|
@ -635,7 +658,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
/>
|
||||
<ExportLanguageModal
|
||||
open={exportOpen}
|
||||
onOpenChange={setExportOpen}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setExportOpen(v)}
|
||||
projectId={projectId ?? ""}
|
||||
languages={languages}
|
||||
valuesByLang={valuesByLang}
|
||||
|
|
@ -644,12 +667,12 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<ProjectSettingsModal
|
||||
open={settingsOpen}
|
||||
onOpenChange={setSettingsOpen}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setSettingsOpen(v)}
|
||||
project={project}
|
||||
onSave={async (update) => {
|
||||
if (!projectId) return;
|
||||
const next = await updateProject({ id: projectId, name: update.name, preferences: update.preferences });
|
||||
setProject(next);
|
||||
stores.projectStore.getState().setProject(next);
|
||||
upsertProjectMeta(next);
|
||||
}}
|
||||
onDelete={async () => {
|
||||
|
|
@ -662,7 +685,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<AiTranslateModal
|
||||
open={!!aiModal?.open}
|
||||
onOpenChange={(v) => setAiModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setAiModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
languages={languages}
|
||||
paths={aiModal?.path ? [aiModal.path] : []}
|
||||
initialSelectedLanguages={aiModal?.path ? computeSuggestedLanguages([aiModal.path]) : []}
|
||||
|
|
@ -670,7 +693,14 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
prompt={project?.preferences?.aiPrompt}
|
||||
model={project?.preferences?.aiModel}
|
||||
onConfirm={async (translations, options) => {
|
||||
if (!projectId || !aiModal) return;
|
||||
if (!projectId) return;
|
||||
|
||||
const { aiModal } = stores.modalStore.getState();
|
||||
if (!aiModal) return;
|
||||
|
||||
const { languages } = stores.projectStore.getState();
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
|
||||
const updates: Record<string, Record<string, string>> = {};
|
||||
try {
|
||||
const targetLangs = options?.selectedLanguages ?? languages;
|
||||
|
|
@ -689,10 +719,10 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
await upsertLanguageTranslations(projectId, lang, next);
|
||||
})
|
||||
);
|
||||
setValuesByLang((old) => ({ ...old, ...updates }));
|
||||
stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...updates }));
|
||||
} catch (e) {
|
||||
const msg = (e as Error)?.message ?? "保存翻译失败";
|
||||
setPageError(msg);
|
||||
stores.uiStore.getState().setPageError(msg);
|
||||
throw e;
|
||||
}
|
||||
}}
|
||||
|
|
@ -700,7 +730,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<AiTranslateModal
|
||||
open={aiBulkOpen}
|
||||
onOpenChange={setAiBulkOpen}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setAiBulkOpen(v)}
|
||||
languages={languages}
|
||||
paths={Array.from(selected)}
|
||||
initialSelectedLanguages={computeSuggestedLanguages(Array.from(selected))}
|
||||
|
|
@ -708,7 +738,14 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
prompt={project?.preferences?.aiPrompt}
|
||||
model={project?.preferences?.aiModel}
|
||||
onConfirm={async (translations, options) => {
|
||||
if (!projectId || selected.size === 0) return;
|
||||
if (!projectId) return;
|
||||
|
||||
const { selected } = stores.uiStore.getState();
|
||||
if (selected.size === 0) return;
|
||||
|
||||
const { languages } = stores.projectStore.getState();
|
||||
const { valuesByLang } = stores.dataStore.getState();
|
||||
|
||||
try {
|
||||
const updatesByLang: Record<string, Record<string, string>> = {};
|
||||
const targetLangs = options?.selectedLanguages ?? languages;
|
||||
|
|
@ -727,9 +764,9 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
await upsertLanguageTranslations(projectId, lang, next);
|
||||
})
|
||||
);
|
||||
setValuesByLang((old) => ({ ...old, ...updatesByLang }));
|
||||
stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...updatesByLang }));
|
||||
} catch (e) {
|
||||
setPageError((e as Error)?.message ?? "批量 AI 翻译保存失败");
|
||||
stores.uiStore.getState().setPageError((e as Error)?.message ?? "批量 AI 翻译保存失败");
|
||||
throw e;
|
||||
}
|
||||
}}
|
||||
|
|
@ -737,16 +774,22 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<EntryNameModal
|
||||
open={!!addModal?.open}
|
||||
onOpenChange={(v) => setAddModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setAddModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
title={addModal?.position === "above" ? "在上方新增条目" : "在下方新增条目"}
|
||||
placeholder="请输入条目名称(不含点)"
|
||||
onConfirm={async (name) => {
|
||||
if (!projectId || !structure || !addModal) return;
|
||||
if (!projectId) return;
|
||||
|
||||
const { structure } = stores.projectStore.getState();
|
||||
const { addModal } = stores.modalStore.getState();
|
||||
|
||||
if (!structure || !addModal) return;
|
||||
|
||||
const nextRoot = insertEntrySibling(structure.root, addModal.path, addModal.position, name);
|
||||
console.log("nextRoot", addModal, nextRoot);
|
||||
|
||||
await upsertStructure({ projectId, root: nextRoot });
|
||||
setStructure({ projectId, root: nextRoot });
|
||||
stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
|
||||
}}
|
||||
validate={(name) => {
|
||||
// 同级重名校验在结构函数中也会做,这里做基础提示即可
|
||||
|
|
@ -757,28 +800,23 @@ export default function Editor({ projectId }: EditorProps) {
|
|||
|
||||
<EntryNameModal
|
||||
open={!!renameModal?.open}
|
||||
onOpenChange={(v) => setRenameModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
onOpenChange={(v) => stores.modalStore.getState().setRenameModal((cur) => (cur ? { ...cur, open: v } : cur))}
|
||||
title="重命名条目"
|
||||
placeholder="请输入新名称(不含点)"
|
||||
defaultValue={renameModal?.path ? renameModal.path.split('.').pop() : ''}
|
||||
onConfirm={async (newName) => {
|
||||
if (!projectId || !structure || !renameModal) return;
|
||||
if (!projectId) return;
|
||||
|
||||
const { structure } = stores.projectStore.getState();
|
||||
const { renameModal } = stores.modalStore.getState();
|
||||
|
||||
if (!structure || !renameModal) return;
|
||||
|
||||
const { root: nextRoot, newPath } = renameEntryAtPath(structure.root, renameModal.path, newName);
|
||||
await upsertStructure({ projectId, root: nextRoot });
|
||||
await renameEntryInAllLanguages(projectId, renameModal.path, newPath);
|
||||
setStructure({ projectId, root: nextRoot });
|
||||
setValuesByLang((old) => {
|
||||
const copy: typeof old = {};
|
||||
for (const [langKey, vals] of Object.entries(old)) {
|
||||
if (Object.prototype.hasOwnProperty.call(vals, renameModal.path)) {
|
||||
const { [renameModal.path]: val, ...rest } = vals;
|
||||
copy[langKey] = { ...rest, [newPath]: val };
|
||||
} else {
|
||||
copy[langKey] = vals;
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
|
||||
stores.dataStore.getState().renameEntry(renameModal.path, newPath);
|
||||
}}
|
||||
validate={(name) => {
|
||||
if (name.includes('.')) return "名称不能包含 '.'";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
import { createStore, useStore } from 'zustand';
|
||||
import type { Project, ProjectStructure } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* 编辑器项目元数据 Store
|
||||
* 变化频率:低(只在切换项目或首次加载时变化)
|
||||
*/
|
||||
interface EditorProjectState {
|
||||
project: Project | null;
|
||||
structure: ProjectStructure | null;
|
||||
languages: string[];
|
||||
setProject: (project: Project | null) => void;
|
||||
setStructure: (structure: ProjectStructure | null) => void;
|
||||
setLanguages: (languages: string[]) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export type EditorProjectStore = ReturnType<typeof createEditorProjectStore>;
|
||||
|
||||
export const createEditorProjectStore = () => {
|
||||
return createStore<EditorProjectState>((set) => ({
|
||||
project: null,
|
||||
structure: null,
|
||||
languages: [],
|
||||
setProject: (project) => set({ project }),
|
||||
setStructure: (structure) => set({ structure }),
|
||||
setLanguages: (languages) => set({ languages }),
|
||||
reset: () => set({ project: null, structure: null, languages: [] }),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑器翻译数据 Store
|
||||
* 变化频率:中(编辑翻译内容时变化)
|
||||
*/
|
||||
interface EditorDataState {
|
||||
valuesByLang: Record<string, Record<string, string>>;
|
||||
setValuesByLang: (valuesByLang: Record<string, Record<string, string>> | ((old: Record<string, Record<string, string>>) => Record<string, Record<string, string>>)) => void;
|
||||
updateLanguageValues: (lang: string, values: Record<string, string>) => void;
|
||||
updateSingleValue: (lang: string, path: string, value: string) => void;
|
||||
deleteEntry: (path: string) => void;
|
||||
renameEntry: (oldPath: string, newPath: string) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export type EditorDataStore = ReturnType<typeof createEditorDataStore>;
|
||||
|
||||
export const createEditorDataStore = () => {
|
||||
return createStore<EditorDataState>((set) => ({
|
||||
valuesByLang: {},
|
||||
setValuesByLang: (valuesByLang) =>
|
||||
set((state) => ({
|
||||
valuesByLang: typeof valuesByLang === 'function' ? valuesByLang(state.valuesByLang) : valuesByLang
|
||||
})),
|
||||
updateLanguageValues: (lang, values) =>
|
||||
set((state) => ({
|
||||
valuesByLang: {
|
||||
...state.valuesByLang,
|
||||
[lang]: { ...state.valuesByLang[lang], ...values },
|
||||
},
|
||||
})),
|
||||
updateSingleValue: (lang, path, value) =>
|
||||
set((state) => ({
|
||||
valuesByLang: {
|
||||
...state.valuesByLang,
|
||||
[lang]: { ...state.valuesByLang[lang], [path]: value },
|
||||
},
|
||||
})),
|
||||
deleteEntry: (path) =>
|
||||
set((state) => {
|
||||
const nextValuesByLang: Record<string, Record<string, string>> = {};
|
||||
for (const [lang, values] of Object.entries(state.valuesByLang)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [path]: _, ...rest } = values;
|
||||
nextValuesByLang[lang] = rest;
|
||||
}
|
||||
return { valuesByLang: nextValuesByLang };
|
||||
}),
|
||||
renameEntry: (oldPath, newPath) =>
|
||||
set((state) => {
|
||||
const nextValuesByLang: Record<string, Record<string, string>> = {};
|
||||
for (const [lang, values] of Object.entries(state.valuesByLang)) {
|
||||
if (Object.prototype.hasOwnProperty.call(values, oldPath)) {
|
||||
const { [oldPath]: value, ...rest } = values;
|
||||
nextValuesByLang[lang] = { ...rest, [newPath]: value };
|
||||
} else {
|
||||
nextValuesByLang[lang] = values;
|
||||
}
|
||||
}
|
||||
return { valuesByLang: nextValuesByLang };
|
||||
}),
|
||||
reset: () => set({ valuesByLang: {} }),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑器 UI 状态 Store
|
||||
* 变化频率:高(加载状态、错误信息、选中状态频繁变化)
|
||||
*/
|
||||
interface EditorUIState {
|
||||
loading: boolean;
|
||||
pageError: string | null;
|
||||
selected: Set<string>;
|
||||
savingAll: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setPageError: (error: string | null) => void;
|
||||
setSelected: (selected: Set<string> | ((prev: Set<string>) => Set<string>)) => void;
|
||||
toggleSelected: (path: string) => void;
|
||||
clearSelected: () => void;
|
||||
setSavingAll: (saving: boolean) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export type EditorUIStore = ReturnType<typeof createEditorUIStore>;
|
||||
|
||||
export const createEditorUIStore = () => {
|
||||
return createStore<EditorUIState>((set) => ({
|
||||
loading: true,
|
||||
pageError: null,
|
||||
selected: new Set(),
|
||||
savingAll: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setPageError: (pageError) => set({ pageError }),
|
||||
setSelected: (selected) =>
|
||||
set((state) => ({
|
||||
selected: typeof selected === 'function' ? selected(state.selected) : selected
|
||||
})),
|
||||
toggleSelected: (path) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.selected);
|
||||
if (next.has(path)) {
|
||||
next.delete(path);
|
||||
} else {
|
||||
next.add(path);
|
||||
}
|
||||
return { selected: next };
|
||||
}),
|
||||
clearSelected: () => set({ selected: new Set() }),
|
||||
setSavingAll: (savingAll) => set({ savingAll }),
|
||||
reset: () => set({ loading: true, pageError: null, selected: new Set(), savingAll: false }),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑器模态框状态 Store
|
||||
* 变化频率:中(打开/关闭弹窗时变化)
|
||||
*/
|
||||
interface EditorModalState {
|
||||
importOpen: boolean;
|
||||
exportOpen: boolean;
|
||||
sourcesWizardOpen: boolean;
|
||||
settingsOpen: boolean;
|
||||
aiBulkOpen: boolean;
|
||||
addModal: { open: boolean; path: string; position: 'above' | 'below' } | null;
|
||||
renameModal: { open: boolean; path: string } | null;
|
||||
aiModal: { open: boolean; path: string } | null;
|
||||
setImportOpen: (open: boolean) => void;
|
||||
setExportOpen: (open: boolean) => void;
|
||||
setSourcesWizardOpen: (open: boolean) => void;
|
||||
setSettingsOpen: (open: boolean) => void;
|
||||
setAiBulkOpen: (open: boolean) => void;
|
||||
setAddModal: (modal: { open: boolean; path: string; position: 'above' | 'below' } | null | ((cur: { open: boolean; path: string; position: 'above' | 'below' } | null) => { open: boolean; path: string; position: 'above' | 'below' } | null)) => void;
|
||||
setRenameModal: (modal: { open: boolean; path: string } | null | ((cur: { open: boolean; path: string } | null) => { open: boolean; path: string } | null)) => void;
|
||||
setAiModal: (modal: { open: boolean; path: string } | null | ((cur: { open: boolean; path: string } | null) => { open: boolean; path: string } | null)) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export type EditorModalStore = ReturnType<typeof createEditorModalStore>;
|
||||
|
||||
export const createEditorModalStore = () => {
|
||||
return createStore<EditorModalState>((set) => ({
|
||||
importOpen: false,
|
||||
exportOpen: false,
|
||||
sourcesWizardOpen: false,
|
||||
settingsOpen: false,
|
||||
aiBulkOpen: false,
|
||||
addModal: null,
|
||||
renameModal: null,
|
||||
aiModal: null,
|
||||
setImportOpen: (importOpen) => set({ importOpen }),
|
||||
setExportOpen: (exportOpen) => set({ exportOpen }),
|
||||
setSourcesWizardOpen: (sourcesWizardOpen) => set({ sourcesWizardOpen }),
|
||||
setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
|
||||
setAiBulkOpen: (aiBulkOpen) => set({ aiBulkOpen }),
|
||||
setAddModal: (addModal) =>
|
||||
set((state) => ({
|
||||
addModal: typeof addModal === 'function' ? addModal(state.addModal) : addModal
|
||||
})),
|
||||
setRenameModal: (renameModal) =>
|
||||
set((state) => ({
|
||||
renameModal: typeof renameModal === 'function' ? renameModal(state.renameModal) : renameModal
|
||||
})),
|
||||
setAiModal: (aiModal) =>
|
||||
set((state) => ({
|
||||
aiModal: typeof aiModal === 'function' ? aiModal(state.aiModal) : aiModal
|
||||
})),
|
||||
reset: () =>
|
||||
set({
|
||||
importOpen: false,
|
||||
exportOpen: false,
|
||||
sourcesWizardOpen: false,
|
||||
settingsOpen: false,
|
||||
aiBulkOpen: false,
|
||||
addModal: null,
|
||||
renameModal: null,
|
||||
aiModal: null,
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出 useStore 以便在自定义 hooks 中使用
|
||||
*/
|
||||
export { useStore };
|
||||
Loading…
Reference in New Issue