290 lines
7.0 KiB
TypeScript
290 lines
7.0 KiB
TypeScript
import { createClient } from '@supabase/supabase-js';
|
|
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
|
|
|
// 环境变量检查和默认值
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://demo.supabase.co';
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'demo-key';
|
|
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'demo-service-key';
|
|
|
|
// 检查是否在开发环境中使用默认配置
|
|
const isDemoMode = supabaseUrl === 'https://demo.supabase.co';
|
|
|
|
// 客户端使用的 Supabase 客户端
|
|
export const supabase = isDemoMode
|
|
? createClient(supabaseUrl, supabaseAnonKey, {
|
|
realtime: {
|
|
params: {
|
|
eventsPerSecond: 0,
|
|
},
|
|
},
|
|
auth: {
|
|
persistSession: false,
|
|
autoRefreshToken: false,
|
|
},
|
|
})
|
|
: createClient(supabaseUrl, supabaseAnonKey);
|
|
|
|
// 组件中使用的 Supabase 客户端
|
|
export const createSupabaseClient = () => {
|
|
if (isDemoMode) {
|
|
// 在演示模式下返回一个模拟客户端
|
|
return {
|
|
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: () => {} } } }),
|
|
}
|
|
} as any;
|
|
}
|
|
return createClientComponentClient();
|
|
};
|
|
|
|
// 服务端使用的 Supabase 客户端(具有管理员权限)
|
|
export const supabaseAdmin = isDemoMode
|
|
? createClient(supabaseUrl, supabaseServiceKey, {
|
|
auth: {
|
|
autoRefreshToken: false,
|
|
persistSession: false,
|
|
},
|
|
realtime: {
|
|
params: {
|
|
eventsPerSecond: 0,
|
|
},
|
|
},
|
|
})
|
|
: createClient(supabaseUrl, supabaseServiceKey, {
|
|
auth: {
|
|
autoRefreshToken: false,
|
|
},
|
|
});
|
|
|
|
// 数据库表名常量
|
|
export const TABLES = {
|
|
USERS: 'users',
|
|
CALLS: 'calls',
|
|
APPOINTMENTS: 'appointments',
|
|
INTERPRETERS: 'interpreters',
|
|
DOCUMENTS: 'document_translations',
|
|
ORDERS: 'orders',
|
|
INVOICES: 'invoices',
|
|
ENTERPRISE_EMPLOYEES: 'enterprise_employees',
|
|
PRICING_RULES: 'pricing_rules',
|
|
NOTIFICATIONS: 'notifications',
|
|
SYSTEM_SETTINGS: 'system_settings',
|
|
ACCOUNT_BALANCES: 'account_balances',
|
|
} as const;
|
|
|
|
// 实时订阅配置
|
|
export const REALTIME_CHANNELS = {
|
|
CALLS: 'calls:*',
|
|
NOTIFICATIONS: 'notifications:*',
|
|
APPOINTMENTS: 'appointments:*',
|
|
} as const;
|
|
|
|
// 用户认证相关函数
|
|
export const auth = {
|
|
// 获取当前用户
|
|
getCurrentUser: async () => {
|
|
const { data: { user }, error } = await supabase.auth.getUser();
|
|
if (error) throw error;
|
|
return user;
|
|
},
|
|
|
|
// 登录
|
|
signIn: async (email: string, password: string) => {
|
|
const { data, error } = await supabase.auth.signInWithPassword({
|
|
email,
|
|
password,
|
|
});
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
|
|
// 注册
|
|
signUp: async (email: string, password: string, metadata?: any) => {
|
|
const { data, error } = await supabase.auth.signUp({
|
|
email,
|
|
password,
|
|
options: {
|
|
data: metadata,
|
|
},
|
|
});
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
|
|
// 登出
|
|
signOut: async () => {
|
|
const { error } = await supabase.auth.signOut();
|
|
if (error) throw error;
|
|
},
|
|
|
|
// 重置密码
|
|
resetPassword: async (email: string) => {
|
|
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
|
|
redirectTo: `${window.location.origin}/auth/reset-password`,
|
|
});
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
|
|
// 更新密码
|
|
updatePassword: async (password: string) => {
|
|
const { data, error } = await supabase.auth.updateUser({
|
|
password,
|
|
});
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// 数据库操作辅助函数
|
|
export const db = {
|
|
// 通用查询函数
|
|
select: async <T>(table: string, query?: any) => {
|
|
let queryBuilder = supabase.from(table).select(query || '*');
|
|
const { data, error } = await queryBuilder;
|
|
if (error) throw error;
|
|
return data as T[];
|
|
},
|
|
|
|
// 通用插入函数
|
|
insert: async <T>(table: string, data: any) => {
|
|
const { data: result, error } = await supabase
|
|
.from(table)
|
|
.insert(data)
|
|
.select()
|
|
.single();
|
|
if (error) throw error;
|
|
return result as T;
|
|
},
|
|
|
|
// 通用更新函数
|
|
update: async <T>(table: string, id: string, data: any) => {
|
|
const { data: result, error } = await supabase
|
|
.from(table)
|
|
.update(data)
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
if (error) throw error;
|
|
return result as T;
|
|
},
|
|
|
|
// 通用删除函数
|
|
delete: async (table: string, id: string) => {
|
|
const { error } = await supabase
|
|
.from(table)
|
|
.delete()
|
|
.eq('id', id);
|
|
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;
|
|
|
|
let queryBuilder = supabase
|
|
.from(table)
|
|
.select(query || '*', { count: 'exact' })
|
|
.range(from, to);
|
|
|
|
if (orderBy) {
|
|
queryBuilder = queryBuilder.order(orderBy.column, {
|
|
ascending: orderBy.ascending ?? true,
|
|
});
|
|
}
|
|
|
|
const { data, error, count } = await queryBuilder;
|
|
if (error) throw error;
|
|
|
|
return {
|
|
data: data as T[],
|
|
total: count || 0,
|
|
page,
|
|
limit,
|
|
has_more: (count || 0) > page * limit,
|
|
};
|
|
},
|
|
};
|
|
|
|
// 文件上传函数
|
|
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,
|
|
});
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
|
|
// 获取文件公共URL
|
|
getPublicUrl: (bucket: string, path: string) => {
|
|
const { data } = supabase.storage
|
|
.from(bucket)
|
|
.getPublicUrl(path);
|
|
return data.publicUrl;
|
|
},
|
|
|
|
// 删除文件
|
|
remove: async (bucket: string, paths: string[]) => {
|
|
const { data, error } = await supabase.storage
|
|
.from(bucket)
|
|
.remove(paths);
|
|
if (error) throw error;
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// 实时订阅函数
|
|
export const realtime = {
|
|
// 订阅表变化
|
|
subscribe: (
|
|
table: string,
|
|
callback: (payload: any) => void,
|
|
filter?: string
|
|
) => {
|
|
if (isDemoMode) {
|
|
// 演示模式下返回模拟的订阅对象
|
|
return {
|
|
unsubscribe: () => {},
|
|
};
|
|
}
|
|
|
|
const channel = supabase
|
|
.channel(`${table}-changes`)
|
|
.on(
|
|
'postgres_changes',
|
|
{
|
|
event: '*',
|
|
schema: 'public',
|
|
table,
|
|
filter,
|
|
},
|
|
callback
|
|
)
|
|
.subscribe();
|
|
|
|
return channel;
|
|
},
|
|
|
|
// 取消订阅
|
|
unsubscribe: (channel: any) => {
|
|
if (isDemoMode) {
|
|
return;
|
|
}
|
|
supabase.removeChannel(channel);
|
|
},
|
|
};
|