feat: 添加Docker支持并优化SEO和用户认证

refactor: 重构页面元数据以支持SEO规范链接
feat(web): 实现用户积分系统和登录验证
docs: 添加Docker使用指南和更新README
build: 添加Docker相关配置文件和脚本
chore: 更新依赖项并添加初始化SQL文件
This commit is contained in:
hex2077
2025-08-21 17:59:17 +08:00
parent d3bd3fdff2
commit 043b0e39f8
20 changed files with 862 additions and 26 deletions

View File

@@ -8,6 +8,9 @@ import { AiOutlineTikTok, AiFillQqCircle, AiOutlineGithub, AiOutlineTwitter, AiF
export const metadata: Metadata = {
title: '联系我们 - PodcastHub',
description: '有任何问题或建议?请随时联系 PodcastHub 团队。我们期待您的声音。',
alternates: {
canonical: '/contact',
},
};
/**

View File

@@ -10,8 +10,9 @@ const inter = Inter({
});
export const metadata: Metadata = {
title: 'PodcastHub - 给创意一个真实的声音',
description: '使用AI技术将您的想法和内容转换为高质量播客音频,支持多种语音和风格选择。',
metadataBase: new URL('https://www.podcasthub.com'),
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
description: 'PodcastHub 利用尖端AI技术为您的创意提供无限可能。轻松将文字和想法转化为专业品质的播客音频支持多种个性化语音和风格选择。立即体验高效创作让您的声音在全球范围内传播吸引更多听众并简化您的播客制作流程。',
keywords: ['播客', 'AI', '语音合成', 'TTS', '音频生成'],
authors: [{ name: 'PodcastHub Team' }],
icons: {
@@ -19,11 +20,18 @@ export const metadata: Metadata = {
apple: '/favicon.webp',
},
openGraph: {
title: 'PodcastHub - 给创意一个真实的声音',
description: '使用AI技术将您的想法和内容转换为高质量的播客音频,支持多种语音和风格选择。',
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
description: 'PodcastHub 利用尖端AI技术为您的创意提供无限可能。轻松将文字和想法转化为专业品质的播客音频,支持多种个性化语音和风格选择。立即体验高效创作,让您的声音在全球范围内传播,吸引更多听众,并简化您的播客制作流程。',
type: 'website',
locale: 'zh_CN',
},
twitter: {
card: 'summary_large_image',
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
},
alternates: {
canonical: '/',
},
};
export const viewport = {

View File

@@ -1,5 +1,20 @@
import { Metadata } from 'next';
import PodcastContent from '@/components/PodcastContent';
export async function generateMetadata({ params }: PodcastDetailPageProps): Promise<Metadata> {
const fileName = decodeURIComponent(params.fileName);
const title = `播客详情 - ${fileName}`;
const description = `收听 ${fileName} 的播客。`;
return {
title,
description,
alternates: {
canonical: `/podcast/${fileName}`,
},
};
}
interface PodcastDetailPageProps {
params: {
fileName: string;

View File

@@ -1,12 +1,20 @@
'use client';
import React from 'react';
import PricingSection from '@/components/PricingSection'; // 导入 PricingSection 组件
const PricingPage: React.FC = () => {
return (
<PricingSection />
);
import { Metadata } from 'next';
import React from 'react';
import PricingSection from '@/components/PricingSection'; // 导入 PricingSection 组件
export const metadata: Metadata = {
title: '定价 - PodcastHub',
description: '查看 PodcastHub 的灵活定价方案,找到最适合您的播客创作计划。',
alternates: {
canonical: '/pricing',
},
};
export default PricingPage;
const PricingPage: React.FC = () => {
return (
<PricingSection />
);
};
export default PricingPage;

View File

@@ -7,6 +7,9 @@ import { Metadata } from 'next';
export const metadata: Metadata = {
title: '隐私政策 - PodcastHub',
description: '了解 PodcastHub 如何保护您的隐私。我们致力于透明化地处理您的数据。',
alternates: {
canonical: '/privacy',
},
};
/**

View File

@@ -7,6 +7,9 @@ import { Metadata } from 'next';
export const metadata: Metadata = {
title: '使用条款 - PodcastHub',
description: '欢迎了解 PodcastHub 的使用条款。本条款旨在保护用户与平台的共同利益。',
alternates: {
canonical: '/terms',
},
};
/**

View File

@@ -16,8 +16,10 @@ import {
import { cn } from '@/lib/utils';
import ConfigSelector from './ConfigSelector';
import VoicesModal from './VoicesModal'; // 引入 VoicesModal
import LoginModal from './LoginModal'; // 引入 LoginModal
import { useToast, ToastContainer } from './Toast'; // 引入 Toast Hook 和 Container
import { setItem, getItem } from '@/lib/storage'; // 引入 localStorage 工具
import { useSession } from '@/lib/auth-client'; // 引入 useSession
import type { PodcastGenerationRequest, TTSConfig, Voice, SettingsFormData } from '@/types';
import { Satisfy } from 'next/font/google'; // 导入艺术字体 Satisfy
@@ -72,6 +74,7 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
const [language, setLanguage] = useState(languageOptions[0].value);
const [duration, setDuration] = useState(durationOptions[0].value);
const [showVoicesModal, setShowVoicesModal] = useState(false); // 新增状态
const [showLoginModal, setShowLoginModal] = useState(false); // 控制登录模态框的显示
const [voices, setVoices] = useState<Voice[]>([]); // 从 ConfigSelector 获取 voices
const [selectedPodcastVoices, setSelectedPodcastVoices] = useState<{[key: string]: Voice[]}>(() => {
// 从 localStorage 读取缓存的说话人配置
@@ -83,8 +86,13 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
const fileInputRef = useRef<HTMLInputElement>(null);
const { toasts, error } = useToast(); // 使用 useToast hook, 引入 success
const { data: session } = useSession(); // 获取 session
const handleSubmit = async () => { // 修改为 async 函数
if (!session?.user) { // 判断是否登录
setShowLoginModal(true); // 未登录则显示登录模态框
return;
}
if (!topic.trim()) {
error("主题不能为空", "请输入播客主题。"); // 使用 toast.error
return;
@@ -206,9 +214,9 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
</g>
</svg>
</div>
<h2 className="text-2xl sm:text-3xl text-black mb-6 break-words">
<h1 className="text-2xl sm:text-3xl text-black mb-6 break-words">
</h2>
</h1>
{/* 模式切换按钮 todo */}
{/* <div className="flex items-center justify-center gap-2 sm:gap-4 mb-8 flex-wrap">
@@ -387,10 +395,10 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
{/* 创作按钮 */}
<button
onClick={handleSubmit}
disabled={!topic.trim() || isGenerating || credits <= 0}
disabled={!topic.trim() || isGenerating}
className={cn(
"btn-primary flex items-center gap-1 text-sm sm:text-base px-3 py-2 sm:px-4 sm:py-2",
(!topic.trim() || isGenerating || credits <= 0) && "opacity-50 cursor-not-allowed"
(!topic.trim() || isGenerating) && "opacity-50 cursor-not-allowed"
)}
>
{isGenerating ? (
@@ -440,6 +448,11 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
}}
/>
)}
{/* Login Modal */}
<LoginModal
isOpen={showLoginModal}
onClose={() => setShowLoginModal(false)}
/>
<ToastContainer toasts={toasts} onRemove={() => {}} /> {/* 添加 ToastContainer */}
</div>