feat: 添加Docker支持并优化SEO和用户认证
refactor: 重构页面元数据以支持SEO规范链接 feat(web): 实现用户积分系统和登录验证 docs: 添加Docker使用指南和更新README build: 添加Docker相关配置文件和脚本 chore: 更新依赖项并添加初始化SQL文件
This commit is contained in:
@@ -8,6 +8,9 @@ import { AiOutlineTikTok, AiFillQqCircle, AiOutlineGithub, AiOutlineTwitter, AiF
|
||||
export const metadata: Metadata = {
|
||||
title: '联系我们 - PodcastHub',
|
||||
description: '有任何问题或建议?请随时联系 PodcastHub 团队。我们期待您的声音。',
|
||||
alternates: {
|
||||
canonical: '/contact',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -7,6 +7,9 @@ import { Metadata } from 'next';
|
||||
export const metadata: Metadata = {
|
||||
title: '隐私政策 - PodcastHub',
|
||||
description: '了解 PodcastHub 如何保护您的隐私。我们致力于透明化地处理您的数据。',
|
||||
alternates: {
|
||||
canonical: '/privacy',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,9 @@ import { Metadata } from 'next';
|
||||
export const metadata: Metadata = {
|
||||
title: '使用条款 - PodcastHub',
|
||||
description: '欢迎了解 PodcastHub 的使用条款。本条款旨在保护用户与平台的共同利益。',
|
||||
alternates: {
|
||||
canonical: '/terms',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user