This commit is contained in:
eric
2026-03-11 23:47:45 -05:00
parent 296d703307
commit 0c2ccadb6b
35 changed files with 2399 additions and 195 deletions

View File

@@ -0,0 +1,38 @@
"use client";
import Link from "next/link";
import { COMMUNITY_HIGHLIGHTS } from "@/app/data/vip.mock";
import styles from "../vip.module.css";
export function CommunityHighlights() {
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>社区精选</h2>
<div
className="animate__animated animate__fadeInUp"
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
>
{COMMUNITY_HIGHLIGHTS.map((item) => (
<Link
key={item.id}
href={item.href}
target="_blank"
rel="noopener noreferrer"
className={styles.card}
style={{
display: "block",
marginBottom: "0.5rem",
textDecoration: "none",
color: "inherit",
}}
>
<span style={{ color: "var(--foreground)" }}>{item.text}</span>
<span style={{ marginLeft: "0.5rem", color: "var(--accent)", fontSize: "0.9rem" }}>
→
</span>
</Link>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,100 @@
"use client";
import Link from "next/link";
import { getLastWatched, getProgress } from "@/app/cloudphone/lib/learning";
import { CURRICULUM_CONFIG } from "@/app/cloudphone/config/course";
import { useState, useEffect } from "react";
import styles from "../vip.module.css";
const TOTAL_LESSONS = CURRICULUM_CONFIG.modules.reduce((s, m) => s + m.lessons.length, 0);
export function ContinueLearning() {
const [last, setLast] = useState<ReturnType<typeof getLastWatched>>(null);
const [progress, setProgress] = useState({ completed: 0, percent: 0 });
useEffect(() => {
setLast(getLastWatched());
setProgress(getProgress(TOTAL_LESSONS));
const onProgress = () => {
setLast(getLastWatched());
setProgress(getProgress(TOTAL_LESSONS));
};
window.addEventListener("learning:progress", onProgress);
return () => window.removeEventListener("learning:progress", onProgress);
}, []);
if (!last || progress.completed >= TOTAL_LESSONS) {
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>继续学习</h2>
<Link
href="/cloudphone/course/0/0"
className={`${styles.card} ${styles.btnPrimary}`}
style={{ display: "block", textAlign: "center" }}
>
{progress.completed >= TOTAL_LESSONS ? "复习课程" : "开始学习"} →
</Link>
</section>
);
}
const module = CURRICULUM_CONFIG.modules[last.moduleIndex];
const lesson = module?.lessons[last.lessonIndex];
const title = lesson?.name || `第 ${last.moduleIndex + 1} 章`;
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>继续学习</h2>
<Link
href={`/cloudphone/course/${last.moduleIndex}/${last.lessonIndex}`}
className={styles.card}
style={{
display: "block",
textDecoration: "none",
color: "inherit",
}}
>
<div style={{ fontWeight: 600, color: "var(--foreground)", marginBottom: "0.5rem" }}>
{title}
</div>
<div className="animate__animated animate__fadeInUp" style={{ animationFillMode: "both" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: "0.8rem",
color: "var(--muted-foreground)",
marginBottom: "0.25rem",
}}
>
<span>进度</span>
<span>
{progress.completed}/{TOTAL_LESSONS} 节
</span>
</div>
<div
style={{
height: "6px",
borderRadius: "3px",
background: "var(--muted)",
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${progress.percent}%`,
background: "var(--accent)",
borderRadius: "3px",
transition: "width 0.3s",
}}
/>
</div>
</div>
<div style={{ marginTop: "0.75rem", color: "var(--accent)", fontSize: "0.9rem" }}>
继续学习 →
</div>
</Link>
</section>
);
}

View File

@@ -0,0 +1,44 @@
"use client";
import Link from "next/link";
import { RECENT_UPDATES } from "@/app/data/vip.mock";
import styles from "../vip.module.css";
export function RecentUpdates() {
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>最近更新</h2>
<div
className="animate__animated animate__fadeInUp"
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
>
{RECENT_UPDATES.map((item) => {
const content = (
<div
className={styles.card}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "0.5rem",
}}
>
<span style={{ color: "var(--foreground)" }}>{item.title}</span>
<span style={{ fontSize: "0.8rem", color: "var(--muted-foreground)" }}>
{item.date}
</span>
</div>
);
if (item.href) {
return (
<Link key={item.id} href={item.href} style={{ textDecoration: "none" }}>
{content}
</Link>
);
}
return <div key={item.id}>{content}</div>;
})}
</div>
</section>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import Link from "next/link";
import { TOPICS } from "@/app/data/vip.mock";
import styles from "../vip.module.css";
export function TopicCards() {
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>模块入口</h2>
<div
className={`${styles.topicGrid} animate__animated animate__fadeInUp`}
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
>
{TOPICS.map((t) => {
const content = (
<>
<span className={styles.topicIcon}>{t.icon}</span>
<span className={styles.topicTitle}>{t.title}</span>
<span className={styles.topicDesc}>{t.desc}</span>
</>
);
if (t.href) {
return (
<Link
key={t.id}
href={t.href}
className={styles.topicCard}
target={t.href.startsWith("http") ? "_blank" : undefined}
rel={t.href.startsWith("http") ? "noopener noreferrer" : undefined}
>
{content}
</Link>
);
}
return (
<div key={t.id} className={styles.topicCard} style={{ cursor: "default", opacity: 0.7 }}>
{content}
</div>
);
})}
</div>
</section>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { ASSET_STATS } from "@/app/data/vip.mock";
import styles from "../vip.module.css";
const STAT_ITEMS = [
{ key: "topics", label: "已解锁模块" },
{ key: "ebooks", label: "电子书" },
{ key: "videos", label: "视频" },
{ key: "templates", label: "模板" },
] as const;
export function UnlockedStats() {
return (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>已解锁</h2>
<div
className={`${styles.statsGrid4} animate__animated animate__fadeInUp`}
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
>
{STAT_ITEMS.map(({ key, label }) => {
const value = String(ASSET_STATS[key as keyof typeof ASSET_STATS]);
return (
<div key={key} className={styles.card} style={{ textAlign: "center" }}>
<div className={styles.bigNumber}>{value}</div>
<div className={styles.bigNumberLabel}>{label}</div>
</div>
);
})}
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { WEEKLY_ACTIONS } from "@/app/data/vip.mock";
import styles from "../vip.module.css";
export function WeeklyActions() {
return (
<section className={styles.section} style={{ paddingBottom: "2rem" }}>
<h2 className={styles.sectionTitle}>本周推荐</h2>
<div
className="animate__animated animate__fadeInUp"
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
>
{WEEKLY_ACTIONS.map((item) => (
<Link
key={item.id}
href={item.href}
target={item.href.startsWith("http") ? "_blank" : undefined}
rel={item.href.startsWith("http") ? "noopener noreferrer" : undefined}
className={styles.card}
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
marginBottom: "0.5rem",
textDecoration: "none",
color: "inherit",
}}
>
<span
style={{
width: "24px",
height: "24px",
borderRadius: "50%",
background: "var(--accent)",
color: "var(--accent-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.75rem",
fontWeight: 600,
}}
>
{item.priority}
</span>
<span style={{ color: "var(--foreground)" }}>{item.text}</span>
<span style={{ marginLeft: "auto", color: "var(--accent)", fontSize: "0.9rem" }}>
→
</span>
</Link>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import Link from "next/link";
import styles from "../vip.module.css";
type WelcomeBlockProps = {
userName: string | null;
};
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
export function WelcomeBlock({ userName }: WelcomeBlockProps) {
const displayName = userName || getStorage("userName") || "会员";
return (
<section className={styles.section} style={{ paddingTop: "1.5rem" }}>
<h1
className="animate__animated animate__fadeInDown"
style={{
fontSize: "clamp(1.5rem, 4vw, 2rem)",
fontWeight: 600,
margin: 0,
animationDelay: "0.1s",
animationFillMode: "both",
}}
>
欢迎回来,{displayName}
</h1>
<p
className="animate__animated animate__fadeInDown"
style={{
fontSize: "0.95rem",
color: "var(--muted-foreground)",
margin: "0.25rem 0 0",
animationDelay: "0.15s",
animationFillMode: "both",
}}
>
以下是您的会员资产与推荐动作
</p>
<Link
href="https://qun.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
marginTop: "1rem",
textDecoration: "none",
color: "var(--accent)",
fontWeight: 500,
}}
>
🌐 进入星球 qun.hackrobot.cn →
</Link>
</section>
);
}