Skip to content

Commit 9b3c7e4

Browse files
committed
feat: 异步函数调用+ 统一提示
1 parent 44033bd commit 9b3c7e4

6 files changed

Lines changed: 363 additions & 129 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# useAsyncTip Hook 使用说明
2+
3+
useAsyncTip 用于“包一层异步函数调用 + 统一提示”。它主要解决:
4+
5+
- 每次请求都要手写 loading/success/error 提示
6+
- message 互相覆盖/误伤(只关闭当前请求的 loading)
7+
- 错误结构不统一(复用 service/request.js 的错误映射能力)
8+
9+
该实现基于 antd v6:通过 App.useApp() 注入的 message 实例(见 src/utils/message.ts)。
10+
11+
## 主要功能
12+
13+
1. 调用开始:显示 loading(带 key,可被后续更新/关闭)
14+
15+
2. 调用成功:显示 success(successText 可配置)
16+
17+
3. 调用失败:
18+
19+
- 如果你提供 onError:仅关闭 loading,并把错误交给你处理(比如跳转/上报/自定义提示)
20+
- 否则:根据错误对象提取文案并提示;同时尽量避免与 service 层重复弹错
21+
22+
## API
23+
24+
```ts
25+
useAsyncTip(fn, options?, deps?)
26+
```
27+
28+
- fn: (...args) => Promise<any> | any
29+
- options?:
30+
- loadingText?: string // loading 文案,默认 "加载中..."
31+
- successText?: string // 成功文案,默认 "完成"
32+
- onError?: (err) => void // 自定义失败处理(提供后 hook 不再默认弹错)
33+
- deps?: React.DependencyList // 可选;不传时默认依赖 [fn, options]
34+
35+
## 行为约定(结合 service/request.js)
36+
37+
- 成功判断:
38+
- 返回值是业务包裹对象且包含 code:code === 0 或 200 视为成功
39+
- 否则如果返回 { success: boolean }:success 为 true 视为成功
40+
- 错误文案:优先使用 err.message / 后端返回的 message/msg;
41+
同时复用 RequestUtils.getHttpErrorMessage/getNetworkErrorMessage 做兜底。
42+
43+
## 基本用法
44+
45+
```tsx
46+
import useAsyncTip from '@hooks/useAsyncTip'
47+
import request from '@src/service/request'
48+
49+
const save = useAsyncTip(
50+
(payload) => request.post('/api/save', payload),
51+
{ loadingText: '保存中...', successText: '保存成功' }
52+
)
53+
54+
<Button onClick={() => save({ a: 1 })}>保存</Button>
55+
```
56+
57+
## 自定义失败处理(比如跳转错误页/上报)
58+
59+
```tsx
60+
const loadDetail = useAsyncTip((id) => request.get('/api/detail', { id }), {
61+
loadingText: '加载中...',
62+
successText: '加载完成',
63+
onError: (err) => {
64+
// 这里可以做:上报、跳转、或者自己弹提示
65+
// showMessage.error('加载失败')
66+
},
67+
})
68+
```
69+
70+
## 重要提示
71+
72+
- 本 hook 使用的提示能力来自 src/utils/message.ts,要求在应用根部已正确 setMessageInstance。
73+
- 如果 service 层(request.js/http.js)已经开启了 showError,hook 默认会尽量避免重复弹错。
74+
75+
## 根组件初始化 message(与 theme.tsx 对应)
76+
77+
项目使用 antd v6 的 App 上下文来获取 message 实例,并注入到 src/utils/message.ts:
78+
79+
```tsx
80+
import { useEffect } from 'react'
81+
import { App as AntdApp } from 'antd'
82+
import { setMessageInstance } from '@utils/message'
83+
84+
const ThemeIndex = () => {
85+
const { message } = AntdApp.useApp()
86+
87+
useEffect(() => {
88+
setMessageInstance(message)
89+
}, [message])
90+
91+
return (...)
92+
}
93+
```
94+
95+
完整实现可参考:src/theme.tsx
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { useCallback, type DependencyList } from 'react'
2+
import { showMessage } from '@src/utils/message'
3+
import { RequestUtils } from '@src/service/request'
4+
5+
type AnyFn = (...args: any[]) => any
6+
type TipHandler = (val: any) => void
7+
8+
type UseAsyncTipOptions = {
9+
loadingText?: string
10+
successText?: string
11+
onError?: TipHandler
12+
}
13+
14+
const isBusinessEnvelope = (val: any) => {
15+
return !!(val && typeof val === 'object' && Object.hasOwn(val, 'code'))
16+
}
17+
18+
const isOk = (res: any) => {
19+
if (!res || typeof res !== 'object') return !!res
20+
// 兼容 { success: true/false }
21+
if (Object.hasOwn(res, 'success')) return !!res.success
22+
// 兼容 { code: 0/200 }
23+
if (isBusinessEnvelope(res)) return res.code === 0 || res.code === 200
24+
// 兼容 axios error 分支可能返回 { status: 4xx/5xx, data: {...} }
25+
if (Object.hasOwn(res, 'status')) return res.status >= 200 && res.status < 300
26+
return true
27+
}
28+
29+
const getErrorMessage = (err: any) => {
30+
if (!err) return '请求失败'
31+
if (typeof err === 'string') return err
32+
33+
// request.js 的自定义错误可能带 status / code / response
34+
const status = err.status || err.response?.status
35+
if (typeof status === 'number') {
36+
return err.message || RequestUtils.getHttpErrorMessage(status) || '请求失败'
37+
}
38+
39+
// axios 网络错误会有 code
40+
if (typeof err.code === 'string') {
41+
return err.message || RequestUtils.getNetworkErrorMessage(err.code) || '网络连接异常'
42+
}
43+
44+
// axios interceptor 里可能 resolve(err.response) 或抛出 { data: ... }
45+
const data = err.data || err.response?.data
46+
if (data && typeof data === 'object') {
47+
return data.message || data.msg || data.error || data.code || err.message || '请求失败'
48+
}
49+
50+
return err.message || err.msg || (err.error && (err.error.message || err.error.code)) || '请求失败'
51+
}
52+
53+
const shouldShowErrorByDefault = (err: any) => {
54+
// request.js/http.js 默认会 showError/isShowError=true 并自行 showMessage.error
55+
// 这里尽量避免重复弹错:如果错误包含明显的 service 痕迹(status/code/response),默认不再重复提示
56+
if (err && typeof err === 'object') {
57+
if ('response' in err || 'status' in err || 'code' in err) return false
58+
}
59+
return true
60+
}
61+
62+
export default (fn: AnyFn, options?: UseAsyncTipOptions, deps?: DependencyList) => {
63+
const callback = useCallback(
64+
(...args: any[]) => {
65+
const key = `async-tip-${Date.now()}-${Math.random().toString(16).slice(2)}`
66+
const loadingText = options?.loadingText || '加载中...'
67+
showMessage.loading(loadingText, key)
68+
69+
let handled = false
70+
71+
return Promise.resolve()
72+
.then(() => fn(...args))
73+
.then((res) => {
74+
if (isOk(res)) {
75+
showMessage.open({
76+
type: 'success',
77+
content: options?.successText || '完成',
78+
key,
79+
duration: 0.5,
80+
})
81+
return res
82+
}
83+
84+
handled = true
85+
if (typeof options?.onError === 'function') {
86+
showMessage.destroy(key)
87+
options.onError(res)
88+
} else if (shouldShowErrorByDefault(res)) {
89+
showMessage.open({ type: 'error', content: getErrorMessage(res), key, duration: 2 })
90+
} else {
91+
showMessage.destroy(key)
92+
}
93+
return Promise.reject(res)
94+
})
95+
.catch((err) => {
96+
if (!handled) {
97+
if (typeof options?.onError === 'function') {
98+
showMessage.destroy(key)
99+
options.onError(err)
100+
} else if (shouldShowErrorByDefault(err)) {
101+
showMessage.open({ type: 'error', content: getErrorMessage(err), key, duration: 2 })
102+
} else {
103+
showMessage.destroy(key)
104+
}
105+
}
106+
return Promise.reject(err)
107+
})
108+
},
109+
Array.isArray(deps) ? deps : [fn, options]
110+
)
111+
return callback
112+
}
Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,51 @@
1-
// FixTabPanel 组件使用说明
2-
//
3-
// FixTabPanel 是一个增强的可滚动面板组件,集成了滚动进度条功能。
4-
// 进度条使用粘性定位(sticky),在滚动时会固定在容器顶部。
5-
//
6-
// ## 基本用法:
7-
// `jsx
8-
// import FixTabPanel from '@stateless/FixTabPanel'
9-
//
10-
// <FixTabPanel style={{ height: '400px' }}>
11-
// <div>可滚动的内容</div>
12-
// </FixTabPanel>
13-
// `
14-
//
15-
// ## 高级用法:
16-
// `jsx
17-
// <FixTabPanel
18-
// showScrollProgress={true}
19-
// scrollProgressProps={{
20-
// height: 4,
21-
// color: '#ff6b6b',
22-
// position: 'sticky',
23-
// springConfig: { stiffness: 200 }
24-
// }}
25-
// className="custom-panel"
26-
// style={{ height: '400px', border: '1px solid #ccc' }}
27-
// >
28-
// <div>内容</div>
29-
// </FixTabPanel>
30-
// `
31-
//
32-
// ## Props:
33-
// - `showScrollProgress?: boolean` - 是否显示滚动进度条(默认 true)
34-
// - `scrollProgressProps?: object` - 滚动进度条的配置属性
35-
// - 继承所有 `div` 的 HTML 属性
36-
//
37-
// ## 重要提示:
38-
// - **必须设置高度**: FixTabPanel 需要明确的 `height` 样式才能正确显示滚动条
39-
// - **粘性定位**: 进度条使用 `position: sticky`,会在滚动时固定在容器顶部
40-
//
41-
// ## 特性:
42-
// - ✅ 相对定位容器
43-
// - ✅ 粘性定位滚动进度条
44-
// - ✅ 返回顶部按钮
45-
// - ✅ 可自定义进度条样式
46-
// - ✅ 支持所有标准 div 属性
1+
# FixTabPanel 组件使用说明
2+
3+
FixTabPanel 是一个增强的可滚动面板组件,集成了滚动进度条功能。
4+
进度条使用粘性定位(sticky),在滚动时会固定在容器顶部。
5+
6+
## 基本用法:
7+
8+
```jsx
9+
import FixTabPanel from '@stateless/FixTabPanel'
10+
11+
;<FixTabPanel style={{ height: '400px' }}>
12+
<div>可滚动的内容</div>
13+
</FixTabPanel>
14+
```
15+
16+
## 高级用法:
17+
18+
```jsx
19+
<FixTabPanel
20+
showScrollProgress={true}
21+
scrollProgressProps={{
22+
height: 4,
23+
color: '#ff6b6b',
24+
position: 'sticky',
25+
springConfig: { stiffness: 200 },
26+
}}
27+
className="custom-panel"
28+
style={{ height: '400px', border: '1px solid #ccc' }}
29+
>
30+
<div>内容</div>
31+
</FixTabPanel>
32+
```
33+
34+
## Props:
35+
36+
- `showScrollProgress?: boolean` - 是否显示滚动进度条(默认 true)
37+
- `scrollProgressProps?: object` - 滚动进度条的配置属性
38+
- 继承所有 `div` 的 HTML 属性
39+
40+
## 重要提示:
41+
42+
- **必须设置高度**: FixTabPanel 需要明确的 `height` 样式才能正确显示滚动条
43+
- **粘性定位**: 进度条使用 `position: sticky`,会在滚动时固定在容器顶部
44+
45+
## 特性:
46+
47+
- ✅ 相对定位容器
48+
- ✅ 粘性定位滚动进度条
49+
- ✅ 返回顶部按钮
50+
- ✅ 可自定义进度条样式
51+
- ✅ 支持所有标准 div 属性

0 commit comments

Comments
 (0)