refactor: split HomeFeatures.vue into sub-components for maintainability

This commit is contained in:
sanbuphy
2026-05-14 11:09:48 +08:00
parent 59de919932
commit 3f169f543d
9 changed files with 3117 additions and 3041 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,324 @@
<script setup>
import { inject, ref, onMounted, onUnmounted } from 'vue'
import { withBase } from 'vitepress'
import './HomeSection.css'
const t = inject('t')
const appendixWrapper = ref(null)
const totalPages = ref(1)
const currentPage = ref(0)
const updatePagination = () => {
if (appendixWrapper.value) {
const { scrollLeft, clientWidth, scrollWidth } = appendixWrapper.value
if (scrollWidth <= clientWidth + 5) {
totalPages.value = 1
currentPage.value = 0
} else {
totalPages.value = Math.ceil(scrollWidth / clientWidth)
currentPage.value = Math.round(scrollLeft / clientWidth)
}
}
}
const onAppendixScroll = () => {
if (!appendixWrapper.value) return
const { scrollLeft, clientWidth } = appendixWrapper.value
const newPage = Math.round(scrollLeft / clientWidth)
if (currentPage.value !== newPage) {
currentPage.value = newPage
}
}
const scrollToPage = (pageIndex) => {
if (appendixWrapper.value) {
const width = appendixWrapper.value.clientWidth
appendixWrapper.value.scrollTo({
left: pageIndex * width,
behavior: 'smooth'
})
}
}
const scrollAppendixByPage = (direction) => {
const nextPage = Math.min(
totalPages.value - 1,
Math.max(0, currentPage.value + direction)
)
scrollToPage(nextPage)
}
onMounted(() => {
if (appendixWrapper.value) {
appendixWrapper.value.addEventListener('scroll', onAppendixScroll)
updatePagination()
window.addEventListener('resize', updatePagination)
}
})
onUnmounted(() => {
if (appendixWrapper.value) {
appendixWrapper.value.removeEventListener('scroll', onAppendixScroll)
}
window.removeEventListener('resize', updatePagination)
})
</script>
<template>
<section
id="appendix"
class="section-container section-appendix"
>
<div class="section-header">
<h2 class="section-category">
{{ t.appendix.cat }}
</h2>
<h3
class="section-headline"
v-html="t.appendix.title"
/>
<p class="section-sub">
{{ t.appendix.sub }}
</p>
</div>
<div
ref="appendixWrapper"
class="appendix-scroll-wrapper"
>
<div class="appendix-track">
<a
v-for="(card, index) in t.appendix.cards"
:key="index"
:href="withBase(card.link)"
class="appendix-card"
>
<span class="appendix-emoji">{{
['🤖', '🧠', '🎨', '🚀', '⚙️', '💾', '🛠️', '🌐'][index] || '📚'
}}</span>
<span class="appendix-title">{{ card.title }}</span>
</a>
</div>
</div>
<div
v-if="totalPages > 1"
class="appendix-scroll-hint"
>
<div class="appendix-progress-track">
<div
class="appendix-progress-thumb"
:style="{
width: `${100 / totalPages}%`,
transform: `translateX(${currentPage * 100}%)`
}"
/>
</div>
<div class="appendix-scroll-actions">
<button
class="appendix-arrow-btn"
:class="{ disabled: currentPage === 0 }"
:disabled="currentPage === 0"
aria-label="向左滑动"
@click="scrollAppendixByPage(-1)"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M11.5 5.5L7 10L11.5 14.5"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<button
class="appendix-arrow-btn"
:class="{ disabled: currentPage >= totalPages - 1 }"
:disabled="currentPage >= totalPages - 1"
aria-label="向右滑动"
@click="scrollAppendixByPage(1)"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M8.5 5.5L13 10L8.5 14.5"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</div>
</section>
</template>
<style scoped>
.appendix-scroll-wrapper {
overflow-x: auto;
scroll-snap-type: x mandatory;
margin: 0 -20px;
padding: 0 20px 12px;
scrollbar-width: none;
-ms-overflow-style: none;
overscroll-behavior-x: contain;
}
.appendix-scroll-wrapper::-webkit-scrollbar {
display: none;
}
.appendix-track {
display: flex;
align-items: flex-start;
gap: 40px;
width: max-content;
}
.appendix-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 12px;
text-decoration: none !important;
color: inherit !important;
background: transparent;
padding: 0;
border: 0;
box-shadow: none;
scroll-snap-align: start;
width: 120px;
min-height: 120px;
transition: transform 0.25s ease;
text-align: center;
}
.appendix-card:hover {
transform: scale(1.03);
}
.appendix-emoji {
font-size: 52px;
line-height: 1;
display: block;
}
.appendix-title {
font-weight: 600;
color: #3c3c43;
margin: 0;
font-size: 14px;
line-height: 1.35;
letter-spacing: -0.01em;
white-space: normal;
}
.dark .appendix-title {
color: var(--vp-c-text-1);
}
.appendix-scroll-hint {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 18px;
margin-top: 20px;
min-height: 40px;
}
.appendix-progress-track {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 160px;
height: 4px;
border-radius: 999px;
background: rgba(60, 60, 67, 0.08);
overflow: hidden;
}
.appendix-progress-thumb {
height: 100%;
border-radius: inherit;
background: rgba(60, 60, 67, 0.28);
transition: transform 0.25s ease;
}
.appendix-scroll-actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
margin-right: 56px;
}
.appendix-arrow-btn {
width: 38px;
height: 38px;
border-radius: 999px;
border: 1px solid rgba(60, 60, 67, 0.05);
background: rgba(60, 60, 67, 0.05);
color: rgba(60, 60, 67, 0.62);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition:
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.appendix-arrow-btn:hover {
background: rgba(255, 255, 255, 0.78);
border-color: rgba(60, 60, 67, 0.08);
color: rgba(60, 60, 67, 0.74);
transform: scale(1.04);
}
.appendix-arrow-btn.disabled,
.appendix-arrow-btn:disabled {
opacity: 0.42;
cursor: default;
transform: none;
}
.appendix-arrow-btn.disabled:hover,
.appendix-arrow-btn:disabled:hover {
background: rgba(60, 60, 67, 0.05);
color: rgba(60, 60, 67, 0.62);
}
.section-appendix {
background: transparent;
border-radius: 0;
padding-top: 64px;
padding-bottom: 64px;
}
.dark .section-appendix {
background: transparent;
}
@media (max-width: 768px) {
.section-appendix {
padding-top: 42px;
padding-bottom: 42px;
}
}
</style>
@@ -0,0 +1,487 @@
<script setup>
import { computed, inject } from 'vue'
import { withBase } from 'vitepress'
const props = defineProps({
isCjkLocale: Boolean
})
const t = inject('t')
const appleFooterInfo = computed(() => {
const locale = t.value._locale || 'zh-cn'
const content = {
'zh-cn': {
notes: [
'1. 学习路径与章节内容会持续更新,显示内容以当前页面为准。',
'2. 示例项目与截图用于教学演示,可能与后续版本界面存在差异。',
'3. 部分章节链接会随着课程迭代调整,建议优先从首页导航进入最新路径。'
],
breadcrumbPrefix: 'Easy-Vibe',
breadcrumbCurrent: '学习导航',
columns: [
{
title: '学习与导航',
links: ['零基础入门', '初中级开发', '高级开发', '附录', '学习地图', '课程总览']
},
{
title: '学习支持',
links: ['常见问题', '学习建议', '章节勘误', '版本更新']
},
{
title: '项目资源',
links: ['GitHub 仓库', '开源协议', '提交 Issue', '贡献指南']
},
{
title: '社区',
links: ['学习社群', '讨论区', '课程反馈']
},
{
title: '关于 Easy-Vibe',
links: ['项目介绍', '更新日志', '联系我们']
}
],
more: '更多学习方式:访问',
moreLink: 'GitHub 仓库',
moreTail: ',获取更新与交流信息。',
copyright: 'Copyright © 2026 Easy-Vibe. 保留所有权利。',
policies: ['隐私政策', '使用条款', '网站地图']
},
en: {
notes: [
'1. Learning paths and chapters are continuously updated.',
'2. Screenshots and demo projects are for educational illustration.',
'3. Some chapter links may change as the course evolves.',
'4. The page is optimized for modern desktop browsers and responsive layouts.'
],
breadcrumbPrefix: 'Easy-Vibe',
breadcrumbCurrent: 'Learning Navigation',
columns: [
{
title: 'Explore',
links: ['Foundations', 'Junior/Mid Dev', 'Senior Dev', 'Appendix', 'Learning Map', 'Course Outline']
},
{
title: 'Support',
links: ['FAQ', 'Learning Tips', 'Errata', 'Release Notes']
},
{
title: 'Resources',
links: ['GitHub Repository', 'License', 'Report Issue', 'Contribution Guide']
},
{
title: 'Community',
links: ['Community', 'Discussions', 'Feedback']
},
{
title: 'About Easy-Vibe',
links: ['Overview', 'Changelog', 'Contact']
}
],
more: 'More ways to learn: visit',
moreLink: 'GitHub Repository',
moreTail: ' for updates and community discussions.',
copyright: 'Copyright © 2026 Easy-Vibe. All rights reserved.',
policies: ['Privacy Policy', 'Terms of Use', 'Sitemap']
}
}
return content[locale] || content.en
})
const footerRepositoryLink = 'https://github.com/datawhalechina/easy-vibe'
const footerPolicyLinkMap = {
'隐私政策': '#',
'使用条款': '#',
'网站地图': '#',
'Privacy Policy': '#',
'Terms of Use': '#',
'Sitemap': '#'
}
const footerColumnLinkMap = {
'零基础入门': '/zh-cn/stage-1/',
'初中级开发': '/zh-cn/stage-2/',
'高级开发': '/zh-cn/stage-3/',
'附录': '/zh-cn/appendix/',
'学习地图': '/zh-cn/stage-1/learning-map/',
'课程总览': '/zh-cn/stage-1/',
'GitHub 仓库': 'https://github.com/datawhalechina/easy-vibe',
'Foundations': '/en/stage-1/',
'Junior/Mid Dev': '/en/stage-2/',
'Senior Dev': '/en/stage-3/',
'Appendix': '/en/appendix/',
'Learning Map': '/en/stage-1/learning-map/',
'Course Outline': '/en/stage-1/',
'GitHub Repository': 'https://github.com/datawhalechina/easy-vibe',
'Overview': '/en/guide/introduction',
'Changelog': 'https://github.com/datawhalechina/easy-vibe/releases'
}
const getFooterLink = (label) => {
return footerColumnLinkMap[label] || '#'
}
const getPolicyLink = (label) => {
return footerPolicyLinkMap[label] || '#'
}
const resolveFooterHref = (link) => {
if (link.startsWith('http://') || link.startsWith('https://')) {
return link
}
return withBase(link)
}
</script>
<template>
<div class="footer-callout">
<h2 v-html="t.footer.title" />
<p>{{ t.footer.desc }}</p>
<a
class="buy-btn large"
:href="withBase('/zh-cn/stage-1/learning-map/')"
>{{ t.footer.btn }}</a>
</div>
<div
class="apple-site-footer"
:class="{ 'is-cjk-locale': isCjkLocale }"
>
<div class="apple-site-footer-inner">
<div class="apple-footer-breadcrumb">
<span></span>
<span></span>
<span>{{ appleFooterInfo.breadcrumbPrefix }}</span>
<span></span>
<span>{{ appleFooterInfo.breadcrumbCurrent }}</span>
</div>
<div class="apple-footer-notes">
<p
v-for="(item, idx) in appleFooterInfo.notes"
:key="idx"
>
{{ item }}
</p>
</div>
<div class="apple-footer-grid">
<div
v-for="(column, index) in appleFooterInfo.columns"
:key="index"
class="apple-footer-column"
>
<h4>{{ column.title }}</h4>
<a
v-for="(link, linkIndex) in column.links"
:key="linkIndex"
:href="resolveFooterHref(getFooterLink(link))"
>
{{ link }}
</a>
</div>
</div>
<div class="apple-footer-more">
{{ appleFooterInfo.more }}
<a :href="footerRepositoryLink">{{ appleFooterInfo.moreLink }}</a>
{{ appleFooterInfo.moreTail }}
</div>
<div class="apple-footer-bottom">
<p>{{ appleFooterInfo.copyright }}</p>
<div class="apple-footer-policy">
<a
v-for="(policy, policyIndex) in appleFooterInfo.policies"
:key="policyIndex"
:href="resolveFooterHref(getPolicyLink(policy))"
>
{{ policy }}
</a>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.footer-callout {
text-align: center;
padding: 92px 20px;
background: #fff;
margin: 0 40px 64px;
border-radius: 40px;
}
.dark .footer-callout {
background: var(--vp-c-bg-soft);
}
.footer-callout h2 {
font-size: 62px;
font-weight: 700;
margin-bottom: 20px;
line-height: 1.08;
letter-spacing: -0.03em;
color: #1d1d1f;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.footer-callout p {
color: #6e6e73;
font-size: 20px;
margin-bottom: 18px;
}
.dark .footer-callout h2 {
color: var(--vp-c-text-1);
}
.dark .footer-callout p {
color: var(--vp-c-text-2);
}
.apple-site-footer {
max-width: 1060px;
margin: 0 auto 56px;
padding: 0 40px;
}
.apple-site-footer-inner {
border-top: 1px solid #d2d2d7;
color: #6e6e73;
font-size: 12px;
}
.apple-footer-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
color: #6e6e73;
font-size: 12px;
padding-top: 12px;
}
.apple-site-footer.is-cjk-locale .apple-footer-breadcrumb {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
letter-spacing: 0.02em;
}
.apple-footer-notes {
padding-top: 18px;
}
.apple-footer-notes p {
margin: 0 0 8px;
line-height: 1.45;
color: #86868b;
}
.apple-site-footer.is-cjk-locale .apple-footer-notes p {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.88;
letter-spacing: 0.03em;
font-weight: 400;
color: #7d7d83;
}
.apple-footer-grid {
margin-top: 18px;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 22px;
}
.apple-footer-column h4 {
margin: 0 0 10px;
color: #1d1d1f;
font-size: 12px;
font-weight: 600;
}
.apple-site-footer.is-cjk-locale .apple-footer-column h4 {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.45;
letter-spacing: 0.025em;
}
.apple-footer-column a {
display: block;
color: #424245;
margin-bottom: 8px;
font-size: 12px;
line-height: 1.25;
}
.apple-site-footer.is-cjk-locale .apple-footer-column a {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.72;
letter-spacing: 0.02em;
margin-bottom: 9px;
}
.apple-footer-column a:hover {
color: #0066cc;
}
.apple-footer-more {
margin-top: 18px;
border-top: 1px solid #d2d2d7;
padding-top: 14px;
color: #6e6e73;
}
.apple-site-footer.is-cjk-locale .apple-footer-more {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.72;
letter-spacing: 0.02em;
}
.apple-footer-more a {
color: #0066cc;
}
.apple-footer-bottom {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid #d2d2d7;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.apple-footer-bottom p {
margin: 0;
color: #86868b;
}
.apple-site-footer.is-cjk-locale .apple-footer-bottom p {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.55;
letter-spacing: 0.02em;
}
.apple-footer-policy {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.apple-footer-policy a {
color: #424245;
}
.apple-footer-policy a:hover {
color: #0066cc;
}
.apple-site-footer.is-cjk-locale .apple-footer-policy a {
font-family:
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC',
sans-serif;
font-size: 13px;
line-height: 1.55;
letter-spacing: 0.02em;
}
@media (min-width: 1024px) {
.apple-site-footer {
max-width: 996px;
padding: 0 24px;
}
.apple-site-footer-inner {
font-size: 11px;
}
.apple-footer-notes p {
font-size: 11px;
line-height: 1.38;
margin-bottom: 6px;
}
.apple-footer-grid {
grid-template-columns: 1.2fr repeat(4, minmax(0, 1fr));
gap: 24px;
}
.apple-footer-column h4 {
font-size: 11px;
margin-bottom: 8px;
}
.apple-footer-column a {
font-size: 11px;
margin-bottom: 7px;
}
.apple-site-footer.is-cjk-locale .site-footer-inner {
font-size: 13px;
}
.apple-site-footer.is-cjk-locale .apple-footer-notes p {
font-size: 13px;
margin-bottom: 7px;
}
.apple-site-footer.is-cjk-locale .apple-footer-column h4 {
font-size: 13px;
}
.apple-site-footer.is-cjk-locale .apple-footer-column a {
font-size: 13px;
margin-bottom: 8px;
}
}
@media (max-width: 768px) {
.footer-callout {
margin: 0 16px 40px;
border-radius: 28px;
}
.footer-callout h2 {
font-size: 38px;
}
.footer-callout p {
font-size: 17px;
}
.apple-site-footer {
padding: 0 16px;
}
.apple-footer-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px 14px;
}
.apple-footer-bottom {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,204 @@
import stage2LovartCover from '../../../../zh-cn/stage-2/frontend/lovart-assets/images/image1.png'
import stage2FigmaCover from '../../../../zh-cn/stage-2/frontend/figma-mastergo/images/image8.png'
import stage2DesignToCodeCover from '../../../../zh-cn/stage-2/frontend/design-to-code/images/image42.png'
import stage2SupabaseCover from '../../../../zh-cn/stage-2/backend/database-supabase/images/image1.png'
import stage2ZeaburCover from '../../../../zh-cn/stage-2/backend/zeabur-deployment/images/image1.png'
import stage2DifyCover from '../../../../zh-cn/stage-2/ai-capabilities/dify-knowledge-base/images/image1.png'
import stage3ElectronCover from '../../../../zh-cn/stage-3/cross-platform/electron-voice-to-text/images/image3.png'
import stage3AgentTeamsCover from '../../../../zh-cn/stage-3/core-skills/agent-teams/images/home-cover.svg'
import stage3LongRunningCover from '../../../../zh-cn/stage-3/core-skills/long-running-tasks/images/home-cover.svg'
import stage3PersonalBrandCover from '../../../../zh-cn/stage-3/personal-brand/personal-website-blog/images/image1.png'
export const locales = [
{ code: 'zh-cn', text: '简体中文' },
{ code: 'en', text: 'English' },
{ code: 'ja-jp', text: '日本語' },
{ code: 'zh-tw', text: '繁體中文' },
{ code: 'ko-kr', text: '한국어' },
{ code: 'es-es', text: 'Español' },
{ code: 'fr-fr', text: 'Français' },
{ code: 'de-de', text: 'Deutsch' },
{ code: 'ar-sa', text: 'العربية' },
{ code: 'vi-vn', text: 'Tiếng Việt' }
]
export const stage1Cards = [
{
title: 'AI 产品经理',
desc: '从想法到高保真原型,你只需要会说话。',
sub: '适合非技术背景',
color: 'linear-gradient(135deg, #FF9A9E 0%, #FECFEF 99%, #FECFEF 100%)',
icon: '🎨',
link: '/zh-cn/stage-1/learning-map/'
},
{
title: '游戏化入门',
desc: '通过制作贪吃蛇、俄罗斯方块,打破对代码的恐惧。',
sub: '边玩边学',
color: 'linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%)',
icon: '🎮',
link: '/zh-cn/stage-1/ai-capabilities-through-games/'
},
{
title: 'Vibe Coding',
desc: '掌握 AI 时代的编程核心:提示词工程与上下文管理。',
sub: '核心心法',
color: 'linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%)',
icon: '💡',
link: '/zh-cn/stage-1/introduction-to-ai-ide/'
}
]
export const stage2Cards = [
{
imageColor: '#E0C3FC',
image: stage2LovartCover,
imageAlt: 'Lovart 素材生产 Agent 界面截图',
link: '/zh-cn/stage-2/frontend/lovart-assets/'
},
{
imageColor: '#D8C4F8',
image: stage2FigmaCover,
imageAlt: 'Figma 与 MasterGo 设计工具截图',
link: '/zh-cn/stage-2/frontend/figma-mastergo/'
},
{
imageColor: '#C7DDFB',
image: stage2DesignToCodeCover,
imageAlt: '设计稿转代码示意截图',
link: '/zh-cn/stage-2/frontend/design-to-code/'
},
{
imageColor: '#8EC5FC',
image: stage2SupabaseCover,
imageAlt: 'Supabase 数据库控制台截图',
link: '/zh-cn/stage-2/backend/database-supabase/'
},
{
imageColor: '#96E6A1',
image: stage2ZeaburCover,
imageAlt: 'Zeabur 部署流程截图',
link: '/zh-cn/stage-2/backend/zeabur-deployment/'
},
{
imageColor: '#A7F3D0',
image: stage2DifyCover,
imageAlt: 'Dify 知识库工作台截图',
link: '/zh-cn/stage-2/ai-capabilities/dify-knowledge-base/'
}
]
export const stage3Cards = [
{
title: '跨平台桌面应用',
desc: '用 Electron 做语音转文字桌面程序,一次开发同时跑在 Windows、macOS、Linux。',
tag: 'Stage 3',
visualType: 'phone',
image: stage3ElectronCover,
imageAlt: 'Electron 语音转文字桌面应用预览图',
link: '/zh-cn/stage-3/cross-platform/electron-voice-to-text/'
},
{
title: 'AI 智能体团队',
desc: '用 Claude Agent Teams 组建 AI 开发小队,多代理协作完成大型任务。',
tag: 'Advanced',
visualType: 'ai',
image: stage3AgentTeamsCover,
imageAlt: 'Claude Agent Teams 协作流程封面图',
link: '/zh-cn/stage-3/core-skills/agent-teams/'
},
{
title: '长效稳定执行',
desc: '用循环脚本和 Ralph 插件管理长时间任务,让 Claude Code 过夜稳定跑完工作。',
tag: 'Architecture',
visualType: 'arch',
image: stage3LongRunningCover,
imageAlt: 'Claude Code 长时间执行与循环任务封面图',
link: '/zh-cn/stage-3/core-skills/long-running-tasks/'
},
{
title: '个人品牌与输出',
desc: '搭建个人网站与技术博客,让你的项目和经验长期沉淀并被更多人看到。',
tag: 'Brand',
visualType: 'brand',
image: stage3PersonalBrandCover,
imageAlt: '个人网站与学术博客示例截图',
imageClass: 'prod-image--personal-brand',
link: '/zh-cn/stage-3/personal-brand/personal-website-blog/'
}
]
export const appendixCards = [
{
title: '人工智能',
desc: 'LLM、Agent、RAG,深入 AI 底层原理。',
tag: 'AI',
link: '/zh-cn/appendix/8-artificial-intelligence/ai-history'
},
{
title: '提示词工程',
desc: '掌握与 AI 高效对话的技巧,解锁潜力。',
tag: 'AI',
link: '/zh-cn/appendix/8-artificial-intelligence/prompt-engineering'
},
{
title: '大语言模型',
desc: '深入浅出解析 LLM 的工作原理与应用。',
tag: 'AI',
link: '/zh-cn/appendix/8-artificial-intelligence/llm-principles'
},
{
title: 'Agent 智能体',
desc: '探索具备自主决策与执行能力的 AI 架构。',
tag: 'AI',
link: '/zh-cn/appendix/8-artificial-intelligence/ai-agents'
},
{
title: '前端基础',
desc: 'HTML/CSS/JS 三大基石,入门必修课。',
tag: 'Frontend',
link: '/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive'
},
{
title: '前端进化史',
desc: '了解前端技术栈演变,把握发展趋势。',
tag: 'Frontend',
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks'
},
{
title: '后端架构',
desc: '从单体到微服务,探索架构演进之路。',
tag: 'Backend',
link: '/zh-cn/appendix/4-server-and-backend/backend-layered-architecture'
},
{
title: '后端语言',
desc: '对比主流后端语言特性,选择最佳技术栈。',
tag: 'Backend',
link: '/zh-cn/appendix/4-server-and-backend/backend-languages'
},
{
title: '数据库原理',
desc: '理解数据库核心原理,掌握数据存储艺术。',
tag: 'Database',
link: '/zh-cn/appendix/5-data/database-fundamentals'
},
{
title: 'API 设计',
desc: 'API 接口设计与开发的基础知识。',
tag: 'API',
link: '/zh-cn/appendix/4-server-and-backend/api-intro'
},
{
title: 'Git 版本控制',
desc: '深入理解 Git 原理与高级用法。',
tag: 'General',
link: '/zh-cn/appendix/2-development-tools/git-version-control'
},
{
title: '计算机网络',
desc: '网络协议与通信原理的基础知识。',
tag: 'General',
link: '/zh-cn/appendix/1-computer-fundamentals/computer-networks'
}
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,88 @@
.section-container {
max-width: 1280px;
margin: 0 auto 96px;
padding: 0 40px;
}
.section-header {
margin-bottom: 44px;
}
.section-category {
font-size: 24px;
font-weight: 700;
margin-bottom: 14px;
border: none;
padding: 0;
color: #1d1d1f;
letter-spacing: -0.024em;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.section-headline {
font-size: 64px;
line-height: 1.08;
font-weight: 700;
letter-spacing: -0.034em;
margin-bottom: 12px;
color: #1d1d1f;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.section-sub {
font-size: 21px;
line-height: 1.4;
font-weight: 400;
letter-spacing: -0.01em;
color: #6e6e73;
max-width: 760px;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'PingFang SC',
sans-serif;
}
.dark .section-category,
.dark .section-headline {
color: var(--vp-c-text-1);
}
.dark .section-sub {
color: var(--vp-c-text-2);
}
a {
text-decoration: none;
color: inherit;
}
:is(.feature-card, .comm-card, .prod-card, .appendix-card, .buy-btn) {
border-bottom: none !important;
outline: none !important;
-webkit-tap-highlight-color: transparent;
}
:is(
.feature-card,
.comm-card,
.prod-card,
.appendix-card,
.buy-btn
):is(:hover, :focus, :focus-visible, :active) {
border-bottom-color: transparent !important;
text-decoration: none !important;
outline: none !important;
}
.highlight {
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.section-headline {
font-size: 42px;
}
}
@@ -0,0 +1,138 @@
<script setup>
import { inject } from 'vue'
import { withBase } from 'vitepress'
import { stage1Cards } from './HomeData'
import './HomeSection.css'
const t = inject('t')
</script>
<template>
<section id="pm" class="section-container section-pm">
<div class="section-header">
<h2 class="section-category">
{{ t.stage1.cat }}
</h2>
<h3
class="section-headline"
v-html="t.stage1.title"
/>
<p class="section-sub">
{{ t.stage1.sub }}
</p>
</div>
<div class="feature-grid">
<a
v-for="(card, i) in stage1Cards"
:key="i"
:href="withBase(t.stage1.cards[i].link)"
class="feature-card glass"
>
<div
class="feature-icon"
:style="{ background: card.color }"
>
{{ card.icon }}
</div>
<div class="feature-content">
<h4>{{ t.stage1.cards[i].title }}</h4>
<p>{{ t.stage1.cards[i].desc }}</p>
</div>
</a>
</div>
</section>
</template>
<style scoped>
.feature-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.feature-card {
background: #fff;
border-radius: 32px;
padding: 32px;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
box-shadow: none;
height: 100%;
position: relative;
overflow: hidden;
text-decoration: none !important;
border: 1px solid rgba(0, 0, 0, 0.025);
}
.dark .feature-card {
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--vp-c-bg-soft);
}
.feature-card:hover {
transform: scale(1.015);
box-shadow: none;
}
.feature-icon {
width: 64px;
height: 64px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
margin-bottom: 24px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45);
}
.feature-content {
display: flex;
flex-direction: column;
}
.feature-content h4 {
font-size: 34px;
font-weight: 700;
margin-bottom: 10px;
color: #1d1d1f;
line-height: 1.3;
letter-spacing: -0.024em;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.feature-content p {
font-size: 17px;
color: #6e6e73;
line-height: 1.6;
margin-top: 4px;
margin-bottom: 0;
}
.dark .feature-content h4 {
color: var(--vp-c-text-1);
}
.dark .feature-content p {
color: var(--vp-c-text-2);
}
@media (max-width: 960px) {
.feature-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.feature-grid {
grid-template-columns: 1fr;
}
.feature-card {
padding: 24px;
}
}
</style>
@@ -0,0 +1,178 @@
<script setup>
import { inject, computed } from 'vue'
import { withBase } from 'vitepress'
import { stage2Cards } from './HomeData'
import './HomeSection.css'
const t = inject('t')
const localizedStage2Cards = computed(() => {
return t.value.stage2.cards.map((card, index) => {
const visual = stage2Cards.find((item) => item.link === card.link) || stage2Cards[index]
return {
...card,
...visual
}
})
})
</script>
<template>
<section
id="junior"
class="section-container section-junior"
>
<div class="section-header">
<h2 class="section-category">
{{ t.stage2.cat }}
</h2>
<h3
class="section-headline"
v-html="t.stage2.title"
/>
<p class="section-sub">
{{ t.stage2.sub }}
</p>
</div>
<div class="comm-grid">
<a
v-for="(card, index) in localizedStage2Cards"
:key="index"
:href="withBase(card.link)"
class="comm-card glass"
>
<div
class="comm-visual"
:style="{ backgroundColor: card.imageColor }"
>
<img
:src="card.image"
:alt="card.imageAlt || card.title"
loading="lazy"
>
</div>
<div class="comm-text">
<h4 class="comm-title">{{ card.title }}</h4>
<p class="comm-desc">{{ card.desc }}</p>
<span class="comm-note">进一步了解 </span>
</div>
</a>
</div>
</section>
</template>
<style scoped>
.section-junior {
margin-top: 72px;
}
@media (max-width: 768px) {
.section-junior {
margin-top: 56px;
}
}
.comm-grid {
display: flex;
gap: 24px;
overflow-x: auto;
width: calc(100% + 40px);
margin: 0 -20px;
padding: 12px 20px 16px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
-ms-overflow-style: none;
}
.comm-grid::-webkit-scrollbar {
display: none;
}
.comm-card {
flex: 0 0 380px;
border-radius: 32px;
overflow: hidden;
background: #fff;
box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.025);
transition: transform 0.3s;
transform-origin: center top;
display: block;
scroll-snap-align: start;
}
.dark .comm-card {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-divider);
}
.comm-card:hover {
transform: scale(1.015);
}
.comm-visual {
height: 220px;
width: 100%;
position: relative;
overflow: hidden;
}
.comm-visual img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
object-position: top center;
}
.comm-text {
padding: 26px 26px 30px;
}
.comm-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
color: #1d1d1f;
letter-spacing: -0.02em;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.comm-desc {
font-size: 16px;
color: #6e6e73;
margin-bottom: 20px;
line-height: 1.5;
}
.comm-note {
font-size: 17px;
color: #0066cc;
letter-spacing: -0.01em;
}
.dark .comm-title {
color: var(--vp-c-text-1);
}
.dark .comm-desc {
color: var(--vp-c-text-2);
}
@media (max-width: 960px) {
.comm-card {
flex-basis: 340px;
}
}
@media (max-width: 640px) {
.comm-card {
flex-basis: min(86vw, 340px);
}
}
</style>
@@ -0,0 +1,156 @@
<script setup>
import { inject } from 'vue'
import { withBase } from 'vitepress'
import { stage3Cards } from './HomeData'
import './HomeSection.css'
const t = inject('t')
</script>
<template>
<section
id="senior"
class="section-container section-senior"
>
<div class="section-header">
<h2 class="section-category">
{{ t.stage3.cat }}
</h2>
<h3
class="section-headline"
v-html="t.stage3.title"
/>
<p class="section-sub">
{{ t.stage3.sub }}
</p>
</div>
<div class="scroll-container">
<div class="scroll-track">
<a
v-for="(card, index) in stage3Cards"
:key="index"
:href="withBase(t.stage3.cards[index].link)"
class="prod-card glass"
>
<div class="prod-tag">{{ card.tag }}</div>
<h4>{{ t.stage3.cards[index].title }}</h4>
<p>{{ t.stage3.cards[index].desc }}</p>
<div class="prod-visual">
<img
:src="card.image"
:alt="card.imageAlt"
:class="card.imageClass"
loading="lazy"
>
</div>
</a>
</div>
</div>
</section>
</template>
<style scoped>
.scroll-container {
overflow-x: auto;
padding-bottom: 40px;
margin: 0 -20px;
padding: 12px 20px 40px;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;
scrollbar-width: none;
-ms-overflow-style: none;
}
.scroll-container::-webkit-scrollbar {
display: none;
}
.scroll-track {
display: flex;
gap: 24px;
width: max-content;
}
.prod-card {
width: 300px;
height: 400px;
border-radius: 32px;
background: #f7f7f9;
padding: 30px;
scroll-snap-align: center;
text-decoration: none !important;
color: inherit !important;
display: flex;
flex-direction: column;
transition: transform 0.3s;
transform-origin: center top;
border: 1px solid rgba(0, 0, 0, 0.025);
box-shadow: none;
}
.dark .prod-card {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-divider);
}
.prod-card:hover {
transform: scale(1.015);
}
.prod-tag {
font-size: 12px;
font-weight: 600;
color: #6e6e73;
margin-bottom: 10px;
text-transform: uppercase;
}
.prod-card h4 {
font-size: 34px;
font-weight: 700;
margin-bottom: 10px;
color: #1d1d1f;
letter-spacing: -0.025em;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC',
sans-serif;
}
.prod-card p {
color: #6e6e73;
font-size: 16px;
line-height: 1.5;
}
.dark .prod-tag,
.dark .prod-card p {
color: var(--vp-c-text-2);
}
.dark .prod-card h4 {
color: var(--vp-c-text-1);
}
.prod-visual {
margin-top: auto;
height: 150px;
border-radius: 20px;
overflow: hidden;
background: linear-gradient(135deg, #dbeafe 0%, #e5e7eb 100%);
}
.prod-visual img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
object-position: center;
}
.prod-visual img.prod-image--personal-brand {
transform: scale(1.18) translateY(-10px);
transform-origin: center top;
}
</style>