526 lines
27 KiB
TypeScript
526 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { SidebarContent, ResizableSidebar } from "@/components/sidebar";
|
|
import { Editor } from "@/components/editor";
|
|
import { useEditorStore } from "@/lib/store";
|
|
import { Hash, X, Plus, ChevronLeft, ChevronRight, ChevronDown, Sparkles, Lock, Unlock, FolderOpen, History, RotateCcw } from "lucide-react";
|
|
import { useSettingsStore } from "@/lib/settings-store";
|
|
import { exportPageAsMarkdown } from "@/lib/export";
|
|
import { useMemo, useState } from "react";
|
|
import { useEffect } from "react";
|
|
import { cn, getTagColor } from "@/lib/utils";
|
|
import { useSearchStore } from "@/lib/search-store";
|
|
import { type Editor as TiptapEditor } from "@tiptap/react";
|
|
import { ImportProvider } from "@/components/import-context";
|
|
import { AIChatPanel } from "@/components/chat/ai-chat-panel";
|
|
import { SearchCommand } from "@/components/search-command";
|
|
import { getBacklinks, getOutgoingLinks, getUnresolvedLinkTitles } from "@/lib/wiki-links";
|
|
import { getPageHistory } from "@/lib/page-history";
|
|
|
|
const ICONS = [
|
|
"📄", "📝", "📒", "📘", "📙", "📕", "📗", "📚",
|
|
"💡", "🔥", "✅", "❗", "⭐", "📌", "📎", "🧠",
|
|
"🎯", "🚀", "⚙️", "🧪", "💼", "📊", "📈", "🧩",
|
|
"🌱", "🌟", "🎨", "🎵", "🎬", "📷", "💬", "🔖",
|
|
"⌛", "🗂️", "📦", "🏷️", "🗓️", "🕒", "🔍", "🧾",
|
|
];
|
|
|
|
export default function Home() {
|
|
const { activePageId, pages, pageSync, updatePage, setActivePageId, addPage, retryPageSync, retryPendingSyncs, flushPendingSyncs } = useEditorStore();
|
|
const { openSearchWithTag } = useSearchStore();
|
|
const { timezone } = useSettingsStore();
|
|
|
|
const activePage = pages.find((p) => p.id === activePageId);
|
|
const activePageSync = activePageId ? pageSync[activePageId] : undefined;
|
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
|
const [editor, setEditor] = useState<TiptapEditor | null>(null);
|
|
const [isAddingTag, setIsAddingTag] = useState(false);
|
|
const [tagInput, setTagInput] = useState("");
|
|
const [isIconPickerOpen, setIsIconPickerOpen] = useState(false);
|
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
|
const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0);
|
|
|
|
const breadcrumbs = useMemo(() => {
|
|
if (!activePage) return [];
|
|
const result = [] as typeof pages;
|
|
let current = activePage;
|
|
while (current) {
|
|
result.unshift(current);
|
|
if (!current.parentId) break;
|
|
const parent = pages.find((p) => p.id === current.parentId);
|
|
if (!parent) break;
|
|
current = parent;
|
|
}
|
|
return result;
|
|
}, [activePage, pages]);
|
|
|
|
const outgoingLinks = useMemo(() => {
|
|
if (!activePage || activePage.type !== "file") return [];
|
|
return getOutgoingLinks(activePage, pages);
|
|
}, [activePage, pages]);
|
|
|
|
const backlinks = useMemo(() => {
|
|
if (!activePage || activePage.type !== "file") return [];
|
|
return getBacklinks(activePage, pages);
|
|
}, [activePage, pages]);
|
|
|
|
const unresolvedLinks = useMemo(() => {
|
|
if (!activePage || activePage.type !== "file") return [];
|
|
return getUnresolvedLinkTitles(activePage, pages);
|
|
}, [activePage, pages]);
|
|
|
|
const localHistory = useMemo(() => {
|
|
if (!activePage || activePage.type !== "file") return [];
|
|
return getPageHistory(activePage.id);
|
|
}, [activePage]);
|
|
const effectiveHistoryIndex = Math.min(selectedHistoryIndex, Math.max(localHistory.length - 1, 0));
|
|
|
|
const addTag = () => {
|
|
if (!activePage) return;
|
|
const next = tagInput.trim();
|
|
if (!next) return;
|
|
const merged = Array.from(new Set([...(activePage.tags || []), next]));
|
|
updatePage(activePage.id, { tags: merged });
|
|
setTagInput("");
|
|
setIsAddingTag(false);
|
|
};
|
|
|
|
const handleOpenWikiLink = async (title: string) => {
|
|
const target = pages.find((p) => p.type === "file" && (p.title || "").trim() === title);
|
|
if (target) {
|
|
setActivePageId(target.id);
|
|
return;
|
|
}
|
|
const shouldCreate = window.confirm(`未找到页面“${title}”。是否立即创建?`);
|
|
if (!shouldCreate) return;
|
|
await addPage(null, "file", { title, content: "" });
|
|
};
|
|
|
|
useEffect(() => {
|
|
const onOnline = () => {
|
|
retryPendingSyncs();
|
|
};
|
|
window.addEventListener("online", onOnline);
|
|
return () => window.removeEventListener("online", onOnline);
|
|
}, [retryPendingSyncs]);
|
|
|
|
useEffect(() => {
|
|
const flush = () => flushPendingSyncs();
|
|
const onVisibilityChange = () => {
|
|
if (document.visibilityState === "hidden") {
|
|
flush();
|
|
}
|
|
};
|
|
|
|
window.addEventListener("beforeunload", flush);
|
|
window.addEventListener("pagehide", flush);
|
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
|
|
return () => {
|
|
window.removeEventListener("beforeunload", flush);
|
|
window.removeEventListener("pagehide", flush);
|
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
};
|
|
}, [flushPendingSyncs]);
|
|
|
|
return (
|
|
<ImportProvider>
|
|
<div className="relative flex h-[100dvh] w-full overflow-hidden bg-background">
|
|
<SearchCommand />
|
|
|
|
<div className={cn(activePageId ? "hidden" : "block h-full flex-1 md:hidden")}>
|
|
<SidebarContent />
|
|
</div>
|
|
|
|
<ResizableSidebar />
|
|
|
|
<main className={cn("relative z-0 flex h-full flex-1 flex-col overflow-hidden", !activePageId && "hidden md:flex")}>
|
|
{activePage ? (
|
|
<div className="flex-1 overflow-y-auto scroll-smooth">
|
|
<div className="mx-auto min-h-screen max-w-7xl px-3 py-4 md:px-16 md:py-6">
|
|
<div className="ui-enter group relative mb-6 pt-10 md:mb-8 md:pt-0">
|
|
<button
|
|
className="absolute left-0 top-0 flex h-9 items-center gap-1 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground md:hidden"
|
|
onClick={() => useEditorStore.getState().setActivePageId(null)}
|
|
>
|
|
<ChevronLeft size={18} />
|
|
返回列表
|
|
</button>
|
|
|
|
<div className="mb-3 flex flex-wrap items-center gap-1 text-xs text-muted-foreground md:mb-4 md:text-sm">
|
|
{breadcrumbs.map((crumb, index) => (
|
|
<div key={crumb.id} className="flex min-w-0 items-center gap-1">
|
|
{index > 0 && <ChevronRight size={14} className="opacity-50" />}
|
|
<button
|
|
onClick={() => useEditorStore.getState().setActivePageId(crumb.id)}
|
|
className={cn(
|
|
"flex min-w-0 items-center gap-1 transition-colors hover:text-foreground hover:underline",
|
|
crumb.id === activePage.id && "pointer-events-none font-medium text-foreground"
|
|
)}
|
|
>
|
|
{crumb.icon && <span>{crumb.icon}</span>}
|
|
<span className="truncate">{crumb.title || "无标题"}</span>
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
{activePage.icon && <span className="select-none pt-0.5 text-[28px] md:text-3xl">{activePage.icon}</span>}
|
|
<input
|
|
value={activePage.title}
|
|
onChange={(e) => updatePage(activePage.id, { title: e.target.value })}
|
|
placeholder="无标题"
|
|
disabled={activePage.isLocked}
|
|
className={cn(
|
|
"w-full border-none bg-transparent text-2xl font-bold leading-tight text-foreground outline-none placeholder:text-muted-foreground/30 md:text-3xl",
|
|
activePage.isLocked && "cursor-not-allowed select-none opacity-80"
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ui-enter-delayed relative z-20 mb-6 flex flex-wrap items-center gap-2">
|
|
{activePage.tags?.map((tag) => {
|
|
const colors = getTagColor(tag);
|
|
return (
|
|
<span
|
|
key={tag}
|
|
onClick={() => openSearchWithTag(tag)}
|
|
className={cn(
|
|
"group/tag inline-flex items-center gap-1 rounded-[4px] border px-2.5 py-1 text-[11px] font-medium shadow-sm",
|
|
colors.bg,
|
|
colors.text,
|
|
colors.border
|
|
)}
|
|
>
|
|
<Hash size={10} className="opacity-70" />
|
|
{tag}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const newTags = activePage.tags?.filter((t) => t !== tag) || [];
|
|
updatePage(activePage.id, { tags: newTags });
|
|
}}
|
|
className="ml-1 rounded-full p-1 opacity-100 transition-all hover:bg-black/10 md:p-0.5 md:opacity-0 md:group-hover/tag:opacity-100 dark:hover:bg-white/10"
|
|
title="移除标签"
|
|
>
|
|
<X size={10} />
|
|
</button>
|
|
</span>
|
|
);
|
|
})}
|
|
|
|
{isAddingTag ? (
|
|
<input
|
|
autoFocus
|
|
type="text"
|
|
value={tagInput}
|
|
onChange={(e) => setTagInput(e.target.value)}
|
|
onBlur={addTag}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
addTag();
|
|
}
|
|
if (e.key === "Escape") {
|
|
setTagInput("");
|
|
setIsAddingTag(false);
|
|
}
|
|
}}
|
|
className="w-28 rounded-md border border-primary/40 bg-background px-2 py-1 text-xs outline-none focus:ring-2 focus:ring-primary/20"
|
|
placeholder="输入标签"
|
|
/>
|
|
) : (
|
|
<button
|
|
onClick={() => setIsAddingTag(true)}
|
|
className="ui-btn-secondary px-2 py-1 text-xs text-muted-foreground"
|
|
>
|
|
<Plus size={12} /> 添加标签
|
|
</button>
|
|
)}
|
|
|
|
<div className="relative z-30">
|
|
<button
|
|
onClick={() => setIsIconPickerOpen((v) => !v)}
|
|
className="ui-btn-secondary px-2 py-1 text-xs text-muted-foreground"
|
|
>
|
|
<Sparkles size={12} /> {activePage.icon ? "更换图标" : "添加图标"}
|
|
</button>
|
|
{isIconPickerOpen && (
|
|
<div className="ui-card absolute left-0 top-full z-[120] mt-1 w-[min(92vw,20rem)] p-2 shadow-xl animate-in fade-in zoom-in-95 sm:w-80">
|
|
<div className="grid h-48 grid-cols-6 gap-1 overflow-y-auto pr-1 sm:grid-cols-8">
|
|
{ICONS.map((icon) => (
|
|
<button
|
|
key={icon}
|
|
onClick={() => {
|
|
updatePage(activePage.id, { icon });
|
|
setIsIconPickerOpen(false);
|
|
}}
|
|
className={cn(
|
|
"flex h-8 w-8 items-center justify-center rounded-md text-lg transition-colors hover:bg-accent",
|
|
activePage.icon === icon && "bg-accent"
|
|
)}
|
|
>
|
|
{icon}
|
|
</button>
|
|
))}
|
|
<button
|
|
onClick={() => {
|
|
updatePage(activePage.id, { icon: null });
|
|
setIsIconPickerOpen(false);
|
|
}}
|
|
className="flex h-8 w-8 items-center justify-center rounded-md text-destructive transition-colors hover:bg-destructive/10"
|
|
title="清除图标"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => updatePage(activePage.id, { isLocked: !activePage.isLocked })}
|
|
className={cn(
|
|
"ui-btn-secondary px-2 py-1 text-xs",
|
|
activePage.isLocked ? "border-orange-200 bg-orange-50 text-orange-600 dark:bg-orange-900/20 dark:text-orange-300" : "text-muted-foreground"
|
|
)}
|
|
title={activePage.isLocked ? "解锁编辑" : "锁定编辑,防止误触"}
|
|
>
|
|
{activePage.isLocked ? <Lock size={12} /> : <Unlock size={12} />}
|
|
{activePage.isLocked ? "已锁定" : "锁定"}
|
|
</button>
|
|
|
|
{activePageSync && (
|
|
<div className="flex items-center gap-1.5">
|
|
<div
|
|
className={cn(
|
|
"rounded border px-2 py-0.5 text-[11px] md:text-xs",
|
|
activePageSync.status === "saving" && "border-blue-200 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300",
|
|
activePageSync.status === "pending" && "border-amber-200 bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-300",
|
|
activePageSync.status === "error" && "border-red-200 bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-300",
|
|
activePageSync.status === "saved" && "border-emerald-200 bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300"
|
|
)}
|
|
title={activePageSync.message}
|
|
>
|
|
{activePageSync.message || "已保存"}
|
|
</div>
|
|
{(activePageSync.status === "pending" || activePageSync.status === "error") && (
|
|
<button
|
|
onClick={() => activePage && retryPageSync(activePage.id)}
|
|
className="ui-btn-secondary px-2 py-0.5 text-[11px] md:text-xs"
|
|
title="立即尝试同步到本地数据库"
|
|
>
|
|
立即同步
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activePage.updatedAt && (
|
|
<div className="w-full pt-0.5 text-[11px] text-muted-foreground/60 md:ml-1 md:h-4 md:w-auto md:border-l md:pl-2 md:pt-0 md:text-xs md:text-muted-foreground/50">
|
|
{new Date(activePage.updatedAt).toLocaleString("zh-CN", {
|
|
timeZone: timezone || "Asia/Shanghai",
|
|
hour12: false,
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{activePage.type === "folder" ? (
|
|
<div className="ui-card ui-enter flex h-[45vh] flex-col items-center justify-center gap-3 text-muted-foreground md:h-[50vh]">
|
|
<div className="ui-float rounded-2xl border border-border/70 bg-muted/40 p-4">
|
|
<FolderOpen size={34} className="text-primary/80" />
|
|
</div>
|
|
<p className="text-base font-medium text-foreground">当前是文件夹</p>
|
|
<p className="text-sm text-muted-foreground">你可以在左侧新建文档,或把文档拖拽到此文件夹。</p>
|
|
</div>
|
|
) : (
|
|
<div className="min-h-[60vh] pb-24">
|
|
<Editor
|
|
content={activePage.content}
|
|
onChange={(content) => updatePage(activePage.id, { content })}
|
|
onEditorReady={setEditor}
|
|
onToggleAI={() => setIsChatOpen(!isChatOpen)}
|
|
onExport={() => exportPageAsMarkdown(activePage)}
|
|
onOpenWikiLink={handleOpenWikiLink}
|
|
editable={!activePage.isLocked}
|
|
/>
|
|
|
|
<section className="ui-enter mt-6 space-y-3 md:mt-7">
|
|
<div className="grid gap-2 md:grid-cols-3">
|
|
<div className="ui-card p-2.5 md:p-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-foreground">关联页面</h3>
|
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{outgoingLinks.length}</span>
|
|
</div>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">正文写 `[[页面名]]` 可自动关联。</p>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{outgoingLinks.length > 0 ? (
|
|
outgoingLinks.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setActivePageId(item.id)}
|
|
className="rounded-md border border-border/70 bg-muted/40 px-2 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
|
>
|
|
{item.icon ? `${item.icon} ` : ""}
|
|
{item.title}
|
|
</button>
|
|
))
|
|
) : (
|
|
<span className="text-[11px] text-muted-foreground">暂无</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ui-card p-2.5 md:p-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-foreground">反向链接</h3>
|
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{backlinks.length}</span>
|
|
</div>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">被哪些页面引用。</p>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{backlinks.length > 0 ? (
|
|
backlinks.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setActivePageId(item.id)}
|
|
className="rounded-md border border-border/70 bg-muted/40 px-2 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
|
>
|
|
{item.icon ? `${item.icon} ` : ""}
|
|
{item.title}
|
|
</button>
|
|
))
|
|
) : (
|
|
<span className="text-[11px] text-muted-foreground">暂无</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ui-card p-2.5 md:p-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-foreground">未解析链接</h3>
|
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{unresolvedLinks.length}</span>
|
|
</div>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">可一键创建缺失页面。</p>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{unresolvedLinks.length > 0 ? (
|
|
unresolvedLinks.map((title) => (
|
|
<button
|
|
key={title}
|
|
onClick={async () => {
|
|
await addPage(null, "file", { title, content: "" });
|
|
}}
|
|
className="rounded-md border border-dashed border-border bg-muted/30 px-2 py-0.5 text-[11px] text-foreground transition-colors hover:bg-accent"
|
|
title={`创建页面:${title}`}
|
|
>
|
|
+ {title}
|
|
</button>
|
|
))
|
|
) : (
|
|
<span className="text-[11px] text-muted-foreground">全部已解析</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ui-card p-2.5 md:p-3">
|
|
<button
|
|
onClick={() => setIsHistoryOpen((v) => !v)}
|
|
className="flex w-full items-center justify-between rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/45"
|
|
aria-expanded={isHistoryOpen}
|
|
aria-label="切换本地历史版本面板"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<History size={14} className="text-muted-foreground" />
|
|
<h3 className="text-sm font-semibold text-foreground">本地历史版本</h3>
|
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{localHistory.length}</span>
|
|
</div>
|
|
<ChevronDown size={15} className={cn("text-muted-foreground transition-transform", isHistoryOpen && "rotate-180")} />
|
|
</button>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">自动保存最近修改快照(当前浏览器本地)。</p>
|
|
{isHistoryOpen && (
|
|
<div className="mt-2 space-y-2.5">
|
|
{localHistory.length > 0 ? (
|
|
<>
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-2">
|
|
<label className="mb-1 block text-[11px] text-muted-foreground">选择要恢复的版本</label>
|
|
<select
|
|
value={selectedHistoryIndex}
|
|
onChange={(e) => setSelectedHistoryIndex(parseInt(e.target.value, 10))}
|
|
className="ui-select h-9 py-1 text-xs"
|
|
>
|
|
{localHistory.slice(0, 20).map((snapshot, index) => (
|
|
<option key={`${snapshot.timestamp}-${index}`} value={index}>
|
|
{new Date(snapshot.timestamp).toLocaleString("zh-CN", {
|
|
timeZone: timezone || "Asia/Shanghai",
|
|
hour12: false,
|
|
})}
|
|
{` · ${snapshot.title || "无标题"}`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="rounded-md border border-border/60 bg-muted/15 px-2.5 py-2">
|
|
<div className="truncate text-xs text-foreground">
|
|
{localHistory[effectiveHistoryIndex]?.title || "无标题"}
|
|
</div>
|
|
<div className="text-[11px] text-muted-foreground">
|
|
{localHistory[effectiveHistoryIndex]
|
|
? new Date(localHistory[effectiveHistoryIndex].timestamp).toLocaleString("zh-CN", {
|
|
timeZone: timezone || "Asia/Shanghai",
|
|
hour12: false,
|
|
})
|
|
: "-"}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={async () => {
|
|
const target = localHistory[effectiveHistoryIndex];
|
|
if (!target || !activePage) return;
|
|
const ok = window.confirm("恢复该历史版本后将覆盖当前内容,是否继续?");
|
|
if (!ok) return;
|
|
await updatePage(activePage.id, {
|
|
title: target.title,
|
|
content: target.content,
|
|
});
|
|
}}
|
|
className="ui-btn-secondary h-9 w-full justify-center px-2 py-1 text-xs sm:h-auto sm:w-auto sm:justify-start"
|
|
>
|
|
<RotateCcw size={12} />
|
|
恢复选中版本
|
|
</button>
|
|
</>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">暂无历史记录</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="ui-enter flex h-full items-center justify-center p-4 md:p-8">
|
|
<div className="ui-card flex w-full max-w-md flex-col items-center gap-3 px-6 py-8 text-center text-muted-foreground">
|
|
<div className="ui-float rounded-2xl border border-border/70 bg-muted/40 p-4">
|
|
<span className="text-4xl">🧠</span>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h3 className="text-lg font-semibold text-foreground">欢迎使用 NoteAI</h3>
|
|
<p className="text-sm text-muted-foreground/90">在左侧选择一个页面,或新建文档开始写作。</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
<AIChatPanel editor={editor} isOpen={isChatOpen} onClose={() => setIsChatOpen(false)} />
|
|
</div>
|
|
</ImportProvider>
|
|
);
|
|
}
|