docs(ui): explain page and shared component lifecycle boundaries

This commit is contained in:
2026-11-12 16:11:24 +08:00
parent 5d82655e0e
commit cc2172319e
10 changed files with 19 additions and 5 deletions
@@ -5,7 +5,8 @@
import { Suspense } from "react";
import GraphPage from "@/components/workspace/graph-page";
/** params按当前Next版本await;Suspense给客户端画布准备过程提供状态占位,不在此发模型请求。 */
/** params按当前Next版本await;Suspense为子树渲染时的挂起提供占位,不在此发模型请求。
* params 的 await 在此边界外;GraphPage 在 effect 中加载模型时的进度/错误由自身状态呈现。 */
export default async function Page({ params }: { params: Promise<{ workspace: string; board: string }> }) {
const { workspace, board } = await params;
return (
+2 -1
View File
@@ -8,7 +8,8 @@ import { ActiveBoardProvider } from "@/components/workspace/active-board";
import DashboardSidebar from "./dashboard";
import styles from "@/components/workspace/workspace.module.css";
/** 当前board信息由ActiveBoardProvider跨侧栏/画布共享;Suspense只包侧栏,fallback保留其布局宽度。 */
/** 当前board信息由ActiveBoardProvider跨侧栏/画布共享;Suspense只包侧栏,fallback保留其布局宽度。
* 该边界响应侧栏渲染期间的 suspend,不自动等待侧栏在 effect 中发起的普通请求。 */
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<ActiveBoardProvider>
+2 -1
View File
@@ -22,7 +22,8 @@ export const metadata: Metadata = {
},
};
/** 服务端布局输出中文html/body壳;客户端反馈Provider包在runtime宿主外侧,供宿主状态提示使用。 */
/** 服务端布局输出中文html/body壳;客户端反馈Provider包在runtime宿主外侧,供宿主状态提示使用。
* children 作为插槽传入客户端宿主,不会因此把所有子页面模块变成客户端代码;宿主的释放逻辑在自身模块。 */
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="zh-CN" data-scroll-behavior="smooth">
+4
View File
@@ -35,6 +35,7 @@ export default function Login() {
verify = hash.get("verify_email"),
failure = hash.get("error");
if (!code && !verify && !failure) return;
// 不只删除认证片段,也删除原 query;当前页面没有保留/恢复任意 returnTo 的流程。
history.replaceState(null, "", location.pathname);
if (failure) {
setError("第三方登录未完成,请重新尝试。");
@@ -68,6 +69,7 @@ export default function Login() {
* HTML输入限制给即时反馈,服务端仍负责真实凭证/注册校验。 */
async function submit(e: FormEvent) {
e.preventDefault();
// busy 是显示状态,不是函数入口互斥锁;按钮禁用限制常规点击,submit 本身不检查已在执行。
setBusy(true);
setError("");
setMessage("");
@@ -76,6 +78,7 @@ export default function Login() {
method: "POST",
body: JSON.stringify({ email, password }),
});
// 使用触发本次提交时 render 闭包里的 register;等待期间切换模式不会改变这次请求的后续分支。
if (register)
setMessage("验证邮件已请求发送,请检查邮箱并点击验证链接。");
else router.push("/dashboard");
@@ -98,6 +101,7 @@ export default function Login() {
setError("无法启动登录,请重新尝试。");
}
}
// submit/oauth 同样没有卸载取消或结果代次校验;回调链结束前可继续调用 router/setState。
// 模式切换只清提示,已有邮箱/密码和可见性独立保留。
// 渲染分为品牌故事、模式切换、受控凭证表单、OAuth按钮与demo入口;错误用alert,非错误提示用status。
return (
+2
View File
@@ -21,12 +21,14 @@ export default async function Home() {
signal: AbortSignal.timeout(15000),
});
// 按响应状态确定是否已认证,再丢弃不需要的body;此处不解析profile字段。
// 先保存 HTTP 成功判定;随后 cancel 若抛错,catch 不会把 authenticated 重置为 false。
authenticated = response.ok;
await response.body?.cancel();
} catch {
// An unverified session must not hide the public landing page.
}
}
// redirect 放在上方 try/catch 外,避免把框架用于导航的控制流异常当作 profile 请求失败吞掉。
// 仅在确认成功后跳工作台,未认证时沿用同一静态展示组件。
if (authenticated) redirect("/dashboard");
return <LandingPage />;
+1
View File
@@ -28,6 +28,7 @@ export default function LandingPage() {
const [active, setActive] = useState(0);
// null表示全部收起;点击当前问题置null,点击其它问题改索引。hidden与aria-expanded/controls对应同一状态。
const [faq, setFaq] = useState<number | null>(null);
// FAQ 的 hidden 只切换可见性,问答节点仍保留在 DOM;页面没有请求型加载或异步清理任务。
// 页面顺序:导航→首屏交互预览→理念横条→三个功能说明→工作步骤→FAQ→行动入口→页脚。
// 功能卡与步骤通过静态内容展示;FAQ以问题文本作key,步骤以序号作key,预览以说明作key。
// 状态仅在本次组件挂载期间保留;切换预览不自动轮播,FAQ展开不发网络请求。
+2 -1
View File
@@ -13,7 +13,8 @@ export const FeedbackContext = createContext(false);
/** 包住页面一次即可共享提示延迟;400ms首次出现,200ms内切换提示可跳过等待。
* Toaster放在右下角,最多展示三条通知,默认4500ms消失并允许关闭;颜色读取主题变量。
* children正常透传,本组件不订阅文档、队列或服务端状态。 */
* children正常透传,本组件不订阅文档、队列或服务端状态。
* visibleToasts 限制同时可见数量,不是业务事件队列容量;通知创建/撤销仍通过通知适配器调用。 */
export function AppFeedback({ children }: { children: ReactNode }) {
return <FeedbackContext.Provider value={true}>
<Tooltip.Provider delayDuration={400} skipDelayDuration={200}>
+1
View File
@@ -53,6 +53,7 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
// 只选择宿主,不另包一层DOM;cn合并变体和调用方样式,业务逻辑不进入这个封装。
// asChild 不把链接转换成原生按钮:disabled 属性本身不保证链接无法导航,调用方须按宿主处理行为。
const Comp = asChild ? Slot : "button";
return (
<Comp
+2 -1
View File
@@ -40,7 +40,8 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
/** 组合Portal、遮罩与居中内容面板,并在children之后固定附带右上角关闭按钮。
* 面板ref和剩余属性交给Radix Content,保留焦点及外部交互事件;关闭按钮有读屏文字。
* 本封装没有单独隐藏默认关闭按钮的参数,业务若要阻止关闭应在受控开关/事件层处理。 */
* 本封装没有单独隐藏默认关闭按钮的参数,业务若要阻止关闭应在受控开关/事件层处理。
* 剩余 props 只传 Content;当前调用没有把 Overlay 的属性入口一并暴露给 DialogContent。 */
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
+1
View File
@@ -21,6 +21,7 @@ export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "chi
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
function IconButton({ icon: Icon, label, title = label, className = "", type = "button", ...props }, ref) {
// Provider存在时由Radix显示提示,移除原生title以免出现两层提示;图标自身对读屏隐藏。
// 后置 ...props 可以覆盖 aria-label 等同名属性;label 是默认名称,最终 DOM 语义由调用者共同负责。
const feedback = useContext(FeedbackContext);
const button = (
<button