[{"data":1,"prerenderedAt":758},["ShallowReactive",2],{"note:code\u002Fvue\u002F14-capstone":3,"site-search-catalogue":757},{"id":4,"title":5,"body":6,"description":16,"extension":749,"meta":750,"navigation":752,"path":753,"seo":754,"stem":755,"__hash__":756},"notes\u002Fsource\u002Fcode\u002Fvue\u002F14-capstone.md","14 毕业项目：课程任务管理器",{"type":7,"value":8,"toc":718},"minimark",[9,13,17,20,25,28,50,58,62,66,69,171,174,191,194,196,260,262,279,283,286,294,297,311,315,318,324,327,338,345,351,356,362,366,369,380,383,389,392,398,401,405,409,412,415,426,430,437,448,451,455,458,474,477,481,484,488,491,495,502,510,514,517,534,538,541,565,569,572,589,592,596,599,622,626,630,633,637,640,644,647,651,654,658,661,665,668,704,707,714],[10,11,5],"h1",{"id":12},"_14-毕业项目课程任务管理器",[14,15,16],"p",{},"毕业项目的目标是把前面所有知识串起来。你要做一个“课程任务管理器”：能展示课程、记录完成进度、创建学习任务、筛选任务、保存数据、模拟请求、写测试并构建。",[14,18,19],{},"本章不是只列功能，而是教你怎么从需求拆到组件、状态、路由和测试。",[21,22,24],"h2",{"id":23},"_1-项目目标","1. 项目目标",[14,26,27],{},"最终应用应该包含：",[29,30,31,35,38,41,44,47],"ul",{},[32,33,34],"li",{},"首页：学习统计和下一步建议。",[32,36,37],{},"课程页：课程列表、搜索、阶段筛选。",[32,39,40],{},"课程详情页：课程说明、完成状态。",[32,42,43],{},"任务页：新增任务、完成任务、删除任务、筛选任务。",[32,45,46],{},"异步页：模拟加载课程，显示 loading\u002Ferror\u002Fempty\u002Fsuccess。",[32,48,49],{},"设置或说明页：展示项目说明。",[14,51,52,53,57],{},"你可以参考 ",[54,55,56],"code",{},"D:\\vue-tutorial\\course-app","，但建议自己从空白项目重新做一次。",[21,59,61],{"id":60},"_2-需求拆解","2. 需求拆解",[63,64,65],"h3",{"id":65},"课程功能",[14,67,68],{},"数据：",[70,71,76],"pre",{"className":72,"code":73,"language":74,"meta":75,"style":75},"language-js shiki shiki-themes github-light github-dark","const lessons = [\n  {\n    id: 'js',\n    phase: '基础',\n    title: 'JavaScript 必备基础',\n    minutes: 80,\n    summary: '变量、类型、函数、模块和异步。'\n  }\n]\n","js","",[54,77,78,98,104,117,128,139,150,159,165],{"__ignoreMap":75},[79,80,83,87,91,94],"span",{"class":81,"line":82},"line",1,[79,84,86],{"class":85},"szBVR","const",[79,88,90],{"class":89},"sj4cs"," lessons",[79,92,93],{"class":85}," =",[79,95,97],{"class":96},"sVt8B"," [\n",[79,99,101],{"class":81,"line":100},2,[79,102,103],{"class":96},"  {\n",[79,105,107,110,114],{"class":81,"line":106},3,[79,108,109],{"class":96},"    id: ",[79,111,113],{"class":112},"sZZnC","'js'",[79,115,116],{"class":96},",\n",[79,118,120,123,126],{"class":81,"line":119},4,[79,121,122],{"class":96},"    phase: ",[79,124,125],{"class":112},"'基础'",[79,127,116],{"class":96},[79,129,131,134,137],{"class":81,"line":130},5,[79,132,133],{"class":96},"    title: ",[79,135,136],{"class":112},"'JavaScript 必备基础'",[79,138,116],{"class":96},[79,140,142,145,148],{"class":81,"line":141},6,[79,143,144],{"class":96},"    minutes: ",[79,146,147],{"class":89},"80",[79,149,116],{"class":96},[79,151,153,156],{"class":81,"line":152},7,[79,154,155],{"class":96},"    summary: ",[79,157,158],{"class":112},"'变量、类型、函数、模块和异步。'\n",[79,160,162],{"class":81,"line":161},8,[79,163,164],{"class":96},"  }\n",[79,166,168],{"class":81,"line":167},9,[79,169,170],{"class":96},"]\n",[14,172,173],{},"功能：",[29,175,176,179,182,185,188],{},[32,177,178],{},"显示课程列表。",[32,180,181],{},"搜索标题。",[32,183,184],{},"按阶段筛选。",[32,186,187],{},"标记完成。",[32,189,190],{},"查看详情。",[63,192,193],{"id":193},"任务功能",[14,195,68],{},[70,197,199],{"className":72,"code":198,"language":74,"meta":75,"style":75},"const todos = [\n  {\n    id: 'todo-1',\n    title: '完成组件通信练习',\n    course: '组件',\n    done: false\n  }\n]\n",[54,200,201,212,216,225,234,244,252,256],{"__ignoreMap":75},[79,202,203,205,208,210],{"class":81,"line":82},[79,204,86],{"class":85},[79,206,207],{"class":89}," todos",[79,209,93],{"class":85},[79,211,97],{"class":96},[79,213,214],{"class":81,"line":100},[79,215,103],{"class":96},[79,217,218,220,223],{"class":81,"line":106},[79,219,109],{"class":96},[79,221,222],{"class":112},"'todo-1'",[79,224,116],{"class":96},[79,226,227,229,232],{"class":81,"line":119},[79,228,133],{"class":96},[79,230,231],{"class":112},"'完成组件通信练习'",[79,233,116],{"class":96},[79,235,236,239,242],{"class":81,"line":130},[79,237,238],{"class":96},"    course: ",[79,240,241],{"class":112},"'组件'",[79,243,116],{"class":96},[79,245,246,249],{"class":81,"line":141},[79,247,248],{"class":96},"    done: ",[79,250,251],{"class":89},"false\n",[79,253,254],{"class":81,"line":152},[79,255,164],{"class":96},[79,257,258],{"class":81,"line":161},[79,259,170],{"class":96},[14,261,173],{},[29,263,264,267,270,273,276],{},[32,265,266],{},"新增任务。",[32,268,269],{},"切换完成。",[32,271,272],{},"删除任务。",[32,274,275],{},"全部\u002F未完成\u002F已完成筛选。",[32,277,278],{},"统计未完成数量。",[21,280,282],{"id":281},"_3-路由设计","3. 路由设计",[14,284,285],{},"推荐：",[70,287,292],{"className":288,"code":290,"language":291,"meta":75},[289],"language-text","\u002F                 首页\n\u002Flessons          课程列表\n\u002Flessons\u002F:id      课程详情\n\u002Ftodos            任务管理\n\u002Fasync            异步请求演示\n\u002Fabout            项目说明\n","text",[54,293,290],{"__ignoreMap":75},[14,295,296],{},"路由职责：",[29,298,299,302,308],{},[32,300,301],{},"URL 表示当前页面。",[32,303,304,307],{},[54,305,306],{},":id"," 表示详情页参数。",[32,309,310],{},"搜索和筛选可以用查询参数，也可以先用组件状态，进阶时再同步到 URL。",[21,312,314],{"id":313},"_4-组件拆分","4. 组件拆分",[14,316,317],{},"建议组件：",[70,319,322],{"className":320,"code":321,"language":291,"meta":75},[289],"AppHeader\nLessonCard\nLessonList\nLessonFilter\nTodoForm\nTodoItem\nTodoStats\nEmptyState\nBaseButton\nBasePanel\n",[54,323,321],{"__ignoreMap":75},[14,325,326],{},"每个组件要回答：",[29,328,329,332,335],{},[32,330,331],{},"它负责显示什么？",[32,333,334],{},"它需要哪些 props？",[32,336,337],{},"它会 emit 哪些事件？",[14,339,340,341,344],{},"例如 ",[54,342,343],{},"TodoItem","：",[70,346,349],{"className":347,"code":348,"language":291,"meta":75},[289],"职责：显示单条任务，提供完成和删除操作。\nprops：todo。\nemits：toggle(id)、remove(id)。\n",[54,350,348],{"__ignoreMap":75},[14,352,340,353,344],{},[54,354,355],{},"TodoForm",[70,357,360],{"className":358,"code":359,"language":291,"meta":75},[289],"职责：收集任务标题和所属阶段。\nprops：可选的阶段列表。\nemits：submit(payload)。\n内部状态：输入框文字、错误信息。\n",[54,361,359],{"__ignoreMap":75},[21,363,365],{"id":364},"_5-状态设计","5. 状态设计",[14,367,368],{},"组件内状态：",[29,370,371,374,377],{},[32,372,373],{},"任务表单输入框文字。",[32,375,376],{},"当前弹窗开关。",[32,378,379],{},"当前页面局部筛选，如果不需要共享。",[14,381,382],{},"Pinia 状态：",[70,384,387],{"className":385,"code":386,"language":291,"meta":75},[289],"useCourseStore\n  completedLessonIds\n  progressPercent\n  toggleLesson(id)\n  isCompleted(id)\n\nuseTodoStore\n  todos\n  filter\n  filteredTodos\n  remainingCount\n  addTodo(payload)\n  toggleTodo(id)\n  removeTodo(id)\n",[54,388,386],{"__ignoreMap":75},[14,390,391],{},"组合式函数：",[70,393,396],{"className":394,"code":395,"language":291,"meta":75},[289],"useLocalStorage\nuseFetch\nuseCounter\n",[54,397,395],{"__ignoreMap":75},[14,399,400],{},"不要把所有状态都塞进一个 store。按业务领域拆。",[21,402,404],{"id":403},"_6-开发顺序","6. 开发顺序",[63,406,408],{"id":407},"第一步静态页面","第一步：静态页面",[14,410,411],{},"先写 HTML 结构和样式，不要急着做交互。",[14,413,414],{},"目标：",[29,416,417,420,423],{},[32,418,419],{},"能看到首页布局。",[32,421,422],{},"能看到课程卡片。",[32,424,425],{},"能看到任务表单和任务列表。",[63,427,429],{"id":428},"第二步局部交互","第二步：局部交互",[14,431,432,433,436],{},"先在页面组件里用 ",[54,434,435],{},"ref"," 实现：",[29,438,439,441,443,445],{},[32,440,266],{},[32,442,272],{},[32,444,269],{},[32,446,447],{},"筛选任务。",[14,449,450],{},"这一步不要急着 Pinia。先确认数据和交互正确。",[63,452,454],{"id":453},"第三步拆组件","第三步：拆组件",[14,456,457],{},"把重复 UI 拆出去：",[29,459,460,465,469],{},[32,461,462,464],{},[54,463,343],{},"。",[32,466,467,464],{},[54,468,355],{},[32,470,471,464],{},[54,472,473],{},"LessonCard",[14,475,476],{},"拆完后检查 props 和 emit 是否清晰。",[63,478,480],{"id":479},"第四步迁移到-pinia","第四步：迁移到 Pinia",[14,482,483],{},"当多个页面都需要任务或课程进度时，再迁移到 store。",[63,485,487],{"id":486},"第五步加-router","第五步：加 Router",[14,489,490],{},"配置页面，处理详情参数和 404。",[63,492,494],{"id":493},"第六步加持久化","第六步：加持久化",[14,496,497,498,501],{},"用 ",[54,499,500],{},"useLocalStorage"," 保存：",[29,503,504,507],{},[32,505,506],{},"任务列表。",[32,508,509],{},"课程完成 id。",[63,511,513],{"id":512},"第七步加异步请求","第七步：加异步请求",[14,515,516],{},"用 mock API 模拟课程加载：",[29,518,519,522,525,528,531],{},[32,520,521],{},"loading。",[32,523,524],{},"error。",[32,526,527],{},"empty。",[32,529,530],{},"success。",[32,532,533],{},"retry。",[63,535,537],{"id":536},"第八步写测试","第八步：写测试",[14,539,540],{},"至少测试：",[29,542,543,549,555],{},[32,544,545,546,464],{},"一个组合式函数，例如 ",[54,547,548],{},"useCounter",[32,550,551,552,554],{},"一个组件事件，例如 ",[54,553,343],{}," emit。",[32,556,557,558,561,562,464],{},"一个 store action，例如 ",[54,559,560],{},"addTodo"," 或 ",[54,563,564],{},"toggleTodo",[63,566,568],{"id":567},"第九步构建验证","第九步：构建验证",[14,570,571],{},"运行：",[70,573,577],{"className":574,"code":575,"language":576,"meta":75,"style":75},"language-powershell shiki shiki-themes github-light github-dark","npm run test\nnpm run build\n","powershell",[54,578,579,584],{"__ignoreMap":75},[79,580,581],{"class":81,"line":82},[79,582,583],{},"npm run test\n",[79,585,586],{"class":81,"line":100},[79,587,588],{},"npm run build\n",[14,590,591],{},"再在浏览器里手动验证关键流程。",[21,593,595],{"id":594},"_7-验收标准","7. 验收标准",[14,597,598],{},"你完成后应该能做到：",[29,600,601,604,607,610,613,616,619],{},[32,602,603],{},"不看答案解释 props 和 emit 的数据流。",[32,605,606],{},"说清楚哪些状态放组件内，哪些放 Pinia。",[32,608,609],{},"解释 Router 和 Pinia 的边界。",[32,611,612],{},"请求失败时页面有错误和重试。",[32,614,615],{},"刷新页面后任务和进度保留。",[32,617,618],{},"测试能证明核心逻辑。",[32,620,621],{},"构建成功。",[21,623,625],{"id":624},"_8-常见项目陷阱","8. 常见项目陷阱",[63,627,629],{"id":628},"陷阱一一开始就写-store","陷阱一：一开始就写 store",[14,631,632],{},"如果你还没想清楚功能，过早写 store 会把简单问题复杂化。先用局部状态跑通。",[63,634,636],{"id":635},"陷阱二组件拆得太碎","陷阱二：组件拆得太碎",[14,638,639],{},"拆组件不是目标。清晰边界才是目标。",[63,641,643],{"id":642},"陷阱三只写成功状态","陷阱三：只写成功状态",[14,645,646],{},"请求页必须有 loading、error、empty、success。",[63,648,650],{"id":649},"陷阱四没有测试就大改","陷阱四：没有测试就大改",[14,652,653],{},"没有测试时，改一个地方很容易弄坏另一个地方。至少给核心逻辑补测试。",[63,655,657],{"id":656},"陷阱五不会复述","陷阱五：不会复述",[14,659,660],{},"如果你只能复制代码，却说不清为什么这样写，说明还没掌握。完成项目后必须能复述设计。",[21,662,664],{"id":663},"_9-最终复盘问题","9. 最终复盘问题",[14,666,667],{},"写完项目后回答：",[669,670,671,674,677,680,683,686,689,692,698,701],"ol",{},[32,672,673],{},"你的路由有哪些？每个页面负责什么？",[32,675,676],{},"你的组件有哪些？每个组件的 props 和 emits 是什么？",[32,678,679],{},"哪些状态在组件内？为什么？",[32,681,682],{},"哪些状态在 Pinia？为什么？",[32,684,685],{},"哪些逻辑抽成了组合式函数？",[32,687,688],{},"请求失败时用户看到什么？",[32,690,691],{},"你的测试验证了哪些行为？",[32,693,694,697],{},[54,695,696],{},"npm run build"," 生成了什么？",[32,699,700],{},"如果要加登录功能，你会新增哪个 store？",[32,702,703],{},"如果任务来自真实后端，你会把 API 逻辑放哪里？",[21,705,706],{"id":706},"本章练习",[14,708,709,710,713],{},"按 ",[54,711,712],{},"D:\\vue-tutorial\\projects\\todo-workshop\\README.md"," 的阶段完成项目。完成后，把上面的复盘问题写进你自己的项目 README。",[715,716,717],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":75,"searchDepth":100,"depth":100,"links":719},[720,721,725,726,727,728,739,740,747,748],{"id":23,"depth":100,"text":24},{"id":60,"depth":100,"text":61,"children":722},[723,724],{"id":65,"depth":106,"text":65},{"id":193,"depth":106,"text":193},{"id":281,"depth":100,"text":282},{"id":313,"depth":100,"text":314},{"id":364,"depth":100,"text":365},{"id":403,"depth":100,"text":404,"children":729},[730,731,732,733,734,735,736,737,738],{"id":407,"depth":106,"text":408},{"id":428,"depth":106,"text":429},{"id":453,"depth":106,"text":454},{"id":479,"depth":106,"text":480},{"id":486,"depth":106,"text":487},{"id":493,"depth":106,"text":494},{"id":512,"depth":106,"text":513},{"id":536,"depth":106,"text":537},{"id":567,"depth":106,"text":568},{"id":594,"depth":100,"text":595},{"id":624,"depth":100,"text":625,"children":741},[742,743,744,745,746],{"id":628,"depth":106,"text":629},{"id":635,"depth":106,"text":636},{"id":642,"depth":106,"text":643},{"id":649,"depth":106,"text":650},{"id":656,"depth":106,"text":657},{"id":663,"depth":100,"text":664},{"id":706,"depth":100,"text":706},"md",{"order":751},14,true,"\u002Fsource\u002Fcode\u002Fvue\u002F14-capstone",{"title":5,"description":16},"source\u002Fcode\u002Fvue\u002F14-capstone","aDhGmxaS0iM9HoTXSQJmgfmHz5jEFJPgjC4vkoWqkKM",null,1784032890131]