| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363 |
- 'use client';
- import {
- useEffect,
- useRef,
- useState,
- type MouseEvent,
- type ReactNode,
- } from 'react';
- import Image from 'next/image';
- import {
- ArrowUpRight,
- ArrowRight,
- ArrowLeft,
- BookOpen,
- Download,
- Check,
- Copy,
- Video as VideoIcon,
- ChevronRight,
- FileCode2,
- ExternalLink,
- RotateCcw,
- ShieldCheck,
- GitBranch,
- House,
- } from 'lucide-react';
- import {
- Sidebar,
- SidebarProvider,
- SidebarContent,
- SidebarHeader,
- SidebarFooter,
- SidebarTrigger,
- useSidebar,
- } from '@/components/ui/sidebar';
- import { Button } from '@/components/ui/button';
- import { Skeleton } from '@/components/ui/skeleton';
- import {
- Table,
- TableHeader,
- TableBody,
- TableRow,
- TableHead,
- TableCell,
- } from '@/components/ui/table';
- import { ProtocolDemo, HttpDemo, AsyncDemo } from './demos';
- import { TaskDeliveryDemo } from './task-delivery-demo';
- import { CourseHome } from './course-home';
- import { ReviewPage } from './review-page';
- import {
- ModuleDiagrams,
- LessonDiagrams,
- hasLessonDiagrams,
- } from './teaching-diagrams';
- import { courseHref, parseCourseQuery } from './course-navigation';
- import { modules, getModule, type ModuleInfo } from './module-catalog';
- import type { Lesson, TeachingLesson, Video } from './lesson-types';
- import apiData from './lessons.json';
- import videoData from './videos.json';
- const videos = videoData as Record<string, Video>;
- type Navigate = (module: string, lesson?: string) => void;
- type Route = { module: string; lesson?: string };
- const overviewSections = [
- ['module-diagrams', '模块图解'],
- ['path', '学习路径'],
- ['context', '工程边界'],
- ['ready', '完成标准'],
- ];
- const lessonSections = [
- ['start', '从问题开始'],
- ['objectives', '这节课要完成什么'],
- ['steps', '一步步完成案例'],
- ['checks', '检查学习结果'],
- ['diagrams', '把过程连起来'],
- ['production', '进一步验证'],
- ['pitfalls', '遇到问题时查这里'],
- ['interview', '面试问答'],
- ['exercise', '自己试一次'],
- ['concepts', '核心概念回顾'],
- ['references', '技术文档'],
- ];
- const reviewSections = [
- ['review-start', '如何自测'],
- ['review-questions', '核心理解问题'],
- ['review-challenges', '情境推演'],
- ['review-mastery', '掌握标准与证据'],
- ['review-result', '自测结果与回学'],
- ];
- function routeLink(module: string, lesson?: string) {
- return courseHref(module, lesson);
- }
- function readRoute(): Route {
- return parseCourseQuery(location.search);
- }
- function groups(lessons: Lesson[]) {
- const result: { name: string; lessons: Lesson[] }[] = [];
- for (const l of lessons) {
- const last = result[result.length - 1];
- if (last?.name === l.group) last.lessons.push(l);
- else result.push({ name: l.group, lessons: [l] });
- }
- return result;
- }
- function LocalLink({
- module = 'home',
- lesson,
- navigate,
- children,
- className,
- ...props
- }: {
- module?: string;
- lesson?: string;
- navigate: Navigate;
- children: ReactNode;
- className?: string;
- 'aria-current'?: 'page';
- }) {
- return (
- <a
- href={routeLink(module, lesson)}
- className={className}
- {...props}
- onClick={(e: MouseEvent<HTMLAnchorElement>) => {
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
- return;
- e.preventDefault();
- navigate(module, lesson);
- }}
- >
- {children}
- </a>
- );
- }
- function Navigation({
- route,
- lessons,
- navigate,
- }: {
- route: Route;
- lessons: Lesson[];
- navigate: Navigate;
- }) {
- const { setOpenMobile } = useSidebar();
- const go: Navigate = (m, l) => {
- navigate(m, l);
- setOpenMobile(false);
- };
- return (
- <Sidebar>
- <SidebarHeader>
- <LocalLink navigate={go} className="brand">
- <Image
- src="/brand/shuh-logo-white.png"
- alt="术核 AI"
- width={1100}
- height={360}
- unoptimized
- />
- <span className="brand-sub">Agent 开发 · 后端工程课件</span>
- </LocalLink>
- </SidebarHeader>
- <SidebarContent>
- <div className="nav-caption">
- 课程目录<span>{String(modules.length).padStart(2, '0')} MODULES</span>
- </div>
- <nav className="module-nav" aria-label="课程模块">
- <LocalLink
- module="home"
- navigate={go}
- className={`module-link ${route.module === 'home' ? 'selected' : ''}`}
- aria-current={
- route.module === 'home' && !route.lesson ? 'page' : undefined
- }
- >
- <House size={18} />
- <span>课程总览</span>
- </LocalLink>
- {modules.map((m, i) => (
- <LocalLink
- key={m.slug}
- module={m.slug}
- navigate={go}
- className={`module-link ${route.module === m.slug ? 'selected' : ''}`}
- aria-current={
- route.module === m.slug && !route.lesson ? 'page' : undefined
- }
- >
- <m.icon size={18} />
- <span>{m.name}</span>
- <span className="module-count">
- {String(i + 1).padStart(2, '0')}
- </span>
- </LocalLink>
- ))}
- </nav>
- {route.lesson && lessons.length > 0 && (
- <nav className="lesson-nav" aria-label="本模块课时">
- <div className="nav-caption">
- 本模块课时<span>{lessons.length} LESSONS</span>
- </div>
- {groups(lessons).map((g, i) => (
- <div key={`${g.name}-${i}`}>
- <p className="lesson-nav-group">{g.name}</p>
- {g.lessons.map((l) => (
- <LocalLink
- key={l.id}
- module={route.module}
- lesson={l.id}
- navigate={go}
- className={`lesson-nav-link ${route.lesson === l.id ? 'current' : ''}`}
- aria-current={route.lesson === l.id ? 'page' : undefined}
- >
- <span>{l.id}</span>
- {l.title}
- </LocalLink>
- ))}
- </div>
- ))}
- </nav>
- )}
- </SidebarContent>
- <SidebarFooter>
- <div className="sidebar-note">
- <BookOpen size={17} />
- <div>
- 后端工程与系统设计<small>理解 · 实现 · 验证 · 恢复</small>
- </div>
- </div>
- </SidebarFooter>
- </Sidebar>
- );
- }
- export default function Course() {
- const pendingNavigationFocus = useRef(false);
- const [route, setRoute] = useState<Route>({ module: 'home' });
- const [data, setData] = useState<Record<string, Lesson[]>>({
- api: apiData as Lesson[],
- });
- const [error, setError] = useState('');
- const [retry, setRetry] = useState(0);
- const current = getModule(route.module);
- const lessons = current ? data[current.slug] : undefined;
- const lesson = lessons?.find((l) => l.id === route.lesson);
- const isHome = route.module === 'home' && !route.lesson;
- const invalid =
- !isHome && (!current || !!(lessons && route.lesson && !lesson));
- useEffect(() => {
- const read = () => {
- setRoute(readRoute());
- setError('');
- };
- read();
- window.addEventListener('popstate', read);
- return () => window.removeEventListener('popstate', read);
- }, []);
- useEffect(() => {
- if (!current || lessons) return;
- const controller = new AbortController();
- fetch(`/content/${current.slug}.json`, { signal: controller.signal })
- .then(async (r) => {
- if (!r.ok) throw new Error('课件暂时无法载入,请重试。');
- const payload = await r.json();
- if (
- !Array.isArray(payload) ||
- payload.length !== current.count ||
- payload.some(
- (l) =>
- !l.id ||
- !l.title ||
- (current.slug === 'review'
- ? l.kind !== 'review' ||
- !Array.isArray(l.questions) ||
- l.questions.length !== 6 ||
- !Array.isArray(l.challenges) ||
- l.challenges.length !== 2 ||
- !Array.isArray(l.mastery) ||
- l.mastery.length !== 4 ||
- !l.intro
- : !Array.isArray(l.steps)),
- )
- )
- throw new Error('课件文件不完整,请重试。');
- setData((previous) => ({ ...previous, [current.slug]: payload }));
- })
- .catch((e) => {
- if (e.name !== 'AbortError')
- setError('课件暂时无法载入。请重试,或刷新页面检查登录状态。');
- });
- return () => controller.abort();
- }, [current, lessons, retry]);
- useEffect(() => {
- document.title = `${isHome ? '课程总览' : lesson?.title || current?.name || '课件'} · 术核 AI 后端工程`;
- }, [lesson, current, isHome]);
- useEffect(() => {
- // Lazy-loaded lessons are not in the DOM when a bookmarked fragment opens.
- const scrollToSection = () => {
- if (!location.hash) return;
- let id: string;
- try {
- id = decodeURIComponent(location.hash.slice(1));
- } catch {
- return;
- }
- document
- .getElementById(id)
- ?.scrollIntoView({ block: 'start', behavior: 'instant' });
- };
- const frame = requestAnimationFrame(scrollToSection);
- window.addEventListener('hashchange', scrollToSection);
- return () => {
- cancelAnimationFrame(frame);
- window.removeEventListener('hashchange', scrollToSection);
- };
- }, [route.module, route.lesson, lessons]);
- useEffect(() => {
- if (
- !pendingNavigationFocus.current ||
- (!isHome && !invalid && !error && !lessons)
- )
- return;
- const frame = requestAnimationFrame(() => {
- const heading = document.querySelector<HTMLElement>('h1');
- if (heading) {
- heading.focus({ preventScroll: true });
- pendingNavigationFocus.current = false;
- }
- });
- return () => cancelAnimationFrame(frame);
- }, [route.module, route.lesson, lessons, isHome, invalid, error]);
- const navigate: Navigate = (module, id) => {
- pendingNavigationFocus.current = true;
- setRoute({ module, lesson: id });
- setError('');
- history.pushState({}, '', routeLink(module, id));
- window.scrollTo({ top: 0, behavior: 'instant' });
- };
- const toc = isHome
- ? [
- ['architecture', 'Agent 架构全景'],
- ['modules', '模块定位与作用'],
- ['journey', '一次长任务的旅程'],
- ['learning-path', '推荐学习顺序'],
- ]
- : lesson?.kind === 'review'
- ? reviewSections
- : lesson
- ? lessonSections
- .filter(
- ([id]) =>
- (id !== 'start' || lesson.intro) &&
- (id !== 'production' || lesson.production) &&
- (id !== 'interview' || lesson.interview) &&
- (id !== 'diagrams' ||
- hasLessonDiagrams(route.module, lesson.id)),
- )
- .map(([id, label]) => [
- id,
- lesson.kind === 'theory'
- ? (
- {
- steps: '一步步推演场景',
- checks: '检查推演结果',
- pitfalls: '常见误区',
- exercise: '自己设计一次',
- references: '延伸阅读',
- } as Record<string, string>
- )[id] || label
- : label,
- ])
- : route.module === 'review'
- ? [
- ['path', '选择自测模块'],
- ['context', '如何判断掌握'],
- ['ready', '总体完成标准'],
- ]
- : overviewSections;
- return (
- <SidebarProvider
- style={{ '--sidebar-width': '270px' } as React.CSSProperties}
- >
- <a className="skip-link" href="#main-content">
- 跳到课件正文
- </a>
- <Navigation route={route} lessons={lessons || []} navigate={navigate} />
- <div className="workspace">
- <header className="topbar">
- <div className="breadcrumbs">
- <SidebarTrigger aria-label="展开或收起课程目录" />
- <LocalLink navigate={navigate}>后端工程</LocalLink>
- <span>/</span>
- <strong>{isHome ? '课程总览' : current?.name || '课件'}</strong>
- {lesson && (
- <>
- <span>/</span>
- <span className="crumb-lesson">{lesson.id}</span>
- </>
- )}
- </div>
- <a
- className="quiet-link"
- href="https://shuh.ai/"
- target="_blank"
- rel="noreferrer"
- >
- 术核 AI
- <ArrowUpRight size={15} />
- </a>
- </header>
- <div className={`document-grid${isHome ? ' document-grid-home' : ''}`}>
- <main id="main-content">
- {isHome ? (
- <CourseHome navigate={navigate} />
- ) : invalid ? (
- <div className="pending-module">
- <h1 tabIndex={-1}>未找到该课件</h1>
- <p>该模块或课时不存在,请从目录重新选择。</p>
- <LocalLink navigate={navigate} className="primary-link">
- 返回课程
- <ArrowRight size={16} />
- </LocalLink>
- </div>
- ) : error ? (
- <div className="load-error" role="alert">
- <h1 tabIndex={-1}>{current?.name}</h1>
- <p>{error}</p>
- <Button
- onClick={() => {
- setError('');
- setRetry((r) => r + 1);
- }}
- >
- <RotateCcw />
- 重新载入
- </Button>
- </div>
- ) : !lessons || !current ? (
- <div aria-busy="true" aria-label="正在载入课件">
- <div className="eyebrow">{current?.name}</div>
- <Skeleton className="h-10 w-3/4 my-7" />
- <Skeleton className="h-5 w-full mb-3" />
- <Skeleton className="h-5 w-4/5 mb-12" />
- {[1, 2, 3].map((n) => (
- <Skeleton key={n} className="h-24 w-full my-4" />
- ))}
- </div>
- ) : lesson?.kind === 'review' ? (
- <ReviewPage
- key={`${route.module}/${lesson.id}`}
- lesson={lesson}
- lessons={lessons}
- navigate={navigate}
- />
- ) : lesson ? (
- <LessonPage
- key={`${route.module}/${lesson.id}`}
- module={current}
- lessons={lessons}
- lesson={lesson}
- navigate={navigate}
- />
- ) : (
- <Overview
- module={current}
- lessons={lessons}
- navigate={navigate}
- />
- )}
- </main>
- <aside className="toc" aria-label="本页导航">
- <p>{isHome ? '课程导览' : lesson ? '本节导航' : '本模块导航'}</p>
- {!invalid &&
- !error &&
- (isHome || !!lessons) &&
- toc.map(([id, label]) => (
- <a key={id} href={`#${id}`}>
- {label}
- </a>
- ))}
- <div className="toc-note">
- <span className="tiny-label">学习准备</span>
- <p>
- {(isHome
- ? ['有 Python 基础即可开始', '先看全景,再逐模块深入']
- : current?.prerequisites
- )?.map((p) => (
- <span key={p} className="prerequisite-line">
- {p}
- </span>
- ))}
- </p>
- </div>
- {current && (
- <a
- className="toc-download"
- href={`/downloads/${current.slug}-lessons.md`}
- download
- >
- <Download size={14} />
- 下载模块课件
- </a>
- )}
- {route.module === 'api' && (
- <a
- className="toc-download"
- href="/downloads/api-lab.zip"
- download
- >
- <FileCode2 size={14} />
- API 示例项目
- </a>
- )}
- </aside>
- </div>
- <footer className="page-footer">
- <span>术核 AI · Agent 开发前置知识</span>
- <span>接口 / 数据 / 并发 / 交付 / 运维 / 架构</span>
- </footer>
- </div>
- </SidebarProvider>
- );
- }
- function Overview({
- module: m,
- lessons,
- navigate,
- }: {
- module: ModuleInfo;
- lessons: Lesson[];
- navigate: Navigate;
- }) {
- const index = modules.findIndex((x) => x.slug === m.slug);
- const next = modules[index + 1];
- const theory = m.slug === 'distributed';
- const review = m.slug === 'review';
- return (
- <>
- <div className="eyebrow">
- MODULE {String(index + 1).padStart(2, '0')}
- <span />
- {m.kicker}
- </div>
- <h1 tabIndex={-1}>
- {m.name}
- <span className="title-dot">.</span>
- </h1>
- <p className="lead">{m.description}</p>
- <div className="meta-row">
- <span>{m.stack}</span>
- <span>
- {lessons.length}{' '}
- {review ? '个自测单元' : `节${theory ? '理论课' : '实战课'}`}
- </span>
- <span>
- {review
- ? '逐模块检验理解与应用'
- : theory
- ? '架构设计与面试'
- : m.slug === 'api'
- ? '接口基础'
- : '生产系统实践'}
- </span>
- </div>
- <div className="overview-actions">
- <LocalLink
- module={m.slug}
- lesson="01"
- navigate={navigate}
- className="primary-link"
- >
- {review ? '开始第一个自测' : '开始第一节'}
- <ArrowRight size={16} />
- </LocalLink>
- <a
- className="secondary-link"
- href={`/downloads/${m.slug}-lessons.md`}
- download
- >
- <Download size={16} />
- 下载模块课件
- </a>
- {m.slug === 'api' && (
- <a href="/downloads/api-lab.zip" download className="secondary-link">
- <FileCode2 size={16} />
- 示例项目
- </a>
- )}
- </div>
- <div className="module-intro">
- <span className="section-index">
- {review ? 'REVIEW' : theory ? 'DESIGN' : 'PROJECT'}
- </span>
- <div>
- <h3>{m.project}</h3>
- <p>{m.scope}</p>
- </div>
- </div>
- {!review && <ModuleDiagrams module={m.slug} />}
- <section id="path">
- <div className="section-heading">
- <h2>{review ? '选择要自测的模块' : '学习路径'}</h2>
- <span>
- {review
- ? '独立回答 → 对照要点 → 验证掌握 → 回学'
- : theory
- ? '问题 → 推演 → 图解 → 概念回顾'
- : '问题 → 实践 → 验证 → 概念回顾'}
- </span>
- </div>
- <div className="curriculum">
- {groups(lessons).map((g, i) => (
- <div className="phase" key={`${g.name}-${i}`}>
- <div className="phase-heading">
- <span className="phase-marker">
- {String(i + 1).padStart(2, '0')}
- </span>
- <div>
- <h3>{g.name}</h3>
- </div>
- <span className="phase-range">
- {g.lessons[0].id}—{g.lessons[g.lessons.length - 1].id}
- </span>
- </div>
- <div className="lesson-rows">
- {g.lessons.map((l) => (
- <LocalLink
- key={l.id}
- module={m.slug}
- lesson={l.id}
- navigate={navigate}
- className="lesson-row"
- >
- <span className="lesson-number">{l.id}</span>
- <div>
- <strong>{l.title}</strong>
- <p>{l.summary}</p>
- </div>
- {l.kind !== 'review' &&
- (l.demo || hasLessonDiagrams(m.slug, l.id)) && (
- <span className="interactive-badge">
- {l.demo ? '交互' : '图解'}
- </span>
- )}
- <ChevronRight size={15} />
- </LocalLink>
- ))}
- </div>
- </div>
- ))}
- </div>
- </section>
- <section id="context">
- <div className="section-heading">
- <h2>{review ? '如何判断掌握' : '工程边界'}</h2>
- <span>{m.stack}</span>
- </div>
- {review ? (
- <div className="engineering-context">
- <div className="context-heading">
- <ShieldCheck size={22} />
- <h3>能解释,能换个条件推演,也能拿出证据</h3>
- </div>
- <p>
- 每个单元先独立回答六个理解问题和两个情境题,再展开参考要点。只在合上要点后仍能说明原因、边界和验证方式时,标记为独立完成。
- </p>
- <div className="context-columns">
- <div>
- <span>本轮达到标准</span>
- <p>
- 八项回答都能独立完成,四项掌握证据均已核对。操作类需要自己的运行记录,理论类需要完整的设计或故障推演。
- </p>
- </div>
- <div>
- <span>发现缺口之后</span>
- <p>
- 沿题目下方链接回到原课时,补做相关实验或推演,再回来闭卷回答。页面汇总你的自评,不自动判断文字答案的正确性。
- </p>
- </div>
- </div>
- </div>
- ) : m.slug === 'api' ? (
- <>
- <ProtocolDemo />
- <p className="section-caption">
- REST 是接口设计约定;SSE 和 WebSocket
- 提供不同的通信方式。真实收发实验请运行配套 API 项目。
- </p>
- <ApiEndpoints />
- </>
- ) : theory ? (
- <div className="engineering-context">
- <div className="context-heading">
- <GitBranch size={22} />
- <h3>从业务约束推导方案,再讨论失效边界</h3>
- </div>
- <p>
- 本模块通过架构图、故障时间线、设计练习和面试问答展开。无需安装服务或运行代码;阅读时先判断业务允许多旧的数据、能等待多久、失败后能否重试。
- </p>
- <div className="context-columns">
- <div>
- <span>设计依据</span>
- <p>业务不变量、容量估算、一致性与可用性目标。</p>
- </div>
- <div>
- <span>方案取舍</span>
- <p>正常路径、失败路径、运维成本与恢复条件。</p>
- </div>
- </div>
- </div>
- ) : (
- <div className="engineering-context">
- <div className="context-heading">
- <ShieldCheck size={22} />
- <h3>正常运行之后,还要验证失败路径</h3>
- </div>
- <p>
- 本模块每节都明确使用场景、技术取舍、故障操作与恢复条件。命令和故障注入应在该节指定的隔离练习环境执行。
- </p>
- <div className="context-columns">
- <div>
- <span>实现约束</span>
- <p>资源上限、超时、事务与权限边界。</p>
- </div>
- <div>
- <span>故障恢复</span>
- <p>错误可观察,恢复有依据,结果可核对。</p>
- </div>
- </div>
- </div>
- )}
- {m.slug === 'tasks' && (
- <>
- <TaskExecutionMatrix />
- <TaskDeliveryDemo />
- </>
- )}
- {m.depends.length > 0 && (
- <div className="dependencies">
- <span>前置模块</span>
- {m.depends.map((slug) => (
- <LocalLink key={slug} module={slug} navigate={navigate}>
- {getModule(slug)?.name}
- <ArrowUpRight size={13} />
- </LocalLink>
- ))}
- </div>
- )}
- </section>
- <section id="ready">
- <div className="section-heading">
- <h2>模块完成标准</h2>
- <span>
- {review
- ? '用具体证据支持自评'
- : theory
- ? '说清选择、代价与适用边界'
- : '以行为和数据验证结果'}
- </span>
- </div>
- <ul className="check-list">
- {m.outcomes.map((o) => (
- <li key={o}>
- <Check size={16} />
- {o}
- </li>
- ))}
- </ul>
- {!review && (
- <div className="overview-actions">
- <LocalLink
- module="review"
- lesson={String(index + 1).padStart(2, '0')}
- navigate={navigate}
- className="secondary-link"
- >
- 自测本模块:理解问题与掌握证据
- <ArrowRight size={16} />
- </LocalLink>
- </div>
- )}
- {next && (
- <LocalLink
- module={next.slug}
- navigate={navigate}
- className="next-module"
- >
- <span>下一模块</span>
- <strong>{next.name}</strong>
- <ArrowRight size={16} />
- </LocalLink>
- )}
- </section>
- </>
- );
- }
- function TaskExecutionMatrix() {
- return (
- <div className="execution-matrix">
- <h3>按工作负载与可靠性要求选择执行方式</h3>
- <Table className="api-table">
- <TableHeader>
- <TableRow>
- <TableHead>执行方式</TableHead>
- <TableHead>主要用途</TableHead>
- <TableHead>必须明确的边界</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {[
- [
- 'asyncio',
- '支持异步调用的网络 I/O',
- '协作式调度;并发必须有上限,任务状态不持久。',
- ],
- [
- '线程池',
- '阻塞式 I/O 库',
- '共享进程内存;超时后不能直接强杀运行中的线程。',
- ],
- [
- '进程池',
- '可拆分的 CPU 计算',
- '参数需要可序列化;控制进程数量与内存。',
- ],
- [
- 'BackgroundTasks',
- '同进程短小的后续操作',
- '随服务进程退出而丢失,不能充当持久队列。',
- ],
- [
- 'Celery + broker',
- '跨进程执行与可恢复的长任务',
- '持久性依赖配置;处理重复投递、幂等和状态恢复。',
- ],
- ].map(([mode, use, boundary]) => (
- <TableRow key={mode}>
- <TableCell>
- <code>{mode}</code>
- </TableCell>
- <TableCell>{use}</TableCell>
- <TableCell>{boundary}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- <p className="section-caption">
- 常规 CPython 的线程不会自动加速 Python CPU
- 密集计算。先确认工作负载、第三方库行为和解释器构建方式,再选择模型。详细实现见本模块第
- 02—08 节。
- </p>
- </div>
- );
- }
- function ApiEndpoints() {
- return (
- <>
- <h3>API 参考项目</h3>
- <Table className="api-table">
- <TableHeader>
- <TableRow>
- <TableHead>接口</TableHead>
- <TableHead>行为</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {[
- ['GET /health', '健康检查'],
- ['GET / POST /products', '查询与创建商品'],
- ['GET / PUT / DELETE /products/{id}', '读取、完整替换与删除'],
- ['GET /private', '请求头依赖与鉴权'],
- ['GET /slow · GET /upstream-demo', '异步等待、上游调用与超时'],
- ['POST /generate · GET /stream', '完整 JSON 与 SSE 流式响应'],
- ['WS /ws', 'echo、ping/pong 与关闭连接'],
- ].map(([path, result]) => (
- <TableRow key={path}>
- <TableCell>
- <code>{path}</code>
- </TableCell>
- <TableCell>{result}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- <div className="resource-strip">
- <FileCode2 size={24} />
- <div>
- <h3>可运行的 FastAPI 参考实现</h3>
- <p>含版本依赖、浏览器客户端与 41 项商品服务测试。</p>
- </div>
- <a href="/downloads/api-lab.zip" download className="secondary-link">
- 下载 ZIP
- <Download size={15} />
- </a>
- </div>
- </>
- );
- }
- function CodeBlock({
- code,
- lang = 'text',
- filename,
- }: {
- code: string;
- lang?: string;
- filename?: string;
- }) {
- const [copied, setCopied] = useState(false);
- const [error, setError] = useState(false);
- useEffect(() => {
- if (!copied && !error) return;
- const t = setTimeout(() => {
- setCopied(false);
- setError(false);
- }, 2200);
- return () => clearTimeout(t);
- }, [copied, error]);
- const copy = async () => {
- try {
- await navigator.clipboard.writeText(code);
- setCopied(true);
- } catch {
- setError(true);
- }
- };
- const highlight = (line: string) =>
- line
- .split(
- /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|#[^\n]*|\b(?:from|import|async|await|def|return|class|if|else|elif|for|in|while|try|except|finally|raise|with|as|yield|True|False|None|const|let|new|function|throw)\b)/g,
- )
- .map((part, i) => (
- <span
- key={i}
- className={
- part.startsWith('#')
- ? 'syntax-comment'
- : /^['"]/.test(part)
- ? 'syntax-string'
- : /^(from|import|async|await|def|return|class|if|else|elif|for|in|while|try|except|finally|raise|with|as|yield|True|False|None|const|let|new|function|throw)$/.test(
- part,
- )
- ? 'syntax-keyword'
- : undefined
- }
- >
- {part}
- </span>
- ));
- return (
- <div className="code-block">
- <div className="code-toolbar">
- <span>
- <FileCode2 size={14} />
- {filename || lang}
- </span>
- <button onClick={copy} className="copy-button" aria-label="复制代码">
- {copied ? <Check size={13} /> : <Copy size={13} />}
- <span aria-live="polite">
- {copied ? '已复制' : error ? '请选择代码手动复制' : '复制'}
- </span>
- </button>
- </div>
- <pre>
- <code>
- {code.split('\n').map((line, i) => (
- <span className="code-line" key={i}>
- <span className="line-number" aria-hidden="true">
- {i + 1}
- </span>
- <span>{highlight(line) || ' '}</span>
- {'\n'}
- </span>
- ))}
- </code>
- </pre>
- </div>
- );
- }
- function VideoBlock({ module, id }: { module: string; id: string }) {
- const video =
- videos[`${module}/${id}`] || (module === 'api' ? videos[id] : undefined);
- if (!video || !/^(https?:\/\/|\/(?!\/))/.test(video.url)) return null;
- return (
- <section id="video" className="video-section">
- {video.type === 'file' ? (
- <div className="video-player">
- <video controls preload="metadata" src={video.url} playsInline>
- <track
- kind="captions"
- src={video.captions}
- srcLang="zh"
- label="中文字幕"
- default={!!video.captions}
- />
- <a href={video.url}>打开视频</a>
- </video>
- <a href={video.url} target="_blank" rel="noreferrer">
- 直接打开视频
- <ExternalLink size={14} />
- </a>
- </div>
- ) : (
- <a
- href={video.url}
- target="_blank"
- rel="noreferrer"
- className="video-pending"
- >
- <VideoIcon size={23} />
- <div>
- <strong>{video.title || '观看本节视频'}</strong>
- <span>在视频平台打开</span>
- </div>
- <ArrowUpRight size={20} />
- </a>
- )}
- </section>
- );
- }
- function LessonPage({
- module: m,
- lessons,
- lesson: l,
- navigate,
- }: {
- module: ModuleInfo;
- lessons: Lesson[];
- lesson: TeachingLesson;
- navigate: Navigate;
- }) {
- const index = lessons.findIndex((x) => x.id === l.id);
- const nextModule = modules[modules.findIndex((x) => x.slug === m.slug) + 1];
- const theory = l.kind === 'theory';
- return (
- <article className="lesson-article">
- <LocalLink module={m.slug} navigate={navigate} className="back-link">
- <ArrowLeft size={14} />
- {m.name} · 全部课时
- </LocalLink>
- <div className="eyebrow">
- LESSON {l.id} / {lessons.length}
- <span />
- {l.group}
- </div>
- <h1 className="lesson-title" tabIndex={-1}>
- {l.title}
- </h1>
- <p className="lead">{l.summary}</p>
- <div className="meta-row">
- <span>{m.name}</span>
- <span>
- {theory ? '理论与面试' : l.production ? '生产系统实践' : '实战课件'}
- </span>
- <span>
- {theory
- ? '图解 · 场景推演 · 设计取舍'
- : l.demo
- ? '含交互演示'
- : '含代码与验收'}
- </span>
- </div>
- {l.intro && (
- <section id="start" className="lesson-start">
- <h2>从一个问题开始</h2>
- <p className="lesson-scenario">{l.intro.scenario}</p>
- <div className="lesson-outcome">
- <span>这次的终点</span>
- <p>{l.intro.outcome}</p>
- </div>
- <h3>开始前,确认你在这里</h3>
- <ul>
- {l.intro.startingPoint.map((item) => (
- <li key={item}>{item}</li>
- ))}
- </ul>
- <LocalLink
- module={m.slug}
- lesson={index > 0 ? lessons[index - 1].id : undefined}
- navigate={navigate}
- className="lesson-start-link"
- >
- <ArrowLeft size={14} />
- {index > 0
- ? `需要时复习上一课:${lessons[index - 1].title}`
- : '查看本模块的学习准备'}
- </LocalLink>
- <p className="lesson-reading-guide">
- {theory
- ? '先按每一步的问题作出判断,再对照推演结果和解释;完成后用课末回顾整理概念。'
- : '每次只做一个步骤,先核对结果,再读原因。结果不一致时,先排查当前步骤,再继续往下。'}
- </p>
- </section>
- )}
- <section id="objectives" className="objectives">
- <h2>这节课要完成什么</h2>
- <ul>
- {l.objectives.map((o, i) => (
- <li key={o}>
- <span>{String(i + 1).padStart(2, '0')}</span>
- {o}
- </li>
- ))}
- </ul>
- </section>
- <VideoBlock module={m.slug} id={l.id} />
- <section id="steps">
- <div className="section-heading">
- <h2>{theory ? '一步步推演场景' : '一步步完成案例'}</h2>
- <span>
- {l.steps.length} 个{theory ? '分析环节' : '步骤'}
- </span>
- </div>
- {l.steps.map((s, i) => (
- <div className="practice-step" key={`${s.title}-${i}`}>
- <div className="practice-title">
- <span>{String(i + 1).padStart(2, '0')}</span>
- <h3>{s.title}</h3>
- </div>
- <p>{s.body}</p>
- {s.code && (
- <CodeBlock code={s.code} lang={s.lang} filename={s.filename} />
- )}
- {s.expected && (
- <div className="step-observation">
- <h4>{theory ? '对照你的推演' : '你应该看到什么'}</h4>
- <p>{s.expected}</p>
- </div>
- )}
- {s.explanation && (
- <div className="step-explanation">
- <h4>{theory ? '从这里理解原因' : '刚才发生了什么'}</h4>
- <p>{s.explanation}</p>
- </div>
- )}
- </div>
- ))}
- </section>
- <section id="checks">
- <div className="section-heading">
- <h2>{theory ? '检查推演结果' : '检查学习结果'}</h2>
- <span>
- {theory ? '能否解释因果、代价与边界' : '核对响应、日志与数据'}
- </span>
- </div>
- <ul className="check-list">
- {l.checks.map((c) => (
- <li key={c}>
- <Check size={16} />
- {c}
- </li>
- ))}
- </ul>
- </section>
- <LessonDiagrams module={m.slug} lesson={l.id} />
- {l.demo === 'http' ? (
- <HttpDemo />
- ) : l.demo === 'async' ? (
- <AsyncDemo />
- ) : l.demo === 'protocol' ? (
- <ProtocolDemo />
- ) : null}
- {m.slug === 'tasks' && l.id === '09' && <TaskDeliveryDemo />}
- {l.production && (
- <section id="production" className="production-panel">
- <div className="section-heading">
- <h2>进一步验证:约束与故障</h2>
- <GitBranch size={21} />
- </div>
- <p className="learning-section-note">
- 先通过上面的学习结果检查,再做这一组实验,观察条件变化以后原来的办法是否仍然有效。
- </p>
- <div className="production-scenario">
- <span>使用场景</span>
- <p>{l.production.scenario}</p>
- </div>
- <h3>实现决策</h3>
- <ul className="decision-list">
- {l.production.decisions.map((d) => (
- <li key={d}>{d}</li>
- ))}
- </ul>
- <div className="failure-drill">
- <span>隔离环境演练</span>
- <p>{l.production.failureDrill}</p>
- </div>
- <h3>恢复验收</h3>
- <ul className="check-list">
- {l.production.acceptance.map((a) => (
- <li key={a}>
- <Check size={15} />
- {a}
- </li>
- ))}
- </ul>
- </section>
- )}
- <section id="pitfalls">
- <div className="section-heading">
- <h2>{theory ? '常见误区' : '遇到问题时查这里'}</h2>
- <span>现象 → 原因 → 处理</span>
- </div>
- <div className="pitfall-list">
- {l.pitfalls.map((p) => (
- <div className="pitfall" key={p.symptom}>
- <h3>{p.symptom}</h3>
- <p>
- <strong>原因</strong>
- {p.cause}
- </p>
- <p>
- <strong>处理</strong>
- {p.fix}
- </p>
- </div>
- ))}
- </div>
- </section>
- {l.interview && (
- <section id="interview" className="exercise interview-panel">
- <div className="section-heading">
- <h2>面试问答</h2>
- <span>先独立回答,再展开参考思路</span>
- </div>
- <h3>{l.interview.question}</h3>
- <details>
- <summary>
- 查看回答思路
- <ChevronRight size={15} />
- </summary>
- <div className="answer-text">{l.interview.answer}</div>
- </details>
- <h3 className="interview-followups">继续追问</h3>
- <ul className="decision-list">
- {l.interview.followUps.map((question) => (
- <li key={question}>{question}</li>
- ))}
- </ul>
- </section>
- )}
- <section id="exercise" className="exercise">
- <div className="section-heading">
- <h2>{theory ? '自己设计一次' : '自己试一次'}</h2>
- <span>完成后再核对答案</span>
- </div>
- <p>{l.exercise.task}</p>
- <details>
- <summary>
- 查看提示
- <ChevronRight size={15} />
- </summary>
- <p>{l.exercise.hint}</p>
- </details>
- <details>
- <summary>
- 查看参考答案
- <ChevronRight size={15} />
- </summary>
- <div className="answer-text">{l.exercise.answer}</div>
- </details>
- </section>
- <section id="concepts" className="lesson-recap">
- <div className="section-heading">
- <h2>核心概念回顾</h2>
- <span>把刚才的操作与现象整理成知识</span>
- </div>
- <p className="learning-section-note">
- {theory
- ? '回想刚才每一种选择造成的结果,再用下面的概念解释自己的判断。'
- : '回想刚才在哪一步用到了它、看到了什么结果。能结合自己的操作解释清楚,才算理解了这个概念。'}
- </p>
- {l.concepts.map((c, i) => (
- <div className="concept" key={c.title}>
- <h3>
- <span>{i + 1}.</span>
- {c.title}
- </h3>
- <p>{c.body}</p>
- </div>
- ))}
- </section>
- <section id="references">
- <div className="section-heading">
- <h2>{theory ? '延伸阅读' : '技术文档'}</h2>
- <span>
- {theory ? '原始论文与官方设计资料' : '原始定义与框架用法'}
- </span>
- </div>
- <div className="reference-links">
- {l.refs.map((r) => (
- <a key={r.url} href={r.url} target="_blank" rel="noreferrer">
- {r.label}
- <ArrowUpRight size={15} />
- </a>
- ))}
- </div>
- </section>
- {index === lessons.length - 1 && (
- <div className="overview-actions">
- <LocalLink
- module="review"
- lesson={String(
- modules.findIndex((item) => item.slug === m.slug) + 1,
- ).padStart(2, '0')}
- navigate={navigate}
- className="secondary-link"
- >
- 学完本模块,去复盘与自测
- <ArrowRight size={16} />
- </LocalLink>
- </div>
- )}
- <div className="lesson-pagination">
- {index > 0 ? (
- <LocalLink
- module={m.slug}
- lesson={lessons[index - 1].id}
- navigate={navigate}
- >
- <span>
- <ArrowLeft size={13} />
- 上一节
- </span>
- <strong>{lessons[index - 1].title}</strong>
- </LocalLink>
- ) : (
- <LocalLink module={m.slug} navigate={navigate}>
- <span>
- <ArrowLeft size={13} />
- 模块概览
- </span>
- <strong>完整学习路径</strong>
- </LocalLink>
- )}
- {index < lessons.length - 1 ? (
- <LocalLink
- module={m.slug}
- lesson={lessons[index + 1].id}
- navigate={navigate}
- className="next"
- >
- <span>
- 下一节
- <ArrowRight size={13} />
- </span>
- <strong>{lessons[index + 1].title}</strong>
- </LocalLink>
- ) : (
- <LocalLink
- module={nextModule?.slug || m.slug}
- navigate={navigate}
- className="next"
- >
- <span>
- {nextModule ? '下一模块' : '模块概览'}
- <ArrowRight size={13} />
- </span>
- <strong>{nextModule?.name || '检查完成标准'}</strong>
- </LocalLink>
- )}
- </div>
- </article>
- );
- }
|