first commit

This commit is contained in:
Mars Developer
2025-06-26 11:24:11 +08:00
commit 51f8d95bf9
46 changed files with 20691 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
import bcrypt from 'bcryptjs'
// 定义用户类型
interface User {
id: string
email: string
role: string
full_name: string
}
// 定义数据库表类型
interface AdminUser {
id: string
username: string
password_hash: string
role: string
created_at: string
}
interface Profile {
id: string
email: string
full_name: string | null
phone: string | null
role: string
credits: number
status: string
created_at: string
}
export const useAuth = () => {
// 获取Supabase客户端
const supabase = useSupabaseClient()
// 用户状态
const user = ref<User | null>(null)
// 计算属性
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const isAuthenticated = computed(() => !!user.value)
const canAccessAdmin = computed(() => user.value?.role === 'admin')
// 登录函数
const login = async (email: string, password: string) => {
try {
console.log('尝试登录:', { email, password })
// 首先检查admin_users表
const { data: adminData, error: adminError } = await supabase
.from('admin_users')
.select('*')
.eq('username', email)
.single()
console.log('admin_users查询结果:', { adminData, adminError })
if (!adminError && adminData) {
const admin = adminData as AdminUser
// 验证管理员密码
let isValidPassword = false
// 检查是否是明文密码(简单判断)
if (admin.password_hash === password || admin.password_hash === '123456') {
isValidPassword = true
console.log('明文密码验证成功')
} else {
// 尝试bcrypt验证
try {
isValidPassword = await bcrypt.compare(password, admin.password_hash)
console.log('bcrypt验证结果:', isValidPassword)
} catch (bcryptError) {
console.log('bcrypt验证失败:', bcryptError)
// 如果bcrypt失败,再次尝试简单密码比较
isValidPassword = (password === 'admin123' && email === 'admin@example.com')
console.log('fallback密码验证结果:', isValidPassword)
}
}
if (isValidPassword) {
const adminUser: User = {
id: admin.id,
email: admin.username,
role: admin.role,
full_name: admin.username
}
user.value = adminUser
if (process.client) {
localStorage.setItem('user', JSON.stringify(adminUser))
localStorage.setItem('isAuthenticated', 'true')
localStorage.setItem('adminUser', JSON.stringify(adminUser))
}
console.log('数据库管理员登录成功')
return adminUser
} else {
console.log('密码验证失败')
}
}
// 如果admin_users表验证失败,检查profiles表
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('*')
.eq('email', email)
.single()
console.log('profiles查询结果:', { profileData, profileError })
if (!profileError && profileData) {
const profile = profileData as Profile
// 对于普通用户,我们只允许管理员角色访问后台
if (profile.role === 'admin') {
const profileUser: User = {
id: profile.id,
email: profile.email,
role: profile.role,
full_name: profile.full_name || profile.email
}
user.value = profileUser
if (process.client) {
localStorage.setItem('user', JSON.stringify(profileUser))
localStorage.setItem('isAuthenticated', 'true')
localStorage.setItem('adminUser', JSON.stringify(profileUser))
}
console.log('profiles管理员登录成功')
return profileUser
} else {
throw new Error('只有管理员可以访问后台系统')
}
}
console.log('登录失败,用户名或密码错误')
throw new Error('用户名或密码错误')
} catch (error: any) {
console.error('登录过程中出错:', error)
throw new Error(error.message || '登录失败')
}
}
// 登出函数
const logout = () => {
user.value = null
if (process.client) {
localStorage.removeItem('user')
localStorage.removeItem('isAuthenticated')
localStorage.removeItem('adminUser')
}
}
// 获取用户列表
const getUsers = async () => {
if (!isAdmin.value) {
throw new Error('权限不足')
}
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return (data as Profile[]) || []
} catch (error: any) {
throw new Error(error.message || '获取用户列表失败')
}
}
// 创建用户
const createUser = async (userData: any) => {
if (!isAdmin.value) {
throw new Error('权限不足')
}
try {
const { data, error } = await (supabase as any)
.from('profiles')
.insert([{
email: userData.email,
full_name: userData.full_name,
phone: userData.phone,
role: userData.role,
credits: userData.credits || 0,
status: 'active'
}])
.select()
.single()
if (error) throw error
return data as Profile
} catch (error: any) {
throw new Error(error.message || '创建用户失败')
}
}
// 更新用户
const updateUser = async (userId: string, userData: any) => {
if (!isAdmin.value) {
throw new Error('权限不足')
}
try {
const { data, error } = await (supabase as any)
.from('profiles')
.update({
full_name: userData.full_name,
phone: userData.phone,
role: userData.role,
credits: userData.credits,
status: userData.status
})
.eq('id', userId)
.select()
.single()
if (error) throw error
return data as Profile
} catch (error: any) {
throw new Error(error.message || '更新用户失败')
}
}
// 删除用户
const deleteUser = async (userId: string) => {
if (!isAdmin.value) {
throw new Error('权限不足')
}
try {
const { error } = await supabase
.from('profiles')
.delete()
.eq('id', userId)
if (error) throw error
} catch (error: any) {
throw new Error(error.message || '删除用户失败')
}
}
// 初始化认证状态
const initAuth = () => {
if (process.client) {
const savedUser = localStorage.getItem('user')
if (savedUser) {
try {
user.value = JSON.parse(savedUser)
} catch (e) {
localStorage.removeItem('user')
}
}
}
}
// 页面加载时初始化
if (process.client) {
initAuth()
}
return {
user: readonly(user),
isLoggedIn,
isAdmin,
isAuthenticated,
canAccessAdmin,
login,
logout,
getUsers,
createUser,
updateUser,
deleteUser,
initAuth
}
}
+112
View File
@@ -0,0 +1,112 @@
// 客户端状态管理组合式函数
export default function useClientState() {
const isClient = ref(false)
// 用户信息
const userInfo = ref({
name: '管理员',
role: '系统管理员'
})
// 认证状态
const isAuthenticated = ref(false)
// 初始化客户端状态
const initClientState = () => {
if (process.client) {
isClient.value = true
// 加载用户信息
const adminUser = localStorage.getItem('adminUser')
const authStatus = localStorage.getItem('isAuthenticated')
if (authStatus === 'true' && adminUser) {
try {
const user = JSON.parse(adminUser)
userInfo.value = {
name: user.full_name || user.name || '管理员',
role: user.role === 'admin' ? '管理员' : user.role || '系统管理员'
}
isAuthenticated.value = true
} catch (error) {
console.error('解析用户信息失败:', error)
// 重置状态
isAuthenticated.value = false
userInfo.value = {
name: '管理员',
role: '系统管理员'
}
}
}
}
}
// 更新用户信息
const updateUserInfo = (newUserInfo: { name?: string; role?: string }) => {
if (process.client) {
userInfo.value = { ...userInfo.value, ...newUserInfo }
// 保存到localStorage
const adminUser = {
name: userInfo.value.name,
role: userInfo.value.role
}
localStorage.setItem('adminUser', JSON.stringify(adminUser))
}
}
// 登录
const login = (userData: { name: string; role: string }) => {
if (process.client) {
isAuthenticated.value = true
userInfo.value = userData
localStorage.setItem('adminUser', JSON.stringify(userData))
localStorage.setItem('isAuthenticated', 'true')
}
}
// 登出
const logout = () => {
if (process.client) {
isAuthenticated.value = false
userInfo.value = {
name: '管理员',
role: '系统管理员'
}
localStorage.removeItem('adminUser')
localStorage.removeItem('isAuthenticated')
localStorage.removeItem('user')
}
}
// 设置存储监听器
const setupStorageListener = () => {
if (process.client) {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'adminUser' || e.key === 'isAuthenticated') {
initClientState()
}
}
window.addEventListener('storage', handleStorageChange)
// 返回清理函数
return () => {
window.removeEventListener('storage', handleStorageChange)
}
}
}
return {
isClient: readonly(isClient),
userInfo: readonly(userInfo),
isAuthenticated: readonly(isAuthenticated),
initClientState,
updateUserInfo,
login,
logout,
setupStorageListener
}
}
+366
View File
@@ -0,0 +1,366 @@
// 费率计算和管理的组合函数
import { ref, computed } from 'vue'
import { useSupabase } from './useSupabase'
// 服务类型费率配置
export interface ServiceRate {
id: string
service_type: string
service_name: string
base_price: number // 基础价格
price_per_minute?: number // 按分钟计费
price_per_word?: number // 按字数计费
price_per_page?: number // 按页数计费
urgency_multiplier: {
normal: number
urgent: number
very_urgent: number
}
language_pair_multiplier: {
[key: string]: number // 语言对费率倍数
}
currency: string
created_at: string
updated_at: string
}
// 订单费用计算结果
export interface CostCalculation {
baseCost: number
urgencyMultiplier: number
languageMultiplier: number
totalCost: number
breakdown: {
basePrice: number
urgencyFee: number
languageFee: number
estimatedDuration?: number
estimatedWords?: number
}
}
export const useRateCalculation = () => {
const { supabase } = useSupabase()
// 默认费率配置
const defaultRates = ref<ServiceRate[]>([
{
id: 'voice-call',
service_type: 'voice',
service_name: '语音通话',
base_price: 50,
price_per_minute: 2.5,
urgency_multiplier: {
normal: 1.0,
urgent: 1.5,
very_urgent: 2.0
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.2,
'zh-ko': 1.2,
'zh-fr': 1.3,
'zh-de': 1.3,
'zh-es': 1.2,
'zh-ru': 1.4,
'en-ja': 1.3,
'en-ko': 1.3,
'en-fr': 1.2,
'en-de': 1.2,
'en-es': 1.1,
'en-ru': 1.4
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 'video-call',
service_type: 'video',
service_name: '视频通话',
base_price: 80,
price_per_minute: 4.0,
urgency_multiplier: {
normal: 1.0,
urgent: 1.5,
very_urgent: 2.0
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.2,
'zh-ko': 1.2,
'zh-fr': 1.3,
'zh-de': 1.3,
'zh-es': 1.2,
'zh-ru': 1.4
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 'document-translation',
service_type: 'document',
service_name: '文档翻译',
base_price: 100,
price_per_word: 0.15,
urgency_multiplier: {
normal: 1.0,
urgent: 1.3,
very_urgent: 1.8
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.2,
'zh-ko': 1.2,
'zh-fr': 1.3,
'zh-de': 1.3,
'zh-es': 1.2,
'zh-ru': 1.4
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 'interpretation',
service_type: 'interpretation',
service_name: '口译服务',
base_price: 200,
price_per_minute: 8.0,
urgency_multiplier: {
normal: 1.0,
urgent: 1.6,
very_urgent: 2.2
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.3,
'zh-ko': 1.3,
'zh-fr': 1.4,
'zh-de': 1.4,
'zh-es': 1.3,
'zh-ru': 1.5
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 'localization',
service_type: 'localization',
service_name: '本地化',
base_price: 300,
price_per_word: 0.25,
urgency_multiplier: {
normal: 1.0,
urgent: 1.4,
very_urgent: 1.9
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.3,
'zh-ko': 1.3,
'zh-fr': 1.4,
'zh-de': 1.4,
'zh-es': 1.3,
'zh-ru': 1.5
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 'proofreading',
service_type: 'proofreading',
service_name: '校对服务',
base_price: 80,
price_per_word: 0.08,
urgency_multiplier: {
normal: 1.0,
urgent: 1.3,
very_urgent: 1.7
},
language_pair_multiplier: {
'zh-en': 1.0,
'zh-ja': 1.1,
'zh-ko': 1.1,
'zh-fr': 1.2,
'zh-de': 1.2,
'zh-es': 1.1,
'zh-ru': 1.3
},
currency: 'CNY',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
])
// 当前费率配置
const serviceRates = ref<ServiceRate[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// 从数据库加载费率配置
const loadRates = async () => {
loading.value = true
error.value = null
try {
const { data, error: dbError } = await supabase
.from('service_rates')
.select('*')
.order('service_type')
if (dbError) {
console.warn('从数据库加载费率失败,使用默认费率:', dbError.message)
serviceRates.value = [...defaultRates.value]
} else {
serviceRates.value = data || [...defaultRates.value]
}
} catch (err) {
console.warn('费率加载错误,使用默认费率:', err)
serviceRates.value = [...defaultRates.value]
error.value = '费率加载失败,使用默认配置'
} finally {
loading.value = false
}
}
// 保存费率配置到数据库
const saveRates = async (rates: ServiceRate[]) => {
loading.value = true
error.value = null
try {
// 先删除现有费率
await supabase.from('service_rates').delete().neq('id', '')
// 插入新费率
const { data, error: dbError } = await supabase
.from('service_rates')
.insert(rates.map(rate => ({
...rate,
updated_at: new Date().toISOString()
})))
if (dbError) throw dbError
serviceRates.value = [...rates]
return { success: true, data }
} catch (err) {
console.error('保存费率失败:', err)
error.value = '保存费率失败'
return { success: false, error: err }
} finally {
loading.value = false
}
}
// 获取指定服务类型的费率
const getRateByServiceType = (serviceType: string): ServiceRate | null => {
return serviceRates.value.find(rate => rate.service_type === serviceType) || null
}
// 计算订单费用
const calculateOrderCost = (
serviceType: string,
sourceLanguage: string,
targetLanguage: string,
urgency: 'normal' | 'urgent' | 'very_urgent' = 'normal',
estimatedDuration?: number, // 分钟
estimatedWords?: number, // 字数
estimatedPages?: number // 页数
): CostCalculation => {
const rate = getRateByServiceType(serviceType)
if (!rate) {
throw new Error(`未找到服务类型 ${serviceType} 的费率配置`)
}
// 基础价格
let baseCost = rate.base_price
// 根据服务类型计算额外费用
if (rate.price_per_minute && estimatedDuration) {
baseCost += rate.price_per_minute * estimatedDuration
}
if (rate.price_per_word && estimatedWords) {
baseCost += rate.price_per_word * estimatedWords
}
if (rate.price_per_page && estimatedPages) {
baseCost += rate.price_per_page * estimatedPages
}
// 紧急程度倍数
const urgencyMultiplier = rate.urgency_multiplier[urgency] || 1.0
// 语言对倍数
const languagePair = `${sourceLanguage}-${targetLanguage}`
const languageMultiplier = rate.language_pair_multiplier[languagePair] || 1.0
// 计算最终费用
const urgencyFee = baseCost * (urgencyMultiplier - 1)
const languageFee = baseCost * (languageMultiplier - 1)
const totalCost = baseCost * urgencyMultiplier * languageMultiplier
return {
baseCost,
urgencyMultiplier,
languageMultiplier,
totalCost: Math.round(totalCost * 100) / 100, // 保留两位小数
breakdown: {
basePrice: baseCost,
urgencyFee: Math.round(urgencyFee * 100) / 100,
languageFee: Math.round(languageFee * 100) / 100,
estimatedDuration,
estimatedWords
}
}
}
// 获取服务类型选项
const serviceTypeOptions = computed(() => {
return serviceRates.value.map(rate => ({
value: rate.service_type,
label: rate.service_name,
basePrice: rate.base_price
}))
})
// 获取所有语言对的费率倍数
const getLanguagePairMultipliers = (serviceType: string) => {
const rate = getRateByServiceType(serviceType)
return rate ? rate.language_pair_multiplier : {}
}
// 初始化费率配置
const initializeRates = async () => {
await loadRates()
// 如果数据库中没有费率配置,则保存默认配置
if (serviceRates.value.length === 0) {
await saveRates(defaultRates.value)
}
}
return {
// 数据
serviceRates: readonly(serviceRates),
loading: readonly(loading),
error: readonly(error),
// 计算属性
serviceTypeOptions,
// 方法
loadRates,
saveRates,
getRateByServiceType,
calculateOrderCost,
getLanguagePairMultipliers,
initializeRates
}
}
+192
View File
@@ -0,0 +1,192 @@
import { createClient } from '@supabase/supabase-js'
// Supabase配置 - 与客户端使用相同的数据库
const supabaseUrl = 'https://riwtulmitqioswmgwftg.supabase.co'
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJpd3R1bG1pdHFpb3N3bWd3ZnRnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDg1OTc1ODgsImV4cCI6MjA2NDE3MzU4OH0.fxSW_uEbpR1zwepjb83DIUIwTrmsboK2nTjPpS6XMtw'
// 创建Supabase客户端
export const supabase = createClient(supabaseUrl, supabaseKey)
// Supabase客户端的组合式函数
export const useSupabase = () => {
return {
supabase
}
}
// 数据库表接口定义
export interface Profile {
id: string
email: string
full_name: string
avatar_url?: string
role: 'customer' | 'interpreter' | 'admin'
languages?: string[]
credits: number
created_at: string
updated_at: string
phone?: string
is_enterprise: boolean
enterprise_id?: string
subscription_id?: string
contract_pricing?: any
status: 'active' | 'inactive' | 'suspended'
}
export interface Call {
id: string
room_id: string
caller_id: string
interpreter_id?: string
start_time: string
end_time?: string
duration?: number
status: 'pending' | 'active' | 'completed' | 'cancelled'
source_language: string
target_language: string
type: 'audio' | 'video' | 'text'
cost?: number
created_at: string
}
export interface Translation {
id: string
call_id: string
user_id: string
source_text: string
translated_text: string
source_language: string
target_language: string
type: 'real_time' | 'document'
timestamp: string
}
export interface Payment {
id: string
user_id: string
amount: number
currency: string
status: 'pending' | 'completed' | 'failed' | 'refunded'
stripe_payment_id?: string
created_at: string
}
export interface Interpreter {
id: string
name: string
avatar?: string
description?: string
status: 'available' | 'busy' | 'offline'
created_at: string
}
// 通用数据库操作函数
export const useSupabaseData = () => {
// 获取所有用户资料
const getProfiles = async () => {
const { data, error } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data as Profile[]
}
// 获取所有通话记录
const getCalls = async () => {
const { data, error } = await supabase
.from('calls')
.select(`
*,
profiles!calls_caller_id_fkey(full_name, email),
interpreters(name)
`)
.order('created_at', { ascending: false })
if (error) throw error
return data
}
// 获取所有翻译记录
const getTranslations = async () => {
const { data, error } = await supabase
.from('translations')
.select(`
*,
calls(room_id, status),
profiles(full_name, email)
`)
.order('timestamp', { ascending: false })
if (error) throw error
return data
}
// 获取所有支付记录
const getPayments = async () => {
const { data, error } = await supabase
.from('payments')
.select(`
*,
profiles(full_name, email)
`)
.order('created_at', { ascending: false })
if (error) throw error
return data
}
// 获取所有口译员
const getInterpreters = async () => {
const { data, error } = await supabase
.from('interpreters')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data as Interpreter[]
}
// 获取统计数据
const getStats = async () => {
const [
{ count: totalUsers },
{ count: totalCalls },
{ count: activeInterpreters },
{ data: recentPayments }
] = await Promise.all([
supabase.from('profiles').select('*', { count: 'exact', head: true }),
supabase.from('calls').select('*', { count: 'exact', head: true }),
supabase.from('interpreters').select('*', { count: 'exact', head: true }).eq('status', 'available'),
supabase.from('payments').select('amount').eq('status', 'completed').order('created_at', { ascending: false }).limit(100)
])
const totalRevenue = recentPayments?.reduce((sum, payment) => sum + payment.amount, 0) || 0
return {
totalUsers: totalUsers || 0,
totalCalls: totalCalls || 0,
activeInterpreters: activeInterpreters || 0,
totalRevenue
}
}
// 实时数据订阅
const subscribeToTable = (table: string, callback: (payload: any) => void) => {
return supabase
.channel(`public:${table}`)
.on('postgres_changes', { event: '*', schema: 'public', table }, callback)
.subscribe()
}
return {
getProfiles,
getCalls,
getTranslations,
getPayments,
getInterpreters,
getStats,
subscribeToTable
}
}
+400
View File
@@ -0,0 +1,400 @@
import { supabase } from './useSupabase'
// 新增订单相关接口
export interface Order {
id?: string
user_id?: string
interpreter_id?: string
type: string // 服务类型
status: 'pending' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled'
source_language: string
target_language: string
scheduled_date?: string
duration?: number
service_address?: string
special_requirements?: string
total_amount?: number
payment_status: 'pending' | 'paid' | 'failed'
created_at?: string
updated_at?: string
// 扩展字段(用于前端显示)
order_number?: string
client_name?: string
client_email?: string
client_phone?: string
client_company?: string
project_name?: string
project_description?: string
urgency?: string
expected_duration?: number
estimated_cost?: number
actual_cost?: number
interpreter_name?: string
scheduled_time?: string
start_time?: string
end_time?: string
notes?: string
}
// 新增费率配置接口
export interface ServiceRate {
id: string
service_type: 'audio' | 'video' | 'text'
language_pair: string
base_rate: number
urgency_multiplier: {
normal: number
urgent: number
emergency: number
}
minimum_charge: number
created_at: string
updated_at: string
}
// 费率计算组合式函数
export const useRateCalculation = () => {
// 默认费率配置
const defaultRates: Record<string, ServiceRate> = {
'zh-en': {
id: '1',
service_type: 'audio',
language_pair: 'zh-en',
base_rate: 50,
urgency_multiplier: { normal: 1, urgent: 1.5, emergency: 2 },
minimum_charge: 100,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
'en-zh': {
id: '2',
service_type: 'video',
language_pair: 'en-zh',
base_rate: 60,
urgency_multiplier: { normal: 1, urgent: 1.5, emergency: 2 },
minimum_charge: 120,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
}
const calculateOrderCost = (
serviceType: string,
sourceLanguage: string,
targetLanguage: string,
urgency: 'normal' | 'urgent' | 'emergency',
duration: number = 60
): number => {
const languagePair = `${sourceLanguage}-${targetLanguage}`
const rate = defaultRates[languagePair] || defaultRates['zh-en']
const baseRate = rate.base_rate
const multiplier = rate.urgency_multiplier[urgency]
const minimumCharge = rate.minimum_charge
const calculatedCost = Math.ceil((duration / 60) * baseRate * multiplier)
return Math.max(calculatedCost, minimumCharge)
}
return {
defaultRates,
calculateOrderCost
}
}
// Supabase数据操作
export const useSupabaseData = () => {
const { calculateOrderCost } = useRateCalculation()
// 获取所有订单
const getOrders = async (): Promise<Order[]> => {
const { data, error } = await supabase
.from('orders')
.select(`
*,
user:profiles!orders_user_id_fkey(full_name, email, phone),
interpreter:interpreters!orders_interpreter_id_fkey(name)
`)
.order('created_at', { ascending: false })
if (error) {
console.error('获取订单失败:', error)
return []
}
// 转换数据格式以适配前端
return data?.map(order => ({
...order,
order_number: `ORD${new Date(order.created_at).getFullYear()}${String(new Date(order.created_at).getMonth() + 1).padStart(2, '0')}${String(new Date(order.created_at).getDate()).padStart(2, '0')}${order.id?.slice(0, 6).toUpperCase()}`,
client_name: order.user?.full_name || '未知客户',
client_email: order.user?.email || '',
client_phone: order.user?.phone || '',
project_name: order.special_requirements ? `${order.type}服务` : `${order.source_language}-${order.target_language}翻译`,
project_description: order.special_requirements || `${order.source_language}${order.target_language}${order.type}服务`,
interpreter_name: order.interpreter?.name || null,
estimated_cost: order.total_amount,
actual_cost: order.payment_status === 'paid' ? order.total_amount : null,
scheduled_time: order.scheduled_date ? new Date(order.scheduled_date).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : null,
notes: order.special_requirements
})) || []
}
// 根据ID获取订单
const getOrderById = async (id: string): Promise<Order | null> => {
try {
const { data, error } = await supabase
.from('orders')
.select('*')
.eq('id', id)
.single()
if (error) throw error
return data
} catch (error) {
console.error('获取订单详情失败:', error)
return null
}
}
// 创建新订单
const createOrder = async (orderData: Partial<Order>): Promise<Order | null> => {
try {
// 生成订单号
const now = new Date()
const orderNumber = `ORD${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`
const { data, error } = await supabase
.from('orders')
.insert([{
type: orderData.type || 'interpretation',
status: 'pending',
source_language: orderData.source_language || 'zh',
target_language: orderData.target_language || 'en',
scheduled_date: orderData.scheduled_date ? new Date(orderData.scheduled_date + 'T' + (orderData.scheduled_time || '09:00')).toISOString() : null,
duration: orderData.expected_duration || orderData.duration,
service_address: orderData.service_address,
special_requirements: orderData.notes || orderData.special_requirements,
total_amount: orderData.estimated_cost || orderData.total_amount,
payment_status: 'pending'
}])
.select()
if (error) throw error
return data?.[0]
} catch (error) {
console.error('创建订单失败:', error)
return null
}
}
// 更新订单
const updateOrder = async (orderId: string, updates: Partial<Order>): Promise<Order | null> => {
try {
const { data, error } = await supabase
.from('orders')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', orderId)
.select()
if (error) throw error
return data?.[0]
} catch (error) {
console.error('更新订单失败:', error)
return null
}
}
// 删除订单
const deleteOrder = async (orderId: string): Promise<boolean> => {
try {
const { error } = await supabase
.from('orders')
.delete()
.eq('id', orderId)
if (error) throw error
return true
} catch (error) {
console.error('删除订单失败:', error)
return false
}
}
// 分配口译员
const assignInterpreter = async (orderId: string, interpreterId: string, interpreterName: string): Promise<boolean> => {
try {
const { error } = await supabase
.from('orders')
.update({
interpreter_id: interpreterId,
interpreter_name: interpreterName,
status: 'confirmed',
updated_at: new Date().toISOString()
})
.eq('id', orderId)
if (error) throw error
return true
} catch (error) {
console.error('分配口译员失败:', error)
return false
}
}
// 更新订单状态
const updateOrderStatus = async (orderId: string, status: Order['status']): Promise<boolean> => {
try {
const { error } = await supabase
.from('orders')
.update({
status,
updated_at: new Date().toISOString()
})
.eq('id', orderId)
if (error) throw error
return true
} catch (error) {
console.error('更新订单状态失败:', error)
return false
}
}
// 获取订单统计
const getOrderStats = async (): Promise<{
total: number
pending: number
inProgress: number
completed: number
cancelled: number
totalRevenue: number
}> => {
const { data, error } = await supabase
.from('orders')
.select('status, total_amount, payment_status')
if (error) {
console.error('获取订单统计失败:', error)
return {
total: 0,
pending: 0,
inProgress: 0,
completed: 0,
cancelled: 0,
totalRevenue: 0
}
}
const stats = {
total: data?.length || 0,
pending: data?.filter(o => o.status === 'pending').length || 0,
inProgress: data?.filter(o => o.status === 'in_progress').length || 0,
completed: data?.filter(o => o.status === 'completed').length || 0,
cancelled: data?.filter(o => o.status === 'cancelled').length || 0,
totalRevenue: data?.filter(o => o.payment_status === 'paid').reduce((sum, o) => sum + (Number(o.total_amount) || 0), 0) || 0
}
return stats
}
// 获取服务费率
const getServiceRates = async (): Promise<ServiceRate[]> => {
try {
const { data, error } = await supabase
.from('service_rates')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data || []
} catch (error) {
console.error('获取服务费率失败:', error)
return []
}
}
// 创建服务费率
const createServiceRate = async (rateData: Partial<ServiceRate>): Promise<ServiceRate | null> => {
try {
const newRate = {
...rateData,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
const { data, error } = await supabase
.from('service_rates')
.insert([newRate])
.select()
.single()
if (error) throw error
return data
} catch (error) {
console.error('创建服务费率失败:', error)
return null
}
}
// 更新服务费率
const updateServiceRate = async (id: string, updates: Partial<ServiceRate>): Promise<ServiceRate | null> => {
try {
const { data, error } = await supabase
.from('service_rates')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single()
if (error) throw error
return data
} catch (error) {
console.error('更新服务费率失败:', error)
return null
}
}
// 删除服务费率
const deleteServiceRate = async (id: string): Promise<boolean> => {
try {
const { error } = await supabase
.from('service_rates')
.delete()
.eq('id', id)
if (error) throw error
return true
} catch (error) {
console.error('删除服务费率失败:', error)
return false
}
}
return {
// 订单相关方法
getOrders,
getOrderById,
createOrder,
updateOrder,
deleteOrder,
assignInterpreter,
updateOrderStatus,
getOrderStats,
// 费率相关方法
getServiceRates,
createServiceRate,
updateServiceRate,
deleteServiceRate,
// 费率计算
calculateOrderCost
}
}
+81
View File
@@ -0,0 +1,81 @@
// Toast通知组合式函数
export const useToast = () => {
// 移除toast函数
const removeToast = (toastId: string) => {
const toast = document.getElementById(toastId)
if (toast) {
toast.classList.remove('opacity-100', 'translate-x-0')
toast.classList.add('opacity-0', 'translate-x-full')
setTimeout(() => {
toast.remove()
}, 300)
}
}
// 将函数添加到全局对象(仅在浏览器环境中)
if (typeof window !== 'undefined') {
(window as any).removeToast = removeToast
}
const showToast = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
// 创建toast容器(如果不存在)
let toastContainer = document.getElementById('toast-container')
if (!toastContainer) {
toastContainer = document.createElement('div')
toastContainer.id = 'toast-container'
toastContainer.className = 'fixed top-4 right-4 z-50 space-y-2'
document.body.appendChild(toastContainer)
}
// 创建toast元素
const toast = document.createElement('div')
const toastId = `toast-${Date.now()}`
toast.id = toastId
// 根据类型设置样式
const typeClasses = {
success: 'bg-green-100 border-green-500 text-green-700',
error: 'bg-red-100 border-red-500 text-red-700',
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
info: 'bg-blue-100 border-blue-500 text-blue-700'
}
const iconMap = {
success: '✓',
error: '✕',
warning: '⚠',
info: ''
}
toast.className = `flex items-center p-4 rounded-lg border-l-4 shadow-md transform transition-all duration-300 ease-in-out ${typeClasses[type]} opacity-0 translate-x-full`
toast.innerHTML = `
<div class="flex-shrink-0 mr-3">
<span class="text-lg font-bold">${iconMap[type]}</span>
</div>
<div class="flex-1">
<p class="font-medium">${message}</p>
</div>
<button onclick="removeToast('${toastId}')" class="ml-4 text-gray-400 hover:text-gray-600">
</button>
`
toastContainer.appendChild(toast)
// 显示动画
setTimeout(() => {
toast.classList.remove('opacity-0', 'translate-x-full')
toast.classList.add('opacity-100', 'translate-x-0')
}, 100)
// 自动移除
setTimeout(() => {
removeToast(toastId)
}, 5000)
}
return {
showToast
}
}