feat: 完成所有页面的演示模式实现

- 更新 DashboardLayout 组件,统一使用演示模式布局
- 实现仪表盘页面的完整演示数据和功能
- 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能
- 实现通话记录页面的演示数据和录音播放功能
- 完成翻译员管理页面的演示模式
- 实现订单管理页面的完整功能
- 完成发票管理页面的演示数据
- 更新文档管理页面
- 添加 utils.ts 工具函数库
- 完善 API 路由和数据库结构
- 修复各种 TypeScript 类型错误
- 统一界面风格和用户体验
This commit is contained in:
2025-06-30 19:42:43 +08:00
parent 0b8be9377a
commit f20988b90c
36 changed files with 8752 additions and 3638 deletions
+323
View File
@@ -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<T> {
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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
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<ApiResponse<DashboardStats>> {
// 先尝试从真实API获取数据
const response = await this.request<DashboardStats>('/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<ApiResponse<RecentActivity[]>> {
const response = await this.request<RecentActivity[]>('/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<ApiResponse<{ users: User[]; total: number }>> {
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<User>): Promise<ApiResponse<User>> {
return this.request<User>('/users', {
method: 'POST',
body: JSON.stringify(userData),
});
}
// 更新用户
async updateUser(userId: string, userData: Partial<User>): Promise<ApiResponse<User>> {
return this.request<User>(`/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
}
// 删除用户
async deleteUser(userId: string): Promise<ApiResponse<void>> {
return this.request<void>(`/users/${userId}`, {
method: 'DELETE',
});
}
// 批量删除用户
async deleteUsers(userIds: string[]): Promise<ApiResponse<void>> {
return this.request<void>('/users/batch-delete', {
method: 'POST',
body: JSON.stringify({ userIds }),
});
}
// 获取用户详情
async getUserDetail(userId: string): Promise<ApiResponse<User>> {
return this.request<User>(`/users/${userId}`);
}
// 检查服务状态
async checkServiceStatus(): Promise<ApiResponse<{ status: string; timestamp: string }>> {
return this.request<{ status: string; timestamp: string }>('/health');
}
}
// 导出单例实例
export const apiService = new ApiService();
// 导出默认实例
export default apiService;
+118
View File
@@ -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<T = any> {
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<User | null> {
try {
const users = await db.select<User>('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
}
}
+157 -87
View File
@@ -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<Database>(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 <T>(
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 <T>(table: string, conditions: Record<string, any>, 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 <T>(table: string, conditions?: Record<string, any>, 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<string, any>) => {
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<UserRole, number> = {
'client': 1,
'interpreter': 2,
'enterprise': 3,
'admin': 4,
};
return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
},
// 取消订阅
unsubscribe: (channel: any) => {
if (isDemoMode) {
return;
}
supabase.removeChannel(channel);
},
};
// 检查用户是否为管理员
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;
+180
View File
@@ -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 = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(null, args), wait);
};
};
/**
* 节流函数
* @param func - 要节流的函数
* @param limit - 限制时间(毫秒)
* @returns 节流后的函数
*/
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func.apply(null, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
/**
* 深拷贝对象
* @param obj - 要拷贝的对象
* @returns 拷贝后的对象
*/
export const deepClone = <T>(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<string, string> => {
const params: Record<string, string> = {};
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);
};