@taicode/common-web 1.1.16 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output/signin/context.d.ts +15 -0
- package/output/signin/context.d.ts.map +1 -0
- package/output/signin/context.js +13 -0
- package/output/signin/index.d.ts +58 -0
- package/output/signin/index.d.ts.map +1 -0
- package/output/signin/index.js +117 -0
- package/output/signin/service.d.ts +92 -0
- package/output/signin/service.d.ts.map +1 -0
- package/output/signin/service.js +520 -0
- package/output/signin/service.test.d.ts +2 -0
- package/output/signin/service.test.d.ts.map +1 -0
- package/output/signin/service.test.js +207 -0
- package/output/signin/types.d.ts +63 -0
- package/output/signin/types.d.ts.map +1 -0
- package/output/signin/types.js +1 -0
- package/output/size-provider/index.d.ts +2 -0
- package/output/size-provider/index.d.ts.map +1 -0
- package/output/size-provider/index.js +1 -0
- package/output/size-provider/size-provider.d.ts +9 -0
- package/output/size-provider/size-provider.d.ts.map +1 -0
- package/output/size-provider/size-provider.js +29 -0
- package/package.json +6 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 登录上下文值
|
|
4
|
+
*/
|
|
5
|
+
interface SigninContextValue {
|
|
6
|
+
signin: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare const SigninContext: React.Context<SigninContextValue | null>;
|
|
9
|
+
/**
|
|
10
|
+
* 使用登录上下文
|
|
11
|
+
* 用于在任何子组件中调用登录方法
|
|
12
|
+
*/
|
|
13
|
+
export declare function useSignin(): SigninContextValue;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../source/signin/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB;;GAEG;AACH,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED,eAAO,MAAM,aAAa,0CAAuD,CAAA;AAEjF;;;GAGG;AACH,wBAAgB,SAAS,uBAMxB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export const SigninContext = React.createContext(null);
|
|
3
|
+
/**
|
|
4
|
+
* 使用登录上下文
|
|
5
|
+
* 用于在任何子组件中调用登录方法
|
|
6
|
+
*/
|
|
7
|
+
export function useSignin() {
|
|
8
|
+
const context = React.useContext(SigninContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('useSigninContext must be used within SigninDialogProvider');
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SigninApi } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* 登录弹窗文案配置
|
|
5
|
+
*/
|
|
6
|
+
export interface SigninDialogTexts {
|
|
7
|
+
/** 打开登录页标题 */
|
|
8
|
+
openingTitle?: string;
|
|
9
|
+
/** 等待登录标题 */
|
|
10
|
+
waitingTitle?: string;
|
|
11
|
+
/** 登录成功标题 */
|
|
12
|
+
successTitle?: string;
|
|
13
|
+
/** 登录失败标题 */
|
|
14
|
+
errorTitle?: string;
|
|
15
|
+
/** 登录已取消标题 */
|
|
16
|
+
cancelledTitle?: string;
|
|
17
|
+
/** 票据已过期标题 */
|
|
18
|
+
expiredTitle?: string;
|
|
19
|
+
/** 正在打开登录窗口描述 */
|
|
20
|
+
openingDescription?: string;
|
|
21
|
+
/** 请在新窗口中完成登录描述 */
|
|
22
|
+
waitingDescription?: string;
|
|
23
|
+
/** 登录成功描述 */
|
|
24
|
+
successDescription?: string;
|
|
25
|
+
/** 登录成功后此窗口将自动关闭提示 */
|
|
26
|
+
waitingHint?: string;
|
|
27
|
+
/** 登录过程中出现错误描述 */
|
|
28
|
+
errorDescription?: string;
|
|
29
|
+
/** 登录已被取消描述 */
|
|
30
|
+
cancelledDescription?: string;
|
|
31
|
+
/** 票据已过期描述 */
|
|
32
|
+
expiredDescription?: string;
|
|
33
|
+
/** 重新打开登录窗口按钮文字 */
|
|
34
|
+
reopenButton?: string;
|
|
35
|
+
/** 重新登录按钮文字 */
|
|
36
|
+
retryButton?: string;
|
|
37
|
+
/** 取消按钮文字 */
|
|
38
|
+
cancelButton?: string;
|
|
39
|
+
/** 关闭按钮文字 */
|
|
40
|
+
closeButton?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 登录弹窗 Provider
|
|
44
|
+
* 提供登录方法给子组件使用
|
|
45
|
+
*/
|
|
46
|
+
interface SigninDialogProviderProps {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
/** API 配置,用于适配不同的认证系统 */
|
|
49
|
+
apis: SigninApi;
|
|
50
|
+
/** 文案配置,用于 i18n */
|
|
51
|
+
texts?: SigninDialogTexts;
|
|
52
|
+
}
|
|
53
|
+
export declare const SigninDialogProvider: ((props: SigninDialogProviderProps) => import("react/jsx-runtime").JSX.Element) & {
|
|
54
|
+
displayName: string;
|
|
55
|
+
};
|
|
56
|
+
export type { SigninApi, SigninApiError, ApplySigninResult, CheckStatusResult, SigninStatus } from './types';
|
|
57
|
+
export { useSignin as useSignin } from './context';
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/signin/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAczB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,cAAc;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc;IACd,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,aAAa;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,eAAe;IACf,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,cAAc;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AA+UD;;;GAGG;AACH,UAAU,yBAAyB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yBAAyB;IACzB,IAAI,EAAE,SAAS,CAAA;IACf,mBAAmB;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAA;CAC1B;AAED,eAAO,MAAM,oBAAoB,WAAoB,yBAAyB;;CAmC5E,CAAA;AAGF,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC5G,OAAO,EAAE,SAAS,IAAI,SAAS,EAAE,MAAM,WAAW,CAAA"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { CheckCircleIcon, XCircleIcon, ExclamationTriangleIcon, ClockIcon } from '@heroicons/react/24/outline';
|
|
6
|
+
import { Dialog, DialogBody } from '../catalyst/dialog';
|
|
7
|
+
import { Button } from '../catalyst/button';
|
|
8
|
+
import { SigninService } from './service';
|
|
9
|
+
import { SigninContext } from './context';
|
|
10
|
+
import { ServiceProvider } from '../service';
|
|
11
|
+
/**
|
|
12
|
+
* 默认文案
|
|
13
|
+
*/
|
|
14
|
+
const defaultTexts = {
|
|
15
|
+
openingTitle: '正在打开登录页',
|
|
16
|
+
waitingTitle: '等待登录',
|
|
17
|
+
successTitle: '登录成功',
|
|
18
|
+
errorTitle: '登录失败',
|
|
19
|
+
cancelledTitle: '登录已取消',
|
|
20
|
+
expiredTitle: '登录已过期',
|
|
21
|
+
openingDescription: '正在为您打开登录窗口...',
|
|
22
|
+
waitingDescription: '请在新窗口中完成登录',
|
|
23
|
+
successDescription: '登录成功!正在跳转...',
|
|
24
|
+
waitingHint: '登录成功后此窗口将自动关闭',
|
|
25
|
+
errorDescription: '登录过程中出现错误',
|
|
26
|
+
cancelledDescription: '您已取消本次登录',
|
|
27
|
+
expiredDescription: '登录票据已过期,请重新登录',
|
|
28
|
+
reopenButton: '重新打开登录窗口',
|
|
29
|
+
retryButton: '重新登录',
|
|
30
|
+
cancelButton: '取消',
|
|
31
|
+
closeButton: '关闭'
|
|
32
|
+
};
|
|
33
|
+
const SigninDialog = observer((props) => {
|
|
34
|
+
const { service, texts } = props;
|
|
35
|
+
// 禁止关闭弹窗,防止用户误操作
|
|
36
|
+
const preventClose = () => {
|
|
37
|
+
// 不执行任何操作,阻止关闭
|
|
38
|
+
};
|
|
39
|
+
// 重新打开登录窗口
|
|
40
|
+
const handleReopenWindow = () => {
|
|
41
|
+
if (service.signinUrl) {
|
|
42
|
+
openSigninWindow(service.signinUrl);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// 取消登录
|
|
46
|
+
const handleCancel = () => {
|
|
47
|
+
service.cancel();
|
|
48
|
+
};
|
|
49
|
+
// 关闭弹窗
|
|
50
|
+
const handleClose = () => {
|
|
51
|
+
service.closeDialog();
|
|
52
|
+
};
|
|
53
|
+
// 重新开始登录
|
|
54
|
+
const handleRetry = async () => {
|
|
55
|
+
const url = await service.start();
|
|
56
|
+
if (url) {
|
|
57
|
+
openSigninWindow(url);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* 打开登录窗口
|
|
62
|
+
*/
|
|
63
|
+
const openSigninWindow = (url) => {
|
|
64
|
+
window.open(url, '_blank');
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Opening 状态 - 正在打开登录窗口
|
|
68
|
+
*/
|
|
69
|
+
const OpeningState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, transition: { duration: 0.2 }, className: "flex flex-col items-center gap-4", children: [_jsxs("div", { className: "relative flex items-center justify-center mb-8", children: [_jsx(motion.div, { className: "absolute size-16 rounded-full border-4 border-transparent border-t-emerald-600 border-r-emerald-600", animate: { rotate: 360 }, transition: { duration: 1, repeat: Infinity, ease: 'linear' } }), _jsx(motion.div, { className: "absolute size-12 rounded-full border-4 border-transparent border-b-emerald-400 border-l-emerald-400", animate: { rotate: -360 }, transition: { duration: 1.5, repeat: Infinity, ease: 'linear' } }), _jsx(motion.div, { className: "size-3 rounded-full bg-emerald-600", animate: {
|
|
70
|
+
scale: [1, 1.2, 1],
|
|
71
|
+
opacity: [1, 0.7, 1]
|
|
72
|
+
}, transition: { duration: 1, repeat: Infinity } })] }), _jsx(motion.p, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { delay: 0.1 }, className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: texts.openingDescription }), _jsxs(motion.div, { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, transition: { delay: 0.2 }, className: "mt-2 flex gap-2", children: [_jsx(Button, { color: "emerald", onClick: handleReopenWindow, children: texts.reopenButton }), _jsx(Button, { outline: true, onClick: handleCancel, children: texts.cancelButton })] })] }, "opening"));
|
|
73
|
+
/**
|
|
74
|
+
* Waiting 状态 - 等待用户登录
|
|
75
|
+
*/
|
|
76
|
+
const WaitingState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, transition: { duration: 0.2 }, className: "flex flex-col items-center gap-4", children: [_jsxs("div", { className: "relative mb-2", children: [_jsx(motion.div, { className: "size-16 rounded-full border-4 border-emerald-200 dark:border-emerald-900", animate: { scale: [1, 1.1, 1] }, transition: { duration: 2, repeat: Infinity } }), _jsx(motion.div, { className: "absolute inset-0 size-16 rounded-full border-4 border-emerald-400", animate: { scale: [1, 1.3, 1], opacity: [0.75, 0, 0.75] }, transition: { duration: 2, repeat: Infinity } }), _jsx(motion.svg, { className: "absolute inset-0 m-auto size-8 text-emerald-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", initial: { scale: 0 }, animate: { scale: 1 }, transition: { delay: 0.1 }, children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" }) })] }), _jsx("p", { className: "text-base font-semibold text-slate-900 dark:text-slate-100", children: texts.waitingDescription }), _jsx("p", { className: "text-xs text-slate-500 dark:text-slate-500", children: texts.waitingHint }), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx(Button, { color: "emerald", onClick: handleReopenWindow, children: texts.reopenButton }), _jsx(Button, { outline: true, onClick: handleCancel, children: texts.cancelButton })] })] }, "waiting"));
|
|
77
|
+
/**
|
|
78
|
+
* Success 状态 - 登录成功
|
|
79
|
+
*/
|
|
80
|
+
const SuccessState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.5 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.5 }, transition: { duration: 0.3, type: 'spring' }, className: "flex flex-col items-center gap-4", children: [_jsx(motion.div, { className: "mb-2", initial: { scale: 0 }, animate: { scale: 1 }, transition: { delay: 0.1, type: 'spring', stiffness: 200 }, children: _jsx(CheckCircleIcon, { className: "size-16 text-emerald-600 dark:text-emerald-400" }) }), _jsx(motion.p, { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, transition: { delay: 0.2 }, className: "text-base font-semibold text-emerald-600 dark:text-emerald-400", children: texts.successDescription })] }, "success"));
|
|
81
|
+
/**
|
|
82
|
+
* Error 状态 - 登录失败
|
|
83
|
+
*/
|
|
84
|
+
const ErrorState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, transition: { duration: 0.2 }, className: "flex flex-col items-center gap-4", children: [_jsx(motion.div, { className: "mb-2", initial: { scale: 0 }, animate: { scale: 1, rotate: [0, -10, 10, -10, 0] }, transition: { scale: { delay: 0.1 }, rotate: { delay: 0.2, duration: 0.5 } }, children: _jsx(XCircleIcon, { className: "size-16 text-red-600 dark:text-red-400" }) }), _jsx("p", { className: "text-base font-semibold text-red-600 dark:text-red-400", children: service.errorMessage || texts.errorDescription }), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx(Button, { color: "emerald", onClick: handleRetry, children: texts.retryButton }), _jsx(Button, { outline: true, onClick: handleClose, children: texts.closeButton })] })] }, "error"));
|
|
85
|
+
/**
|
|
86
|
+
* Cancelled 状态 - 用户取消登录
|
|
87
|
+
*/
|
|
88
|
+
const CancelledState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, transition: { duration: 0.2 }, className: "flex flex-col items-center gap-4", children: [_jsx(motion.div, { className: "mb-2", initial: { scale: 0 }, animate: { scale: 1 }, transition: { delay: 0.1, type: 'spring' }, children: _jsx(ExclamationTriangleIcon, { className: "size-16 text-amber-600 dark:text-amber-400" }) }), _jsx("p", { className: "text-base font-semibold text-amber-600 dark:text-amber-400", children: texts.cancelledDescription }), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx(Button, { color: "emerald", onClick: handleRetry, children: texts.retryButton }), _jsx(Button, { outline: true, onClick: handleClose, children: texts.closeButton })] })] }, "cancelled"));
|
|
89
|
+
/**
|
|
90
|
+
* Expired 状态 - 票据过期
|
|
91
|
+
*/
|
|
92
|
+
const ExpiredState = () => (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, transition: { duration: 0.2 }, className: "flex flex-col items-center gap-4", children: [_jsx(motion.div, { className: "mb-2", initial: { scale: 0 }, animate: { scale: 1 }, transition: { delay: 0.1, type: 'spring' }, children: _jsx(ClockIcon, { className: "size-16 text-orange-600 dark:text-orange-400" }) }), _jsx("p", { className: "text-base font-semibold text-orange-600 dark:text-orange-400", children: texts.expiredDescription }), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx(Button, { color: "emerald", onClick: handleRetry, children: texts.retryButton }), _jsx(Button, { outline: true, onClick: handleClose, children: texts.closeButton })] })] }, "expired"));
|
|
93
|
+
return (_jsx(Dialog, { open: service.isDialogOpen, onClose: preventClose, children: _jsx(DialogBody, { children: _jsx("div", { className: "flex flex-col items-center justify-center py-8", children: _jsxs(AnimatePresence, { mode: "wait", children: [service.dialogStatus === 'opening' && _jsx(OpeningState, {}), service.dialogStatus === 'waiting' && _jsx(WaitingState, {}), service.dialogStatus === 'success' && _jsx(SuccessState, {}), service.dialogStatus === 'error' && _jsx(ErrorState, {}), service.dialogStatus === 'cancelled' && _jsx(CancelledState, {}), service.dialogStatus === 'expired' && _jsx(ExpiredState, {})] }) }) }) }));
|
|
94
|
+
});
|
|
95
|
+
export const SigninDialogProvider = observer((props) => {
|
|
96
|
+
const { children, apis: apiConfig, texts: customTexts } = props;
|
|
97
|
+
// 创建服务实例,使用 useRef 保持引用稳定
|
|
98
|
+
const signinService = React.useRef();
|
|
99
|
+
if (!signinService.current) {
|
|
100
|
+
signinService.current = new SigninService(apiConfig);
|
|
101
|
+
signinService.current.init();
|
|
102
|
+
}
|
|
103
|
+
// 合并默认文案和自定义文案
|
|
104
|
+
const texts = React.useMemo(() => (Object.assign(Object.assign({}, defaultTexts), customTexts)), [customTexts]);
|
|
105
|
+
const handleSignin = React.useCallback(async () => {
|
|
106
|
+
var _a;
|
|
107
|
+
const url = await ((_a = signinService.current) === null || _a === void 0 ? void 0 : _a.start());
|
|
108
|
+
if (url) {
|
|
109
|
+
window.open(url, '_blank');
|
|
110
|
+
}
|
|
111
|
+
}, []);
|
|
112
|
+
const contextValue = React.useMemo(() => ({
|
|
113
|
+
signin: handleSignin
|
|
114
|
+
}), [handleSignin]);
|
|
115
|
+
return (_jsx(SigninContext.Provider, { value: contextValue, children: _jsxs(ServiceProvider, { services: [{ provide: SigninService, useValue: signinService.current }], children: [children, signinService.current && _jsx(SigninDialog, { service: signinService.current, texts: texts })] }) }));
|
|
116
|
+
});
|
|
117
|
+
export { useSignin as useSignin } from './context';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Service } from '@taicode/common-base';
|
|
2
|
+
import type { SigninApi, SigninStatus } from './types';
|
|
3
|
+
export type SigninDialogStatus = 'idle' | 'opening' | 'waiting' | 'success' | 'error' | 'cancelled' | 'expired';
|
|
4
|
+
/**
|
|
5
|
+
* 登录服务配置
|
|
6
|
+
*/
|
|
7
|
+
export interface SigninServiceConfig {
|
|
8
|
+
/** 票据轮询间隔(毫秒),默认 2000ms */
|
|
9
|
+
pollInterval?: number;
|
|
10
|
+
/** 错误自动关闭延迟(毫秒),默认 3000ms */
|
|
11
|
+
errorAutoCloseDelay?: number;
|
|
12
|
+
/** 成功状态显示时长(毫秒),默认 1500ms */
|
|
13
|
+
successDisplayDuration?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class SigninService extends Service {
|
|
16
|
+
accessor inited: boolean;
|
|
17
|
+
private isPolling;
|
|
18
|
+
private readonly disposer;
|
|
19
|
+
private readonly apiConfig;
|
|
20
|
+
private readonly config;
|
|
21
|
+
private pollingTimer;
|
|
22
|
+
private accessor check;
|
|
23
|
+
private accessor ticket;
|
|
24
|
+
accessor dialogStatus: SigninDialogStatus;
|
|
25
|
+
accessor errorMessage: string;
|
|
26
|
+
constructor(apiConfig: SigninApi, config?: SigninServiceConfig);
|
|
27
|
+
get status(): SigninStatus | null;
|
|
28
|
+
get signinUrl(): string | null;
|
|
29
|
+
get isDialogOpen(): boolean;
|
|
30
|
+
get hasActiveTicket(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 检查票据是否过期
|
|
33
|
+
*/
|
|
34
|
+
private isTicketExpired;
|
|
35
|
+
/**
|
|
36
|
+
* 保存状态到 localStorage
|
|
37
|
+
*/
|
|
38
|
+
private saveState;
|
|
39
|
+
/**
|
|
40
|
+
* 从 localStorage 恢复状态
|
|
41
|
+
*/
|
|
42
|
+
private restoreState;
|
|
43
|
+
/**
|
|
44
|
+
* 清理持久化状态
|
|
45
|
+
*/
|
|
46
|
+
private clearPersistedState;
|
|
47
|
+
/**
|
|
48
|
+
* 设置错误状态
|
|
49
|
+
*/
|
|
50
|
+
private setError;
|
|
51
|
+
/**
|
|
52
|
+
* 停止票据状态轮询
|
|
53
|
+
*/
|
|
54
|
+
private stopPolling;
|
|
55
|
+
/**
|
|
56
|
+
* 处理认证成功
|
|
57
|
+
*/
|
|
58
|
+
private handleAuthSuccess;
|
|
59
|
+
/**
|
|
60
|
+
* 轮询检查票据状态
|
|
61
|
+
*/
|
|
62
|
+
private watchTicketStatus;
|
|
63
|
+
/**
|
|
64
|
+
* 安排下一次轮询
|
|
65
|
+
*/
|
|
66
|
+
private scheduleNextPoll;
|
|
67
|
+
/**
|
|
68
|
+
* 开始登录流程
|
|
69
|
+
*/
|
|
70
|
+
start(): Promise<string | null>;
|
|
71
|
+
/**
|
|
72
|
+
* 关闭对话框
|
|
73
|
+
*/
|
|
74
|
+
closeDialog(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* 取消登录流程
|
|
77
|
+
*/
|
|
78
|
+
cancel(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* 初始化服务
|
|
81
|
+
*/
|
|
82
|
+
init(): Promise<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* 清理资源
|
|
85
|
+
*/
|
|
86
|
+
private cleanup;
|
|
87
|
+
/**
|
|
88
|
+
* 销毁服务
|
|
89
|
+
*/
|
|
90
|
+
dispose(): void;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../source/signin/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAsB,MAAM,sBAAsB,CAAA;AAElE,OAAO,KAAK,EAAE,SAAS,EAAwC,YAAY,EAAE,MAAM,SAAS,CAAA;AAE5F,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAA;AAE/G;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6BAA6B;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,6BAA6B;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAoBD,qBAAa,aAAc,SAAQ,OAAO;IACxC,SACgB,MAAM,EAAE,OAAO,CAAQ;IAEvC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,YAAY,CAA6C;IAGjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAGvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IAGxD,SACgB,YAAY,EAAE,kBAAkB,CAAS;IAEzD,SACgB,YAAY,EAAE,MAAM,CAAK;gBAE7B,SAAS,EAAE,SAAS,EAAE,MAAM,GAAE,mBAAwB;IAMlE,IACW,MAAM,IAAI,YAAY,GAAG,IAAI,CAIvC;IAED,IACW,SAAS,IAAI,MAAM,GAAG,IAAI,CAEpC;IAED,IACW,YAAY,IAAI,OAAO,CAEjC;IAED,IACW,eAAe,IAAI,OAAO,CAEpC;IAED;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACH,OAAO,CAAC,SAAS;IAgBjB;;OAEG;IACH,OAAO,CAAC,YAAY;IAuCpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAU3B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAehB;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;YACW,iBAAiB;IA4G/B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAQxB;;OAEG;IAEG,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA+CrC;;OAEG;IAEG,WAAW;IASjB;;OAEG;IAEG,MAAM;IAqBZ;;OAEG;IAEU,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IA8BrC;;OAEG;IACH,OAAO,CAAC,OAAO;IAWf;;OAEG;IACI,OAAO;CAIf"}
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
36
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
37
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
38
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
39
|
+
};
|
|
40
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
41
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
42
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
43
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
44
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
45
|
+
};
|
|
46
|
+
import { computed, observable, runInAction, reaction } from 'mobx';
|
|
47
|
+
import { Service, debounce, Disposer } from '@taicode/common-base';
|
|
48
|
+
const DEFAULT_CONFIG = {
|
|
49
|
+
pollInterval: 2000,
|
|
50
|
+
errorAutoCloseDelay: 3000,
|
|
51
|
+
successDisplayDuration: 1500
|
|
52
|
+
};
|
|
53
|
+
const STORAGE_KEY = '__signin_service_state__';
|
|
54
|
+
let SigninService = (() => {
|
|
55
|
+
var _a, _SigninService_inited_accessor_storage, _SigninService_check_accessor_storage, _SigninService_ticket_accessor_storage, _SigninService_dialogStatus_accessor_storage, _SigninService_errorMessage_accessor_storage;
|
|
56
|
+
var _b, _c;
|
|
57
|
+
let _classSuper = Service;
|
|
58
|
+
let _instanceExtraInitializers = [];
|
|
59
|
+
let _inited_decorators;
|
|
60
|
+
let _inited_initializers = [];
|
|
61
|
+
let _inited_extraInitializers = [];
|
|
62
|
+
let _check_decorators;
|
|
63
|
+
let _check_initializers = [];
|
|
64
|
+
let _check_extraInitializers = [];
|
|
65
|
+
let _ticket_decorators;
|
|
66
|
+
let _ticket_initializers = [];
|
|
67
|
+
let _ticket_extraInitializers = [];
|
|
68
|
+
let _dialogStatus_decorators;
|
|
69
|
+
let _dialogStatus_initializers = [];
|
|
70
|
+
let _dialogStatus_extraInitializers = [];
|
|
71
|
+
let _errorMessage_decorators;
|
|
72
|
+
let _errorMessage_initializers = [];
|
|
73
|
+
let _errorMessage_extraInitializers = [];
|
|
74
|
+
let _get_status_decorators;
|
|
75
|
+
let _get_signinUrl_decorators;
|
|
76
|
+
let _get_isDialogOpen_decorators;
|
|
77
|
+
let _get_hasActiveTicket_decorators;
|
|
78
|
+
let _start_decorators;
|
|
79
|
+
let _closeDialog_decorators;
|
|
80
|
+
let _cancel_decorators;
|
|
81
|
+
let _init_decorators;
|
|
82
|
+
return _a = class SigninService extends _classSuper {
|
|
83
|
+
get inited() { return __classPrivateFieldGet(this, _SigninService_inited_accessor_storage, "f"); }
|
|
84
|
+
set inited(value) { __classPrivateFieldSet(this, _SigninService_inited_accessor_storage, value, "f"); }
|
|
85
|
+
get check() { return __classPrivateFieldGet(this, _SigninService_check_accessor_storage, "f"); }
|
|
86
|
+
set check(value) { __classPrivateFieldSet(this, _SigninService_check_accessor_storage, value, "f"); }
|
|
87
|
+
get ticket() { return __classPrivateFieldGet(this, _SigninService_ticket_accessor_storage, "f"); }
|
|
88
|
+
set ticket(value) { __classPrivateFieldSet(this, _SigninService_ticket_accessor_storage, value, "f"); }
|
|
89
|
+
// UI 状态管理
|
|
90
|
+
get dialogStatus() { return __classPrivateFieldGet(this, _SigninService_dialogStatus_accessor_storage, "f"); }
|
|
91
|
+
set dialogStatus(value) { __classPrivateFieldSet(this, _SigninService_dialogStatus_accessor_storage, value, "f"); }
|
|
92
|
+
get errorMessage() { return __classPrivateFieldGet(this, _SigninService_errorMessage_accessor_storage, "f"); }
|
|
93
|
+
set errorMessage(value) { __classPrivateFieldSet(this, _SigninService_errorMessage_accessor_storage, value, "f"); }
|
|
94
|
+
constructor(apiConfig, config = {}) {
|
|
95
|
+
super();
|
|
96
|
+
_SigninService_inited_accessor_storage.set(this, (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _inited_initializers, false)));
|
|
97
|
+
this.isPolling = (__runInitializers(this, _inited_extraInitializers), false);
|
|
98
|
+
this.disposer = new Disposer();
|
|
99
|
+
this.pollingTimer = null;
|
|
100
|
+
_SigninService_check_accessor_storage.set(this, __runInitializers(this, _check_initializers, null));
|
|
101
|
+
_SigninService_ticket_accessor_storage.set(this, (__runInitializers(this, _check_extraInitializers), __runInitializers(this, _ticket_initializers, null
|
|
102
|
+
// UI 状态管理
|
|
103
|
+
)));
|
|
104
|
+
_SigninService_dialogStatus_accessor_storage.set(this, (__runInitializers(this, _ticket_extraInitializers), __runInitializers(this, _dialogStatus_initializers, 'idle')));
|
|
105
|
+
_SigninService_errorMessage_accessor_storage.set(this, (__runInitializers(this, _dialogStatus_extraInitializers), __runInitializers(this, _errorMessage_initializers, '')));
|
|
106
|
+
__runInitializers(this, _errorMessage_extraInitializers);
|
|
107
|
+
this.apiConfig = apiConfig;
|
|
108
|
+
this.config = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
|
|
109
|
+
}
|
|
110
|
+
get status() {
|
|
111
|
+
if (this.ticket == null)
|
|
112
|
+
return null;
|
|
113
|
+
if (this.check == null)
|
|
114
|
+
return null;
|
|
115
|
+
return this.check.status;
|
|
116
|
+
}
|
|
117
|
+
get signinUrl() {
|
|
118
|
+
var _b, _c;
|
|
119
|
+
return (_c = (_b = this.ticket) === null || _b === void 0 ? void 0 : _b.signinUrl) !== null && _c !== void 0 ? _c : null;
|
|
120
|
+
}
|
|
121
|
+
get isDialogOpen() {
|
|
122
|
+
return this.dialogStatus !== 'idle';
|
|
123
|
+
}
|
|
124
|
+
get hasActiveTicket() {
|
|
125
|
+
return this.ticket != null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 检查票据是否过期
|
|
129
|
+
*/
|
|
130
|
+
isTicketExpired(ticket) {
|
|
131
|
+
const expiredTime = new Date(ticket.expiredTime);
|
|
132
|
+
return new Date() > expiredTime;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 保存状态到 localStorage
|
|
136
|
+
*/
|
|
137
|
+
saveState() {
|
|
138
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const state = {
|
|
142
|
+
ticket: this.ticket,
|
|
143
|
+
dialogStatus: this.dialogStatus,
|
|
144
|
+
errorMessage: this.errorMessage,
|
|
145
|
+
timestamp: Date.now()
|
|
146
|
+
};
|
|
147
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.warn('Failed to save signin state:', error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 从 localStorage 恢复状态
|
|
155
|
+
*/
|
|
156
|
+
restoreState() {
|
|
157
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
158
|
+
return false;
|
|
159
|
+
try {
|
|
160
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
161
|
+
if (!data)
|
|
162
|
+
return false;
|
|
163
|
+
const state = JSON.parse(data);
|
|
164
|
+
// 检查票据是否存在且未过期(使用票据本身的过期时间)
|
|
165
|
+
if (state.ticket && !this.isTicketExpired(state.ticket)) {
|
|
166
|
+
runInAction(() => {
|
|
167
|
+
this.ticket = state.ticket;
|
|
168
|
+
this.dialogStatus = state.dialogStatus;
|
|
169
|
+
this.errorMessage = state.errorMessage;
|
|
170
|
+
});
|
|
171
|
+
// 如果是等待或打开中状态,恢复轮询并显示弹窗
|
|
172
|
+
if (state.dialogStatus === 'waiting' || state.dialogStatus === 'opening') {
|
|
173
|
+
// 确保弹窗显示
|
|
174
|
+
runInAction(() => {
|
|
175
|
+
this.dialogStatus = 'waiting';
|
|
176
|
+
});
|
|
177
|
+
this.watchTicketStatus();
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
// 票据无效或过期,清理状态
|
|
182
|
+
this.clearPersistedState();
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn('Failed to restore signin state:', error);
|
|
187
|
+
this.clearPersistedState();
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 清理持久化状态
|
|
193
|
+
*/
|
|
194
|
+
clearPersistedState() {
|
|
195
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.warn('Failed to clear signin state:', error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 设置错误状态
|
|
206
|
+
*/
|
|
207
|
+
setError(message, autoClose = true) {
|
|
208
|
+
runInAction(() => {
|
|
209
|
+
this.dialogStatus = 'error';
|
|
210
|
+
this.errorMessage = message;
|
|
211
|
+
});
|
|
212
|
+
if (autoClose) {
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (this.dialogStatus === 'error') {
|
|
215
|
+
runInAction(() => this.dialogStatus = 'idle');
|
|
216
|
+
}
|
|
217
|
+
}, this.config.errorAutoCloseDelay);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 停止票据状态轮询
|
|
222
|
+
*/
|
|
223
|
+
stopPolling() {
|
|
224
|
+
this.isPolling = false;
|
|
225
|
+
if (this.pollingTimer) {
|
|
226
|
+
clearTimeout(this.pollingTimer);
|
|
227
|
+
this.pollingTimer = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 处理认证成功
|
|
232
|
+
*/
|
|
233
|
+
async handleAuthSuccess(token) {
|
|
234
|
+
try {
|
|
235
|
+
runInAction(() => {
|
|
236
|
+
this.ticket = null;
|
|
237
|
+
this.dialogStatus = 'success';
|
|
238
|
+
});
|
|
239
|
+
this.clearPersistedState();
|
|
240
|
+
// 显示成功状态一段时间
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, this.config.successDisplayDuration));
|
|
242
|
+
if (this.apiConfig.onSuccess) {
|
|
243
|
+
await this.apiConfig.onSuccess(token);
|
|
244
|
+
}
|
|
245
|
+
runInAction(() => {
|
|
246
|
+
this.dialogStatus = 'idle';
|
|
247
|
+
this.errorMessage = '';
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.error('Auth success handler failed:', error);
|
|
252
|
+
this.setError('Failed to complete authentication');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 轮询检查票据状态
|
|
257
|
+
*/
|
|
258
|
+
async watchTicketStatus() {
|
|
259
|
+
if (!this.ticket || this.isPolling)
|
|
260
|
+
return;
|
|
261
|
+
this.isPolling = true;
|
|
262
|
+
const poll = async () => {
|
|
263
|
+
if (!this.ticket || !this.isPolling) {
|
|
264
|
+
this.stopPolling();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
// 检查票据是否过期
|
|
269
|
+
if (this.isTicketExpired(this.ticket)) {
|
|
270
|
+
this.stopPolling();
|
|
271
|
+
this.setError('Ticket expired');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// 调用外部提供的检查方法
|
|
275
|
+
const result = await this.apiConfig.checkStatus(this.ticket.ticket);
|
|
276
|
+
// 检查是否有错误
|
|
277
|
+
if (result.isError()) {
|
|
278
|
+
console.error('Check ticket failed:', result.error);
|
|
279
|
+
// API 调用失败,继续轮询
|
|
280
|
+
this.scheduleNextPoll(poll);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const check = result.value;
|
|
284
|
+
// 更新检查结果
|
|
285
|
+
runInAction(() => this.check = check);
|
|
286
|
+
// 根据状态处理
|
|
287
|
+
switch (check.status) {
|
|
288
|
+
case 'Finished':
|
|
289
|
+
this.stopPolling();
|
|
290
|
+
if (check.token) {
|
|
291
|
+
await this.handleAuthSuccess(check.token);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
this.setError('Authentication completed but no token received');
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
case 'Cancelled':
|
|
298
|
+
this.stopPolling();
|
|
299
|
+
runInAction(() => {
|
|
300
|
+
this.dialogStatus = 'cancelled';
|
|
301
|
+
this.ticket = null;
|
|
302
|
+
});
|
|
303
|
+
this.clearPersistedState();
|
|
304
|
+
// 调用 onCancel 回调
|
|
305
|
+
if (this.apiConfig.onCancel) {
|
|
306
|
+
try {
|
|
307
|
+
await this.apiConfig.onCancel();
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error('Cancel handler error:', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// 显示取消状态后自动关闭
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
if (this.dialogStatus === 'cancelled') {
|
|
316
|
+
runInAction(() => this.dialogStatus = 'idle');
|
|
317
|
+
}
|
|
318
|
+
}, this.config.errorAutoCloseDelay);
|
|
319
|
+
break;
|
|
320
|
+
case 'Expired':
|
|
321
|
+
this.stopPolling();
|
|
322
|
+
runInAction(() => {
|
|
323
|
+
this.dialogStatus = 'expired';
|
|
324
|
+
this.ticket = null;
|
|
325
|
+
});
|
|
326
|
+
this.clearPersistedState();
|
|
327
|
+
// 显示过期状态后自动关闭
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
if (this.dialogStatus === 'expired') {
|
|
330
|
+
runInAction(() => this.dialogStatus = 'idle');
|
|
331
|
+
}
|
|
332
|
+
}, this.config.errorAutoCloseDelay);
|
|
333
|
+
break;
|
|
334
|
+
case 'Pending':
|
|
335
|
+
// 继续轮询
|
|
336
|
+
this.scheduleNextPoll(poll);
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
// 未知状态,继续轮询
|
|
340
|
+
this.scheduleNextPoll(poll);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
console.error('Ticket polling error:', error);
|
|
345
|
+
// 出错继续轮询,避免暂时性网络问题导致整个流程中断
|
|
346
|
+
this.scheduleNextPoll(poll);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
// 开始轮询
|
|
350
|
+
poll();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 安排下一次轮询
|
|
354
|
+
*/
|
|
355
|
+
scheduleNextPoll(pollFn) {
|
|
356
|
+
if (!this.isPolling)
|
|
357
|
+
return;
|
|
358
|
+
this.pollingTimer = setTimeout(() => {
|
|
359
|
+
pollFn();
|
|
360
|
+
}, this.config.pollInterval);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 开始登录流程
|
|
364
|
+
*/
|
|
365
|
+
async start() {
|
|
366
|
+
// 防止重复启动
|
|
367
|
+
if (this.dialogStatus !== 'idle') {
|
|
368
|
+
console.warn('Login already in progress');
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
runInAction(() => {
|
|
372
|
+
this.dialogStatus = 'opening';
|
|
373
|
+
this.errorMessage = '';
|
|
374
|
+
});
|
|
375
|
+
try {
|
|
376
|
+
// 调用外部提供的申请票据方法
|
|
377
|
+
const result = await this.apiConfig.applySignin();
|
|
378
|
+
// 检查是否有错误
|
|
379
|
+
if (result.isError()) {
|
|
380
|
+
this.setError(result.error.message || 'Failed to get signin ticket');
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
const ticket = result.value;
|
|
384
|
+
// 验证票据有效性
|
|
385
|
+
if (this.isTicketExpired(ticket)) {
|
|
386
|
+
this.setError('Received expired ticket');
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
runInAction(() => {
|
|
390
|
+
this.check = null;
|
|
391
|
+
this.ticket = ticket;
|
|
392
|
+
this.dialogStatus = 'waiting';
|
|
393
|
+
});
|
|
394
|
+
// 开始轮询
|
|
395
|
+
this.watchTicketStatus();
|
|
396
|
+
return this.signinUrl;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
400
|
+
this.setError(message);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 关闭对话框
|
|
406
|
+
*/
|
|
407
|
+
async closeDialog() {
|
|
408
|
+
this.stopPolling();
|
|
409
|
+
this.clearPersistedState();
|
|
410
|
+
runInAction(() => {
|
|
411
|
+
this.dialogStatus = 'idle';
|
|
412
|
+
this.errorMessage = '';
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 取消登录流程
|
|
417
|
+
*/
|
|
418
|
+
async cancel() {
|
|
419
|
+
this.stopPolling();
|
|
420
|
+
// 调用 onCancel 回调
|
|
421
|
+
if (this.apiConfig.onCancel) {
|
|
422
|
+
try {
|
|
423
|
+
await this.apiConfig.onCancel();
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.error('Cancel handler error:', error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
this.clearPersistedState();
|
|
430
|
+
runInAction(() => {
|
|
431
|
+
this.ticket = null;
|
|
432
|
+
this.check = null;
|
|
433
|
+
this.dialogStatus = 'idle';
|
|
434
|
+
this.errorMessage = '';
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 初始化服务
|
|
439
|
+
*/
|
|
440
|
+
async init() {
|
|
441
|
+
this.disposer.addDisposer(() => this.cleanup());
|
|
442
|
+
// 监听状态变化并自动保存
|
|
443
|
+
this.disposer.addDisposer(reaction(() => ({
|
|
444
|
+
ticket: this.ticket,
|
|
445
|
+
dialogStatus: this.dialogStatus,
|
|
446
|
+
errorMessage: this.errorMessage
|
|
447
|
+
}), () => {
|
|
448
|
+
// 只在有活跃票据或等待状态时保存
|
|
449
|
+
if (this.ticket || this.dialogStatus !== 'idle') {
|
|
450
|
+
this.saveState();
|
|
451
|
+
}
|
|
452
|
+
}, { delay: 100 } // 防抖,避免过于频繁的保存
|
|
453
|
+
));
|
|
454
|
+
// 尝试恢复持久化状态
|
|
455
|
+
const restored = this.restoreState();
|
|
456
|
+
if (restored) {
|
|
457
|
+
console.log('Signin state restored from localStorage');
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* 清理资源
|
|
463
|
+
*/
|
|
464
|
+
cleanup() {
|
|
465
|
+
this.stopPolling();
|
|
466
|
+
this.clearPersistedState();
|
|
467
|
+
runInAction(() => {
|
|
468
|
+
this.ticket = null;
|
|
469
|
+
this.check = null;
|
|
470
|
+
this.dialogStatus = 'idle';
|
|
471
|
+
this.errorMessage = '';
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 销毁服务
|
|
476
|
+
*/
|
|
477
|
+
dispose() {
|
|
478
|
+
this.cleanup();
|
|
479
|
+
this.disposer.dispose();
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
_SigninService_inited_accessor_storage = new WeakMap(),
|
|
483
|
+
_SigninService_check_accessor_storage = new WeakMap(),
|
|
484
|
+
_SigninService_ticket_accessor_storage = new WeakMap(),
|
|
485
|
+
_SigninService_dialogStatus_accessor_storage = new WeakMap(),
|
|
486
|
+
_SigninService_errorMessage_accessor_storage = new WeakMap(),
|
|
487
|
+
(() => {
|
|
488
|
+
var _b;
|
|
489
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
|
|
490
|
+
_inited_decorators = [observable];
|
|
491
|
+
_check_decorators = [(_b = observable).ref.bind(_b)];
|
|
492
|
+
_ticket_decorators = [(_c = observable).ref.bind(_c)];
|
|
493
|
+
_dialogStatus_decorators = [observable];
|
|
494
|
+
_errorMessage_decorators = [observable];
|
|
495
|
+
_get_status_decorators = [computed];
|
|
496
|
+
_get_signinUrl_decorators = [computed];
|
|
497
|
+
_get_isDialogOpen_decorators = [computed];
|
|
498
|
+
_get_hasActiveTicket_decorators = [computed];
|
|
499
|
+
_start_decorators = [debounce(100)];
|
|
500
|
+
_closeDialog_decorators = [debounce(100)];
|
|
501
|
+
_cancel_decorators = [debounce(100)];
|
|
502
|
+
_init_decorators = [debounce(100)];
|
|
503
|
+
__esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
|
|
504
|
+
__esDecorate(_a, null, _check_decorators, { kind: "accessor", name: "check", static: false, private: false, access: { has: obj => "check" in obj, get: obj => obj.check, set: (obj, value) => { obj.check = value; } }, metadata: _metadata }, _check_initializers, _check_extraInitializers);
|
|
505
|
+
__esDecorate(_a, null, _ticket_decorators, { kind: "accessor", name: "ticket", static: false, private: false, access: { has: obj => "ticket" in obj, get: obj => obj.ticket, set: (obj, value) => { obj.ticket = value; } }, metadata: _metadata }, _ticket_initializers, _ticket_extraInitializers);
|
|
506
|
+
__esDecorate(_a, null, _dialogStatus_decorators, { kind: "accessor", name: "dialogStatus", static: false, private: false, access: { has: obj => "dialogStatus" in obj, get: obj => obj.dialogStatus, set: (obj, value) => { obj.dialogStatus = value; } }, metadata: _metadata }, _dialogStatus_initializers, _dialogStatus_extraInitializers);
|
|
507
|
+
__esDecorate(_a, null, _errorMessage_decorators, { kind: "accessor", name: "errorMessage", static: false, private: false, access: { has: obj => "errorMessage" in obj, get: obj => obj.errorMessage, set: (obj, value) => { obj.errorMessage = value; } }, metadata: _metadata }, _errorMessage_initializers, _errorMessage_extraInitializers);
|
|
508
|
+
__esDecorate(_a, null, _get_status_decorators, { kind: "getter", name: "status", static: false, private: false, access: { has: obj => "status" in obj, get: obj => obj.status }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
509
|
+
__esDecorate(_a, null, _get_signinUrl_decorators, { kind: "getter", name: "signinUrl", static: false, private: false, access: { has: obj => "signinUrl" in obj, get: obj => obj.signinUrl }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
510
|
+
__esDecorate(_a, null, _get_isDialogOpen_decorators, { kind: "getter", name: "isDialogOpen", static: false, private: false, access: { has: obj => "isDialogOpen" in obj, get: obj => obj.isDialogOpen }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
511
|
+
__esDecorate(_a, null, _get_hasActiveTicket_decorators, { kind: "getter", name: "hasActiveTicket", static: false, private: false, access: { has: obj => "hasActiveTicket" in obj, get: obj => obj.hasActiveTicket }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
512
|
+
__esDecorate(_a, null, _start_decorators, { kind: "method", name: "start", static: false, private: false, access: { has: obj => "start" in obj, get: obj => obj.start }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
513
|
+
__esDecorate(_a, null, _closeDialog_decorators, { kind: "method", name: "closeDialog", static: false, private: false, access: { has: obj => "closeDialog" in obj, get: obj => obj.closeDialog }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
514
|
+
__esDecorate(_a, null, _cancel_decorators, { kind: "method", name: "cancel", static: false, private: false, access: { has: obj => "cancel" in obj, get: obj => obj.cancel }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
515
|
+
__esDecorate(_a, null, _init_decorators, { kind: "method", name: "init", static: false, private: false, access: { has: obj => "init" in obj, get: obj => obj.init }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
516
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
517
|
+
})(),
|
|
518
|
+
_a;
|
|
519
|
+
})();
|
|
520
|
+
export { SigninService };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../source/signin/service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { SigninService } from './service';
|
|
3
|
+
// Mock localStorage
|
|
4
|
+
const localStorageMock = (() => {
|
|
5
|
+
let store = {};
|
|
6
|
+
return {
|
|
7
|
+
getItem: (key) => store[key] || null,
|
|
8
|
+
setItem: (key, value) => {
|
|
9
|
+
store[key] = value;
|
|
10
|
+
},
|
|
11
|
+
removeItem: (key) => {
|
|
12
|
+
delete store[key];
|
|
13
|
+
},
|
|
14
|
+
clear: () => {
|
|
15
|
+
store = {};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
})();
|
|
19
|
+
Object.defineProperty(window, 'localStorage', {
|
|
20
|
+
value: localStorageMock,
|
|
21
|
+
writable: true
|
|
22
|
+
});
|
|
23
|
+
describe('SigninService', () => {
|
|
24
|
+
let service;
|
|
25
|
+
let mockApi;
|
|
26
|
+
const mockTicket = {
|
|
27
|
+
ticket: 'test-ticket-123',
|
|
28
|
+
signinUrl: 'https://example.com/signin',
|
|
29
|
+
expiredTime: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10 分钟后过期
|
|
30
|
+
};
|
|
31
|
+
const mockPendingCheck = {
|
|
32
|
+
status: 'Pending'
|
|
33
|
+
};
|
|
34
|
+
const mockFinishedCheck = {
|
|
35
|
+
status: 'Finished',
|
|
36
|
+
token: 'test-token-456'
|
|
37
|
+
};
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
localStorageMock.clear();
|
|
40
|
+
vi.clearAllTimers();
|
|
41
|
+
mockApi = {
|
|
42
|
+
applySignin: vi.fn().mockResolvedValue({
|
|
43
|
+
isError: () => false,
|
|
44
|
+
value: mockTicket
|
|
45
|
+
}),
|
|
46
|
+
checkStatus: vi.fn().mockResolvedValue({
|
|
47
|
+
isError: () => false,
|
|
48
|
+
value: mockPendingCheck
|
|
49
|
+
}),
|
|
50
|
+
onSuccess: vi.fn(),
|
|
51
|
+
onCancel: vi.fn()
|
|
52
|
+
};
|
|
53
|
+
service = new SigninService(mockApi, {
|
|
54
|
+
pollInterval: 100,
|
|
55
|
+
errorAutoCloseDelay: 1000,
|
|
56
|
+
successDisplayDuration: 500
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
service.dispose();
|
|
61
|
+
});
|
|
62
|
+
describe('状态持久化', () => {
|
|
63
|
+
it('应该在状态变化时自动保存到 localStorage', async () => {
|
|
64
|
+
await service.init();
|
|
65
|
+
// 开始登录流程
|
|
66
|
+
await service.start();
|
|
67
|
+
// 等待状态保存 (reaction 有 100ms delay)
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
69
|
+
// 验证 localStorage 中有保存的状态
|
|
70
|
+
const saved = localStorageMock.getItem('__signin_service_state__');
|
|
71
|
+
expect(saved).toBeTruthy();
|
|
72
|
+
const state = JSON.parse(saved);
|
|
73
|
+
expect(state.ticket).toEqual(mockTicket);
|
|
74
|
+
expect(state.dialogStatus).toBe('waiting');
|
|
75
|
+
});
|
|
76
|
+
it('应该在初始化时恢复之前保存的状态', async () => {
|
|
77
|
+
// 模拟已保存的状态
|
|
78
|
+
const savedState = {
|
|
79
|
+
ticket: mockTicket,
|
|
80
|
+
dialogStatus: 'waiting',
|
|
81
|
+
errorMessage: '',
|
|
82
|
+
timestamp: Date.now()
|
|
83
|
+
};
|
|
84
|
+
localStorageMock.setItem('__signin_service_state__', JSON.stringify(savedState));
|
|
85
|
+
// 创建新的服务实例
|
|
86
|
+
const newService = new SigninService(mockApi);
|
|
87
|
+
await newService.init();
|
|
88
|
+
// 验证状态已恢复
|
|
89
|
+
expect(newService.hasActiveTicket).toBe(true);
|
|
90
|
+
expect(newService.dialogStatus).toBe('waiting');
|
|
91
|
+
expect(newService.signinUrl).toBe(mockTicket.signinUrl);
|
|
92
|
+
newService.dispose();
|
|
93
|
+
});
|
|
94
|
+
it('应该在票据过期时不恢复状态', async () => {
|
|
95
|
+
// 模拟已过期的票据
|
|
96
|
+
const expiredTicket = Object.assign(Object.assign({}, mockTicket), { expiredTime: new Date(Date.now() - 1000).toISOString() // 已过期
|
|
97
|
+
});
|
|
98
|
+
const savedState = {
|
|
99
|
+
ticket: expiredTicket,
|
|
100
|
+
dialogStatus: 'waiting',
|
|
101
|
+
errorMessage: '',
|
|
102
|
+
timestamp: Date.now()
|
|
103
|
+
};
|
|
104
|
+
localStorageMock.setItem('__signin_service_state__', JSON.stringify(savedState));
|
|
105
|
+
// 创建新的服务实例
|
|
106
|
+
const newService = new SigninService(mockApi);
|
|
107
|
+
await newService.init();
|
|
108
|
+
// 验证状态未恢复
|
|
109
|
+
expect(newService.hasActiveTicket).toBe(false);
|
|
110
|
+
expect(newService.dialogStatus).toBe('idle');
|
|
111
|
+
// 验证 localStorage 已清理
|
|
112
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeNull();
|
|
113
|
+
newService.dispose();
|
|
114
|
+
});
|
|
115
|
+
it('应该在登录成功后清理持久化状态', async () => {
|
|
116
|
+
await service.init();
|
|
117
|
+
await service.start();
|
|
118
|
+
// 等待状态保存
|
|
119
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
120
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeTruthy();
|
|
121
|
+
// 模拟登录成功
|
|
122
|
+
mockApi.checkStatus = vi.fn().mockResolvedValue({
|
|
123
|
+
isError: () => false,
|
|
124
|
+
value: mockFinishedCheck
|
|
125
|
+
});
|
|
126
|
+
// 等待轮询检测到成功状态
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
128
|
+
// 等待成功处理完成
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
130
|
+
// 验证状态已清理
|
|
131
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
it('应该在取消登录后清理持久化状态', async () => {
|
|
134
|
+
await service.init();
|
|
135
|
+
await service.start();
|
|
136
|
+
// 等待状态保存
|
|
137
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
138
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeTruthy();
|
|
139
|
+
// 取消登录
|
|
140
|
+
await service.cancel();
|
|
141
|
+
// 验证状态已清理
|
|
142
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeNull();
|
|
143
|
+
expect(service.dialogStatus).toBe('idle');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('页面刷新场景', () => {
|
|
147
|
+
it('应该在页面刷新后继续轮询检查登录状态', async () => {
|
|
148
|
+
// 第一次会话:开始登录
|
|
149
|
+
await service.init();
|
|
150
|
+
await service.start();
|
|
151
|
+
// 等待状态保存
|
|
152
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
153
|
+
// 模拟页面刷新 - 销毁旧服务
|
|
154
|
+
service.dispose();
|
|
155
|
+
// 创建新服务实例(模拟页面重新加载)
|
|
156
|
+
const newService = new SigninService(mockApi, {
|
|
157
|
+
pollInterval: 100,
|
|
158
|
+
errorAutoCloseDelay: 1000,
|
|
159
|
+
successDisplayDuration: 500
|
|
160
|
+
});
|
|
161
|
+
await newService.init();
|
|
162
|
+
// 验证状态已恢复且开始轮询
|
|
163
|
+
expect(newService.dialogStatus).toBe('waiting');
|
|
164
|
+
expect(newService.hasActiveTicket).toBe(true);
|
|
165
|
+
// 验证轮询正在进行
|
|
166
|
+
expect(mockApi.checkStatus).toHaveBeenCalled();
|
|
167
|
+
newService.dispose();
|
|
168
|
+
});
|
|
169
|
+
it('应该在页面刷新后显示登录弹窗', async () => {
|
|
170
|
+
// 第一次会话:开始登录
|
|
171
|
+
await service.init();
|
|
172
|
+
await service.start();
|
|
173
|
+
// 等待状态保存
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
175
|
+
expect(service.isDialogOpen).toBe(true);
|
|
176
|
+
// 模拟页面刷新
|
|
177
|
+
service.dispose();
|
|
178
|
+
// 创建新服务实例
|
|
179
|
+
const newService = new SigninService(mockApi);
|
|
180
|
+
await newService.init();
|
|
181
|
+
// 验证弹窗仍然显示
|
|
182
|
+
expect(newService.isDialogOpen).toBe(true);
|
|
183
|
+
expect(newService.dialogStatus).toBe('waiting');
|
|
184
|
+
newService.dispose();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('错误处理', () => {
|
|
188
|
+
it('应该处理 localStorage 损坏的数据', async () => {
|
|
189
|
+
// 保存无效的 JSON
|
|
190
|
+
localStorageMock.setItem('__signin_service_state__', 'invalid json');
|
|
191
|
+
const newService = new SigninService(mockApi);
|
|
192
|
+
await newService.init();
|
|
193
|
+
// 应该优雅处理并从 idle 状态开始
|
|
194
|
+
expect(newService.dialogStatus).toBe('idle');
|
|
195
|
+
expect(newService.hasActiveTicket).toBe(false);
|
|
196
|
+
newService.dispose();
|
|
197
|
+
});
|
|
198
|
+
it('应该在恢复状态失败时清理 localStorage', async () => {
|
|
199
|
+
localStorageMock.setItem('__signin_service_state__', 'invalid json');
|
|
200
|
+
const newService = new SigninService(mockApi);
|
|
201
|
+
await newService.init();
|
|
202
|
+
// 验证已清理
|
|
203
|
+
expect(localStorageMock.getItem('__signin_service_state__')).toBeNull();
|
|
204
|
+
newService.dispose();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CatchResult } from '@taicode/common-base';
|
|
2
|
+
/**
|
|
3
|
+
* 登录状态
|
|
4
|
+
*/
|
|
5
|
+
export type SigninStatus = 'Pending' | 'Finished' | 'Expired' | 'Cancelled';
|
|
6
|
+
/**
|
|
7
|
+
* 登录票据信息
|
|
8
|
+
*/
|
|
9
|
+
export interface ApplySigninResult {
|
|
10
|
+
/** 票据 ID */
|
|
11
|
+
ticket: string;
|
|
12
|
+
/** 登录页 URL */
|
|
13
|
+
signinUrl: string;
|
|
14
|
+
/** 过期时间 */
|
|
15
|
+
expiredTime: string | Date;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 登录状态检查结果
|
|
19
|
+
*/
|
|
20
|
+
export interface CheckStatusResult {
|
|
21
|
+
/** 当前状态 */
|
|
22
|
+
status: SigninStatus;
|
|
23
|
+
/** 认证成功后的令牌(仅在 status 为 Finished 时存在) */
|
|
24
|
+
token?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* API 错误类型
|
|
28
|
+
*/
|
|
29
|
+
export interface SigninApiError {
|
|
30
|
+
/** 错误消息 */
|
|
31
|
+
message: string;
|
|
32
|
+
/** 错误代码(可选) */
|
|
33
|
+
code?: string;
|
|
34
|
+
/** 原始错误(可选) */
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 登录 API 配置
|
|
39
|
+
* 应用需要实现这些方法来适配自己的认证系统
|
|
40
|
+
*/
|
|
41
|
+
export interface SigninApi {
|
|
42
|
+
/**
|
|
43
|
+
* 用户取消登录时的回调
|
|
44
|
+
*/
|
|
45
|
+
onCancel?: () => Promise<void> | void;
|
|
46
|
+
/**
|
|
47
|
+
* 认证成功后的回调
|
|
48
|
+
* @param token 认证令牌
|
|
49
|
+
*/
|
|
50
|
+
onSuccess?: (token: string) => Promise<void> | void;
|
|
51
|
+
/**
|
|
52
|
+
* 申请跨域认证票据
|
|
53
|
+
* @returns 返回票据信息或错误
|
|
54
|
+
*/
|
|
55
|
+
applySignin: () => Promise<CatchResult<ApplySigninResult, SigninApiError>>;
|
|
56
|
+
/**
|
|
57
|
+
* 检查票据状态
|
|
58
|
+
* @param ticket 票据 ID
|
|
59
|
+
* @returns 返回票据状态信息或错误
|
|
60
|
+
*/
|
|
61
|
+
checkStatus: (ticket: string) => Promise<CatchResult<CheckStatusResult, SigninApiError>>;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../source/signin/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,CAAA;AAE3E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,cAAc;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW;IACX,MAAM,EAAE,YAAY,CAAA;IACpB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,OAAO,EAAE,MAAM,CAAA;IACf,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,eAAe;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB;;KAEC;IACD,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAErC;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAEnD;;;OAGG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC,CAAA;IAE1E;;;;OAIG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC,CAAA;CACzF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/size-provider/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SizeProvider } from './size-provider';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
interface SizeProviderProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
render?: (width: number, height: number) => ReactNode;
|
|
5
|
+
children?: (width: number, height: number) => ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare const SizeProvider: React.FC<SizeProviderProps>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=size-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-provider.d.ts","sourceRoot":"","sources":["../../source/size-provider/size-provider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAA+B,SAAS,EAAE,MAAM,OAAO,CAAA;AAErE,UAAU,iBAAiB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,CAAA;IACrD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,CAAA;CACxD;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAuCpD,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useRef, useState, useEffect } from 'react';
|
|
4
|
+
export const SizeProvider = (props) => {
|
|
5
|
+
const { children, className } = props;
|
|
6
|
+
const [width, setWidth] = useState(0);
|
|
7
|
+
const [height, setHeight] = useState(0);
|
|
8
|
+
const sizeRef = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const { width, height } = entry.contentRect;
|
|
13
|
+
setWidth(width);
|
|
14
|
+
setHeight(height);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (sizeRef.current) {
|
|
18
|
+
resizeObserver.observe(sizeRef.current);
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
if (sizeRef.current) {
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
resizeObserver.unobserve(sizeRef.current);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
const render = props.render || children || null;
|
|
28
|
+
return (_jsxs("div", { className: clsx(className, 'relative w-full h-full overflow-hidden isolate'), children: [_jsx("div", { ref: sizeRef, className: 'absolute flex-shrink-0 w-full h-full z-0 opacity-0', children: "SizeProvider" }), _jsx("div", { className: 'absolute w-full h-full z-1', children: render === null || render === void 0 ? void 0 : render(width, height) })] }));
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taicode/common-web",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.18",
|
|
4
4
|
"author": "Alain",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "",
|
|
@@ -30,11 +30,14 @@
|
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@taicode/common-base": ">=1.1.0",
|
|
32
32
|
"@needle-di/core": ">=1.0.0",
|
|
33
|
+
"@heroicons/react": "^2.2.0",
|
|
34
|
+
"mobx-react-lite": "^4.1.1",
|
|
33
35
|
"@types/react": ">=18",
|
|
34
36
|
"mobx": ">=6",
|
|
35
37
|
"react": ">=18"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
40
|
+
"@heroicons/react": "^2.2.0",
|
|
38
41
|
"@headlessui/react": "^2.2.4",
|
|
39
42
|
"framer-motion": "^12.19.2",
|
|
40
43
|
"clsx": "^2.1.1"
|
|
@@ -45,8 +48,9 @@
|
|
|
45
48
|
"jsdom": "^23.0.0",
|
|
46
49
|
"vitest": "^1.0.0",
|
|
47
50
|
"@vitest/ui": "^1.0.0",
|
|
48
|
-
"@needle-di/core": "^1.0.0",
|
|
49
51
|
"@types/react": "^18.0.0",
|
|
52
|
+
"@needle-di/core": "^1.0.0",
|
|
53
|
+
"mobx-react-lite": "^4.1.1",
|
|
50
54
|
"mobx": "^6.0.0",
|
|
51
55
|
"react": "^18.0.0"
|
|
52
56
|
}
|