'init2'
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
mockEbooks,
|
mockEbooks,
|
||||||
mockEbookChapters,
|
mockEbookChapters,
|
||||||
} from '@/data/mockData.js'
|
} from '@/data/mockData.js'
|
||||||
|
import { ACCESS_LEVEL } from '@/utils/access.js'
|
||||||
|
|
||||||
export async function fetchEbooks() {
|
export async function fetchEbooks() {
|
||||||
return [...mockEbooks]
|
return [...mockEbooks]
|
||||||
@@ -35,3 +36,22 @@ export async function fetchCourse(courseId) {
|
|||||||
const list = await fetchCourses()
|
const list = await fetchCourses()
|
||||||
return list.find((item) => String(item.id) === String(courseId)) || null
|
return list.find((item) => String(item.id) === String(courseId)) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取各课程的首个免费试看章节 */
|
||||||
|
export async function fetchFreeTrialChapters() {
|
||||||
|
const courses = await fetchCourses()
|
||||||
|
const result = []
|
||||||
|
for (const course of courses) {
|
||||||
|
const chapters = mockCourseChapters[course.id] || []
|
||||||
|
const freeChapter = chapters.find((ch) => ch.access_level === ACCESS_LEVEL.FREE)
|
||||||
|
if (freeChapter) {
|
||||||
|
result.push({
|
||||||
|
courseId: course.id,
|
||||||
|
courseTitle: course.title,
|
||||||
|
chapterId: freeChapter.id,
|
||||||
|
chapterTitle: freeChapter.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,48 @@ export const NAV_ITEMS = [
|
|||||||
{ path: '/', label: '首页' },
|
{ path: '/', label: '首页' },
|
||||||
{ path: '/vip', label: '会员' },
|
{ path: '/vip', label: '会员' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** 网盘配置 - 多个独立资源 */
|
||||||
|
export const NETDISK_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: '大模型', desc: 'GPT、Claude、国产大模型资源包', url: 'https://pan.baidu.com', icon: 'fire-o' },
|
||||||
|
{ id: 2, name: '懒人安装包', desc: '一键安装,开箱即用', url: 'https://pan.baidu.com', icon: 'down' },
|
||||||
|
{ id: 3, name: '课程讲义', desc: '配套 PPT、思维导图', url: 'https://pan.baidu.com', icon: 'notes-o' },
|
||||||
|
{ id: 4, name: '源码合集', desc: '课程示例代码与项目', url: 'https://pan.baidu.com', icon: 'records-o' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 限时优惠配置 */
|
||||||
|
export const PROMO_CONFIG = {
|
||||||
|
enabled: false,
|
||||||
|
title: '限时优惠',
|
||||||
|
desc: '报名立享 8 折,仅限本周',
|
||||||
|
endAt: null, // 如 '2025-03-18 23:59:59' 可显示倒计时
|
||||||
|
href: '/vip',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 免费试看配置 */
|
||||||
|
export const FREE_TRIAL_CONFIG = {
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 课程预告配置 */
|
||||||
|
export const COMING_SOON_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
items: [
|
||||||
|
{ id: 'soon-1', title: '多模态 AI 实战', desc: '图文理解、语音识别、视频生成', eta: '即将上线' },
|
||||||
|
{ id: 'soon-2', title: 'AI 副业变现', desc: '从 0 到 1 打造 AI 产品', eta: '筹备中' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学习群配置 - 仅展示 QQ 群,微信群隐藏 */
|
||||||
|
export const STUDY_GROUP_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
title: '加入学习群',
|
||||||
|
desc: '与 AI 学习者一起交流,获取最新资料与答疑',
|
||||||
|
items: [
|
||||||
|
{ id: 'wechat', name: '微信学习群', icon: 'chat-o', desc: '扫码加入,每日更新', href: '#', type: 'wechat', hidden: true },
|
||||||
|
{ id: 'qq', name: 'QQ 交流群', icon: 'friends-o', desc: '群号 123456789', href: '#', type: 'qq' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import { getCourseMarkdownRef, getEbookMarkdownRef } from '@/markdown/config.js'
|
import { getCourseMarkdownRef, getEbookMarkdownRef } from '@/markdown/config.js'
|
||||||
import { ACCESS_LEVEL } from '@/utils/access.js'
|
import { ACCESS_LEVEL } from '@/utils/access.js'
|
||||||
|
|
||||||
export const mockEbooks = [
|
/** 本地封面占位图(SVG data URI,含标题文字,无网络请求) */
|
||||||
{
|
function localCover(title, gradient = 0, scale = 1) {
|
||||||
id: 1,
|
const gradients = [
|
||||||
title: 'Prompt 工程实战手册',
|
['#059669', '#047857'],
|
||||||
cover: 'https://picsum.photos/400/300?random=ai1',
|
['#0891b2', '#0e7490'],
|
||||||
description: '从零到精通:提示词设计、Few-shot、思维链,让大模型听懂你的需求。',
|
['#6366f1', '#4f46e5'],
|
||||||
category_id: 'ai',
|
['#8b5cf6', '#7c3aed'],
|
||||||
sort_order: 1,
|
]
|
||||||
},
|
const [c1, c2] = gradients[gradient % gradients.length]
|
||||||
{
|
const baseSize = String(title).length > 12 ? 18 : 22
|
||||||
id: 2,
|
const fontSize = Math.round(baseSize * scale)
|
||||||
title: 'AI Agent 开发指南',
|
const safe = String(title).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||||
cover: 'https://picsum.photos/400/300?random=ai2',
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="${c1}"/><stop offset="100%" stop-color="${c2}"/></linearGradient><filter id="shadow"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.4"/></filter></defs><rect width="400" height="300" fill="url(#g)"/><text x="200" y="155" text-anchor="middle" fill="rgba(255,255,255,0.95)" font-size="${fontSize}" font-weight="600" font-family="system-ui,-apple-system,sans-serif" filter="url(#shadow)">${safe}</text></svg>`
|
||||||
description: 'RAG、Function Calling、多 Agent 协作,打造可落地的智能体应用。',
|
return 'data:image/svg+xml,' + encodeURIComponent(svg)
|
||||||
category_id: 'ai',
|
}
|
||||||
sort_order: 2,
|
|
||||||
},
|
const ebookData = [
|
||||||
|
{ id: 1, title: 'Prompt 工程实战手册', description: '从零到精通:提示词设计、Few-shot、思维链,让大模型听懂你的需求。', category_id: 'ai', sort_order: 1 },
|
||||||
|
{ id: 2, title: 'AI Agent 开发指南', description: 'RAG、Function Calling、多 Agent 协作,打造可落地的智能体应用。', category_id: 'ai', sort_order: 2 },
|
||||||
]
|
]
|
||||||
|
export const mockEbooks = ebookData.map((item, i) => ({ ...item, cover: localCover(item.title, i) }))
|
||||||
|
|
||||||
export const mockEbookChapters = {
|
export const mockEbookChapters = {
|
||||||
1: [
|
1: [
|
||||||
@@ -67,24 +70,11 @@ export const mockEbookChapters = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mockCourses = [
|
const courseData = [
|
||||||
{
|
{ id: 1, title: 'ChatGPT 与大模型实战', description: 'GPT-4、Claude、国产大模型 API 调用,从 Prompt 到 RAG 全流程实战。', category_id: 'ai', sort_order: 1 },
|
||||||
id: 1,
|
{ id: 2, title: 'AI Agent 从 0 到 1', description: 'LangChain、AutoGPT、多 Agent 协作,打造能自主完成任务的智能体。', category_id: 'ai', sort_order: 2 },
|
||||||
title: 'ChatGPT 与大模型实战',
|
|
||||||
cover: 'https://picsum.photos/400/300?random=ai3',
|
|
||||||
description: 'GPT-4、Claude、国产大模型 API 调用,从 Prompt 到 RAG 全流程实战。',
|
|
||||||
category_id: 'ai',
|
|
||||||
sort_order: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'AI Agent 从 0 到 1',
|
|
||||||
cover: 'https://picsum.photos/400/300?random=ai4',
|
|
||||||
description: 'LangChain、AutoGPT、多 Agent 协作,打造能自主完成任务的智能体。',
|
|
||||||
category_id: 'ai',
|
|
||||||
sort_order: 2,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
export const mockCourses = courseData.map((item, i) => ({ ...item, cover: localCover(item.title, i, 2) }))
|
||||||
|
|
||||||
export const mockCourseChapters = {
|
export const mockCourseChapters = {
|
||||||
1: [
|
1: [
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<template v-else-if="course">
|
<template v-else-if="course">
|
||||||
<div class="mainContent">
|
<div class="mainContent">
|
||||||
<header class="courseHeader">
|
<header class="courseHeader">
|
||||||
<div class="cover" :class="{ 'cover--empty': !course.cover }" :style="course.cover ? { backgroundImage: `url(${course.cover})` } : {}">
|
<div class="cover" :class="{ 'cover--empty': !course.cover }">
|
||||||
<van-icon name="play-circle" class="cover__play" />
|
<img v-if="course.cover" :src="course.cover" alt="" class="cover__img" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="courseTitle">{{ course.title }}</h1>
|
<h1 class="courseTitle">{{ course.title }}</h1>
|
||||||
<p class="courseDesc">{{ course.description }}</p>
|
<p class="courseDesc">{{ course.description }}</p>
|
||||||
@@ -225,28 +225,39 @@ onMounted(async () => {
|
|||||||
padding: @space-sm 0 @space-lg;
|
padding: @space-sm 0 @space-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 视频封面 - 参考方糖技能栈 */
|
||||||
.cover {
|
.cover {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-color: var(--color-bg-muted);
|
background-color: var(--color-bg-muted);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.cover--empty {
|
&.cover--empty {
|
||||||
background: linear-gradient(135deg, var(--color-bg-muted) 0%, var(--color-border) 100%);
|
background: linear-gradient(135deg, var(--color-bg-muted) 0%, var(--color-border) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__play {
|
&.cover--empty::after {
|
||||||
position: absolute;
|
display: none;
|
||||||
inset: 0;
|
|
||||||
margin: auto;
|
|
||||||
font-size: 56px;
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-else-if="ebook">
|
<template v-else-if="ebook">
|
||||||
<div class="coverWrap">
|
<div class="coverWrap">
|
||||||
<div class="cover" :style="{ backgroundImage: `url(${ebook.cover})` }" />
|
<div class="cover">
|
||||||
|
<img v-if="ebook.cover" :src="ebook.cover" alt="" class="cover__img" />
|
||||||
|
</div>
|
||||||
<p class="desc">{{ ebook.description }}</p>
|
<p class="desc">{{ ebook.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="chapterList">
|
<div class="chapterList">
|
||||||
@@ -143,13 +145,22 @@ onMounted(async () => {
|
|||||||
aspect-ratio: 3 / 4;
|
aspect-ratio: 3 / 4;
|
||||||
margin: 0 auto 16px;
|
margin: 0 auto 16px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background-size: cover;
|
background-color: var(--color-bg-muted);
|
||||||
background-position: center;
|
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
|
|||||||
@@ -7,15 +7,51 @@
|
|||||||
<div class="hero__stats">
|
<div class="hero__stats">
|
||||||
<span class="hero__stat">{{ filteredCourses.length }} 节课程</span>
|
<span class="hero__stat">{{ filteredCourses.length }} 节课程</span>
|
||||||
<span class="hero__stat">{{ filteredEbooks.length }} 本电子书</span>
|
<span class="hero__stat">{{ filteredEbooks.length }} 本电子书</span>
|
||||||
|
<span class="hero__stat">{{ netdiskItems.length }} 个网盘资源</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="hero__cta" type="button" @click="$router.push('/vip')">立即报名 →</button>
|
<button class="hero__cta" type="button" @click="$router.push('/vip')">立即报名 →</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 限时优惠 Banner -->
|
||||||
|
<section v-if="PROMO_CONFIG.enabled" class="promoBanner" @click="$router.push(PROMO_CONFIG.href)">
|
||||||
|
<div class="promoBanner__inner">
|
||||||
|
<span class="promoBanner__tag">限时</span>
|
||||||
|
<div class="promoBanner__text">
|
||||||
|
<strong>{{ PROMO_CONFIG.title }}</strong>
|
||||||
|
<span>{{ PROMO_CONFIG.desc }}</span>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="promoBanner__arrow" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 免费试看 -->
|
||||||
|
<section v-if="FREE_TRIAL_CONFIG.enabled && freeTrials.length" class="freeTrial">
|
||||||
|
<h2 class="freeTrial__title">免费试看</h2>
|
||||||
|
<p class="freeTrial__desc">无需报名,立即体验</p>
|
||||||
|
<div class="freeTrial__list">
|
||||||
|
<router-link
|
||||||
|
v-for="t in freeTrials"
|
||||||
|
:key="`${t.courseId}-${t.chapterId}`"
|
||||||
|
:to="`/course/${t.courseId}/chapter/${t.chapterId}`"
|
||||||
|
class="freeTrial__item"
|
||||||
|
>
|
||||||
|
<span class="freeTrial__course">{{ t.courseTitle }}</span>
|
||||||
|
<span class="freeTrial__chapter">{{ t.chapterTitle }}</span>
|
||||||
|
<van-icon name="play-circle-o" size="20" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 重点推荐:课程模块(首页核心) -->
|
<!-- 重点推荐:课程模块(首页核心) -->
|
||||||
<main class="main catalog catalog--featured" ref="catalogRef">
|
<main class="main catalog catalog--featured" ref="catalogRef">
|
||||||
<section v-if="loading" class="catalog__skeleton">
|
<section v-if="loading" class="catalog__skeleton">
|
||||||
<van-skeleton title :row="4" />
|
<div class="skeletonGrid skeletonGrid--course">
|
||||||
<van-skeleton title :row="4" class="skeletonSecond" />
|
<div v-for="i in 6" :key="i" class="skeletonCard skeletonCard--course">
|
||||||
|
<div class="skeletonCard__cover" />
|
||||||
|
<div class="skeletonCard__line skeletonCard__line--title" />
|
||||||
|
<div class="skeletonCard__line skeletonCard__line--desc" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -26,19 +62,22 @@
|
|||||||
<van-tab title="课程" :badge="filteredCourses.length || ''">
|
<van-tab title="课程" :badge="filteredCourses.length || ''">
|
||||||
<div class="catalog__content">
|
<div class="catalog__content">
|
||||||
<van-empty v-if="!filteredCourses.length" description="暂无课程" class="catalog__empty" />
|
<van-empty v-if="!filteredCourses.length" description="暂无课程" class="catalog__empty" />
|
||||||
<div v-else class="courseGrid">
|
<template v-else>
|
||||||
|
<div class="catalogGrid catalogGrid--course">
|
||||||
<article
|
<article
|
||||||
v-for="(item, i) in filteredCourses"
|
v-for="(item, i) in displayedCourses"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="courseCard courseCard--featured animate__animated animate__fadeInUp"
|
class="courseCard courseCard--featured animate__animated animate__fadeInUp"
|
||||||
:style="{ animationDelay: `${i * 0.05}s` }"
|
:style="{ animationDelay: `${Math.min(i, 15) * 0.03}s` }"
|
||||||
@click="$router.push(`/course/${item.id}`)"
|
@click="$router.push(`/course/${item.id}`)"
|
||||||
>
|
>
|
||||||
<div class="courseCard__cover">
|
<div class="courseCard__cover">
|
||||||
<img v-if="!item._coverError" :src="item.cover" alt="" @error="item._coverError = true" loading="lazy" />
|
<img v-if="!item._coverError" :src="item.cover" alt="" @error="item._coverError = true" loading="lazy" decoding="async" />
|
||||||
<van-icon v-else name="photo-fail" size="28" class="coverFallback" />
|
<van-icon v-else name="photo-fail" size="28" class="coverFallback" />
|
||||||
<div class="courseCard__play">
|
<div class="courseCard__play">
|
||||||
<van-icon name="play-circle-o" size="48" />
|
<span class="courseCard__playIcon">
|
||||||
|
<van-icon name="play" size="24" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="courseCard__body">
|
<div class="courseCard__body">
|
||||||
@@ -47,21 +86,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasMoreCourses" class="catalog__loadMore">
|
||||||
|
<button type="button" class="loadMoreBtn" @click="loadMoreCourses">加载更多</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
<van-tab title="电子书" :badge="filteredEbooks.length || ''">
|
<van-tab title="电子书" :badge="filteredEbooks.length || ''">
|
||||||
<div class="catalog__content">
|
<div class="catalog__content">
|
||||||
<van-empty v-if="!filteredEbooks.length" description="暂无电子书" class="catalog__empty" />
|
<van-empty v-if="!filteredEbooks.length" description="暂无电子书" class="catalog__empty" />
|
||||||
<div v-else class="ebookGrid">
|
<template v-else>
|
||||||
|
<div class="catalogGrid catalogGrid--ebook">
|
||||||
<article
|
<article
|
||||||
v-for="(item, i) in filteredEbooks"
|
v-for="(item, i) in displayedEbooks"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="ebookCard animate__animated animate__fadeInUp"
|
class="ebookCard animate__animated animate__fadeInUp"
|
||||||
:style="{ animationDelay: `${i * 0.05}s` }"
|
:style="{ animationDelay: `${Math.min(i, 15) * 0.03}s` }"
|
||||||
@click="$router.push(`/ebook/${item.id}`)"
|
@click="$router.push(`/ebook/${item.id}`)"
|
||||||
>
|
>
|
||||||
<div class="ebookCard__cover">
|
<div class="ebookCard__cover">
|
||||||
<img v-if="!item._coverError" :src="item.cover" alt="" @error="item._coverError = true" loading="lazy" />
|
<img v-if="!item._coverError" :src="item.cover" alt="" @error="item._coverError = true" loading="lazy" decoding="async" />
|
||||||
<van-icon v-else name="photo-fail" size="28" class="coverFallback" />
|
<van-icon v-else name="photo-fail" size="28" class="coverFallback" />
|
||||||
<span class="ebookCard__tag">电子书</span>
|
<span class="ebookCard__tag">电子书</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +115,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasMoreEbooks" class="catalog__loadMore">
|
||||||
|
<button type="button" class="loadMoreBtn" @click="loadMoreEbooks">加载更多</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</van-tab>
|
||||||
|
<van-tab title="网盘" :badge="netdiskItems.length || ''">
|
||||||
|
<div class="catalog__content">
|
||||||
|
<van-empty v-if="!netdiskItems.length" description="暂无网盘资料" class="catalog__empty" />
|
||||||
|
<template v-else>
|
||||||
|
<div class="catalogGrid catalogGrid--netdisk">
|
||||||
|
<a
|
||||||
|
v-for="(item, i) in displayedNetdisk"
|
||||||
|
:key="item.id"
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="netdiskCard animate__animated animate__fadeInUp"
|
||||||
|
:style="{ animationDelay: `${Math.min(i, 15) * 0.03}s` }"
|
||||||
|
>
|
||||||
|
<div class="netdiskCard__icon">
|
||||||
|
<van-icon :name="item.icon || 'down'" size="24" />
|
||||||
|
</div>
|
||||||
|
<h3 class="netdiskCard__title">{{ item.name }}</h3>
|
||||||
|
<p class="netdiskCard__desc">{{ item.desc }}</p>
|
||||||
|
<span class="netdiskCard__action">前往下载 <van-icon name="arrow" size="12" /></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasMoreNetdisk" class="catalog__loadMore">
|
||||||
|
<button type="button" class="loadMoreBtn" @click="loadMoreNetdisk">加载更多</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
@@ -78,6 +154,45 @@
|
|||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 学习群模块(仅展示 QQ 群) -->
|
||||||
|
<section v-if="STUDY_GROUP_CONFIG.enabled && visibleStudyGroups.length" class="studyGroup">
|
||||||
|
<div class="studyGroup__inner">
|
||||||
|
<h2 class="studyGroup__title">{{ STUDY_GROUP_CONFIG.title }}</h2>
|
||||||
|
<p class="studyGroup__desc">{{ STUDY_GROUP_CONFIG.desc }}</p>
|
||||||
|
<div class="studyGroup__grid">
|
||||||
|
<a
|
||||||
|
v-for="item in visibleStudyGroups"
|
||||||
|
:key="item.id"
|
||||||
|
:href="item.href"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="studyGroup__card"
|
||||||
|
:class="`studyGroup__card--${item.type}`"
|
||||||
|
>
|
||||||
|
<div class="studyGroup__cardIcon">
|
||||||
|
<van-icon :name="item.icon" size="28" />
|
||||||
|
</div>
|
||||||
|
<h3 class="studyGroup__cardTitle">{{ item.name }}</h3>
|
||||||
|
<p class="studyGroup__cardDesc">{{ item.desc }}</p>
|
||||||
|
<span class="studyGroup__cardAction">加入 <van-icon name="arrow" size="14" /></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 课程预告 -->
|
||||||
|
<section v-if="COMING_SOON_CONFIG.enabled && COMING_SOON_CONFIG.items?.length" class="comingSoon">
|
||||||
|
<h2 class="comingSoon__title">课程预告</h2>
|
||||||
|
<p class="comingSoon__desc">更多实战课程,敬请期待</p>
|
||||||
|
<div class="comingSoon__grid">
|
||||||
|
<div v-for="item in COMING_SOON_CONFIG.items" :key="item.id" class="comingSoon__card">
|
||||||
|
<span class="comingSoon__eta">{{ item.eta }}</span>
|
||||||
|
<h3 class="comingSoon__titleCard">{{ item.title }}</h3>
|
||||||
|
<p class="comingSoon__descCard">{{ item.desc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 次要介绍:弱于课程,但保留完整内容 -->
|
<!-- 次要介绍:弱于课程,但保留完整内容 -->
|
||||||
<section class="audience audience--secondary">
|
<section class="audience audience--secondary">
|
||||||
<h2 class="audience__title">适合谁学习?</h2>
|
<h2 class="audience__title">适合谁学习?</h2>
|
||||||
@@ -151,23 +266,82 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { showFailToast } from 'vant'
|
import { showFailToast } from 'vant'
|
||||||
import { fetchCourses, fetchEbooks } from '@/composables/useKnowledge.js'
|
import { fetchCourses, fetchEbooks } from '@/composables/useKnowledge.js'
|
||||||
import { mockCourses, mockEbooks } from '@/data/mockData.js'
|
import { mockCourses, mockEbooks } from '@/data/mockData.js'
|
||||||
|
import { NETDISK_CONFIG, PROMO_CONFIG, FREE_TRIAL_CONFIG, COMING_SOON_CONFIG, STUDY_GROUP_CONFIG } from '@/config/app.js'
|
||||||
|
import { fetchFreeTrialChapters } from '@/composables/useKnowledge.js'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 12
|
||||||
const ebooks = ref([])
|
const ebooks = ref([])
|
||||||
const courses = ref([])
|
const courses = ref([])
|
||||||
|
const freeTrials = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const catalogRef = ref(null)
|
const catalogRef = ref(null)
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
const coursePage = ref(1)
|
||||||
|
const ebookPage = ref(1)
|
||||||
|
const netdiskPage = ref(1)
|
||||||
|
|
||||||
const filteredEbooks = computed(() => ebooks.value)
|
const filteredEbooks = computed(() => ebooks.value)
|
||||||
const filteredCourses = computed(() => courses.value)
|
const filteredCourses = computed(() => courses.value)
|
||||||
|
|
||||||
function onTabChange() {
|
const netdiskItems = computed(() => (NETDISK_CONFIG.enabled && NETDISK_CONFIG.items) ? NETDISK_CONFIG.items : [])
|
||||||
// 切换 tab 时可选滚动或其它逻辑
|
|
||||||
|
const visibleStudyGroups = computed(() =>
|
||||||
|
(STUDY_GROUP_CONFIG.items || []).filter((item) => !item.hidden && item.type === 'qq')
|
||||||
|
)
|
||||||
|
|
||||||
|
const displayedCourses = computed(() =>
|
||||||
|
filteredCourses.value.slice(0, PAGE_SIZE * coursePage.value)
|
||||||
|
)
|
||||||
|
const displayedEbooks = computed(() =>
|
||||||
|
filteredEbooks.value.slice(0, PAGE_SIZE * ebookPage.value)
|
||||||
|
)
|
||||||
|
const displayedNetdisk = computed(() =>
|
||||||
|
netdiskItems.value.slice(0, PAGE_SIZE * netdiskPage.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasMoreCourses = computed(() => displayedCourses.value.length < filteredCourses.value.length)
|
||||||
|
const hasMoreEbooks = computed(() => displayedEbooks.value.length < filteredEbooks.value.length)
|
||||||
|
const hasMoreNetdisk = computed(() => displayedNetdisk.value.length < netdiskItems.value.length)
|
||||||
|
|
||||||
|
function loadMoreCourses() {
|
||||||
|
coursePage.value += 1
|
||||||
}
|
}
|
||||||
|
function loadMoreEbooks() {
|
||||||
|
ebookPage.value += 1
|
||||||
|
}
|
||||||
|
function loadMoreNetdisk() {
|
||||||
|
netdiskPage.value += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabChange() {
|
||||||
|
coursePage.value = 1
|
||||||
|
ebookPage.value = 1
|
||||||
|
netdiskPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到底自动加载更多(虚拟滚动体验)
|
||||||
|
let loadMoreObserver = null
|
||||||
|
function setupLoadMoreObserver() {
|
||||||
|
loadMoreObserver?.disconnect()
|
||||||
|
loadMoreObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!entries.some((e) => e.isIntersecting)) return
|
||||||
|
if (activeTab.value === 0 && hasMoreCourses.value) loadMoreCourses()
|
||||||
|
else if (activeTab.value === 1 && hasMoreEbooks.value) loadMoreEbooks()
|
||||||
|
else if (activeTab.value === 2 && hasMoreNetdisk.value) loadMoreNetdisk()
|
||||||
|
},
|
||||||
|
{ rootMargin: '120px', threshold: 0 }
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.catalog__loadMore').forEach((el) => loadMoreObserver.observe(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([coursePage, ebookPage, netdiskPage, activeTab], () => {
|
||||||
|
setTimeout(setupLoadMoreObserver, 150)
|
||||||
|
}, { flush: 'post' })
|
||||||
|
|
||||||
function scrollToCatalog() {
|
function scrollToCatalog() {
|
||||||
catalogRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
catalogRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
@@ -175,19 +349,31 @@ function scrollToCatalog() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
setTimeout(setupLoadMoreObserver, 500)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [ebookList, courseList] = await Promise.all([fetchEbooks(), fetchCourses()])
|
const [ebookList, courseList, trialList] = await Promise.all([
|
||||||
|
fetchEbooks(),
|
||||||
|
fetchCourses(),
|
||||||
|
fetchFreeTrialChapters(),
|
||||||
|
])
|
||||||
ebooks.value = (ebookList || []).map((item) => ({ ...item, _coverError: false }))
|
ebooks.value = (ebookList || []).map((item) => ({ ...item, _coverError: false }))
|
||||||
courses.value = (courseList || []).map((item) => ({ ...item, _coverError: false }))
|
courses.value = (courseList || []).map((item) => ({ ...item, _coverError: false }))
|
||||||
|
freeTrials.value = trialList || []
|
||||||
} catch {
|
} catch {
|
||||||
showFailToast('加载失败,已切换为本地数据')
|
showFailToast('加载失败,已切换为本地数据')
|
||||||
ebooks.value = (mockEbooks || []).map((item) => ({ ...item, _coverError: false }))
|
ebooks.value = (mockEbooks || []).map((item) => ({ ...item, _coverError: false }))
|
||||||
courses.value = (mockCourses || []).map((item) => ({ ...item, _coverError: false }))
|
courses.value = (mockCourses || []).map((item) => ({ ...item, _coverError: false }))
|
||||||
|
freeTrials.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
setTimeout(setupLoadMoreObserver, 200)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
loadMoreObserver?.disconnect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -244,6 +430,181 @@ onMounted(async () => {
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 限时优惠 Banner */
|
||||||
|
.promoBanner {
|
||||||
|
margin: 0 var(--page-padding) 24px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: linear-gradient(135deg, rgba(5, 150, 105, 0.15) 0%, rgba(5, 150, 105, 0.06) 100%);
|
||||||
|
border: 1px solid rgba(5, 150, 105, 0.3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner__tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: rgba(5, 150, 105, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner__text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner__arrow {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 免费试看 */
|
||||||
|
.freeTrial {
|
||||||
|
padding: 24px var(--page-padding) 32px;
|
||||||
|
background: var(--color-bg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__desc {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__item:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__course {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__chapter {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTrial__item .van-icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程预告 */
|
||||||
|
.comingSoon {
|
||||||
|
padding: 32px var(--page-padding) 40px;
|
||||||
|
max-width: var(--page-max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__desc {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__card {
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: var(--color-bg-muted);
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__eta {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__titleCard {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon__descCard {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* 适合谁 - 次要层级,弱于课程 */
|
/* 适合谁 - 次要层级,弱于课程 */
|
||||||
.audience--secondary {
|
.audience--secondary {
|
||||||
padding: 32px var(--page-padding) 36px;
|
padding: 32px var(--page-padding) 36px;
|
||||||
@@ -438,13 +799,80 @@ onMounted(async () => {
|
|||||||
padding: 48px 0;
|
padding: 48px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 电子书 - 竖版卡片 */
|
.catalog__loadMore {
|
||||||
.ebookGrid {
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreBtn {
|
||||||
|
padding: 10px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreBtn:hover {
|
||||||
|
background: rgba(5, 150, 105, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动态网格 - 适配 20+ 资源 */
|
||||||
|
.catalogGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalogGrid--course {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalogGrid--ebook {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalogGrid--netdisk {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.catalogGrid--course {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.catalogGrid--ebook {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.catalogGrid--netdisk {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.catalogGrid--course {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.catalogGrid--ebook {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.catalogGrid--netdisk {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电子书 - 竖版卡片 */
|
||||||
|
|
||||||
.ebookCard {
|
.ebookCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -526,11 +954,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 课程 - 横版卡片(重点推荐) */
|
/* 课程 - 横版卡片(重点推荐) */
|
||||||
.courseGrid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.courseCard--featured {
|
.courseCard--featured {
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
@@ -566,6 +989,7 @@ onMounted(async () => {
|
|||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 视频封面 - 方糖技能栈风格:渐变遮罩 + 圆形播放按钮 */
|
||||||
.courseCard__cover {
|
.courseCard__cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -573,29 +997,57 @@ onMounted(async () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--color-bg-muted);
|
background: var(--color-bg-muted);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.55) 0%, transparent 45%);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
|
transition: transform 0.35s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.courseCard:hover .courseCard__cover img {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
.courseCard__play {
|
.courseCard__play {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 48px;
|
z-index: 2;
|
||||||
color: rgba(255, 255, 255, 0.95);
|
pointer-events: none;
|
||||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseCard:hover .courseCard__play {
|
.courseCard__playIcon {
|
||||||
background: rgba(0, 0, 0, 0.25);
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
border-radius: 50%;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.courseCard:hover .courseCard__playIcon {
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseCard__body {
|
.courseCard__body {
|
||||||
@@ -629,14 +1081,241 @@ onMounted(async () => {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog__skeleton {
|
/* 网盘 - 独立资源卡片 */
|
||||||
padding: 24px var(--page-padding);
|
.netdiskCard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.skeletonSecond {
|
.netdiskCard:hover {
|
||||||
margin-top: 18px;
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 8px 20px rgba(5, 150, 105, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiskCard__icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(5, 150, 105, 0.1);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiskCard__title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiskCard__desc {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiskCard__action {
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 学习群模块 */
|
||||||
|
.studyGroup {
|
||||||
|
padding: 40px var(--page-padding) 48px;
|
||||||
|
background: linear-gradient(180deg, var(--color-bg-muted) 0%, var(--color-bg) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__inner {
|
||||||
|
max-width: var(--page-max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__desc {
|
||||||
|
margin: 0 0 28px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28px 24px;
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 12px 24px rgba(5, 150, 105, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card--wechat:hover {
|
||||||
|
border-color: #07c160;
|
||||||
|
box-shadow: 0 12px 24px rgba(7, 193, 96, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card--qq:hover {
|
||||||
|
border-color: #12b7f5;
|
||||||
|
box-shadow: 0 12px 24px rgba(18, 183, 245, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__cardIcon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-bg-muted);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card--wechat .studyGroup__cardIcon {
|
||||||
|
background: rgba(7, 193, 96, 0.12);
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__card--qq .studyGroup__cardIcon {
|
||||||
|
background: rgba(18, 183, 245, 0.12);
|
||||||
|
color: #12b7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__cardTitle {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__cardDesc {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__cardAction {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏 - 匹配课程卡片布局 */
|
||||||
|
.catalog__skeleton {
|
||||||
|
padding: 0 var(--page-padding) 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonGrid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonGrid--course {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.skeletonGrid--course {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.skeletonGrid--course {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard--course .skeletonCard__cover {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: linear-gradient(90deg, var(--color-bg-muted) 25%, var(--color-border) 50%, var(--color-bg-muted) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeletonShimmer 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard__line {
|
||||||
|
height: 14px;
|
||||||
|
margin: 12px 16px 0;
|
||||||
|
background: linear-gradient(90deg, var(--color-bg-muted) 25%, var(--color-border) 50%, var(--color-bg-muted) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeletonShimmer 1.5s ease-in-out infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard__line--title {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard__line--desc {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeletonShimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.hero--compact {
|
.hero--compact {
|
||||||
padding: 24px 20px 20px;
|
padding: 24px 20px 20px;
|
||||||
@@ -674,11 +1353,16 @@ onMounted(async () => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ebookGrid {
|
.catalogGrid--ebook {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalogGrid--netdisk {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.ebookCard__body {
|
.ebookCard__body {
|
||||||
padding: 10px 2px 0;
|
padding: 10px 2px 0;
|
||||||
}
|
}
|
||||||
@@ -687,9 +1371,25 @@ onMounted(async () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseGrid {
|
.catalogGrid--course {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup {
|
||||||
|
padding: 32px var(--page-padding) 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studyGroup__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.studyGroup__card {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user