P8 平台轻量化改造 + SSG修复 + 编程导师 + 文档完善
- Prisma: Tool 模型加 affiliateLink;免费用户沙盒 10→5 次/日 - 后端: Tools API + /admin/tools CRUD 5 端点;Practices 完整模块 - 导航: 主菜单隐藏企业版/社区(URL 可访问) - 首页: 重定位为 AI 工具指南;新增精选工具区块;Feature 重写 - 工具页: affiliateLink 绿色推荐 Badge - SSG 修复: config.ts 构建时直连 localhost:4000,页面 108→127 - 沙盒: 新增编程导师场景(苏格拉底教学法) - 练习系统: Practices 多场景练习(含结构化评分) - 技能广场: 6 个付费 Skill(标题大师/回款助手等) - 管理后台: Models/Posts/Practices CRUD 页面 - 文档: README + progress.md 全面更新;AGENTS.md 同步定位 - 清理: .env.example 移除;tsbuildinfo gitignore
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
import { Search, Menu, X, Bell, Languages } from 'lucide-react';
|
||||
import { Search, Menu, X, Bell, Languages, ChevronDown } from 'lucide-react';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useLang, useT } from '@/i18n';
|
||||
@@ -53,18 +53,35 @@ export function Header() {
|
||||
const { lang, setLang } = useLang();
|
||||
const t = useT();
|
||||
|
||||
const navItems = [
|
||||
const mainNavItems = [
|
||||
{ href: '/', label: t.nav.home },
|
||||
{ href: '/courses', label: t.nav.courses },
|
||||
{ href: '/marketplace', label: t.nav.marketplace },
|
||||
{ href: '/sandbox', label: t.nav.sandbox },
|
||||
{ href: '/skills', label: t.nav.skills },
|
||||
{ href: '/models', label: t.nav.models },
|
||||
{ href: '/prompts', label: t.nav.prompts },
|
||||
{ href: '/contents', label: t.nav.articles },
|
||||
{ href: '/tools', label: t.nav.tools },
|
||||
{ href: '/community', label: t.nav.community },
|
||||
{ href: '/practices', label: t.nav.practices },
|
||||
// { href: '/community', label: t.nav.community },
|
||||
// { href: '/enterprise', label: t.nav.enterprise },
|
||||
];
|
||||
|
||||
const secondaryNavItems = [
|
||||
{ href: '/courses', label: t.nav.courses },
|
||||
{ href: '/prompts', label: t.nav.prompts },
|
||||
{ href: '/models', label: t.nav.models },
|
||||
{ href: '/tools', label: t.nav.tools },
|
||||
{ href: '/contents', label: t.nav.articles },
|
||||
];
|
||||
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const moreTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function openMore() {
|
||||
if (moreTimer.current) clearTimeout(moreTimer.current);
|
||||
setMoreOpen(true);
|
||||
}
|
||||
|
||||
function closeMore() {
|
||||
moreTimer.current = setTimeout(() => setMoreOpen(false), 200);
|
||||
}
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === '/') return pathname === '/';
|
||||
return pathname.startsWith(href);
|
||||
@@ -92,7 +109,7 @@ export function Header() {
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{navItems.map((item) => (
|
||||
{mainNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
@@ -105,6 +122,46 @@ export function Header() {
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={openMore}
|
||||
onMouseLeave={closeMore}
|
||||
>
|
||||
<button
|
||||
className={`flex items-center gap-1 px-3 py-2 text-sm font-medium rounded-lg transition-all ${
|
||||
moreOpen
|
||||
? 'bg-accent text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{t.nav.more}
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${moreOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<div
|
||||
className="absolute right-0 top-full z-50 pt-1"
|
||||
onMouseEnter={openMore}
|
||||
onMouseLeave={closeMore}
|
||||
>
|
||||
{moreOpen && (
|
||||
<div className="w-40 bg-popover border border-border rounded-lg shadow-lg py-1 animate-fade-in">
|
||||
{secondaryNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`block px-3 py-2 text-sm transition-colors ${
|
||||
isActive(item.href)
|
||||
? 'bg-accent text-foreground font-semibold'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
}`}
|
||||
onClick={() => setMoreOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
@@ -170,7 +227,22 @@ export function Header() {
|
||||
|
||||
{mobileOpen && (
|
||||
<nav className="md:hidden pb-4 border-t border-border pt-4 animate-fade-in">
|
||||
{navItems.map((item) => (
|
||||
{mainNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`block py-2.5 px-2 text-sm rounded-lg transition-colors ${
|
||||
isActive(item.href)
|
||||
? 'bg-accent text-foreground font-semibold'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="border-t border-border my-2 mx-2" />
|
||||
{secondaryNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { type ThemeProviderProps } from 'next-themes/dist/types'
|
||||
import { type ThemeProviderProps } from 'next-themes'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import DOMPurify from 'dompurify'
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
@@ -48,7 +49,9 @@ export function CodeBlock({ code, language }: CodeBlockProps) {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('Copy failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -61,7 +64,7 @@ export function CodeBlock({ code, language }: CodeBlockProps) {
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4 text-sm leading-relaxed bg-[#1e1e1e]">
|
||||
<code ref={codeRef} className={`language-${language || ''}`} dangerouslySetInnerHTML={{ __html: highlighted }} />
|
||||
<code ref={codeRef} className={`language-${language || ''}`} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(highlighted) }} />
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function PaymentModal({ open, orderNo, payResult, payChannel, onP
|
||||
|
||||
if (payChannel === 'alipay') {
|
||||
setMessage(t.member.alipayRedirect);
|
||||
if (redirectUrl && redirectUrl !== 'mock://pay') {
|
||||
if (redirectUrl) {
|
||||
window.open(redirectUrl, '_blank');
|
||||
}
|
||||
startPolling();
|
||||
|
||||
Reference in New Issue
Block a user