mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
183 lines
4.5 KiB
TypeScript
183 lines
4.5 KiB
TypeScript
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,
|
||
}
|
||
}
|