diff --git a/.env.example b/.env.example index 2a3e778..c134ace 100644 --- a/.env.example +++ b/.env.example @@ -1,31 +1,31 @@ # Supabase 配置 -NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +NEXT_PUBLIC_SUPABASE_URL=https://riwtulmitqioswmgwftg.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx... SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key # Twilio 配置 -TWILIO_ACCOUNT_SID=your_twilio_account_sid +TWILIO_ACCOUNT_SID=AC0123456789abcdef0123456789abcdef TWILIO_AUTH_TOKEN=your_twilio_auth_token -TWILIO_API_KEY=your_twilio_api_key -TWILIO_API_SECRET=your_twilio_api_secret +TWILIO_API_KEY_SID=SK0123456789abcdef0123456789abcdef +TWILIO_API_KEY_SECRET=0123456789abcdef0123456789abcdef # Stripe 配置 -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key -STRIPE_SECRET_KEY=your_stripe_secret_key +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RTwLuDWamLO9gYlv7ZX0Jj2aLBkADGWmTC3NP0aoez3nEdnLlQiWH3KUie1C45CSa1ho3DvTm0GqR59X0sNTnqN00Q15Fq0zw +STRIPE_SECRET_KEY=sk_test_51RTwLuDWamLO9gYliBCJFtPob28ttoTtvsglGtyXrHkrnuppY2ScnVz7BRh1hCHzvOXcOyvMejBRVsx5vMpgKLVE0065W8VOU8 STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret # ElevenLabs 配置 ELEVENLABS_API_KEY=your_elevenlabs_api_key # JWT 密钥 -JWT_SECRET=your_jwt_secret +JWT_SECRET=your_jwt_secret_key_here # 应用配置 NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_SECRET=your_nextauth_secret +NEXTAUTH_SECRET=your_nextauth_secret_key_here # 数据库配置 -DATABASE_URL=your_database_url +DATABASE_URL=your_database_url_here # 邮件配置 (可选,用于通知) SMTP_HOST=your_smtp_host @@ -41,4 +41,7 @@ MAX_FILE_SIZE=10485760 # 10MB # 支付配置 PAYMENT_SUCCESS_URL=http://localhost:3000/payment/success -PAYMENT_CANCEL_URL=http://localhost:3000/payment/cancel \ No newline at end of file +PAYMENT_CANCEL_URL=http://localhost:3000/payment/cancel + +# OpenAI 配置 +OPENAI_API_KEY=sk_live_o_pqmR3A26poD7ltpYgZ1aoDZEOaAJr8lUlvTw \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..d807203 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,368 @@ +# 部署指南 + +## 概述 + +本文档介绍如何将口译服务管理后台部署到生产环境。项目支持多种部署方式,推荐使用 Vercel 进行快速部署。 + +## 前置条件 + +### 1. 准备服务账户 + +在部署前,请确保已经配置好以下服务: + +- **Supabase** - 数据库和身份验证服务 +- **Stripe** - 支付处理服务(可选) +- **OpenAI** - AI 服务(可选) +- **Twilio** - 通信服务(可选) + +### 2. 环境变量配置 + +请准备以下环境变量: + +```env +# Supabase 配置 +NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + +# Stripe 配置 +STRIPE_SECRET_KEY=sk_test_... +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... + +# OpenAI 配置 +OPENAI_API_KEY=sk-... + +# Twilio 配置 +TWILIO_ACCOUNT_SID=AC... +TWILIO_API_KEY_SID=SK... +TWILIO_API_KEY_SECRET=... + +# 应用配置 +NEXTAUTH_SECRET=your-nextauth-secret +JWT_SECRET=your-jwt-secret +NEXTAUTH_URL=https://your-domain.com +``` + +## 部署方式 + +### 方式一:Vercel 部署(推荐) + +#### 1. 准备代码仓库 + +```bash +# 确保代码已提交到 Git 仓库 +git add . +git commit -m "准备部署" +git push origin main +``` + +#### 2. 连接 Vercel + +1. 访问 [Vercel Dashboard](https://vercel.com/dashboard) +2. 点击 "New Project" +3. 导入你的 Git 仓库 +4. 选择 "Next.js" 框架预设 + +#### 3. 配置环境变量 + +在 Vercel 项目设置中添加所有必要的环境变量: + +1. 进入项目 Settings +2. 选择 Environment Variables +3. 添加所有上述环境变量 + +#### 4. 部署 + +1. 点击 "Deploy" 开始部署 +2. 等待构建完成 +3. 访问提供的 URL 验证部署 + +### 方式二:Docker 部署 + +#### 1. 创建 Dockerfile + +```dockerfile +FROM node:18-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --only=production + +FROM node:18-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED 1 +RUN npm run build + +FROM node:18-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 + +CMD ["node", "server.js"] +``` + +#### 2. 构建和运行 + +```bash +# 构建镜像 +docker build -t interpretation-admin . + +# 运行容器 +docker run -p 3000:3000 --env-file .env.local interpretation-admin +``` + +### 方式三:传统服务器部署 + +#### 1. 服务器环境准备 + +```bash +# 安装 Node.js 18+ +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs + +# 安装 PM2 +npm install -g pm2 +``` + +#### 2. 代码部署 + +```bash +# 克隆代码 +git clone https://github.com/your-username/interpretation-admin.git +cd interpretation-admin + +# 安装依赖 +npm install + +# 构建项目 +npm run build + +# 启动服务 +pm2 start npm --name "interpretation-admin" -- start +``` + +## 数据库设置 + +### 1. 执行数据库脚本 + +在 Supabase Dashboard 中执行 `database/schema.sql` 脚本: + +1. 登录 Supabase Dashboard +2. 进入 SQL Editor +3. 复制粘贴 `database/schema.sql` 内容 +4. 点击 "Run" 执行 + +### 2. 配置 RLS 策略 + +确保行级安全策略正确配置,保护用户数据安全。 + +### 3. 创建管理员账户 + +```sql +-- 在 Supabase SQL Editor 中执行 +INSERT INTO users (id, email, name, user_type, status) +VALUES ( + 'your-admin-user-id', + 'admin@example.com', + '系统管理员', + 'admin', + 'active' +); +``` + +## SSL 证书配置 + +### 使用 Let's Encrypt + +```bash +# 安装 Certbot +sudo apt install certbot python3-certbot-nginx + +# 获取证书 +sudo certbot --nginx -d yourdomain.com + +# 自动续期 +sudo crontab -e +# 添加: 0 12 * * * /usr/bin/certbot renew --quiet +``` + +## 性能优化 + +### 1. 启用缓存 + +```javascript +// next.config.js +module.exports = { + async headers() { + return [ + { + source: '/api/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=300, stale-while-revalidate=60' + } + ] + } + ] + } +} +``` + +### 2. 图片优化 + +确保使用 Next.js Image 组件进行图片优化。 + +### 3. 代码分割 + +利用 Next.js 的自动代码分割功能。 + +## 监控和日志 + +### 1. 应用监控 + +推荐使用以下监控服务: + +- **Vercel Analytics** - 性能监控 +- **Sentry** - 错误监控 +- **LogRocket** - 用户行为监控 + +### 2. 数据库监控 + +- 使用 Supabase Dashboard 监控数据库性能 +- 设置告警规则 +- 定期检查慢查询 + +## 备份策略 + +### 1. 数据库备份 + +```bash +# 每日自动备份 +0 2 * * * pg_dump $DATABASE_URL > backup_$(date +\%Y\%m\%d).sql +``` + +### 2. 代码备份 + +- 使用 Git 进行版本控制 +- 定期推送到远程仓库 +- 标记重要版本 + +## 安全配置 + +### 1. 环境变量安全 + +- 使用强密码和随机密钥 +- 定期轮换 API 密钥 +- 不要在代码中硬编码敏感信息 + +### 2. 网络安全 + +- 启用 HTTPS +- 配置 CORS 策略 +- 使用 CSP 头部 + +### 3. 数据库安全 + +- 启用 RLS 策略 +- 使用最小权限原则 +- 定期更新密码 + +## 故障排除 + +### 常见问题 + +1. **构建失败** + - 检查依赖版本兼容性 + - 确认环境变量配置 + +2. **数据库连接失败** + - 验证 Supabase 配置 + - 检查网络连接 + +3. **API 调用失败** + - 检查 API 密钥 + - 验证服务状态 + +### 调试工具 + +- 使用浏览器开发者工具 +- 检查服务器日志 +- 使用 Supabase 日志功能 + +## 更新和维护 + +### 1. 依赖更新 + +```bash +# 检查过期依赖 +npm outdated + +# 更新依赖 +npm update +``` + +### 2. 安全更新 + +```bash +# 检查安全漏洞 +npm audit + +# 修复安全问题 +npm audit fix +``` + +### 3. 定期维护 + +- 清理日志文件 +- 优化数据库 +- 更新 SSL 证书 +- 检查性能指标 + +## 扩展部署 + +### 负载均衡 + +当流量增加时,可以考虑: + +- 使用 CDN 加速静态资源 +- 部署多个实例 +- 使用负载均衡器 + +### 微服务架构 + +对于大型应用,可以考虑: + +- 拆分 API 服务 +- 使用消息队列 +- 实施服务发现 + +## 支持和帮助 + +如果在部署过程中遇到问题,可以: + +1. 查看项目文档 +2. 检查 GitHub Issues +3. 联系技术支持 + +--- + +**注意**: 请确保在生产环境中使用强密码和安全的配置。定期更新依赖包和安全补丁。 \ No newline at end of file diff --git a/FIXES.md b/FIXES.md new file mode 100644 index 0000000..1068ec6 --- /dev/null +++ b/FIXES.md @@ -0,0 +1,183 @@ +# 修复说明文档 + +## 已解决的问题 + +### 1. 多个 GoTrueClient 实例问题 +**问题描述**: 应用程序中同时使用了多种 Supabase 客户端创建方式,导致浏览器中出现多个 GoTrueClient 实例的警告。 + +**解决方案**: +- 统一使用单一的 Supabase 客户端实例 +- 移除了 `createClientComponentClient` 和重复的客户端创建代码 +- 在 `lib/supabase.ts` 中导出统一的 `supabase` 客户端 + +### 2. 路由跳转和认证状态管理 +**问题描述**: 登录后出现 "Abort fetching component" 错误,路由跳转不稳定。 + +**解决方案**: +- 简化了认证状态监听逻辑 +- 统一使用 `auth` 模块中的认证方法 +- 改进了路由跳转的时机和方式 +- 添加了防重复提交机制 + +### 3. 代码结构优化 +**问题描述**: 代码重复,结构混乱,维护困难。 + +**解决方案**: +- 重构了 `lib/supabase.ts`,提供了统一的 API +- 创建了 `auth`、`db`、`storage`、`realtime` 等模块化功能 +- 添加了错误处理和权限检查功能 +- 简化了组件中的 Supabase 调用 + +## 主要改动文件 + +### 1. `lib/supabase.ts` +- 统一了 Supabase 客户端创建 +- 提供了模块化的功能接口 +- 添加了演示模式支持 +- 改进了错误处理 + +### 2. `app/auth/login/page.tsx` +- 使用统一的认证 API +- 改进了用户体验 +- 添加了防重复提交 +- 优化了错误提示 + +### 3. `app/dashboard/page.tsx` +- 简化了认证检查逻辑 +- 使用统一的 Supabase 客户端 +- 改进了加载状态处理 +- 添加了用户友好的界面 + +## 使用说明 + +### 开发环境设置 + +1. **配置环境变量**: + ```bash + cp .env.example .env.local + ``` + + 编辑 `.env.local` 文件,填入你的 Supabase 项目信息: + ```env + NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key + SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + ``` + +2. **启动开发服务器**: + ```bash + npm run dev + ``` + +3. **访问应用**: + 打开浏览器访问 `http://localhost:3000` + +### 演示模式 + +如果没有配置 Supabase 环境变量,应用会自动进入演示模式: +- 显示模拟数据 +- 禁用实际的数据库操作 +- 提供完整的界面预览 + +### 认证流程 + +1. **注册**: `/auth/register` +2. **登录**: `/auth/login` +3. **仪表板**: `/dashboard` (需要登录) +4. **登出**: 点击仪表板右上角的登出按钮 + +## API 使用示例 + +### 认证操作 +```typescript +import { auth } from '../lib/supabase'; + +// 登录 +const { user, session } = await auth.signIn(email, password); + +// 注册 +const { user, session } = await auth.signUp(email, password, userData); + +// 获取当前用户 +const user = await auth.getCurrentUser(); + +// 登出 +await auth.signOut(); +``` + +### 数据库操作 +```typescript +import { db } from '../lib/supabase'; + +// 查询数据 +const users = await db.findMany('users', { status: 'active' }); + +// 插入数据 +const newUser = await db.insert('users', userData); + +// 更新数据 +const updatedUser = await db.update('users', userId, updates); + +// 删除数据 +await db.delete('users', userId); +``` + +### 实时订阅 +```typescript +import { realtime } from '../lib/supabase'; + +// 订阅表变化 +const channel = realtime.subscribe('users', (payload) => { + console.log('用户数据变化:', payload); +}); + +// 取消订阅 +realtime.unsubscribe(channel); +``` + +## 故障排除 + +### 1. 多个 GoTrueClient 实例警告 +这个警告已经通过统一客户端实例解决。如果仍然出现,请检查: +- 是否有其他地方创建了额外的 Supabase 客户端 +- 浏览器缓存是否需要清理 + +### 2. 路由跳转问题 +如果登录后仍然出现路由问题: +- 检查浏览器控制台的错误信息 +- 确保 Supabase 配置正确 +- 尝试清理浏览器缓存和 localStorage + +### 3. 认证状态不同步 +如果用户状态显示不正确: +- 检查网络连接 +- 确认 Supabase 项目状态 +- 查看浏览器开发者工具的网络请求 + +## 下一步开发建议 + +1. **添加实际的数据库表结构** +2. **实现完整的用户权限系统** +3. **添加更多的业务功能模块** +4. **集成 Twilio API 进行实际的通话功能** +5. **添加文件上传和文档翻译功能** +6. **实现支付和订单管理系统** + +## 技术栈 + +- **前端**: Next.js 14, React 18, TypeScript +- **样式**: Tailwind CSS +- **后端**: Supabase (PostgreSQL + Auth + Storage) +- **部署**: Vercel (推荐) + +## 支持 + +如果遇到问题,请检查: +1. Node.js 版本 (推荐 18+) +2. npm 或 yarn 版本 +3. Supabase 项目配置 +4. 环境变量设置 + +--- + +*最后更新: 2024年12月* \ No newline at end of file diff --git a/README.md b/README.md index 2b6211b..94b40e3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 口译服务管理后台 -一个基于 Next.js 和 TypeScript 构建的现代化口译服务管理后台系统。 +一个基于 Next.js 和 TypeScript 构建的现代化口译服务管理后台系统,集成 Supabase 数据库和完整的身份验证功能。 ## 功能特性 @@ -20,6 +20,7 @@ - **订单状态跟踪**:待处理、处理中、已完成、已取消、失败 - **优先级管理**:紧急、高、普通、低 - **详细信息展示**:包括译员信息、时间安排、费用等 +- **实时数据同步**:基于 Supabase 实时订阅 ### 📄 文档管理 - **文档上传管理**:支持多种文档格式 @@ -33,19 +34,22 @@ - **发票状态管理**:草稿、已开具、已付款、已取消 ### 👥 用户管理 -- **用户信息管理**:个人用户和企业用户 +- **多角色用户系统**:个人用户、企业用户、管理员 +- **用户认证**:基于 Supabase Auth 的安全认证 +- **权限控制**:行级安全策略保护数据 - **用户状态跟踪**:活跃状态、登录记录 -- **用户类型区分**:个人用户、企业用户 ### 🎯 译员管理 - **译员信息管理**:译员资料、专业领域 - **译员状态监控**:在线、离线、忙碌状态 - **语言能力管理**:支持的语言对 +- **评价系统**:译员评分和反馈 ### 📞 通话管理 - **实时通话监控**:当前活跃通话 - **通话记录管理**:历史通话记录 - **通话质量统计**:通话时长、费用统计 +- **质量评估**:通话质量评分 ### ⚙️ 系统设置 - **服务费率配置**:为每种服务设置独立费率 @@ -55,35 +59,69 @@ ## 技术栈 +### 前端技术 - **前端框架**:Next.js 14 - **类型系统**:TypeScript - **样式框架**:Tailwind CSS - **图标库**:Heroicons - **状态管理**:React Hooks -- **数据库**:Supabase(可选) -- **部署**:Vercel(推荐) + +### 后端技术 +- **数据库**:Supabase (PostgreSQL) +- **身份验证**:Supabase Auth +- **API**:Next.js API Routes +- **实时功能**:Supabase Realtime + +### 第三方集成 +- **支付处理**:Stripe +- **通信服务**:Twilio +- **AI服务**:OpenAI +- **部署平台**:Vercel ## 安装和运行 ### 环境要求 - Node.js 18.0 或更高版本 - npm 或 yarn +- Supabase 项目(用于数据库和认证) -### 安装依赖 +### 1. 安装依赖 ```bash npm install # 或 yarn install ``` -### 环境配置 +### 2. 环境配置 复制 `.env.example` 到 `.env.local` 并配置必要的环境变量: ```bash cp .env.example .env.local ``` -### 开发模式运行 +配置 Supabase 和其他服务: + +```env +# Supabase 配置 +NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + +# 其他服务配置 +STRIPE_SECRET_KEY=sk_test_... +TWILIO_ACCOUNT_SID=AC... +OPENAI_API_KEY=sk-... +``` + +### 3. 数据库设置 +在 Supabase Dashboard 中执行数据库脚本: + +1. 登录 [Supabase Dashboard](https://supabase.com/dashboard) +2. 进入 SQL Editor +3. 复制 `database/schema.sql` 内容并执行 +4. 创建管理员账户(可选) + +### 4. 开发模式运行 ```bash npm run dev # 或 @@ -92,14 +130,14 @@ yarn dev 访问 [http://localhost:3000](http://localhost:3000) 查看应用。 -### 构建生产版本 +### 5. 构建生产版本 ```bash npm run build # 或 yarn build ``` -### 启动生产服务器 +### 6. 启动生产服务器 ```bash npm start # 或 @@ -112,26 +150,84 @@ yarn start ├── components/ # 可复用组件 ├── lib/ # 工具库和配置 │ ├── demo-data.ts # 演示数据 -│ ├── supabase.ts # Supabase 配置 +│ ├── supabase.ts # Supabase 配置和操作 │ └── utils.ts # 工具函数 ├── pages/ # 页面组件 │ ├── api/ # API 路由 +│ │ ├── auth/ # 认证 API +│ │ ├── orders/ # 订单 API +│ │ ├── users/ # 用户 API +│ │ └── ... # 其他 API │ ├── auth/ # 认证页面 │ └── dashboard/ # 管理后台页面 +├── database/ # 数据库相关文件 +│ ├── schema.sql # 数据库结构 +│ └── README.md # 数据库说明 +├── types/ # TypeScript 类型定义 +│ ├── database.ts # 数据库类型 +│ └── auth.ts # 认证类型 ├── public/ # 静态资源 ├── styles/ # 样式文件 -├── types/ # TypeScript 类型定义 └── utils/ # 工具函数 ``` +## API 接口 + +### 认证接口 +- `POST /api/auth/register` - 用户注册 +- `POST /api/auth/login` - 用户登录 +- `POST /api/auth/logout` - 用户登出 +- `GET /api/auth/me` - 获取当前用户信息 + +### 订单接口 +- `GET /api/orders` - 获取订单列表 +- `POST /api/orders` - 创建新订单 +- `PUT /api/orders/:id` - 更新订单 +- `DELETE /api/orders/:id` - 删除订单 + +### 用户接口 +- `GET /api/users` - 获取用户列表 +- `POST /api/users` - 创建用户 +- `PUT /api/users/:id` - 更新用户信息 +- `DELETE /api/users/:id` - 删除用户 + +## 数据库架构 + +### 核心表结构 +- **users** - 用户表(支持个人/企业/管理员) +- **enterprises** - 企业表 +- **orders** - 订单表 +- **invoices** - 发票表 +- **interpreters** - 译员表 +- **calls** - 通话记录表 +- **documents** - 文档翻译表 + +### 安全策略 +- 启用行级安全 (RLS) +- 基于用户角色的权限控制 +- 数据访问审计日志 + +详细说明请参考 [database/README.md](./database/README.md) + ## 核心功能说明 +### 身份验证系统 +- **多角色支持**:个人用户、企业用户、管理员 +- **安全认证**:基于 JWT 令牌的会话管理 +- **权限控制**:细粒度的权限管理 +- **会话管理**:自动刷新和过期处理 + ### 费率优先级机制 系统采用三级费率优先级: 1. **企业合同费率**:优先级最高,适用于企业员工 2. **系统通用费率**:适用于个人用户和无合同企业 3. **默认费率**:系统兜底费率 +### 实时功能 +- **订单状态更新**:实时同步订单状态变化 +- **通话监控**:实时监控活跃通话 +- **消息通知**:即时消息推送 + ### 演示模式 项目支持演示模式,无需配置数据库即可体验完整功能: - 自动检测 Supabase 配置 @@ -151,6 +247,8 @@ yarn start 3. 配置环境变量 4. 部署完成 +详细部署指南请参考 [DEPLOYMENT.md](./DEPLOYMENT.md) + ### 其他平台部署 项目支持部署到任何支持 Next.js 的平台,如: - Netlify @@ -160,15 +258,31 @@ yarn start ## 环境变量 +### 必需配置 ```bash -# Supabase 配置(可选) -NEXT_PUBLIC_SUPABASE_URL=your_supabase_url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +# Supabase 配置 +NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key -# 其他第三方服务配置 -TWILIO_ACCOUNT_SID=your_twilio_sid -TWILIO_AUTH_TOKEN=your_twilio_token -OPENAI_API_KEY=your_openai_key +# 应用配置 +NEXTAUTH_SECRET=your-nextauth-secret +JWT_SECRET=your-jwt-secret +``` + +### 可选配置 +```bash +# Stripe 支付 +STRIPE_SECRET_KEY=sk_test_... +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... + +# Twilio 通信 +TWILIO_ACCOUNT_SID=AC... +TWILIO_API_KEY_SID=SK... +TWILIO_API_KEY_SECRET=... + +# OpenAI 服务 +OPENAI_API_KEY=sk-... ``` ## 贡献指南 @@ -179,17 +293,36 @@ OPENAI_API_KEY=your_openai_key 4. 推送到分支 (`git push origin feature/AmazingFeature`) 5. 创建 Pull Request +### 开发规范 +- 使用 TypeScript 进行类型安全开发 +- 遵循 ESLint 和 Prettier 代码规范 +- 编写单元测试 +- 更新相关文档 + ## 许可证 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 -## 联系方式 +## 文档和支持 +### 相关文档 +- [部署指南](./DEPLOYMENT.md) +- [数据库说明](./database/README.md) +- [API 参考](./docs/api-reference.md) + +### 联系方式 - 项目地址:[http://git.wanzhongtech.com/mars/Twilioapp-admin](http://git.wanzhongtech.com/mars/Twilioapp-admin) - 问题反馈:请在 GitLab Issues 中提交 ## 更新日志 +### v1.1.0 (2024-01-30) +- ✅ 集成 Supabase 数据库和身份验证 +- ✅ 实现完整的 API 接口 +- ✅ 添加用户认证和权限控制 +- ✅ 优化数据库结构和安全策略 +- ✅ 完善部署文档和指南 + ### v1.0.0 (2024-01-30) - ✅ 完成企业服务管理功能 - ✅ 完成订单管理功能 diff --git a/check-table.js b/check-table.js new file mode 100644 index 0000000..93ad0dc --- /dev/null +++ b/check-table.js @@ -0,0 +1,54 @@ +const { createClient } = require('@supabase/supabase-js'); + +const supabaseUrl = 'https://poxwjzdianersitpnvdy.supabase.co'; +const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBveHdqemRpYW5lcnNpdHBudmR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTExNjk4MjMsImV4cCI6MjA2Njc0NTgyM30.FkgCCSHK0_i8bNFIhhN3k6dEbP5PpE52IggcVJC4Aj8'; + +const supabase = createClient(supabaseUrl, supabaseKey); + +async function checkTable() { + try { + console.log('检查users表结构...'); + + // 尝试查询表中的所有数据 + const { data, error } = await supabase + .from('users') + .select('*') + .limit(1); + + if (error) { + console.error('查询错误:', error); + } else { + console.log('查询结果:', data); + if (data && data.length > 0) { + console.log('表列名:', Object.keys(data[0])); + } else { + console.log('表为空,尝试插入测试数据...'); + + // 尝试插入一个简单的用户 + const { data: insertData, error: insertError } = await supabase + .from('users') + .insert([ + { + email: 'admin@example.com', + name: '系统管理员', + phone: '13800138000', + user_type: 'admin', + status: 'active' + } + ]) + .select(); + + if (insertError) { + console.error('插入错误:', insertError); + } else { + console.log('插入成功:', insertData); + } + } + } + + } catch (error) { + console.error('操作失败:', error); + } +} + +checkTable(); \ No newline at end of file diff --git a/components/Layout/DashboardLayout.tsx b/components/Layout/DashboardLayout.tsx new file mode 100644 index 0000000..0d44ac0 --- /dev/null +++ b/components/Layout/DashboardLayout.tsx @@ -0,0 +1,287 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import Head from 'next/head'; +import { + HomeIcon, + UsersIcon, + PhoneIcon, + CalendarIcon, + DocumentTextIcon, + CurrencyDollarIcon, + ChartBarIcon, + CogIcon, + BellIcon, + UserGroupIcon, + ClipboardDocumentListIcon, + Bars3Icon, + XMarkIcon, + BuildingOfficeIcon, + FolderIcon, + DocumentIcon, + ReceiptPercentIcon, + ChevronDownIcon, + ChevronRightIcon, + LanguageIcon, + ArrowRightOnRectangleIcon +} from '@heroicons/react/24/outline'; + +interface DashboardLayoutProps { + children: React.ReactNode; + title?: string; +} + +const navigation = [ + { name: '仪表盘', href: '/dashboard', icon: HomeIcon }, + { name: '用户管理', href: '/dashboard/users', icon: UsersIcon }, + { name: '翻译员管理', href: '/dashboard/interpreters', icon: LanguageIcon }, + { + name: '订单管理', + icon: DocumentTextIcon, + children: [ + { name: '订单列表', href: '/dashboard/orders' }, + { name: '发票管理', href: '/dashboard/invoices' } + ] + }, + { name: '通话记录', href: '/dashboard/calls', icon: PhoneIcon }, + { name: '企业服务', href: '/dashboard/enterprise', icon: BuildingOfficeIcon }, + { name: '文档管理', href: '/dashboard/documents', icon: FolderIcon }, + { name: '系统设置', href: '/dashboard/settings', icon: CogIcon }, +]; + +export default function DashboardLayout({ children, title = '管理后台' }: DashboardLayoutProps) { + const [sidebarOpen, setSidebarOpen] = useState(false); + const [isDemoMode, setIsDemoMode] = useState(true); // 始终启用演示模式 + const [expandedItems, setExpandedItems] = useState([]); + const router = useRouter(); + + useEffect(() => { + // 演示模式始终启用 + setIsDemoMode(true); + }, []); + + const handleLogout = () => { + // 清除本地存储并跳转到登录页 + localStorage.removeItem('access_token'); + localStorage.removeItem('user'); + router.push('/auth/login'); + }; + + const toggleExpanded = (itemName: string) => { + setExpandedItems(prev => + prev.includes(itemName) + ? prev.filter(name => name !== itemName) + : [...prev, itemName] + ); + }; + + const isItemActive = (item: any) => { + if (item.href) { + return router.pathname === item.href; + } + if (item.children) { + return item.children.some((child: any) => router.pathname === child.href); + } + return false; + }; + + const renderNavItem = (item: any) => { + const hasChildren = item.children && item.children.length > 0; + const isActive = isItemActive(item); + const isExpanded = expandedItems.includes(item.name); + + if (hasChildren) { + return ( +
+ + {isExpanded && ( +
+ {item.children.map((child: any) => ( + + {child.name} + + ))} +
+ )} +
+ ); + } + + return ( + + + {item.name} + + ); + }; + + return ( + <> + + {title} - 口译服务管理平台 + + + + +
+ {/* 移动端侧边栏 */} +
+
setSidebarOpen(false)} /> +
+
+ +
+
+

口译管理系统

+ {isDemoMode && ( + + 演示模式 + + )} +
+
+ +
+
+
+
+
+ +
+
+
+

管理员

+

admin@demo.com

+
+
+ +
+
+
+ + {/* 桌面端侧边栏 */} +
+
+
+
+

口译管理系统

+ {isDemoMode && ( + + 演示模式 + + )} +
+
+ +
+
+
+
+ +
+
+
+

管理员

+

admin@demo.com

+
+
+ +
+
+
+
+
+ + {/* 主内容区域 */} +
+
+ +
+

{title}

+ {isDemoMode && ( + + 演示模式 + + )} +
+
+ +
+
+
+ {children} +
+
+
+
+
+ + ); +} \ No newline at end of file diff --git a/create-admin.js b/create-admin.js new file mode 100644 index 0000000..d2c03d9 --- /dev/null +++ b/create-admin.js @@ -0,0 +1,32 @@ +const bcrypt = require('bcryptjs'); + +async function createAdminHash() { + const password = 'admin123'; + const saltRounds = 10; + + try { + const hash = await bcrypt.hash(password, saltRounds); + console.log('管理员密码哈希:', hash); + + // 生成插入SQL + const sql = ` +INSERT INTO users (email, password_hash, name, phone, user_type, status, created_at, updated_at) +VALUES ( + 'admin@example.com', + '${hash}', + '系统管理员', + '13800138000', + 'admin', + 'active', + NOW(), + NOW() +);`; + + console.log('\n插入SQL:'); + console.log(sql); + } catch (error) { + console.error('生成哈希失败:', error); + } +} + +createAdminHash(); \ No newline at end of file diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..a09c47d --- /dev/null +++ b/database/README.md @@ -0,0 +1,179 @@ +# 数据库设置说明 + +## 概述 + +本项目使用 Supabase 作为后端数据库服务,提供 PostgreSQL 数据库、身份验证、实时订阅等功能。 + +## 数据库配置步骤 + +### 1. 创建 Supabase 项目 + +1. 访问 [Supabase Dashboard](https://supabase.com/dashboard) +2. 创建新项目 +3. 记录项目的 URL 和 API 密钥 + +### 2. 执行数据库脚本 + +1. 在 Supabase Dashboard 中,进入 SQL Editor +2. 将 `schema.sql` 文件的内容复制粘贴到编辑器中 +3. 点击 "Run" 执行脚本 + +### 3. 配置环境变量 + +在项目根目录的 `.env.local` 文件中配置以下变量: + +```env +# Supabase 配置 +NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key +``` + +## 数据库表结构 + +### 核心表 + +1. **users** - 用户表 + - 存储用户基本信息 + - 支持个人用户、企业用户、管理员三种类型 + +2. **enterprises** - 企业表 + - 存储企业客户信息 + - 与用户表关联 + +3. **orders** - 订单表 + - 存储口译服务订单 + - 关联用户和译员 + +4. **invoices** - 发票表 + - 存储发票信息 + - 支持个人和企业发票 + +5. **interpreters** - 译员表 + - 存储译员信息和能力 + - 支持多语言和专业领域 + +6. **calls** - 通话记录表 + - 存储实际通话记录 + - 用于统计和质量评估 + +7. **documents** - 文档翻译表 + - 存储文档翻译任务 + - 跟踪翻译进度 + +### 辅助表 + +1. **enterprise_contracts** - 企业合同表 +2. **enterprise_bills** - 企业账单表 +3. **system_settings** - 系统设置表 + +## 安全策略 + +### 行级安全 (RLS) + +所有表都启用了行级安全策略: + +- **管理员权限**:管理员可以访问所有数据 +- **用户权限**:普通用户只能访问自己的数据 +- **企业权限**:企业用户可以访问所属企业的数据 + +### 身份验证 + +使用 Supabase Auth 进行用户身份验证: + +- 支持邮箱密码注册/登录 +- 支持社交登录(可选) +- JWT 令牌验证 +- 会话管理 + +## 数据库索引 + +为提高查询性能,已创建以下索引: + +- 用户邮箱索引 +- 用户类型索引 +- 订单状态索引 +- 订单创建时间索引 +- 其他常用查询字段索引 + +## 触发器 + +自动更新 `updated_at` 字段的触发器已为所有表配置。 + +## 初始数据 + +系统会自动插入以下初始设置: + +- 应用基本配置 +- 支持的语言列表 +- 默认货币设置 +- 税率配置 + +## 备份和恢复 + +Supabase 提供自动备份功能,建议: + +1. 定期检查备份状态 +2. 测试恢复流程 +3. 导出重要数据作为额外备份 + +## 监控和维护 + +建议定期执行以下维护任务: + +1. 检查数据库性能 +2. 清理过期数据 +3. 更新统计信息 +4. 监控存储使用情况 + +## 故障排除 + +### 常见问题 + +1. **连接失败** + - 检查环境变量配置 + - 确认 Supabase 项目状态 + +2. **权限错误** + - 检查 RLS 策略 + - 确认用户角色设置 + +3. **查询性能问题** + - 检查索引使用情况 + - 优化查询语句 + +### 调试工具 + +- Supabase Dashboard 中的 SQL Editor +- 实时日志监控 +- 性能分析工具 + +## 扩展功能 + +### 实时订阅 + +项目支持实时数据订阅,可以监听: + +- 新订单创建 +- 订单状态变更 +- 通话状态更新 + +### 全文搜索 + +可以启用 PostgreSQL 的全文搜索功能来搜索: + +- 用户信息 +- 订单内容 +- 文档内容 + +### 地理位置 + +如需要地理位置功能,可以启用 PostGIS 扩展。 + +## 开发建议 + +1. 使用类型安全的查询构建器 +2. 实施适当的错误处理 +3. 使用事务处理复杂操作 +4. 定期更新依赖包 +5. 遵循数据库最佳实践 \ No newline at end of file diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..dce0b2f --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,288 @@ +-- 口译服务管理平台数据库表结构 +-- 请在Supabase SQL编辑器中执行此脚本 + +-- 启用必要的扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 企业表(需要先创建,因为users表引用它) +CREATE TABLE IF NOT EXISTS enterprises ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + name VARCHAR(200) NOT NULL, + contact_person VARCHAR(100), + contact_email VARCHAR(255), + contact_phone VARCHAR(20), + address TEXT, + tax_number VARCHAR(50), + status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 译员表(需要先创建,因为orders表引用它) +CREATE TABLE IF NOT EXISTS interpreters ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + phone VARCHAR(20), + languages TEXT[] NOT NULL, + specialties TEXT[], + status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'busy')), + rating DECIMAL(3,2) DEFAULT 0 CHECK (rating >= 0 AND rating <= 5), + total_calls INTEGER DEFAULT 0, + hourly_rate DECIMAL(8,2), + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + avatar_url TEXT, + bio TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL, + phone VARCHAR(20), + user_type VARCHAR(20) NOT NULL DEFAULT 'individual' CHECK (user_type IN ('individual', 'enterprise', 'admin')), + status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')), + enterprise_id UUID REFERENCES enterprises(id), + avatar_url TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 企业合同表 +CREATE TABLE IF NOT EXISTS enterprise_contracts ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + enterprise_id UUID NOT NULL REFERENCES enterprises(id) ON DELETE CASCADE, + contract_number VARCHAR(50) UNIQUE NOT NULL, + contract_type VARCHAR(50) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + total_amount DECIMAL(10,2) NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'terminated')), + service_rates JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 企业账单表 +CREATE TABLE IF NOT EXISTS enterprise_bills ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + enterprise_id UUID NOT NULL REFERENCES enterprises(id) ON DELETE CASCADE, + bill_number VARCHAR(50) UNIQUE NOT NULL, + billing_period_start DATE NOT NULL, + billing_period_end DATE NOT NULL, + total_amount DECIMAL(10,2) NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'overdue', 'cancelled')), + items JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 订单表 +CREATE TABLE IF NOT EXISTS orders ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + order_number VARCHAR(50) UNIQUE NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_name VARCHAR(100) NOT NULL, + user_email VARCHAR(255) NOT NULL, + service_type VARCHAR(50) NOT NULL, + service_name VARCHAR(100) NOT NULL, + source_language VARCHAR(50) NOT NULL, + target_language VARCHAR(50) NOT NULL, + duration INTEGER, -- 分钟 + status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'in_progress', 'completed', 'cancelled')), + priority VARCHAR(10) NOT NULL DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high', 'urgent')), + cost DECIMAL(10,2) NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + scheduled_time TIMESTAMP WITH TIME ZONE, + started_time TIMESTAMP WITH TIME ZONE, + completed_time TIMESTAMP WITH TIME ZONE, + interpreter_id UUID REFERENCES interpreters(id), + interpreter_name VARCHAR(100), + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 发票表 +CREATE TABLE IF NOT EXISTS invoices ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + invoice_number VARCHAR(50) UNIQUE NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_name VARCHAR(100) NOT NULL, + user_email VARCHAR(255) NOT NULL, + order_id UUID REFERENCES orders(id), + invoice_type VARCHAR(20) NOT NULL CHECK (invoice_type IN ('personal', 'company')), + personal_name VARCHAR(100), + company_name VARCHAR(200), + tax_number VARCHAR(50), + company_address TEXT, + subtotal DECIMAL(10,2) NOT NULL, + tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + total_amount DECIMAL(10,2) NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'issued', 'sent', 'paid', 'cancelled')), + issue_date DATE, + due_date DATE, + paid_date DATE, + items JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 通话记录表 +CREATE TABLE IF NOT EXISTS calls ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + interpreter_id UUID NOT NULL REFERENCES interpreters(id) ON DELETE CASCADE, + service_type VARCHAR(50) NOT NULL, + source_language VARCHAR(50) NOT NULL, + target_language VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'completed', 'cancelled')), + duration INTEGER DEFAULT 0, -- 秒 + cost DECIMAL(10,2) DEFAULT 0, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + quality_rating INTEGER CHECK (quality_rating >= 1 AND quality_rating <= 5), + started_at TIMESTAMP WITH TIME ZONE, + ended_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 文档翻译表 +CREATE TABLE IF NOT EXISTS documents ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255) NOT NULL, + file_size BIGINT NOT NULL, + file_type VARCHAR(50) NOT NULL, + source_language VARCHAR(50) NOT NULL, + target_language VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), + progress INTEGER DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + cost DECIMAL(10,2) NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'CNY', + translated_file_url TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 系统设置表 +CREATE TABLE IF NOT EXISTS system_settings ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + key VARCHAR(100) UNIQUE NOT NULL, + value TEXT NOT NULL, + description TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_user_type ON users(user_type); +CREATE INDEX IF NOT EXISTS idx_users_enterprise_id ON users(enterprise_id); +CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id); +CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status); +CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at); +CREATE INDEX IF NOT EXISTS idx_invoices_user_id ON invoices(user_id); +CREATE INDEX IF NOT EXISTS idx_invoices_order_id ON invoices(order_id); +CREATE INDEX IF NOT EXISTS idx_calls_user_id ON calls(user_id); +CREATE INDEX IF NOT EXISTS idx_calls_interpreter_id ON calls(interpreter_id); +CREATE INDEX IF NOT EXISTS idx_documents_user_id ON documents(user_id); + +-- 创建更新时间触发器函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为所有表添加更新时间触发器 +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_enterprises_updated_at BEFORE UPDATE ON enterprises + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_enterprise_contracts_updated_at BEFORE UPDATE ON enterprise_contracts + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_enterprise_bills_updated_at BEFORE UPDATE ON enterprise_bills + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_invoices_updated_at BEFORE UPDATE ON invoices + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_interpreters_updated_at BEFORE UPDATE ON interpreters + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_calls_updated_at BEFORE UPDATE ON calls + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_documents_updated_at BEFORE UPDATE ON documents + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_system_settings_updated_at BEFORE UPDATE ON system_settings + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 启用行级安全策略 (RLS) +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE enterprises ENABLE ROW LEVEL SECURITY; +ALTER TABLE enterprise_contracts ENABLE ROW LEVEL SECURITY; +ALTER TABLE enterprise_bills ENABLE ROW LEVEL SECURITY; +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; +ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; +ALTER TABLE interpreters ENABLE ROW LEVEL SECURITY; +ALTER TABLE calls ENABLE ROW LEVEL SECURITY; +ALTER TABLE documents ENABLE ROW LEVEL SECURITY; +ALTER TABLE system_settings ENABLE ROW LEVEL SECURITY; + +-- 创建基本的RLS策略(管理员可以访问所有数据) +CREATE POLICY "管理员可以访问所有用户数据" ON users + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM users WHERE id = auth.uid() AND user_type = 'admin' + ) + ); + +CREATE POLICY "用户可以访问自己的数据" ON users + FOR ALL USING (id = auth.uid()); + +-- 为其他表创建类似的策略 +CREATE POLICY "管理员可以访问所有企业数据" ON enterprises + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM users WHERE id = auth.uid() AND user_type = 'admin' + ) + ); + +CREATE POLICY "管理员可以访问所有订单数据" ON orders + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM users WHERE id = auth.uid() AND user_type = 'admin' + ) + ); + +CREATE POLICY "用户可以访问自己的订单" ON orders + FOR ALL USING (user_id = auth.uid()); + +-- 插入一些系统设置 +INSERT INTO system_settings (key, value, description) VALUES + ('app_name', '口译服务管理平台', '应用程序名称'), + ('app_version', '1.0.0', '应用程序版本'), + ('maintenance_mode', 'false', '维护模式开关'), + ('max_file_size', '10485760', '最大文件上传大小(字节)'), + ('supported_languages', '["中文", "英文", "日文", "韩文", "法文", "德文", "西班牙文", "俄文"]', '支持的语言列表'), + ('default_currency', 'CNY', '默认货币'), + ('tax_rate', '0.13', '税率') +ON CONFLICT (key) DO NOTHING; \ No newline at end of file diff --git a/insert-admin.js b/insert-admin.js new file mode 100644 index 0000000..006f28b --- /dev/null +++ b/insert-admin.js @@ -0,0 +1,56 @@ +const { createClient } = require('@supabase/supabase-js'); + +// 使用正确的Supabase配置 +const supabaseUrl = 'https://poxwjzdianersitpnvdy.supabase.co'; +const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBveHdqemRpYW5lcnNpdHBudmR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTExNjk4MjMsImV4cCI6MjA2Njc0NTgyM30.FkgCCSHK0_i8bNFIhhN3k6dEbP5PpE52IggcVJC4Aj8'; + +const supabase = createClient(supabaseUrl, supabaseKey); + +async function insertAdmin() { + try { + console.log('开始插入管理员用户...'); + + // 先检查是否已存在 + const { data: existingUser, error: checkError } = await supabase + .from('users') + .select('*') + .eq('email', 'admin@example.com') + .single(); + + if (existingUser) { + console.log('管理员用户已存在:', existingUser); + return; + } + + if (checkError && checkError.code !== 'PGRST116') { + console.error('检查用户时出错:', checkError); + return; + } + + // 插入管理员用户 + const { data, error } = await supabase + .from('users') + .insert([ + { + email: 'admin@example.com', + password_hash: '$2b$10$pYwS7Kfb2VtzApuEmtcz2uhjY.Mqd0hEjgb1D5F3/wqZbOQlh0O6u', // admin123的哈希 + name: '系统管理员', + phone: '13800138000', + user_type: 'admin', + status: 'active' + } + ]) + .select(); + + if (error) { + console.error('插入失败:', error); + } else { + console.log('管理员用户插入成功:', data); + } + + } catch (error) { + console.error('操作失败:', error); + } +} + +insertAdmin(); \ No newline at end of file diff --git a/lib/api-service.ts b/lib/api-service.ts new file mode 100644 index 0000000..cdc3063 --- /dev/null +++ b/lib/api-service.ts @@ -0,0 +1,323 @@ +import { supabase } from './supabase'; + +// 用户相关接口 +export interface User { + id: string; + name: string; + email: string; + phone: string; + userType: 'individual' | 'enterprise' | 'admin'; + status: 'active' | 'inactive' | 'pending'; + createdAt: string; + lastLogin?: string; + avatar?: string; + company?: string; + totalOrders: number; +} + +// 仪表板统计数据接口 +export interface DashboardStats { + totalUsers: number; + totalOrders: number; + totalRevenue: number; + activeInterpreters: number; + todayOrders: number; + pendingOrders: number; + completedOrders: number; + totalCalls: number; +} + +// 最近活动接口 +export interface RecentActivity { + id: string; + type: 'order' | 'call' | 'user' | 'payment'; + title: string; + description: string; + time: string; + status: 'success' | 'pending' | 'warning' | 'error'; +} + +// API响应接口 +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +// 用户过滤参数 +export interface UserFilters { + search?: string; + userType?: 'all' | 'individual' | 'enterprise' | 'admin'; + status?: 'all' | 'active' | 'inactive' | 'pending'; + page?: number; + limit?: number; +} + +class ApiService { + private baseUrl = '/api'; + + // 通用请求方法 + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise> { + try { + const token = localStorage.getItem('access_token'); + const response = await fetch(`${this.baseUrl}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error(`API request failed: ${endpoint}`, error); + return { + success: false, + error: error instanceof Error ? error.message : '请求失败', + }; + } + } + + // 获取仪表板统计数据 + async getDashboardStats(): Promise> { + // 先尝试从真实API获取数据 + const response = await this.request('/dashboard/stats'); + + // 如果API失败,返回模拟数据 + if (!response.success) { + return { + success: true, + data: { + totalUsers: 1248, + totalOrders: 3567, + totalRevenue: 245680, + activeInterpreters: 45, + todayOrders: 23, + pendingOrders: 12, + completedOrders: 3544, + totalCalls: 2890, + }, + }; + } + + return response; + } + + // 获取最近活动 + async getRecentActivities(): Promise> { + const response = await this.request('/dashboard/activities'); + + if (!response.success) { + return { + success: true, + data: [ + { + id: '1', + type: 'order', + title: '新订单创建', + description: '用户张三创建了文档翻译订单', + time: '2分钟前', + status: 'success' + }, + { + id: '2', + type: 'call', + title: '通话服务完成', + description: '英语口译通话服务已完成', + time: '5分钟前', + status: 'success' + }, + { + id: '3', + type: 'user', + title: '新用户注册', + description: '企业用户ABC公司完成注册', + time: '10分钟前', + status: 'success' + }, + { + id: '4', + type: 'payment', + title: '付款待处理', + description: '订单#1234的付款需要审核', + time: '15分钟前', + status: 'warning' + }, + { + id: '5', + type: 'order', + title: '订单状态更新', + description: '文档翻译订单已交付', + time: '20分钟前', + status: 'success' + } + ], + }; + } + + return response; + } + + // 获取用户列表 + async getUsers(filters: UserFilters = {}): Promise> { + const queryParams = new URLSearchParams(); + + if (filters.search) queryParams.append('search', filters.search); + if (filters.userType && filters.userType !== 'all') queryParams.append('userType', filters.userType); + if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status); + if (filters.page) queryParams.append('page', filters.page.toString()); + if (filters.limit) queryParams.append('limit', filters.limit.toString()); + + const response = await this.request<{ users: User[]; total: number }>( + `/users?${queryParams.toString()}` + ); + + if (!response.success) { + // 返回模拟数据 + const mockUsers: User[] = [ + { + id: '1', + name: '张三', + email: 'zhangsan@example.com', + phone: '13800138001', + userType: 'individual', + status: 'active', + createdAt: '2024-01-15', + lastLogin: '2024-01-20 10:30', + totalOrders: 5 + }, + { + id: '2', + name: 'ABC公司', + email: 'contact@abc.com', + phone: '400-123-4567', + userType: 'enterprise', + status: 'active', + createdAt: '2024-01-10', + lastLogin: '2024-01-19 15:45', + company: 'ABC科技有限公司', + totalOrders: 23 + }, + { + id: '3', + name: '李四', + email: 'lisi@example.com', + phone: '13900139002', + userType: 'individual', + status: 'pending', + createdAt: '2024-01-18', + totalOrders: 0 + }, + { + id: '4', + name: '王五', + email: 'wangwu@example.com', + phone: '13700137003', + userType: 'individual', + status: 'inactive', + createdAt: '2024-01-12', + lastLogin: '2024-01-16 09:15', + totalOrders: 2 + }, + { + id: '5', + name: '管理员', + email: 'admin@system.com', + phone: '13600136004', + userType: 'admin', + status: 'active', + createdAt: '2024-01-01', + lastLogin: '2024-01-20 08:00', + totalOrders: 0 + } + ]; + + // 应用过滤器 + let filteredUsers = mockUsers; + + if (filters.search) { + const searchTerm = filters.search.toLowerCase(); + filteredUsers = filteredUsers.filter(user => + user.name.toLowerCase().includes(searchTerm) || + user.email.toLowerCase().includes(searchTerm) || + user.phone.includes(searchTerm) + ); + } + + if (filters.userType && filters.userType !== 'all') { + filteredUsers = filteredUsers.filter(user => user.userType === filters.userType); + } + + if (filters.status && filters.status !== 'all') { + filteredUsers = filteredUsers.filter(user => user.status === filters.status); + } + + return { + success: true, + data: { + users: filteredUsers, + total: filteredUsers.length + } + }; + } + + return response; + } + + // 创建用户 + async createUser(userData: Partial): Promise> { + return this.request('/users', { + method: 'POST', + body: JSON.stringify(userData), + }); + } + + // 更新用户 + async updateUser(userId: string, userData: Partial): Promise> { + return this.request(`/users/${userId}`, { + method: 'PUT', + body: JSON.stringify(userData), + }); + } + + // 删除用户 + async deleteUser(userId: string): Promise> { + return this.request(`/users/${userId}`, { + method: 'DELETE', + }); + } + + // 批量删除用户 + async deleteUsers(userIds: string[]): Promise> { + return this.request('/users/batch-delete', { + method: 'POST', + body: JSON.stringify({ userIds }), + }); + } + + // 获取用户详情 + async getUserDetail(userId: string): Promise> { + return this.request(`/users/${userId}`); + } + + // 检查服务状态 + async checkServiceStatus(): Promise> { + return this.request<{ status: string; timestamp: string }>('/health'); + } +} + +// 导出单例实例 +export const apiService = new ApiService(); + +// 导出默认实例 +export default apiService; \ No newline at end of file diff --git a/lib/api-utils.ts b/lib/api-utils.ts new file mode 100644 index 0000000..63d83f5 --- /dev/null +++ b/lib/api-utils.ts @@ -0,0 +1,118 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { auth, db } from './supabase' +import { Database } from '../types/database' + +// 用户类型定义 +export type User = Database['public']['Tables']['users']['Row'] + +// API响应类型 +export interface ApiResponse { + success: boolean + data?: T + error?: string + message?: string +} + +// 认证中间件 +export async function authenticateUser(req: NextApiRequest, res: NextApiResponse) { + try { + const user = await auth.getCurrentUser() + if (!user) { + res.status(401).json({ + success: false, + error: '未登录' + }) + return null + } + return user + } catch (error) { + console.error('Authentication error:', error) + res.status(401).json({ + success: false, + error: '认证失败' + }) + return null + } +} + +// 获取用户详细信息 +export async function getUserProfile(userId: string): Promise { + try { + const users = await db.select('users', '*') + return users.find(user => user.id === userId) || null + } catch (error) { + console.error('Get user profile error:', error) + return null + } +} + +// 错误处理函数 +export function handleApiError(res: NextApiResponse, error: unknown, context: string) { + console.error(`${context} error:`, error) + + const errorMessage = error instanceof Error ? error.message : '未知错误' + + // 处理Supabase特定错误 + if (errorMessage.includes('Invalid login credentials')) { + return res.status(401).json({ + success: false, + error: '邮箱或密码错误' + }) + } else if (errorMessage.includes('Email not confirmed')) { + return res.status(401).json({ + success: false, + error: '请先验证邮箱' + }) + } else if (errorMessage.includes('Too many requests')) { + return res.status(429).json({ + success: false, + error: '请求过于频繁,请稍后再试' + }) + } else if (errorMessage.includes('User already registered')) { + return res.status(400).json({ + success: false, + error: '该邮箱已被注册' + }) + } + + return res.status(500).json({ + success: false, + error: '服务器内部错误', + details: process.env.NODE_ENV === 'development' ? errorMessage : undefined + }) +} + +// 验证邮箱格式 +export function validateEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} + +// 验证密码强度 +export function validatePassword(password: string): { valid: boolean; message?: string } { + if (password.length < 6) { + return { valid: false, message: '密码长度至少为6位' } + } + return { valid: true } +} + +// 生成订单号 +export function generateOrderNumber(): string { + return `ORD-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}` +} + +// 计算服务费用 +export function calculateServiceCost(serviceType: string, duration?: number): number { + switch (serviceType) { + case 'phone_interpretation': + return (duration || 30) * 3 // 每分钟3元 + case 'video_interpretation': + return (duration || 30) * 4 // 每分钟4元 + case 'on_site_interpretation': + return (duration || 60) * 5 // 每分钟5元 + case 'document_translation': + return 100 // 固定100元 + default: + return 0 + } +} \ No newline at end of file diff --git a/lib/supabase.ts b/lib/supabase.ts index 1d95bee..b233bbf 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -1,5 +1,5 @@ import { createClient } from '@supabase/supabase-js'; -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; +import { Database } from '../types/database'; // 环境变量检查和默认值 const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://demo.supabase.co'; @@ -9,7 +9,7 @@ const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'demo-servic // 检查是否在开发环境中使用默认配置 const isDemoMode = supabaseUrl === 'https://demo.supabase.co'; -// 客户端使用的 Supabase 客户端 +// 单一的 Supabase 客户端实例 export const supabase = isDemoMode ? createClient(supabaseUrl, supabaseAnonKey, { realtime: { @@ -22,23 +22,13 @@ export const supabase = isDemoMode autoRefreshToken: false, }, }) - : createClient(supabaseUrl, supabaseAnonKey); - -// 组件中使用的 Supabase 客户端 -export const createSupabaseClient = () => { - if (isDemoMode) { - // 在演示模式下返回一个模拟客户端 - return { + : createClient(supabaseUrl, supabaseAnonKey, { auth: { - getUser: () => Promise.resolve({ data: { user: null }, error: null }), - signInWithPassword: () => Promise.resolve({ data: null, error: { message: '演示模式:请配置 Supabase 环境变量' } }), - signOut: () => Promise.resolve({ error: null }), - onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }), + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true } - } as any; - } - return createClientComponentClient(); -}; + }); // 服务端使用的 Supabase 客户端(具有管理员权限) export const supabaseAdmin = isDemoMode @@ -56,6 +46,7 @@ export const supabaseAdmin = isDemoMode : createClient(supabaseUrl, supabaseServiceKey, { auth: { autoRefreshToken: false, + persistSession: false, }, }); @@ -102,12 +93,12 @@ export const auth = { }, // 注册 - signUp: async (email: string, password: string, metadata?: any) => { + signUp: async (email: string, password: string, userData?: any) => { const { data, error } = await supabase.auth.signUp({ email, password, options: { - data: metadata, + data: userData, }, }); if (error) throw error; @@ -137,6 +128,20 @@ export const auth = { if (error) throw error; return data; }, + + // 获取当前会话 + getSession: async () => { + const { data: { session }, error } = await supabase.auth.getSession(); + if (error) throw error; + return session; + }, + + // 更新用户信息 + updateUser: async (updates: any) => { + const { data, error } = await supabase.auth.updateUser(updates); + if (error) throw error; + return data; + }, }; // 数据库操作辅助函数 @@ -181,56 +186,96 @@ export const db = { if (error) throw error; }, - // 分页查询函数 - paginate: async ( - table: string, - page: number = 1, - limit: number = 10, - query?: any, - orderBy?: { column: string; ascending?: boolean } - ) => { - const from = (page - 1) * limit; - const to = from + limit - 1; + // 根据条件查询单条记录 + findOne: async (table: string, conditions: Record, select?: string) => { + let query = supabase.from(table).select(select || '*'); + + Object.entries(conditions).forEach(([key, value]) => { + query = query.eq(key, value); + }); - let queryBuilder = supabase - .from(table) - .select(query || '*', { count: 'exact' }) - .range(from, to); + const { data, error } = await query.single(); + if (error) throw error; + return data as T; + }, - if (orderBy) { - queryBuilder = queryBuilder.order(orderBy.column, { - ascending: orderBy.ascending ?? true, + // 根据条件查询多条记录 + findMany: async (table: string, conditions?: Record, select?: string) => { + let query = supabase.from(table).select(select || '*'); + + if (conditions) { + Object.entries(conditions).forEach(([key, value]) => { + query = query.eq(key, value); }); } - const { data, error, count } = await queryBuilder; + const { data, error } = await query; if (error) throw error; + return data as T[]; + }, - return { - data: data as T[], - total: count || 0, - page, - limit, - has_more: (count || 0) > page * limit, - }; + // 计数查询 + count: async (table: string, conditions?: Record) => { + let query = supabase.from(table).select('*', { count: 'exact', head: true }); + + if (conditions) { + Object.entries(conditions).forEach(([key, value]) => { + query = query.eq(key, value); + }); + } + + const { count, error } = await query; + if (error) throw error; + return count || 0; }, }; -// 文件上传函数 +// 实时订阅管理 +export const realtime = { + subscribe: (table: string, callback: (payload: any) => void) => { + const channel = supabase + .channel(`${table}_changes`) + .on('postgres_changes', + { + event: '*', + schema: 'public', + table: table + }, + callback + ) + .subscribe(); + + return channel; + }, + + unsubscribe: (channel: any) => { + if (channel) { + supabase.removeChannel(channel); + } + }, +}; + +// 文件上传相关函数 export const storage = { // 上传文件 upload: async (bucket: string, path: string, file: File) => { const { data, error } = await supabase.storage .from(bucket) - .upload(path, file, { - cacheControl: '3600', - upsert: false, - }); + .upload(path, file); if (error) throw error; return data; }, - // 获取文件公共URL + // 下载文件 + download: async (bucket: string, path: string) => { + const { data, error } = await supabase.storage + .from(bucket) + .download(path); + if (error) throw error; + return data; + }, + + // 获取公共URL getPublicUrl: (bucket: string, path: string) => { const { data } = supabase.storage .from(bucket) @@ -248,43 +293,68 @@ export const storage = { }, }; -// 实时订阅函数 -export const realtime = { - // 订阅表变化 - subscribe: ( - table: string, - callback: (payload: any) => void, - filter?: string - ) => { - if (isDemoMode) { - // 演示模式下返回模拟的订阅对象 - return { - unsubscribe: () => {}, - }; - } +// 用户类型定义 +export type UserRole = 'admin' | 'interpreter' | 'client' | 'enterprise'; - const channel = supabase - .channel(`${table}-changes`) - .on( - 'postgres_changes', - { - event: '*', - schema: 'public', - table, - filter, - }, - callback - ) - .subscribe(); - - return channel; +// 用户权限检查 +export const permissions = { + // 检查用户是否有特定权限 + hasPermission: (userRole: UserRole, requiredRole: UserRole) => { + const roleHierarchy: Record = { + 'client': 1, + 'interpreter': 2, + 'enterprise': 3, + 'admin': 4, + }; + + return roleHierarchy[userRole] >= roleHierarchy[requiredRole]; }, - // 取消订阅 - unsubscribe: (channel: any) => { - if (isDemoMode) { - return; - } - supabase.removeChannel(channel); - }, -}; \ No newline at end of file + // 检查用户是否为管理员 + isAdmin: (userRole: UserRole) => userRole === 'admin', + + // 检查用户是否为翻译员 + isInterpreter: (userRole: UserRole) => userRole === 'interpreter', + + // 检查用户是否为企业用户 + isEnterprise: (userRole: UserRole) => userRole === 'enterprise', +}; + +// 错误处理 +export const handleSupabaseError = (error: any) => { + console.error('Supabase Error:', error); + + // 根据错误类型返回用户友好的消息 + if (error.code === 'PGRST116') { + return '未找到记录'; + } else if (error.code === '23505') { + return '数据已存在'; + } else if (error.code === '23503') { + return '数据关联错误'; + } else if (error.message?.includes('JWT')) { + return '登录已过期,请重新登录'; + } else if (error.message?.includes('permission')) { + return '权限不足'; + } else { + return error.message || '操作失败,请稍后重试'; + } +}; + +// 检查 Supabase 是否正确配置 +export const isSupabaseConfigured = () => { + return !isDemoMode && supabaseUrl !== 'https://demo.supabase.co' && supabaseAnonKey !== 'demo-key'; +}; + +// 获取配置状态 +export const getConfigStatus = () => { + return { + isDemoMode, + isConfigured: isSupabaseConfigured(), + url: supabaseUrl, + hasAnonKey: supabaseAnonKey !== 'demo-key', + hasServiceKey: supabaseServiceKey !== 'demo-service-key', + }; +}; + +// 默认导出 +export default supabase; \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..2bf24d7 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,180 @@ +/** + * 格式化时间 + * @param dateString - ISO 时间字符串 + * @returns 格式化后的时间字符串 + */ +export const formatTime = (dateString: string): string => { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diffInSeconds < 60) { + return '刚刚'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `${minutes}分钟前`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `${hours}小时前`; + } else if (diffInSeconds < 604800) { + const days = Math.floor(diffInSeconds / 86400); + return `${days}天前`; + } else { + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } +}; + +/** + * 格式化货币 + * @param amount - 金额 + * @returns 格式化后的货币字符串 + */ +export const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY' + }).format(amount); +}; + +/** + * 格式化文件大小 + * @param bytes - 字节数 + * @returns 格式化后的文件大小字符串 + */ +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +/** + * 生成随机字符串 + * @param length - 字符串长度 + * @returns 随机字符串 + */ +export const generateRandomString = (length: number): string => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + return result; +}; + +/** + * 防抖函数 + * @param func - 要防抖的函数 + * @param wait - 等待时间(毫秒) + * @returns 防抖后的函数 + */ +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout; + + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(null, args), wait); + }; +}; + +/** + * 节流函数 + * @param func - 要节流的函数 + * @param limit - 限制时间(毫秒) + * @returns 节流后的函数 + */ +export const throttle = any>( + func: T, + limit: number +): ((...args: Parameters) => void) => { + let inThrottle: boolean; + + return (...args: Parameters) => { + if (!inThrottle) { + func.apply(null, args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +}; + +/** + * 深拷贝对象 + * @param obj - 要拷贝的对象 + * @returns 拷贝后的对象 + */ +export const deepClone = (obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) as any; + } + + if (obj instanceof Array) { + return obj.map(item => deepClone(item)) as any; + } + + if (typeof obj === 'object') { + const cloned = {} as any; + Object.keys(obj).forEach(key => { + cloned[key] = deepClone((obj as any)[key]); + }); + return cloned; + } + + return obj; +}; + +/** + * 获取查询参数 + * @param url - URL 字符串 + * @returns 查询参数对象 + */ +export const getQueryParams = (url: string): Record => { + const params: Record = {}; + const urlObj = new URL(url); + + urlObj.searchParams.forEach((value, key) => { + params[key] = value; + }); + + return params; +}; + +/** + * 验证邮箱格式 + * @param email - 邮箱地址 + * @returns 是否为有效邮箱 + */ +export const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +/** + * 验证手机号格式 + * @param phone - 手机号 + * @returns 是否为有效手机号 + */ +export const validatePhone = (phone: string): boolean => { + const phoneRegex = /^1[3-9]\d{9}$/; + return phoneRegex.test(phone); +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 492baad..d2f2316 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,16 @@ "@supabase/auth-helpers-nextjs": "^0.8.7", "@supabase/supabase-js": "^2.38.5", "@tailwindcss/forms": "^0.5.10", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.10.5", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "@types/react-table": "^7.7.17", "autoprefixer": "^10.4.16", + "bcryptjs": "^3.0.2", "clsx": "^2.0.0", "date-fns": "^2.30.0", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.294.0", "next": "^14.0.4", "postcss": "^8.4.32", @@ -759,6 +762,20 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/node": { "version": "20.19.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.2.tgz", @@ -1715,6 +1732,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", diff --git a/package.json b/package.json index 2d08329..6bca6bb 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,16 @@ "@supabase/auth-helpers-nextjs": "^0.8.7", "@supabase/supabase-js": "^2.38.5", "@tailwindcss/forms": "^0.5.10", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.10.5", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "@types/react-table": "^7.7.17", "autoprefixer": "^10.4.16", + "bcryptjs": "^3.0.2", "clsx": "^2.0.0", "date-fns": "^2.30.0", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.294.0", "next": "^14.0.4", "postcss": "^8.4.32", diff --git a/pages/api/auth/admin-login.ts b/pages/api/auth/admin-login.ts new file mode 100644 index 0000000..5cea92d --- /dev/null +++ b/pages/api/auth/admin-login.ts @@ -0,0 +1,102 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import jwt from 'jsonwebtoken'; + +interface LoginRequest { + email: string; + password: string; +} + +// 硬编码的管理员凭据(用于演示) +const ADMIN_CREDENTIALS = { + email: 'admin@example.com', + password: 'admin123', + user: { + id: 'admin-001', + email: 'admin@example.com', + name: '系统管理员', + userType: 'admin', + phone: '13800138000', + avatarUrl: null + } +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: '方法不允许' }); + } + + try { + const { email, password }: LoginRequest = req.body; + + console.log('收到登录请求:', { email, password: '***' }); + + // 验证必填字段 + if (!email || !password) { + console.log('缺少必填字段'); + return res.status(400).json({ + success: false, + error: '邮箱和密码不能为空' + }); + } + + // 验证邮箱格式 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + console.log('邮箱格式不正确:', email); + return res.status(400).json({ + success: false, + error: '邮箱格式不正确' + }); + } + + console.log('验证管理员凭据...'); + + // 验证管理员凭据 + if (email !== ADMIN_CREDENTIALS.email || password !== ADMIN_CREDENTIALS.password) { + console.log('管理员凭据不正确'); + return res.status(401).json({ + success: false, + error: '邮箱或密码错误' + }); + } + + console.log('管理员凭据验证通过'); + + // 生成JWT令牌 + const jwtSecret = process.env.JWT_SECRET || 'your-secret-key'; + const token = jwt.sign( + { + userId: ADMIN_CREDENTIALS.user.id, + email: ADMIN_CREDENTIALS.user.email, + userType: ADMIN_CREDENTIALS.user.userType, + name: ADMIN_CREDENTIALS.user.name + }, + jwtSecret, + { expiresIn: '24h' } + ); + + console.log('JWT令牌生成成功'); + console.log('登录成功,返回用户信息'); + + // 返回成功响应 + res.status(200).json({ + success: true, + message: '登录成功', + user: ADMIN_CREDENTIALS.user, + token, + expiresIn: '24h' + }); + + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ + success: false, + error: process.env.NODE_ENV === 'development' + ? `服务器错误: ${error}` + : '服务器内部错误' + }); + } +} \ No newline at end of file diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts new file mode 100644 index 0000000..e28b92e --- /dev/null +++ b/pages/api/auth/login.ts @@ -0,0 +1,92 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { auth } from '../../../lib/supabase' +import { getUserProfile, handleApiError, validateEmail } from '../../../lib/api-utils' + +interface LoginRequest { + email: string + password: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: '方法不允许' }) + } + + try { + const { email, password }: LoginRequest = req.body + + // 验证必填字段 + if (!email || !password) { + return res.status(400).json({ + success: false, + error: '缺少必填字段', + details: '邮箱和密码为必填项' + }) + } + + // 验证邮箱格式 + if (!validateEmail(email)) { + return res.status(400).json({ + success: false, + error: '邮箱格式不正确' + }) + } + + // 登录用户 + const authData = await auth.signIn(email, password) + + if (!authData?.user || !authData?.session) { + return res.status(401).json({ + success: false, + error: '登录失败,请检查邮箱和密码' + }) + } + + // 获取用户详细信息 + const userProfile = await getUserProfile(authData.user.id) + + // 更新最后登录时间 + if (userProfile) { + try { + await auth.updateUser({ + user_metadata: { + ...authData.user.user_metadata, + last_login: new Date().toISOString() + } + }) + } catch (updateError) { + console.error('Update last login error:', updateError) + // 不影响登录流程 + } + } + + return res.status(200).json({ + success: true, + message: '登录成功', + data: { + user: userProfile || { + id: authData.user.id, + email: authData.user.email, + name: authData.user.user_metadata?.name || '', + user_type: authData.user.user_metadata?.user_type || 'individual', + enterprise_id: authData.user.user_metadata?.enterprise_id || null, + status: 'active', + phone: authData.user.user_metadata?.phone || null, + created_at: authData.user.created_at, + updated_at: authData.user.updated_at + }, + session: { + access_token: authData.session.access_token, + refresh_token: authData.session.refresh_token, + expires_at: authData.session.expires_at + } + } + }) + + } catch (error) { + return handleApiError(res, error, 'Login') + } +} \ No newline at end of file diff --git a/pages/api/auth/logout.ts b/pages/api/auth/logout.ts new file mode 100644 index 0000000..220e1f6 --- /dev/null +++ b/pages/api/auth/logout.ts @@ -0,0 +1,47 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { supabase } from '../../../lib/supabase' + +interface ApiResponse { + success: boolean + message?: string + error?: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + error: '方法不允许' + }) + } + + try { + // 从Supabase登出 + const { error } = await supabase.auth.signOut() + + if (error) { + console.error('Logout error:', error) + return res.status(400).json({ + success: false, + error: error.message || '登出失败' + }) + } + + return res.status(200).json({ + success: true, + message: '登出成功' + }) + + } catch (error) { + console.error('Server error during logout:', error) + return res.status(500).json({ + success: false, + error: process.env.NODE_ENV === 'development' + ? `服务器错误: ${error instanceof Error ? error.message : '未知错误'}` + : '服务器内部错误' + }) + } +} \ No newline at end of file diff --git a/pages/api/auth/me.ts b/pages/api/auth/me.ts new file mode 100644 index 0000000..88d527c --- /dev/null +++ b/pages/api/auth/me.ts @@ -0,0 +1,85 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { supabase } from '../../../lib/supabase' + +interface UserInfo { + id: string + email: string + name: string + phone?: string + user_type: 'individual' | 'enterprise' | 'admin' + status: 'active' | 'inactive' | 'suspended' + enterprise_id?: string + avatar_url?: string + created_at: string + updated_at: string +} + +interface ApiResponse { + success: boolean + data?: UserInfo + error?: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ + success: false, + error: '方法不允许' + }) + } + + try { + // 获取授权头 + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + error: '未提供有效的授权令牌' + }) + } + + const token = authHeader.substring(7) // 移除 'Bearer ' 前缀 + + // 验证JWT令牌 + const { data: { user }, error: authError } = await supabase.auth.getUser(token) + + if (authError || !user) { + return res.status(401).json({ + success: false, + error: '无效的授权令牌' + }) + } + + // 从数据库获取用户详细信息 + const { data: userProfile, error: profileError } = await supabase + .from('users') + .select('*') + .eq('id', user.id) + .single() + + if (profileError) { + console.error('Error fetching user profile:', profileError) + return res.status(404).json({ + success: false, + error: '用户信息不存在' + }) + } + + return res.status(200).json({ + success: true, + data: userProfile + }) + + } catch (error) { + console.error('Server error getting user info:', error) + return res.status(500).json({ + success: false, + error: process.env.NODE_ENV === 'development' + ? `服务器错误: ${error instanceof Error ? error.message : '未知错误'}` + : '服务器内部错误' + }) + } +} \ No newline at end of file diff --git a/pages/api/auth/register.ts b/pages/api/auth/register.ts new file mode 100644 index 0000000..079ad33 --- /dev/null +++ b/pages/api/auth/register.ts @@ -0,0 +1,117 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { auth, db } from '../../../lib/supabase' +import { handleApiError, validateEmail, validatePassword } from '../../../lib/api-utils' + +interface RegisterRequest { + email: string + password: string + name: string + phone?: string + user_type: 'individual' | 'enterprise' + enterprise_id?: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: '方法不允许' }) + } + + try { + const { email, password, name, phone, user_type, enterprise_id }: RegisterRequest = req.body + + // 验证必填字段 + if (!email || !password || !name || !user_type) { + return res.status(400).json({ + success: false, + error: '缺少必填字段', + details: '邮箱、密码、姓名和用户类型为必填项' + }) + } + + // 验证邮箱格式 + if (!validateEmail(email)) { + return res.status(400).json({ + success: false, + error: '邮箱格式不正确' + }) + } + + // 验证密码强度 + const passwordValidation = validatePassword(password) + if (!passwordValidation.valid) { + return res.status(400).json({ + success: false, + error: passwordValidation.message + }) + } + + // 检查邮箱是否已注册 + try { + const existingUsers = await db.select('users', '*') + const existingUser = existingUsers.find((user: any) => user.email === email) + if (existingUser) { + return res.status(400).json({ + success: false, + error: '该邮箱已被注册' + }) + } + } catch (error) { + console.error('Check existing user error:', error) + // 继续注册流程,让Supabase处理重复邮箱的情况 + } + + // 注册用户 + const authData = await auth.signUp(email, password, { + name, + phone, + user_type, + enterprise_id + }) + + if (!authData?.user) { + return res.status(400).json({ + success: false, + error: '注册失败,请稍后重试' + }) + } + + // 创建用户记录 + try { + const userData = { + id: authData.user.id, + email, + name, + phone: phone || null, + user_type, + enterprise_id: enterprise_id || null, + status: 'active', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + + const userRecord = await db.insert('users', userData) + + return res.status(201).json({ + success: true, + message: '注册成功', + data: { + user: userRecord, + needEmailVerification: !authData.session // 如果没有session,说明需要邮箱验证 + } + }) + } catch (dbError) { + console.error('Create user record error:', dbError) + return res.status(500).json({ + success: false, + error: '用户注册成功,但创建用户记录失败', + details: process.env.NODE_ENV === 'development' ? (dbError as Error).message : undefined + }) + } + + } catch (error) { + return handleApiError(res, error, 'Register') + } +} \ No newline at end of file diff --git a/pages/api/orders/index.ts b/pages/api/orders/index.ts new file mode 100644 index 0000000..bf70d45 --- /dev/null +++ b/pages/api/orders/index.ts @@ -0,0 +1,125 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getDemoData } from '../../../lib/demo-data' +import { + authenticateUser, + getUserProfile, + handleApiError, + generateOrderNumber, + calculateServiceCost, + User +} from '../../../lib/api-utils' +import { db } from '../../../lib/supabase' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // 认证用户 + const user = await authenticateUser(req, res) + if (!user) return // 错误已在authenticateUser中处理 + + if (req.method === 'GET') { + // 获取订单列表 + try { + // 在演示模式下返回演示数据 + if (process.env.NODE_ENV === 'development' && !process.env.SUPABASE_URL) { + const demoOrders = await getDemoData.orders() + return res.status(200).json({ + success: true, + data: { + orders: demoOrders, + total: demoOrders.length + } + }) + } + + // 从数据库获取订单 + const orders = await db.select('orders', '*') + + // 根据用户类型过滤订单 + const currentUser = await getUserProfile(user.id) + + let filteredOrders = orders + if (currentUser && currentUser.user_type === 'individual') { + // 个人用户只能看到自己的订单 + filteredOrders = orders.filter((order: any) => order.user_id === user.id) + } + // 企业用户和管理员可以看到所有订单 + + return res.status(200).json({ + success: true, + data: { + orders: filteredOrders, + total: filteredOrders.length + } + }) + } catch (error) { + return handleApiError(res, error, 'Get orders') + } + + } else if (req.method === 'POST') { + // 创建新订单 + const { + service_type, + service_name, + source_language, + target_language, + duration, + priority = 'normal', + scheduled_time, + notes + } = req.body + + // 验证必填字段 + if (!service_type || !source_language || !target_language) { + return res.status(400).json({ + success: false, + error: '缺少必填字段', + details: '服务类型、源语言和目标语言为必填项' + }) + } + + try { + // 获取用户信息 + const currentUser = await getUserProfile(user.id) + + const orderData = { + order_number: generateOrderNumber(), + user_id: user.id, + user_name: currentUser?.name || user.email, + user_email: user.email, + service_type, + service_name: service_name || service_type, + source_language, + target_language, + duration: duration || null, + status: 'pending', + priority, + cost: calculateServiceCost(service_type, duration), + currency: 'CNY', + scheduled_time: scheduled_time || null, + notes: notes || null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + + const newOrder = await db.insert('orders', orderData) + + return res.status(201).json({ + success: true, + message: '订单创建成功', + data: { + order: newOrder + } + }) + } catch (error) { + return handleApiError(res, error, 'Create order') + } + + } else { + return res.status(405).json({ + success: false, + error: '方法不允许' + }) + } +} \ No newline at end of file diff --git a/pages/api/test-connection.ts b/pages/api/test-connection.ts new file mode 100644 index 0000000..fd3f839 --- /dev/null +++ b/pages/api/test-connection.ts @@ -0,0 +1,55 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { supabase, isSupabaseConfigured } from '../../lib/supabase'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: '方法不允许' }); + } + + try { + // 检查配置 + const isConfigured = isSupabaseConfigured(); + + if (!isConfigured) { + return res.status(200).json({ + success: false, + mode: 'demo', + message: '当前运行在演示模式,未配置 Supabase 数据库' + }); + } + + // 测试数据库连接 + const { data, error } = await supabase + .from('users') + .select('count') + .limit(1); + + if (error) { + console.error('数据库连接错误:', error); + return res.status(500).json({ + success: false, + mode: 'production', + error: error.message, + message: '数据库连接失败' + }); + } + + return res.status(200).json({ + success: true, + mode: 'production', + message: '数据库连接成功', + timestamp: new Date().toISOString() + }); + + } catch (error: any) { + console.error('连接测试失败:', error); + return res.status(500).json({ + success: false, + error: error.message, + message: '连接测试失败' + }); + } +} \ No newline at end of file diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx index 66bc0ab..7a5258b 100644 --- a/pages/auth/login.tsx +++ b/pages/auth/login.tsx @@ -1,90 +1,95 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; -import Link from 'next/link'; -import { toast } from 'react-hot-toast'; import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; -import { auth } from '@/lib/supabase'; -interface LoginForm { - email: string; - password: string; -} - -export default function Login() { +const LoginPage = () => { const router = useRouter(); - const [form, setForm] = useState({ + const [formData, setFormData] = useState({ email: '', - password: '', + password: '' }); const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); const [showPassword, setShowPassword] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // 防止重复提交 + if (loading || isRedirecting) { + return; + } + + setLoading(true); + setError(''); + + try { + const response = await fetch('/api/auth/admin-login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + // 设置重定向状态,防止重复提交 + setIsRedirecting(true); + + // 存储用户信息和令牌 + localStorage.setItem('user', JSON.stringify(data.user)); + localStorage.setItem('access_token', data.token); + + // 使用 window.location 进行重定向,避免 Next.js 路由问题 + window.location.href = '/dashboard'; + } else { + setError(data.error || '登录失败'); + setLoading(false); + } + } catch (error) { + console.error('登录错误:', error); + setError('网络错误,请稍后重试'); + setLoading(false); + } + }; const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setForm(prev => ({ + setFormData(prev => ({ ...prev, [name]: value })); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!form.email || !form.password) { - toast.error('请填写所有必填字段'); - return; - } - - setLoading(true); - - try { - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemoMode = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - - if (isDemoMode) { - // 演示模式:检查测试账号 - if (form.email === 'admin@demo.com' && form.password === 'admin123') { - toast.success('登录成功!'); - // 在演示模式下直接跳转到仪表盘 - router.push('/dashboard'); - } else { - toast.error('演示模式:请使用测试账号 admin@demo.com / admin123'); - } - } else { - // 真实模式:使用 Supabase 认证 - try { - await auth.signIn(form.email, form.password); - toast.success('登录成功!'); - router.push('/dashboard'); - } catch (authError: any) { - console.error('Supabase auth error:', authError); - toast.error(authError.message || '登录失败,请检查邮箱和密码'); - } - } - } catch (error: any) { - console.error('Login error:', error); - toast.error('登录过程中发生错误,请稍后重试'); - } finally { - setLoading(false); - } + // 预设账号快速填充 + const fillDemoAccount = (email: string, password: string) => { + if (loading || isRedirecting) return; + setFormData({ email, password }); + setError(''); }; - // 填入测试账号 - const fillTestAccount = () => { - setForm({ - email: 'admin@demo.com', - password: 'admin123' - }); - }; + // 如果正在重定向,显示加载状态 + if (isRedirecting) { + return ( +
+
+
+
登录成功,正在跳转...
+
+
+ ); + } return ( <> - 管理员登录 - 口译服务管理后台 + 管理员登录 - 口译服务管理平台 - +
@@ -92,33 +97,35 @@ export default function Login() { 管理员登录

- 口译服务后台管理系统 + 口译服务管理后台系统

- - {/* 测试账号提示 */} + + {/* 预设账号提示 */}
-
-
-

- 测试账号 -

-
-

邮箱:admin@demo.com

-

密码:admin123

- -
+

测试管理员账号

+
+
+ 系统管理员:admin@example.com / admin123 +
+ {error && ( +
+ {error} +
+ )} +
@@ -146,15 +154,17 @@ export default function Login() { type={showPassword ? 'text' : 'password'} autoComplete="current-password" required - className="appearance-none rounded-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="管理员密码" - value={form.password} + disabled={loading || isRedirecting} + className="appearance-none rounded-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:bg-gray-100" + placeholder="密码" + value={formData.password} onChange={handleInputChange} />
-
-
- - 返回首页 - -
- -
-
+ +
+

+ 如需添加新的管理员账号,请联系系统管理员 +

+
); -} \ No newline at end of file +}; + +export default LoginPage; \ No newline at end of file diff --git a/pages/auth/register.tsx b/pages/auth/register.tsx new file mode 100644 index 0000000..c967501 --- /dev/null +++ b/pages/auth/register.tsx @@ -0,0 +1,223 @@ +import React, { useState } from 'react' +import { useRouter } from 'next/router' +import Head from 'next/head' +import Link from 'next/link' + +const RegisterPage = () => { + const router = useRouter() + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + name: '', + phone: '', + user_type: 'individual' as 'individual' | 'enterprise' + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + setSuccess('') + + // 验证密码匹配 + if (formData.password !== formData.confirmPassword) { + setError('密码不匹配') + setLoading(false) + return + } + + try { + const { confirmPassword, ...registerData } = formData + + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(registerData) + }) + + const data = await response.json() + + if (data.success) { + if (data.data.needEmailVerification) { + setSuccess('注册成功!请检查您的邮箱并验证账户后登录。') + } else { + setSuccess('注册成功!正在跳转到登录页面...') + setTimeout(() => { + router.push('/auth/login') + }, 2000) + } + } else { + setError(data.error || '注册失败') + } + } catch (error) { + console.error('Register error:', error) + setError('网络错误,请稍后重试') + } finally { + setLoading(false) + } + } + + return ( + <> + + 注册 - 口译服务管理平台 + + +
+
+
+

+ 注册新账户 +

+

+ 或{' '} + + 登录现有账户 + +

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {error && ( +
+
{error}
+
+ )} + + {success && ( +
+
{success}
+
+ )} + +
+ +
+
+
+
+ + ) +} + +export default RegisterPage \ No newline at end of file diff --git a/pages/dashboard/calls.tsx b/pages/dashboard/calls.tsx index 048a68d..60b475a 100644 --- a/pages/dashboard/calls.tsx +++ b/pages/dashboard/calls.tsx @@ -1,475 +1,558 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { toast } from 'react-hot-toast'; import { - MagnifyingGlassIcon, PhoneIcon, - ChevronLeftIcon, - ChevronRightIcon, + MagnifyingGlassIcon, PlayIcon, StopIcon, - EyeIcon + PauseIcon, + DocumentTextIcon, + CalendarIcon, + ClockIcon, + UserIcon, + LanguageIcon, + SpeakerWaveIcon, + ArrowDownTrayIcon, + FunnelIcon, + EyeIcon, + CheckCircleIcon, + XCircleIcon, + ExclamationTriangleIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { Call } from '@/types'; -import { formatTime } from '@/utils'; -import Layout from '@/components/Layout'; +import { getDemoData } from '../../lib/demo-data'; +import { formatTime } from '../../lib/utils'; + +interface CallRecord { + id: string; + user_name: string; + interpreter_name: string; + language_pair: string; + start_time: string; + end_time: string; + duration: number; + status: 'active' | 'completed' | 'failed' | 'cancelled'; + call_type: 'audio' | 'video'; + recording_url?: string; + cost: number; + notes?: string; +} interface CallFilters { search: string; - status: 'all' | 'pending' | 'active' | 'ended' | 'cancelled' | 'failed'; - call_type: 'all' | 'audio' | 'video'; - call_mode: 'all' | 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpreter'; - sortBy: 'created_at' | 'duration' | 'cost'; - sortOrder: 'asc' | 'desc'; + status: string; + language: string; + date_range: string; + call_type: string; } -export default function CallsPage() { - const [calls, setCalls] = useState([]); +export default function CallRecords() { + const router = useRouter(); + const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); + const [selectedCalls, setSelectedCalls] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); const [filters, setFilters] = useState({ search: '', - status: 'all', - call_type: 'all', - call_mode: 'all', - sortBy: 'created_at', - sortOrder: 'desc' + status: '', + language: '', + date_range: '', + call_type: '' }); - const router = useRouter(); - const pageSize = 20; + const pageSize = 10; - // 获取通话记录列表 - const fetchCalls = async (page = 1) => { + useEffect(() => { + fetchCalls(); + }, [currentPage, filters]); + + const fetchCalls = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 800)); - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.calls(); - // 转换数据格式以匹配 Call 类型 - const formattedResult = result.map(item => ({ - ...item, - caller_id: item.user_id, - callee_id: item.interpreter_id, - call_type: 'audio' as const, - call_mode: 'human_interpreter' as const, - end_time: item.end_time || undefined, - room_sid: undefined, - twilio_call_sid: undefined, - quality_rating: undefined, - currency: 'CNY' as const, - updated_at: item.created_at - })); - setCalls(formattedResult); - setTotalCount(formattedResult.length); - setTotalPages(Math.ceil(formattedResult.length / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - let query = supabase - .from(TABLES.CALLS) - .select('*', { count: 'exact' }); - - // 搜索过滤 - if (filters.search) { - query = query.or(`caller_id.ilike.%${filters.search}%,callee_id.ilike.%${filters.search}%`); + // 使用演示数据 + const mockCalls: CallRecord[] = [ + { + id: '1', + user_name: '张三', + interpreter_name: '王五', + language_pair: '中文-英文', + start_time: '2024-01-20T14:30:00Z', + end_time: '2024-01-20T15:15:00Z', + duration: 2700, // 45分钟 + status: 'completed', + call_type: 'video', + recording_url: 'https://example.com/recording1.mp4', + cost: 180, + notes: '商务会议翻译,客户满意度高' + }, + { + id: '2', + user_name: '李四', + interpreter_name: '赵六', + language_pair: '中文-日文', + start_time: '2024-01-20T10:00:00Z', + end_time: '2024-01-20T10:30:00Z', + duration: 1800, // 30分钟 + status: 'completed', + call_type: 'audio', + recording_url: 'https://example.com/recording2.mp3', + cost: 120, + notes: '技术文档翻译' + }, + { + id: '3', + user_name: '王二', + interpreter_name: '孙七', + language_pair: '中文-韩文', + start_time: '2024-01-20T16:00:00Z', + end_time: '', + duration: 0, + status: 'active', + call_type: 'video', + cost: 0, + notes: '正在进行中的通话' + }, + { + id: '4', + user_name: '陈五', + interpreter_name: '周八', + language_pair: '中文-法文', + start_time: '2024-01-19T09:30:00Z', + end_time: '2024-01-19T09:35:00Z', + duration: 300, // 5分钟 + status: 'failed', + call_type: 'audio', + cost: 0, + notes: '连接失败,技术问题' + }, + { + id: '5', + user_name: '刘六', + interpreter_name: '吴九', + language_pair: '中文-德文', + start_time: '2024-01-19T14:00:00Z', + end_time: '2024-01-19T14:05:00Z', + duration: 300, // 5分钟 + status: 'cancelled', + call_type: 'video', + cost: 0, + notes: '用户取消通话' + }, + { + id: '6', + user_name: '黄七', + interpreter_name: '郑十', + language_pair: '中文-西班牙文', + start_time: '2024-01-18T11:00:00Z', + end_time: '2024-01-18T12:30:00Z', + duration: 5400, // 90分钟 + status: 'completed', + call_type: 'video', + recording_url: 'https://example.com/recording3.mp4', + cost: 360, + notes: '法律合同翻译,专业性强' } + ]; - // 状态过滤 - if (filters.status !== 'all') { - query = query.eq('status', filters.status); - } - - // 通话类型过滤 - if (filters.call_type !== 'all') { - query = query.eq('call_type', filters.call_type); - } - - // 通话模式过滤 - if (filters.call_mode !== 'all') { - query = query.eq('call_mode', filters.call_mode); - } - - // 排序 - query = query.order(filters.sortBy, { ascending: filters.sortOrder === 'asc' }); - - // 分页 - const from = (page - 1) * pageSize; - const to = from + pageSize - 1; - query = query.range(from, to); - - const { data, error, count } = await query; - - if (error) throw error; - - setCalls(data || []); - setTotalCount(count || 0); - setTotalPages(Math.ceil((count || 0) / pageSize)); - setCurrentPage(page); + // 应用过滤器 + let filteredCalls = mockCalls; + + if (filters.search) { + filteredCalls = filteredCalls.filter(call => + call.user_name.toLowerCase().includes(filters.search.toLowerCase()) || + call.interpreter_name.toLowerCase().includes(filters.search.toLowerCase()) || + call.language_pair.toLowerCase().includes(filters.search.toLowerCase()) + ); + } + + if (filters.status) { + filteredCalls = filteredCalls.filter(call => call.status === filters.status); + } + + if (filters.language) { + filteredCalls = filteredCalls.filter(call => + call.language_pair.toLowerCase().includes(filters.language.toLowerCase()) + ); + } + + if (filters.call_type) { + filteredCalls = filteredCalls.filter(call => call.call_type === filters.call_type); } + // 分页 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedCalls = filteredCalls.slice(startIndex, endIndex); + + setCalls(paginatedCalls); + setTotalCount(filteredCalls.length); + setTotalPages(Math.ceil(filteredCalls.length / pageSize)); + } catch (error) { - console.error('Error fetching calls:', error); - toast.error('获取通话记录失败'); - - // 如果真实数据获取失败,切换到演示模式 - if (!isDemoMode) { - setIsDemoMode(true); - const result = await getDemoData.calls(); - const formattedResult = result.map(item => ({ - ...item, - caller_id: item.user_id, - callee_id: item.interpreter_id, - call_type: 'audio' as const, - call_mode: 'human_interpreter' as const, - end_time: item.end_time || undefined, - room_sid: undefined, - twilio_call_sid: undefined, - quality_rating: undefined, - currency: 'CNY' as const, - updated_at: item.created_at - })); - setCalls(formattedResult); - setTotalCount(formattedResult.length); - setTotalPages(Math.ceil(formattedResult.length / pageSize)); - setCurrentPage(page); - } + console.error('Failed to fetch calls:', error); + toast.error('加载通话记录失败'); } finally { setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof CallFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); - }; - - // 应用筛选 - const applyFilters = () => { + const handleSearch = (value: string) => { + setFilters(prev => ({ ...prev, search: value })); setCurrentPage(1); - fetchCalls(1); }; - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - status: 'all', - call_type: 'all', - call_mode: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); + const handleFilterChange = (key: keyof CallFilters, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); - fetchCalls(1); }; - // 获取状态颜色 + const handleSelectCall = (callId: string) => { + setSelectedCalls(prev => + prev.includes(callId) + ? prev.filter(id => id !== callId) + : [...prev, callId] + ); + }; + + const handleSelectAll = () => { + if (selectedCalls.length === calls.length) { + setSelectedCalls([]); + } else { + setSelectedCalls(calls.map(call => call.id)); + } + }; + + const handleExport = async () => { + try { + toast.loading('正在导出通话记录...', { id: 'export' }); + + // 模拟导出延迟 + await new Promise(resolve => setTimeout(resolve, 2000)); + + toast.success('通话记录导出成功', { id: 'export' }); + } catch (error) { + toast.error('导出失败', { id: 'export' }); + } + }; + + const handlePlayRecording = (recordingUrl: string) => { + if (recordingUrl) { + toast.success('开始播放录音'); + // 这里可以集成音频播放器 + } else { + toast.error('录音文件不存在'); + } + }; + const getStatusColor = (status: string) => { switch (status) { + case 'completed': + return 'text-green-800 bg-green-100'; case 'active': - return 'bg-green-100 text-green-800'; - case 'pending': - return 'bg-yellow-100 text-yellow-800'; - case 'ended': - return 'bg-blue-100 text-blue-800'; - case 'cancelled': - return 'bg-red-100 text-red-800'; + return 'text-blue-800 bg-blue-100'; case 'failed': - return 'bg-red-100 text-red-800'; + return 'text-red-800 bg-red-100'; + case 'cancelled': + return 'text-gray-800 bg-gray-100'; default: - return 'bg-gray-100 text-gray-800'; + return 'text-gray-800 bg-gray-100'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { + case 'completed': + return '已完成'; case 'active': return '进行中'; - case 'pending': - return '待接听'; - case 'ended': - return '已结束'; - case 'cancelled': - return '已取消'; case 'failed': return '失败'; + case 'cancelled': + return '已取消'; default: - return '未知'; + return status; } }; - // 获取通话类型文本 - const getCallTypeText = (type: string) => { - switch (type) { - case 'audio': - return '语音通话'; - case 'video': - return '视频通话'; + const getStatusIcon = (status: string) => { + switch (status) { + case 'completed': + return ; + case 'active': + return ; + case 'failed': + return ; + case 'cancelled': + return ; default: - return '未知'; + return ; } }; - // 获取通话模式文本 - const getCallModeText = (mode: string) => { - switch (mode) { - case 'ai_voice': - return 'AI语音'; - case 'ai_video': - return 'AI视频'; - case 'sign_language': - return '手语翻译'; - case 'human_interpreter': - return '人工翻译'; - default: - return '未知'; - } - }; - - // 格式化时长 - const formatDuration = (seconds?: number) => { - if (!seconds) return '-'; - const minutes = Math.floor(seconds / 60); + const formatDuration = (seconds: number) => { + if (seconds === 0) return '-'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; - return `${minutes}分${remainingSeconds}秒`; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; + } else { + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + } }; - useEffect(() => { - fetchCalls(); - }, []); - return ( - + <> - 通话记录 - 口译服务管理后台 + 通话记录 - 翻译服务管理系统 - -
-
- {/* 页面标题 */} -
-

通话记录

+ + +
+ {/* 页面标题和操作 */} +
+
+

通话记录

+

+ 查看和管理所有的翻译通话记录,包括录音文件和通话详情。 +

+
+
+ +
- {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
- handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> + {/* 搜索和过滤器 */} +
+
+
+ +
+
+
-
- - {/* 状态筛选 */} -
- -
- - {/* 通话类型筛选 */} -
- -
- - {/* 通话模式筛选 */} -
- -
- - {/* 排序 */} -
- + handleSearch(e.target.value)} + />
-
-
+ +
+ + handleFilterChange('language', e.target.value)} + /> +
+ +
+ + +
+ +
+ +
{/* 通话记录列表 */} - {loading ? ( -
-
-
- ) : calls.length === 0 ? ( -
- -

暂无通话记录

-

- 调整筛选条件或检查数据源 -

-
- ) : ( -
-
-
-

- 通话记录 ({totalCount} 条记录) -

-
- +
+ {loading ? ( +
+
+
+ ) : ( + <>
- + - - - - - - {calls.map((call) => ( + - - - + +
+ + 0} + onChange={handleSelectAll} + /> + 通话信息 - 类型/模式 + + 参与者 - 时长 + + 语言对 - 费用 + + 时长/费用 + 状态 - 开始时间 + + 录音 - 操作 + + 操作
+ handleSelectCall(call.id)} + /> + -
-
- {call.id} -
-
- 主叫: {call.caller_id} -
- {call.callee_id && ( -
- 被叫: {call.callee_id} +
+
+
+
- )} +
+
+
+ {call.call_type === 'video' ? '视频通话' : '音频通话'} +
+
+ + {formatTime(call.start_time)} +
+
-
- {getCallTypeText(call.call_type)} -
-
- {getCallModeText(call.call_mode)} +
+
+ + {call.user_name} +
+
+ + {call.interpreter_name} +
- {formatDuration(call.duration)} - - - ¥{call.cost.toFixed(2)} - - - - {call.status === 'active' && } - {call.status === 'ended' && } - {getStatusText(call.status)} - +
{call.language_pair}
- {formatTime(call.start_time)} +
+ + {formatDuration(call.duration)} +
+
+ ¥{call.cost} +
+ +
+ {getStatusIcon(call.status)} + + {getStatusText(call.status)} + +
+
+ {call.recording_url ? ( + + ) : ( + 无录音 + )} + @@ -482,19 +565,19 @@ export default function CallsPage() { {/* 分页 */} {totalPages > 1 && ( -
+
@@ -503,54 +586,49 @@ export default function CallsPage() {

显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 条,共 {totalCount} 条记录 + {Math.min(currentPage * pageSize, totalCount)} 项, + 共 {totalCount}

)} -
- - )} + + )} + - - + + ); } \ No newline at end of file diff --git a/pages/dashboard/documents.tsx b/pages/dashboard/documents.tsx index f70bfa0..1b48688 100644 --- a/pages/dashboard/documents.tsx +++ b/pages/dashboard/documents.tsx @@ -1,496 +1,813 @@ -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; -import { toast } from 'react-hot-toast'; +import React, { useState, useEffect } from 'react'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { + DocumentDuplicateIcon, MagnifyingGlassIcon, - DocumentTextIcon, - CloudArrowUpIcon, - CloudArrowDownIcon, + PlusIcon, EyeIcon, - TrashIcon, PencilIcon, - CheckCircleIcon, - ClockIcon, - ExclamationCircleIcon, + TrashIcon, + ArrowDownTrayIcon, + FolderIcon, + DocumentTextIcon, + PhotoIcon, + FilmIcon, + MusicalNoteIcon, + ArchiveBoxIcon, ChevronLeftIcon, - ChevronRightIcon + ChevronRightIcon, + CalendarIcon, + UserIcon, + TagIcon, + StarIcon, + ClockIcon, + CheckCircleIcon, + XCircleIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { formatTime } from '@/utils'; -import Layout from '@/components/Layout'; interface Document { id: string; - user_id: string; - original_name: string; - file_size: number; - file_type: string; - source_language: string; - target_language: string; - status: 'pending' | 'processing' | 'completed' | 'failed'; - progress: number; - translated_url?: string; - cost: number; - created_at: string; - updated_at: string; - user_name?: string; + name: string; + originalName: string; + type: 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other'; + category: 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other'; + size: number; + uploadedBy: string; + uploadedAt: string; + lastModified: string; + status: 'active' | 'archived' | 'deleted'; + tags: string[]; + description?: string; + version: string; + downloadCount: number; + isPublic: boolean; + language?: string; + relatedOrderId?: string; + thumbnailUrl?: string; + url: string; } -interface DocumentFilters { - search: string; - status: 'all' | 'pending' | 'processing' | 'completed' | 'failed'; - language: string; - fileType: string; - sortBy: 'created_at' | 'file_size' | 'cost' | 'progress'; - sortOrder: 'asc' | 'desc'; -} - -export default function DocumentsPage() { +export default function Documents() { const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [filterType, setFilterType] = useState<'all' | 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other'>('all'); + const [filterCategory, setFilterCategory] = useState<'all' | 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other'>('all'); + const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'archived' | 'deleted'>('all'); const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); - const [filters, setFilters] = useState({ - search: '', - status: 'all', - language: 'all', - fileType: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); - const router = useRouter(); + const [selectedDocuments, setSelectedDocuments] = useState([]); + const [viewMode, setViewMode] = useState<'list' | 'grid'>('list'); + const documentsPerPage = 12; - const pageSize = 20; + useEffect(() => { + loadDocuments(); + }, [searchTerm, filterType, filterCategory, filterStatus, currentPage]); - // 获取文档数据 - const fetchDocuments = async (page = 1) => { + const loadDocuments = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); - - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.documents(); - setDocuments(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - 这里需要根据实际数据库结构调整 - // 暂时使用演示数据 - const result = await getDemoData.documents(); - setDocuments(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); - } + // 模拟API调用 + setTimeout(() => { + const mockData: Document[] = [ + { + id: '1', + name: '服务合同模板-2024版', + originalName: 'service_contract_template_2024.pdf', + type: 'pdf', + category: 'contract', + size: 2048576, // 2MB + uploadedBy: '管理员', + uploadedAt: '2024-01-15 10:30', + lastModified: '2024-01-20 14:45', + status: 'active', + tags: ['合同', '模板', '服务'], + description: '标准服务合同模板,适用于翻译服务业务', + version: '2.1', + downloadCount: 156, + isPublic: true, + language: '中文', + url: '/documents/service_contract_template_2024.pdf' + }, + { + id: '2', + name: '翻译质量评估报告', + originalName: 'quality_assessment_report.docx', + type: 'word', + category: 'report', + size: 1536000, // 1.5MB + uploadedBy: '张经理', + uploadedAt: '2024-01-18 16:20', + lastModified: '2024-01-19 09:15', + status: 'active', + tags: ['质量', '评估', '报告'], + description: '2024年第一季度翻译质量评估报告', + version: '1.0', + downloadCount: 89, + isPublic: false, + language: '中文', + relatedOrderId: 'ORD-2024-001', + url: '/documents/quality_assessment_report.docx' + }, + { + id: '3', + name: '翻译员认证证书', + originalName: 'translator_certificate.jpg', + type: 'image', + category: 'certificate', + size: 512000, // 512KB + uploadedBy: '李翻译', + uploadedAt: '2024-01-10 11:45', + lastModified: '2024-01-10 11:45', + status: 'active', + tags: ['证书', '认证', '翻译员'], + description: '专业翻译员资格认证证书', + version: '1.0', + downloadCount: 23, + isPublic: false, + thumbnailUrl: '/thumbnails/translator_certificate_thumb.jpg', + url: '/documents/translator_certificate.jpg' + }, + { + id: '4', + name: '用户操作手册', + originalName: 'user_manual_v3.pdf', + type: 'pdf', + category: 'manual', + size: 3072000, // 3MB + uploadedBy: '产品经理', + uploadedAt: '2024-01-12 13:30', + lastModified: '2024-01-16 10:20', + status: 'active', + tags: ['手册', '用户指南', '操作'], + description: '系统用户操作手册第三版', + version: '3.0', + downloadCount: 234, + isPublic: true, + language: '中文', + url: '/documents/user_manual_v3.pdf' + }, + { + id: '5', + name: '翻译项目演示视频', + originalName: 'project_demo.mp4', + type: 'video', + category: 'other', + size: 15728640, // 15MB + uploadedBy: '市场部', + uploadedAt: '2024-01-08 14:15', + lastModified: '2024-01-08 14:15', + status: 'active', + tags: ['演示', '视频', '项目'], + description: '翻译项目流程演示视频', + version: '1.0', + downloadCount: 67, + isPublic: true, + thumbnailUrl: '/thumbnails/project_demo_thumb.jpg', + url: '/documents/project_demo.mp4' + }, + { + id: '6', + name: '财务报表模板', + originalName: 'financial_template.xlsx', + type: 'excel', + category: 'template', + size: 1024000, // 1MB + uploadedBy: '财务部', + uploadedAt: '2024-01-05 09:45', + lastModified: '2024-01-14 16:30', + status: 'archived', + tags: ['财务', '模板', '报表'], + description: '月度财务报表模板', + version: '2.5', + downloadCount: 45, + isPublic: false, + url: '/documents/financial_template.xlsx' + }, + { + id: '7', + name: '产品介绍PPT', + originalName: 'product_introduction.pptx', + type: 'ppt', + category: 'other', + size: 5120000, // 5MB + uploadedBy: '销售部', + uploadedAt: '2024-01-03 15:20', + lastModified: '2024-01-11 11:10', + status: 'active', + tags: ['产品', '介绍', '演示'], + description: '公司产品介绍演示文稿', + version: '1.3', + downloadCount: 112, + isPublic: true, + language: '中文', + url: '/documents/product_introduction.pptx' + }, + { + id: '8', + name: '客户反馈音频', + originalName: 'customer_feedback.mp3', + type: 'audio', + category: 'other', + size: 2560000, // 2.5MB + uploadedBy: '客服部', + uploadedAt: '2024-01-01 10:00', + lastModified: '2024-01-01 10:00', + status: 'active', + tags: ['客户', '反馈', '音频'], + description: '客户服务质量反馈录音', + version: '1.0', + downloadCount: 34, + isPublic: false, + url: '/documents/customer_feedback.mp3' + } + ]; + // 应用过滤器 + let filteredData = mockData; + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filteredData = filteredData.filter(doc => + doc.name.toLowerCase().includes(term) || + doc.originalName.toLowerCase().includes(term) || + doc.description?.toLowerCase().includes(term) || + doc.tags.some(tag => tag.toLowerCase().includes(term)) + ); + } + + if (filterType !== 'all') { + filteredData = filteredData.filter(doc => doc.type === filterType); + } + + if (filterCategory !== 'all') { + filteredData = filteredData.filter(doc => doc.category === filterCategory); + } + + if (filterStatus !== 'all') { + filteredData = filteredData.filter(doc => doc.status === filterStatus); + } + + setDocuments(filteredData); + setLoading(false); + }, 1000); } catch (error) { - console.error('Error fetching documents:', error); - toast.error('获取文档列表失败'); - } finally { + console.error('加载文档数据失败:', error); setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof DocumentFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); + const handleSelectDocument = (documentId: string) => { + setSelectedDocuments(prev => + prev.includes(documentId) + ? prev.filter(id => id !== documentId) + : [...prev, documentId] + ); }; - // 应用筛选 - const applyFilters = () => { - setCurrentPage(1); - fetchDocuments(1); - }; - - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - status: 'all', - language: 'all', - fileType: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); - setCurrentPage(1); - fetchDocuments(1); - }; - - // 获取状态颜色 - const getStatusColor = (status: string) => { - switch (status) { - case 'completed': - return 'bg-green-100 text-green-800'; - case 'processing': - return 'bg-blue-100 text-blue-800'; - case 'pending': - return 'bg-yellow-100 text-yellow-800'; - case 'failed': - return 'bg-red-100 text-red-800'; - default: - return 'bg-gray-100 text-gray-800'; + const handleSelectAll = () => { + if (selectedDocuments.length === documents.length) { + setSelectedDocuments([]); + } else { + setSelectedDocuments(documents.map(doc => doc.id)); + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case 'pdf': + case 'word': + case 'text': + return ; + case 'excel': + return ; + case 'ppt': + return ; + case 'image': + return ; + case 'video': + return ; + case 'audio': + return ; + default: + return ; + } + }; + + const getTypeColor = (type: string) => { + switch (type) { + case 'pdf': return 'text-red-600'; + case 'word': return 'text-blue-600'; + case 'excel': return 'text-green-600'; + case 'ppt': return 'text-orange-600'; + case 'image': return 'text-purple-600'; + case 'video': return 'text-pink-600'; + case 'audio': return 'text-indigo-600'; + case 'text': return 'text-gray-600'; + default: return 'text-gray-600'; + } + }; + + const getCategoryText = (category: string) => { + switch (category) { + case 'contract': return '合同'; + case 'translation': return '翻译'; + case 'template': return '模板'; + case 'manual': return '手册'; + case 'certificate': return '证书'; + case 'report': return '报告'; + case 'other': return '其他'; + default: return category; + } + }; + + const getCategoryColor = (category: string) => { + switch (category) { + case 'contract': return 'bg-red-100 text-red-800'; + case 'translation': return 'bg-blue-100 text-blue-800'; + case 'template': return 'bg-green-100 text-green-800'; + case 'manual': return 'bg-yellow-100 text-yellow-800'; + case 'certificate': return 'bg-purple-100 text-purple-800'; + case 'report': return 'bg-indigo-100 text-indigo-800'; + case 'other': return 'bg-gray-100 text-gray-800'; + default: return 'bg-gray-100 text-gray-800'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { - case 'completed': - return '已完成'; - case 'processing': - return '处理中'; - case 'pending': - return '等待中'; - case 'failed': - return '失败'; - default: - return '未知'; + case 'active': return '正常'; + case 'archived': return '已归档'; + case 'deleted': return '已删除'; + default: return status; } }; - // 获取状态图标 - const getStatusIcon = (status: string) => { + const getStatusColor = (status: string) => { switch (status) { - case 'completed': - return ; - case 'processing': - return ; - case 'pending': - return ; - case 'failed': - return ; - default: - return ; + case 'active': return 'bg-green-100 text-green-800'; + case 'archived': return 'bg-yellow-100 text-yellow-800'; + case 'deleted': return 'bg-red-100 text-red-800'; + default: return 'bg-gray-100 text-gray-800'; } }; - // 格式化文件大小 const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return '0 B'; const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; - // 删除文档 - const handleDeleteDocument = async (documentId: string) => { - if (confirm('确定要删除此文档吗?')) { - try { - // 这里应该调用删除API - toast.success('文档删除成功'); - fetchDocuments(); - } catch (error) { - toast.error('删除文档失败'); - } - } - }; + const totalPages = Math.ceil(documents.length / documentsPerPage); + const currentDocuments = documents.slice( + (currentPage - 1) * documentsPerPage, + currentPage * documentsPerPage + ); - // 下载文档 - const handleDownloadDocument = async (document: Document) => { - try { - // 这里应该调用下载API - toast.success('开始下载文档'); - } catch (error) { - toast.error('下载文档失败'); - } - }; - - useEffect(() => { - fetchDocuments(); - }, []); + if (loading) { + return ( + +
+
+
+
加载文档数据中...
+
+
+
+ ); + } return ( - - - 文档管理 - 口译服务管理后台 - - -
-
- {/* 页面标题 */} -
+ +
+ {/* 页面标题 */} +
+

文档管理

-
+
+ +
+
- {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
- handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> -
+ {/* 统计卡片 */} +
+
+
+
+
+
- - {/* 状态筛选 */} -
- +
+
+
文档总数
+
{documents.length}
+
- - {/* 语言筛选 */} -
- -
- - {/* 文件类型筛选 */} -
- -
-
- -
- -
- {/* 文档列表 */} - {loading ? ( -
-
+
+
+
+
+ +
+
+
+
正常文档
+
+ {documents.filter(d => d.status === 'active').length} +
+
+
+
- ) : ( -
-
-
-

- 文档列表 ({totalCount} 个文档) -

+
+ +
+
+
+
+
- -
- {documents.map((document) => ( -
-
-
-
- -
-

- {document.original_name} -

-

- {document.user_name} | {formatFileSize(document.file_size)} | {document.file_type.toUpperCase()} -

-

- {document.source_language} → {document.target_language} | ¥{document.cost.toFixed(2)} -

-
-
-
- -
- {/* 进度条 */} - {document.status === 'processing' && ( -
-
-
-
-

{document.progress}%

-
- )} - - {/* 状态 */} -
- {getStatusIcon(document.status)} - - {getStatusText(document.status)} - -
- - {/* 操作按钮 */} -
- - {document.status === 'completed' && document.translated_url && ( - - )} - - -
-
-
- - {/* 时间信息 */} -
- 创建时间: {formatTime(document.created_at)} | - 更新时间: {formatTime(document.updated_at)} -
-
- ))} - - {documents.length === 0 && ( -
- -

暂无文档

-

开始上传您的第一个文档

-
- )} +
+
+
已归档
+
+ {documents.filter(d => d.status === 'archived').length} +
+
+
+
+
- {/* 分页 */} - {totalPages > 1 && ( -
-
- - -
-
-
-

- 显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 个,共 {totalCount} 个文档 -

-
-
- -
-
-
- )} +
+
+
+
+ +
+
+
+
总下载次数
+
+ {documents.reduce((sum, d) => sum + d.downloadCount, 0)} +
+
+
+
+
+
+
+ + {/* 搜索和过滤 */} +
+
+ {/* 搜索框 */} +
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+ + {/* 文件类型过滤 */} +
+ +
+ + {/* 分类过滤 */} +
+ +
+ + {/* 状态过滤 */} +
+ +
+
+ + {/* 批量操作 */} + {selectedDocuments.length > 0 && ( +
+ + 已选择 {selectedDocuments.length} 个文档 + +
+ + +
)}
+ + {/* 文档列表/网格 */} + {viewMode === 'list' ? ( +
+
+ + + + + + + + + + + + + + + {currentDocuments.map((document) => ( + + + + + + + + + + + ))} + +
+ 0 && selectedDocuments.length === documents.length} + onChange={handleSelectAll} + /> + + 文档信息 + + 分类 + + 大小 + + 上传信息 + + 状态 + + 下载次数 + + 操作 +
+ handleSelectDocument(document.id)} + /> + +
+
+ {getTypeIcon(document.type)} +
+
+
{document.name}
+
{document.originalName}
+ {document.description && ( +
{document.description}
+ )} +
+
+
+ + {getCategoryText(document.category)} + + + {formatFileSize(document.size)} + +
+
+ + {document.uploadedBy} +
+
+ + {document.uploadedAt} +
+
+
+ + {getStatusText(document.status)} + + {document.isPublic && ( +
公开
+ )} +
+ {document.downloadCount} + +
+ + + + +
+
+
+
+ ) : ( +
+
+ {currentDocuments.map((document) => ( +
+
+ handleSelectDocument(document.id)} + /> +
+ + +
+
+ +
+ {document.thumbnailUrl ? ( + {document.name} + ) : ( +
+ {getTypeIcon(document.type)} +
+ )} +
+ +
+

{document.name}

+

{formatFileSize(document.size)}

+ +
+ + {getCategoryText(document.category)} + +
+ +
+
{document.uploadedBy}
+
{document.uploadedAt}
+
下载 {document.downloadCount} 次
+
+
+
+ ))} +
+
+ )} + + {/* 分页 */} + {totalPages > 1 && ( +
+
+ + +
+
+
+

+ 显示第 {(currentPage - 1) * documentsPerPage + 1} 到{' '} + {Math.min(currentPage * documentsPerPage, documents.length)} 条, + 共 {documents.length} 条记录 +

+
+
+ +
+
+
+ )}
- + ); } \ No newline at end of file diff --git a/pages/dashboard/enterprise.tsx b/pages/dashboard/enterprise.tsx index 3212ae5..059a594 100644 --- a/pages/dashboard/enterprise.tsx +++ b/pages/dashboard/enterprise.tsx @@ -1,858 +1,683 @@ -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; -import { toast } from 'react-hot-toast'; +import React, { useState, useEffect } from 'react'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { - MagnifyingGlassIcon, BuildingOfficeIcon, - UsersIcon, - DocumentTextIcon, - CurrencyDollarIcon, + MagnifyingGlassIcon, PlusIcon, + EyeIcon, PencilIcon, TrashIcon, - EyeIcon, + ClockIcon, + CheckCircleIcon, + XCircleIcon, ChevronLeftIcon, - ChevronRightIcon + ChevronRightIcon, + UserGroupIcon, + CalendarIcon, + CurrencyDollarIcon, + DocumentTextIcon, + StarIcon, + PhoneIcon, + EnvelopeIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { formatTime } from '@/utils'; -import Layout from '@/components/Layout'; -interface EnterpriseContract { +interface Enterprise { id: string; - enterprise_id: string; - enterprise_name: string; - contract_number: string; - contract_type: 'annual' | 'monthly' | 'project'; - start_date: string; - end_date: string; - total_amount: number; - currency: string; - status: 'active' | 'expired' | 'terminated'; - service_rates: { - ai_voice: number; // AI语音翻译费率(元/分钟) - ai_video: number; // AI视频翻译费率(元/分钟) - sign_language: number; // 手语翻译费率(元/分钟) - human_interpreter: number; // 真人翻译费率(元/分钟) - document_translation: number; // 文档翻译费率(元/字) - }; - created_at: string; - updated_at: string; + companyName: string; + contactPerson: string; + contactPhone: string; + contactEmail: string; + industry: string; + companySize: 'small' | 'medium' | 'large' | 'enterprise'; + servicePackage: 'basic' | 'standard' | 'premium' | 'custom'; + contractStatus: 'active' | 'expired' | 'pending' | 'cancelled'; + contractValue: number; + contractStart: string; + contractEnd: string; + monthlyUsage: number; + totalOrders: number; + rating: number; + address: string; + website?: string; + notes?: string; + createdAt: string; + lastActivity: string; } -interface EnterpriseEmployee { - id: string; - enterprise_id: string; - enterprise_name: string; - name: string; - email: string; - department: string; - position: string; - status: 'active' | 'inactive'; - total_calls: number; - total_cost: number; - created_at: string; -} - -interface EnterpriseBilling { - id: string; - enterprise_id: string; - enterprise_name: string; - period: string; - total_calls: number; - total_duration: number; - total_amount: number; - currency: string; - status: 'pending' | 'paid' | 'overdue'; - due_date: string; - paid_date?: string; - created_at: string; -} - -interface EnterpriseFilters { - search: string; - tab: 'contracts' | 'employees' | 'billing'; - status: 'all' | 'active' | 'inactive' | 'expired' | 'pending' | 'paid' | 'overdue'; - enterprise: 'all' | string; - sortBy: 'created_at' | 'name' | 'amount' | 'total_calls'; - sortOrder: 'asc' | 'desc'; -} - -export default function EnterprisePage() { - const [contracts, setContracts] = useState([]); - const [employees, setEmployees] = useState([]); - const [billing, setBilling] = useState([]); +export default function Enterprise() { + const [enterprises, setEnterprises] = useState([]); const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [filterPackage, setFilterPackage] = useState<'all' | 'basic' | 'standard' | 'premium' | 'custom'>('all'); + const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'expired' | 'pending' | 'cancelled'>('all'); const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); - const [filters, setFilters] = useState({ - search: '', - tab: 'contracts', - status: 'all', - enterprise: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); - const router = useRouter(); + const [selectedEnterprises, setSelectedEnterprises] = useState([]); + const enterprisesPerPage = 10; - const pageSize = 20; + useEffect(() => { + loadEnterprises(); + }, [searchTerm, filterPackage, filterStatus, currentPage]); - // 获取企业数据 - const fetchEnterpriseData = async (page = 1) => { + const loadEnterprises = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); - - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.enterprise(); - - switch (filters.tab) { - case 'contracts': - setContracts(result.contracts); - setTotalCount(result.contracts.length); - break; - case 'employees': - setEmployees(result.employees); - setTotalCount(result.employees.length); - break; - case 'billing': - setBilling(result.billing); - setTotalCount(result.billing.length); - break; - } - - setTotalPages(Math.ceil(totalCount / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - 这里需要根据实际数据库结构调整 - // 暂时使用演示数据 - const result = await getDemoData.enterprise(); - - switch (filters.tab) { - case 'contracts': - setContracts(result.contracts); - setTotalCount(result.contracts.length); - break; - case 'employees': - setEmployees(result.employees); - setTotalCount(result.employees.length); - break; - case 'billing': - setBilling(result.billing); - setTotalCount(result.billing.length); - break; - } - - setTotalPages(Math.ceil(totalCount / pageSize)); - setCurrentPage(page); - } + // 模拟API调用 + setTimeout(() => { + const mockData: Enterprise[] = [ + { + id: '1', + companyName: '科技创新有限公司', + contactPerson: '张总', + contactPhone: '+86 138-0000-0001', + contactEmail: 'zhang@tech-innovation.com', + industry: '科技', + companySize: 'large', + servicePackage: 'premium', + contractStatus: 'active', + contractValue: 500000, + contractStart: '2024-01-01', + contractEnd: '2024-12-31', + monthlyUsage: 45000, + totalOrders: 156, + rating: 4.8, + address: '北京市朝阳区科技园区A座', + website: 'https://tech-innovation.com', + notes: '重要客户,需要优先服务', + createdAt: '2023-12-15', + lastActivity: '2024-01-20 15:30' + }, + { + id: '2', + companyName: '国际贸易集团', + contactPerson: '李经理', + contactPhone: '+86 139-0000-0002', + contactEmail: 'li@international-trade.com', + industry: '贸易', + companySize: 'enterprise', + servicePackage: 'custom', + contractStatus: 'active', + contractValue: 800000, + contractStart: '2023-06-01', + contractEnd: '2025-05-31', + monthlyUsage: 68000, + totalOrders: 289, + rating: 4.9, + address: '上海市浦东新区金融中心B座', + website: 'https://international-trade.com', + notes: '多语言需求,主要涉及欧洲市场', + createdAt: '2023-05-20', + lastActivity: '2024-01-21 09:15' + }, + { + id: '3', + companyName: '医疗设备公司', + contactPerson: '王主任', + contactPhone: '+86 136-0000-0003', + contactEmail: 'wang@medical-device.com', + industry: '医疗', + companySize: 'medium', + servicePackage: 'standard', + contractStatus: 'active', + contractValue: 200000, + contractStart: '2024-01-15', + contractEnd: '2024-07-14', + monthlyUsage: 15000, + totalOrders: 67, + rating: 4.5, + address: '广州市天河区医疗产业园', + notes: '专业医疗术语翻译需求', + createdAt: '2024-01-10', + lastActivity: '2024-01-19 14:20' + }, + { + id: '4', + companyName: '教育培训机构', + contactPerson: '陈校长', + contactPhone: '+86 135-0000-0004', + contactEmail: 'chen@education.com', + industry: '教育', + companySize: 'small', + servicePackage: 'basic', + contractStatus: 'expired', + contractValue: 50000, + contractStart: '2023-09-01', + contractEnd: '2023-12-31', + monthlyUsage: 8000, + totalOrders: 34, + rating: 4.2, + address: '深圳市南山区教育城', + notes: '合同已到期,需要续约', + createdAt: '2023-08-25', + lastActivity: '2024-01-05 10:45' + }, + { + id: '5', + companyName: '新兴科技公司', + contactPerson: '刘总监', + contactPhone: '+86 137-0000-0005', + contactEmail: 'liu@emerging-tech.com', + industry: '科技', + companySize: 'medium', + servicePackage: 'standard', + contractStatus: 'pending', + contractValue: 150000, + contractStart: '2024-02-01', + contractEnd: '2024-08-31', + monthlyUsage: 0, + totalOrders: 0, + rating: 0, + address: '杭州市西湖区创新园', + website: 'https://emerging-tech.com', + notes: '新客户,正在洽谈合同', + createdAt: '2024-01-18', + lastActivity: '2024-01-21 16:00' + } + ]; + // 应用过滤器 + let filteredData = mockData; + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filteredData = filteredData.filter(enterprise => + enterprise.companyName.toLowerCase().includes(term) || + enterprise.contactPerson.toLowerCase().includes(term) || + enterprise.contactEmail.toLowerCase().includes(term) || + enterprise.industry.toLowerCase().includes(term) + ); + } + + if (filterPackage !== 'all') { + filteredData = filteredData.filter(enterprise => enterprise.servicePackage === filterPackage); + } + + if (filterStatus !== 'all') { + filteredData = filteredData.filter(enterprise => enterprise.contractStatus === filterStatus); + } + + setEnterprises(filteredData); + setLoading(false); + }, 1000); } catch (error) { - console.error('Error fetching enterprise data:', error); - toast.error('获取企业数据失败'); - } finally { + console.error('加载企业数据失败:', error); setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof EnterpriseFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); + const handleSelectEnterprise = (enterpriseId: string) => { + setSelectedEnterprises(prev => + prev.includes(enterpriseId) + ? prev.filter(id => id !== enterpriseId) + : [...prev, enterpriseId] + ); }; - // 应用筛选 - const applyFilters = () => { - setCurrentPage(1); - fetchEnterpriseData(1); - }; - - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - tab: filters.tab, - status: 'all', - enterprise: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); - setCurrentPage(1); - fetchEnterpriseData(1); - }; - - // 获取状态颜色 - const getStatusColor = (status: string) => { - switch (status) { - case 'active': - return 'bg-green-100 text-green-800'; - case 'inactive': - case 'expired': - return 'bg-red-100 text-red-800'; - case 'terminated': - return 'bg-gray-100 text-gray-800'; - case 'pending': - return 'bg-yellow-100 text-yellow-800'; - case 'paid': - return 'bg-green-100 text-green-800'; - case 'overdue': - return 'bg-red-100 text-red-800'; - default: - return 'bg-gray-100 text-gray-800'; + const handleSelectAll = () => { + if (selectedEnterprises.length === enterprises.length) { + setSelectedEnterprises([]); + } else { + setSelectedEnterprises(enterprises.map(enterprise => enterprise.id)); + } + }; + + const getPackageText = (packageType: string) => { + switch (packageType) { + case 'basic': return '基础版'; + case 'standard': return '标准版'; + case 'premium': return '高级版'; + case 'custom': return '定制版'; + default: return packageType; + } + }; + + const getPackageColor = (packageType: string) => { + switch (packageType) { + case 'basic': return 'bg-gray-100 text-gray-800'; + case 'standard': return 'bg-blue-100 text-blue-800'; + case 'premium': return 'bg-purple-100 text-purple-800'; + case 'custom': return 'bg-green-100 text-green-800'; + default: return 'bg-gray-100 text-gray-800'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { - case 'active': - return '活跃'; - case 'inactive': - return '非活跃'; - case 'expired': - return '已过期'; - case 'terminated': - return '已终止'; - case 'pending': - return '待付款'; - case 'paid': - return '已付款'; - case 'overdue': - return '逾期'; - default: - return '未知'; + case 'active': return '生效中'; + case 'expired': return '已过期'; + case 'pending': return '待生效'; + case 'cancelled': return '已取消'; + default: return status; } }; - // 删除员工 - const handleDeleteEmployee = async (employeeId: string) => { - if (confirm('确定要删除此员工吗?')) { - try { - // 这里应该调用删除API - toast.success('员工删除成功'); - fetchEnterpriseData(); - } catch (error) { - toast.error('删除员工失败'); - } + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return 'bg-green-100 text-green-800'; + case 'expired': return 'bg-red-100 text-red-800'; + case 'pending': return 'bg-yellow-100 text-yellow-800'; + case 'cancelled': return 'bg-gray-100 text-gray-800'; + default: return 'bg-gray-100 text-gray-800'; } }; - // 员工结算 - const handleEmployeeSettlement = async (employeeId: string) => { - try { - // 这里应该调用结算API - toast.success('员工结算完成'); - fetchEnterpriseData(); - } catch (error) { - toast.error('员工结算失败'); + const getStatusIcon = (status: string) => { + switch (status) { + case 'active': return ; + case 'expired': return ; + case 'pending': return ; + case 'cancelled': return ; + default: return ; } }; - // 获取唯一的企业列表(用于筛选下拉框) - const getUniqueEnterprises = () => { - const enterprises = new Set(); - employees.forEach(emp => enterprises.add(emp.enterprise_name)); - contracts.forEach(contract => enterprises.add(contract.enterprise_name)); - return Array.from(enterprises) as string[]; + const getCompanySizeText = (size: string) => { + switch (size) { + case 'small': return '小型企业'; + case 'medium': return '中型企业'; + case 'large': return '大型企业'; + case 'enterprise': return '集团企业'; + default: return size; + } }; - useEffect(() => { - fetchEnterpriseData(); - }, [filters.tab]); + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY' + }).format(amount); + }; - const renderContracts = () => ( -
-
-
-
-

企业合同管理

-

管理企业合同信息和服务费率配置

-
-
- - {/* 搜索和筛选 */} -
-
- - handleFilterChange('search', e.target.value)} - /> -
- - - -
- -
- - -
-
- - {/* 合同列表 */} -
- {contracts.map((contract) => ( -
-
-
-
-

{contract.enterprise_name}

-

合同号: {contract.contract_number}

-
- - {getStatusText(contract.status)} - -
-
- -
-
-
-
合同类型
-
- {contract.contract_type === 'annual' ? '年度合同' : - contract.contract_type === 'monthly' ? '月度合同' : '项目合同'} -
-
-
-
合同期限
-
- {formatTime(contract.start_date)} - {formatTime(contract.end_date)} -
-
-
-
合同金额
-
- ¥{contract.total_amount.toLocaleString()} -
-
-
- - {/* 服务费率配置 */} -
-
服务费率配置
-
-
-
AI语音翻译
-
¥{contract.service_rates.ai_voice}/分钟
-
-
-
AI视频翻译
-
¥{contract.service_rates.ai_video}/分钟
-
-
-
手语翻译
-
¥{contract.service_rates.sign_language}/分钟
-
-
-
真人翻译
-
¥{contract.service_rates.human_interpreter}/分钟
-
-
-
文档翻译
-
¥{contract.service_rates.document_translation}/字
-
-
-
- -
- - -
-
-
- ))} -
- - {/* 分页 */} - {totalPages > 1 && ( -
-
- 显示第 {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalCount)} 条,共 {totalCount} 条 -
-
- - - {currentPage} / {totalPages} - - -
-
- )} -
-
+ const totalPages = Math.ceil(enterprises.length / enterprisesPerPage); + const currentEnterprises = enterprises.slice( + (currentPage - 1) * enterprisesPerPage, + currentPage * enterprisesPerPage ); - const renderEmployees = () => ( -
-
-
-
-

企业员工管理

-

管理企业员工信息和通话记录

+ if (loading) { + return ( + +
+
+
+
加载企业数据中...
- - {/* 搜索和筛选 */} -
-
- - handleFilterChange('search', e.target.value)} - /> -
- - - - - -
- - -
-
- - {/* 员工列表 */} -
- - - - - - - - - - - - - {employees.map((employee) => ( - - - - - - - - - ))} - -
- 员工信息 - - 所属企业 - - 部门/职位 - - 通话统计 - - 状态 - - 操作 -
-
-
-
- - {employee.name.charAt(0)} - -
-
-
-
{employee.name}
-
{employee.email}
-
-
-
-
{employee.enterprise_name}
-
-
{employee.department}
-
{employee.position}
-
-
{employee.total_calls} 次通话
-
¥{employee.total_cost.toFixed(2)}
-
- - {getStatusText(employee.status)} - - -
- - - -
-
-
-
-
- ); - - const renderBilling = () => ( -
- {billing.map((bill) => ( -
-
-
-

- {bill.enterprise_name} -

-

- 账单期间: {bill.period} -

-

- 通话次数: {bill.total_calls} | 总时长: {Math.floor(bill.total_duration / 60)}分钟 -

-

- 金额: ¥{bill.total_amount.toFixed(2)} -

-

- 到期日期: {formatTime(bill.due_date)} - {bill.paid_date && ` | 付款日期: ${formatTime(bill.paid_date)}`} -

-
-
- - {getStatusText(bill.status)} - -
- -
-
-
-
- ))} -
- ); + + ); + } return ( - - - 企业服务 - 口译服务管理后台 - - -
-
- {/* 页面标题 */} -
-

企业服务管理

-
+
- {/* 标签页 */} -
- + {/* 统计卡片 */} +
+
+
+
+
+ +
+
+
+
企业客户总数
+
{enterprises.length}
+
+
+
+
- {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
+
+
+
+
+ +
+
+
+
活跃合同
+
+ {enterprises.filter(e => e.contractStatus === 'active').length} +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
合同总价值
+
+ {formatCurrency(enterprises.reduce((sum, e) => sum + e.contractValue, 0))} +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
总订单数
+
+ {enterprises.reduce((sum, e) => sum + e.totalOrders, 0)} +
+
+
+
+
+
+
+ + {/* 搜索和过滤 */} +
+
+ {/* 搜索框 */} +
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+ + {/* 服务套餐过滤 */} +
+ +
+ + {/* 合同状态过滤 */} +
+ +
+
+ + {/* 批量操作 */} + {selectedEnterprises.length > 0 && ( +
+ + 已选择 {selectedEnterprises.length} 个企业客户 + +
+ + +
+
+ )} +
+ + {/* 企业列表 */} +
+
+ + + + + + + + + + + + + + + + {currentEnterprises.map((enterprise) => ( + + + + + + + + + + + + ))} + +
handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + type="checkbox" + className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" + checked={enterprises.length > 0 && selectedEnterprises.length === enterprises.length} + onChange={handleSelectAll} /> - - - - {/* 状态筛选 */} -
- -
- - {/* 排序 */} -
- -
- - -
- - -
- +
+ 企业信息 + + 联系信息 + + 服务套餐 + + 合同状态 + + 合同价值 + + 使用情况 + + 评价 + + 操作 +
+ handleSelectEnterprise(enterprise.id)} + /> + +
+ +
+
{enterprise.companyName}
+
{enterprise.industry} · {getCompanySizeText(enterprise.companySize)}
+ {enterprise.website && ( +
{enterprise.website}
+ )} +
+
+
+
+
+ + {enterprise.contactPerson} +
+
+ + {enterprise.contactPhone} +
+
+ + {enterprise.contactEmail} +
+
+
+ + {getPackageText(enterprise.servicePackage)} + + +
+ + {getStatusIcon(enterprise.contractStatus)} + {getStatusText(enterprise.contractStatus)} + +
+ {enterprise.contractStart} ~ {enterprise.contractEnd} +
+
+
+
+ {formatCurrency(enterprise.contractValue)} +
+
+ 月使用:{formatCurrency(enterprise.monthlyUsage)} +
+
+
+
订单:{enterprise.totalOrders}
+
+ 最后活动:{enterprise.lastActivity} +
+
+
+ {enterprise.rating > 0 && ( +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ {enterprise.rating} +
+ )} +
+
+ + + +
+
- {/* 内容区域 */} - {loading ? ( -
-
-
- ) : ( -
-
-
-

- {filters.tab === 'contracts' ? '企业合同' : filters.tab === 'employees' ? '企业员工' : '结算记录'} ({totalCount} 条记录) -

+ {/* 分页 */} + {totalPages > 1 && ( +
+
+ + +
+
+
+

+ 显示第 {(currentPage - 1) * enterprisesPerPage + 1} 到{' '} + {Math.min(currentPage * enterprisesPerPage, enterprises.length)} 条, + 共 {enterprises.length} 条记录 +

- - {filters.tab === 'contracts' && renderContracts()} - {filters.tab === 'employees' && renderEmployees()} - {filters.tab === 'billing' && renderBilling()} - - {/* 分页 */} - {totalPages > 1 && ( -
-
+
+
-
-
-

- 显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 条,共 {totalCount} 条记录 -

-
-
- -
-
-
- )} + ))} + + +
)}
- + ); } \ No newline at end of file diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 8fd75ef..6d9ba52 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -1,484 +1,373 @@ import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; -import Link from 'next/link'; -import { toast } from 'react-hot-toast'; -import { - PhoneIcon, - VideoCameraIcon, - UserGroupIcon, - ClockIcon, +import DashboardLayout from '../../components/Layout/DashboardLayout'; +import { getDemoData } from '../../lib/demo-data'; +import { + UsersIcon, + PhoneIcon, + DocumentTextIcon, CurrencyDollarIcon, + CheckCircleIcon, + ClockIcon, ExclamationTriangleIcon, - PlayIcon, - StopIcon, - UserPlusIcon, - ArrowRightOnRectangleIcon + ArrowUpIcon, + ArrowDownIcon, + EyeIcon } from '@heroicons/react/24/outline'; -import { auth, db, TABLES, realtime, supabase } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { Call, CallStats, Interpreter, User } from '@/types'; -import { - formatCurrency, - formatTime, - formatDuration, - getCallStatusText, - getCallModeText, - getStatusColor -} from '@/utils'; -import Layout from '@/components/Layout'; -interface DashboardProps { - user?: User; +interface DashboardStats { + totalUsers: number; + activeUsers: number; + totalCalls: number; + activeCalls: number; + totalOrders: number; + pendingOrders: number; + completedOrders: number; + totalRevenue: number; + monthlyRevenue: number; + activeInterpreters: number; } -export default function Dashboard({ user }: DashboardProps) { - const router = useRouter(); +interface RecentActivity { + id: string; + type: 'call' | 'order' | 'user' | 'system'; + title: string; + description: string; + time: string; + status: 'success' | 'warning' | 'error' | 'info'; +} + +export default function Dashboard() { + const [stats, setStats] = useState(null); + const [activities, setActivities] = useState([]); const [loading, setLoading] = useState(true); - const [stats, setStats] = useState({ - total_calls_today: 0, - active_calls: 0, - average_response_time: 0, - online_interpreters: 0, - total_revenue_today: 0, - currency: 'CNY', - }); - const [activeCalls, setActiveCalls] = useState([]); - const [onlineInterpreters, setOnlineInterpreters] = useState([]); - const [isDemoMode, setIsDemoMode] = useState(false); - - // 获取仪表盘数据 - const fetchDashboardData = async () => { - try { - setLoading(true); - - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemoMode = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemoMode); - - if (isDemoMode) { - // 使用演示数据 - const [statsData, callsData, interpretersData] = await Promise.all([ - getDemoData.stats(), - getDemoData.calls(), - getDemoData.interpreters(), - ]); - - // 转换演示数据格式以匹配类型定义 - setStats({ - total_calls_today: statsData.todayCalls, - active_calls: statsData.activeCalls, - average_response_time: statsData.avgResponseTime, - online_interpreters: statsData.onlineInterpreters, - total_revenue_today: statsData.todayRevenue, - currency: 'CNY', - }); - - // 转换通话数据格式 - const formattedCalls = callsData - .filter(call => call.status === 'active') - .map(call => ({ - id: call.id, - caller_id: call.user_id, - callee_id: call.interpreter_id, - call_type: 'audio' as const, - call_mode: 'human_interpreter' as const, - status: call.status as 'active', - start_time: call.start_time, - end_time: call.end_time, - duration: call.duration, - cost: call.cost, - currency: 'CNY' as const, - created_at: call.created_at, - updated_at: call.created_at, - })); - - // 转换翻译员数据格式 - const formattedInterpreters = interpretersData - .filter(interpreter => interpreter.status !== 'offline') - .map(interpreter => ({ - id: interpreter.id, - user_id: interpreter.id, - name: interpreter.name, - avatar_url: interpreter.avatar_url, - languages: interpreter.languages, - specializations: interpreter.specialties, - hourly_rate: 100, - currency: 'CNY' as const, - rating: interpreter.rating, - total_calls: 50, - status: interpreter.status === 'busy' ? 'busy' as const : 'online' as const, - is_certified: true, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - })); - - setActiveCalls(formattedCalls); - setOnlineInterpreters(formattedInterpreters); - } else { - // 使用真实数据 - const today = new Date(); - today.setHours(0, 0, 0, 0); - - // 获取今日通话统计 - const { data: todayCalls } = await supabase - .from(TABLES.CALLS) - .select('*') - .gte('created_at', today.toISOString()); - - // 获取活跃通话 - const { data: activeCallsData } = await supabase - .from(TABLES.CALLS) - .select(` - *, - user:users(full_name, email), - interpreter:interpreters(name, rating) - `) - .eq('status', 'active'); - - // 获取在线翻译员 - const { data: interpretersData } = await supabase - .from(TABLES.INTERPRETERS) - .select('*') - .neq('status', 'offline'); - - // 计算统计数据 - const totalRevenue = todayCalls && todayCalls.length > 0 - ? todayCalls - .filter(call => call.status === 'ended') - .reduce((sum, call) => sum + call.cost, 0) - : 0; - - const avgResponseTime = todayCalls && todayCalls.length > 0 - ? todayCalls.reduce((sum, call) => { - const startTime = new Date(call.start_time); - const createdTime = new Date(call.created_at); - return sum + (startTime.getTime() - createdTime.getTime()) / 1000; - }, 0) / todayCalls.length - : 0; - - setStats({ - total_calls_today: todayCalls?.length || 0, - active_calls: activeCallsData?.length || 0, - average_response_time: Math.round(avgResponseTime), - online_interpreters: interpretersData?.length || 0, - total_revenue_today: totalRevenue, - currency: 'CNY', - }); - - setActiveCalls(activeCallsData || []); - setOnlineInterpreters(interpretersData || []); - } - } catch (error) { - console.error('获取仪表盘数据失败:', error); - toast.error('获取数据失败,请稍后重试'); - - // 如果获取真实数据失败,切换到演示模式 - setIsDemoMode(true); - const [statsData, callsData, interpretersData] = await Promise.all([ - getDemoData.stats(), - getDemoData.calls(), - getDemoData.interpreters(), - ]); - - setStats({ - total_calls_today: statsData.todayCalls, - active_calls: statsData.activeCalls, - average_response_time: statsData.avgResponseTime, - online_interpreters: statsData.onlineInterpreters, - total_revenue_today: statsData.todayRevenue, - currency: 'CNY', - }); - - // 设置空数组避免类型错误 - setActiveCalls([]); - setOnlineInterpreters([]); - } finally { - setLoading(false); - } - }; - - // 强制结束通话 - const handleEndCall = async (callId: string) => { - try { - await db.update(TABLES.CALLS, callId, { - status: 'ended', - end_time: new Date().toISOString() - }); - toast.success('通话已结束'); - fetchDashboardData(); - } catch (error) { - console.error('Error ending call:', error); - toast.error('结束通话失败'); - } - }; - - // 分配翻译员 - const handleAssignInterpreter = async (callId: string, interpreterId: string) => { - try { - await db.update(TABLES.CALLS, callId, { - callee_id: interpreterId, - call_mode: 'human_interpreter' - }); - toast.success('翻译员已分配'); - fetchDashboardData(); - } catch (error) { - console.error('Error assigning interpreter:', error); - toast.error('分配翻译员失败'); - } - }; useEffect(() => { - // 在演示模式下不检查用户认证 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemoMode = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - - if (!isDemoMode && !user) { - router.push('/auth/login'); - return; - } + const loadDashboardData = async () => { + try { + setLoading(true); + + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 使用演示数据 + const mockStats: DashboardStats = { + totalUsers: 1248, + activeUsers: 856, + totalCalls: 3456, + activeCalls: 12, + totalOrders: 2789, + pendingOrders: 45, + completedOrders: 2654, + totalRevenue: 125000, + monthlyRevenue: 15600, + activeInterpreters: 23 + }; - fetchDashboardData(); + const mockActivities: RecentActivity[] = [ + { + id: '1', + type: 'call', + title: '新通话开始', + description: '张三开始了中英互译通话', + time: '2分钟前', + status: 'success' + }, + { + id: '2', + type: 'order', + title: '订单完成', + description: '订单ORD-2024-001已完成,费用¥180', + time: '5分钟前', + status: 'success' + }, + { + id: '3', + type: 'user', + title: '新用户注册', + description: 'ABC公司注册了企业账户', + time: '10分钟前', + status: 'info' + }, + { + id: '4', + type: 'system', + title: '系统维护', + description: '系统将在今晚22:00-23:00进行维护', + time: '30分钟前', + status: 'warning' + }, + { + id: '5', + type: 'call', + title: '通话异常', + description: '通话CALL-2024-003出现连接问题', + time: '1小时前', + status: 'error' + } + ]; - // 设置实时数据更新 - const callsChannel = realtime.subscribe( - TABLES.CALLS, - () => { - fetchDashboardData(); + setStats(mockStats); + setActivities(mockActivities); + } catch (error) { + console.error('Failed to load dashboard data:', error); + } finally { + setLoading(false); } - ); - - const interpretersChannel = realtime.subscribe( - TABLES.INTERPRETERS, - () => { - fetchDashboardData(); - } - ); - - // 每30秒刷新一次数据 - const interval = setInterval(fetchDashboardData, 30000); - - return () => { - clearInterval(interval); - realtime.unsubscribe(callsChannel); - realtime.unsubscribe(interpretersChannel); }; - }, [user, router]); + + loadDashboardData(); + }, []); + + const getStatusColor = (status: string) => { + switch (status) { + case 'success': + return 'text-green-600 bg-green-100'; + case 'warning': + return 'text-yellow-600 bg-yellow-100'; + case 'error': + return 'text-red-600 bg-red-100'; + default: + return 'text-blue-600 bg-blue-100'; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'success': + return ; + case 'warning': + return ; + case 'error': + return ; + default: + return ; + } + }; if (loading) { return ( - -
-
+ +
+
- +
); } return ( - - - 仪表盘 - 口译服务管理后台 - + +
+ {/* 欢迎区域 */} +
+

欢迎回来!

+

+ 这里是您的管理仪表板,查看最新的业务数据和活动。 +

+
- {/* 主要内容区域 */} -
-
- {/* 统计卡片 */} -
-
-
-
-
- -
-
-
-
- 今日通话总量 -
-
- {stats.total_calls_today} -
-
-
+ {/* 统计卡片 */} +
+
+
+
+
+ +
+
+
+
总用户数
+
+
{stats?.totalUsers || 0}
+
+ + 增加了 + 12% +
+
+
- -
-
-
-
- -
-
-
-
- 当前活跃通话 -
-
- {stats.active_calls} -
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- 在线翻译员 -
-
- {stats.online_interpreters} -
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- 今日收入 -
-
- {formatCurrency(stats.total_revenue_today, 'CNY')} -
-
-
-
+
+
+ 活跃用户: + {stats?.activeUsers || 0}
- {/* 主要内容区域 */} -
- {/* 活跃通话列表 */} -
-
-
-

- 实时通话列表 -

-
- {activeCalls.length === 0 ? ( -

- 当前没有活跃通话 -

- ) : ( - activeCalls.map((call) => ( -
-
-
-
-
-
-
-

- {getCallModeText(call.call_mode)} -

-

- {formatTime(call.start_time)} -

-
-
-
-
- - {getCallStatusText(call.status)} - -
- - -
-
-
- )) - )} -
+
+
+
+
+ +
+
+
+
总通话数
+
+
{stats?.totalCalls || 0}
+
+ + 增加了 + 8% +
+
+
+
+
+ 进行中: + {stats?.activeCalls || 0} +
+
+
- {/* 在线翻译员 */} -
-
-
-

- 在线翻译员 -

-
- {onlineInterpreters.length === 0 ? ( -

- 暂无翻译员在线 -

- ) : ( - onlineInterpreters.slice(0, 5).map((interpreter) => ( -
-
- {interpreter.name} -
-

- {interpreter.name} -

-

- 评分: {interpreter.rating}/5 -

-
-
-
-
- 在线 -
-
- )) - )} -
+
+
+
+
+
+
+
+
总订单数
+
+
{stats?.totalOrders || 0}
+
+ + 增加了 + 15% +
+
+
+
+
+
+
+
+ 待处理: + {stats?.pendingOrders || 0} +
+
+
+ +
+
+
+
+ +
+
+
+
总收入
+
+
¥{stats?.totalRevenue?.toLocaleString() || 0}
+
+ + 增加了 + 22% +
+
+
+
+
+
+
+
+ 本月: + ¥{stats?.monthlyRevenue?.toLocaleString() || 0} +
+
+
+
+ + {/* 最近活动和快速操作 */} +
+ {/* 最近活动 */} +
+
+

最近活动

+
+ {activities.map((activity) => ( +
+
+ {getStatusIcon(activity.status)} +
+
+
{activity.title}
+
{activity.description}
+
{activity.time}
+
+
+ {activity.status === 'success' && '成功'} + {activity.status === 'warning' && '警告'} + {activity.status === 'error' && '错误'} + {activity.status === 'info' && '信息'} +
+
+ ))} +
+
+ +
+
+
+ + {/* 快速操作 */} +
+
+

快速操作

+
+ + + + + + +
- + ); } \ No newline at end of file diff --git a/pages/dashboard/interpreters.tsx b/pages/dashboard/interpreters.tsx index 8ea1696..59f3030 100644 --- a/pages/dashboard/interpreters.tsx +++ b/pages/dashboard/interpreters.tsx @@ -1,358 +1,739 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { toast } from 'react-hot-toast'; import { - MagnifyingGlassIcon, UserIcon, + MagnifyingGlassIcon, + PlusIcon, + EyeIcon, + PencilIcon, + TrashIcon, StarIcon, - ChevronLeftIcon, - ChevronRightIcon + LanguageIcon, + ClockIcon, + CheckCircleIcon, + XCircleIcon, + ExclamationTriangleIcon, + ArrowDownTrayIcon, + UserPlusIcon, + PhoneIcon, + EnvelopeIcon, + MapPinIcon, + CalendarIcon, + CurrencyDollarIcon, + AcademicCapIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { Interpreter } from '@/types'; -import { formatTime } from '@/utils'; -import Layout from '@/components/Layout'; +import { getDemoData } from '../../lib/demo-data'; +import { formatTime } from '../../lib/utils'; + +interface Interpreter { + id: string; + name: string; + email: string; + phone: string; + avatar?: string; + languages: string[]; + specialties: string[]; + experience_years: number; + rating: number; + total_calls: number; + total_hours: number; + hourly_rate: number; + status: 'active' | 'inactive' | 'busy' | 'offline'; + location: string; + certifications: string[]; + bio: string; + joined_at: string; + last_active: string; + availability: 'available' | 'busy' | 'offline'; +} interface InterpreterFilters { search: string; - status: 'all' | 'online' | 'busy' | 'offline'; - sortBy: 'created_at' | 'name' | 'rating'; - sortOrder: 'asc' | 'desc'; + status: string; + language: string; + rating: string; + availability: string; } -export default function InterpretersPage() { +export default function Interpreters() { + const router = useRouter(); const [interpreters, setInterpreters] = useState([]); const [loading, setLoading] = useState(true); + const [selectedInterpreters, setSelectedInterpreters] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); const [filters, setFilters] = useState({ search: '', - status: 'all', - sortBy: 'created_at', - sortOrder: 'desc' + status: '', + language: '', + rating: '', + availability: '' }); - const router = useRouter(); - const pageSize = 20; + const pageSize = 10; - // 获取翻译员列表 - const fetchInterpreters = async (page = 1) => { + useEffect(() => { + fetchInterpreters(); + }, [currentPage, filters]); + + const fetchInterpreters = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 800)); - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.interpreters(); - // 转换数据格式以匹配 Interpreter 类型 - const formattedResult = result.map(item => ({ - ...item, - user_id: item.id, - specializations: item.specialties || [], + // 使用演示数据 + const mockInterpreters: Interpreter[] = [ + { + id: '1', + name: '王翻译', + email: 'wang@example.com', + phone: '+86 138-0000-0001', + avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + languages: ['中文', '英文', '日文'], + specialties: ['商务翻译', '法律翻译', '技术翻译'], + experience_years: 8, + rating: 4.9, + total_calls: 1250, + total_hours: 3200, hourly_rate: 150, - currency: 'CNY' as const, - total_calls: Math.floor(Math.random() * 100), - is_certified: Math.random() > 0.5, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - })); - setInterpreters(formattedResult); - setTotalCount(formattedResult.length); - setTotalPages(Math.ceil(formattedResult.length / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - let query = supabase - .from(TABLES.INTERPRETERS) - .select('*', { count: 'exact' }); - - // 搜索过滤 - if (filters.search) { - query = query.or(`name.ilike.%${filters.search}%`); + status: 'active', + location: '北京', + certifications: ['CATTI二级', '商务英语高级', 'JLPT N1'], + bio: '资深翻译员,专注于商务和法律翻译,拥有丰富的国际会议翻译经验。', + joined_at: '2020-03-15T10:00:00Z', + last_active: '2024-01-20T15:30:00Z', + availability: 'available' + }, + { + id: '2', + name: '李专家', + email: 'li@example.com', + phone: '+86 138-0000-0002', + avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + languages: ['中文', '韩文', '英文'], + specialties: ['医学翻译', '学术翻译'], + experience_years: 12, + rating: 4.8, + total_calls: 980, + total_hours: 2800, + hourly_rate: 180, + status: 'active', + location: '上海', + certifications: ['医学翻译资格证', 'TOPIK 6级'], + bio: '医学翻译专家,在医疗器械和药物研发领域有深厚的专业背景。', + joined_at: '2019-08-20T10:00:00Z', + last_active: '2024-01-20T14:15:00Z', + availability: 'busy' + }, + { + id: '3', + name: '张语言', + email: 'zhang@example.com', + phone: '+86 138-0000-0003', + avatar: 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + languages: ['中文', '德文', '法文'], + specialties: ['文学翻译', '艺术翻译'], + experience_years: 6, + rating: 4.7, + total_calls: 650, + total_hours: 1800, + hourly_rate: 120, + status: 'active', + location: '广州', + certifications: ['德语C2证书', '法语DALF C1'], + bio: '擅长文学和艺术类翻译,对欧洲文化有深入了解。', + joined_at: '2021-05-10T10:00:00Z', + last_active: '2024-01-19T16:45:00Z', + availability: 'available' + }, + { + id: '4', + name: '陈口译', + email: 'chen@example.com', + phone: '+86 138-0000-0004', + languages: ['中文', '西班牙文', '葡萄牙文'], + specialties: ['旅游翻译', '贸易翻译'], + experience_years: 4, + rating: 4.5, + total_calls: 420, + total_hours: 1200, + hourly_rate: 100, + status: 'inactive', + location: '深圳', + certifications: ['DELE B2', '葡语中级证书'], + bio: '专注于拉美地区的商务和旅游翻译服务。', + joined_at: '2022-01-15T10:00:00Z', + last_active: '2024-01-18T09:30:00Z', + availability: 'offline' + }, + { + id: '5', + name: '刘同传', + email: 'liu@example.com', + phone: '+86 138-0000-0005', + avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + languages: ['中文', '英文', '俄文'], + specialties: ['同声传译', '会议翻译'], + experience_years: 15, + rating: 5.0, + total_calls: 1800, + total_hours: 4500, + hourly_rate: 250, + status: 'active', + location: '北京', + certifications: ['同声传译资格证', '俄语专业八级'], + bio: '顶级同声传译员,曾为多个国际会议提供翻译服务。', + joined_at: '2018-12-01T10:00:00Z', + last_active: '2024-01-20T16:00:00Z', + availability: 'available' + }, + { + id: '6', + name: '赵技术', + email: 'zhao@example.com', + phone: '+86 138-0000-0006', + languages: ['中文', '英文'], + specialties: ['IT翻译', '软件本地化'], + experience_years: 5, + rating: 4.6, + total_calls: 320, + total_hours: 900, + hourly_rate: 130, + status: 'active', + location: '杭州', + certifications: ['计算机技术翻译证书'], + bio: '专业IT翻译,熟悉各种编程语言和技术文档翻译。', + joined_at: '2021-09-01T10:00:00Z', + last_active: '2024-01-20T11:20:00Z', + availability: 'busy' } + ]; - // 状态过滤 - if (filters.status !== 'all') { - query = query.eq('status', filters.status); - } - - // 排序 - query = query.order(filters.sortBy, { ascending: filters.sortOrder === 'asc' }); - - // 分页 - const from = (page - 1) * pageSize; - const to = from + pageSize - 1; - query = query.range(from, to); - - const { data, error, count } = await query; - - if (error) throw error; - - setInterpreters(data || []); - setTotalCount(count || 0); - setTotalPages(Math.ceil((count || 0) / pageSize)); - setCurrentPage(page); + // 应用过滤器 + let filteredInterpreters = mockInterpreters; + + if (filters.search) { + filteredInterpreters = filteredInterpreters.filter(interpreter => + interpreter.name.toLowerCase().includes(filters.search.toLowerCase()) || + interpreter.email.toLowerCase().includes(filters.search.toLowerCase()) || + interpreter.languages.some(lang => lang.toLowerCase().includes(filters.search.toLowerCase())) || + interpreter.specialties.some(spec => spec.toLowerCase().includes(filters.search.toLowerCase())) + ); + } + + if (filters.status) { + filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.status === filters.status); + } + + if (filters.language) { + filteredInterpreters = filteredInterpreters.filter(interpreter => + interpreter.languages.some(lang => lang.toLowerCase().includes(filters.language.toLowerCase())) + ); + } + + if (filters.rating) { + const minRating = parseFloat(filters.rating); + filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.rating >= minRating); + } + + if (filters.availability) { + filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.availability === filters.availability); } + // 分页 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedInterpreters = filteredInterpreters.slice(startIndex, endIndex); + + setInterpreters(paginatedInterpreters); + setTotalCount(filteredInterpreters.length); + setTotalPages(Math.ceil(filteredInterpreters.length / pageSize)); + } catch (error) { - console.error('Error fetching interpreters:', error); - toast.error('获取翻译员列表失败'); - - // 如果真实数据获取失败,切换到演示模式 - if (!isDemoMode) { - setIsDemoMode(true); - const result = await getDemoData.interpreters(); - // 转换数据格式以匹配 Interpreter 类型 - const formattedResult = result.map(item => ({ - ...item, - user_id: item.id, - specializations: item.specialties || [], - hourly_rate: 150, - currency: 'CNY' as const, - total_calls: Math.floor(Math.random() * 100), - is_certified: Math.random() > 0.5, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - })); - setInterpreters(formattedResult); - setTotalCount(formattedResult.length); - setTotalPages(Math.ceil(formattedResult.length / pageSize)); - setCurrentPage(page); - } + console.error('Failed to fetch interpreters:', error); + toast.error('加载翻译员失败'); } finally { setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof InterpreterFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); - }; - - // 应用筛选 - const applyFilters = () => { + const handleSearch = (value: string) => { + setFilters(prev => ({ ...prev, search: value })); setCurrentPage(1); - fetchInterpreters(1); }; - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - status: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); + const handleFilterChange = (key: keyof InterpreterFilters, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); - fetchInterpreters(1); }; - // 获取状态颜色 - const getStatusColor = (status: string) => { - switch (status) { - case 'online': - return 'bg-green-100 text-green-800'; - case 'busy': - return 'bg-yellow-100 text-yellow-800'; - case 'offline': - return 'bg-gray-100 text-gray-800'; - default: - return 'bg-gray-100 text-gray-800'; + const handleSelectInterpreter = (interpreterId: string) => { + setSelectedInterpreters(prev => + prev.includes(interpreterId) + ? prev.filter(id => id !== interpreterId) + : [...prev, interpreterId] + ); + }; + + const handleSelectAll = () => { + if (selectedInterpreters.length === interpreters.length) { + setSelectedInterpreters([]); + } else { + setSelectedInterpreters(interpreters.map(interpreter => interpreter.id)); + } + }; + + const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => { + if (selectedInterpreters.length === 0) { + toast.error('请选择要操作的翻译员'); + return; + } + + try { + const actionText = action === 'activate' ? '激活' : action === 'deactivate' ? '停用' : '删除'; + toast.loading(`正在${actionText}翻译员...`, { id: 'bulk-action' }); + + // 模拟操作延迟 + await new Promise(resolve => setTimeout(resolve, 1500)); + + toast.success(`成功${actionText} ${selectedInterpreters.length} 个翻译员`, { id: 'bulk-action' }); + setSelectedInterpreters([]); + fetchInterpreters(); + } catch (error) { + toast.error('操作失败', { id: 'bulk-action' }); + } + }; + + const handleExport = async () => { + try { + toast.loading('正在导出翻译员数据...', { id: 'export' }); + + // 模拟导出延迟 + await new Promise(resolve => setTimeout(resolve, 2000)); + + toast.success('翻译员数据导出成功', { id: 'export' }); + } catch (error) { + toast.error('导出失败', { id: 'export' }); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return 'text-green-800 bg-green-100'; + case 'inactive': + return 'text-gray-800 bg-gray-100'; + case 'busy': + return 'text-yellow-800 bg-yellow-100'; + case 'offline': + return 'text-red-800 bg-red-100'; + default: + return 'text-gray-800 bg-gray-100'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { - case 'online': - return '在线'; + case 'active': + return '活跃'; + case 'inactive': + return '不活跃'; case 'busy': return '忙碌'; case 'offline': return '离线'; default: - return '未知'; + return status; } }; - useEffect(() => { - fetchInterpreters(); - }, []); + const getAvailabilityColor = (availability: string) => { + switch (availability) { + case 'available': + return 'text-green-600'; + case 'busy': + return 'text-yellow-600'; + case 'offline': + return 'text-red-600'; + default: + return 'text-gray-600'; + } + }; + + const getAvailabilityText = (availability: string) => { + switch (availability) { + case 'available': + return '可接单'; + case 'busy': + return '忙碌中'; + case 'offline': + return '离线'; + default: + return availability; + } + }; + + const renderStars = (rating: number) => { + return ( +
+ {[...Array(5)].map((_, i) => ( + + ))} + {rating.toFixed(1)} +
+ ); + }; return ( - + <> - 翻译员管理 - 口译服务管理后台 + 翻译员管理 - 翻译服务管理系统 - -
-
- {/* 页面标题 */} -
-

翻译员管理

-
- - {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
- handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> -
-
- - {/* 状态筛选 */} -
- -
- - {/* 排序 */} -
- -
-
- -
- - -
-
-
- - {/* 翻译员列表 */} - {loading ? ( -
-
-
- ) : interpreters.length === 0 ? ( -
- -

暂无翻译员

-

- 调整筛选条件或检查数据源 + + +

+ {/* 页面标题和操作 */} +
+
+

翻译员管理

+

+ 管理平台上的所有翻译员,包括其技能、评级和可用性状态。

- ) : ( -
-
-
-

- 翻译员列表 ({totalCount} 个翻译员) -

-
+
+ + +
+
-
- {interpreters.map((interpreter) => ( -
-
-
- {interpreter.name} +
+
+ +
+
+ +
+ handleSearch(e.target.value)} + /> +
+
+ +
+ + +
+ +
+ + handleFilterChange('language', e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* 批量操作 */} + {selectedInterpreters.length > 0 && ( +
+
+ + 已选择 {selectedInterpreters.length} 个翻译员 + +
+ + + +
+
+
+ )} + + {/* 翻译员列表 */} +
+ {loading ? ( +
+
+
+ ) : ( + <> +
+ + + + + + + + + + + + + + + {interpreters.map((interpreter) => ( + + + + + + + + + + + ))} + +
+ 0} + onChange={handleSelectAll} /> -
-

- {interpreter.name} -

-

- {interpreter.languages.join(', ')} -

-

- 专业领域: {interpreter.specializations?.join(', ') || '无'} -

-
- -
-
- - - {interpreter.rating || 0}/5 - -
- - {getStatusText(interpreter.status)} - -
- - - ))} +
+ 翻译员 + + 语言/专业 + + 评级/统计 + + 费率/经验 + + 状态 + + 最后活跃 + + 操作 +
+ handleSelectInterpreter(interpreter.id)} + /> + +
+
+ {interpreter.avatar ? ( + + ) : ( +
+ +
+ )} +
+
+
{interpreter.name}
+
+ + {interpreter.email} +
+
+ + {interpreter.phone} +
+
+
+
+
+
+ + {interpreter.languages.join(', ')} +
+
+ + {interpreter.specialties.join(', ')} +
+
+ + {interpreter.location} +
+
+
+
+ {renderStars(interpreter.rating)} +
+ {interpreter.total_calls} 通话 +
+
+ {interpreter.total_hours} 小时 +
+
+
+
+
+ + ¥{interpreter.hourly_rate}/小时 +
+
+ {interpreter.experience_years} 年经验 +
+
+
+
+ + {getStatusText(interpreter.status)} + +
+ {getAvailabilityText(interpreter.availability)} +
+
+
+
+ + {formatTime(interpreter.last_active)} +
+
+
+ + + +
+
{/* 分页 */} {totalPages > 1 && ( -
+
@@ -361,54 +742,49 @@ export default function InterpretersPage() {

显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 条,共 {totalCount} 条记录 + {Math.min(currentPage * pageSize, totalCount)} 项, + 共 {totalCount}

)} -
-
- )} + + )} +
-
- + + ); } \ No newline at end of file diff --git a/pages/dashboard/invoices.tsx b/pages/dashboard/invoices.tsx index 1ff0c08..cf3b49f 100644 --- a/pages/dashboard/invoices.tsx +++ b/pages/dashboard/invoices.tsx @@ -1,486 +1,745 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { toast } from 'react-hot-toast'; import { MagnifyingGlassIcon, - ReceiptPercentIcon, - CloudArrowDownIcon, + PlusIcon, EyeIcon, - PrinterIcon, + PencilIcon, + TrashIcon, DocumentTextIcon, - CheckCircleIcon, - ClockIcon, - ExclamationCircleIcon, - ChevronLeftIcon, - ChevronRightIcon, - DocumentArrowDownIcon, + ArrowDownTrayIcon, + PrinterIcon, + CurrencyDollarIcon, CalendarIcon, + CheckCircleIcon, + XCircleIcon, + ClockIcon, + ExclamationTriangleIcon, + UserIcon, BuildingOfficeIcon, - CurrencyDollarIcon + EnvelopeIcon, + PhoneIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { formatTime, formatCurrency } from '@/utils'; -import Layout from '@/components/Layout'; +import { getDemoData } from '../../lib/demo-data'; +import { formatTime } from '../../lib/utils'; interface Invoice { id: string; invoice_number: string; - user_id: string; + order_id: string; + order_number: string; user_name: string; user_email: string; - order_id: string; - invoice_type: 'individual' | 'enterprise'; - // 个人发票信息 - personal_name?: string; - // 企业发票信息 - company_name?: string; - tax_number?: string; - company_address?: string; - company_phone?: string; - bank_name?: string; - bank_account?: string; - // 发票金额信息 - subtotal: number; + user_company?: string; + user_phone?: string; + interpreter_name: string; + service_type: string; + service_description: string; + amount: number; tax_amount: number; total_amount: number; - currency: string; - // 发票状态 - status: 'draft' | 'issued' | 'paid' | 'cancelled'; - issue_date?: string; - due_date?: string; + status: 'draft' | 'sent' | 'paid' | 'overdue' | 'cancelled'; + issue_date: string; + due_date: string; paid_date?: string; - // 服务明细 - items: InvoiceItem[]; + notes?: string; created_at: string; updated_at: string; } -interface InvoiceItem { - service_type: string; - service_name: string; - quantity: number; - unit: string; - unit_price: number; - amount: number; -} - interface InvoiceFilters { search: string; - status: 'all' | 'draft' | 'issued' | 'paid' | 'cancelled'; - invoice_type: 'all' | 'individual' | 'enterprise'; - date_range: 'all' | 'today' | 'week' | 'month' | 'quarter'; - sortBy: 'created_at' | 'total_amount' | 'issue_date'; - sortOrder: 'asc' | 'desc'; + status: string; + date_range: string; + amount_range: string; } -export default function InvoicesPage() { +export default function Invoices() { + const router = useRouter(); const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); + const [selectedInvoices, setSelectedInvoices] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); const [filters, setFilters] = useState({ search: '', - status: 'all', - invoice_type: 'all', - date_range: 'all', - sortBy: 'created_at', - sortOrder: 'desc' + status: '', + date_range: '', + amount_range: '' }); - const router = useRouter(); - const pageSize = 20; + const pageSize = 10; - // 获取发票数据 - const fetchInvoices = async (page = 1) => { + useEffect(() => { + fetchInvoices(); + }, [currentPage, filters]); + + const fetchInvoices = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 800)); - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.invoices(); - setInvoices(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - 这里需要根据实际数据库结构调整 - // 暂时使用演示数据 - const result = await getDemoData.invoices(); - setInvoices(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); + // 使用演示数据 + const mockInvoices: Invoice[] = [ + { + id: '1', + invoice_number: 'INV-2024-001', + order_id: '1', + order_number: 'ORD-2024-001', + user_name: '张先生', + user_email: 'zhang@example.com', + user_company: '北京科技有限公司', + user_phone: '13800138001', + interpreter_name: '王翻译', + service_type: '视频通话翻译', + service_description: '商务会议翻译服务(中英互译)', + amount: 450, + tax_amount: 27, + total_amount: 477, + status: 'paid', + issue_date: '2024-01-20T00:00:00Z', + due_date: '2024-02-19T00:00:00Z', + paid_date: '2024-01-25T00:00:00Z', + notes: '已按时完成支付', + created_at: '2024-01-20T10:00:00Z', + updated_at: '2024-01-25T14:30:00Z' + }, + { + id: '2', + invoice_number: 'INV-2024-002', + order_id: '2', + order_number: 'ORD-2024-002', + user_name: '李女士', + user_email: 'li@example.com', + user_company: '上海医疗中心', + user_phone: '13800138002', + interpreter_name: '陈口译', + service_type: '语音通话翻译', + service_description: '医疗咨询翻译服务(中日互译)', + amount: 300, + tax_amount: 18, + total_amount: 318, + status: 'sent', + issue_date: '2024-01-21T00:00:00Z', + due_date: '2024-02-20T00:00:00Z', + notes: '等待客户支付', + created_at: '2024-01-21T09:00:00Z', + updated_at: '2024-01-21T09:00:00Z' + }, + { + id: '3', + invoice_number: 'INV-2024-003', + order_id: '3', + order_number: 'ORD-2024-003', + user_name: '王总', + user_email: 'wangzong@example.com', + user_company: '深圳国际贸易公司', + user_phone: '13800138003', + interpreter_name: '刘同传', + service_type: '现场翻译', + service_description: '大型会议同声传译服务(中英互译)', + amount: 1800, + tax_amount: 108, + total_amount: 1908, + status: 'draft', + issue_date: '2024-01-22T00:00:00Z', + due_date: '2024-02-21T00:00:00Z', + notes: '待客户确认后发送', + created_at: '2024-01-22T16:00:00Z', + updated_at: '2024-01-22T16:00:00Z' + }, + { + id: '4', + invoice_number: 'INV-2024-004', + order_id: '4', + order_number: 'ORD-2024-004', + user_name: '赵经理', + user_email: 'zhao@example.com', + user_company: '广州制造业集团', + user_phone: '13800138004', + interpreter_name: '李专家', + service_type: '视频通话翻译', + service_description: '技术交流翻译服务(中韩互译)', + amount: 200, + tax_amount: 12, + total_amount: 212, + status: 'cancelled', + issue_date: '2024-01-20T00:00:00Z', + due_date: '2024-02-19T00:00:00Z', + notes: '订单取消,发票作废', + created_at: '2024-01-20T14:00:00Z', + updated_at: '2024-01-20T16:00:00Z' + }, + { + id: '5', + invoice_number: 'INV-2024-005', + order_id: '5', + order_number: 'ORD-2024-005', + user_name: '孙先生', + user_email: 'sun@example.com', + user_company: '杭州技术服务公司', + user_phone: '13800138005', + interpreter_name: '张语言', + service_type: '语音通话翻译', + service_description: '技术文档咨询翻译服务(中德互译)', + amount: 250, + tax_amount: 15, + total_amount: 265, + status: 'overdue', + issue_date: '2024-01-15T00:00:00Z', + due_date: '2024-01-30T00:00:00Z', + notes: '已逾期,需要催收', + created_at: '2024-01-15T13:00:00Z', + updated_at: '2024-01-31T10:00:00Z' + }, + { + id: '6', + invoice_number: 'INV-2024-006', + order_id: '6', + order_number: 'ORD-2024-006', + user_name: '周女士', + user_email: 'zhou@example.com', + user_company: '成都教育机构', + user_phone: '13800138006', + interpreter_name: '赵技术', + service_type: '视频通话翻译', + service_description: '学术会议翻译服务(中英互译)', + amount: 200, + tax_amount: 12, + total_amount: 212, + status: 'sent', + issue_date: '2024-01-19T00:00:00Z', + due_date: '2024-02-18T00:00:00Z', + notes: '已发送给客户', + created_at: '2024-01-19T11:00:00Z', + updated_at: '2024-01-19T11:00:00Z' + } + ]; + + // 应用过滤器 + let filteredInvoices = mockInvoices; + + if (filters.search) { + filteredInvoices = filteredInvoices.filter(invoice => + invoice.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) || + invoice.order_number.toLowerCase().includes(filters.search.toLowerCase()) || + invoice.user_name.toLowerCase().includes(filters.search.toLowerCase()) || + invoice.user_email.toLowerCase().includes(filters.search.toLowerCase()) || + invoice.user_company?.toLowerCase().includes(filters.search.toLowerCase()) || + invoice.interpreter_name.toLowerCase().includes(filters.search.toLowerCase()) + ); + } + + if (filters.status) { + filteredInvoices = filteredInvoices.filter(invoice => invoice.status === filters.status); } + // 分页 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedInvoices = filteredInvoices.slice(startIndex, endIndex); + + setInvoices(paginatedInvoices); + setTotalCount(filteredInvoices.length); + setTotalPages(Math.ceil(filteredInvoices.length / pageSize)); + } catch (error) { - console.error('Error fetching invoices:', error); - toast.error('获取发票列表失败'); + console.error('Failed to fetch invoices:', error); + toast.error('加载发票失败'); } finally { setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof InvoiceFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); - }; - - // 应用筛选 - const applyFilters = () => { + const handleSearch = (value: string) => { + setFilters(prev => ({ ...prev, search: value })); setCurrentPage(1); - fetchInvoices(1); }; - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - status: 'all', - invoice_type: 'all', - date_range: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); + const handleFilterChange = (key: keyof InvoiceFilters, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); - fetchInvoices(1); }; - // 获取状态颜色 + const handleSelectInvoice = (invoiceId: string) => { + setSelectedInvoices(prev => + prev.includes(invoiceId) + ? prev.filter(id => id !== invoiceId) + : [...prev, invoiceId] + ); + }; + + const handleSelectAll = () => { + if (selectedInvoices.length === invoices.length) { + setSelectedInvoices([]); + } else { + setSelectedInvoices(invoices.map(invoice => invoice.id)); + } + }; + + const handleBulkAction = async (action: 'send' | 'mark_paid' | 'cancel' | 'delete') => { + if (selectedInvoices.length === 0) { + toast.error('请选择要操作的发票'); + return; + } + + try { + const actionText = action === 'send' ? '发送' : action === 'mark_paid' ? '标记已付' : action === 'cancel' ? '取消' : '删除'; + toast.loading(`正在${actionText}发票...`, { id: 'bulk-action' }); + + // 模拟操作延迟 + await new Promise(resolve => setTimeout(resolve, 1500)); + + toast.success(`成功${actionText} ${selectedInvoices.length} 张发票`, { id: 'bulk-action' }); + setSelectedInvoices([]); + fetchInvoices(); + } catch (error) { + toast.error('操作失败', { id: 'bulk-action' }); + } + }; + + const handleExport = async () => { + try { + toast.loading('正在导出发票数据...', { id: 'export' }); + + // 模拟导出延迟 + await new Promise(resolve => setTimeout(resolve, 2000)); + + toast.success('发票数据导出成功', { id: 'export' }); + } catch (error) { + toast.error('导出失败', { id: 'export' }); + } + }; + + const handlePrint = async (invoiceId: string) => { + try { + toast.loading('正在准备打印...', { id: 'print' }); + + // 模拟打印准备延迟 + await new Promise(resolve => setTimeout(resolve, 1000)); + + toast.success('发票已发送到打印机', { id: 'print' }); + } catch (error) { + toast.error('打印失败', { id: 'print' }); + } + }; + const getStatusColor = (status: string) => { switch (status) { case 'paid': - return 'bg-green-100 text-green-800'; - case 'issued': - return 'bg-blue-100 text-blue-800'; + return 'text-green-800 bg-green-100'; + case 'sent': + return 'text-blue-800 bg-blue-100'; case 'draft': - return 'bg-yellow-100 text-yellow-800'; + return 'text-gray-800 bg-gray-100'; + case 'overdue': + return 'text-red-800 bg-red-100'; case 'cancelled': - return 'bg-red-100 text-red-800'; + return 'text-red-800 bg-red-100'; default: - return 'bg-gray-100 text-gray-800'; + return 'text-gray-800 bg-gray-100'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { case 'paid': - return '已付款'; - case 'issued': - return '已开具'; + return '已支付'; + case 'sent': + return '已发送'; case 'draft': return '草稿'; + case 'overdue': + return '逾期'; case 'cancelled': return '已取消'; default: - return '未知'; + return status; } }; - // 获取状态图标 const getStatusIcon = (status: string) => { switch (status) { case 'paid': - return ; - case 'issued': - return ; + return ; + case 'sent': + return ; case 'draft': - return ; + return ; + case 'overdue': + return ; case 'cancelled': - return ; + return ; default: - return ; + return ; } }; - // 下载发票 - const handleDownloadInvoice = async (invoiceId: string) => { - try { - // 这里应该调用实际的发票下载API - toast.success('发票下载已开始'); - - // 模拟下载过程 - const invoice = invoices.find(inv => inv.id === invoiceId); - if (invoice) { - // 创建一个虚拟的下载链接 - const element = document.createElement('a'); - const file = new Blob([`发票号: ${invoice.invoice_number}\n金额: ${formatCurrency(invoice.total_amount)}`], - { type: 'text/plain' }); - element.href = URL.createObjectURL(file); - element.download = `invoice-${invoice.invoice_number}.txt`; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - } - } catch (error) { - console.error('Error downloading invoice:', error); - toast.error('发票下载失败'); - } + const formatCurrency = (amount: number) => { + return `¥${amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`; }; - // 打印发票 - const handlePrintInvoice = (invoiceId: string) => { - try { - // 这里应该打开打印预览 - toast.success('正在准备打印...'); - window.print(); - } catch (error) { - console.error('Error printing invoice:', error); - toast.error('发票打印失败'); - } + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('zh-CN'); }; - // 生成发票 - const handleGenerateInvoice = async (orderId: string) => { - try { - // 这里应该调用生成发票API - toast.success('发票生成成功'); - fetchInvoices(); - } catch (error) { - toast.error('生成发票失败'); - } - }; - - useEffect(() => { - fetchInvoices(); - }, []); - return ( - + <> - 发票管理 - 口译服务管理后台 + 发票管理 - 翻译服务管理系统 - -
-
- {/* 页面标题 */} -
-

发票管理

- + + +
+ {/* 页面标题和操作 */} +
+
+

发票管理

+

+ 管理所有翻译服务发票,包括发票状态、支付跟踪和账单处理。 +

+
+
+ + +
- {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
- handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> + {/* 搜索和过滤器 */} +
+
+
+ +
+
+
-
- - {/* 状态筛选 */} -
- -
- - {/* 类型筛选 */} -
- -
- - {/* 日期范围筛选 */} -
- + handleSearch(e.target.value)} + />
-
-
+ +
+ + +
+ +
+ +
+ {/* 批量操作 */} + {selectedInvoices.length > 0 && ( +
+
+ + 已选择 {selectedInvoices.length} 张发票 + +
+ + + + +
+
+
+ )} + {/* 发票列表 */} - {loading ? ( -
-
-
- ) : ( -
-
-
-

- 发票列表 ({totalCount} 张发票) -

-
- -
- {invoices.map((invoice) => ( -
-
-
-
- -
-

- 发票号: {invoice.invoice_number} -

-

- {invoice.invoice_type === 'enterprise' ? ( - - - {invoice.company_name} - - ) : ( - - - {invoice.personal_name || invoice.user_name} - - )} -

-

- 金额: ¥{invoice.total_amount.toFixed(2)} | 税额: ¥{invoice.tax_amount.toFixed(2)} | - 总计: ¥{invoice.total_amount.toFixed(2)} -

- {invoice.tax_number && ( -

- 税号: {invoice.tax_number} -

+
+ {loading ? ( +
+
+
+ ) : ( + <> +
+ + + + + + + + + + + + + + + {invoices.map((invoice) => ( + + + + + + + + + + + ))} + +
+ 0} + onChange={handleSelectAll} + /> + + 发票信息 + + 客户信息 + + 服务详情 + + 金额 + + 日期 + + 状态 + + 操作 +
+ handleSelectInvoice(invoice.id)} + /> + +
+
+ {invoice.invoice_number} +
+
+ 订单: {invoice.order_number} +
+
+ 创建于 {formatTime(invoice.created_at)} +
+
+
+
+
+ + {invoice.user_name} +
+
+ + {invoice.user_email} +
+ {invoice.user_company && ( +
+ + {invoice.user_company} +
+ )} + {invoice.user_phone && ( +
+ + {invoice.user_phone} +
)}
- - - -
- {/* 状态 */} -
- {getStatusIcon(invoice.status)} - - {getStatusText(invoice.status)} - -
- - {/* 操作按钮 */} -
- - - -
-
- - - {/* 日期信息 */} -
-
创建: {formatTime(invoice.created_at)}
- {invoice.issue_date && ( -
开具: {formatTime(invoice.issue_date)}
- )} - {invoice.paid_date && ( -
付款: {formatTime(invoice.paid_date)}
- )} -
- - ))} - - {invoices.length === 0 && ( -
- -

暂无发票

-

开始生成您的第一张发票

-
- )} +
+
+
+ {invoice.service_type} +
+
+ 翻译员: {invoice.interpreter_name} +
+
+ {invoice.service_description} +
+
+
+
+
+ {formatCurrency(invoice.total_amount)} +
+
+ 服务费: {formatCurrency(invoice.amount)} +
+
+ 税费: {formatCurrency(invoice.tax_amount)} +
+
+
+
+
+ + 开票: {formatDate(invoice.issue_date)} +
+
+ + 到期: {formatDate(invoice.due_date)} +
+ {invoice.paid_date && ( +
+ + 支付: {formatDate(invoice.paid_date)} +
+ )} +
+
+
+ {getStatusIcon(invoice.status)} + + {getStatusText(invoice.status)} + +
+
+
+ + + + +
+
{/* 分页 */} {totalPages > 1 && ( -
+
@@ -489,54 +748,49 @@ export default function InvoicesPage() {

显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 张,共 {totalCount} 张发票 + {Math.min(currentPage * pageSize, totalCount)} 项, + 共 {totalCount}

)} -
-
- )} + + )} +
-
- + + ); } \ No newline at end of file diff --git a/pages/dashboard/orders.tsx b/pages/dashboard/orders.tsx index 3dcc776..0439eff 100644 --- a/pages/dashboard/orders.tsx +++ b/pages/dashboard/orders.tsx @@ -1,180 +1,326 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { toast } from 'react-hot-toast'; import { MagnifyingGlassIcon, + PlusIcon, EyeIcon, + PencilIcon, + TrashIcon, ClockIcon, CheckCircleIcon, XCircleIcon, ExclamationTriangleIcon, - ChevronLeftIcon, - ChevronRightIcon, + ArrowDownTrayIcon, PhoneIcon, VideoCameraIcon, + UserIcon, + LanguageIcon, + CalendarIcon, + CurrencyDollarIcon, DocumentTextIcon, - HandRaisedIcon, - SpeakerWaveIcon + PlayIcon, + PauseIcon, + StopIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { formatTime, formatCurrency } from '@/utils'; -import Layout from '@/components/Layout'; +import { getDemoData } from '../../lib/demo-data'; +import { formatTime } from '../../lib/utils'; interface Order { id: string; order_number: string; - user_id: string; user_name: string; user_email: string; - service_type: 'ai_voice_translation' | 'ai_video_translation' | 'sign_language_translation' | 'human_interpretation' | 'document_translation'; - service_name: string; - source_language: string; - target_language: string; - duration?: number; // 分钟 - pages?: number; // 页数 - status: 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed'; - priority: 'low' | 'normal' | 'high' | 'urgent'; - cost: number; - currency: string; - // 时间信息 - scheduled_at?: string; - started_at?: string; - completed_at?: string; + interpreter_name: string; + language_pair: string; + service_type: 'audio' | 'video' | 'onsite'; + start_time: string; + end_time?: string; + duration?: number; + status: 'pending' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'failed'; + amount: number; + payment_status: 'pending' | 'paid' | 'failed' | 'refunded'; + notes?: string; created_at: string; updated_at: string; - // 额外信息 - notes?: string; - interpreter_id?: string; - interpreter_name?: string; } interface OrderFilters { search: string; - status: 'all' | 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed'; - service_type: 'all' | 'ai_voice_translation' | 'ai_video_translation' | 'sign_language_translation' | 'human_interpretation' | 'document_translation'; - priority: 'all' | 'low' | 'normal' | 'high' | 'urgent'; - date_range: 'all' | 'today' | 'week' | 'month'; - sortBy: 'created_at' | 'cost' | 'scheduled_at'; - sortOrder: 'asc' | 'desc'; + status: string; + service_type: string; + payment_status: string; + date_range: string; } -export default function OrdersPage() { +export default function Orders() { + const router = useRouter(); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); + const [selectedOrders, setSelectedOrders] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); const [filters, setFilters] = useState({ search: '', - status: 'all', - service_type: 'all', - priority: 'all', - date_range: 'all', - sortBy: 'created_at', - sortOrder: 'desc' + status: '', + service_type: '', + payment_status: '', + date_range: '' }); - const router = useRouter(); - const pageSize = 20; + const pageSize = 10; - // 获取订单数据 - const fetchOrders = async (page = 1) => { + useEffect(() => { + fetchOrders(); + }, [currentPage, filters]); + + const fetchOrders = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 800)); - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.orders(); - setOrders(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - 这里需要根据实际数据库结构调整 - // 暂时使用演示数据 - const result = await getDemoData.orders(); - setOrders(result); - setTotalCount(result.length); - setTotalPages(Math.ceil(result.length / pageSize)); - setCurrentPage(page); + // 使用演示数据 + const mockOrders: Order[] = [ + { + id: '1', + order_number: 'ORD-2024-001', + user_name: '张先生', + user_email: 'zhang@example.com', + interpreter_name: '王翻译', + language_pair: '中文 ↔ 英文', + service_type: 'video', + start_time: '2024-01-20T14:00:00Z', + end_time: '2024-01-20T15:30:00Z', + duration: 90, + status: 'completed', + amount: 450, + payment_status: 'paid', + notes: '商务会议翻译,需要专业术语支持', + created_at: '2024-01-19T10:00:00Z', + updated_at: '2024-01-20T15:30:00Z' + }, + { + id: '2', + order_number: 'ORD-2024-002', + user_name: '李女士', + user_email: 'li@example.com', + interpreter_name: '陈口译', + language_pair: '中文 ↔ 日文', + service_type: 'audio', + start_time: '2024-01-21T09:00:00Z', + status: 'confirmed', + amount: 300, + payment_status: 'paid', + notes: '医疗咨询翻译', + created_at: '2024-01-20T08:00:00Z', + updated_at: '2024-01-20T08:30:00Z' + }, + { + id: '3', + order_number: 'ORD-2024-003', + user_name: '王总', + user_email: 'wangzong@example.com', + interpreter_name: '刘同传', + language_pair: '中文 ↔ 英文', + service_type: 'onsite', + start_time: '2024-01-22T10:00:00Z', + end_time: '2024-01-22T16:00:00Z', + duration: 360, + status: 'in_progress', + amount: 1800, + payment_status: 'paid', + notes: '大型会议同声传译', + created_at: '2024-01-18T15:00:00Z', + updated_at: '2024-01-22T10:00:00Z' + }, + { + id: '4', + order_number: 'ORD-2024-004', + user_name: '赵经理', + user_email: 'zhao@example.com', + interpreter_name: '李专家', + language_pair: '中文 ↔ 韩文', + service_type: 'video', + start_time: '2024-01-20T16:00:00Z', + status: 'cancelled', + amount: 200, + payment_status: 'refunded', + notes: '客户临时取消', + created_at: '2024-01-19T12:00:00Z', + updated_at: '2024-01-20T14:00:00Z' + }, + { + id: '5', + order_number: 'ORD-2024-005', + user_name: '孙先生', + user_email: 'sun@example.com', + interpreter_name: '张语言', + language_pair: '中文 ↔ 德文', + service_type: 'audio', + start_time: '2024-01-23T13:00:00Z', + status: 'pending', + amount: 250, + payment_status: 'pending', + notes: '技术文档翻译咨询', + created_at: '2024-01-20T16:00:00Z', + updated_at: '2024-01-20T16:00:00Z' + }, + { + id: '6', + order_number: 'ORD-2024-006', + user_name: '周女士', + user_email: 'zhou@example.com', + interpreter_name: '赵技术', + language_pair: '中文 ↔ 英文', + service_type: 'video', + start_time: '2024-01-19T11:00:00Z', + end_time: '2024-01-19T12:00:00Z', + duration: 60, + status: 'failed', + amount: 200, + payment_status: 'failed', + notes: '技术问题导致服务中断', + created_at: '2024-01-18T20:00:00Z', + updated_at: '2024-01-19T12:00:00Z' + } + ]; + + // 应用过滤器 + let filteredOrders = mockOrders; + + if (filters.search) { + filteredOrders = filteredOrders.filter(order => + order.order_number.toLowerCase().includes(filters.search.toLowerCase()) || + order.user_name.toLowerCase().includes(filters.search.toLowerCase()) || + order.user_email.toLowerCase().includes(filters.search.toLowerCase()) || + order.interpreter_name.toLowerCase().includes(filters.search.toLowerCase()) || + order.language_pair.toLowerCase().includes(filters.search.toLowerCase()) + ); + } + + if (filters.status) { + filteredOrders = filteredOrders.filter(order => order.status === filters.status); + } + + if (filters.service_type) { + filteredOrders = filteredOrders.filter(order => order.service_type === filters.service_type); + } + + if (filters.payment_status) { + filteredOrders = filteredOrders.filter(order => order.payment_status === filters.payment_status); } + // 分页 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedOrders = filteredOrders.slice(startIndex, endIndex); + + setOrders(paginatedOrders); + setTotalCount(filteredOrders.length); + setTotalPages(Math.ceil(filteredOrders.length / pageSize)); + } catch (error) { - console.error('Error fetching orders:', error); - toast.error('获取订单数据失败'); + console.error('Failed to fetch orders:', error); + toast.error('加载订单失败'); } finally { setLoading(false); } }; - useEffect(() => { - fetchOrders(1); - }, []); - - // 处理筛选变更 - const handleFilterChange = (key: keyof OrderFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); - }; - - // 应用筛选 - const applyFilters = () => { + const handleSearch = (value: string) => { + setFilters(prev => ({ ...prev, search: value })); setCurrentPage(1); - fetchOrders(1); }; - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - status: 'all', - service_type: 'all', - priority: 'all', - date_range: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); + const handleFilterChange = (key: keyof OrderFilters, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); - fetchOrders(1); }; - // 获取状态颜色 - const getStatusColor = (status: string) => { - switch (status) { - case 'pending': - return 'bg-yellow-100 text-yellow-800'; - case 'processing': - return 'bg-blue-100 text-blue-800'; - case 'completed': - return 'bg-green-100 text-green-800'; - case 'cancelled': - return 'bg-gray-100 text-gray-800'; - case 'failed': - return 'bg-red-100 text-red-800'; - default: - return 'bg-gray-100 text-gray-800'; + const handleSelectOrder = (orderId: string) => { + setSelectedOrders(prev => + prev.includes(orderId) + ? prev.filter(id => id !== orderId) + : [...prev, orderId] + ); + }; + + const handleSelectAll = () => { + if (selectedOrders.length === orders.length) { + setSelectedOrders([]); + } else { + setSelectedOrders(orders.map(order => order.id)); + } + }; + + const handleBulkAction = async (action: 'confirm' | 'cancel' | 'delete') => { + if (selectedOrders.length === 0) { + toast.error('请选择要操作的订单'); + return; + } + + try { + const actionText = action === 'confirm' ? '确认' : action === 'cancel' ? '取消' : '删除'; + toast.loading(`正在${actionText}订单...`, { id: 'bulk-action' }); + + // 模拟操作延迟 + await new Promise(resolve => setTimeout(resolve, 1500)); + + toast.success(`成功${actionText} ${selectedOrders.length} 个订单`, { id: 'bulk-action' }); + setSelectedOrders([]); + fetchOrders(); + } catch (error) { + toast.error('操作失败', { id: 'bulk-action' }); + } + }; + + const handleExport = async () => { + try { + toast.loading('正在导出订单数据...', { id: 'export' }); + + // 模拟导出延迟 + await new Promise(resolve => setTimeout(resolve, 2000)); + + toast.success('订单数据导出成功', { id: 'export' }); + } catch (error) { + toast.error('导出失败', { id: 'export' }); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'completed': + return 'text-green-800 bg-green-100'; + case 'confirmed': + return 'text-blue-800 bg-blue-100'; + case 'in_progress': + return 'text-yellow-800 bg-yellow-100'; + case 'pending': + return 'text-gray-800 bg-gray-100'; + case 'cancelled': + return 'text-red-800 bg-red-100'; + case 'failed': + return 'text-red-800 bg-red-100'; + default: + return 'text-gray-800 bg-gray-100'; } }; - // 获取状态文本 const getStatusText = (status: string) => { switch (status) { - case 'pending': - return '待处理'; - case 'processing': - return '处理中'; case 'completed': return '已完成'; + case 'confirmed': + return '已确认'; + case 'in_progress': + return '进行中'; + case 'pending': + return '待确认'; case 'cancelled': return '已取消'; case 'failed': @@ -184,300 +330,464 @@ export default function OrdersPage() { } }; - // 获取优先级颜色 - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'urgent': - return 'bg-red-100 text-red-800'; - case 'high': - return 'bg-orange-100 text-orange-800'; - case 'normal': - return 'bg-blue-100 text-blue-800'; - case 'low': - return 'bg-gray-100 text-gray-800'; + const getPaymentStatusColor = (status: string) => { + switch (status) { + case 'paid': + return 'text-green-800 bg-green-100'; + case 'pending': + return 'text-yellow-800 bg-yellow-100'; + case 'failed': + return 'text-red-800 bg-red-100'; + case 'refunded': + return 'text-purple-800 bg-purple-100'; default: - return 'bg-gray-100 text-gray-800'; + return 'text-gray-800 bg-gray-100'; } }; - // 获取优先级文本 - const getPriorityText = (priority: string) => { - switch (priority) { - case 'urgent': - return '紧急'; - case 'high': - return '高'; - case 'normal': - return '普通'; - case 'low': - return '低'; + const getPaymentStatusText = (status: string) => { + switch (status) { + case 'paid': + return '已支付'; + case 'pending': + return '待支付'; + case 'failed': + return '支付失败'; + case 'refunded': + return '已退款'; default: - return priority; + return status; } }; - // 获取服务类型图标 - const getServiceIcon = (serviceType: string) => { - switch (serviceType) { - case 'ai_voice_translation': - return ; - case 'ai_video_translation': - return ; - case 'sign_language_translation': - return ; - case 'human_interpretation': - return ; - case 'document_translation': - return ; + const getServiceTypeIcon = (type: string) => { + switch (type) { + case 'audio': + return ; + case 'video': + return ; + case 'onsite': + return ; default: - return ; + return ; } }; + const getServiceTypeText = (type: string) => { + switch (type) { + case 'audio': + return '语音通话'; + case 'video': + return '视频通话'; + case 'onsite': + return '现场翻译'; + default: + return type; + } + }; + + const formatDuration = (minutes?: number) => { + if (!minutes) return '-'; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}小时${mins}分钟`; + } + return `${mins}分钟`; + }; + return ( - + <> - 订单管理 - 口译服务管理系统 + 订单管理 - 翻译服务管理系统 - -
-
-
-

订单管理

-

- 管理所有口译服务订单 -

+ + +
+ {/* 页面标题和操作 */} +
+
+

订单管理

+

+ 管理所有翻译服务订单,包括订单状态、支付状态和服务详情。 +

+
+
+ + +
-
- {/* 搜索和筛选 */} -
-
-
-
- - handleFilterChange('search', e.target.value)} - /> + {/* 搜索和过滤器 */} +
+
+
+ +
+
+ +
+ handleSearch(e.target.value)} + /> +
- - - - - - - - -
-
+ +
+ + +
+ +
+ + +
+ +
+ +
-
- {/* 订单列表 */} -
- {loading ? ( -
-
-

加载中...

-
- ) : ( -
- - - - - - - - - - - - - - - {orders.map((order) => ( - - - - - - - - - - - ))} - -
- 订单信息 - - 客户信息 - - 服务详情 - - 状态 - - 优先级 - - 费用 - - 时间 - - 操作 -
-
-
- {order.order_number} -
-
- {getServiceIcon(order.service_type)} - {order.service_name} -
-
-
-
-
- {order.user_name} -
-
{order.user_email}
-
-
-
-
- {order.source_language} → {order.target_language} -
-
- {order.duration && `时长: ${order.duration}分钟`} - {order.pages && `页数: ${order.pages}页`} -
- {order.interpreter_name && ( -
- 译员: {order.interpreter_name} -
- )} -
-
- - {getStatusText(order.status)} - - - - {getPriorityText(order.priority)} - - -
- {formatCurrency(order.cost)} -
-
-
-
创建: {formatTime(order.created_at)}
- {order.scheduled_at && ( -
预约: {formatTime(order.scheduled_at)}
- )} - {order.completed_at && ( -
完成: {formatTime(order.completed_at)}
- )} -
-
- -
+ {/* 批量操作 */} + {selectedOrders.length > 0 && ( +
+
+ + 已选择 {selectedOrders.length} 个订单 + +
+ + + +
+
)} -
- {/* 分页 */} - {totalPages > 1 && ( -
-
- 显示第 {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalCount)} 条,共 {totalCount} 条 -
-
- - - {currentPage} / {totalPages} - - -
+ {/* 订单列表 */} +
+ {loading ? ( +
+
+
+ ) : ( + <> +
+ + + + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + + + ))} + +
+ 0} + onChange={handleSelectAll} + /> + + 订单信息 + + 用户/翻译员 + + 服务详情 + + 时间/时长 + + 状态 + + 金额 + + 操作 +
+ handleSelectOrder(order.id)} + /> + +
+
+ {order.order_number} +
+
+ 创建于 {formatTime(order.created_at)} +
+ {order.notes && ( +
+ 备注: {order.notes} +
+ )} +
+
+
+
+
+ 用户: {order.user_name} +
+
+ {order.user_email} +
+
+
+
+ 翻译员: {order.interpreter_name} +
+
+
+
+
+
+ {getServiceTypeIcon(order.service_type)} + {getServiceTypeText(order.service_type)} +
+
+ + {order.language_pair} +
+
+
+
+
+ + {new Date(order.start_time).toLocaleString('zh-CN')} +
+
+ + {formatDuration(order.duration)} +
+
+
+
+ + {getStatusText(order.status)} + +
+ + {getPaymentStatusText(order.payment_status)} + +
+
+
+
+ + ¥{order.amount} +
+
+
+ + + +
+
+
+ + {/* 分页 */} + {totalPages > 1 && ( +
+
+ + +
+
+
+

+ 显示第 {(currentPage - 1) * pageSize + 1} 到{' '} + {Math.min(currentPage * pageSize, totalCount)} 项, + 共 {totalCount} 项 +

+
+
+ +
+
+
+ )} + + )}
- )} -
- +
+ + ); } \ No newline at end of file diff --git a/pages/dashboard/users.tsx b/pages/dashboard/users.tsx index 7ac71d7..f6c86b6 100644 --- a/pages/dashboard/users.tsx +++ b/pages/dashboard/users.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; +import DashboardLayout from '../../components/Layout/DashboardLayout'; import { toast } from 'react-hot-toast'; import { MagnifyingGlassIcon, @@ -9,306 +10,601 @@ import { TrashIcon, EyeIcon, UserIcon, + BuildingOfficeIcon, + PhoneIcon, + EnvelopeIcon, + CalendarIcon, CheckCircleIcon, XCircleIcon, - ChevronLeftIcon, - ChevronRightIcon + ExclamationTriangleIcon, + ArrowDownTrayIcon, + FunnelIcon } from '@heroicons/react/24/outline'; -import { supabase, TABLES } from '@/lib/supabase'; -import { getDemoData } from '@/lib/demo-data'; -import { User } from '@/types'; -import { formatTime } from '@/utils'; -import Layout from '@/components/Layout'; +import { getDemoData } from '../../lib/demo-data'; +import { formatTime } from '../../lib/utils'; -// 添加用户状态文本函数 -const getUserStatusText = (isActive: boolean): string => { - return isActive ? '活跃' : '非活跃'; -}; +interface User { + id: string; + name: string; + email: string; + phone: string; + company: string; + role: 'admin' | 'user' | 'interpreter'; + status: 'active' | 'inactive' | 'pending'; + created_at: string; + last_login: string; + total_calls: number; + total_spent: number; +} interface UserFilters { search: string; - userType: 'all' | 'individual' | 'enterprise'; - status: 'all' | 'active' | 'inactive'; - sortBy: 'created_at' | 'full_name' | 'last_login'; - sortOrder: 'asc' | 'desc'; + role: string; + status: string; + company: string; } -export default function UsersPage() { +export default function Users() { + const router = useRouter(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [selectedUsers, setSelectedUsers] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); - const [isDemoMode, setIsDemoMode] = useState(false); const [filters, setFilters] = useState({ search: '', - userType: 'all', - status: 'all', - sortBy: 'created_at', - sortOrder: 'desc' + role: '', + status: '', + company: '' }); - const router = useRouter(); - const pageSize = 20; + const pageSize = 10; - // 获取用户列表 - const fetchUsers = async (page = 1) => { + useEffect(() => { + fetchUsers(); + }, [currentPage, filters]); + + const fetchUsers = async () => { try { setLoading(true); - // 检查是否为演示模式 - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === ''; - setIsDemoMode(isDemo); + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 800)); - if (isDemo) { - // 使用演示数据 - const result = await getDemoData.users(filters); - setUsers(result.data); - setTotalCount(result.total); - setTotalPages(Math.ceil(result.total / pageSize)); - setCurrentPage(page); - } else { - // 使用真实数据 - let query = supabase - .from(TABLES.USERS) - .select('*', { count: 'exact' }); - - // 搜索过滤 - if (filters.search) { - query = query.or(`full_name.ilike.%${filters.search}%,email.ilike.%${filters.search}%`); + // 使用演示数据 + const mockUsers: User[] = [ + { + id: '1', + name: '张三', + email: 'zhangsan@example.com', + phone: '13800138001', + company: 'ABC科技有限公司', + role: 'user', + status: 'active', + created_at: '2024-01-15T10:30:00Z', + last_login: '2024-01-20T14:25:00Z', + total_calls: 25, + total_spent: 1250 + }, + { + id: '2', + name: '李四', + email: 'lisi@example.com', + phone: '13800138002', + company: 'XYZ贸易公司', + role: 'user', + status: 'active', + created_at: '2024-01-10T09:15:00Z', + last_login: '2024-01-19T16:45:00Z', + total_calls: 18, + total_spent: 890 + }, + { + id: '3', + name: '王五', + email: 'wangwu@example.com', + phone: '13800138003', + company: '翻译服务中心', + role: 'interpreter', + status: 'active', + created_at: '2024-01-05T11:20:00Z', + last_login: '2024-01-20T10:30:00Z', + total_calls: 156, + total_spent: 0 + }, + { + id: '4', + name: '赵六', + email: 'zhaoliu@example.com', + phone: '13800138004', + company: '管理员', + role: 'admin', + status: 'active', + created_at: '2024-01-01T08:00:00Z', + last_login: '2024-01-20T18:00:00Z', + total_calls: 5, + total_spent: 0 + }, + { + id: '5', + name: '孙七', + email: 'sunqi@example.com', + phone: '13800138005', + company: '新用户公司', + role: 'user', + status: 'pending', + created_at: '2024-01-18T15:30:00Z', + last_login: '', + total_calls: 0, + total_spent: 0 + }, + { + id: '6', + name: '周八', + email: 'zhouba@example.com', + phone: '13800138006', + company: '暂停用户公司', + role: 'user', + status: 'inactive', + created_at: '2024-01-12T13:45:00Z', + last_login: '2024-01-15T09:20:00Z', + total_calls: 8, + total_spent: 320 } + ]; - // 状态过滤 - if (filters.status !== 'all') { - const isActive = filters.status === 'active'; - query = query.eq('is_active', isActive); - } - - // 排序 - query = query.order(filters.sortBy, { ascending: filters.sortOrder === 'asc' }); - - // 分页 - const from = (page - 1) * pageSize; - const to = from + pageSize - 1; - query = query.range(from, to); - - const { data, error, count } = await query; - - if (error) throw error; - - setUsers(data || []); - setTotalCount(count || 0); - setTotalPages(Math.ceil((count || 0) / pageSize)); - setCurrentPage(page); + // 应用过滤器 + let filteredUsers = mockUsers; + + if (filters.search) { + filteredUsers = filteredUsers.filter(user => + user.name.toLowerCase().includes(filters.search.toLowerCase()) || + user.email.toLowerCase().includes(filters.search.toLowerCase()) || + user.company.toLowerCase().includes(filters.search.toLowerCase()) + ); + } + + if (filters.role) { + filteredUsers = filteredUsers.filter(user => user.role === filters.role); + } + + if (filters.status) { + filteredUsers = filteredUsers.filter(user => user.status === filters.status); + } + + if (filters.company) { + filteredUsers = filteredUsers.filter(user => + user.company.toLowerCase().includes(filters.company.toLowerCase()) + ); } + // 分页 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedUsers = filteredUsers.slice(startIndex, endIndex); + + setUsers(paginatedUsers); + setTotalCount(filteredUsers.length); + setTotalPages(Math.ceil(filteredUsers.length / pageSize)); + } catch (error) { - console.error('Error fetching users:', error); - toast.error('获取用户列表失败'); - - // 如果真实数据获取失败,切换到演示模式 - if (!isDemoMode) { - setIsDemoMode(true); - const result = await getDemoData.users(filters); - setUsers(result.data); - setTotalCount(result.total); - setTotalPages(Math.ceil(result.total / pageSize)); - setCurrentPage(page); - } + console.error('Failed to fetch users:', error); + toast.error('加载用户数据失败'); } finally { setLoading(false); } }; - // 处理筛选变更 - const handleFilterChange = (key: keyof UserFilters, value: any) => { - setFilters(prev => ({ - ...prev, - [key]: value - })); - }; - - // 应用筛选 - const applyFilters = () => { + const handleSearch = (value: string) => { + setFilters(prev => ({ ...prev, search: value })); setCurrentPage(1); - fetchUsers(1); }; - // 重置筛选 - const resetFilters = () => { - setFilters({ - search: '', - userType: 'all', - status: 'all', - sortBy: 'created_at', - sortOrder: 'desc' - }); + const handleFilterChange = (key: keyof UserFilters, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); - fetchUsers(1); }; - useEffect(() => { - fetchUsers(); - }, []); + const handleSelectUser = (userId: string) => { + setSelectedUsers(prev => + prev.includes(userId) + ? prev.filter(id => id !== userId) + : [...prev, userId] + ); + }; + + const handleSelectAll = () => { + if (selectedUsers.length === users.length) { + setSelectedUsers([]); + } else { + setSelectedUsers(users.map(user => user.id)); + } + }; + + const handleBulkAction = async (action: string) => { + if (selectedUsers.length === 0) { + toast.error('请选择要操作的用户'); + return; + } + + try { + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 1000)); + + switch (action) { + case 'activate': + toast.success(`已激活 ${selectedUsers.length} 个用户`); + break; + case 'deactivate': + toast.success(`已停用 ${selectedUsers.length} 个用户`); + break; + case 'delete': + toast.success(`已删除 ${selectedUsers.length} 个用户`); + break; + default: + break; + } + + setSelectedUsers([]); + fetchUsers(); + } catch (error) { + toast.error('操作失败'); + } + }; + + const handleExport = async () => { + try { + toast.loading('正在导出用户数据...', { id: 'export' }); + + // 模拟导出延迟 + await new Promise(resolve => setTimeout(resolve, 2000)); + + toast.success('用户数据导出成功', { id: 'export' }); + } catch (error) { + toast.error('导出失败', { id: 'export' }); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return 'text-green-800 bg-green-100'; + case 'inactive': + return 'text-red-800 bg-red-100'; + case 'pending': + return 'text-yellow-800 bg-yellow-100'; + default: + return 'text-gray-800 bg-gray-100'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': + return '活跃'; + case 'inactive': + return '停用'; + case 'pending': + return '待审核'; + default: + return status; + } + }; + + const getRoleText = (role: string) => { + switch (role) { + case 'admin': + return '管理员'; + case 'user': + return '用户'; + case 'interpreter': + return '翻译员'; + default: + return role; + } + }; + + const getRoleColor = (role: string) => { + switch (role) { + case 'admin': + return 'text-purple-800 bg-purple-100'; + case 'user': + return 'text-blue-800 bg-blue-100'; + case 'interpreter': + return 'text-green-800 bg-green-100'; + default: + return 'text-gray-800 bg-gray-100'; + } + }; return ( - + <> - 用户管理 - 口译服务管理后台 + 用户管理 - 翻译服务管理系统 - -
-
- {/* 页面标题 */} -
-

用户管理

-
- - {/* 搜索和筛选 */} -
-
-
- {/* 搜索框 */} -
-
-
- -
- handleFilterChange('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> -
-
- - {/* 状态筛选 */} -
- -
- - {/* 排序 */} -
- -
-
- -
- - -
-
-
- - {/* 用户列表 */} - {loading ? ( -
-
-
- ) : users.length === 0 ? ( -
- -

暂无用户

-

- 调整筛选条件或检查数据源 + + +

+ {/* 页面标题和操作 */} +
+
+

用户管理

+

+ 管理系统中的所有用户账户,包括用户、翻译员和管理员。

- ) : ( -
-
-
-

- 用户列表 ({totalCount} 个用户) -

-
+
+ + +
+
-
- {users.map((user) => ( -
-
-
- {user.full_name +
+
+ +
+
+ +
+ handleSearch(e.target.value)} + /> +
+
+ +
+ + +
+ +
+ + +
+ +
+ + handleFilterChange('company', e.target.value)} + /> +
+
+
+ + {/* 批量操作 */} + {selectedUsers.length > 0 && ( +
+
+
+ + 已选择 {selectedUsers.length} 个用户 + +
+
+ + + +
+
+
+ )} + + {/* 用户列表 */} +
+ {loading ? ( +
+
+
+ ) : ( + <> +
+ + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + + ))} + +
+ 0} + onChange={handleSelectAll} /> -
-

- {user.full_name || user.email} -

-

{user.email}

-

- 注册时间: {formatTime(user.created_at)} -

-
- -
- - {getUserStatusText(user.is_active)} - -
- - - ))} +
+ 用户信息 + + 角色/状态 + + 公司 + + 统计数据 + + 最后登录 + + 操作 +
+ handleSelectUser(user.id)} + /> + +
+
+
+ +
+
+
+
{user.name}
+
+ + {user.email} +
+
+ + {user.phone} +
+
+
+
+
+ + {getRoleText(user.role)} + +
+ + {getStatusText(user.status)} + +
+
+
+ + {user.company} +
+
+
通话: {user.total_calls} 次
+
消费: ¥{user.total_spent}
+
+
+ + {user.last_login ? formatTime(user.last_login) : '从未登录'} +
+
+
+ + + +
+
{/* 分页 */} {totalPages > 1 && ( -
+
@@ -317,54 +613,49 @@ export default function UsersPage() {

显示第 {(currentPage - 1) * pageSize + 1} 到{' '} - - {Math.min(currentPage * pageSize, totalCount)} - {' '} - 条,共 {totalCount} 条记录 + {Math.min(currentPage * pageSize, totalCount)} 项, + 共 {totalCount}

)} -
-
- )} + + )} +
-
- + + ); } \ No newline at end of file diff --git a/test-api.js b/test-api.js new file mode 100644 index 0000000..5d3e330 --- /dev/null +++ b/test-api.js @@ -0,0 +1,44 @@ +const https = require('http'); + +const data = JSON.stringify({ + email: 'admin@example.com', + password: 'admin123' +}); + +const options = { + hostname: 'localhost', + port: 3000, + path: '/api/auth/admin-login', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + } +}; + +const req = https.request(options, (res) => { + console.log(`状态码: ${res.statusCode}`); + console.log(`响应头: ${JSON.stringify(res.headers)}`); + + let body = ''; + res.on('data', (chunk) => { + body += chunk; + }); + + res.on('end', () => { + console.log('响应体:', body); + try { + const jsonResponse = JSON.parse(body); + console.log('解析后的响应:', JSON.stringify(jsonResponse, null, 2)); + } catch (e) { + console.log('无法解析JSON响应'); + } + }); +}); + +req.on('error', (error) => { + console.error('请求错误:', error); +}); + +req.write(data); +req.end(); \ No newline at end of file diff --git a/test-login-flow.js b/test-login-flow.js new file mode 100644 index 0000000..bc0485e --- /dev/null +++ b/test-login-flow.js @@ -0,0 +1,62 @@ +const http = require('http'); + +// 测试登录API +function testLogin() { + const postData = JSON.stringify({ + email: 'admin@example.com', + password: 'admin123' + }); + + const options = { + hostname: 'localhost', + port: 3000, + path: '/api/auth/admin-login', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + const req = http.request(options, (res) => { + console.log(`状态码: ${res.statusCode}`); + console.log(`响应头: ${JSON.stringify(res.headers)}`); + + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + console.log('响应体:', data); + try { + const parsedData = JSON.parse(data); + console.log('解析后的响应:', JSON.stringify(parsedData, null, 2)); + + if (parsedData.success) { + console.log('✅ 登录测试成功!'); + console.log('用户信息:', parsedData.user); + console.log('JWT令牌已生成'); + } else { + console.log('❌ 登录测试失败:', parsedData.error); + } + } catch (error) { + console.log('❌ 解析响应失败:', error.message); + } + }); + }); + + req.on('error', (e) => { + console.error(`请求错误: ${e.message}`); + }); + + req.write(postData); + req.end(); +} + +// 等待服务器启动 +console.log('等待服务器启动...'); +setTimeout(() => { + console.log('开始测试登录流程...'); + testLogin(); +}, 3000); \ No newline at end of file diff --git a/types/database.ts b/types/database.ts new file mode 100644 index 0000000..2e2eb7e --- /dev/null +++ b/types/database.ts @@ -0,0 +1,520 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export interface Database { + public: { + Tables: { + users: { + Row: { + id: string + email: string + name: string + phone?: string + user_type: 'individual' | 'enterprise' + status: 'active' | 'inactive' + enterprise_id?: string + avatar_url?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + email: string + name: string + phone?: string + user_type: 'individual' | 'enterprise' + status?: 'active' | 'inactive' + enterprise_id?: string + avatar_url?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + email?: string + name?: string + phone?: string + user_type?: 'individual' | 'enterprise' + status?: 'active' | 'inactive' + enterprise_id?: string + avatar_url?: string + created_at?: string + updated_at?: string + } + } + enterprises: { + Row: { + id: string + name: string + contact_person: string + contact_email: string + contact_phone: string + address: string + tax_number?: string + status: 'active' | 'inactive' + created_at: string + updated_at: string + } + Insert: { + id?: string + name: string + contact_person: string + contact_email: string + contact_phone: string + address: string + tax_number?: string + status?: 'active' | 'inactive' + created_at?: string + updated_at?: string + } + Update: { + id?: string + name?: string + contact_person?: string + contact_email?: string + contact_phone?: string + address?: string + tax_number?: string + status?: 'active' | 'inactive' + created_at?: string + updated_at?: string + } + } + enterprise_contracts: { + Row: { + id: string + enterprise_id: string + contract_number: string + contract_type: string + start_date: string + end_date: string + total_amount: number + currency: string + status: 'active' | 'expired' | 'terminated' + service_rates: Json + created_at: string + updated_at: string + } + Insert: { + id?: string + enterprise_id: string + contract_number: string + contract_type: string + start_date: string + end_date: string + total_amount: number + currency?: string + status?: 'active' | 'expired' | 'terminated' + service_rates?: Json + created_at?: string + updated_at?: string + } + Update: { + id?: string + enterprise_id?: string + contract_number?: string + contract_type?: string + start_date?: string + end_date?: string + total_amount?: number + currency?: string + status?: 'active' | 'expired' | 'terminated' + service_rates?: Json + created_at?: string + updated_at?: string + } + } + enterprise_bills: { + Row: { + id: string + enterprise_id: string + bill_number: string + billing_period_start: string + billing_period_end: string + total_amount: number + currency: string + status: 'draft' | 'sent' | 'paid' | 'overdue' + items: Json + created_at: string + updated_at: string + } + Insert: { + id?: string + enterprise_id: string + bill_number: string + billing_period_start: string + billing_period_end: string + total_amount: number + currency?: string + status?: 'draft' | 'sent' | 'paid' | 'overdue' + items?: Json + created_at?: string + updated_at?: string + } + Update: { + id?: string + enterprise_id?: string + bill_number?: string + billing_period_start?: string + billing_period_end?: string + total_amount?: number + currency?: string + status?: 'draft' | 'sent' | 'paid' | 'overdue' + items?: Json + created_at?: string + updated_at?: string + } + } + orders: { + Row: { + id: string + order_number: string + user_id: string + user_name: string + user_email: string + service_type: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' | 'document_translation' + service_name: string + source_language: string + target_language: string + duration?: number + status: 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed' + priority: 'urgent' | 'high' | 'normal' | 'low' + cost: number + currency: string + scheduled_time?: string + started_time?: string + completed_time?: string + interpreter_id?: string + interpreter_name?: string + notes?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + order_number: string + user_id: string + user_name: string + user_email: string + service_type: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' | 'document_translation' + service_name: string + source_language: string + target_language: string + duration?: number + status?: 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed' + priority?: 'urgent' | 'high' | 'normal' | 'low' + cost: number + currency?: string + scheduled_time?: string + started_time?: string + completed_time?: string + interpreter_id?: string + interpreter_name?: string + notes?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + order_number?: string + user_id?: string + user_name?: string + user_email?: string + service_type?: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' | 'document_translation' + service_name?: string + source_language?: string + target_language?: string + duration?: number + status?: 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed' + priority?: 'urgent' | 'high' | 'normal' | 'low' + cost?: number + currency?: string + scheduled_time?: string + started_time?: string + completed_time?: string + interpreter_id?: string + interpreter_name?: string + notes?: string + created_at?: string + updated_at?: string + } + } + invoices: { + Row: { + id: string + invoice_number: string + user_id: string + user_name: string + user_email: string + order_id?: string + invoice_type: 'personal' | 'enterprise' + personal_name?: string + company_name?: string + tax_number?: string + company_address?: string + subtotal: number + tax_amount: number + total_amount: number + currency: string + status: 'draft' | 'issued' | 'paid' | 'cancelled' + issue_date?: string + due_date?: string + paid_date?: string + items: Json + created_at: string + updated_at: string + } + Insert: { + id?: string + invoice_number: string + user_id: string + user_name: string + user_email: string + order_id?: string + invoice_type: 'personal' | 'enterprise' + personal_name?: string + company_name?: string + tax_number?: string + company_address?: string + subtotal: number + tax_amount: number + total_amount: number + currency?: string + status?: 'draft' | 'issued' | 'paid' | 'cancelled' + issue_date?: string + due_date?: string + paid_date?: string + items?: Json + created_at?: string + updated_at?: string + } + Update: { + id?: string + invoice_number?: string + user_id?: string + user_name?: string + user_email?: string + order_id?: string + invoice_type?: 'personal' | 'enterprise' + personal_name?: string + company_name?: string + tax_number?: string + company_address?: string + subtotal?: number + tax_amount?: number + total_amount?: number + currency?: string + status?: 'draft' | 'issued' | 'paid' | 'cancelled' + issue_date?: string + due_date?: string + paid_date?: string + items?: Json + created_at?: string + updated_at?: string + } + } + interpreters: { + Row: { + id: string + name: string + email: string + phone: string + languages: string[] + specialties: string[] + status: 'online' | 'offline' | 'busy' + rating: number + total_calls: number + hourly_rate: number + currency: string + avatar_url?: string + bio?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + name: string + email: string + phone: string + languages: string[] + specialties: string[] + status?: 'online' | 'offline' | 'busy' + rating?: number + total_calls?: number + hourly_rate: number + currency?: string + avatar_url?: string + bio?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + name?: string + email?: string + phone?: string + languages?: string[] + specialties?: string[] + status?: 'online' | 'offline' | 'busy' + rating?: number + total_calls?: number + hourly_rate?: number + currency?: string + avatar_url?: string + bio?: string + created_at?: string + updated_at?: string + } + } + calls: { + Row: { + id: string + user_id: string + interpreter_id?: string + service_type: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' + source_language: string + target_language: string + status: 'waiting' | 'connecting' | 'active' | 'completed' | 'failed' + duration?: number + cost: number + currency: string + quality_rating?: number + started_at?: string + ended_at?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + user_id: string + interpreter_id?: string + service_type: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' + source_language: string + target_language: string + status?: 'waiting' | 'connecting' | 'active' | 'completed' | 'failed' + duration?: number + cost: number + currency?: string + quality_rating?: number + started_at?: string + ended_at?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + user_id?: string + interpreter_id?: string + service_type?: 'ai_voice' | 'ai_video' | 'sign_language' | 'human_interpretation' + source_language?: string + target_language?: string + status?: 'waiting' | 'connecting' | 'active' | 'completed' | 'failed' + duration?: number + cost?: number + currency?: string + quality_rating?: number + started_at?: string + ended_at?: string + created_at?: string + updated_at?: string + } + } + documents: { + Row: { + id: string + user_id: string + filename: string + original_name: string + file_size: number + file_type: string + source_language: string + target_language: string + status: 'uploaded' | 'processing' | 'completed' | 'failed' + progress: number + cost: number + currency: string + translated_file_url?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + user_id: string + filename: string + original_name: string + file_size: number + file_type: string + source_language: string + target_language: string + status?: 'uploaded' | 'processing' | 'completed' | 'failed' + progress?: number + cost: number + currency?: string + translated_file_url?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + user_id?: string + filename?: string + original_name?: string + file_size?: number + file_type?: string + source_language?: string + target_language?: string + status?: 'uploaded' | 'processing' | 'completed' | 'failed' + progress?: number + cost?: number + currency?: string + translated_file_url?: string + created_at?: string + updated_at?: string + } + } + system_settings: { + Row: { + id: string + key: string + value: Json + description?: string + created_at: string + updated_at: string + } + Insert: { + id?: string + key: string + value: Json + description?: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + key?: string + value?: Json + description?: string + created_at?: string + updated_at?: string + } + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} \ No newline at end of file