Feat: 将编辑器状态存储在 Store 里面,调整头部连接组件逻辑

This commit is contained in:
奇趣保罗 2026-02-05 14:34:48 +08:00
parent bf1a7dde48
commit b214fa944b
5 changed files with 641 additions and 145 deletions

View File

@ -2,6 +2,8 @@ import {
disconnectLanguage, disconnectLanguage,
useFileConnections, useFileConnections,
} from "@/store/file-connection"; } from "@/store/file-connection";
import { useProjectSourcesStore } from "@/store/sources-store";
import { isTauriEnv } from "@/lib/is-tauri";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -11,6 +13,99 @@ type Props = {
}; };
export function HeaderConnectionIndicator({ projectId }: 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 snap = useFileConnections(projectId);
const list = Object.values(snap.connections); const list = Object.values(snap.connections);
const hasAny = list.length > 0; const hasAny = list.length > 0;
@ -31,11 +126,11 @@ export function HeaderConnectionIndicator({ projectId }: Props) {
{hasAny ? `已连线 ${list.length}` : "未连线"} {hasAny ? `已连线 ${list.length}` : "未连线"}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="p-4"> <TooltipContent className="p-4" align="end">
<div className="text-sm font-medium mb-2">线</div> <div className="text-sm font-medium mb-2">线</div>
{list.length === 0 ? ( {list.length === 0 ? (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
线 JSON线 线"导入 JSON"线
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">

View File

@ -98,7 +98,7 @@ function ProjectSettingsModalImpl({ open, onOpenChange, project, onSave, onDelet
</div> </div>
{err && <div className="text-sm text-red-600">{err}</div>} {err && <div className="text-sm text-red-600">{err}</div>}
<DialogFooter> <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="button" variant="outline" onClick={() => onOpenChange(false)} disabled={saving}></Button>
<Button type="submit" disabled={saving}></Button> <Button type="submit" disabled={saving}></Button>
</DialogFooter> </DialogFooter>

View File

@ -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();
}

View File

@ -1,11 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
getProject, getProject,
getStructure, getStructure,
listLanguages, listLanguages,
type Project,
type ProjectStructure,
getLanguageTranslations, getLanguageTranslations,
upsertLanguageTranslations, upsertLanguageTranslations,
upsertStructure, upsertStructure,
@ -36,37 +34,52 @@ import { useLanguageAdapter } from "@/hooks/use-language-adapter";
import { isTauriEnv } from "@/lib/is-tauri"; import { isTauriEnv } from "@/lib/is-tauri";
import EditorTable from "@/components/biz/editor/table"; import EditorTable from "@/components/biz/editor/table";
import { useEditorKeyboardShortcuts } from "@/hooks/biz/use-editor-keyboard-shortcuts"; import { useEditorKeyboardShortcuts } from "@/hooks/biz/use-editor-keyboard-shortcuts";
import { EditorProvider, useEditorProjectStore, useEditorDataStore, useEditorUIStore, useEditorModalStore, useEditorStores } from "@/contexts/editor-context";
type EditorProps = { type EditorProps = {
projectId?: string; projectId?: string;
}; };
// 新的 Editor 组件:包裹 EditorProvider
export default function Editor({ projectId }: EditorProps) { export default function Editor({ projectId }: EditorProps) {
const [project, setProject] = useState<Project | null>(null); return (
const [structure, setStructure] = useState<ProjectStructure | null>(null); <EditorProvider>
const [languages, setLanguages] = useState<string[]>([]); <EditorContent projectId={projectId} />
const [loading, setLoading] = useState(true); </EditorProvider>
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);
const [aiModal, setAiModal] = useState<{ open: boolean; path: string } | null>(null); // 原来的 Editor 逻辑移到这里
const [aiBulkOpen, setAiBulkOpen] = useState(false); function EditorContent({ projectId }: EditorProps) {
const [settingsOpen, setSettingsOpen] = useState(false); // 从 Context Store 获取状态(只订阅需要渲染的状态)
const [selected, setSelected] = useState<Set<string>>(new Set()); 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({ const inlineEdit = useTranslationInlineEdit({
projectId, projectId,
valuesByLang, valuesByLang,
setValuesByLang, setValuesByLang,
onError: (message) => setPageError(message), onError: (message) => stores.uiStore.getState().setPageError(message),
}); });
const connSnap = useFileConnections(projectId ?? ""); const connSnap = useFileConnections(projectId ?? "");
@ -99,35 +112,32 @@ export default function Editor({ projectId }: EditorProps) {
nextValues[file.language] = flattened; nextValues[file.language] = flattened;
await upsertLanguageTranslations(projectId, file.language, flattened); await upsertLanguageTranslations(projectId, file.language, flattened);
if (loaded.default_language && file.language === loaded.default_language) { if (loaded.default_language && file.language === loaded.default_language) {
// 使用函数式更新来检查当前 structure 状态,避免依赖它 // 获取当前 structure 状态检查
setStructure((currentStructure) => { const currentStructure = stores.projectStore.getState().structure;
if (!currentStructure) { if (!currentStructure) {
try { try {
const root = buildStructureFromObject(parsed); const root = buildStructureFromObject(parsed);
void upsertStructure({ projectId, root }); await upsertStructure({ projectId, root });
return { projectId, root }; stores.projectStore.getState().setStructure({ projectId, root });
} catch (err) { } catch (err) {
console.error("生成结构失败", err); console.error("生成结构失败", err);
return currentStructure;
}
} }
return currentStructure; }
});
} }
} }
const loadedLanguages = Object.keys(nextValues); const loadedLanguages = Object.keys(nextValues);
if (loadedLanguages.length === 0) { if (loadedLanguages.length === 0) {
return false; return false;
} }
setValuesByLang((old) => ({ ...old, ...nextValues })); stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...nextValues }));
setLanguages(loadedLanguages); stores.projectStore.getState().setLanguages(loadedLanguages);
if (!opts?.silent) { if (!opts?.silent) {
toast.success("翻译内容已同步", { toast.success("翻译内容已同步", {
description: loaded.base_dir ? `来源:${loaded.base_dir}` : undefined, description: loaded.base_dir ? `来源:${loaded.base_dir}` : undefined,
}); });
} }
return true; return true;
}, [projectId, setSourcesMeta]); }, [projectId, setSourcesMeta, stores]);
const syncFromAdapter = useCallback( const syncFromAdapter = useCallback(
async (opts?: { silent?: boolean }) => { async (opts?: { silent?: boolean }) => {
@ -182,11 +192,11 @@ export default function Editor({ projectId }: EditorProps) {
if (!projectId) return; if (!projectId) return;
const [nextProject, nextStructure, nextLanguages] = await Promise.all([getProject(projectId), getStructure(projectId), listLanguages(projectId)]); const [nextProject, nextStructure, nextLanguages] = await Promise.all([getProject(projectId), getStructure(projectId), listLanguages(projectId)]);
if (!nextProject) throw new Error("项目不存在"); if (!nextProject) throw new Error("项目不存在");
setProject(nextProject); stores.projectStore.getState().setProject(nextProject);
upsertProjectMeta(nextProject); upsertProjectMeta(nextProject);
setStructure(nextStructure ?? null); stores.projectStore.getState().setStructure(nextStructure ?? null);
setLanguages(nextLanguages); stores.projectStore.getState().setLanguages(nextLanguages);
}, [projectId, upsertProjectMeta]); }, [projectId, stores, upsertProjectMeta]);
const bootstrapSources = useCallback(async (): Promise<LoadedProjectSources | null> => { const bootstrapSources = useCallback(async (): Promise<LoadedProjectSources | null> => {
if (!projectId) return null; if (!projectId) return null;
@ -228,7 +238,8 @@ export default function Editor({ projectId }: EditorProps) {
return false; return false;
} }
// 获取默认语言的翻译内容 // 从 store 获取最新的翻译内容
const { valuesByLang } = stores.dataStore.getState();
const translations = valuesByLang[defaultLang]; const translations = valuesByLang[defaultLang];
if (!translations || Object.keys(translations).length === 0) { if (!translations || Object.keys(translations).length === 0) {
if (!opts?.silent) { if (!opts?.silent) {
@ -244,7 +255,7 @@ export default function Editor({ projectId }: EditorProps) {
const root = buildStructureFromObject(nestedObj); const root = buildStructureFromObject(nestedObj);
// 保存到数据库 // 保存到数据库
await upsertStructure({ projectId, root }); await upsertStructure({ projectId, root });
setStructure({ projectId, root }); stores.projectStore.getState().setStructure({ projectId, root });
if (!opts?.silent) { if (!opts?.silent) {
toast.success("翻译结构已更新"); toast.success("翻译结构已更新");
@ -259,7 +270,7 @@ export default function Editor({ projectId }: EditorProps) {
return false; return false;
} }
}, },
[projectId, sourcesState.defaultLanguage, valuesByLang] [projectId, sourcesState.defaultLanguage, stores]
); );
const syncExternalSources = useCallback( const syncExternalSources = useCallback(
@ -288,18 +299,18 @@ export default function Editor({ projectId }: EditorProps) {
const refresh = useCallback( const refresh = useCallback(
async (opts?: { silent?: boolean }) => { async (opts?: { silent?: boolean }) => {
if (!projectId) return; if (!projectId) return;
setLoading(true); stores.uiStore.getState().setLoading(true);
setPageError(null); stores.uiStore.getState().setPageError(null);
try { try {
await bootstrapProject(); await bootstrapProject();
await syncExternalSources({ silent: opts?.silent ?? true, reloadConfig: true }); await syncExternalSources({ silent: opts?.silent ?? true, reloadConfig: true });
} catch (e) { } catch (e) {
setPageError((e as Error)?.message ?? "加载失败"); stores.uiStore.getState().setPageError((e as Error)?.message ?? "加载失败");
} finally { } finally {
setLoading(false); stores.uiStore.getState().setLoading(false);
} }
}, },
[bootstrapProject, projectId, syncExternalSources] [bootstrapProject, projectId, stores, syncExternalSources]
); );
const handleWizardSourcesLoaded = useCallback( const handleWizardSourcesLoaded = useCallback(
@ -323,23 +334,23 @@ export default function Editor({ projectId }: EditorProps) {
// 只在 projectId 变化时触发,避免因 refresh 引用变化导致的循环 // 只在 projectId 变化时触发,避免因 refresh 引用变化导致的循环
useEffect(() => { useEffect(() => {
if (!projectId) return; if (!projectId) return;
setLoading(true); stores.uiStore.getState().setLoading(true);
setPageError(null); stores.uiStore.getState().setPageError(null);
const initialize = async () => { const initialize = async () => {
try { try {
await bootstrapProject(); await bootstrapProject();
await syncExternalSources({ silent: true, reloadConfig: true }); await syncExternalSources({ silent: true, reloadConfig: true });
} catch (e) { } catch (e) {
setPageError((e as Error)?.message ?? "加载失败"); stores.uiStore.getState().setPageError((e as Error)?.message ?? "加载失败");
} finally { } finally {
setLoading(false); stores.uiStore.getState().setLoading(false);
} }
}; };
void initialize(); void initialize();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); // 依赖 projectId }, [projectId, stores]); // 依赖 projectId 和 stores
const entries: FlatEntry[] = useMemo(() => { const entries: FlatEntry[] = useMemo(() => {
if (!structure) return []; if (!structure) return [];
@ -347,7 +358,13 @@ export default function Editor({ projectId }: EditorProps) {
}, [structure]); }, [structure]);
const handleSaveAllConnected = useCallback(async () => { 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) { if (!adapter) {
toast.error("当前项目未配置可写入的语言文件"); toast.error("当前项目未配置可写入的语言文件");
return; return;
@ -366,7 +383,7 @@ export default function Editor({ projectId }: EditorProps) {
return; return;
} }
setSavingAll(true); stores.uiStore.getState().setSavingAll(true);
try { try {
const orderedPaths = flattenEntries(structure.root).map((e) => e.path); const orderedPaths = flattenEntries(structure.root).map((e) => e.path);
const payloads = targetLanguages.map((lang) => ({ const payloads = targetLanguages.map((lang) => ({
@ -385,11 +402,14 @@ export default function Editor({ projectId }: EditorProps) {
} catch (err) { } catch (err) {
toast.error((err as Error)?.message ?? "保存失败"); toast.error((err as Error)?.message ?? "保存失败");
} finally { } 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 []; if (languages.length === 0 || pathsInput.length === 0) return [];
const missing = new Set<string>(); const missing = new Set<string>();
@ -412,9 +432,12 @@ export default function Editor({ projectId }: EditorProps) {
const arr = Array.from(missing); const arr = Array.from(missing);
return arr.length > 0 ? arr : languages; // 如无缺失则默认全选 return arr.length > 0 ? arr : languages; // 如无缺失则默认全选
} }, [stores]);
const getExistingByPath = useCallback((path: string) => { const getExistingByPath = useCallback((path: string) => {
const { languages } = stores.projectStore.getState();
const { valuesByLang } = stores.dataStore.getState();
const result: Record<string, string> = {}; const result: Record<string, string> = {};
for (const lang of languages) { for (const lang of languages) {
const text = valuesByLang[lang]?.[path]; const text = valuesByLang[lang]?.[path];
@ -424,60 +447,64 @@ export default function Editor({ projectId }: EditorProps) {
} }
return result; return result;
}, [languages, valuesByLang]); }, [stores]);
const handleOpenAddEntry = useCallback((path: string, position: "above" | "below") => { 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) => { const handleOpenAiTranslate = useCallback((path: string) => {
setAiModal({ open: true, path }); stores.modalStore.getState().setAiModal({ open: true, path });
}, []); }, [stores]);
const handleOpenRenameEntry = useCallback((path: string) => { 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) => { 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 { try {
const nextRoot = moveEntryByOffset(structure.root, path, offset); const nextRoot = moveEntryByOffset(structure.root, path, offset);
await upsertStructure({ projectId, root: nextRoot }); await upsertStructure({ projectId, root: nextRoot });
setStructure({ projectId, root: nextRoot }); stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
} catch (e) { } 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) => { const handleDeleteEntry = useCallback(async (path: string) => {
if (!projectId || !structure) return; if (!projectId) return;
const { structure } = stores.projectStore.getState();
if (!structure) return;
try { try {
const nextRoot = removeEntryAtPath(structure.root, path); const nextRoot = removeEntryAtPath(structure.root, path);
await upsertStructure({ projectId, root: nextRoot }); await upsertStructure({ projectId, root: nextRoot });
await deleteEntryFromAllLanguages(projectId, path); await deleteEntryFromAllLanguages(projectId, path);
setStructure({ projectId, root: nextRoot }); stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
setValuesByLang((old) => { stores.dataStore.getState().deleteEntry(path);
const copy: typeof old = {}; stores.uiStore.getState().setSelected((prev) => {
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) => {
if (!prev.has(path)) return prev; if (!prev.has(path)) return prev;
const next = new Set(prev); const next = new Set(prev);
next.delete(path); next.delete(path);
return next; return next;
}); });
} catch (e) { } 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[]) => { 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 { try {
let nextRoot = structure.root; let nextRoot = structure.root;
for (const p of paths) { for (const p of paths) {
@ -485,25 +512,21 @@ export default function Editor({ projectId }: EditorProps) {
} }
await upsertStructure({ projectId, root: nextRoot }); await upsertStructure({ projectId, root: nextRoot });
await Promise.all(paths.map((p) => deleteEntryFromAllLanguages(projectId, p))); await Promise.all(paths.map((p) => deleteEntryFromAllLanguages(projectId, p)));
setStructure({ projectId, root: nextRoot }); stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
setValuesByLang((old) => {
const copy: typeof old = {}; // 使用 store 的批量删除
for (const [langKey, vals] of Object.entries(old)) { for (const p of paths) {
const next = { ...vals } as Record<string, string>; stores.dataStore.getState().deleteEntry(p);
for (const p of paths) delete next[p]; }
copy[langKey] = next; stores.uiStore.getState().setSelected(new Set());
}
return copy;
});
setSelected(new Set());
} catch (e) { } catch (e) {
setPageError((e as Error)?.message ?? "批量删除失败"); stores.uiStore.getState().setPageError((e as Error)?.message ?? "批量删除失败");
} }
}, [projectId, setSelected, setValuesByLang, structure]); }, [projectId, stores]);
const handleOpenBulkAiTranslate = useCallback(() => { const handleOpenBulkAiTranslate = useCallback(() => {
setAiBulkOpen(true); stores.modalStore.getState().setAiBulkOpen(true);
}, []); }, [stores]);
useEffect(() => { useEffect(() => {
if (!projectId || languages.length === 0) return; if (!projectId || languages.length === 0) return;
@ -514,12 +537,12 @@ export default function Editor({ projectId }: EditorProps) {
const rec = await getLanguageTranslations(projectId, lang); const rec = await getLanguageTranslations(projectId, lang);
all[lang] = rec?.values ?? {}; all[lang] = rec?.values ?? {};
} }
if (!cancelled) setValuesByLang(all); if (!cancelled) stores.dataStore.getState().setValuesByLang(all);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [projectId, languages]); }, [projectId, languages, stores]);
const disabledItems = useMemo(() => { const disabledItems = useMemo(() => {
return { return {
@ -530,7 +553,7 @@ export default function Editor({ projectId }: EditorProps) {
"import-with-files": false, "import-with-files": false,
export: languages.length === 0, export: languages.length === 0,
}; };
}, [adapter, structure, languages.length]); }, [adapter, structure, languages.length, savingAll, connSnap.connections]);
const onClickItem = useCallback((item: CommonMenubarItem) => { const onClickItem = useCallback((item: CommonMenubarItem) => {
switch (item) { switch (item) {
@ -544,16 +567,16 @@ export default function Editor({ projectId }: EditorProps) {
void handleSaveAllConnected(); void handleSaveAllConnected();
break; break;
case "import-with-directory": case "import-with-directory":
setSourcesWizardOpen(true); stores.modalStore.getState().setSourcesWizardOpen(true);
break; break;
case "import-with-files": case "import-with-files":
setImportOpen(true); stores.modalStore.getState().setImportOpen(true);
break; break;
case "export": case "export":
setExportOpen(true); stores.modalStore.getState().setExportOpen(true);
break; break;
} }
}, [handleSaveAllConnected, setSourcesWizardOpen, setImportOpen, setExportOpen, syncExternalSources]); }, [handleSaveAllConnected, syncExternalSources, stores]);
// 注册键盘快捷方式 // 注册键盘快捷方式
useEditorKeyboardShortcuts({ useEditorKeyboardShortcuts({
@ -571,7 +594,7 @@ export default function Editor({ projectId }: EditorProps) {
<div className="flex items-center justify-end gap-2 text-foreground"> <div className="flex items-center justify-end gap-2 text-foreground">
<HeaderConnectionIndicator projectId={projectId ?? ""} /> <HeaderConnectionIndicator projectId={projectId ?? ""} />
{project && ( {project && (
<Button variant="ghost" onClick={() => setSettingsOpen(true)}> <Button variant="ghost" onClick={() => stores.modalStore.getState().setSettingsOpen(true)}>
<Settings /> <Settings />
<span className="sr-only"></span> <span className="sr-only"></span>
</Button> </Button>
@ -592,7 +615,7 @@ export default function Editor({ projectId }: EditorProps) {
JSON JSON
</div> </div>
<div className="mt-3"> <div className="mt-3">
<Button onClick={() => { setSourcesWizardOpen(true); }}> <Button onClick={() => { stores.modalStore.getState().setSourcesWizardOpen(true); }}>
<FolderOpen /> <FolderOpen />
</Button> </Button>
@ -619,7 +642,7 @@ export default function Editor({ projectId }: EditorProps) {
<ProjectSourcesWizard <ProjectSourcesWizard
open={sourcesWizardOpen} open={sourcesWizardOpen}
onOpenChange={setSourcesWizardOpen} onOpenChange={(v) => stores.modalStore.getState().setSourcesWizardOpen(v)}
projectId={projectId ?? ""} projectId={projectId ?? ""}
onCompleted={refresh} onCompleted={refresh}
onSourcesLoaded={handleWizardSourcesLoaded} onSourcesLoaded={handleWizardSourcesLoaded}
@ -627,7 +650,7 @@ export default function Editor({ projectId }: EditorProps) {
<ImportLanguageModal <ImportLanguageModal
open={importOpen} open={importOpen}
onOpenChange={setImportOpen} onOpenChange={(v) => stores.modalStore.getState().setImportOpen(v)}
projectId={projectId ?? ""} projectId={projectId ?? ""}
hasStructure={!!structure} hasStructure={!!structure}
onImported={refresh} onImported={refresh}
@ -635,7 +658,7 @@ export default function Editor({ projectId }: EditorProps) {
/> />
<ExportLanguageModal <ExportLanguageModal
open={exportOpen} open={exportOpen}
onOpenChange={setExportOpen} onOpenChange={(v) => stores.modalStore.getState().setExportOpen(v)}
projectId={projectId ?? ""} projectId={projectId ?? ""}
languages={languages} languages={languages}
valuesByLang={valuesByLang} valuesByLang={valuesByLang}
@ -644,12 +667,12 @@ export default function Editor({ projectId }: EditorProps) {
<ProjectSettingsModal <ProjectSettingsModal
open={settingsOpen} open={settingsOpen}
onOpenChange={setSettingsOpen} onOpenChange={(v) => stores.modalStore.getState().setSettingsOpen(v)}
project={project} project={project}
onSave={async (update) => { onSave={async (update) => {
if (!projectId) return; if (!projectId) return;
const next = await updateProject({ id: projectId, name: update.name, preferences: update.preferences }); const next = await updateProject({ id: projectId, name: update.name, preferences: update.preferences });
setProject(next); stores.projectStore.getState().setProject(next);
upsertProjectMeta(next); upsertProjectMeta(next);
}} }}
onDelete={async () => { onDelete={async () => {
@ -662,7 +685,7 @@ export default function Editor({ projectId }: EditorProps) {
<AiTranslateModal <AiTranslateModal
open={!!aiModal?.open} 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} languages={languages}
paths={aiModal?.path ? [aiModal.path] : []} paths={aiModal?.path ? [aiModal.path] : []}
initialSelectedLanguages={aiModal?.path ? computeSuggestedLanguages([aiModal.path]) : []} initialSelectedLanguages={aiModal?.path ? computeSuggestedLanguages([aiModal.path]) : []}
@ -670,7 +693,14 @@ export default function Editor({ projectId }: EditorProps) {
prompt={project?.preferences?.aiPrompt} prompt={project?.preferences?.aiPrompt}
model={project?.preferences?.aiModel} model={project?.preferences?.aiModel}
onConfirm={async (translations, options) => { 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>> = {}; const updates: Record<string, Record<string, string>> = {};
try { try {
const targetLangs = options?.selectedLanguages ?? languages; const targetLangs = options?.selectedLanguages ?? languages;
@ -689,10 +719,10 @@ export default function Editor({ projectId }: EditorProps) {
await upsertLanguageTranslations(projectId, lang, next); await upsertLanguageTranslations(projectId, lang, next);
}) })
); );
setValuesByLang((old) => ({ ...old, ...updates })); stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...updates }));
} catch (e) { } catch (e) {
const msg = (e as Error)?.message ?? "保存翻译失败"; const msg = (e as Error)?.message ?? "保存翻译失败";
setPageError(msg); stores.uiStore.getState().setPageError(msg);
throw e; throw e;
} }
}} }}
@ -700,7 +730,7 @@ export default function Editor({ projectId }: EditorProps) {
<AiTranslateModal <AiTranslateModal
open={aiBulkOpen} open={aiBulkOpen}
onOpenChange={setAiBulkOpen} onOpenChange={(v) => stores.modalStore.getState().setAiBulkOpen(v)}
languages={languages} languages={languages}
paths={Array.from(selected)} paths={Array.from(selected)}
initialSelectedLanguages={computeSuggestedLanguages(Array.from(selected))} initialSelectedLanguages={computeSuggestedLanguages(Array.from(selected))}
@ -708,7 +738,14 @@ export default function Editor({ projectId }: EditorProps) {
prompt={project?.preferences?.aiPrompt} prompt={project?.preferences?.aiPrompt}
model={project?.preferences?.aiModel} model={project?.preferences?.aiModel}
onConfirm={async (translations, options) => { 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 { try {
const updatesByLang: Record<string, Record<string, string>> = {}; const updatesByLang: Record<string, Record<string, string>> = {};
const targetLangs = options?.selectedLanguages ?? languages; const targetLangs = options?.selectedLanguages ?? languages;
@ -727,9 +764,9 @@ export default function Editor({ projectId }: EditorProps) {
await upsertLanguageTranslations(projectId, lang, next); await upsertLanguageTranslations(projectId, lang, next);
}) })
); );
setValuesByLang((old) => ({ ...old, ...updatesByLang })); stores.dataStore.getState().setValuesByLang((old) => ({ ...old, ...updatesByLang }));
} catch (e) { } catch (e) {
setPageError((e as Error)?.message ?? "批量 AI 翻译保存失败"); stores.uiStore.getState().setPageError((e as Error)?.message ?? "批量 AI 翻译保存失败");
throw e; throw e;
} }
}} }}
@ -737,16 +774,22 @@ export default function Editor({ projectId }: EditorProps) {
<EntryNameModal <EntryNameModal
open={!!addModal?.open} 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" ? "在上方新增条目" : "在下方新增条目"} title={addModal?.position === "above" ? "在上方新增条目" : "在下方新增条目"}
placeholder="请输入条目名称(不含点)" placeholder="请输入条目名称(不含点)"
onConfirm={async (name) => { 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); const nextRoot = insertEntrySibling(structure.root, addModal.path, addModal.position, name);
console.log("nextRoot", addModal, nextRoot); console.log("nextRoot", addModal, nextRoot);
await upsertStructure({ projectId, root: nextRoot }); await upsertStructure({ projectId, root: nextRoot });
setStructure({ projectId, root: nextRoot }); stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
}} }}
validate={(name) => { validate={(name) => {
// 同级重名校验在结构函数中也会做,这里做基础提示即可 // 同级重名校验在结构函数中也会做,这里做基础提示即可
@ -757,28 +800,23 @@ export default function Editor({ projectId }: EditorProps) {
<EntryNameModal <EntryNameModal
open={!!renameModal?.open} 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="重命名条目" title="重命名条目"
placeholder="请输入新名称(不含点)" placeholder="请输入新名称(不含点)"
defaultValue={renameModal?.path ? renameModal.path.split('.').pop() : ''} defaultValue={renameModal?.path ? renameModal.path.split('.').pop() : ''}
onConfirm={async (newName) => { 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); const { root: nextRoot, newPath } = renameEntryAtPath(structure.root, renameModal.path, newName);
await upsertStructure({ projectId, root: nextRoot }); await upsertStructure({ projectId, root: nextRoot });
await renameEntryInAllLanguages(projectId, renameModal.path, newPath); await renameEntryInAllLanguages(projectId, renameModal.path, newPath);
setStructure({ projectId, root: nextRoot }); stores.projectStore.getState().setStructure({ projectId, root: nextRoot });
setValuesByLang((old) => { stores.dataStore.getState().renameEntry(renameModal.path, newPath);
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;
});
}} }}
validate={(name) => { validate={(name) => {
if (name.includes('.')) return "名称不能包含 '.'"; if (name.includes('.')) return "名称不能包含 '.'";

214
src/store/editor-store.ts Normal file
View File

@ -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 };