Skip to content

Commit 07f4274

Browse files
committed
fix resend
1 parent 34f958b commit 07f4274

9 files changed

Lines changed: 304 additions & 414 deletions

File tree

bun.lock

Lines changed: 163 additions & 390 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,33 @@
1212
"@ant-design/charts": "^2.6.7",
1313
"@ant-design/icons": "^6.1.0",
1414
"@rsbuild/core": "^1.7.3",
15-
"@rsbuild/plugin-react": "^1.4.5",
16-
"@rsbuild/plugin-svgr": "^1.3.0",
17-
"@tanstack/react-query": "^5.90.21",
18-
"antd": "^6.3.1",
19-
"dayjs": "^1.11.19",
15+
"@rsbuild/plugin-react": "^1.4.6",
16+
"@rsbuild/plugin-svgr": "^1.3.1",
17+
"@tanstack/react-query": "^5.95.2",
18+
"antd": "^6.3.4",
19+
"dayjs": "^1.11.20",
2020
"git-url-parse": "^16.1.0",
2121
"hash-wasm": "^4.12.0",
2222
"history": "^5.3.0",
2323
"json-diff-kit": "^1.0.35",
2424
"react": "^19.2.4",
2525
"react-dom": "^19.2.4",
26-
"react-router-dom": "^7.13.1",
26+
"react-router-dom": "^7.13.2",
2727
"ua-parser-js": "^2.0.9",
2828
"vanilla-jsoneditor": "^3.11.0",
2929
"xlsx": "^0.18.5"
3030
},
3131
"devDependencies": {
32-
"@biomejs/biome": "2.4.5",
33-
"@tailwindcss/postcss": "^4.2.1",
32+
"@biomejs/biome": "2.4.8",
33+
"@tailwindcss/postcss": "^4.2.2",
3434
"@types/git-url-parse": "^16.0.2",
35-
"@types/node": "^25.3.3",
35+
"@types/node": "^25.5.0",
3636
"@types/react": "^19",
3737
"@types/react-dom": "^19",
3838
"@types/react-router-dom": "^5.3.3",
3939
"mitata": "^1.0.34",
40-
"tailwindcss": "^4.2.1",
41-
"typescript": "^5.9.3"
40+
"tailwindcss": "^4.2.2",
41+
"typescript": "^6.0.2"
4242
},
4343
"trustedDependencies": [
4444
"core-js"

src/constants/local-storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const activationEmailResendCooldownStorageKey =
2+
'activation-email-resend-sent-at';

src/pages/inactivated.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useMutation } from '@tanstack/react-query';
22
import { Button, message, Result } from 'antd';
33
import { useEffect } from 'react';
4+
import { activationEmailResendCooldownStorageKey } from '@/constants/local-storage';
45
import { api } from '@/services/api';
56
import { getUserEmail } from '@/services/auth';
7+
import { useLocalStorageCooldown } from '@/utils/hooks';
68
import { rootRouterPath, router } from '../router';
79

810
export const Inactivated = () => {
@@ -11,9 +13,17 @@ export const Inactivated = () => {
1113
router.navigate(rootRouterPath.login);
1214
}
1315
}, []);
16+
17+
const { isCoolingDown, remainingSeconds, startCooldown } =
18+
useLocalStorageCooldown({
19+
storageKey: activationEmailResendCooldownStorageKey,
20+
durationMs: 60_000,
21+
});
22+
1423
const { mutate: sendEmail, isPending } = useMutation({
1524
mutationFn: () => api.sendEmail({ email: getUserEmail() }),
1625
onSuccess: () => {
26+
startCooldown();
1727
message.info('邮件发送成功,请注意查收');
1828
},
1929
onError: () => {
@@ -30,8 +40,9 @@ export const Inactivated = () => {
3040
type="primary"
3141
onClick={() => sendEmail()}
3242
loading={isPending}
43+
disabled={isCoolingDown}
3344
>
34-
再次发送
45+
{isCoolingDown ? `${remainingSeconds}s 后可再次发送` : '再次发送'}
3546
</Button>,
3647
<Button key="back" href="/user">
3748
返回登录

src/pages/welcome.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useMutation } from '@tanstack/react-query';
22
import { Button, message, Result } from 'antd';
33
import { useEffect } from 'react';
4+
import { activationEmailResendCooldownStorageKey } from '@/constants/local-storage';
45
import { api } from '@/services/api';
56
import { getUserEmail } from '@/services/auth';
7+
import { useLocalStorageCooldown } from '@/utils/hooks';
68
import { rootRouterPath, router } from '../router';
79

810
export const Welcome = () => {
@@ -12,9 +14,16 @@ export const Welcome = () => {
1214
}
1315
}, []);
1416

17+
const { isCoolingDown, remainingSeconds, startCooldown } =
18+
useLocalStorageCooldown({
19+
storageKey: activationEmailResendCooldownStorageKey,
20+
durationMs: 60_000,
21+
});
22+
1523
const { mutate: sendEmail, isPending } = useMutation({
1624
mutationFn: () => api.sendEmail({ email: getUserEmail() }),
1725
onSuccess: () => {
26+
startCooldown();
1827
message.info('邮件发送成功,请注意查收');
1928
},
2029
onError: () => {
@@ -36,8 +45,13 @@ export const Welcome = () => {
3645
}
3746
subTitle="如未收到激活邮件,请点击"
3847
extra={
39-
<Button type="primary" onClick={() => sendEmail()} loading={isPending}>
40-
重新发送
48+
<Button
49+
type="primary"
50+
onClick={() => sendEmail()}
51+
loading={isPending}
52+
disabled={isCoolingDown}
53+
>
54+
{isCoolingDown ? `${remainingSeconds}s 后可再次发送` : '重新发送'}
4155
</Button>
4256
}
4357
/>

src/services/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import request from './request';
44

55
export const api = {
66
login: (params: { email: string; pwd: string }) =>
7-
request<{ token: string }>('post', '/user/login', params),
7+
request<{ token: string }>('post', '/user/login', params, {
8+
suppressErrorToast: true,
9+
}),
810
activate: (params: { token: string }) =>
911
request('post', '/user/activate', params),
1012
me: () => request<User>('get', '/user/me'),

src/services/auth.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { message } from 'antd';
33
import { md5 } from 'hash-wasm';
44
import { rootRouterPath, router } from '@/router';
55
import { api } from '@/services/api';
6-
import { setToken } from '@/services/request';
6+
import { RequestError, setToken } from '@/services/request';
77

88
let _email = '';
99
export const setUserEmail = (email: string) => {
@@ -26,11 +26,11 @@ export async function login(email: string, password: string) {
2626
router.navigate(loginFrom || rootRouterPath.user);
2727
}
2828
} catch (err) {
29-
const e = err as Error;
30-
if (e.message.startsWith('423:')) {
29+
if (err instanceof RequestError && err.status === 423) {
3130
router.navigate(rootRouterPath.inactivated);
3231
} else {
33-
message.error(e.message);
32+
const errorMessage = err instanceof Error ? err.message : '登录失败';
33+
message.error(errorMessage);
3434
}
3535
}
3636
}

src/services/request.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,25 @@ interface PushyResponse {
4343
message?: string;
4444
}
4545

46+
export class RequestError extends Error {
47+
status?: number;
48+
49+
constructor(message: string, status?: number) {
50+
super(message);
51+
this.name = 'RequestError';
52+
this.status = status;
53+
}
54+
}
55+
56+
export interface RequestOptions {
57+
suppressErrorToast?: boolean;
58+
}
59+
4660
export default async function request<T extends Record<any, any>>(
4761
method: 'get' | 'post' | 'put' | 'delete',
4862
path: string,
4963
params?: Record<any, any>,
64+
requestOptions: RequestOptions = {},
5065
) {
5166
const headers: HeadersInit = {};
5267
const options: RequestInit = { method, headers };
@@ -75,14 +90,26 @@ export default async function request<T extends Record<any, any>>(
7590
return json as T & PushyResponse;
7691
}
7792

78-
message.error(json.message);
79-
throw Error(`${response.status}: ${json.message}`);
93+
const error = new RequestError(
94+
json.message || `Request failed with status ${response.status}`,
95+
response.status,
96+
);
97+
if (!requestOptions.suppressErrorToast && error.message) {
98+
message.error(error.message);
99+
}
100+
throw error;
80101
} catch (err) {
102+
if (err instanceof RequestError) {
103+
throw err;
104+
}
105+
81106
if ((err as Error).message.includes('Unauthorized')) {
82107
logout();
83108
} else {
84-
message.error(`错误:${(err as Error).message}`);
85-
message.error('如有使用代理,请关闭代理后重试');
109+
if (!requestOptions.suppressErrorToast) {
110+
message.error(`错误:${(err as Error).message}`);
111+
message.error('如有使用代理,请关闭代理后重试');
112+
}
86113
throw err;
87114
}
88115
}

src/utils/hooks.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,72 @@ import relativeTime from 'dayjs/plugin/relativeTime';
44
import { api } from '@/services/api';
55
import { getToken } from '@/services/request';
66
import 'dayjs/locale/zh-cn';
7-
import { useMemo } from 'react';
7+
import { useEffect, useMemo, useState } from 'react';
88

99
dayjs.locale('zh-cn');
1010
dayjs.extend(relativeTime);
1111

12+
const getCooldownRemainingSeconds = (
13+
storageKey: string,
14+
durationMs: number,
15+
) => {
16+
const storedSentAt = window.localStorage.getItem(storageKey);
17+
const sentAt = Number(storedSentAt);
18+
19+
if (!Number.isFinite(sentAt) || sentAt <= 0) {
20+
return 0;
21+
}
22+
23+
const remainingMs = durationMs - (Date.now() - sentAt);
24+
if (remainingMs <= 0) {
25+
window.localStorage.removeItem(storageKey);
26+
return 0;
27+
}
28+
29+
return Math.ceil(remainingMs / 1000);
30+
};
31+
32+
export const useLocalStorageCooldown = ({
33+
storageKey,
34+
durationMs,
35+
}: {
36+
storageKey: string;
37+
durationMs: number;
38+
}) => {
39+
const [remainingSeconds, setRemainingSeconds] = useState(0);
40+
41+
useEffect(() => {
42+
const syncRemainingSeconds = () => {
43+
setRemainingSeconds(getCooldownRemainingSeconds(storageKey, durationMs));
44+
};
45+
46+
syncRemainingSeconds();
47+
const timer = window.setInterval(syncRemainingSeconds, 1000);
48+
const handleStorage = (event: StorageEvent) => {
49+
if (event.key === storageKey) {
50+
syncRemainingSeconds();
51+
}
52+
};
53+
54+
window.addEventListener('storage', handleStorage);
55+
return () => {
56+
window.clearInterval(timer);
57+
window.removeEventListener('storage', handleStorage);
58+
};
59+
}, [storageKey, durationMs]);
60+
61+
const startCooldown = () => {
62+
window.localStorage.setItem(storageKey, String(Date.now()));
63+
setRemainingSeconds(Math.ceil(durationMs / 1000));
64+
};
65+
66+
return {
67+
isCoolingDown: remainingSeconds > 0,
68+
remainingSeconds,
69+
startCooldown,
70+
};
71+
};
72+
1273
export const useUserInfo = () => {
1374
const { data } = useQuery({
1475
queryKey: ['userInfo'],

0 commit comments

Comments
 (0)