feat: 完成所有页面的演示模式实现
- 更新 DashboardLayout 组件,统一使用演示模式布局 - 实现仪表盘页面的完整演示数据和功能 - 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能 - 实现通话记录页面的演示数据和录音播放功能 - 完成翻译员管理页面的演示模式 - 实现订单管理页面的完整功能 - 完成发票管理页面的演示数据 - 更新文档管理页面 - 添加 utils.ts 工具函数库 - 完善 API 路由和数据库结构 - 修复各种 TypeScript 类型错误 - 统一界面风格和用户体验
This commit is contained in:
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user