Files
ai-learning-platform/frontend/src/components/layout/footer.tsx
T
TradeMate Dev 31cc1e02ce feat: 联盟返佣系统规范化并打通技能广场(无 ICP 证合规变现主路径)
- 新增 AffiliateProgram / AffiliateLink / AffiliateClick 规范化 Prisma 模型
  取代原手写裸表 affiliate_stats / affiliate_clicks
- 新增迁移 prisma/migrations/20260711000000_add_affiliate_models
- 重构 affiliate.service.ts 改用 Prisma ORM,消除 $queryRawUnsafe SQL 注入
- 重构 affiliate.controller.ts 接口:programs / links(?skillId,?toolId) / stats / click
- 前端 affiliate 页接入真实接口,移除硬编码 demo 数据
- 技能详情页新增「学此技能推荐使用的工具」联盟链接区块
- 新增 affiliate.service.spec.ts(4 用例通过)与幂等种子 seed-affiliate.ts
- 更新 docs/progress/current.md,明确无 ICP 经营许可证下以联盟返佣为合规变现主路径
- 含此前工作区未提交改动(工具 slug 路由、支付/订单、SEO 等)

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-11 14:47:41 +08:00

119 lines
6.3 KiB
TypeScript
Executable File

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useT } from '@/i18n';
import { API_BASE } from '@/lib/config';
import Image from 'next/image';
interface BeianInfo {
icp: string
gongan: string
gonganLink: string
showGongan: boolean
}
function getBeianInfo(): BeianInfo {
if (typeof window === 'undefined') {
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
}
const hostname = window.location.hostname
if (hostname === 'yuzhiran.com' || hostname === 'www.yuzhiran.com') {
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
}
if (hostname === 'yuzhiran.com.cn' || hostname === 'www.yuzhiran.com.cn') {
return { icp: '京ICP备2026007249号-2', gongan: '京公网安备11011502039622号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039622', showGongan: true }
}
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
}
const qrCodes = [
{ src: '/images/yzr/yuzhiran.jpg', alt: '微信公众号', label: '微信公众号' },
{ src: '/images/yzr/yuzhiran-tech.jpg', alt: '微信服务号', label: '微信服务号' },
{ src: '/images/yzr/yuzhiran-yhl.jpg', alt: '小程序', label: '小程序' },
{ src: '/images/yzr/kefu.png', alt: '微信客服', label: '微信客服' },
]
export function Footer() {
const t = useT();
const [config, setConfig] = useState<Record<string, string>>({});
const [beian, setBeian] = useState<BeianInfo>({ icp: '', gongan: '', gonganLink: '', showGongan: false });
useEffect(() => {
setBeian(getBeianInfo());
fetch(`${API_BASE}/public/config`)
.then(r => r.json()).then(data => setConfig(data || {}))
.catch(() => {});
}, []);
return (
<footer className="border-t border-border bg-muted/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
<div className="grid grid-cols-2 md:grid-cols-5 gap-8">
<div className="col-span-2 md:col-span-2">
<h3 className="text-lg font-bold bg-gradient-to-r from-brand-600 to-brand-400 bg-clip-text text-transparent mb-4"> AI</h3>
<p className="text-sm text-muted-foreground mb-4">{t.footer.tagline}</p>
<div className="flex flex-wrap gap-3">
{qrCodes.map((qr) => (
<div key={qr.alt} className="text-center group">
<Image
src={qr.src}
alt={qr.alt}
width={72}
height={72}
className="rounded-lg bg-white transition-transform duration-200 group-hover:scale-150 group-hover:z-10 group-hover:shadow-xl cursor-pointer"
unoptimized
/>
<p className="text-xs text-muted-foreground mt-1">{qr.label}</p>
</div>
))}
</div>
</div>
<div>
<h4 className="text-sm font-semibold mb-3">{t.footer.explore}</h4>
<ul className="space-y-2.5">
<li><Link href="/courses" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.courses}</Link></li>
<li><Link href="/sandbox" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.sandbox}</Link></li>
<li><Link href="/prompts" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.nav.prompts}</Link></li>
<li><Link href="/resources" className="text-sm text-muted-foreground hover:text-foreground transition-colors"></Link></li>
<li><Link href="/tools" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiTools}</Link></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold mb-3">{t.footer.about}</h4>
<ul className="space-y-2.5">
<li><Link href="/about" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aboutUs}</Link></li>
<li><Link href="/privacy" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.privacy}</Link></li>
<li><Link href="/terms" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.terms}</Link></li>
<li><Link href="/ai-agreement" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiAgreement}</Link></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold mb-3">{t.footer.contact}</h4>
<ul className="space-y-2.5">
<li className="text-sm text-muted-foreground">{config.contact_email ? `邮箱:${config.contact_email}` : '邮箱:contact@yuzhiran.com'}</li>
<li className="text-sm text-muted-foreground"></li>
</ul>
</div>
</div>
<div className="mt-10 pt-8 border-t border-border">
<div className="flex flex-col md:flex-row items-center justify-between gap-2 text-xs text-muted-foreground">
<p>{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}</p>
<div className="flex items-center gap-3">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
{beian.icp}
</a>
{beian.showGongan && (
<a href={beian.gonganLink} target="_blank" rel="noreferrer" className="hover:text-foreground transition-colors inline-flex items-center gap-1">
<Image src="/images/beian/gongan-beian.png" alt="公安备案" width={16} height={16} unoptimized />
{beian.gongan}
</a>
)}
</div>
</div>
</div>
</div>
</footer>
);
}