feat: i18n 翻译覆盖 + DB 动态数据 + 模型选择器 API

This commit is contained in:
yuzhiran-dev
2026-05-22 16:55:18 +08:00
parent 23edb74bce
commit 1b4c19daa6
16 changed files with 763 additions and 1036 deletions
+2 -1
View File
@@ -8,13 +8,14 @@ import { AnalyticsController } from './analytics.controller';
import { SettingsController } from './settings.controller'; import { SettingsController } from './settings.controller';
import { OperationsController } from './operations.controller'; import { OperationsController } from './operations.controller';
import { UsersController } from './users.controller'; import { UsersController } from './users.controller';
import { PublicController } from './public.controller';
import { CoursesModule } from '../courses/courses.module'; import { CoursesModule } from '../courses/courses.module';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { AIModule } from '../ai/ai.module'; import { AIModule } from '../ai/ai.module';
@Module({ @Module({
imports: [CoursesModule, AuthModule, AIModule], imports: [CoursesModule, AuthModule, AIModule],
controllers: [AdminController, AnalyticsController, SettingsController, OperationsController, UsersController], controllers: [AdminController, AnalyticsController, SettingsController, OperationsController, UsersController, PublicController],
providers: [AdminService, AdminGuard, AdminAiAssistantService], providers: [AdminService, AdminGuard, AdminAiAssistantService],
exports: [AdminService], exports: [AdminService],
}) })
+326 -65
View File
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '../../prisma/prisma.service'; import { PrismaService } from '../../prisma/prisma.service';
export interface ToolDefinition { export interface ToolDefinition {
@@ -25,44 +26,45 @@ export class AdminAiAssistantService {
private prisma: PrismaService, private prisma: PrismaService,
) {} ) {}
getAvailableTools(): ToolDefinition[] { private crudTools(): ToolDefinition[] {
return [ return [
{ {
name: 'navigate', name: 'navigate',
description: '跳转到管理后台的某个页面', description: '跳转到管理后台的某个页面',
parameters: { parameters: { path: { type: 'string', description: '页面路径,如 /admin/users、/admin/orders、/admin/analytics、/admin/enterprise、/admin/operations/banners、/admin/operations/notifications、/admin/settings/roles、/admin/settings/config、/admin/comments、/admin/courses、/admin/prompts、/admin/contents、/admin/tools' } },
path: { type: 'string', description: '页面路径,如 /admin/users、/admin/orders、/admin/analytics、/admin/enterprise、/admin/operations/banners、/admin/operations/notifications、/admin/settings/roles、/admin/settings/config、/admin/comments、/admin/courses、/admin/prompts、/admin/contents、/admin/tools' },
},
}, },
{ {
name: 'get-dashboard', name: 'get-dashboard',
description: '获取仪表盘概览数据(用户数、课程数、内容数、提示词数、订单数)', description: '获取仪表盘概览数据(用户数、课程数、内容数、提示词数、订单数)',
parameters: {}, parameters: {},
}, },
// ---- 用户管理 ----
{ {
name: 'list-users', name: 'list-users',
description: '列出用户,可按关键词搜索', description: '列出用户,可按关键词搜索',
parameters: { parameters: { search: { type: 'string', description: '搜索关键词(可选)' }, page: { type: 'number', description: '页码(可选)' }, pageSize: { type: 'number', description: '每页数量(可选)' } },
search: { type: 'string', description: '搜索关键词(可选)' },
page: { type: 'number', description: '页码(可选,默认1' },
pageSize: { type: 'number', description: '每页数量(可选,默认20' },
},
}, },
{ {
name: 'get-user', name: 'get-user',
description: '查看某个用户的详细信息', description: '查看用户详情(包含最近订单)',
parameters: { parameters: { id: { type: 'number', description: '用户ID' } },
id: { type: 'number', description: '用户ID' },
},
}, },
{ {
name: 'update-user-status', name: 'update-user-status',
description: '修改用户状态(启用/禁用/封禁)', description: '修改用户状态',
parameters: { parameters: { id: { type: 'number', description: '用户ID' }, status: { type: 'string', enum: ['ACTIVE', 'INACTIVE', 'BANNED'], description: '新状态' } },
id: { type: 'number', description: '用户ID' },
status: { type: 'string', enum: ['ACTIVE', 'INACTIVE', 'BANNED'], description: '新状态' },
},
}, },
{
name: 'update-user',
description: '修改用户信息(昵称、手机号、邮箱、状态等)',
parameters: { id: { type: 'number', description: '用户ID' }, nickname: { type: 'string', description: '新昵称(可选)' }, phone: { type: 'string', description: '新手机号(可选)' }, email: { type: 'string', description: '新邮箱(可选)' }, status: { type: 'string', enum: ['ACTIVE', 'INACTIVE', 'BANNED'], description: '新状态(可选)' }, memberPlan: { type: 'string', enum: ['FREE', 'MONTHLY', 'YEARLY'], description: '会员计划(可选)' }, sandboxDaily: { type: 'number', description: '每日沙盒次数(可选)' } },
},
{
name: 'delete-user',
description: '软删除一个用户',
parameters: { id: { type: 'number', description: '用户ID' } },
},
// ---- 订单 ----
{ {
name: 'list-orders', name: 'list-orders',
description: '查看最近订单列表', description: '查看最近订单列表',
@@ -73,82 +75,169 @@ export class AdminAiAssistantService {
description: '获取数据分析概览(用户增长、收入、趋势)', description: '获取数据分析概览(用户增长、收入、趋势)',
parameters: {}, parameters: {},
}, },
// ---- 评论审核 ----
{ {
name: 'list-comments', name: 'list-comments',
description: '查看评论列表,可按状态筛选', description: '查看评论列表,可按状态筛选',
parameters: { parameters: { status: { type: 'string', enum: ['PENDING_REVIEW', 'PUBLISHED', 'REJECTED'], description: '评论状态(可选)' } },
status: { type: 'string', enum: ['PENDING_REVIEW', 'PUBLISHED', 'REJECTED'], description: '评论状态(可选)' },
},
}, },
{ {
name: 'approve-comment', name: 'approve-comment', description: '通过一条待审核评论', parameters: { id: { type: 'number', description: '评论ID' } },
description: '通过一条待审核评论',
parameters: {
id: { type: 'number', description: '评论ID' },
},
}, },
{ {
name: 'reject-comment', name: 'reject-comment', description: '拒绝一条评论', parameters: { id: { type: 'number', description: '评论ID' }, reason: { type: 'string', description: '拒绝原因(可选)' } },
description: '拒绝一条评论并给出原因', },
parameters: { // ---- 课程管理 ----
id: { type: 'number', description: '评论ID' }, {
reason: { type: 'string', description: '拒绝原因(可选)' }, name: 'list-courses', description: '查看所有课程', parameters: {},
},
}, },
{ {
name: 'list-banners', name: 'get-course', description: '查看课程详情', parameters: { id: { type: 'number', description: '课程ID' } },
description: '查看所有Banner列表',
parameters: {},
}, },
{ {
name: 'list-notifications', name: 'create-course', description: '创建新课程(title 必填,其他可选)',
description: '查看所有系统通知', parameters: { title: { type: 'string', description: '课程标题(必填)' }, description: { type: 'string', description: '课程描述' }, price: { type: 'number', description: '价格(默认0' }, isFree: { type: 'boolean', description: '是否免费(默认true' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '状态(可选)' } },
parameters: {},
}, },
{ {
name: 'list-config', name: 'update-course', description: '修改课程信息',
description: '查看系统配置项', parameters: { id: { type: 'number', description: '课程ID' }, title: { type: 'string', description: '新标题' }, description: { type: 'string', description: '新描述' }, price: { type: 'number', description: '新价格' }, isFree: { type: 'boolean', description: '是否免费' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '新状态' } },
parameters: {},
}, },
{ {
name: 'list-roles', name: 'delete-course', description: '软删除一个课程', parameters: { id: { type: 'number', description: '课程ID' } },
description: '查看所有管理角色',
parameters: {},
}, },
{ {
name: 'list-admins', name: 'toggle-course-status', description: '上架/下架一个课程(自动切换)', parameters: { id: { type: 'number', description: '课程ID' } },
description: '查看所有管理员账号', },
parameters: {}, // ---- 内容管理 ----
{
name: 'list-contents', description: '查看所有内容', parameters: {},
}, },
{ {
name: 'toggle-course-status', name: 'create-content', description: '创建内容',
description: '上架或下架一个课程', parameters: { title: { type: 'string', description: '标题(必填)' }, summary: { type: 'string', description: '摘要' }, content: { type: 'string', description: '正文' }, tags: { type: 'string', description: '标签(逗号分隔)' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '状态' } },
parameters: {
id: { type: 'number', description: '课程ID' },
},
}, },
{ {
name: 'toggle-content-status', name: 'update-content', description: '修改内容',
description: '上架或下架一个内容', parameters: { id: { type: 'number', description: '内容ID' }, title: { type: 'string', description: '新标题' }, summary: { type: 'string', description: '新摘要' }, content: { type: 'string', description: '新正文' }, tags: { type: 'string', description: '新标签' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '新状态' } },
parameters: {
id: { type: 'number', description: '内容ID' },
},
}, },
{ {
name: 'toggle-prompt-status', name: 'delete-content', description: '软删除一个内容', parameters: { id: { type: 'number', description: '内容ID' } },
description: '上架或下架一个提示词',
parameters: {
id: { type: 'number', description: '提示词ID' },
},
}, },
{ {
name: 'get-enterprise-orgs', name: 'toggle-content-status', description: '上架/下架一个内容', parameters: { id: { type: 'number', description: '内容ID' } },
description: '查看所有企业版组织', },
parameters: {}, // ---- 提示词 ----
{
name: 'list-prompts', description: '查看所有提示词', parameters: {},
},
{
name: 'create-prompt', description: '创建提示词',
parameters: { title: { type: 'string', description: '标题(必填)' }, content: { type: 'string', description: '提示词内容(必填)' }, description: { type: 'string', description: '描述' }, tags: { type: 'string', description: '标签' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '状态' } },
},
{
name: 'update-prompt', description: '修改提示词',
parameters: { id: { type: 'number', description: '提示词ID' }, title: { type: 'string', description: '新标题' }, content: { type: 'string', description: '新内容' }, description: { type: 'string', description: '新描述' }, tags: { type: 'string', description: '新标签' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], description: '新状态' } },
},
{
name: 'delete-prompt', description: '软删除一个提示词', parameters: { id: { type: 'number', description: '提示词ID' } },
},
{
name: 'toggle-prompt-status', description: '上架/下架一个提示词', parameters: { id: { type: 'number', description: '提示词ID' } },
},
// ---- Banner ----
{
name: 'list-banners', description: '查看所有Banner', parameters: {},
},
{
name: 'create-banner', description: '创建Banner',
parameters: { title: { type: 'string', description: '标题(必填)' }, image: { type: 'string', description: '图片URL(必填)' }, link: { type: 'string', description: '跳转链接' }, position: { type: 'string', description: '位置(默认home' }, sortOrder: { type: 'number', description: '排序(可选)' } },
},
{
name: 'update-banner', description: '修改Banner',
parameters: { id: { type: 'number', description: 'BannerID' }, title: { type: 'string', description: '新标题' }, image: { type: 'string', description: '新图片URL' }, link: { type: 'string', description: '新链接' }, sortOrder: { type: 'number', description: '新排序' } },
},
{
name: 'delete-banner', description: '删除一个Banner', parameters: { id: { type: 'number', description: 'BannerID' } },
},
// ---- 通知 ----
{
name: 'list-notifications', description: '查看系统通知', parameters: {},
},
{
name: 'create-notification', description: '创建系统通知(发送给所有用户或指定用户)',
parameters: { title: { type: 'string', description: '通知标题(必填)' }, content: { type: 'string', description: '通知内容(可选)' }, link: { type: 'string', description: '跳转链接(可选)' }, userId: { type: 'number', description: '指定用户ID,不填则发全体通知(可选)' } },
},
{
name: 'delete-notification', description: '删除一条通知', parameters: { id: { type: 'number', description: '通知ID' } },
},
// ---- 配置 ----
{
name: 'list-config', description: '查看系统配置', parameters: {},
},
{
name: 'update-config', description: '修改系统配置值',
parameters: { key: { type: 'string', description: '配置键(必填)' }, value: { type: 'string', description: '新值(必填)' }, category: { type: 'string', description: '分类(可选)' }, description: { type: 'string', description: '描述(可选)' } },
},
// ---- 角色权限 ----
{
name: 'list-roles', description: '查看所有管理角色', parameters: {},
},
{
name: 'create-role', description: '创建管理角色',
parameters: { name: { type: 'string', description: '角色名称(必填)' }, description: { type: 'string', description: '角色描述' }, permissions: { type: 'array', description: '权限列表,如["dashboard","users","courses"](可选)' } },
},
{
name: 'update-role', description: '修改角色',
parameters: { id: { type: 'string', description: '角色ID' }, name: { type: 'string', description: '新名称' }, description: { type: 'string', description: '新描述' }, permissions: { type: 'array', description: '新权限列表' } },
},
{
name: 'delete-role', description: '禁用一个角色', parameters: { id: { type: 'string', description: '角色ID' } },
},
// ---- 管理员 ----
{
name: 'list-admins', description: '查看所有管理员', parameters: {},
},
{
name: 'create-admin', description: '创建管理员账号',
parameters: { username: { type: 'string', description: '用户名(必填)' }, password: { type: 'string', description: '密码(必填)' }, nickname: { type: 'string', description: '昵称(可选)' }, roleId: { type: 'string', description: '角色ID(可选)' } },
},
{
name: 'update-admin', description: '修改管理员信息',
parameters: { id: { type: 'number', description: '管理员ID' }, nickname: { type: 'string', description: '新昵称' }, roleId: { type: 'string', description: '新角色ID' }, password: { type: 'string', description: '新密码' } },
},
{
name: 'delete-admin', description: '禁用一个管理员账号', parameters: { id: { type: 'number', description: '管理员ID' } },
},
// ---- 企业版 ----
{
name: 'get-enterprise-orgs', description: '查看所有企业组织', parameters: {},
},
{
name: 'create-organization', description: '创建企业组织',
parameters: { name: { type: 'string', description: '组织名称(必填)' }, description: { type: 'string', description: '描述' }, contactName: { type: 'string', description: '联系人' }, contactPhone: { type: 'string', description: '联系电话' } },
},
{
name: 'update-organization', description: '修改企业组织信息',
parameters: { id: { type: 'number', description: '组织ID' }, name: { type: 'string', description: '新名称' }, description: { type: 'string', description: '新描述' }, contactName: { type: 'string', description: '新联系人' }, contactPhone: { type: 'string', description: '新电话' } },
},
{
name: 'delete-organization', description: '删除一个企业组织', parameters: { id: { type: 'number', description: '组织ID' } },
},
{
name: 'add-org-member', description: '向企业组织添加成员',
parameters: { organizationId: { type: 'number', description: '组织ID' }, userId: { type: 'number', description: '用户ID' }, role: { type: 'string', enum: ['ADMIN', 'MEMBER'], description: '角色(默认MEMBER' } },
},
{
name: 'remove-org-member', description: '从企业组织移除成员',
parameters: { organizationId: { type: 'number', description: '组织ID' }, userId: { type: 'number', description: '用户ID' } },
}, },
]; ];
} }
getAvailableTools(): ToolDefinition[] {
return this.crudTools();
}
getToolsDescription(): string { getToolsDescription(): string {
return this.getAvailableTools().map(t => { return this.getAvailableTools().map(t => {
const params = Object.entries(t.parameters) const params = Object.entries(t.parameters)
@@ -299,6 +388,178 @@ export class AdminAiAssistantService {
return { success: true, data: { items, total: items.length }, summary: `${items.length} 个企业组织` }; return { success: true, data: { items, total: items.length }, summary: `${items.length} 个企业组织` };
} }
// ---- 用户 CRUD ----
case 'update-user': {
const { id, ...data } = call.params;
await this.prisma.user.update({ where: { id }, data });
return { success: true, data: { id }, summary: `用户 ${id} 已更新` };
}
case 'delete-user': {
await this.prisma.user.update({ where: { id: call.params.id }, data: { deletedAt: new Date() } });
return { success: true, data: { id: call.params.id }, summary: `用户 ${call.params.id} 已删除` };
}
// ---- 课程 CRUD ----
case 'list-courses': {
const items = await this.prisma.course.findMany({ where: { deletedAt: null }, take: 50, orderBy: { createdAt: 'desc' }, select: { id: true, title: true, price: true, isFree: true, status: true, createdAt: true } });
return { success: true, data: { items, total: items.length }, summary: `${items.length} 个课程` };
}
case 'get-course': {
const item = await this.prisma.course.findUnique({ where: { id: call.params.id } });
if (!item) throw new Error('课程不存在');
return { success: true, data: item, summary: `课程: ${item.title}` };
}
case 'create-course': {
const created = await this.prisma.course.create({ data: { title: call.params.title, description: call.params.description || '', price: call.params.price ?? 0, isFree: call.params.isFree ?? true, status: call.params.status || 'DRAFT' } });
return { success: true, data: created, summary: `课程「${created.title}」已创建 (ID:${created.id})` };
}
case 'update-course': {
const { id: courseId, ...courseData } = call.params;
await this.prisma.course.update({ where: { id: courseId }, data: courseData });
return { success: true, data: { id: courseId }, summary: `课程 ${courseId} 已更新` };
}
case 'delete-course': {
await this.prisma.course.update({ where: { id: call.params.id }, data: { deletedAt: new Date() } });
return { success: true, data: { id: call.params.id }, summary: `课程 ${call.params.id} 已删除` };
}
// ---- 内容 CRUD ----
case 'list-contents': {
const items = await this.prisma.content.findMany({ where: { deletedAt: null }, take: 50, orderBy: { createdAt: 'desc' }, select: { id: true, title: true, status: true, contentType: true, createdAt: true } });
return { success: true, data: { items, total: items.length }, summary: `${items.length} 个内容` };
}
case 'create-content': {
const created = await this.prisma.content.create({ data: { title: call.params.title, summary: call.params.summary || '', content: call.params.content || '', tags: call.params.tags || '', status: call.params.status || 'DRAFT' } });
return { success: true, data: created, summary: `内容「${created.title}」已创建 (ID:${created.id})` };
}
case 'update-content': {
const { id: contentId, ...contentData } = call.params;
await this.prisma.content.update({ where: { id: contentId }, data: contentData });
return { success: true, data: { id: contentId }, summary: `内容 ${contentId} 已更新` };
}
case 'delete-content': {
await this.prisma.content.update({ where: { id: call.params.id }, data: { deletedAt: new Date() } });
return { success: true, data: { id: call.params.id }, summary: `内容 ${call.params.id} 已删除` };
}
// ---- 提示词 CRUD ----
case 'list-prompts': {
const items = await this.prisma.prompt.findMany({ where: { deletedAt: null }, take: 50, orderBy: { createdAt: 'desc' }, select: { id: true, title: true, status: true, isPublic: true, createdAt: true } });
return { success: true, data: { items, total: items.length }, summary: `${items.length} 个提示词` };
}
case 'create-prompt': {
const created = await this.prisma.prompt.create({ data: { title: call.params.title, content: call.params.content, description: call.params.description || '', tags: call.params.tags || '', status: call.params.status || 'PUBLISHED' } });
return { success: true, data: created, summary: `提示词「${created.title}」已创建 (ID:${created.id})` };
}
case 'update-prompt': {
const { id: promptId, ...promptData } = call.params;
await this.prisma.prompt.update({ where: { id: promptId }, data: promptData });
return { success: true, data: { id: promptId }, summary: `提示词 ${promptId} 已更新` };
}
case 'delete-prompt': {
await this.prisma.prompt.update({ where: { id: call.params.id }, data: { deletedAt: new Date() } });
return { success: true, data: { id: call.params.id }, summary: `提示词 ${call.params.id} 已删除` };
}
// ---- Banner CRUD ----
case 'create-banner': {
const created = await this.prisma.banner.create({ data: { title: call.params.title, image: call.params.image, link: call.params.link || '', position: call.params.position || 'home', sortOrder: call.params.sortOrder ?? 0 } });
return { success: true, data: created, summary: `Banner「${created.title}」已创建` };
}
case 'update-banner': {
const { id: bannerId, ...bannerData } = call.params;
await this.prisma.banner.update({ where: { id: bannerId }, data: bannerData });
return { success: true, data: { id: bannerId }, summary: `Banner ${bannerId} 已更新` };
}
case 'delete-banner': {
await this.prisma.banner.delete({ where: { id: call.params.id } });
return { success: true, data: { id: call.params.id }, summary: `Banner ${call.params.id} 已删除` };
}
// ---- 通知 ----
case 'create-notification': {
const { title: notifTitle, content: notifContent, link: notifLink, userId: notifUserId } = call.params;
if (notifUserId) {
await this.prisma.notification.create({ data: { userId: notifUserId, type: 'system', title: notifTitle, content: notifContent || '', link: notifLink || '' } });
return { success: true, data: {}, summary: `通知已发送给用户 ${notifUserId}` };
}
const users = await this.prisma.user.findMany({ where: { deletedAt: null }, select: { id: true } });
await this.prisma.notification.createMany({ data: users.map(u => ({ userId: u.id, type: 'system', title: notifTitle, content: notifContent || '', link: notifLink || '' })) });
return { success: true, data: { userCount: users.length }, summary: `通知已发送给 ${users.length} 个用户` };
}
case 'delete-notification': {
await this.prisma.notification.delete({ where: { id: call.params.id } });
return { success: true, data: { id: call.params.id }, summary: `通知 ${call.params.id} 已删除` };
}
// ---- 配置 ----
case 'update-config': {
const { key: cfgKey, value: cfgValue, category: cfgCategory, description: cfgDesc } = call.params;
const existing = await this.prisma.systemConfig.findUnique({ where: { key: cfgKey } });
if (existing) {
await this.prisma.systemConfig.update({ where: { key: cfgKey }, data: { value: cfgValue } });
return { success: true, data: { key: cfgKey }, summary: `配置 ${cfgKey} 已更新为 ${cfgValue}` };
}
await this.prisma.systemConfig.create({ data: { key: cfgKey, value: cfgValue, category: cfgCategory || 'general', description: cfgDesc || '' } });
return { success: true, data: { key: cfgKey }, summary: `配置 ${cfgKey} 已创建` };
}
// ---- 角色 CRUD ----
case 'create-role': {
const created = await this.prisma.adminRole.create({ data: { name: call.params.name, description: call.params.description || '', permissions: call.params.permissions || [] } });
return { success: true, data: created, summary: `角色「${created.name}」已创建 (ID:${created.id})` };
}
case 'update-role': {
const { id: roleId, ...roleData } = call.params;
if (roleData.permissions) roleData.permissions = JSON.stringify(roleData.permissions);
await this.prisma.adminRole.update({ where: { id: roleId }, data: roleData });
return { success: true, data: { id: roleId }, summary: `角色 ${roleId} 已更新` };
}
case 'delete-role': {
await this.prisma.adminRole.update({ where: { id: call.params.id }, data: { status: 'INACTIVE' } });
return { success: true, data: { id: call.params.id }, summary: `角色 ${call.params.id} 已禁用` };
}
// ---- 管理员 CRUD ----
case 'create-admin': {
const hash = await bcrypt.hash(call.params.password, 10);
const created = await this.prisma.adminUser.create({ data: { username: call.params.username, passwordHash: hash, nickname: call.params.nickname || call.params.username, roleId: call.params.roleId || null } });
return { success: true, data: { id: created.id }, summary: `管理员「${created.username}」已创建` };
}
case 'update-admin': {
const { id: adminId, password, ...adminData } = call.params;
if (password) adminData['passwordHash'] = await bcrypt.hash(password, 10);
await this.prisma.adminUser.update({ where: { id: adminId }, data: adminData as any });
return { success: true, data: { id: adminId }, summary: `管理员 ${adminId} 已更新` };
}
case 'delete-admin': {
await this.prisma.adminUser.update({ where: { id: call.params.id }, data: { status: 'INACTIVE' } });
return { success: true, data: { id: call.params.id }, summary: `管理员 ${call.params.id} 已禁用` };
}
// ---- 企业版 ----
case 'create-organization': {
const created = await this.prisma.organization.create({ data: { name: call.params.name, description: call.params.description || '', contactName: call.params.contactName || '', contactPhone: call.params.contactPhone || '' } });
return { success: true, data: created, summary: `组织「${created.name}」已创建` };
}
case 'update-organization': {
const { id: orgId, ...orgData } = call.params;
await this.prisma.organization.update({ where: { id: orgId }, data: orgData });
return { success: true, data: { id: orgId }, summary: `组织 ${orgId} 已更新` };
}
case 'delete-organization': {
await this.prisma.organization.update({ where: { id: call.params.id }, data: { status: 'INACTIVE' } });
return { success: true, data: { id: call.params.id }, summary: `组织 ${call.params.id} 已删除` };
}
case 'add-org-member': {
await this.prisma.organizationMember.create({ data: { organizationId: call.params.organizationId, userId: call.params.userId, role: call.params.role || 'MEMBER' } });
return { success: true, data: {}, summary: `用户 ${call.params.userId} 已加入组织 ${call.params.organizationId}` };
}
case 'remove-org-member': {
await this.prisma.organizationMember.delete({ where: { organizationId_userId: { organizationId: call.params.organizationId, userId: call.params.userId } } });
return { success: true, data: {}, summary: `用户 ${call.params.userId} 已从组织 ${call.params.organizationId} 移除` };
}
default: default:
throw new Error(`未知工具: ${call.tool}`); throw new Error(`未知工具: ${call.tool}`);
} }
@@ -0,0 +1,38 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { PrismaService } from '../../prisma/prisma.service';
@ApiTags('Public')
@Controller('public')
export class PublicController {
constructor(private prisma: PrismaService) {}
@Get('banners')
async banners(@Query('position') position?: string) {
const now = new Date();
const where: any = {
status: { not: 'DRAFT' },
AND: [
{ OR: [{ startAt: null }, { startAt: { lte: now } }] },
{ OR: [{ endAt: null }, { endAt: { gte: now } }] },
],
};
if (position) where.position = position;
const items = await this.prisma.banner.findMany({ where, orderBy: { sortOrder: 'asc' } });
return { items };
}
@Get('config')
async getConfig() {
const configs = await this.prisma.systemConfig.findMany({ where: { status: 'ACTIVE' } });
const result: Record<string, string> = {};
for (const c of configs) result[c.key] = c.value;
return result;
}
@Get('models')
async getModels() {
const items = await this.prisma.aiModel.findMany({ where: { status: 'ACTIVE' }, orderBy: { sortOrder: 'asc' } });
return { items: items.map(m => ({ id: m.id, name: m.name, provider: m.provider, description: m.description, capabilities: m.capabilities, pricing: m.pricing, isFree: m.isFree })) };
}
}
File diff suppressed because one or more lines are too long
+30 -28
View File
@@ -10,12 +10,14 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { useAuth } from '@/lib/auth-context'; import { useAuth } from '@/lib/auth-context';
import { useT } from '@/i18n';
import { API_BASE } from '@/lib/config'; import { API_BASE } from '@/lib/config';
function AuthForm() { function AuthForm() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const { login } = useAuth(); const { login } = useAuth();
const t = useT();
const [tab, setTab] = useState<'login' | 'register'>(() => const [tab, setTab] = useState<'login' | 'register'>(() =>
searchParams.get('tab') === 'register' ? 'register' : 'login' searchParams.get('tab') === 'register' ? 'register' : 'login'
); );
@@ -28,7 +30,7 @@ function AuthForm() {
async function handleLogin(e: FormEvent) { async function handleLogin(e: FormEvent) {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
if (!loginForm.account || !loginForm.password) { setError('请填写账号和密码'); return; } if (!loginForm.account || !loginForm.password) { setError(t.auth.fillAccountAndPassword); return; }
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`${API_BASE}/auth/login`, { const res = await fetch(`${API_BASE}/auth/login`, {
@@ -36,9 +38,9 @@ function AuthForm() {
body: JSON.stringify({ account: loginForm.account, password: loginForm.password }), body: JSON.stringify({ account: loginForm.account, password: loginForm.password }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.message || '登录失败'); if (!res.ok) throw new Error(data.message || t.auth.loginFailed);
login(data.accessToken, data.refreshToken); login(data.accessToken, data.refreshToken);
toast.success('登录成功', { description: '欢迎回来!' }); toast.success(t.auth.loginSuccess);
router.push('/'); router.push('/');
} catch (err: any) { setError(err.message); } } catch (err: any) { setError(err.message); }
finally { setLoading(false); } finally { setLoading(false); }
@@ -48,10 +50,10 @@ function AuthForm() {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
const { phone, email, password, confirmPassword, nickname } = registerForm; const { phone, email, password, confirmPassword, nickname } = registerForm;
if (!phone && !email) { setError('请填写手机号或邮箱'); return; } if (!phone && !email) { setError(t.auth.fillPhoneOrEmail); return; }
if (!password) { setError('请填写密码'); return; } if (!password) { setError(t.auth.fillPassword); return; }
if (password.length < 6) { setError('密码至少 6 位'); return; } if (password.length < 6) { setError(t.auth.passwordMinLength); return; }
if (password !== confirmPassword) { setError('两次密码不一致'); return; } if (password !== confirmPassword) { setError(t.auth.passwordsNotMatch); return; }
setLoading(true); setLoading(true);
try { try {
const body: Record<string, string> = { password }; const body: Record<string, string> = { password };
@@ -62,9 +64,9 @@ function AuthForm() {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.message || '注册失败'); if (!res.ok) throw new Error(data.message || t.auth.registerFailed);
login(data.accessToken, data.refreshToken); login(data.accessToken, data.refreshToken);
toast.success('注册成功', { description: '欢迎加入宇之然!' }); toast.success(t.auth.registerSuccess);
router.push('/'); router.push('/');
} catch (err: any) { setError(err.message); } } catch (err: any) { setError(err.message); }
finally { setLoading(false); } finally { setLoading(false); }
@@ -77,16 +79,16 @@ function AuthForm() {
<div className="mx-auto mb-3 w-12 h-12 bg-gradient-to-br from-brand-500 to-brand-700 rounded-2xl flex items-center justify-center"> <div className="mx-auto mb-3 w-12 h-12 bg-gradient-to-br from-brand-500 to-brand-700 rounded-2xl flex items-center justify-center">
<span className="text-white font-bold text-lg">Y</span> <span className="text-white font-bold text-lg">Y</span>
</div> </div>
<CardTitle className="text-xl">{tab === 'login' ? '欢迎回来' : '加入宇之然'}</CardTitle> <CardTitle className="text-xl">{tab === 'login' ? t.auth.welcomeBack : t.auth.joinTitle}</CardTitle>
<CardDescription> <CardDescription>
{tab === 'login' ? '登录继续你的 AI 探索之旅' : '免费注册,开始探索 AI'} {tab === 'login' ? t.auth.loginSubtitle : t.auth.registerSubtitle}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Tabs value={tab} onValueChange={(v) => { setTab(v as 'login' | 'register'); setError(''); }}> <Tabs value={tab} onValueChange={(v) => { setTab(v as 'login' | 'register'); setError(''); }}>
<TabsList className="w-full mb-6"> <TabsList className="w-full mb-6">
<TabsTrigger value="login" className="flex-1"></TabsTrigger> <TabsTrigger value="login" className="flex-1">{t.auth.loginTitle}</TabsTrigger>
<TabsTrigger value="register" className="flex-1"></TabsTrigger> <TabsTrigger value="register" className="flex-1">{t.auth.registerTitle}</TabsTrigger>
</TabsList> </TabsList>
{error && ( {error && (
@@ -97,38 +99,38 @@ function AuthForm() {
<TabsContent value="login"> <TabsContent value="login">
<form onSubmit={handleLogin} className="space-y-4"> <form onSubmit={handleLogin} className="space-y-4">
<Input type="text" placeholder="手机号 / 邮箱" value={loginForm.account} <Input type="text" placeholder={t.auth.accountPlaceholder} value={loginForm.account}
onChange={(e) => setLoginForm({ ...loginForm, account: e.target.value })} /> onChange={(e) => setLoginForm({ ...loginForm, account: e.target.value })} />
<Input type="password" placeholder="密码" value={loginForm.password} <Input type="password" placeholder={t.auth.password} value={loginForm.password}
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })} /> onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })} />
<Button type="submit" disabled={loading} className="w-full"> <Button type="submit" disabled={loading} className="w-full">
{loading ? '登录中...' : '登录'} {loading ? t.auth.loggingIn : t.auth.loginTitle}
</Button> </Button>
</form> </form>
</TabsContent> </TabsContent>
<TabsContent value="register"> <TabsContent value="register">
<form onSubmit={handleRegister} className="space-y-4"> <form onSubmit={handleRegister} className="space-y-4">
<Input type="text" placeholder="手机号(选填)" value={registerForm.phone} <Input type="text" placeholder={t.auth.phone} value={registerForm.phone}
onChange={(e) => setRegisterForm({ ...registerForm, phone: e.target.value })} /> onChange={(e) => setRegisterForm({ ...registerForm, phone: e.target.value })} />
<Input type="email" placeholder="邮箱(选填,与手机号至少填一项)" value={registerForm.email} <Input type="email" placeholder="Email" value={registerForm.email}
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })} /> onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })} />
<Input type="text" placeholder="昵称(选填)" value={registerForm.nickname} <Input type="text" placeholder={t.auth.nicknameOptional} value={registerForm.nickname}
onChange={(e) => setRegisterForm({ ...registerForm, nickname: e.target.value })} /> onChange={(e) => setRegisterForm({ ...registerForm, nickname: e.target.value })} />
<Input type="password" placeholder="密码(至少 6 位)" value={registerForm.password} <Input type="password" placeholder={t.auth.passwordHint} value={registerForm.password}
onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })} /> onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })} />
<Input type="password" placeholder="确认密码" value={registerForm.confirmPassword} <Input type="password" placeholder={t.auth.confirmPassword} value={registerForm.confirmPassword}
onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} /> onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} />
<Button type="submit" disabled={loading} className="w-full"> <Button type="submit" disabled={loading} className="w-full">
{loading ? '注册中...' : '注册'} {loading ? t.auth.registering : t.auth.registerTitle}
</Button> </Button>
<p className="text-xs text-muted-foreground text-center leading-relaxed"> <p className="text-xs text-muted-foreground text-center leading-relaxed">
{' '} {t.auth.agreePrefix}{' '}
<Link href="/terms" className="text-brand-600 hover:underline dark:text-brand-400"></Link> <Link href="/terms" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.termsOfService}</Link>
{' '}{' '} {' '}{' '}
<Link href="/privacy" className="text-brand-600 hover:underline dark:text-brand-400"></Link> <Link href="/privacy" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.privacyPolicy}</Link>
{' '}{' '} {' '}{' '}
<Link href="/ai-agreement" className="text-brand-600 hover:underline dark:text-brand-400">AI </Link> <Link href="/ai-agreement" className="text-brand-600 hover:underline dark:text-brand-400">{t.auth.aiAgreement}</Link>
</p> </p>
</form> </form>
</TabsContent> </TabsContent>
+38 -98
View File
@@ -7,28 +7,13 @@ import { apiFetch } from "../../lib/auth";
import { useAuth } from "@/lib/auth-context"; import { useAuth } from "@/lib/auth-context";
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { useT } from '@/i18n';
interface Post { interface Post { id: number; title: string; content: string; tags?: string | null; viewCount: number; likeCount: number; commentCount: number; createdAt: string; user: { id: number; nickname: string; avatar: string | null }; comments?: Comment[] }
id: number; interface Comment { id: number; content: string; createdAt: string; user: { id: number; nickname: string; avatar: string | null } }
title: string;
content: string;
tags?: string | null;
viewCount: number;
likeCount: number;
commentCount: number;
createdAt: string;
user: { id: number; nickname: string; avatar: string | null };
comments?: Comment[];
}
interface Comment {
id: number;
content: string;
createdAt: string;
user: { id: number; nickname: string; avatar: string | null };
}
export default function CommunityPage() { export default function CommunityPage() {
const t = useT();
const router = useRouter(); const router = useRouter();
const [posts, setPosts] = useState<Post[]>([]); const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -45,9 +30,7 @@ export default function CommunityPage() {
setLoading(true); setLoading(true);
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const url = activeTab === 'feed' && token const url = activeTab === 'feed' && token ? '/community/feed' : '/community/posts';
? '/community/feed'
: '/community/posts';
const res = await apiFetch(url); const res = await apiFetch(url);
const data = await res.json(); const data = await res.json();
setPosts(data.items || []); setPosts(data.items || []);
@@ -66,10 +49,7 @@ export default function CommunityPage() {
if (post.user?.id) { if (post.user?.id) {
try { try {
const res = await apiFetch(`/community/users/${post.user.id}/follow`); const res = await apiFetch(`/community/users/${post.user.id}/follow`);
if (res.ok) { if (res.ok) { const d = await res.json(); if (d.followed) followed.add(post.user.id); }
const d = await res.json();
if (d.followed) followed.add(post.user.id);
}
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
} }
@@ -81,37 +61,23 @@ export default function CommunityPage() {
if (!title.trim() || !content.trim()) return; if (!title.trim() || !content.trim()) return;
setSubmitting(true); setSubmitting(true);
try { try {
await apiFetch("/community/posts", { await apiFetch("/community/posts", { method: "POST", body: JSON.stringify({ title, content, tags: tags || undefined }) });
method: "POST", setTitle(""); setContent(""); setTags(""); setShowForm(false);
body: JSON.stringify({ title, content, tags: tags || undefined }),
});
setTitle(""); setContent(""); setTags("");
setShowForm(false);
loadPosts(); loadPosts();
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
setSubmitting(false); setSubmitting(false);
} }
async function handleLike(postId: number) { async function handleLike(postId: number) {
try { try { await apiFetch(`/community/posts/${postId}/like`, { method: "POST" }); loadPosts(); } catch (e) { console.error(e) }
await apiFetch(`/community/posts/${postId}/like`, { method: "POST" });
loadPosts();
} catch (e) { console.error(e) }
} }
async function handleFollow(userId: number) { async function handleFollow(userId: number) {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token'); if (!token) return;
if (!token) return;
try { try {
const isFollowed = followedUsers.has(userId); const isFollowed = followedUsers.has(userId);
await apiFetch(`/community/users/${userId}/follow`, { await apiFetch(`/community/users/${userId}/follow`, { method: isFollowed ? 'DELETE' : 'POST' });
method: isFollowed ? 'DELETE' : 'POST', setFollowedUsers(prev => { const next = new Set(prev); isFollowed ? next.delete(userId) : next.add(userId); return next; });
});
setFollowedUsers(prev => {
const next = new Set(prev);
isFollowed ? next.delete(userId) : next.add(userId);
return next;
});
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
@@ -125,12 +91,8 @@ export default function CommunityPage() {
if (!comment.trim()) return; if (!comment.trim()) return;
setSubmittingComment(true); setSubmittingComment(true);
try { try {
await apiFetch(`/community/posts/${post.id}/comments`, { await apiFetch(`/community/posts/${post.id}/comments`, { method: "POST", body: JSON.stringify({ content: comment }) });
method: "POST", setComment(""); loadPosts();
body: JSON.stringify({ content: comment }),
});
setComment("");
loadPosts();
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
setSubmittingComment(false); setSubmittingComment(false);
} }
@@ -149,12 +111,8 @@ export default function CommunityPage() {
</Link> </Link>
{isLoggedIn && ( {isLoggedIn && (
<button onClick={() => handleFollow(post.user.id)} <button onClick={() => handleFollow(post.user.id)}
className={`ml-auto text-xs px-2 py-1 rounded transition-colors ${ className={`ml-auto text-xs px-2 py-1 rounded transition-colors ${followedUsers.has(post.user.id) ? 'bg-muted text-muted-foreground hover:bg-accent' : 'bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-900/50'}`}>
followedUsers.has(post.user.id) {followedUsers.has(post.user.id) ? t.community.followed : t.community.follow}
? 'bg-muted text-muted-foreground hover:bg-accent'
: 'bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-900/50'
}`}>
{followedUsers.has(post.user.id) ? '已关注' : '+ 关注'}
</button> </button>
)} )}
</div> </div>
@@ -170,21 +128,15 @@ export default function CommunityPage() {
</div> </div>
)} )}
<div className="flex items-center gap-6 text-sm text-muted-foreground"> <div className="flex items-center gap-6 text-sm text-muted-foreground">
<button onClick={() => handleLike(post.id)} className="flex items-center gap-1 hover:text-brand-600 transition-colors"> <button onClick={() => handleLike(post.id)} className="flex items-center gap-1 hover:text-brand-600 transition-colors"> {post.likeCount}</button>
{post.likeCount} <button onClick={() => setExpanded(!expanded)} className="hover:text-brand-600 transition-colors">💬 {post.commentCount}</button>
</button>
<button onClick={() => setExpanded(!expanded)} className="hover:text-brand-600 transition-colors">
💬 {post.commentCount}
</button>
<span>👁 {post.viewCount}</span> <span>👁 {post.viewCount}</span>
</div> </div>
{expanded && ( {expanded && (
<div className="mt-4 pt-4 border-t border-border"> <div className="mt-4 pt-4 border-t border-border">
{post.comments?.map((c: Comment) => ( {post.comments?.map((c: Comment) => (
<div key={c.id} className="flex gap-3 mb-3"> <div key={c.id} className="flex gap-3 mb-3">
<Avatar className="w-6 h-6"> <Avatar className="w-6 h-6"><AvatarFallback className="text-[10px]">{c.user.nickname?.[0] || "U"}</AvatarFallback></Avatar>
<AvatarFallback className="text-[10px]">{c.user.nickname?.[0] || "U"}</AvatarFallback>
</Avatar>
<div className="flex-1"> <div className="flex-1">
<div className="text-xs text-muted-foreground mb-1">{c.user.nickname} · {new Date(c.createdAt).toLocaleDateString()}</div> <div className="text-xs text-muted-foreground mb-1">{c.user.nickname} · {new Date(c.createdAt).toLocaleDateString()}</div>
<p className="text-sm text-muted-foreground">{c.content}</p> <p className="text-sm text-muted-foreground">{c.content}</p>
@@ -192,12 +144,11 @@ export default function CommunityPage() {
</div> </div>
))} ))}
<form onSubmit={handleComment} className="mt-3 flex gap-2"> <form onSubmit={handleComment} className="mt-3 flex gap-2">
<input value={comment} onChange={e => setComment(e.target.value)} <input value={comment} onChange={e => setComment(e.target.value)} placeholder={t.community.commentPlaceholder}
placeholder="写下你的评论..."
className="flex-1 px-3 py-2 bg-background border border-input rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> className="flex-1 px-3 py-2 bg-background border border-input rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
<button type="submit" disabled={submittingComment} <button type="submit" disabled={submittingComment}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700 disabled:opacity-50"> className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700 disabled:opacity-50">
{submittingComment ? "发送中..." : "评论"} {submittingComment ? t.community.sending : t.community.comment}
</button> </button>
</form> </form>
</div> </div>
@@ -206,57 +157,46 @@ export default function CommunityPage() {
); );
} }
if (loading) { if (loading) return (
return ( <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <Skeleton className="h-8 w-48 mb-6" /><Skeleton className="h-4 w-32 mb-8" />
<Skeleton className="h-8 w-48 mb-6" /> <Skeleton className="h-64 w-full mb-4" /><Skeleton className="h-4 w-full mb-2" /><Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-32 mb-8" /> </div>
<Skeleton className="h-64 w-full mb-4" /> );
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</div>
);
}
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-foreground"></h1> <h1 className="text-3xl font-bold text-foreground">{t.community.title}</h1>
<p className="mt-2 text-muted-foreground"> AI </p> <p className="mt-2 text-muted-foreground">{t.community.desc}</p>
</div> </div>
<button onClick={() => setShowForm(!showForm)} <button onClick={() => setShowForm(!showForm)}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 transition-colors"> className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 transition-colors">
{showForm ? "取消" : "+ 发帖"} {showForm ? t.common.cancel : t.community.createPost}
</button> </button>
</div> </div>
<div className="flex gap-1 mb-6 bg-muted rounded-lg p-1"> <div className="flex gap-1 mb-6 bg-muted rounded-lg p-1">
<button onClick={() => setActiveTab('latest')} <button onClick={() => setActiveTab('latest')}
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${ className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'latest' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>{t.community.latest}</button>
activeTab === 'latest' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
}`}></button>
<button onClick={() => setActiveTab('feed')} <button onClick={() => setActiveTab('feed')}
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${ className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'feed' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>{t.community.following}</button>
activeTab === 'feed' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
}`}></button>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-card rounded-xl border border-border p-6 mb-6"> <form onSubmit={handleCreate} className="bg-card rounded-xl border border-border p-6 mb-6">
<h3 className="text-lg font-semibold text-foreground mb-4"></h3> <h3 className="text-lg font-semibold text-foreground mb-4">{t.community.newPost}</h3>
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="标题" <input value={title} onChange={e => setTitle(e.target.value)} placeholder={t.community.postTitle}
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" /> className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" />
<textarea value={content} onChange={e => setContent(e.target.value)} <textarea value={content} onChange={e => setContent(e.target.value)} placeholder={t.community.contentPlaceholder} rows={4}
placeholder="分享你的 AI 学习心得、实战经验..." rows={4}
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring resize-none" /> className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
<input value={tags} onChange={e => setTags(e.target.value)} <input value={tags} onChange={e => setTags(e.target.value)} placeholder={t.community.tagsPlaceholder}
placeholder="标签(逗号分隔,如:AI,提示词)"
className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" /> className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-ring" />
<div className="flex justify-end"> <div className="flex justify-end">
<button type="submit" disabled={submitting} <button type="submit" disabled={submitting}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50"> className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
{submitting ? "发布中..." : "发布"} {submitting ? t.community.publishing : t.community.publish}
</button> </button>
</div> </div>
</form> </form>
@@ -264,7 +204,7 @@ export default function CommunityPage() {
{posts.length === 0 ? ( {posts.length === 0 ? (
<div className="text-center py-20 text-muted-foreground"> <div className="text-center py-20 text-muted-foreground">
<p>{activeTab === 'feed' ? '关注更多用户,发现精彩内容' : '还没有帖子,来发第一帖吧!'}</p> <p>{activeTab === 'feed' ? t.community.feedEmpty : t.community.postsEmpty}</p>
</div> </div>
) : ( ) : (
<div>{posts.map(post => <PostCard key={post.id} post={post} />)}</div> <div>{posts.map(post => <PostCard key={post.id} post={post} />)}</div>
+12 -15
View File
@@ -5,13 +5,11 @@ import Link from 'next/link';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { BookOpen, Users } from 'lucide-react'; import { BookOpen } from 'lucide-react';
import { API_BASE } from '@/lib/config'; import { API_BASE } from '@/lib/config';
import { useT } from '@/i18n';
interface Course { interface Course { id: number; title: string; description: string; cover: string | null; isFree: boolean; chapters?: { lessons: any[] }[] }
id: number; title: string; description: string; cover: string | null;
isFree: boolean; chapters?: { lessons: any[] }[];
}
function CourseSkeleton() { function CourseSkeleton() {
return ( return (
@@ -25,6 +23,7 @@ function CourseSkeleton() {
} }
export default function CoursesPage() { export default function CoursesPage() {
const t = useT();
const [courses, setCourses] = useState<Course[]>([]); const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -37,18 +36,16 @@ export default function CoursesPage() {
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-10"> <div className="mb-10">
<h1 className="text-3xl font-bold text-foreground"></h1> <h1 className="text-3xl font-bold text-foreground">{t.courses.title}</h1>
<p className="mt-2 text-muted-foreground"> AI</p> <p className="mt-2 text-muted-foreground">{t.courses.desc}</p>
</div> </div>
{loading ? ( {loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">{[1,2,3,4,5,6].map(i => <CourseSkeleton key={i} />)}</div>
{[1,2,3,4,5,6].map(i => <CourseSkeleton key={i} />)}
</div>
) : courses.length === 0 ? ( ) : courses.length === 0 ? (
<div className="text-center py-20 text-muted-foreground"> <div className="text-center py-20 text-muted-foreground">
<BookOpen className="w-12 h-12 mx-auto mb-4 opacity-30" /> <BookOpen className="w-12 h-12 mx-auto mb-4 opacity-30" />
<p></p> <p>{t.courses.empty}</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -63,15 +60,15 @@ export default function CoursesPage() {
</div> </div>
)} )}
<div className="p-5"> <div className="p-5">
<Badge variant={course.isFree ? 'success' : 'destructive'} className="mb-3"> <Badge variant={course.isFree ? 'secondary' : 'destructive'} className="mb-3">
{course.isFree ? '免费' : '付费'} {course.isFree ? t.courses.free : t.courses.paid}
</Badge> </Badge>
<h3 className="font-semibold group-hover:text-brand-600 transition-colors mb-2">{course.title}</h3> <h3 className="font-semibold group-hover:text-brand-600 transition-colors mb-2">{course.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">{course.description}</p> <p className="text-sm text-muted-foreground line-clamp-2">{course.description}</p>
{course.chapters && ( {course.chapters && (
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground"> <div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
<Users className="w-3.5 h-3.5" /> <BookOpen className="w-3.5 h-3.5" />
<span>{course.chapters.reduce((s, ch) => s + (ch.lessons?.length || 0), 0)} </span> <span>{t.courses.moduleCount.replace('{n}', String(course.chapters.reduce((s, ch) => s + (ch.lessons?.length || 0), 0)))}</span>
</div> </div>
)} )}
</div> </div>
+86 -212
View File
@@ -6,56 +6,18 @@ import { Progress } from '@/components/ui/progress';
import Link from 'next/link'; import Link from 'next/link';
import { apiFetch, isLoggedIn, clearTokens } from '../../lib/auth'; import { apiFetch, isLoggedIn, clearTokens } from '../../lib/auth';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useT } from '@/i18n';
interface Stats { interface Stats { inProgressCourses: number; completedLessons: number; favoritePrompts: number; studyDays: number; todayLearned: number }
inProgressCourses: number; interface UserInfo { nickname: string; avatar: string | null; memberPlan: string; memberExpire: string | null; sandboxDaily: number; joinedAt: string }
completedLessons: number; interface CourseProgress { course: { id: number; title: string; cover: string | null }; progress: number; completedCount: number; totalCount: number; recentLessons: { id: number; title: string; completed: boolean; progress: number; updatedAt: string }[] }
favoritePrompts: number; interface RecentRecord { lessonId: number; lessonTitle: string; courseId: number; courseTitle: string; completed: boolean; progress: number; updatedAt: string }
studyDays: number; interface PromptFavorite { id: number; promptId: number; title: string; description: string | null; model: string | null; viewCount: number; likeCount: number; favoritedAt: string }
todayLearned: number;
}
interface UserInfo {
nickname: string;
avatar: string | null;
memberPlan: string;
memberExpire: string | null;
sandboxDaily: number;
joinedAt: string;
}
interface CourseProgress {
course: { id: number; title: string; cover: string | null };
progress: number;
completedCount: number;
totalCount: number;
recentLessons: { id: number; title: string; completed: boolean; progress: number; updatedAt: string }[];
}
interface RecentRecord {
lessonId: number;
lessonTitle: string;
courseId: number;
courseTitle: string;
completed: boolean;
progress: number;
updatedAt: string;
}
interface PromptFavorite {
id: number;
promptId: number;
title: string;
description: string | null;
model: string | null;
viewCount: number;
likeCount: number;
favoritedAt: string;
}
type Tab = 'progress' | 'favorites' | 'profile'; type Tab = 'progress' | 'favorites' | 'profile';
export default function DashboardPage() { export default function DashboardPage() {
const t = useT();
const router = useRouter(); const router = useRouter();
const [activeTab, setActiveTab] = useState<Tab>('progress'); const [activeTab, setActiveTab] = useState<Tab>('progress');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -70,149 +32,97 @@ export default function DashboardPage() {
const [saveMsg, setSaveMsg] = useState(''); const [saveMsg, setSaveMsg] = useState('');
useEffect(() => { useEffect(() => {
if (!isLoggedIn()) { if (!isLoggedIn()) { router.push('/auth'); return; }
router.push('/auth');
return;
}
loadAll(); loadAll();
}, []); }, []);
async function loadAll() { async function loadAll() {
setLoading(true); setLoading(true); setError('');
setError('');
try { try {
const [statsRes, progressRes, favRes, profileRes] = await Promise.all([ const [statsRes, progressRes, favRes, profileRes] = await Promise.all([
apiFetch('/dashboard/stats'), apiFetch('/dashboard/stats'), apiFetch('/dashboard/progress'), apiFetch('/dashboard/favorites'), apiFetch('/dashboard/profile'),
apiFetch('/dashboard/progress'),
apiFetch('/dashboard/favorites'),
apiFetch('/dashboard/profile'),
]); ]);
if (!statsRes.ok || !progressRes.ok || !favRes.ok || !profileRes.ok) throw new Error(t.dashboard.loadFailed);
if (!statsRes.ok || !progressRes.ok || !favRes.ok || !profileRes.ok) {
throw new Error('加载数据失败');
}
const statsData = await statsRes.json(); const statsData = await statsRes.json();
setStats(statsData.stats); setUserInfo(statsData.user);
const progressData = await progressRes.json(); const progressData = await progressRes.json();
const favData = await favRes.json();
const profileData = await profileRes.json();
setStats(statsData.stats);
setUserInfo(statsData.user);
setCourses(progressData.courses || []); setCourses(progressData.courses || []);
setRecentRecords(progressData.recentRecords || []); setRecentRecords(progressData.recentRecords || []);
setFavorites(favData || []); setFavorites((await favRes.json()) || []);
setNickname(profileData.nickname || ''); setNickname((await profileRes.json()).nickname || '');
} catch (e: any) { } catch (e: any) {
if (e.message?.includes('401') || e.message?.includes('Unauthorized')) { if (e.message?.includes('401') || e.message?.includes('Unauthorized')) { clearTokens(); router.push('/auth'); }
clearTokens(); setError(e.message || t.dashboard.loadFailed);
router.push('/auth'); } finally { setLoading(false); }
}
setError(e.message || '加载失败');
} finally {
setLoading(false);
}
} }
async function handleSaveProfile() { async function handleSaveProfile() {
setSaving(true); setSaving(true); setSaveMsg('');
setSaveMsg('');
try { try {
const res = await apiFetch('/dashboard/profile', { const res = await apiFetch('/dashboard/profile', { method: 'PUT', body: JSON.stringify({ nickname }) });
method: 'PUT', if (!res.ok) throw new Error(t.dashboard.saveFailed);
body: JSON.stringify({ nickname }), setSaveMsg(t.dashboard.saveSuccess);
});
if (!res.ok) throw new Error('保存失败');
setSaveMsg('保存成功');
setUserInfo(prev => prev ? { ...prev, nickname } : prev); setUserInfo(prev => prev ? { ...prev, nickname } : prev);
} catch { } catch { setSaveMsg(t.dashboard.saveFailed); }
setSaveMsg('保存失败'); finally { setSaving(false); setTimeout(() => setSaveMsg(''), 2000); }
} finally {
setSaving(false);
setTimeout(() => setSaveMsg(''), 2000);
}
} }
function handleLogout() { function handleLogout() { clearTokens(); router.push('/'); }
clearTokens();
router.push('/');
}
if (loading) { if (loading) return (
return ( <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <Skeleton className="h-8 w-48 mb-2" /><Skeleton className="h-5 w-72 mb-8" />
<Skeleton className="h-8 w-48 mb-2" /> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">{[1,2,3,4].map(i => <Skeleton key={i} className="h-24 rounded-xl" />)}</div>
<Skeleton className="h-5 w-72 mb-8" /> <Skeleton className="h-64 rounded-xl" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> </div>
{[1,2,3,4].map(i => <Skeleton key={i} className="h-24 rounded-xl" />)} );
</div>
<Skeleton className="h-64 rounded-xl" />
</div>
);
}
if (error) { if (error) return (
return ( <div className="max-w-7xl mx-auto px-4 py-20 text-center">
<div className="max-w-7xl mx-auto px-4 py-20 text-center"> <p className="text-red-500 mb-4">{error}</p>
<p className="text-red-500 mb-4">{error}</p> <button onClick={loadAll} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700">{t.common.retry}</button>
<button onClick={loadAll} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700"> </div>
);
</button>
</div>
);
}
const tabs: { key: Tab; label: string }[] = [ const tabs: { key: Tab; label: string }[] = [
{ key: 'progress', label: '学习进度' }, { key: 'progress', label: t.dashboard.tabProgress },
{ key: 'favorites', label: '收藏夹' }, { key: 'favorites', label: t.dashboard.tabFavorites },
{ key: 'profile', label: '个人设置' }, { key: 'profile', label: t.dashboard.tabProfile },
];
const statItems = [
{ label: t.dashboard.inProgressCourses, value: stats?.inProgressCourses ?? 0 },
{ label: t.dashboard.completedLessons, value: stats?.completedLessons ?? 0 },
{ label: t.dashboard.favoritePrompts, value: stats?.favoritePrompts ?? 0 },
{ label: t.dashboard.studyDays, value: stats?.studyDays ?? 0 },
{ label: t.dashboard.todayLearned, value: stats?.todayLearned ?? 0 },
]; ];
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-start justify-between mb-8"> <div className="flex items-start justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-foreground"></h1> <h1 className="text-3xl font-bold text-foreground">{t.dashboard.title}</h1>
<p className="mt-2 text-muted-foreground"></p> <p className="mt-2 text-muted-foreground">{t.dashboard.desc}</p>
</div> </div>
<button <button onClick={handleLogout} className="text-sm text-muted-foreground hover:text-red-500 transition-colors mt-1">{t.common.logout}</button>
onClick={handleLogout}
className="text-sm text-muted-foreground hover:text-red-500 transition-colors mt-1"
>
退
</button>
</div> </div>
{stats && ( <div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8"> {statItems.map((item) => (
{[ <div key={item.label} className="bg-card rounded-xl border border-border p-4 text-center">
{ label: '学习中课程', value: stats.inProgressCourses }, <div className="text-2xl font-bold text-brand-600">{item.value}</div>
{ label: '已完成课时', value: stats.completedLessons }, <div className="text-xs text-muted-foreground mt-1">{item.label}</div>
{ label: '收藏提示词', value: stats.favoritePrompts }, </div>
{ label: '学习天数', value: stats.studyDays }, ))}
{ label: '今日学习', value: stats.todayLearned }, </div>
].map((item) => (
<div key={item.label} className="bg-card rounded-xl border border-border p-4 text-center">
<div className="text-2xl font-bold text-brand-600">{item.value}</div>
<div className="text-xs text-muted-foreground mt-1">{item.label}</div>
</div>
))}
</div>
)}
<div className="flex gap-6 flex-col lg:flex-row"> <div className="flex gap-6 flex-col lg:flex-row">
<div className="lg:w-48 flex-shrink-0"> <div className="lg:w-48 flex-shrink-0">
<nav className="flex lg:flex-col gap-1"> <nav className="flex lg:flex-col gap-1">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button key={tab.key} onClick={() => setActiveTab(tab.key)}
key={tab.key} className={`px-4 py-2.5 text-sm font-medium rounded-lg text-left transition-colors ${activeTab === tab.key ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent'}`}>
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-2.5 text-sm font-medium rounded-lg text-left transition-colors ${
activeTab === tab.key
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent'
}`}
>
{tab.label} {tab.label}
</button> </button>
))} ))}
@@ -224,51 +134,32 @@ export default function DashboardPage() {
<div> <div>
{courses.length === 0 ? ( {courses.length === 0 ? (
<div className="bg-card rounded-xl border border-border p-12 text-center"> <div className="bg-card rounded-xl border border-border p-12 text-center">
<p className="text-muted-foreground mb-4"></p> <p className="text-muted-foreground mb-4">{t.dashboard.noLearningRecords}</p>
<Link href="/courses" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700"> <Link href="/courses" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">{t.dashboard.browseCourses}</Link>
</Link>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{courses.map((entry) => ( {courses.map((entry) => (
<div key={entry.course.id} className="bg-card rounded-xl border border-border p-6"> <div key={entry.course.id} className="bg-card rounded-xl border border-border p-6">
<Link href={`/courses/${entry.course.id}`} className="text-lg font-semibold text-foreground hover:text-brand-600"> <Link href={`/courses/${entry.course.id}`} className="text-lg font-semibold text-foreground hover:text-brand-600">{entry.course.title}</Link>
{entry.course.title}
</Link>
<div className="mt-3"> <div className="mt-3">
<div className="flex items-center justify-between text-sm text-muted-foreground mb-1.5"> <div className="flex items-center justify-between text-sm text-muted-foreground mb-1.5">
<span></span> <span>{t.dashboard.learningProgress}</span>
<span>{entry.completedCount}/{entry.totalCount} ({entry.progress}%)</span> <span>{t.dashboard.lessonCount.replace('{completed}', String(entry.completedCount)).replace('{total}', String(entry.totalCount)).replace('{progress}', String(entry.progress))}</span>
</div> </div>
<Progress value={entry.progress} className="h-2" /> <Progress value={entry.progress} className="h-2" />
</div> </div>
{entry.recentLessons.length > 0 && (
<div className="mt-4 pt-4 border-t border-border">
<div className="text-xs text-muted-foreground mb-2"></div>
<div className="space-y-1.5">
{entry.recentLessons.map((lesson) => (
<div key={lesson.id} className="flex items-center gap-2 text-sm">
<span className={`w-1.5 h-1.5 rounded-full ${lesson.completed ? 'bg-green-500' : 'bg-brand-300'}`} />
<span className="text-muted-foreground">{lesson.title}</span>
</div>
))}
</div>
</div>
)}
</div> </div>
))} ))}
{recentRecords.length > 0 && ( {recentRecords.length > 0 && (
<div className="bg-card rounded-xl border border-border p-6"> <div className="bg-card rounded-xl border border-border p-6">
<h3 className="text-base font-semibold text-foreground mb-4"></h3> <h3 className="text-base font-semibold text-foreground mb-4">{t.dashboard.recentLearning}</h3>
<div className="space-y-3"> <div className="space-y-3">
{recentRecords.map((r, i) => ( {recentRecords.map((r, i) => (
<div key={i} className="flex items-center justify-between text-sm"> <div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full ${r.completed ? 'bg-green-500' : 'bg-brand-300'}`} /> <span className={`w-1.5 h-1.5 rounded-full ${r.completed ? 'bg-green-500' : 'bg-brand-300'}`} />
<span className="text-muted-foreground">{r.lessonTitle}</span> <span className="text-muted-foreground">{r.lessonTitle} - {r.courseTitle}</span>
<span className="text-muted-foreground">- {r.courseTitle}</span>
</div> </div>
<span className="text-xs text-muted-foreground">{new Date(r.updatedAt).toLocaleDateString()}</span> <span className="text-xs text-muted-foreground">{new Date(r.updatedAt).toLocaleDateString()}</span>
</div> </div>
@@ -285,25 +176,19 @@ export default function DashboardPage() {
<div> <div>
{favorites.length === 0 ? ( {favorites.length === 0 ? (
<div className="bg-card rounded-xl border border-border p-12 text-center"> <div className="bg-card rounded-xl border border-border p-12 text-center">
<p className="text-muted-foreground mb-4"></p> <p className="text-muted-foreground mb-4">{t.dashboard.noFavorites}</p>
<Link href="/prompts" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700"> <Link href="/prompts" className="inline-flex px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">{t.dashboard.browsePrompts}</Link>
</Link>
</div> </div>
) : ( ) : (
<div className="grid gap-4"> <div className="grid gap-4">
{favorites.map((fav) => ( {favorites.map((fav) => (
<Link <Link key={fav.id} href="/prompts" className="block bg-card rounded-xl border border-border p-5 hover:border-brand-200 transition-colors">
key={fav.id}
href={`/prompts`}
className="block bg-card rounded-xl border border-border p-5 hover:border-brand-200 transition-colors"
>
<h3 className="font-semibold text-foreground">{fav.title}</h3> <h3 className="font-semibold text-foreground">{fav.title}</h3>
{fav.description && <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{fav.description}</p>} {fav.description && <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{fav.description}</p>}
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground"> <div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
{fav.model && <span>: {fav.model}</span>} {fav.model && <span>{t.dashboard.modelLabel.replace('{model}', fav.model)}</span>}
<span>{fav.viewCount} </span> <span>{t.dashboard.viewCount.replace('{n}', String(fav.viewCount))}</span>
<span>{fav.likeCount} </span> <span>{t.dashboard.likeCount.replace('{n}', String(fav.likeCount))}</span>
<span className="ml-auto">{new Date(fav.favoritedAt).toLocaleDateString()}</span> <span className="ml-auto">{new Date(fav.favoritedAt).toLocaleDateString()}</span>
</div> </div>
</Link> </Link>
@@ -315,45 +200,34 @@ export default function DashboardPage() {
{activeTab === 'profile' && ( {activeTab === 'profile' && (
<div className="bg-card rounded-xl border border-border p-6"> <div className="bg-card rounded-xl border border-border p-6">
<h3 className="text-base font-semibold text-foreground mb-6"></h3> <h3 className="text-base font-semibold text-foreground mb-6">{t.dashboard.profile}</h3>
<div className="space-y-5 max-w-md"> <div className="space-y-5 max-w-md">
<div> <div>
<label className="block text-sm font-medium text-foreground mb-1"></label> <label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.nicknameLabel}</label>
<input <input type="text" value={nickname} onChange={e => setNickname(e.target.value)}
type="text"
value={nickname}
onChange={e => setNickname(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent" className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
placeholder="输入昵称" placeholder={t.dashboard.nicknamePlaceholder} />
/>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-foreground mb-1"></label> <label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.memberPlan}</label>
<p className="text-sm text-muted-foreground">{userInfo?.memberPlan === 'FREE' ? '免费用户' : userInfo?.memberPlan}</p> <p className="text-sm text-muted-foreground">{userInfo?.memberPlan === 'FREE' ? t.dashboard.freeUser : userInfo?.memberPlan}</p>
</div> </div>
{userInfo?.memberExpire && ( {userInfo?.memberExpire && (
<div> <div>
<label className="block text-sm font-medium text-foreground mb-1"></label> <label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.memberExpire}</label>
<p className="text-sm text-muted-foreground">{new Date(userInfo.memberExpire).toLocaleDateString()}</p> <p className="text-sm text-muted-foreground">{new Date(userInfo.memberExpire).toLocaleDateString()}</p>
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-foreground mb-1"></label> <label className="block text-sm font-medium text-foreground mb-1">{t.dashboard.joinDate}</label>
<p className="text-sm text-muted-foreground">{userInfo?.joinedAt ? new Date(userInfo.joinedAt).toLocaleDateString() : '-'}</p> <p className="text-sm text-muted-foreground">{userInfo?.joinedAt ? new Date(userInfo.joinedAt).toLocaleDateString() : '-'}</p>
</div> </div>
<div> <div>
<button <button onClick={handleSaveProfile} disabled={saving}
onClick={handleSaveProfile} className="px-6 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
disabled={saving} {saving ? t.common.loading : t.settings.saveChanges}
className="px-6 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50"
>
{saving ? '保存中...' : '保存'}
</button> </button>
{saveMsg && ( {saveMsg && <span className={`ml-3 text-sm ${saveMsg === t.dashboard.saveSuccess ? 'text-green-600' : 'text-red-500'}`}>{saveMsg}</span>}
<span className={`ml-3 text-sm ${saveMsg === '保存成功' ? 'text-green-600' : 'text-red-500'}`}>
{saveMsg}
</span>
)}
</div> </div>
</div> </div>
</div> </div>
+16 -50
View File
@@ -5,29 +5,17 @@ import Link from 'next/link';
import { apiFetch } from '@/lib/auth'; import { apiFetch } from '@/lib/auth';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { useT } from '@/i18n';
interface Notification { interface Notification { id: number; type: 'like' | 'comment' | 'follow' | 'system'; title: string; content?: string; link?: string; relatedId?: number; isRead: boolean; createdAt: string }
id: number;
type: 'like' | 'comment' | 'follow' | 'system';
title: string;
content?: string;
link?: string;
relatedId?: number;
isRead: boolean;
createdAt: string;
}
function NotificationIcon({ type }: { type: string }) { function NotificationIcon({ type }: { type: string }) {
const icons: Record<string, string> = { const icons: Record<string, string> = { like: '❤️', comment: '💬', follow: '👤', system: '🔔' };
like: '❤️',
comment: '💬',
follow: '👤',
system: '🔔',
};
return <span className="text-lg">{icons[type] || '🔔'}</span>; return <span className="text-lg">{icons[type] || '🔔'}</span>;
} }
export default function NotificationsPage() { export default function NotificationsPage() {
const t = useT();
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -35,11 +23,7 @@ export default function NotificationsPage() {
const load = useCallback(async () => { const load = useCallback(async () => {
try { try {
const res = await apiFetch('/notifications'); const res = await apiFetch('/notifications');
if (res.ok) { if (res.ok) { const data = await res.json(); setNotifications(data.items || []); setUnreadCount(data.unread || 0); }
const data = await res.json();
setNotifications(data.items || []);
setUnreadCount(data.unread || 0);
}
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
setLoading(false); setLoading(false);
}, []); }, []);
@@ -64,11 +48,8 @@ export default function NotificationsPage() {
if (loading) return ( if (loading) return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Skeleton className="h-8 w-48 mb-6" /> <Skeleton className="h-8 w-48 mb-6" /><Skeleton className="h-4 w-32 mb-8" />
<Skeleton className="h-4 w-32 mb-8" /> <Skeleton className="h-64 w-full mb-4" /><Skeleton className="h-4 w-full mb-2" /><Skeleton className="h-4 w-3/4" />
<Skeleton className="h-64 w-full mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</div> </div>
); );
@@ -76,54 +57,39 @@ export default function NotificationsPage() {
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-foreground"></h1> <h1 className="text-3xl font-bold text-foreground">{t.notifications.title}</h1>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
{unreadCount > 0 ? `你有 ${unreadCount} 条未读通知` : '暂无未读通知'} {unreadCount > 0 ? t.notifications.unreadCount.replace('{n}', String(unreadCount)) : t.notifications.noUnread}
</p> </p>
</div> </div>
{unreadCount > 0 && ( {unreadCount > 0 && (
<Button variant="outline" size="sm" onClick={markAllRead}> <Button variant="outline" size="sm" onClick={markAllRead}>{t.notifications.markAllRead}</Button>
</Button>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{notifications.map(n => ( {notifications.map(n => (
<div key={n.id} <div key={n.id} className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${n.isRead ? 'bg-card border-border' : 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'}`}>
className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${
n.isRead
? 'bg-card border-border'
: 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'
}`}>
<div className="mt-1"><NotificationIcon type={n.type} /></div> <div className="mt-1"><NotificationIcon type={n.type} /></div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{n.link ? ( {n.link ? (
<Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }} <Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }} className="text-sm font-medium text-foreground hover:text-brand-600">{n.title}</Link>
className="text-sm font-medium text-foreground hover:text-brand-600">
{n.title}
</Link>
) : ( ) : (
<p className="text-sm font-medium text-foreground">{n.title}</p> <p className="text-sm font-medium text-foreground">{n.title}</p>
)} )}
{n.content && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{n.content}</p>} {n.content && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{n.content}</p>}
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">{new Date(n.createdAt).toLocaleString('zh-CN')}</p>
{new Date(n.createdAt).toLocaleString('zh-CN')}
</p>
</div> </div>
{!n.isRead && ( {!n.isRead && (
<button onClick={() => markRead(n.id)} <button onClick={() => markRead(n.id)} className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">{t.notifications.markRead}</button>
className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">
</button>
)} )}
</div> </div>
))} ))}
{notifications.length === 0 && ( {notifications.length === 0 && (
<div className="text-center py-20 text-muted-foreground"> <div className="text-center py-20 text-muted-foreground">
<p className="text-4xl mb-4">🔔</p> <p className="text-4xl mb-4">🔔</p>
<p></p> <p>{t.notifications.emptyTitle}</p>
<p className="text-sm mt-1"></p> <p className="text-sm mt-1">{t.notifications.emptyDesc}</p>
</div> </div>
)} )}
</div> </div>
+37 -109
View File
@@ -1,22 +1,24 @@
'use client';
import Link from 'next/link'; import Link from 'next/link';
import { HomePageClient } from './home-client'; import { HomePageClient } from './home-client';
import { ArrowRight, Sparkles, BookOpen, Bot, Compass, Zap } from 'lucide-react'; import { ArrowRight, Sparkles, BookOpen, Bot, Compass, Zap } from 'lucide-react';
import { useT } from '@/i18n';
const stats = [
{ value: '50+', label: 'AI 专题' },
{ value: '200+', label: '精选提示词' },
{ value: '30+', label: 'AI 工具评测' },
{ value: '10,000+', label: '探索者' },
];
const features = [
{ icon: Compass, title: '分领域指南', desc: '按职业和场景分类内容,学即所用,精准提升 AI 应用能力' },
{ icon: Bot, title: 'AI 沙盒实战', desc: '内置 AI 对话沙盒,边学边练,在实践中掌握提示词技巧' },
{ icon: BookOpen, title: '提示词库', desc: '精选 200+ 提示词模板,覆盖办公、编程、创作等场景' },
{ icon: Zap, title: '持续更新', desc: '紧跟大模型迭代,内容实时更新,始终走在 AI 前沿' },
];
export default function HomePage() { export default function HomePage() {
const t = useT();
const stats = [
{ value: '50+', label: t.home.statTopics },
{ value: '200+', label: t.home.statPrompts },
{ value: '30+', label: t.home.statTools },
{ value: '10,000+', label: t.home.statExplorers },
];
const features = [
{ icon: Compass, title: t.home.featureGuide, desc: t.home.featureGuideDesc },
{ icon: Bot, title: t.home.featureSandbox, desc: t.home.featureSandboxDesc },
{ icon: BookOpen, title: t.home.featurePrompts, desc: t.home.featurePromptsDesc },
{ icon: Zap, title: t.home.featureUpdate, desc: t.home.featureUpdateDesc },
];
return ( return (
<HomePageClient> <HomePageClient>
{/* Hero */} {/* Hero */}
@@ -30,33 +32,25 @@ export default function HomePage() {
<div className="text-center max-w-3xl mx-auto animate-fade-in-up"> <div className="text-center max-w-3xl mx-auto animate-fade-in-up">
<span className="inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-brand-700 dark:text-brand-300 bg-brand-100 dark:bg-brand-900/50 rounded-full mb-8 border border-brand-200 dark:border-brand-800"> <span className="inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-brand-700 dark:text-brand-300 bg-brand-100 dark:bg-brand-900/50 rounded-full mb-8 border border-brand-200 dark:border-brand-800">
<Sparkles className="w-3.5 h-3.5" /> <Sparkles className="w-3.5 h-3.5" />
AI {t.home.badge}
</span> </span>
<h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight"> <h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight">
<span className="bg-gradient-to-r from-brand-600 via-brand-500 to-blue-500 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-brand-600 via-brand-500 to-blue-500 bg-clip-text text-transparent">
{t.home.heroHighlight}
</span> </span>
<br /> AI <br />{t.home.heroRest}
</h1> </h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto"> <p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
AI AI {t.home.desc}
<br className="hidden sm:block" />
AI
</p> </p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center"> <div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Link <Link href="/courses" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-all shadow-lg shadow-brand-200/50 dark:shadow-brand-900/30 hover:shadow-xl hover:-translate-y-0.5 active:scale-[0.98]">
href="/courses"
className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-all shadow-lg shadow-brand-200/50 dark:shadow-brand-900/30 hover:shadow-xl hover:-translate-y-0.5 active:scale-[0.98]"
>
<BookOpen className="w-5 h-5" /> <BookOpen className="w-5 h-5" />
{t.home.startExplore}
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
</Link> </Link>
<Link <Link href="/auth?tab=register" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-foreground bg-card border border-border rounded-xl hover:bg-accent transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-sm">
href="/auth?tab=register" {t.home.freeRegister}
className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-foreground bg-card border border-border rounded-xl hover:bg-accent transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-sm"
>
</Link> </Link>
</div> </div>
</div> </div>
@@ -83,8 +77,8 @@ export default function HomePage() {
<section className="py-20"> <section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold"></h2> <h2 className="text-3xl font-bold">{t.home.whyTitle}</h2>
<p className="mt-4 text-lg text-muted-foreground"> AI</p> <p className="mt-4 text-lg text-muted-foreground">{t.home.whyDesc}</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{features.map((feat) => ( {features.map((feat) => (
@@ -105,18 +99,18 @@ export default function HomePage() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10"> <div className="flex items-center justify-between mb-10">
<div> <div>
<h2 className="text-3xl font-bold"></h2> <h2 className="text-3xl font-bold">{t.home.popularTopics}</h2>
<p className="mt-2 text-muted-foreground"> AI</p> <p className="mt-2 text-muted-foreground">{t.home.popularDesc}</p>
</div> </div>
<Link href="/courses" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group"> <Link href="/courses" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" /> {t.common.viewAll} <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</Link> </Link>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[ {[
{ title: 'AI 通识:零基础入门', lessons: '12 模块', students: '1,280', tag: '免费', gradient: 'from-brand-500 to-blue-500', desc: '面向零基础用户,带你了解 AI 的基本概念、发展历程和实际应用。' }, { title: 'AI 通识:零基础入门', students: '1,280', tag: t.courses.free, gradient: 'from-brand-500 to-blue-500', desc: '面向零基础用户,带你了解 AI 的基本概念、发展历程和实际应用。' },
{ title: '提示词工程从入门到精通', lessons: '20 模块', students: '860', tag: '热门', gradient: 'from-violet-500 to-purple-500', desc: '系统学习提示词编写技巧,掌握与 AI 高效沟通的方法。' }, { title: '提示词工程从入门到精通', students: '860', tag: '热门', gradient: 'from-violet-500 to-purple-500', desc: '系统学习提示词编写技巧,掌握与 AI 高效沟通的方法。' },
{ title: '用 AI 提升 10 倍办公效率', lessons: '15 模块', students: '2,150', tag: '推荐', gradient: 'from-amber-500 to-orange-500', desc: '学习使用 AI 工具处理文档、数据分析、演示制作等日常工作。' }, { title: '用 AI 提升 10 倍办公效率', students: '2,150', tag: '推荐', gradient: 'from-amber-500 to-orange-500', desc: '学习使用 AI 工具处理文档、数据分析、演示制作等日常工作。' },
].map((course) => ( ].map((course) => (
<div key={course.title} className="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all hover:-translate-y-1"> <div key={course.title} className="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all hover:-translate-y-1">
<div className={`h-2 bg-gradient-to-r ${course.gradient}`} /> <div className={`h-2 bg-gradient-to-r ${course.gradient}`} />
@@ -127,14 +121,7 @@ export default function HomePage() {
<h3 className="text-lg font-semibold mb-2 group-hover:text-brand-600 transition-colors">{course.title}</h3> <h3 className="text-lg font-semibold mb-2 group-hover:text-brand-600 transition-colors">{course.title}</h3>
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{course.desc}</p> <p className="text-sm text-muted-foreground mb-4 line-clamp-2">{course.desc}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground"> <div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1"><BookOpen className="w-4 h-4" />{t.home.moduleCount.replace('{n}', course.students)}</span>
<BookOpen className="w-4 h-4" />
{course.lessons}
</span>
<span className="flex items-center gap-1">
<span className="text-lg leading-none">·</span>
{course.students}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -143,73 +130,14 @@ export default function HomePage() {
</div> </div>
</section> </section>
{/* AI Sandbox Preview */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-3xl font-bold">AI </h2>
<p className="mt-2 text-muted-foreground">线 AI </p>
</div>
<Link href="/sandbox" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
<div className="bg-card border border-border rounded-2xl overflow-hidden shadow-xl">
<div className="flex items-center gap-1.5 px-4 pt-3 pb-2 border-b border-border">
<div className="flex gap-1.5">
<span className="w-3 h-3 rounded-full bg-red-400" />
<span className="w-3 h-3 rounded-full bg-yellow-400" />
<span className="w-3 h-3 rounded-full bg-green-400" />
</div>
<span className="ml-2 text-xs text-muted-foreground">AI - 线</span>
</div>
<div className="p-4 space-y-4 bg-muted/30 dark:bg-muted/10">
<div className="flex items-start gap-3">
<span className="w-7 h-7 bg-brand-600 rounded-lg flex items-center justify-center text-white text-xs font-bold shrink-0">Y</span>
<div className="bg-card dark:bg-card rounded-xl rounded-tl-none px-3 py-2.5 text-sm shadow-sm max-w-[80%]">
AI AI
</div>
</div>
<div className="flex items-start gap-3 justify-end">
<div className="bg-brand-50 dark:bg-brand-900/30 rounded-xl rounded-tr-none px-3 py-2.5 text-sm max-w-[80%]">
Python Fibonacci
</div>
<span className="w-7 h-7 bg-muted-foreground/20 rounded-lg flex items-center justify-center text-xs font-bold shrink-0"></span>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground pl-9">
<span className="w-2 h-2 bg-brand-500 rounded-full animate-pulse" />
...
</div>
</div>
<div className="border-t border-border p-3 bg-card">
<div className="flex gap-2">
<input
type="text"
placeholder="输入你的问题..."
readOnly
className="flex-1 bg-muted border-0 rounded-lg px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
<button className="px-4 py-2 bg-brand-600 text-white text-sm font-medium rounded-lg hover:bg-brand-700 transition-colors cursor-default">
</button>
</div>
</div>
</div>
</div>
</section>
{/* CTA */} {/* CTA */}
<section className="py-20 bg-gradient-to-r from-brand-600 to-brand-800 dark:from-brand-900 dark:to-brand-950 relative overflow-hidden"> <section className="py-20 bg-gradient-to-r from-brand-600 to-brand-800 dark:from-brand-900 dark:to-brand-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAgMjB2MTBoLTEwVjIwaDEwek0yMCAwaDEwdjEwSDIwVjB6IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjAuMDMiLz48L3N2Zz4=')] opacity-50" /> <div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAgMjB2MTBoLTEwVjIwaDEwek0yMCAwaDEwdjEwSDIwVjB6IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjAuMDMiLz48L3N2Zz4=')] opacity-50" />
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative">
<h2 className="text-3xl font-bold text-white mb-4"> AI </h2> <h2 className="text-3xl font-bold text-white mb-4">{t.home.ctaTitle}</h2>
<p className="text-brand-100/80 dark:text-brand-200/80 mb-8 text-lg"></p> <p className="text-brand-100/80 dark:text-brand-200/80 mb-8 text-lg">{t.home.ctaDesc}</p>
<Link <Link href="/auth?tab=register" className="inline-flex items-center gap-2 px-8 py-3 text-base font-medium text-brand-600 bg-white rounded-xl hover:bg-brand-50 transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-xl">
href="/auth?tab=register" {t.home.freeRegister}
className="inline-flex items-center gap-2 px-8 py-3 text-base font-medium text-brand-600 bg-white rounded-xl hover:bg-brand-50 transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-xl"
>
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
+57 -19
View File
@@ -14,25 +14,63 @@ interface Message {
const AVAILABLE_TOOLS_DESC = `可用工具列表(需要执行操作时,返回 JSON{"tool":"工具名","params":{...},"description":"简述"}): const AVAILABLE_TOOLS_DESC = `可用工具列表(需要执行操作时,返回 JSON{"tool":"工具名","params":{...},"description":"简述"}):
- **get-dashboard**: 获取仪表盘概览数据(用户数、课程数、内容数、提示词数、订单数) === 查看 ===
- **list-users**: 列出用户,可按关键词搜索(参数: search, page, pageSize - **get-dashboard**: 仪表盘概览
- **get-user**: 查看用户详情(参数: id) - **get-analytics-overview**: 数据分析概览
- **update-user-status**: 修改用户状态(参数: id, status: ACTIVE|INACTIVE|BANNED - **list-users**: 用户列表(search?, page?, pageSize?
- **list-orders**: 查看最近订单列表 - **get-user**: 用户详情(id
- **get-analytics-overview**: 获取数据分析概览(用户增长、收入、趋势) - **list-orders**: 订单列表
- **list-comments**: 查看评论(参数: status: PENDING_REVIEW|PUBLISHED|REJECTED - **list-courses**: 课程列表 / **get-course**: 课程详情(id
- **approve-comment**: 通过评论(参数: id - **list-contents**: 内容列表
- **reject-comment**: 拒绝评论(参数: id, reason? - **list-prompts**: 提示词列表
- **list-banners**: 查看所有Banner - **list-comments**: 评论列表(status?
- **list-notifications**: 查看系统通知 - **list-banners**: Banner列表
- **list-config**: 查看系统配置 - **list-notifications**: 通知列表
- **list-roles**: 查看管理角色 - **list-config**: 配置列表
- **list-admins**: 查看管理员 - **list-roles**: 角色列表
- **get-enterprise-orgs**: 查看企业版组织 - **list-admins**: 管理员列表
- **toggle-course-status**: 切换课程上下架(参数: id) - **get-enterprise-orgs**: 企业组织列表
- **toggle-content-status**: 切换内容上下架(参数: id)
- **toggle-prompt-status**: 切换提示词上下架(参数: id) === 创建 ===
- **navigate**: 跳转到某个管理页面(参数: path — 如 /admin/users, /admin/orders, /admin/analytics, /admin/enterprise, /admin/operations/banners, /admin/operations/notifications, /admin/settings/roles, /admin/settings/config, /admin/comments, /admin/courses, /admin/prompts, /admin/contents, /admin/tools - **create-course**: 创建课程(title必填, description?, price?, isFree?, status?
- **create-content**: 创建内容(title必填, summary?, content?, tags?, status?
- **create-prompt**: 创建提示词(title必填, content必填, description?, tags?, status?
- **create-banner**: 创建Bannertitle必填, image必填, link?, position?, sortOrder?
- **create-notification**: 创建通知(title必填, content?, link?, userId?
- **create-role**: 创建角色(name必填, description?, permissions?
- **create-admin**: 创建管理员(username必填, password必填, nickname?, roleId?
- **create-organization**: 创建企业组织(name必填, description?, contactName?, contactPhone?
=== 修改 ===
- **update-user**: 修改用户(id, nickname?, phone?, email?, status?, memberPlan?, sandboxDaily?
- **update-user-status**: 修改用户状态(id, status: ACTIVE|INACTIVE|BANNED
- **update-course**: 修改课程(id, title?, description?, price?, isFree?, status?
- **update-content**: 修改内容(id, title?, summary?, content?, tags?, status?
- **update-prompt**: 修改提示词(id, title?, content?, description?, tags?, status?
- **update-banner**: 修改Bannerid, title?, image?, link?, sortOrder?
- **update-config**: 修改配置(key必填, value必填, category?, description?
- **update-role**: 修改角色(id, name?, description?, permissions?
- **update-admin**: 修改管理员(id, nickname?, roleId?, password?
- **update-organization**: 修改企业组织(id, name?, description?, contactName?, contactPhone?
- **toggle-course-status**: 切换课程上下架(id
- **toggle-content-status**: 切换内容上下架(id
- **toggle-prompt-status**: 切换提示词上下架(id
=== 删除 ===
- **delete-user**: 删除用户(id
- **delete-course**: 删除课程(id
- **delete-content**: 删除内容(id
- **delete-prompt**: 删除提示词(id
- **delete-banner**: 删除Bannerid
- **delete-notification**: 删除通知(id
- **delete-role**: 禁用角色(id
- **delete-admin**: 禁用管理员(id
- **delete-organization**: 删除组织(id
- **add-org-member**: 添加组织成员(organizationId, userId, role?
- **remove-org-member**: 移除组织成员(organizationId, userId
=== 导航 ===
- **navigate**: 跳转到页面(path: /admin/users等)
当用户请求执行操作时,先调用对应工具。工具执行完毕后会用自然语言总结结果。`; 当用户请求执行操作时,先调用对应工具。工具执行完毕后会用自然语言总结结果。`;
+16 -14
View File
@@ -1,35 +1,37 @@
import Link from 'next/link'; import Link from 'next/link';
import { useT } from '@/i18n';
export function Footer() { export function Footer() {
const t = useT();
return ( return (
<footer className="border-t border-border bg-muted/30"> <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="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-4 gap-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-8">
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<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> <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"> AI</p> <p className="text-sm text-muted-foreground">{t.footer.tagline}</p>
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold mb-3"></h4> <h4 className="text-sm font-semibold mb-3">{t.footer.explore}</h4>
<ul className="space-y-2.5"> <ul className="space-y-2.5">
<li><Link href="/courses" className="text-sm text-muted-foreground hover:text-foreground transition-colors"></Link></li> <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">AI </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"></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="/models" className="text-sm text-muted-foreground hover:text-foreground transition-colors"></Link></li> <li><Link href="/models" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.models}</Link></li>
<li><Link href="/tools" className="text-sm text-muted-foreground hover:text-foreground transition-colors">AI </Link></li> <li><Link href="/tools" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiTools}</Link></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold mb-3"></h4> <h4 className="text-sm font-semibold mb-3">{t.footer.about}</h4>
<ul className="space-y-2.5"> <ul className="space-y-2.5">
<li><Link href="/about" className="text-sm text-muted-foreground hover:text-foreground transition-colors"></Link></li> <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"></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"></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">AI </Link></li> <li><Link href="/ai-agreement" className="text-sm text-muted-foreground hover:text-foreground transition-colors">{t.footer.aiAgreement}</Link></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold mb-3"></h4> <h4 className="text-sm font-semibold mb-3">{t.footer.contact}</h4>
<ul className="space-y-2.5"> <ul className="space-y-2.5">
<li className="text-sm text-muted-foreground">contact@yuzhiran.com</li> <li className="text-sm text-muted-foreground">contact@yuzhiran.com</li>
<li className="text-sm text-muted-foreground"></li> <li className="text-sm text-muted-foreground"></li>
@@ -38,7 +40,7 @@ export function Footer() {
</div> </div>
<div className="mt-10 pt-8 border-t border-border"> <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"> <div className="flex flex-col md:flex-row items-center justify-between gap-2 text-xs text-muted-foreground">
<p>&copy; {new Date().getFullYear()} </p> <p>{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}</p>
<p> <p>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors"> <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
ICP ICP备XXXXXXXX号 ICP ICP备XXXXXXXX号
+23 -22
View File
@@ -9,19 +9,7 @@ import { ThemeToggle } from '@/components/ui/theme-toggle';
import { Search, Menu, X, Bell, Languages } from 'lucide-react'; import { Search, Menu, X, Bell, Languages } from 'lucide-react';
import { apiFetch } from '@/lib/auth'; import { apiFetch } from '@/lib/auth';
import { useAuth } from '@/lib/auth-context'; import { useAuth } from '@/lib/auth-context';
import { useLang } from '@/i18n'; import { useLang, useT } from '@/i18n';
const navItems = [
{ href: '/', label: '首页' },
{ href: '/courses', label: '专题' },
{ href: '/sandbox', label: '沙盒' },
{ href: '/skills', label: '技能' },
{ href: '/models', label: '模型' },
{ href: '/prompts', label: '提示词' },
{ href: '/contents', label: '文章' },
{ href: '/tools', label: 'AI 工具' },
{ href: '/community', label: '社区' },
];
function NotificationBellComponent() { function NotificationBellComponent() {
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
@@ -63,6 +51,19 @@ export function Header() {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const { isLoggedIn, logout } = useAuth(); const { isLoggedIn, logout } = useAuth();
const { lang, setLang } = useLang(); const { lang, setLang } = useLang();
const t = useT();
const navItems = [
{ href: '/', label: t.nav.home },
{ href: '/courses', label: t.nav.courses },
{ href: '/sandbox', label: t.nav.sandbox },
{ href: '/skills', label: t.discover?.skills || '技能' },
{ href: '/models', label: t.discover?.models || '模型' },
{ href: '/prompts', label: t.nav.prompts },
{ href: '/contents', label: t.discover?.articles || '文章' },
{ href: '/tools', label: t.nav.tools },
{ href: '/community', label: t.nav.community },
];
function isActive(href: string) { function isActive(href: string) {
if (href === '/') return pathname === '/'; if (href === '/') return pathname === '/';
@@ -112,7 +113,7 @@ export function Header() {
<Input <Input
name="q" name="q"
type="text" type="text"
placeholder="搜索..." placeholder={t.common.search}
className="w-36 lg:w-48 pl-8 h-9 text-sm bg-muted/50 border-0 focus-visible:ring-1" className="w-36 lg:w-48 pl-8 h-9 text-sm bg-muted/50 border-0 focus-visible:ring-1"
/> />
</form> </form>
@@ -128,23 +129,23 @@ export function Header() {
<> <>
<NotificationBellComponent /> <NotificationBellComponent />
<Button variant="default" size="sm" asChild> <Button variant="default" size="sm" asChild>
<Link href="/dashboard"></Link> <Link href="/dashboard">{t.nav?.my || '控制台'}</Link>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { logout(); window.location.href = '/'; }} onClick={() => { logout(); window.location.href = '/'; }}
> >
退 {t.common.logout}
</Button> </Button>
</> </>
) : ( ) : (
<> <>
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" asChild>
<Link href="/auth"></Link> <Link href="/auth">{t.common.login}</Link>
</Button> </Button>
<Button variant="default" size="sm" asChild> <Button variant="default" size="sm" asChild>
<Link href="/auth?tab=register"></Link> <Link href="/auth?tab=register">{t.common.register}</Link>
</Button> </Button>
</> </>
)} )}
@@ -187,19 +188,19 @@ export function Header() {
{isLoggedIn ? ( {isLoggedIn ? (
<> <>
<Button className="w-full" size="sm" asChild> <Button className="w-full" size="sm" asChild>
<Link href="/dashboard" onClick={() => setMobileOpen(false)}></Link> <Link href="/dashboard" onClick={() => setMobileOpen(false)}>{t.nav?.my || '控制台'}</Link>
</Button> </Button>
<Button variant="outline" size="sm" className="w-full" onClick={() => { logout(); window.location.href = '/'; }}> <Button variant="outline" size="sm" className="w-full" onClick={() => { logout(); window.location.href = '/'; }}>
退 {t.common.logout}
</Button> </Button>
</> </>
) : ( ) : (
<> <>
<Button variant="outline" className="w-full" size="sm" asChild> <Button variant="outline" className="w-full" size="sm" asChild>
<Link href="/auth" onClick={() => setMobileOpen(false)}></Link> <Link href="/auth" onClick={() => setMobileOpen(false)}>{t.common.login}</Link>
</Button> </Button>
<Button className="w-full" size="sm" asChild> <Button className="w-full" size="sm" asChild>
<Link href="/auth?tab=register" onClick={() => setMobileOpen(false)}></Link> <Link href="/auth?tab=register" onClick={() => setMobileOpen(false)}>{t.common.register}</Link>
</Button> </Button>
</> </>
)} )}
+23 -4
View File
@@ -1,7 +1,8 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { AVAILABLE_MODELS, type ModelOption } from '@/lib/models'; import { AVAILABLE_MODELS as FALLBACK_MODELS, type ModelOption } from '@/lib/models';
import { API_BASE } from '@/lib/config';
interface ModelSelectorProps { interface ModelSelectorProps {
value: string; value: string;
@@ -11,7 +12,25 @@ interface ModelSelectorProps {
export function ModelSelector({ value, onChange, className = '' }: ModelSelectorProps) { export function ModelSelector({ value, onChange, className = '' }: ModelSelectorProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const selected = AVAILABLE_MODELS.find(m => m.id === value) || AVAILABLE_MODELS[0]; const [models, setModels] = useState<ModelOption[]>(FALLBACK_MODELS);
useEffect(() => {
fetch(`${API_BASE}/public/models`)
.then(r => r.json())
.then(data => {
if (data.items?.length) {
setModels(data.items.map((m: any) => ({
id: m.id,
label: m.name,
provider: m.provider,
desc: m.description || '',
})));
}
})
.catch(() => {});
}, []);
const selected = models.find(m => m.id === value) || models[0];
return ( return (
<div className={`relative ${className}`}> <div className={`relative ${className}`}>
@@ -27,7 +46,7 @@ export function ModelSelector({ value, onChange, className = '' }: ModelSelector
<> <>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} /> <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute right-0 top-full mt-1 z-20 w-64 bg-card border border-border rounded-xl shadow-lg overflow-hidden"> <div className="absolute right-0 top-full mt-1 z-20 w-64 bg-card border border-border rounded-xl shadow-lg overflow-hidden">
{AVAILABLE_MODELS.map(m => ( {models.map(m => (
<button key={m.id} onClick={() => { onChange(m.id); setOpen(false); }} <button key={m.id} onClick={() => { onChange(m.id); setOpen(false); }}
className={`w-full text-left px-4 py-3 hover:bg-accent transition-colors flex items-center gap-3 ${m.id === value ? 'bg-accent' : ''}`}> className={`w-full text-left px-4 py-3 hover:bg-accent transition-colors flex items-center gap-3 ${m.id === value ? 'bg-accent' : ''}`}>
<span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} /> <span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} />
+29 -199
View File
@@ -1,205 +1,35 @@
import type { Translations } from './zh' import type { Translations } from './zh'
const en: Translations = { const en: Translations = {
common: { common: { loading: 'Loading...', save: 'Save', cancel: 'Cancel', delete: 'Delete', confirm: 'Confirm', search: 'Search', back: 'Back', login: 'Login', register: 'Register', logout: 'Logout', retry: 'Retry', noData: 'No data', viewAll: 'View all' },
loading: 'Loading...', nav: { home: 'Home', courses: 'Courses', prompts: 'Prompts', sandbox: 'AI Sandbox', discover: 'Discover', my: 'My', tools: 'Tools', community: 'Community' },
save: 'Save', home: { badge: 'Free AI Learning Community', heroHighlight: 'Empower Everyone', heroRest: 'to Master AI', desc: 'AI knowledge, prompt engineering, sandbox practice & model encyclopedia', startExplore: 'Get Started', freeRegister: 'Register Free', statTopics: 'AI Topics', statPrompts: 'Curated Prompts', statTools: 'AI Tool Reviews', statExplorers: 'Explorers', whyTitle: 'Why Yuzhiran?', whyDesc: 'Four core advantages to master AI fast', featureGuide: 'Guided Learning', featureGuideDesc: 'Content organized by role and scenario', featureSandbox: 'AI Sandbox', featureSandboxDesc: 'Built-in AI sandbox to learn by doing', featurePrompts: 'Prompt Library', featurePromptsDesc: '200+ curated prompt templates', featureUpdate: 'Always Up-to-date', featureUpdateDesc: 'Content updated as AI evolves', popularTopics: 'Popular Topics', popularDesc: 'From beginner to expert, explore AI systematically', moduleCount: '{n} modules', studentCount: '{n} learners', openSandbox: 'Open Sandbox', ctaTitle: 'Ready to Start Your AI Journey?', ctaDesc: 'Register now and explore everything for free' },
cancel: 'Cancel', auth: { loginTitle: 'Login', registerTitle: 'Register', phone: 'Phone', password: 'Password', nickname: 'Nickname', welcomeBack: 'Welcome back', loginSubtitle: 'Log in to continue your AI journey', joinTitle: 'Join Yuzhiran', registerSubtitle: 'Register for free and explore AI', accountPlaceholder: 'Phone / Email', loggingIn: 'Logging in...', nicknameOptional: 'Nickname (optional)', passwordHint: 'Password (min 6 characters)', confirmPassword: 'Confirm password', registering: 'Registering...', agreePrefix: 'By registering, you agree to our', termsOfService: 'Terms of Service', privacyPolicy: 'Privacy Policy', aiAgreement: 'AI Service Agreement', fillAccountAndPassword: 'Please enter account and password', fillPhoneOrEmail: 'Please enter phone or email', fillPassword: 'Please enter password', passwordMinLength: 'Password must be at least 6 characters', passwordsNotMatch: 'Passwords do not match', loginFailed: 'Login failed', registerFailed: 'Registration failed', loginSuccess: 'Login successful', registerSuccess: 'Registration successful' },
delete: 'Delete', dashboard: { title: 'My Learning', desc: 'Track your learning progress and stats', inProgressCourses: 'Courses in Progress', completedLessons: 'Lessons Completed', favoritePrompts: 'Favorite Prompts', studyDays: 'Study Days', todayLearned: "Today's Learning", tabProgress: 'Progress', tabFavorites: 'Favorites', tabProfile: 'Profile', noLearningRecords: 'No learning records yet', browseCourses: 'Browse Courses', learningProgress: 'Learning Progress', lessonCount: '{completed}/{total} lessons ({progress}%)', noFavorites: 'No favorite prompts yet', browsePrompts: 'Browse Prompts', profile: 'Profile', nicknameLabel: 'Nickname', nicknamePlaceholder: 'Enter nickname', memberPlan: 'Membership', freeUser: 'Free User', memberExpire: 'Membership Expires', joinDate: 'Joined', saveSuccess: 'Saved successfully', saveFailed: 'Save failed', loadFailed: 'Failed to load data' },
confirm: 'Confirm', community: { title: 'Community', desc: 'Share AI learning experiences with others', createPost: '+ New Post', latest: 'Latest', following: 'Following', newPost: 'New Post', postTitle: 'Title', contentPlaceholder: 'Share your AI learning experience...', tagsPlaceholder: 'Tags (comma-separated)', publishing: 'Publishing...', publish: 'Publish', feedEmpty: 'Follow more users to discover great content', postsEmpty: 'No posts yet — be the first!', commentPlaceholder: 'Write a comment...', sending: 'Sending...', comment: 'Comment', followed: 'Following', follow: '+ Follow' },
search: 'Search', notifications: { title: 'Notifications', unreadCount: 'You have {n} unread notifications', noUnread: 'No unread notifications', markAllRead: 'Mark all read', markRead: 'Read', emptyTitle: 'No notifications', emptyDesc: 'Likes, comments and follows will appear here' },
back: 'Back', my: { desc: 'Manage your profile and favorites', learningProgress: 'Learning Progress', learningProgressDesc: 'View your course progress', favorites: 'My Favorites', favoritesDesc: 'Saved prompts and courses', memberCenter: 'Membership', memberDesc: 'Manage subscription and benefits', settings: 'Settings', settingsDesc: 'Account settings and preferences', analyticsDesc: 'Knowledge mastery analysis based on conversations', pathDesc: 'Master AI skills systematically' },
login: 'Login', settings: { title: 'Settings', desc: 'Manage your account preferences', personalInfo: 'Personal Info', nicknameLabel: 'Nickname', emailLabel: 'Email', saveChanges: 'Save Changes', saveSuccess: 'Saved successfully', accountSecurity: 'Account Security' },
register: 'Register', myLearning: { back: 'Back', title: 'My Learning', desc: 'Track your course learning progress', empty: 'No courses taken yet', browseCourses: 'Browse Courses', lessonCount: '{completed}/{total} lessons completed' },
logout: 'Logout', favorites: { back: 'Back', title: 'My Favorites', desc: 'Saved prompts and courses', empty: 'No favorites yet', browsePrompts: 'Browse Prompts' },
retry: 'Retry', courses: { title: 'Courses', desc: 'Explore AI systematically from beginner to expert', empty: 'No courses available', free: 'Free', paid: 'Paid', moduleCount: '{n} modules' },
noData: 'No data', search: { title: 'Search Results', placeholder: 'Search courses, prompts, tools, articles...', emptyHint: 'Enter keywords to search', noResults: 'No results found for "{q}"', resultsCount: '{n} results found', groupCourse: 'Courses', groupPrompt: 'Prompts', groupTool: 'AI Tools', groupContent: 'Articles' },
viewAll: 'View all', tools: { title: 'AI Tools', desc: 'Curated AI tools to boost your productivity' },
}, discover: { desc: 'Explore trending content and curated picks', hotCourses: 'Hot Courses', hotPrompts: 'Trending Prompts', hotPosts: 'Popular Discussions' },
nav: { footer: { tagline: 'Empowering Everyone to Master AI', explore: 'Explore', about: 'About', aboutUs: 'About Us', privacy: 'Privacy Policy', terms: 'Terms of Service', aiAgreement: 'AI Service Agreement', contact: 'Contact', copyright: '© {year} Yuzhiran Technology Center. All rights reserved.', models: 'Models', aiTools: 'AI Tools', articles: 'Articles', skills: 'Skills' },
home: 'Home', models: { title: 'AI Model Encyclopedia', desc: 'Compare mainstream LLMs to find the best fit', tableName: 'Model', tableProvider: 'Provider', tableCapabilities: 'Capabilities', tableContext: 'Context', tableMaxOutput: 'Max Output', tablePricing: 'Pricing', free: 'Free', recommended: 'Recommended', pricingFree: 'Free', pricingMixed: 'Free/Paid', pricingPaid: 'Paid' },
courses: 'Courses', error: { title: 'Something went wrong', desc: 'Page failed to load. Please try again.', reload: 'Reload' },
prompts: 'Prompts', notFound: { title: '404', desc: 'Page not found', backToHome: 'Back to Home' },
sandbox: 'AI Sandbox', share: { missingToken: 'Missing share token', invalidLink: 'Invalid share link', notAvailable: 'Shared content not available', expired: 'This share link may have expired', goToSandbox: 'Go to AI Sandbox', backToSandbox: 'AI Sandbox', modelInfo: 'Model: {model} · {date}' },
discover: 'Discover', path: { back: 'Back', totalProgress: 'Total Progress', taskCount: '{completed}/{total} tasks' },
my: 'My', sandbox: { title: 'AI Sandbox', subtitle: 'Experience AI conversations online', placeholder: 'Ask me anything...', send: 'Send', sending: 'Sending', newChat: 'New Chat', history: 'History', searchHistory: 'Search history...', noHistory: 'No history', sceneGeneral: 'General', sceneCoding: 'Coding', sceneWriting: 'Writing', sceneStudy: 'Study', sceneEnglish: 'English', modelGeneral: 'General', modelDeepSeek: 'DeepSeek V4 Flash', advancedParams: 'Advanced', temperature: 'Temperature', topP: 'Top P', maxTokens: 'Max Tokens', helpful: 'Helpful', notHelpful: 'Not Helpful', runInCodeSandbox: 'Run in Code Sandbox', shareToCommunity: 'Share to Community', copyShareLink: 'Copy Link', linkCopied: 'Link copied', loginForMore: 'Login for more', dailyQuota: 'Used {used} today, {remaining} remaining', aiReplyDisclaimer: 'AI replies are for reference only.', loginForMoreQuota: 'Login for more daily quota and models.', justNow: 'just now', minutesAgo: '{n}m ago', hoursAgo: '{n}h ago' },
tools: 'Tools', learning: { analytics: 'Learning Analytics', analyticsDesc: 'Analyze your learning based on AI conversations', path: 'Learning Path', pathDesc: 'Master AI skills systematically', totalSessions: 'AI Sessions', domainsCovered: 'Domains Covered', avgMastery: 'Avg Mastery', knowledgeDomains: 'Knowledge Domains', weakAreas: 'Weak Areas', weakDesc: 'Consider strengthening these areas:', recommendations: 'Recommendations', recDesc: 'Based on your weak areas', toStrengthen: 'To Strengthen', conversations: '{count} conversations', clickToGo: 'Go' },
community: 'Community', member: { title: 'Membership', desc: 'Manage your subscription', currentPlan: 'Current Plan', freeUser: 'You are on the Free plan', monthly: 'Monthly', yearly: 'Yearly', monthlyPrice: '¥29.9/month', yearlyPrice: '¥199/year', expires: 'Expires: {date}', benefits: 'All courses + unlimited sandbox + premium prompts + ad-free', orderHistory: 'Order History', noOrders: 'No orders yet', processing: 'Processing...', planFree: 'Free', planMonthly: 'Monthly', planYearly: 'Yearly', priceMonthly: '¥29.9', priceYearly: '¥199', perMonth: '/mo', perYear: '/yr', popular: 'Popular', featureSandbox: 'AI Sandbox', featureSandboxFree: '10/day', featureSandboxPro: '100/day', featureSandboxUnlimited: 'Unlimited', featureModels: 'Models', featureModelsFree: '1 model', featureModelsPro: '2 models', featureModelsPremium: 'All models', featurePrompts: 'Prompts', featurePromptsFree: 'Basic', featurePromptsPro: 'All', featurePromptsPremium: 'All + Exclusive', featureCourses: 'Courses', featureCoursesFree: 'Partial', featureCoursesPro: 'All', featureCoursesPremium: 'All', featureAds: 'Ads', featureAdsFree: 'Ads', featureAdsPro: 'Ad-free', featureAdsPremium: 'Ad-free', dailyQuota: 'Daily Quota', used: '{n} used', subscribe: 'Subscribe', currentPlan_badge: 'Current' },
}, compare: { title: 'Compare Lab', desc: 'Compare how different models respond', placeholder: 'Enter a question or prompt to compare...', startCompare: 'Start Compare', comparing: 'Comparing...', backToSandbox: 'Back to Sandbox', noResponse: 'No response' },
sandbox: { codeSandbox: { title: 'Code Sandbox', run: 'Run', runShortcut: 'Run (⌘⏎)', template: 'Template...', blank: 'Blank', react: 'React (CDN)', chart: 'Chart (Chart.js)', three: '3D (Three.js)', console: 'Console' },
title: 'AI Sandbox', skills: { title: 'Skills', desc: 'Composable AI learning skill modules', search: 'Search skills...', allCategories: 'All Categories', allDifficulties: 'All Levels', beginner: 'Beginner', intermediate: 'Intermediate', advanced: 'Advanced', tasks: 'Practice Tasks', starters: 'Try These', prerequisites: 'Prerequisites', apply: 'Use This Skill', categories: { basic: 'Basic', technical: 'Technical', creative: 'Creative', education: 'Education', advanced: 'Advanced', career: 'Career' } },
subtitle: 'Experience AI conversations online, learn by doing', promptWorkshop: { title: 'Prompt Workshop', desc: 'Write, test, and optimize your prompts', editor: 'Prompt Editor', test: 'Test Prompt', testing: 'Testing...', clear: 'Clear', saveToLibrary: 'Save to Library', saveSuccess: 'Saved successfully!', variables: 'Variables', role: 'Role', task: 'Task', outputFormat: 'Output Format', constraints: 'Constraints', insert: 'Insert', testResult: 'Test Result', saveDialogTitle: 'Save Prompt', saveTitle: 'Title *', saveDesc: 'Description', saveTags: 'Tags', saveTagsPlaceholder: 'e.g. programming,Python,debug', saving: 'Saving...' },
placeholder: 'Type your question...', assistant: { title: 'AI Assistant', greeting: 'Hi! I can help you explore and use this site. Try asking:', placeholder: 'Ask me anything...', error: 'Error: {message}', loginPrompt: '📝 Log in to unlock the full AI experience.\n\nClick "Login" or "Register" in the top right corner.' },
send: 'Send',
sending: 'Sending...',
newChat: 'New Chat',
history: 'History',
searchHistory: 'Search history...',
noHistory: 'No history yet',
sceneGeneral: 'General',
sceneCoding: 'Coding',
sceneWriting: 'Writing',
sceneStudy: 'Study',
sceneEnglish: 'English',
modelGeneral: 'General',
modelDeepSeek: 'DeepSeek V4 Flash',
advancedParams: 'Advanced',
temperature: 'Temperature',
topP: 'Top P',
maxTokens: 'Max Tokens',
helpful: 'Helpful',
notHelpful: 'Not helpful',
runInCodeSandbox: 'Run in Code Sandbox',
shareToCommunity: 'Share to Community',
copyShareLink: 'Copy Share Link',
linkCopied: 'Link copied',
loginForMore: 'Login for more',
dailyQuota: '{used} used today, {remaining} remaining',
aiReplyDisclaimer: 'AI replies are for reference only.',
loginForMoreQuota: 'Login to get more usage and models.',
justNow: 'just now',
minutesAgo: '{n} min ago',
hoursAgo: '{n} hour ago',
},
auth: {
loginTitle: 'Login',
registerTitle: 'Register',
phone: 'Phone',
password: 'Password',
nickname: 'Nickname',
},
learning: {
analytics: 'Learning Analytics',
analyticsDesc: 'Analyze your learning progress from AI sandbox conversations',
path: 'Learning Path',
pathDesc: 'Master AI skills systematically, step by step',
totalSessions: 'AI Conversations',
domainsCovered: 'Domains Covered',
avgMastery: 'Avg Mastery',
knowledgeDomains: 'Knowledge Domains',
weakAreas: 'Weak Areas',
weakDesc: 'You have less engagement in these areas. Consider strengthening:',
recommendations: 'Recommendations',
recDesc: 'Based on your weak areas, we recommend:',
toStrengthen: 'Needs work',
conversations: '{count} conversations',
clickToGo: 'Click to visit',
},
member: {
title: 'Membership',
desc: 'Manage your subscription',
currentPlan: 'Current Plan',
freeUser: 'You are on the free plan',
monthly: 'Monthly',
yearly: 'Yearly',
monthlyPrice: 'Subscribe ¥29.9/month',
yearlyPrice: 'Subscribe ¥199/year',
expires: 'Expires: {date}',
benefits: 'All courses + unlimited sandbox + exclusive prompts + ad-free',
orderHistory: 'Order History',
noOrders: 'No orders yet',
processing: 'Processing...',
planFree: 'Free',
planMonthly: 'Monthly',
planYearly: 'Yearly',
priceMonthly: '¥29.9',
priceYearly: '¥199',
perMonth: '/mo',
perYear: '/yr',
popular: 'Most Popular',
featureSandbox: 'AI Sandbox',
featureSandboxFree: '10/day',
featureSandboxPro: '100/day',
featureSandboxUnlimited: 'Unlimited',
featureModels: 'Models',
featureModelsFree: '1 model',
featureModelsPro: '2 models',
featureModelsPremium: 'All models',
featurePrompts: 'Prompts',
featurePromptsFree: 'Basic',
featurePromptsPro: 'All',
featurePromptsPremium: 'All + Exclusive',
featureCourses: 'Courses',
featureCoursesFree: 'Some free',
featureCoursesPro: 'All',
featureCoursesPremium: 'All',
featureAds: 'Ads',
featureAdsFree: 'Yes',
featureAdsPro: 'Ad-free',
featureAdsPremium: 'Ad-free',
dailyQuota: 'Daily Quota Usage',
used: '{n} used',
subscribe: 'Subscribe',
currentPlan_badge: 'Current Plan',
},
compare: {
title: 'Compare Lab',
desc: 'Compare model responses side by side',
placeholder: 'Enter your prompt to compare...',
startCompare: 'Compare',
comparing: 'Comparing...',
},
codeSandbox: {
title: 'Code Sandbox',
run: 'Run',
runShortcut: 'Run (⌘⏎)',
template: 'Template...',
blank: 'Blank',
react: 'React (CDN)',
chart: 'Chart.js',
three: 'Three.js',
console: 'Console Output',
},
skills: {
title: 'Skill Library',
desc: 'Composable AI learning skill modules',
search: 'Search skills...',
allCategories: 'All Categories',
allDifficulties: 'All Levels',
beginner: 'Beginner',
intermediate: 'Intermediate',
advanced: 'Advanced',
tasks: 'Practice Tasks',
starters: 'Try these questions',
prerequisites: 'Prerequisites',
apply: 'Use this skill',
categories: {
basic: 'Basic',
technical: 'Technical',
creative: 'Creative',
education: 'Education',
advanced: 'Advanced',
career: 'Career',
},
},
promptWorkshop: {
title: 'Prompt Workshop',
desc: 'Write, test, and optimize your prompts',
editor: 'Prompt Editor',
test: 'Test Prompt',
testing: 'Testing...',
clear: 'Clear',
saveToLibrary: 'Save to Library',
saveSuccess: 'Saved!',
variables: 'Variables',
role: 'Role',
task: 'Task',
outputFormat: 'Output Format',
constraints: 'Constraints',
insert: 'Insert',
testResult: 'Test Result',
saveDialogTitle: 'Save Prompt',
saveTitle: 'Title *',
saveDesc: 'Description',
saveTags: 'Tags',
saveTagsPlaceholder: 'Separate by commas, e.g. coding,Python,debug',
saving: 'Saving...',
},
assistant: {
title: 'AI Assistant',
greeting: 'Hi! I\'m the Yuzhiran AI assistant. I can help you learn about and use this site. Try asking:',
placeholder: 'Type your question...',
},
} }
export default en export default en
+29 -199
View File
@@ -1,203 +1,33 @@
const zh = { const zh = {
common: { common: { loading: '加载中...', save: '保存', cancel: '取消', delete: '删除', confirm: '确认', search: '搜索', back: '返回', login: '登录', register: '注册', logout: '退出登录', retry: '重试', noData: '暂无数据', viewAll: '查看全部' },
loading: '加载中...', nav: { home: '首页', courses: '课程', prompts: '提示词库', sandbox: 'AI 沙盒', discover: '发现', my: '我的', tools: '工具', community: '社区' },
save: '保存', home: { badge: '免费 AI 知识社区', heroHighlight: '让每个人', heroRest: '都能用好 AI', desc: '涵盖 AI 通识、提示词工程、沙盒实战、模型百科', startExplore: '开始探索', freeRegister: '免费注册', statTopics: 'AI 专题', statPrompts: '精选提示词', statTools: 'AI 工具评测', statExplorers: '探索者', whyTitle: '为什么选择宇之然?', whyDesc: '四大核心优势,助你快速掌握 AI', featureGuide: '分领域指南', featureGuideDesc: '按职业和场景分类内容,学即所用', featureSandbox: 'AI 沙盒实战', featureSandboxDesc: '内置 AI 对话沙盒,边学边练', featurePrompts: '提示词库', featurePromptsDesc: '精选 200+ 提示词模板', featureUpdate: '持续更新', featureUpdateDesc: '紧跟大模型迭代,内容实时更新', popularTopics: '热门专题', popularDesc: '从入门到精通,系统探索 AI', moduleCount: '{n} 模块', studentCount: '{n} 人关注', openSandbox: '打开沙盒', ctaTitle: '准备好开启 AI 之旅了吗?', ctaDesc: '立即注册,免费探索所有内容' },
cancel: '取消', auth: { loginTitle: '登录', registerTitle: '注册', phone: '手机号', password: '密码', nickname: '昵称', welcomeBack: '欢迎回来', loginSubtitle: '登录继续你的 AI 探索之旅', joinTitle: '加入宇之然', registerSubtitle: '免费注册,开始探索 AI', accountPlaceholder: '手机号 / 邮箱', loggingIn: '登录中...', nicknameOptional: '昵称(选填)', passwordHint: '密码(至少 6 位)', confirmPassword: '确认密码', registering: '注册中...', agreePrefix: '注册即表示同意', termsOfService: '服务协议', privacyPolicy: '隐私政策', aiAgreement: 'AI 服务协议', fillAccountAndPassword: '请填写账号和密码', fillPhoneOrEmail: '请填写手机号或邮箱', fillPassword: '请填写密码', passwordMinLength: '密码至少 6 位', passwordsNotMatch: '两次密码不一致', loginFailed: '登录失败', registerFailed: '注册失败', loginSuccess: '登录成功', registerSuccess: '注册成功' },
delete: '删除', dashboard: { title: '我的学习', desc: '掌握你的学习进度和统计', inProgressCourses: '学习中课程', completedLessons: '已完成课时', favoritePrompts: '收藏提示词', studyDays: '学习天数', todayLearned: '今日学习', tabProgress: '学习进度', tabFavorites: '收藏夹', tabProfile: '个人设置', noLearningRecords: '还没有学习记录', browseCourses: '浏览课程', learningProgress: '学习进度', lessonCount: '{completed}/{total} 课时 ({progress}%)', noFavorites: '还没有收藏的提示词', browsePrompts: '浏览提示词', profile: '个人资料', nicknameLabel: '昵称', nicknamePlaceholder: '输入昵称', memberPlan: '会员计划', freeUser: '免费用户', memberExpire: '会员到期', joinDate: '注册时间', saveSuccess: '保存成功', saveFailed: '保存失败', loadFailed: '加载数据失败' },
confirm: '确认', community: { title: '社区', desc: '与 AI 学习者交流心得', createPost: '+ 发帖', latest: '最新', following: '关注', newPost: '发布新帖', postTitle: '标题', contentPlaceholder: '分享你的 AI 学习心得、实战经验...', tagsPlaceholder: '标签(逗号分隔)', publishing: '发布中...', publish: '发布', feedEmpty: '关注更多用户,发现精彩内容', postsEmpty: '还没有帖子,来发第一帖吧!', commentPlaceholder: '写下你的评论...', sending: '发送中...', comment: '评论', followed: '已关注', follow: '+ 关注' },
search: '搜索', notifications: { title: '通知', unreadCount: '你有 {n} 条未读通知', noUnread: '暂无未读通知', markAllRead: '全部已读', markRead: '已读', emptyTitle: '暂无通知', emptyDesc: '点赞、评论或关注你的人会出现在这里' },
back: '返回', my: { desc: '管理你的个人信息和收藏', learningProgress: '学习进度', learningProgressDesc: '查看你的课程学习进度', favorites: '我的收藏', favoritesDesc: '提示词、课程等收藏内容', memberCenter: '会员中心', memberDesc: '管理会员订阅和权益', settings: '设置', settingsDesc: '账号设置和安全偏好', analyticsDesc: '基于对话的知识掌握度分析', pathDesc: '分阶段系统掌握 AI 技能' },
login: '登录', settings: { title: '设置', desc: '管理你的账号偏好', personalInfo: '个人信息', nicknameLabel: '昵称', emailLabel: '邮箱', saveChanges: '保存修改', saveSuccess: '保存成功', accountSecurity: '账号安全' },
register: '注册', myLearning: { back: '返回我的', title: '学习进度', desc: '跟踪你的课程学习进度', empty: '还没有学习任何课程', browseCourses: '去选课', lessonCount: '已完成 {completed}/{total} 课时' },
logout: '退出登录', favorites: { back: '返回我的', title: '我的收藏', desc: '收藏的提示词和课程内容', empty: '还没有收藏任何内容', browsePrompts: '浏览提示词' },
retry: '重试', courses: { title: '专题', desc: '系统化探索 AI,从入门到精通', empty: '暂无专题内容', free: '免费', paid: '付费', moduleCount: '{n} 模块' },
noData: '暂无数据', search: { title: '搜索结果', placeholder: '搜索专题、提示词、工具、文章...', emptyHint: '输入关键词搜索', noResults: '未找到与 "{q}" 相关的结果', resultsCount: '找到 {n} 个结果', groupCourse: '专题', groupPrompt: '提示词', groupTool: 'AI 工具', groupContent: '文章' },
viewAll: '查看全部', tools: { title: 'AI 工具库', desc: '收录优质 AI 工具,助力工作效率提升' },
}, discover: { desc: '探索热门内容和精选推荐', hotCourses: '热门课程', hotPrompts: '热门提示词', hotPosts: '热门讨论' },
nav: { footer: { tagline: '让每个人都能用好 AI', explore: '探索', about: '关于', aboutUs: '关于我们', privacy: '隐私政策', terms: '服务协议', aiAgreement: 'AI 服务协议', contact: '联系方式', copyright: '© {year} 北京宇之然科技中心 版权所有', models: '模型百科', aiTools: 'AI 工具', articles: '文章', skills: '技能' },
home: '首页', models: { title: 'AI 模型百科', desc: '收录主流大语言模型,全面对比各项参数', tableName: '模型名称', tableProvider: '提供商', tableCapabilities: '能力', tableContext: '上下文', tableMaxOutput: '最大输出', tablePricing: '价格', free: '免费', recommended: '推荐', pricingFree: '免费', pricingMixed: '免费/付费', pricingPaid: '付费' },
courses: '课程', error: { title: '出错了', desc: '页面加载失败,请稍后重试', reload: '重新加载' },
prompts: '提示词库', notFound: { title: '404', desc: '页面未找到', backToHome: '返回首页' },
sandbox: 'AI 沙盒', share: { missingToken: '缺少分享参数', invalidLink: '分享链接无效', notAvailable: '分享内容不可用', expired: '该分享链接可能已过期或不存在', goToSandbox: '前往 AI 沙盒', backToSandbox: 'AI 沙盒', modelInfo: '模型: {model} · {date}' },
discover: '发现', path: { back: '返回我的', totalProgress: '总进度', taskCount: '{completed}/{total} 任务' },
my: '我的', sandbox: { title: 'AI 沙盒', subtitle: '在线体验 AI 对话,边学边练', placeholder: '输入你的问题...', send: '发送', sending: '发送中', newChat: '新对话', history: '历史记录', searchHistory: '搜索历史...', noHistory: '暂无历史记录', sceneGeneral: '通用对话', sceneCoding: '编程助手', sceneWriting: '写作助手', sceneStudy: '学习辅导', sceneEnglish: '英语学习', modelGeneral: '通用模式', modelDeepSeek: 'DeepSeek V4 Flash', advancedParams: '高级参数', temperature: 'Temperature', topP: 'Top P', maxTokens: 'Max Tokens', helpful: '有用', notHelpful: '没用', runInCodeSandbox: '在代码沙盒中运行', shareToCommunity: '分享到社区', copyShareLink: '复制分享链接', linkCopied: '链接已复制', loginForMore: '登录使用更多', dailyQuota: '今日已用 {used} 次,剩余 {remaining} 次', aiReplyDisclaimer: 'AI 回复由人工智能生成,仅供参考。', loginForMoreQuota: '登录后可获得更多使用次数和更多模型选择。', justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前' },
tools: '工具', learning: { analytics: '学情分析', analyticsDesc: '基于 AI 沙盒对话分析你的学习情况', path: '学习路径', pathDesc: '从入门到精通,系统掌握 AI 技能', totalSessions: 'AI 对话次数', domainsCovered: '涉及知识领域', avgMastery: '平均掌握度', knowledgeDomains: '知识领域覆盖', weakAreas: '薄弱环节', weakDesc: '以下领域你较少涉及,建议加强学习:', recommendations: '推荐学习', recDesc: '根据你的薄弱环节推荐以下内容', toStrengthen: '待加强', conversations: '{count} 次对话', clickToGo: '点击前往' },
community: '社区', member: { title: '会员中心', desc: '管理你的会员订阅', currentPlan: '当前会员', freeUser: '你当前是免费用户', monthly: '月卡会员', yearly: '年卡会员', monthlyPrice: '开通月卡 ¥29.9/月', yearlyPrice: '开通年卡 ¥199/年', expires: '到期时间:{date}', benefits: '会员权益:全部课程 + 不限次沙箱 + 专属提示词库 + 去广告', orderHistory: '订单记录', noOrders: '暂无订单记录', processing: '处理中...', planFree: '免费', planMonthly: '月卡', planYearly: '年卡', priceMonthly: '¥29.9', priceYearly: '¥199', perMonth: '/月', perYear: '/年', popular: '最受欢迎', featureSandbox: 'AI 沙盒', featureSandboxFree: '10 次/日', featureSandboxPro: '100 次/日', featureSandboxUnlimited: '不限次', featureModels: '模型选择', featureModelsFree: '1 个模型', featureModelsPro: '2 个模型', featureModelsPremium: '全部模型', featurePrompts: '提示词库', featurePromptsFree: '基础', featurePromptsPro: '全部', featurePromptsPremium: '全部 + 专属', featureCourses: '课程学习', featureCoursesFree: '部分免费', featureCoursesPro: '全部', featureCoursesPremium: '全部', featureAds: '广告', featureAdsFree: '有广告', featureAdsPro: '去广告', featureAdsPremium: '去广告', dailyQuota: '日配额用量', used: '已用 {n} 次', subscribe: '开通', currentPlan_badge: '当前方案' },
}, compare: { title: '对比实验室', desc: '同题对比不同模型的表现', placeholder: '输入你想对比的问题或提示词...', startCompare: '开始对比', comparing: '对比中...', backToSandbox: '返回沙箱', noResponse: '无响应' },
sandbox: { codeSandbox: { title: '代码沙盒', run: '运行', runShortcut: '运行 (⌘⏎)', template: '模板...', blank: '空白', react: 'React (CDN)', chart: '图表 (Chart.js)', three: '3D (Three.js)', console: '控制台输出' },
title: 'AI 沙盒', skills: { title: '技能库', desc: '可组合的 AI 学习技能模块', search: '搜索技能...', allCategories: '全部分类', allDifficulties: '全部难度', beginner: '入门', intermediate: '中级', advanced: '高级', tasks: '练习任务', starters: '试试这些问题', prerequisites: '前置技能', apply: '使用此技能', categories: { basic: '基础', technical: '技术', creative: '创意', education: '教育', advanced: '进阶', career: '职业' } },
subtitle: '在线体验 AI 对话,边学边练', promptWorkshop: { title: '提示词工坊', desc: '编写、测试、优化你的提示词', editor: '提示词编辑', test: '测试提示词', testing: '测试中...', clear: '清空', saveToLibrary: '保存到提示词库', saveSuccess: '保存成功!', variables: '变量设置', role: '角色', task: '任务', outputFormat: '输出格式', constraints: '约束条件', insert: '插入', testResult: '测试结果', saveDialogTitle: '保存提示词', saveTitle: '标题 *', saveDesc: '描述', saveTags: '标签', saveTagsPlaceholder: '用逗号分隔,如:编程,Python,调试', saving: '保存中...' },
placeholder: '输入你的问题...', assistant: { title: 'AI 助手', greeting: '你好!我是宇之然 AI 助手,可以帮你了解和使用本站功能。试试下面的问题:', placeholder: '输入你的问题...', error: '出错啦:{message}', loginPrompt: '📝 登录后可体验完整 AI 对话功能。\n\n点击右上角「登录」或「注册」即可开始使用,解锁 AI 助手的全部能力。' },
send: '发送',
sending: '发送中',
newChat: '新对话',
history: '历史记录',
searchHistory: '搜索历史...',
noHistory: '暂无历史记录',
sceneGeneral: '通用对话',
sceneCoding: '编程助手',
sceneWriting: '写作助手',
sceneStudy: '学习辅导',
sceneEnglish: '英语学习',
modelGeneral: '通用模式',
modelDeepSeek: 'DeepSeek V4 Flash',
advancedParams: '高级参数',
temperature: 'Temperature',
topP: 'Top P',
maxTokens: 'Max Tokens',
helpful: '有用',
notHelpful: '没用',
runInCodeSandbox: '在代码沙盒中运行',
shareToCommunity: '分享到社区',
copyShareLink: '复制分享链接',
linkCopied: '链接已复制',
loginForMore: '登录使用更多',
dailyQuota: '今日已用 {used} 次,剩余 {remaining} 次',
aiReplyDisclaimer: 'AI 回复由人工智能生成,仅供参考。',
loginForMoreQuota: '登录后可获得更多使用次数和更多模型选择。',
justNow: '刚刚',
minutesAgo: '{n} 分钟前',
hoursAgo: '{n} 小时前',
},
auth: {
loginTitle: '登录',
registerTitle: '注册',
phone: '手机号',
password: '密码',
nickname: '昵称',
},
learning: {
analytics: '学情分析',
analyticsDesc: '基于 AI 沙盒对话分析你的学习情况',
path: '学习路径',
pathDesc: '从入门到精通,系统掌握 AI 技能',
totalSessions: 'AI 对话次数',
domainsCovered: '涉及知识领域',
avgMastery: '平均掌握度',
knowledgeDomains: '知识领域覆盖',
weakAreas: '薄弱环节',
weakDesc: '以下领域你较少涉及,建议加强学习:',
recommendations: '推荐学习',
recDesc: '根据你的薄弱环节推荐以下内容',
toStrengthen: '待加强',
conversations: '{count} 次对话',
clickToGo: '点击前往',
},
member: {
title: '会员中心',
desc: '管理你的会员订阅',
currentPlan: '当前会员',
freeUser: '你当前是免费用户',
monthly: '月卡会员',
yearly: '年卡会员',
monthlyPrice: '开通月卡 ¥29.9/月',
yearlyPrice: '开通年卡 ¥199/年',
expires: '到期时间:{date}',
benefits: '会员权益:全部课程 + 不限次沙箱 + 专属提示词库 + 去广告',
orderHistory: '订单记录',
noOrders: '暂无订单记录',
processing: '处理中...',
planFree: '免费',
planMonthly: '月卡',
planYearly: '年卡',
priceMonthly: '¥29.9',
priceYearly: '¥199',
perMonth: '/月',
perYear: '/年',
popular: '最受欢迎',
featureSandbox: 'AI 沙盒',
featureSandboxFree: '10 次/日',
featureSandboxPro: '100 次/日',
featureSandboxUnlimited: '不限次',
featureModels: '模型选择',
featureModelsFree: '1 个模型',
featureModelsPro: '2 个模型',
featureModelsPremium: '全部模型',
featurePrompts: '提示词库',
featurePromptsFree: '基础',
featurePromptsPro: '全部',
featurePromptsPremium: '全部 + 专属',
featureCourses: '课程学习',
featureCoursesFree: '部分免费',
featureCoursesPro: '全部',
featureCoursesPremium: '全部',
featureAds: '广告',
featureAdsFree: '有广告',
featureAdsPro: '去广告',
featureAdsPremium: '去广告',
dailyQuota: '日配额用量',
used: '已用 {n} 次',
subscribe: '开通',
currentPlan_badge: '当前方案',
},
compare: {
title: '对比实验室',
desc: '同题对比不同模型的表现',
placeholder: '输入你想对比的问题或提示词...',
startCompare: '开始对比',
comparing: '对比中...',
},
codeSandbox: {
title: '代码沙盒',
run: '运行',
runShortcut: '运行 (⌘⏎)',
template: '模板...',
blank: '空白',
react: 'React (CDN)',
chart: '图表 (Chart.js)',
three: '3D (Three.js)',
console: '控制台输出',
},
skills: {
title: '技能库',
desc: '可组合的 AI 学习技能模块',
search: '搜索技能...',
allCategories: '全部分类',
allDifficulties: '全部难度',
beginner: '入门',
intermediate: '中级',
advanced: '高级',
tasks: '练习任务',
starters: '试试这些问题',
prerequisites: '前置技能',
apply: '使用此技能',
categories: {
basic: '基础',
technical: '技术',
creative: '创意',
education: '教育',
advanced: '进阶',
career: '职业',
},
},
promptWorkshop: {
title: '提示词工坊',
desc: '编写、测试、优化你的提示词',
editor: '提示词编辑',
test: '测试提示词',
testing: '测试中...',
clear: '清空',
saveToLibrary: '保存到提示词库',
saveSuccess: '保存成功!',
variables: '变量设置',
role: '角色',
task: '任务',
outputFormat: '输出格式',
constraints: '约束条件',
insert: '插入',
testResult: '测试结果',
saveDialogTitle: '保存提示词',
saveTitle: '标题 *',
saveDesc: '描述',
saveTags: '标签',
saveTagsPlaceholder: '用逗号分隔,如:编程,Python,调试',
saving: '保存中...',
},
assistant: {
title: 'AI 助手',
greeting: '你好!我是宇之然 AI 助手,可以帮你了解和使用本站功能。试试下面的问题:',
placeholder: '输入你的问题...',
},
} }
export type Translations = typeof zh export type Translations = typeof zh