first commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user