mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 22:40:28 +00:00
feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能
- 增加全局掉线检测弹窗 - 增强登录错误解析,支持显示 serverErrorCode 和 message - 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取 - 核心层解耦,通过事件抛出 KickedOffLine 通知 - 支持前端点击刷新二维码接口
This commit is contained in:
parent
fbccf8be24
commit
0918b17257
@ -125,7 +125,7 @@ export class NapCatCore {
|
|||||||
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
||||||
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
|
ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => {
|
||||||
container.bind(ServiceClass).toSelf();
|
container.bind(ServiceClass).toSelf();
|
||||||
//console.log(`Registering service handler for: ${serviceName}`);
|
// console.log(`Registering service handler for: ${serviceName}`);
|
||||||
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
|
this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => {
|
||||||
const serviceInstance = container.get(ServiceClass);
|
const serviceInstance = container.get(ServiceClass);
|
||||||
return serviceInstance.handler(seq, hex_data);
|
return serviceInstance.handler(seq, hex_data);
|
||||||
@ -177,8 +177,10 @@ export class NapCatCore {
|
|||||||
|
|
||||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||||
// 下线通知
|
// 下线通知
|
||||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
|
||||||
|
this.context.logger.logError(tips);
|
||||||
this.selfInfo.online = false;
|
this.selfInfo.online = false;
|
||||||
|
this.event.emit('KickedOffLine', tips);
|
||||||
};
|
};
|
||||||
msgListener.onRecvMsg = (msgs) => {
|
msgListener.onRecvMsg = (msgs) => {
|
||||||
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { TypedEventEmitter } from './typeEvent';
|
import { TypedEventEmitter } from './typeEvent';
|
||||||
|
|
||||||
export interface AppEvents {
|
export interface AppEvents {
|
||||||
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number };
|
'event:emoji_like': { groupId: string; senderUin: string; emojiId: string, msgSeq: string, isAdd: boolean, count: number; };
|
||||||
|
KickedOffLine: string;
|
||||||
}
|
}
|
||||||
export const appEvent = new TypedEventEmitter<AppEvents>();
|
export const appEvent = new TypedEventEmitter<AppEvents>();
|
||||||
|
|||||||
@ -246,7 +246,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter);
|
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleConfigChange<CT extends NetworkAdapterConfig>(
|
private async handleConfigChange<CT extends NetworkAdapterConfig> (
|
||||||
prevConfig: NetworkAdapterConfig[],
|
prevConfig: NetworkAdapterConfig[],
|
||||||
nowConfig: NetworkAdapterConfig[],
|
nowConfig: NetworkAdapterConfig[],
|
||||||
adapterClass: new (
|
adapterClass: new (
|
||||||
@ -384,6 +384,7 @@ export class NapCatOneBot11Adapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
msgListener.onKickedOffLine = async (kick) => {
|
msgListener.onKickedOffLine = async (kick) => {
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(false);
|
||||||
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
|
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);
|
||||||
this.networkManager
|
this.networkManager
|
||||||
.emitEvent(event)
|
.emitEvent(event)
|
||||||
|
|||||||
@ -128,10 +128,13 @@ async function handleLogin (
|
|||||||
|
|
||||||
const loginListener = new NodeIKernelLoginListener();
|
const loginListener = new NodeIKernelLoginListener();
|
||||||
loginListener.onUserLoggedIn = (userid: string) => {
|
loginListener.onUserLoggedIn = (userid: string) => {
|
||||||
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
|
const tips = `当前账号(${userid})已登录,无法重复登录`;
|
||||||
|
logger.logError(tips);
|
||||||
|
WebUiDataRuntime.setQQLoginError(tips);
|
||||||
};
|
};
|
||||||
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
||||||
context.isLogined = true;
|
context.isLogined = true;
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(true);
|
||||||
inner_resolve({
|
inner_resolve({
|
||||||
uid: loginResult.uid,
|
uid: loginResult.uid,
|
||||||
uin: loginResult.uin,
|
uin: loginResult.uin,
|
||||||
@ -170,13 +173,16 @@ async function handleLogin (
|
|||||||
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
|
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
|
||||||
if (errType === 1 && errCode === 3) {
|
if (errType === 1 && errCode === 3) {
|
||||||
// 二维码过期刷新
|
// 二维码过期刷新
|
||||||
|
WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新');
|
||||||
}
|
}
|
||||||
loginService.getQRCodePicture();
|
loginService.getQRCodePicture();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loginListener.onLoginFailed = (...args) => {
|
loginListener.onLoginFailed = (...args) => {
|
||||||
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
|
const errInfo = JSON.stringify(args);
|
||||||
|
logger.logError('[Core] [Login] Login Error , ErrInfo: ', errInfo);
|
||||||
|
WebUiDataRuntime.setQQLoginError(`登录失败: ${errInfo}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
||||||
@ -184,17 +190,29 @@ async function handleLogin (
|
|||||||
return await selfInfo;
|
return await selfInfo;
|
||||||
}
|
}
|
||||||
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
|
async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
|
||||||
|
// 注册刷新二维码回调
|
||||||
|
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
|
||||||
|
loginService.getQRCodePicture();
|
||||||
|
});
|
||||||
|
|
||||||
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
if (uin) {
|
if (uin) {
|
||||||
logger.log('正在快速登录 ', uin);
|
logger.log('正在快速登录 ', uin);
|
||||||
loginService.quickLoginWithUin(uin).then(res => {
|
loginService.quickLoginWithUin(uin).then(res => {
|
||||||
if (res.loginErrorInfo.errMsg) {
|
if (res.loginErrorInfo.errMsg) {
|
||||||
|
WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg);
|
||||||
|
loginService.getQRCodePicture();
|
||||||
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
||||||
|
} else {
|
||||||
|
WebUiDataRuntime.setQQLoginStatus(true);
|
||||||
|
WebUiDataRuntime.setQQLoginError('');
|
||||||
|
resolve({ result: true, message: '' });
|
||||||
}
|
}
|
||||||
resolve({ result: true, message: '' });
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.logError(e);
|
logger.logError(e);
|
||||||
|
WebUiDataRuntime.setQQLoginError('快速登录发生错误');
|
||||||
|
loginService.getQRCodePicture();
|
||||||
resolve({ result: false, message: '快速登录发生错误' });
|
resolve({ result: false, message: '快速登录发生错误' });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -209,6 +227,7 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.loginErrorInfo.errMsg) {
|
if (result.loginErrorInfo.errMsg) {
|
||||||
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
||||||
|
WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg);
|
||||||
if (!context.isLogined) loginService.getQRCodePicture();
|
if (!context.isLogined) loginService.getQRCodePicture();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -452,6 +471,10 @@ export class NapCatShell {
|
|||||||
|
|
||||||
async InitNapCat () {
|
async InitNapCat () {
|
||||||
await this.core.initCore();
|
await this.core.initCore();
|
||||||
|
// 监听下线通知并同步到 WebUI
|
||||||
|
this.core.event.on('KickedOffLine', (tips: string) => {
|
||||||
|
WebUiDataRuntime.setQQLoginError(tips);
|
||||||
|
});
|
||||||
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
||||||
// 注册到 WebUiDataRuntime,供调试功能使用
|
// 注册到 WebUiDataRuntime,供调试功能使用
|
||||||
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||||
@ -459,4 +482,3 @@ export class NapCatShell {
|
|||||||
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-vite",
|
"name": "napcat-vite",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build"
|
"_build": "vite build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./*": {
|
||||||
".": {
|
"import": "./*"
|
||||||
"import": "./index.ts"
|
|
||||||
},
|
|
||||||
"./*": {
|
|
||||||
"import": "./*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -27,9 +27,17 @@ export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
|||||||
|
|
||||||
// 获取QQ登录状态
|
// 获取QQ登录状态
|
||||||
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
|
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
|
||||||
|
// 从 OneBot 上下文获取实时的 selfInfo.online 状态
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
const selfInfo = oneBotContext?.core?.selfInfo;
|
||||||
|
const isOnline = selfInfo?.online;
|
||||||
|
const qqLoginStatus = WebUiDataRuntime.getQQLoginStatus();
|
||||||
|
// 必须同时满足:已登录且在线(online 必须明确为 true)
|
||||||
|
const isLogin = qqLoginStatus && isOnline === true;
|
||||||
const data = {
|
const data = {
|
||||||
isLogin: WebUiDataRuntime.getQQLoginStatus(),
|
isLogin,
|
||||||
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
|
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
|
||||||
|
loginError: WebUiDataRuntime.getQQLoginError(),
|
||||||
};
|
};
|
||||||
return sendSuccess(res, data);
|
return sendSuccess(res, data);
|
||||||
};
|
};
|
||||||
@ -88,3 +96,15 @@ export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
|
|||||||
await WebUiConfig.UpdateAutoLoginAccount(uin);
|
await WebUiConfig.UpdateAutoLoginAccount(uin);
|
||||||
return sendSuccess(res, null);
|
return sendSuccess(res, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 刷新QQ登录二维码
|
||||||
|
export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => {
|
||||||
|
// 判断是否已经登录
|
||||||
|
if (WebUiDataRuntime.getQQLoginStatus()) {
|
||||||
|
// 已经登录
|
||||||
|
return sendError(res, 'QQ Is Logined');
|
||||||
|
}
|
||||||
|
// 刷新二维码
|
||||||
|
await WebUiDataRuntime.refreshQRCode();
|
||||||
|
return sendSuccess(res, null);
|
||||||
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
uin: '',
|
uin: '',
|
||||||
nick: '',
|
nick: '',
|
||||||
},
|
},
|
||||||
|
QQLoginError: '',
|
||||||
QQVersion: 'unknown',
|
QQVersion: 'unknown',
|
||||||
OneBotContext: null,
|
OneBotContext: null,
|
||||||
onQQLoginStatusChange: async (status: boolean) => {
|
onQQLoginStatusChange: async (status: boolean) => {
|
||||||
@ -21,6 +22,9 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
},
|
},
|
||||||
onWebUiTokenChange: async (_token: string) => {
|
onWebUiTokenChange: async (_token: string) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
onRefreshQRCode: async () => {
|
||||||
|
// 默认空实现,由 shell 注册真实回调
|
||||||
},
|
},
|
||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
onOB11ConfigChanged: async () => {
|
onOB11ConfigChanged: async () => {
|
||||||
@ -163,4 +167,22 @@ export const WebUiDataRuntime = {
|
|||||||
getOneBotContext (): any | null {
|
getOneBotContext (): any | null {
|
||||||
return LoginRuntime.OneBotContext;
|
return LoginRuntime.OneBotContext;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setQQLoginError (error: string): void {
|
||||||
|
LoginRuntime.QQLoginError = error;
|
||||||
|
},
|
||||||
|
|
||||||
|
getQQLoginError (): string {
|
||||||
|
return LoginRuntime.QQLoginError;
|
||||||
|
},
|
||||||
|
|
||||||
|
setRefreshQRCodeCallback (func: () => Promise<void>): void {
|
||||||
|
LoginRuntime.onRefreshQRCode = func;
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshQRCode (): Promise<void> {
|
||||||
|
// 清除错误信息
|
||||||
|
LoginRuntime.QQLoginError = '';
|
||||||
|
await LoginRuntime.onRefreshQRCode();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
getQQLoginInfoHandler,
|
getQQLoginInfoHandler,
|
||||||
getAutoLoginAccountHandler,
|
getAutoLoginAccountHandler,
|
||||||
setAutoLoginAccountHandler,
|
setAutoLoginAccountHandler,
|
||||||
|
QQRefreshQRcodeHandler,
|
||||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -28,5 +29,7 @@ router.post('/GetQQLoginInfo', getQQLoginInfoHandler);
|
|||||||
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler);
|
||||||
// router:设置自动登录QQ账号
|
// router:设置自动登录QQ账号
|
||||||
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
||||||
|
// router:刷新QQ登录二维码
|
||||||
|
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
||||||
|
|
||||||
export { router as QQLoginRouter };
|
export { router as QQLoginRouter };
|
||||||
|
|||||||
@ -43,9 +43,11 @@ export interface LoginRuntimeType {
|
|||||||
QQQRCodeURL: string;
|
QQQRCodeURL: string;
|
||||||
QQLoginUin: string;
|
QQLoginUin: string;
|
||||||
QQLoginInfo: SelfInfo;
|
QQLoginInfo: SelfInfo;
|
||||||
|
QQLoginError: string;
|
||||||
QQVersion: string;
|
QQVersion: string;
|
||||||
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
||||||
onWebUiTokenChange: (token: string) => Promise<void>;
|
onWebUiTokenChange: (token: string) => Promise<void>;
|
||||||
|
onRefreshQRCode: () => Promise<void>;
|
||||||
WebUiConfigQuickFunction: () => Promise<void>;
|
WebUiConfigQuickFunction: () => Promise<void>;
|
||||||
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
onNativeClose();
|
onNativeClose();
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
backdrop: 'z-[99]',
|
backdrop: 'z-[99] backdrop-blur-sm',
|
||||||
wrapper: 'z-[99]',
|
wrapper: 'z-[99]',
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@ -1,22 +1,70 @@
|
|||||||
|
import { Button } from '@heroui/button';
|
||||||
import { Spinner } from '@heroui/spinner';
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { IoAlertCircle, IoRefresh } from 'react-icons/io5';
|
||||||
|
|
||||||
interface QrCodeLoginProps {
|
interface QrCodeLoginProps {
|
||||||
qrcode: string
|
qrcode: string;
|
||||||
|
loginError?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode, loginError, onRefresh }) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
|
{loginError
|
||||||
{!qrcode && (
|
? (
|
||||||
<div className='absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center'>
|
<div className='flex flex-col items-center py-4'>
|
||||||
<Spinner color='primary' />
|
<div className='w-full flex justify-center mb-6'>
|
||||||
|
<div className='p-4 bg-danger-50 rounded-full'>
|
||||||
|
<IoAlertCircle className='text-danger' size={64} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-center space-y-2 px-4'>
|
||||||
|
<div className='text-xl font-bold text-danger'>登录失败</div>
|
||||||
|
<div className='text-default-600 text-sm leading-relaxed max-w-[300px]'>
|
||||||
|
{loginError}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onRefresh && (
|
||||||
|
<Button
|
||||||
|
className='mt-8 min-w-[160px]'
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
size='lg'
|
||||||
|
startContent={<IoRefresh />}
|
||||||
|
onPress={onRefresh}
|
||||||
|
>
|
||||||
|
重新获取二维码
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
|
||||||
|
{!qrcode && (
|
||||||
|
<div className='absolute left-0 top-0 right-0 bottom-0 bg-white dark:bg-zinc-900 bg-opacity-90 backdrop-blur-sm flex items-center justify-center z-10'>
|
||||||
|
<Spinner color='primary' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<QRCodeSVG size={180} value={qrcode || ' '} />
|
||||||
|
</div>
|
||||||
|
<div className='mt-5 text-center text-default-500 text-sm'>请使用QQ或者TIM扫描上方二维码</div>
|
||||||
|
{onRefresh && qrcode && (
|
||||||
|
<Button
|
||||||
|
className='mt-4'
|
||||||
|
variant='flat'
|
||||||
|
color='primary'
|
||||||
|
size='sm'
|
||||||
|
startContent={<IoRefresh />}
|
||||||
|
onPress={onRefresh}
|
||||||
|
>
|
||||||
|
刷新二维码
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<QRCodeSVG size={180} value={qrcode} />
|
|
||||||
</div>
|
|
||||||
<div className='mt-5 text-center'>请使用QQ或者TIM扫描上方二维码</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,8 +20,8 @@ export default class QQManager {
|
|||||||
public static async checkQQLoginStatus () {
|
public static async checkQQLoginStatus () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{
|
ServerResponse<{
|
||||||
isLogin: string
|
isLogin: string;
|
||||||
qrcodeurl: string
|
qrcodeurl: string;
|
||||||
}>
|
}>
|
||||||
>('/QQLogin/CheckLoginStatus');
|
>('/QQLogin/CheckLoginStatus');
|
||||||
|
|
||||||
@ -30,16 +30,20 @@ export default class QQManager {
|
|||||||
|
|
||||||
public static async checkQQLoginStatusWithQrcode () {
|
public static async checkQQLoginStatusWithQrcode () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{ qrcodeurl: string; isLogin: string }>
|
ServerResponse<{ qrcodeurl: string; isLogin: string; loginError?: string; }>
|
||||||
>('/QQLogin/CheckLoginStatus');
|
>('/QQLogin/CheckLoginStatus');
|
||||||
|
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async refreshQRCode () {
|
||||||
|
await serverRequest.post<ServerResponse<null>>('/QQLogin/RefreshQRcode');
|
||||||
|
}
|
||||||
|
|
||||||
public static async getQQLoginQrcode () {
|
public static async getQQLoginQrcode () {
|
||||||
const data = await serverRequest.post<
|
const data = await serverRequest.post<
|
||||||
ServerResponse<{
|
ServerResponse<{
|
||||||
qrcode: string
|
qrcode: string;
|
||||||
}>
|
}>
|
||||||
>('/QQLogin/GetQQLoginQrcode');
|
>('/QQLogin/GetQQLoginQrcode');
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import errorFallbackRender from '@/components/error_fallback';
|
|||||||
import SideBar from '@/components/sidebar';
|
import SideBar from '@/components/sidebar';
|
||||||
|
|
||||||
import useAuth from '@/hooks/auth';
|
import useAuth from '@/hooks/auth';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
|
|
||||||
import type { MenuItem } from '@/config/site';
|
import type { MenuItem } from '@/config/site';
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from '@/config/site';
|
||||||
@ -48,7 +49,43 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
|
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
|
||||||
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuth } = useAuth();
|
const { isAuth, revokeAuth } = useAuth();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const isOnlineRef = useRef(true);
|
||||||
|
|
||||||
|
// 定期检查 QQ 在线状态,掉线时弹窗提示
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuth) return;
|
||||||
|
const checkOnlineStatus = async () => {
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
if (currentPath === '/qq_login' || currentPath === '/web_login') return;
|
||||||
|
try {
|
||||||
|
const info = await QQManager.getQQLoginInfo();
|
||||||
|
if (info?.online === false && isOnlineRef.current === true) {
|
||||||
|
isOnlineRef.current = false;
|
||||||
|
dialog.confirm({
|
||||||
|
title: '账号已离线',
|
||||||
|
content: '您的 QQ 账号已下线,请重新登录。',
|
||||||
|
confirmText: '重新登陆',
|
||||||
|
cancelText: '退出账户',
|
||||||
|
onConfirm: () => navigate('/qq_login'),
|
||||||
|
onCancel: () => {
|
||||||
|
revokeAuth();
|
||||||
|
navigate('/web_login');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (info?.online === true) {
|
||||||
|
isOnlineRef.current = true;
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// 忽略请求错误
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const timer = setInterval(checkOnlineStatus, 5000);
|
||||||
|
checkOnlineStatus();
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [isAuth, location.pathname]);
|
||||||
|
|
||||||
const checkIsQQLogin = async () => {
|
const checkIsQQLogin = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await QQManager.checkQQLoginStatus();
|
const result = await QQManager.checkQQLoginStatus();
|
||||||
|
|||||||
@ -16,14 +16,39 @@ import type { QQItem } from '@/components/quick_login';
|
|||||||
import { ThemeSwitch } from '@/components/theme-switch';
|
import { ThemeSwitch } from '@/components/theme-switch';
|
||||||
|
|
||||||
import QQManager from '@/controllers/qq_manager';
|
import QQManager from '@/controllers/qq_manager';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
import PureLayout from '@/layouts/pure';
|
import PureLayout from '@/layouts/pure';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
|
const parseLoginError = (errorStr: string) => {
|
||||||
|
if (errorStr.startsWith('登录失败: ')) {
|
||||||
|
const jsonPart = errorStr.substring('登录失败: '.length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonPart);
|
||||||
|
|
||||||
|
if (Array.isArray(parsed) && parsed[1]) {
|
||||||
|
const info = parsed[1];
|
||||||
|
const codeStr = info.serverErrorCode ? ` (错误码: ${info.serverErrorCode})` : '';
|
||||||
|
|
||||||
|
return `${info.message || errorStr}${codeStr}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorStr;
|
||||||
|
};
|
||||||
|
|
||||||
export default function QQLoginPage () {
|
export default function QQLoginPage () {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const dialog = useDialog();
|
||||||
const [uinValue, setUinValue] = useState<string>('');
|
const [uinValue, setUinValue] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [qrcode, setQrcode] = useState<string>('');
|
const [qrcode, setQrcode] = useState<string>('');
|
||||||
|
const [loginError, setLoginError] = useState<string>('');
|
||||||
|
const lastErrorRef = useRef<string>('');
|
||||||
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
|
const [qqList, setQQList] = useState<(QQItem | LoginListItem)[]>([]);
|
||||||
const [refresh, setRefresh] = useState<boolean>(false);
|
const [refresh, setRefresh] = useState<boolean>(false);
|
||||||
const firstLoad = useRef<boolean>(true);
|
const firstLoad = useRef<boolean>(true);
|
||||||
@ -61,6 +86,20 @@ export default function QQLoginPage () {
|
|||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} else {
|
} else {
|
||||||
setQrcode(data.qrcodeurl);
|
setQrcode(data.qrcodeurl);
|
||||||
|
if (data.loginError && data.loginError !== lastErrorRef.current) {
|
||||||
|
lastErrorRef.current = data.loginError;
|
||||||
|
setLoginError(data.loginError);
|
||||||
|
const friendlyMsg = parseLoginError(data.loginError);
|
||||||
|
|
||||||
|
dialog.alert({
|
||||||
|
title: '登录失败',
|
||||||
|
content: friendlyMsg,
|
||||||
|
confirmText: '确定',
|
||||||
|
});
|
||||||
|
} else if (!data.loginError) {
|
||||||
|
lastErrorRef.current = '';
|
||||||
|
setLoginError('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = (error as Error).message;
|
const msg = (error as Error).message;
|
||||||
@ -99,6 +138,18 @@ export default function QQLoginPage () {
|
|||||||
setUinValue(e.target.value);
|
setUinValue(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefreshQRCode = async () => {
|
||||||
|
try {
|
||||||
|
lastErrorRef.current = '';
|
||||||
|
setLoginError('');
|
||||||
|
await QQManager.refreshQRCode();
|
||||||
|
toast.success('已发送刷新请求');
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
toast.error(`刷新二维码失败: ${msg}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
onUpdateQrCode();
|
onUpdateQrCode();
|
||||||
@ -159,7 +210,11 @@ export default function QQLoginPage () {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key='qrcode' title='扫码登录'>
|
<Tab key='qrcode' title='扫码登录'>
|
||||||
<QrCodeLogin qrcode={qrcode} />
|
<QrCodeLogin
|
||||||
|
loginError={parseLoginError(loginError)}
|
||||||
|
qrcode={qrcode}
|
||||||
|
onRefresh={onRefreshQRCode}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import viteCompression from 'vite-plugin-compression';
|
// import viteCompression from 'vite-plugin-compression';
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
ViteImageOptimizer({})
|
ViteImageOptimizer({}),
|
||||||
],
|
],
|
||||||
base: '/webui/',
|
base: '/webui/',
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user