115 lines
3.7 KiB
JavaScript
115 lines
3.7 KiB
JavaScript
import { Component, useEffect, useState } from "react";
|
|
import MainShell from "./components/MainShell";
|
|
import AnnotationPage from "./pages/AnnotationPage";
|
|
import SecondaryWindow from "./windows/SecondaryWindow";
|
|
|
|
function AppErrorFallback({ error }) {
|
|
return (
|
|
<div className="grid min-h-dvh place-items-center bg-red-950 p-6 text-white">
|
|
<div className="w-full max-w-3xl rounded-[18px] border border-red-300 bg-white p-6 text-red-950 shadow-2xl">
|
|
<div className="text-[20px] font-black">应用渲染失败</div>
|
|
<div className="mt-4 space-y-2 font-mono text-[12px] leading-5">
|
|
<div>href: {window.location.href}</div>
|
|
<div>hash: {window.location.hash}</div>
|
|
<div>error: {String(error?.message || error)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
class AppErrorBoundary extends Component {
|
|
state = { error: null };
|
|
|
|
componentDidCatch(error) {
|
|
this.setState({ error });
|
|
}
|
|
|
|
render() {
|
|
if (this.state.error) {
|
|
return <AppErrorFallback error={this.state.error} />;
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function App() {
|
|
const [theme, setTheme] = useState(() => localStorage.getItem("theme") || "light");
|
|
const [activeTab, setActiveTab] = useState("clone");
|
|
const [activeKnowledge, setActiveKnowledge] = useState("全部");
|
|
const [activeSkill, setActiveSkill] = useState("全部");
|
|
const [settingSection, setSettingSection] = useState("基础配置");
|
|
const [route, setRoute] = useState(() => window.location.hash.replace("#", "") || "/");
|
|
|
|
useEffect(() => {
|
|
document.documentElement.dataset.theme = theme;
|
|
localStorage.setItem("theme", theme);
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
const onStorage = (event) => {
|
|
if (event.key === "theme" && event.newValue) {
|
|
setTheme(event.newValue);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("storage", onStorage);
|
|
return () => window.removeEventListener("storage", onStorage);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const timers = new WeakMap();
|
|
const revealScrollbar = (event) => {
|
|
const target = event.target instanceof Element ? event.target : document.scrollingElement;
|
|
if (!target) return;
|
|
target.classList.add("scrollbar-active");
|
|
window.clearTimeout(timers.get(target));
|
|
timers.set(target, window.setTimeout(() => target.classList.remove("scrollbar-active"), 700));
|
|
};
|
|
|
|
document.addEventListener("scroll", revealScrollbar, true);
|
|
return () => document.removeEventListener("scroll", revealScrollbar, true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onHashChange = () => setRoute(window.location.hash.replace("#", "") || "/");
|
|
window.addEventListener("hashchange", onHashChange);
|
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
}, []);
|
|
|
|
const isWindowRoute = route.startsWith("/window/");
|
|
const isAnnotationRoute = route === "/annotate";
|
|
|
|
return (
|
|
<AppErrorBoundary>
|
|
<div className="min-h-dvh overflow-hidden rounded-[8px] bg-page text-text">
|
|
{isAnnotationRoute ? (
|
|
<AnnotationPage />
|
|
) : isWindowRoute ? (
|
|
<SecondaryWindow
|
|
route={route}
|
|
theme={theme}
|
|
setTheme={setTheme}
|
|
settingSection={settingSection}
|
|
setSettingSection={setSettingSection}
|
|
/>
|
|
) : (
|
|
<MainShell
|
|
activeTab={activeTab}
|
|
setActiveTab={setActiveTab}
|
|
theme={theme}
|
|
setTheme={setTheme}
|
|
activeKnowledge={activeKnowledge}
|
|
setActiveKnowledge={setActiveKnowledge}
|
|
activeSkill={activeSkill}
|
|
setActiveSkill={setActiveSkill}
|
|
/>
|
|
)}
|
|
</div>
|
|
</AppErrorBoundary>
|
|
);
|
|
}
|
|
|
|
export default App;
|