mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
Initial commit
This commit is contained in:
182
frontend/src/composables/useAsyncAction.ts
Normal file
182
frontend/src/composables/useAsyncAction.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
/**
|
||||
* 异步操作通用逻辑
|
||||
*
|
||||
* 统一处理:
|
||||
* - Loading 状态管理
|
||||
* - try/catch 错误处理
|
||||
* - Toast 通知(成功/失败)
|
||||
* - 可选的成功/失败回调
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { loading, execute } = useAsyncAction()
|
||||
*
|
||||
* // 简单用法
|
||||
* await execute(() => api.deleteItem(id), {
|
||||
* successMessage: '删除成功',
|
||||
* })
|
||||
*
|
||||
* // 带回调
|
||||
* await execute(() => api.createItem(data), {
|
||||
* successMessage: '创建成功',
|
||||
* onSuccess: (result) => {
|
||||
* router.push(`/items/${result.id}`)
|
||||
* },
|
||||
* })
|
||||
*
|
||||
* // 自定义错误消息
|
||||
* await execute(() => api.updateItem(id, data), {
|
||||
* successMessage: '更新成功',
|
||||
* errorMessage: '更新失败,请重试',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface UseAsyncActionOptions<T> {
|
||||
/** 成功时显示的消息 */
|
||||
successMessage?: string
|
||||
/** 成功消息的标题 */
|
||||
successTitle?: string
|
||||
/** 失败时显示的消息(如果不提供,将解析 API 错误) */
|
||||
errorMessage?: string
|
||||
/** 失败消息的标题 */
|
||||
errorTitle?: string
|
||||
/** 成功时的回调 */
|
||||
onSuccess?: (result: T) => void
|
||||
/** 失败时的回调 */
|
||||
onError?: (error: unknown) => void
|
||||
/** 是否在失败时显示 toast(默认 true) */
|
||||
showErrorToast?: boolean
|
||||
/** 是否在成功时显示 toast(默认:有 successMessage 时为 true) */
|
||||
showSuccessToast?: boolean
|
||||
}
|
||||
|
||||
export interface UseAsyncActionReturn {
|
||||
/** 是否正在执行 */
|
||||
loading: Ref<boolean>
|
||||
/** 执行异步操作 */
|
||||
execute: <T>(
|
||||
action: () => Promise<T>,
|
||||
options?: UseAsyncActionOptions<T>
|
||||
) => Promise<T | undefined>
|
||||
}
|
||||
|
||||
export function useAsyncAction(): UseAsyncActionReturn {
|
||||
const loading = ref(false)
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
async function execute<T>(
|
||||
action: () => Promise<T>,
|
||||
options?: UseAsyncActionOptions<T>
|
||||
): Promise<T | undefined> {
|
||||
const {
|
||||
successMessage,
|
||||
successTitle,
|
||||
errorMessage,
|
||||
errorTitle = '错误',
|
||||
onSuccess,
|
||||
onError,
|
||||
showErrorToast = true,
|
||||
showSuccessToast,
|
||||
} = options || {}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await action()
|
||||
|
||||
// 显示成功消息
|
||||
const shouldShowSuccess = showSuccessToast ?? !!successMessage
|
||||
if (shouldShowSuccess && successMessage) {
|
||||
success(successMessage, successTitle)
|
||||
}
|
||||
|
||||
// 调用成功回调
|
||||
onSuccess?.(result)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
// 解析错误消息
|
||||
const message = errorMessage || parseApiError(error, '操作失败')
|
||||
|
||||
// 显示错误消息
|
||||
if (showErrorToast) {
|
||||
showError(message, errorTitle)
|
||||
}
|
||||
|
||||
// 调用错误回调
|
||||
onError?.(error)
|
||||
|
||||
return undefined
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
execute,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多个独立的异步操作
|
||||
*
|
||||
* 当需要在同一个组件中跟踪多个独立的 loading 状态时使用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { actions, isAnyLoading } = useMultipleAsyncActions(['save', 'delete', 'refresh'])
|
||||
*
|
||||
* // 使用各自的 loading 状态
|
||||
* await actions.save.execute(() => api.save(data), { successMessage: '保存成功' })
|
||||
* await actions.delete.execute(() => api.delete(id), { successMessage: '删除成功' })
|
||||
*
|
||||
* // 检查是否有任何操作正在进行
|
||||
* <Button :disabled="isAnyLoading">操作</Button>
|
||||
* ```
|
||||
*/
|
||||
export function useMultipleAsyncActions<K extends string>(
|
||||
keys: K[]
|
||||
): {
|
||||
actions: Record<K, UseAsyncActionReturn>
|
||||
isAnyLoading: Ref<boolean>
|
||||
} {
|
||||
const actions = {} as Record<K, UseAsyncActionReturn>
|
||||
const loadingStates: Ref<boolean>[] = []
|
||||
|
||||
for (const key of keys) {
|
||||
const action = useAsyncAction()
|
||||
actions[key] = action
|
||||
loadingStates.push(action.loading)
|
||||
}
|
||||
|
||||
const isAnyLoading = ref(false)
|
||||
|
||||
// 使用 watchEffect 来监听所有 loading 状态
|
||||
// 这里简化处理,在每次 execute 时会自动更新
|
||||
// 如果需要响应式,可以使用 computed
|
||||
const checkAnyLoading = () => {
|
||||
isAnyLoading.value = loadingStates.some((state) => state.value)
|
||||
}
|
||||
|
||||
// 包装每个 action 的 execute 以更新 isAnyLoading
|
||||
for (const key of keys) {
|
||||
const originalExecute = actions[key].execute
|
||||
actions[key].execute = async (action, options) => {
|
||||
checkAnyLoading()
|
||||
try {
|
||||
return await originalExecute(action, options)
|
||||
} finally {
|
||||
checkAnyLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
actions,
|
||||
isAnyLoading,
|
||||
}
|
||||
}
|
||||
43
frontend/src/composables/useClipboard.ts
Normal file
43
frontend/src/composables/useClipboard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useToast } from './useToast'
|
||||
|
||||
export function useClipboard() {
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
success('已复制到剪贴板')
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback for non-secure contexts
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-999999px'
|
||||
textArea.style.top = '-999999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
if (successful) {
|
||||
success('已复制到剪贴板')
|
||||
return true
|
||||
}
|
||||
showError('复制失败,请手动复制')
|
||||
return false
|
||||
} finally {
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
showError('复制失败,请手动选择文本进行复制')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return { copyToClipboard }
|
||||
}
|
||||
112
frontend/src/composables/useConfirm.ts
Normal file
112
frontend/src/composables/useConfirm.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type ConfirmVariant = 'danger' | 'destructive' | 'warning' | 'info' | 'question'
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title?: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
variant?: ConfirmVariant
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
isOpen: boolean
|
||||
resolve?: (value: boolean) => void
|
||||
}
|
||||
|
||||
const state = ref<ConfirmState>({
|
||||
isOpen: false,
|
||||
message: '',
|
||||
title: '确认操作',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
variant: 'question'
|
||||
})
|
||||
|
||||
export function useConfirm() {
|
||||
/**
|
||||
* 显示确认对话框
|
||||
* @param options 对话框选项
|
||||
* @returns Promise<boolean> - true表示确认,false表示取消
|
||||
*/
|
||||
const confirm = (options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
state.value = {
|
||||
isOpen: true,
|
||||
title: options.title || '确认操作',
|
||||
message: options.message,
|
||||
confirmText: options.confirmText || '确认',
|
||||
cancelText: options.cancelText || '取消',
|
||||
variant: options.variant || 'question',
|
||||
resolve
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法:危险操作确认(红色主题)
|
||||
*/
|
||||
const confirmDanger = (message: string, title?: string): Promise<boolean> => {
|
||||
return confirm({
|
||||
message,
|
||||
title: title || '危险操作',
|
||||
confirmText: '删除',
|
||||
variant: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法:警告确认(黄色主题)
|
||||
*/
|
||||
const confirmWarning = (message: string, title?: string): Promise<boolean> => {
|
||||
return confirm({
|
||||
message,
|
||||
title: title || '警告',
|
||||
confirmText: '继续',
|
||||
variant: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法:信息确认(蓝色主题)
|
||||
*/
|
||||
const confirmInfo = (message: string, title?: string): Promise<boolean> => {
|
||||
return confirm({
|
||||
message,
|
||||
title: title || '提示',
|
||||
confirmText: '确定',
|
||||
variant: 'info'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理确认
|
||||
*/
|
||||
const handleConfirm = () => {
|
||||
if (state.value.resolve) {
|
||||
state.value.resolve(true)
|
||||
}
|
||||
state.value.isOpen = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理取消
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
if (state.value.resolve) {
|
||||
state.value.resolve(false)
|
||||
}
|
||||
state.value.isOpen = false
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
confirm,
|
||||
confirmDanger,
|
||||
confirmWarning,
|
||||
confirmInfo,
|
||||
handleConfirm,
|
||||
handleCancel
|
||||
}
|
||||
}
|
||||
132
frontend/src/composables/useDarkMode.ts
Normal file
132
frontend/src/composables/useDarkMode.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { effectScope, ref, watch, computed } from 'vue'
|
||||
|
||||
const THEME_STORAGE_KEY = 'theme'
|
||||
|
||||
// 主题模式类型
|
||||
export type ThemeMode = 'system' | 'light' | 'dark'
|
||||
|
||||
// 全局共享的状态
|
||||
const themeMode = ref<ThemeMode>('system')
|
||||
const isDark = ref(false)
|
||||
let initialized = false
|
||||
let scope: ReturnType<typeof effectScope> | null = null
|
||||
let mediaQuery: MediaQueryList | null = null
|
||||
|
||||
const applyDarkMode = (value: boolean) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle('dark', value)
|
||||
|
||||
if (document.body) {
|
||||
document.body.setAttribute('theme-mode', value ? 'dark' : 'light')
|
||||
}
|
||||
}
|
||||
|
||||
const getSystemPreference = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
const updateDarkMode = () => {
|
||||
if (themeMode.value === 'system') {
|
||||
isDark.value = getSystemPreference()
|
||||
} else {
|
||||
isDark.value = themeMode.value === 'dark'
|
||||
}
|
||||
applyDarkMode(isDark.value)
|
||||
}
|
||||
|
||||
const handleSystemChange = (e: MediaQueryListEvent) => {
|
||||
if (themeMode.value === 'system') {
|
||||
isDark.value = e.matches
|
||||
applyDarkMode(isDark.value)
|
||||
}
|
||||
}
|
||||
|
||||
const ensureWatcher = () => {
|
||||
if (scope) {
|
||||
return
|
||||
}
|
||||
|
||||
scope = effectScope(true)
|
||||
scope.run(() => {
|
||||
watch(
|
||||
themeMode,
|
||||
(value) => {
|
||||
updateDarkMode()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, value)
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const initialize = () => {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
initialized = true
|
||||
ensureWatcher()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedTheme = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode | null
|
||||
|
||||
if (storedTheme === 'dark' || storedTheme === 'light' || storedTheme === 'system') {
|
||||
themeMode.value = storedTheme
|
||||
} else {
|
||||
// 兼容旧版本存储格式,旧版本直接存储 'dark' 或 'light'
|
||||
themeMode.value = 'system'
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addEventListener('change', handleSystemChange)
|
||||
}
|
||||
|
||||
updateDarkMode()
|
||||
}
|
||||
|
||||
export function useDarkMode() {
|
||||
initialize()
|
||||
ensureWatcher()
|
||||
applyDarkMode(isDark.value)
|
||||
|
||||
const setDarkMode = (value: boolean) => {
|
||||
themeMode.value = value ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const setThemeMode = (mode: ThemeMode) => {
|
||||
themeMode.value = mode
|
||||
}
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
// 循环切换:system -> light -> dark -> system
|
||||
if (themeMode.value === 'system') {
|
||||
themeMode.value = 'light'
|
||||
} else if (themeMode.value === 'light') {
|
||||
themeMode.value = 'dark'
|
||||
} else {
|
||||
themeMode.value = 'system'
|
||||
}
|
||||
}
|
||||
|
||||
// 是否为跟随系统模式
|
||||
const isSystemMode = computed(() => themeMode.value === 'system')
|
||||
|
||||
return {
|
||||
isDark,
|
||||
themeMode,
|
||||
isSystemMode,
|
||||
toggleDarkMode,
|
||||
setDarkMode,
|
||||
setThemeMode
|
||||
}
|
||||
}
|
||||
115
frontend/src/composables/useFormDialog.ts
Normal file
115
frontend/src/composables/useFormDialog.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
||||
|
||||
/**
|
||||
* 表单对话框通用逻辑
|
||||
*
|
||||
* 统一处理:
|
||||
* - 编辑/创建模式切换
|
||||
* - 对话框打开/关闭
|
||||
* - 表单重置
|
||||
* - 数据加载
|
||||
* - Loading 状态阻止关闭
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
|
||||
* isOpen: () => props.modelValue,
|
||||
* entity: () => props.provider,
|
||||
* isLoading: loading,
|
||||
* onClose: () => emit('update:modelValue', false),
|
||||
* loadData: loadProviderData,
|
||||
* resetForm,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface UseFormDialogOptions<E> {
|
||||
/** 对话框是否打开(getter 函数) */
|
||||
isOpen: () => boolean
|
||||
/** 编辑的实体(getter 函数,返回 null/undefined 表示创建模式) */
|
||||
entity: () => E | null | undefined
|
||||
/** 是否处于加载状态 */
|
||||
isLoading: Ref<boolean> | ComputedRef<boolean>
|
||||
/** 关闭对话框的回调 */
|
||||
onClose: () => void
|
||||
/** 加载实体数据的函数(编辑模式时调用) */
|
||||
loadData: () => void
|
||||
/** 重置表单的函数 */
|
||||
resetForm: () => void
|
||||
/** 额外的加载状态(如清理缓存等),可选 */
|
||||
extraLoadingStates?: Array<Ref<boolean> | ComputedRef<boolean>>
|
||||
}
|
||||
|
||||
export interface UseFormDialogReturn {
|
||||
/** 是否为编辑模式 */
|
||||
isEditMode: ComputedRef<boolean>
|
||||
/** 处理对话框更新事件(用于 @update:model-value) */
|
||||
handleDialogUpdate: (value: boolean) => void
|
||||
/** 处理取消按钮点击 */
|
||||
handleCancel: () => void
|
||||
}
|
||||
|
||||
export function useFormDialog<E>(
|
||||
options: UseFormDialogOptions<E>
|
||||
): UseFormDialogReturn {
|
||||
const {
|
||||
isOpen,
|
||||
entity,
|
||||
isLoading,
|
||||
onClose,
|
||||
loadData,
|
||||
resetForm,
|
||||
extraLoadingStates = [],
|
||||
} = options
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEditMode = computed(() => !!entity())
|
||||
|
||||
// 检查是否有任何加载状态
|
||||
const isAnyLoading = computed(() => {
|
||||
if (isLoading.value) return true
|
||||
return extraLoadingStates.some((state) => state.value)
|
||||
})
|
||||
|
||||
// 处理对话框更新事件
|
||||
function handleDialogUpdate(value: boolean) {
|
||||
// 加载中不允许关闭
|
||||
if (!value && isAnyLoading.value) {
|
||||
return
|
||||
}
|
||||
// 关闭时重置表单
|
||||
if (!value) {
|
||||
resetForm()
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 处理取消按钮
|
||||
function handleCancel() {
|
||||
if (isAnyLoading.value) return
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 监听打开状态变化
|
||||
watch(isOpen, (val) => {
|
||||
if (val) {
|
||||
if (isEditMode.value && entity()) {
|
||||
loadData()
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听实体变化(编辑模式切换)
|
||||
watch(entity, (newEntity) => {
|
||||
if (newEntity && isOpen()) {
|
||||
loadData()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
return {
|
||||
isEditMode,
|
||||
handleDialogUpdate,
|
||||
handleCancel,
|
||||
}
|
||||
}
|
||||
64
frontend/src/composables/useRowClick.ts
Normal file
64
frontend/src/composables/useRowClick.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 处理表格行点击的 composable
|
||||
* 用于在点击行时打开详情抽屉,同时排除文本选择操作
|
||||
*/
|
||||
export function useRowClick() {
|
||||
// 记录 mousedown 时的位置和选中状态
|
||||
const mouseDownPos = ref<{ x: number; y: number } | null>(null)
|
||||
const hadSelectionOnMouseDown = ref(false)
|
||||
|
||||
/**
|
||||
* 处理 mousedown 事件,记录位置和选中状态
|
||||
*/
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
mouseDownPos.value = { x: event.clientX, y: event.clientY }
|
||||
const selection = window.getSelection()
|
||||
hadSelectionOnMouseDown.value = !!(selection && selection.toString().trim().length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该触发行点击事件
|
||||
* 如果用户正在选择文本或取消选中,则返回 false
|
||||
*/
|
||||
function shouldTriggerRowClick(event?: MouseEvent): boolean {
|
||||
// 如果 mousedown 时已有选中文本,说明用户可能是在取消选中
|
||||
if (hadSelectionOnMouseDown.value) {
|
||||
hadSelectionOnMouseDown.value = false
|
||||
mouseDownPos.value = null
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果鼠标移动超过阈值,说明用户在拖动选择
|
||||
if (event && mouseDownPos.value) {
|
||||
const dx = Math.abs(event.clientX - mouseDownPos.value.x)
|
||||
const dy = Math.abs(event.clientY - mouseDownPos.value.y)
|
||||
if (dx > 5 || dy > 5) {
|
||||
mouseDownPos.value = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
mouseDownPos.value = null
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个行点击处理函数
|
||||
* @param callback 当应该触发点击时执行的回调
|
||||
*/
|
||||
function createRowClickHandler<T>(callback: (item: T) => void) {
|
||||
return (event: MouseEvent, item: T) => {
|
||||
if (shouldTriggerRowClick(event)) {
|
||||
callback(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleMouseDown,
|
||||
shouldTriggerRowClick,
|
||||
createRowClickHandler
|
||||
}
|
||||
}
|
||||
75
frontend/src/composables/useToast.ts
Normal file
75
frontend/src/composables/useToast.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ref } from 'vue'
|
||||
import { TOAST_CONFIG } from '@/config/constants'
|
||||
|
||||
export type ToastVariant = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface Toast {
|
||||
id: string
|
||||
title?: string
|
||||
message?: string
|
||||
variant?: ToastVariant
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const toasts = ref<Toast[]>([])
|
||||
|
||||
export function useToast() {
|
||||
function showToast(options: Omit<Toast, 'id'>) {
|
||||
const toast: Toast = {
|
||||
id: Date.now().toString(),
|
||||
variant: 'info',
|
||||
duration: 5000,
|
||||
...options
|
||||
}
|
||||
|
||||
|
||||
toasts.value.push(toast)
|
||||
|
||||
// 注释掉这里的 setTimeout,因为现在由组件自己处理
|
||||
// if (toast.duration && toast.duration > 0) {
|
||||
// setTimeout(() => {
|
||||
// removeToast(toast.id)
|
||||
// }, toast.duration)
|
||||
// }
|
||||
|
||||
return toast.id
|
||||
}
|
||||
|
||||
function removeToast(id: string) {
|
||||
const index = toasts.value.findIndex(t => t.id === id)
|
||||
if (index > -1) {
|
||||
toasts.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function success(message: string, title?: string) {
|
||||
return showToast({ message, title, variant: 'success', duration: TOAST_CONFIG.SUCCESS_DURATION })
|
||||
}
|
||||
|
||||
function error(message: string, title?: string) {
|
||||
return showToast({ message, title, variant: 'error', duration: TOAST_CONFIG.ERROR_DURATION })
|
||||
}
|
||||
|
||||
function warning(message: string, title?: string) {
|
||||
return showToast({ message, title, variant: 'warning', duration: TOAST_CONFIG.WARNING_DURATION })
|
||||
}
|
||||
|
||||
function info(message: string, title?: string) {
|
||||
return showToast({ message, title, variant: 'info', duration: TOAST_CONFIG.INFO_DURATION })
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
toasts.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
toasts,
|
||||
showToast,
|
||||
removeToast,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
clearAll
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user