feat(i18n): 添加播客生成任务的多语言支持
refactor(login): 改进登录后重定向逻辑 style(components): 统一加载中的文本显示 chore(docker): 为服务添加名称配置
This commit is contained in:
@@ -12,6 +12,7 @@ services:
|
|||||||
- /opt/audio/output:/app/server/output
|
- /opt/audio/output:/app/server/output
|
||||||
- /opt/audio/sqlite.db:/app/web/sqlite.db
|
- /opt/audio/sqlite.db:/app/web/sqlite.db
|
||||||
restart: always
|
restart: always
|
||||||
|
name: podcast-web
|
||||||
depends_on:
|
depends_on:
|
||||||
- server
|
- server
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /opt/audio/output:/app/server/output
|
- /opt/audio/output:/app/server/output
|
||||||
restart: always
|
restart: always
|
||||||
|
name: podcast-server
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
audio-data:
|
audio-data:
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
"save20Percent": "Save 20%"
|
"save20Percent": "Save 20%"
|
||||||
},
|
},
|
||||||
"configSelector": {
|
"configSelector": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading..",
|
||||||
"selectTTSConfig": "Select TTS Config",
|
"selectTTSConfig": "Select TTS Config",
|
||||||
"noAvailableTTSConfig": "No available TTS config",
|
"noAvailableTTSConfig": "No available TTS config",
|
||||||
"pleaseConfigTTS": "Please configure TTS service in settings first"
|
"pleaseConfigTTS": "Loading.."
|
||||||
},
|
},
|
||||||
"contentSection": {
|
"contentSection": {
|
||||||
"viewAll": "View All",
|
"viewAll": "View All",
|
||||||
@@ -151,7 +151,9 @@
|
|||||||
"storytellingMode": "Storytelling Mode",
|
"storytellingMode": "Storytelling Mode",
|
||||||
"apiAccess": "API Access"
|
"apiAccess": "API Access"
|
||||||
},
|
},
|
||||||
"comingSoon": "(Coming Soon)"
|
"comingSoon": "(Coming Soon)",
|
||||||
|
"pricing_page_title": "Pricing Plans",
|
||||||
|
"pricing_page_description": "Flexible plans for all creators."
|
||||||
},
|
},
|
||||||
"settingsForm": {
|
"settingsForm": {
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
,
|
,
|
||||||
"failed_to_get_task_status": "Failed to get task status"
|
"failed_to_get_task_status": "Failed to get task status"
|
||||||
,
|
,
|
||||||
"insufficient_points_raw": "积分不足"
|
"insufficient_points_raw": "Not enough points",
|
||||||
|
"podcast_generation_task": "Podcast Generation Task"
|
||||||
,
|
,
|
||||||
"forbidden_user_id": "Forbidden: User ID not allowed to access this resource",
|
"forbidden_user_id": "Forbidden: User ID not allowed to access this resource",
|
||||||
"invalid_request_parameters_add_points": "Invalid request parameters. `userId`, `pointsToAdd` (positive number), `reasonCode`, and `description` are required.",
|
"invalid_request_parameters_add_points": "Invalid request parameters. `userId`, `pointsToAdd` (positive number), `reasonCode`, and `description` are required.",
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
"save20Percent": "20%割引"
|
"save20Percent": "20%割引"
|
||||||
},
|
},
|
||||||
"configSelector": {
|
"configSelector": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中",
|
||||||
"selectTTSConfig": "TTS設定を選択",
|
"selectTTSConfig": "TTS設定を選択",
|
||||||
"noAvailableTTSConfig": "利用可能なTTS設定がありません",
|
"noAvailableTTSConfig": "利用可能なTTS設定がありません",
|
||||||
"pleaseConfigTTS": "最初に設定でTTSサービスを設定してください"
|
"pleaseConfigTTS": "読み込み中"
|
||||||
},
|
},
|
||||||
"contentSection": {
|
"contentSection": {
|
||||||
"viewAll": "すべて表示",
|
"viewAll": "すべて表示",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
,
|
,
|
||||||
"failed_to_get_task_status": "タスクステータスの取得に失敗しました"
|
"failed_to_get_task_status": "タスクステータスの取得に失敗しました"
|
||||||
,
|
,
|
||||||
"insufficient_points_raw": "ポイント不足"
|
"insufficient_points_raw": "ポイント不足",
|
||||||
|
"podcast_generation_task": "ポッドキャスト生成タスク"
|
||||||
,
|
,
|
||||||
"forbidden_user_id": "禁止:このリソースへのアクセスが許可されていないユーザーID",
|
"forbidden_user_id": "禁止:このリソースへのアクセスが許可されていないユーザーID",
|
||||||
"invalid_request_parameters_add_points": "無効なリクエストパラメータ。`userId`、`pointsToAdd`(正の数)、`reasonCode`、および`description`が必要です。",
|
"invalid_request_parameters_add_points": "無効なリクエストパラメータ。`userId`、`pointsToAdd`(正の数)、`reasonCode`、および`description`が必要です。",
|
||||||
@@ -45,5 +46,6 @@
|
|||||||
,
|
,
|
||||||
"invalid_pagination_parameters": "無効なページネーションパラメータ"
|
"invalid_pagination_parameters": "無効なページネーションパラメータ"
|
||||||
,
|
,
|
||||||
"cannot_read_tts_provider_config": "TTSプロバイダー構成ファイルを読み取れません"
|
"cannot_read_tts_provider_config": "TTSプロバイダー構成ファイルを読み取れません",
|
||||||
|
"invalid_provider": "無効なTTSプロバイダー"
|
||||||
}
|
}
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
"save20Percent": "节省 20%"
|
"save20Percent": "节省 20%"
|
||||||
},
|
},
|
||||||
"configSelector": {
|
"configSelector": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中..",
|
||||||
"selectTTSConfig": "选择TTS配置",
|
"selectTTSConfig": "选择TTS配置",
|
||||||
"noAvailableTTSConfig": "暂无可用的TTS配置",
|
"noAvailableTTSConfig": "暂无可用的TTS配置",
|
||||||
"pleaseConfigTTS": "请先在设置中配置TTS服务"
|
"pleaseConfigTTS": "加载中.."
|
||||||
},
|
},
|
||||||
"contentSection": {
|
"contentSection": {
|
||||||
"viewAll": "查看全部",
|
"viewAll": "查看全部",
|
||||||
@@ -151,7 +151,9 @@
|
|||||||
"storytellingMode": "说书模式",
|
"storytellingMode": "说书模式",
|
||||||
"apiAccess": "API访问"
|
"apiAccess": "API访问"
|
||||||
},
|
},
|
||||||
"comingSoon": "(即将推出)"
|
"comingSoon": "(即将推出)",
|
||||||
|
"pricing_page_title": "价格方案",
|
||||||
|
"pricing_page_description": "适用于所有创作者的灵活方案。"
|
||||||
},
|
},
|
||||||
"settingsForm": {
|
"settingsForm": {
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
,
|
,
|
||||||
"failed_to_get_task_status": "获取任务状态失败"
|
"failed_to_get_task_status": "获取任务状态失败"
|
||||||
,
|
,
|
||||||
"insufficient_points_raw": "积分不足"
|
"insufficient_points_raw": "积分不足",
|
||||||
|
"podcast_generation_task": "播客生成任务"
|
||||||
,
|
,
|
||||||
"forbidden_user_id": "禁止:该用户ID无权访问此资源",
|
"forbidden_user_id": "禁止:该用户ID无权访问此资源",
|
||||||
"invalid_request_parameters_add_points": "无效的请求参数。`userId`、`pointsToAdd`(正数)、`reasonCode`和`description`是必需的。",
|
"invalid_request_parameters_add_points": "无效的请求参数。`userId`、`pointsToAdd`(正数)、`reasonCode`和`description`是必需的。",
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import { createPointsAccount, recordPointsTransaction, checkUserPointsAccount }
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const sessionData = await getSessionData();
|
const sessionData = await getSessionData();
|
||||||
console.log('获取到的 session:', sessionData);
|
let baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "/";
|
||||||
|
const pathname = request.nextUrl.searchParams.get('pathname');
|
||||||
|
if(!!pathname){
|
||||||
|
baseUrl += pathname.replace('/','');
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有获取到 session,直接重定向到根目录
|
// 如果没有获取到 session,直接重定向到根目录
|
||||||
if (!sessionData?.user) {
|
if (!sessionData?.user) {
|
||||||
const url = new URL('/', request.url);
|
const url = new URL(baseUrl, request.url);
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const userId = sessionData.user.id; // 获取 userId
|
const userId = sessionData.user.id; // 获取 userId
|
||||||
|
|
||||||
// 检查用户是否已存在积分账户
|
// 检查用户是否已存在积分账户
|
||||||
@@ -34,7 +39,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个 URL 对象,指向要重定向到的根目录
|
// 创建一个 URL 对象,指向要重定向到的根目录
|
||||||
const url = new URL('/', request.url);
|
const url = new URL(baseUrl, request.url);
|
||||||
// 返回重定向响应
|
// 返回重定向响应
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
// 5. 扣减积分
|
// 5. 扣减积分
|
||||||
const pointsToDeduct = parseInt(process.env.POINTS_PER_PODCAST || '10', 10); // 从环境变量获取,默认10
|
const pointsToDeduct = parseInt(process.env.POINTS_PER_PODCAST || '10', 10); // 从环境变量获取,默认10
|
||||||
const reasonCode = "podcast_generation";
|
const reasonCode = "podcast_generation";
|
||||||
const description = `播客生成任务:${task_id}`; // Keep this as is if task_id is dynamic or not translatable
|
const description = `${t("podcast_generation_task")}: ${task_id}`; // 多语言实现
|
||||||
|
|
||||||
await deductUserPoints(userId, pointsToDeduct, reasonCode, description);
|
await deductUserPoints(userId, pointsToDeduct, reasonCode, description);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
|
|||||||
const { t, i18n } = useTranslation(lang, 'components'); // 初始化 useTranslation
|
const { t, i18n } = useTranslation(lang, 'components'); // 初始化 useTranslation
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentPath = usePathname(); // 将 usePathname 移到组件顶层
|
const currentPath = usePathname(); // 将 usePathname 移到组件顶层
|
||||||
|
// console.log('i18n.language', i18n.language, lang);
|
||||||
|
|
||||||
const switchLanguage = (locale: string) => {
|
const switchLanguage = (locale: string) => {
|
||||||
// 获取当前路径,并替换语言部分
|
// 获取当前路径,并替换语言部分
|
||||||
@@ -34,7 +35,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => switchLanguage('zh-CN')}
|
onClick={() => switchLanguage('zh-CN')}
|
||||||
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
||||||
i18n.language === 'zh-CN'
|
lang === 'zh-CN'
|
||||||
? 'text-gray-900 transition-colors duration-200'
|
? 'text-gray-900 transition-colors duration-200'
|
||||||
: 'hover:text-gray-900 transition-colors duration-200'
|
: 'hover:text-gray-900 transition-colors duration-200'
|
||||||
}`}
|
}`}
|
||||||
@@ -44,7 +45,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => switchLanguage('')}
|
onClick={() => switchLanguage('')}
|
||||||
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
||||||
i18n.language === 'en'
|
lang === 'en'
|
||||||
? 'text-gray-900 transition-colors duration-200'
|
? 'text-gray-900 transition-colors duration-200'
|
||||||
: 'hover:text-gray-900 transition-colors duration-200'
|
: 'hover:text-gray-900 transition-colors duration-200'
|
||||||
}`}
|
}`}
|
||||||
@@ -54,7 +55,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => switchLanguage('ja')}
|
onClick={() => switchLanguage('ja')}
|
||||||
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
|
||||||
i18n.language === 'ja'
|
lang === 'ja'
|
||||||
? 'text-gray-900 transition-colors duration-200'
|
? 'text-gray-900 transition-colors duration-200'
|
||||||
: 'hover:text-gray-900 transition-colors duration-200'
|
: 'hover:text-gray-900 transition-colors duration-200'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { createPortal } from "react-dom";
|
|||||||
import { XMarkIcon } from "@heroicons/react/24/outline"; // 导入关闭图标
|
import { XMarkIcon } from "@heroicons/react/24/outline"; // 导入关闭图标
|
||||||
import { AiOutlineChrome, AiOutlineGithub } from "react-icons/ai"; // 从 react-icons/ai 导入 Google 和 GitHub 图标
|
import { AiOutlineChrome, AiOutlineGithub } from "react-icons/ai"; // 从 react-icons/ai 导入 Google 和 GitHub 图标
|
||||||
import { useTranslation } from '../i18n/client'; // 导入 useTranslation
|
import { useTranslation } from '../i18n/client'; // 导入 useTranslation
|
||||||
|
import { usePathname } from 'next/navigation'; // 导入 usePathname
|
||||||
|
import { getTruePathFromPathname } from '../lib/utils'; // 导入新函数
|
||||||
|
|
||||||
interface LoginModalProps {
|
interface LoginModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -30,6 +32,8 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const pathname = usePathname();
|
||||||
|
const truePath = getTruePathFromPathname(pathname, lang);
|
||||||
// 使用 React Portal 将模态框渲染到 body 下,避免Z-index问题和父组件样式影响
|
// 使用 React Portal 将模态框渲染到 body 下,避免Z-index问题和父组件样式影响
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
@@ -58,7 +62,7 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => signIn.social({ provider: "google" , newUserCallbackURL: "/api/newuser?provider=google"})}
|
onClick={() => signIn.social({ provider: "google" , newUserCallbackURL: "/api/newuser?provider=google&pathname=" + truePath})}
|
||||||
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
<AiOutlineChrome className="h-6 w-6" />
|
<AiOutlineChrome className="h-6 w-6" />
|
||||||
@@ -66,7 +70,7 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => signIn.social({ provider: "github" , newUserCallbackURL: "/api/newuser?provider=github" })}
|
onClick={() => signIn.social({ provider: "github" , newUserCallbackURL: "/api/newuser?provider=github&pathname=" + truePath })}
|
||||||
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
<AiOutlineGithub className="h-6 w-6" />
|
<AiOutlineGithub className="h-6 w-6" />
|
||||||
|
|||||||
Reference in New Issue
Block a user