213 lines
9.4 KiB
TypeScript
213 lines
9.4 KiB
TypeScript
import { ref } from 'vue'
|
||
import { useLocale } from '~/utils/locale'
|
||
|
||
interface ToastMessage {
|
||
id: number
|
||
message: string
|
||
type: 'success' | 'error' | 'info' | 'warning'
|
||
duration: number
|
||
}
|
||
|
||
const toasts = ref<ToastMessage[]>([])
|
||
let toastId = 0
|
||
|
||
const exactToastTranslations = new Map<string, string>([
|
||
['需要登录才能点歌', 'Please sign in to request songs'],
|
||
['需要登录才能投票', 'Please sign in to vote'],
|
||
['您已经为这首歌投过票了', 'You have already voted for this song'],
|
||
['需要登录才能撤回歌曲', 'Please sign in to withdraw songs'],
|
||
['需要登录才能删除歌曲', 'Please sign in to delete songs'],
|
||
['歌曲已成功删除', 'Song deleted successfully'],
|
||
['歌曲已成功删除!', 'Song deleted successfully'],
|
||
['需要登录才能标记歌曲', 'Please sign in to mark songs'],
|
||
['歌曲已成功标记为已播放!', 'Song marked as played successfully'],
|
||
['需要登录才能撤回歌曲已播放状态', 'Please sign in to revert played status'],
|
||
['歌曲已成功撤回已播放状态!', 'Song played status reverted successfully'],
|
||
['需要登录才能申请重播', 'Please sign in to request replay'],
|
||
['申请重播成功', 'Replay request submitted'],
|
||
['您已经申请过重播这首歌了', 'You have already requested replay for this song'],
|
||
['需要登录才能取消重播申请', 'Please sign in to cancel replay request'],
|
||
['已取消重播申请', 'Replay request canceled'],
|
||
['没有上一首歌曲', 'No previous song'],
|
||
['没有下一首歌曲', 'No next song'],
|
||
['播放上一首歌曲失败', 'Failed to play previous song'],
|
||
['播放下一首歌曲失败', 'Failed to play next song'],
|
||
[
|
||
'浏览器限制了自动播放,请点击播放器中的播放按钮继续播放',
|
||
'The browser blocked autoplay. Click the play button in the player to continue.'
|
||
],
|
||
[
|
||
'浏览器拦截了新标签页,请允许弹窗后重试',
|
||
'The browser blocked the new tab. Allow pop-ups and try again.'
|
||
],
|
||
['已为你打开原始链接', 'Opened the original link for you'],
|
||
['当前播放链接无效,已切换备用音源', 'Current playback link is invalid. Switched to a backup source.'],
|
||
[
|
||
'连续多首歌曲播放失败,已停止自动播放',
|
||
'Multiple songs failed to play consecutively. Autoplay has been stopped.'
|
||
],
|
||
['哔哩哔哩视频播放失败,自动跳过', 'Bilibili video playback failed. Skipping automatically.'],
|
||
['已切换为单曲循环', 'Switched to single-track loop'],
|
||
['已切换为单曲播放', 'Switched to single-track playback'],
|
||
['已切换为列表循环', 'Switched to playlist loop'],
|
||
['请先登录网易云账号后再点赞评论', 'Please sign in to NetEase Cloud Music before liking comments'],
|
||
['评论点赞失败', 'Failed to like comment'],
|
||
['导入失败', 'Import failed'],
|
||
['预览已刷新', 'Preview refreshed'],
|
||
['正在准备打印', 'Preparing print...'],
|
||
['正在准备打印...', 'Preparing print...'],
|
||
['PDF导出成功', 'PDF exported successfully'],
|
||
['正在准备PDF', 'Preparing PDF...'],
|
||
['正在准备PDF...', 'Preparing PDF...'],
|
||
['预览内容未找到', 'Preview content not found'],
|
||
['排期内容过长,将自动分段导出', 'Schedule content is too long. It will be exported in sections.'],
|
||
['图片导出成功', 'Image exported successfully'],
|
||
['当前没有排期数据,请先在排期管理中添加排期', 'No schedule data yet. Add schedules in Schedule Management first.'],
|
||
['快速合并内存不足,已切换兼容模式重试', 'Not enough memory for quick merge. Retrying in compatibility mode.'],
|
||
['导入环境配置失败', 'Failed to import environment configuration'],
|
||
['批量更新状态成功', 'Batch status updated successfully'],
|
||
['用户更新成功', 'User updated successfully'],
|
||
['用户创建成功', 'User created successfully'],
|
||
['用户删除成功', 'User deleted successfully'],
|
||
['禁止在用户管理中修改自己的账户', 'You cannot modify your own account in User Management'],
|
||
['禁止在用户管理中重置自己的密码', 'You cannot reset your own password in User Management'],
|
||
['不能删除自己的账户', 'You cannot delete your own account'],
|
||
['密码重置成功', 'Password reset successfully'],
|
||
[
|
||
'Excel处理库加载失败,请刷新页面后重试',
|
||
'Failed to load the Excel processing library. Refresh the page and try again.'
|
||
],
|
||
['歌曲已成功上传到网易云音乐云盘', 'Song uploaded to NetEase Cloud Music Drive successfully'],
|
||
[
|
||
'数据库恢复完成,3秒后将返回首页重新登录',
|
||
'Database restore completed. Returning home to sign in again in 3 seconds.'
|
||
],
|
||
['无法解析备份文件,请检查文件格式是否正确', 'Unable to parse backup file. Please check the file format.'],
|
||
['无法解析备份文件,请检查文件格式是否正确。', 'Unable to parse backup file. Please check the file format.'],
|
||
['备份创建失败:无效的响应格式', 'Backup creation failed: invalid response format'],
|
||
['备份创建失败:服务器响应格式错误', 'Backup creation failed: invalid server response format'],
|
||
['文件下载失败', 'File download failed'],
|
||
['未知错误', 'Unknown error']
|
||
])
|
||
|
||
const toastOperationTranslations = new Map<string, string>([
|
||
['播放', 'Playback'],
|
||
['上传', 'Upload'],
|
||
['预下载', 'Pre-download'],
|
||
['合并', 'Merge'],
|
||
['加载排期', 'Loading schedule'],
|
||
['打印', 'Print'],
|
||
['导出PDF', 'PDF export'],
|
||
['导出长图', 'Long image export'],
|
||
['文件下载', 'File download'],
|
||
['删除用户', 'Deleting user'],
|
||
['加载用户', 'Loading users'],
|
||
['导入备份', 'Backup import'],
|
||
['备份导入', 'Backup import'],
|
||
['创建备份', 'Backup creation'],
|
||
['保存用户', 'Saving user'],
|
||
['重置密码', 'Password reset'],
|
||
['批量更新', 'Batch update'],
|
||
['添加歌曲', 'Adding song'],
|
||
['更新歌曲', 'Updating song'],
|
||
['删除歌曲', 'Deleting song'],
|
||
['批量删除', 'Batch deletion'],
|
||
['驳回歌曲', 'Rejecting song'],
|
||
['刷新歌曲', 'Refreshing songs'],
|
||
['标记已播放', 'Marking as played'],
|
||
['标记未播放', 'Marking as unplayed'],
|
||
['导入', 'Import']
|
||
])
|
||
|
||
const translateToastDetail = (detail: string) => exactToastTranslations.get(detail.trim()) || detail
|
||
|
||
const normalizeToastMessage = (message: unknown): string => {
|
||
if (typeof message === 'string') return message
|
||
return String(message ?? '')
|
||
}
|
||
|
||
const translateToastMessage = (message: unknown, locale: string) => {
|
||
const normalizedMessage = normalizeToastMessage(message)
|
||
if (locale === 'zh-CN') return normalizedMessage
|
||
|
||
const normalized = normalizedMessage.trim()
|
||
const withoutPunctuation = normalized.replace(/[。!]$/, '')
|
||
|
||
const exactMatch = exactToastTranslations.get(normalized)
|
||
if (exactMatch) return exactMatch
|
||
|
||
const punctuationlessMatch = exactToastTranslations.get(withoutPunctuation)
|
||
if (punctuationlessMatch) return punctuationlessMatch
|
||
|
||
const dynamicRules: Array<[RegExp, (match: RegExpMatchArray) => string]> = [
|
||
[/^已取消对歌曲《(.+)》的投票$/, (match) => `Vote for "${match[1]}" canceled`],
|
||
[/^为歌曲《(.+)》投票成功!?$/, (match) => `Voted for "${match[1]}" successfully`],
|
||
[/^备份文件已下载[::]\s*(.+)$/, (match) => `Backup file downloaded: ${match[1]}`],
|
||
[/^文件下载失败[::]\s*(.+)$/, (match) => `File download failed: ${translateToastDetail(match[1])}`],
|
||
[/^成功下载\s*(\d+)\s*首歌曲$/, (match) => `Downloaded ${match[1]} songs successfully`],
|
||
[/^已完成分段导出,共\s*(\d+)\s*张图片$/, (match) => `Sectioned export completed with ${match[1]} images`],
|
||
[/^部分更新成功,(\d+)\s*个用户因权限或状态等原因跳过$/, (match) => `Partially updated successfully. ${match[1]} users were skipped due to permissions or status.`],
|
||
[/^成功更新\s*(\d+)\s*个用户的年级$/, (match) => `Updated grades for ${match[1]} users successfully`],
|
||
[/^(.+?)失败[::]\s*(.+)$/, (match) => {
|
||
const operation = toastOperationTranslations.get(match[1]) || match[1]
|
||
return `${operation} failed: ${translateToastDetail(match[2])}`
|
||
}]
|
||
]
|
||
|
||
for (const [pattern, translate] of dynamicRules) {
|
||
const match = normalized.match(pattern)
|
||
if (match) return translate(match)
|
||
}
|
||
|
||
return normalizedMessage
|
||
}
|
||
|
||
export const useToast = () => {
|
||
const { currentLocale } = useLocale()
|
||
|
||
const showToast = (
|
||
message: unknown,
|
||
type: 'success' | 'error' | 'info' | 'warning' = 'info',
|
||
duration = 3000
|
||
) => {
|
||
const id = ++toastId
|
||
const toast: ToastMessage = {
|
||
id,
|
||
message: translateToastMessage(message, currentLocale.value),
|
||
type,
|
||
duration
|
||
}
|
||
|
||
toasts.value.push(toast)
|
||
|
||
// 自动移除toast
|
||
setTimeout(() => {
|
||
removeToast(id)
|
||
}, duration)
|
||
|
||
return id
|
||
}
|
||
|
||
const removeToast = (id: number) => {
|
||
const index = toasts.value.findIndex((toast) => toast.id === id)
|
||
if (index > -1) {
|
||
toasts.value.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
const success = (message: unknown, duration?: number) => showToast(message, 'success', duration)
|
||
const error = (message: unknown, duration?: number) => showToast(message, 'error', duration)
|
||
const info = (message: unknown, duration?: number) => showToast(message, 'info', duration)
|
||
const warning = (message: unknown, duration?: number) => showToast(message, 'warning', duration)
|
||
|
||
return {
|
||
toasts,
|
||
showToast,
|
||
removeToast,
|
||
success,
|
||
error,
|
||
info,
|
||
warning
|
||
}
|
||
}
|