feat: 接入 QQBot 更新登录进度展示
This commit is contained in:
parent
dea3306b41
commit
d2af27dd9a
@ -84,6 +84,14 @@ export namespace QqbotApi {
|
|||||||
webuiPort?: null | number;
|
webuiPort?: null | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountScanEvent {
|
||||||
|
createdAt: number;
|
||||||
|
message: string;
|
||||||
|
result?: AccountScanResult;
|
||||||
|
status: 'error' | 'info' | 'processing' | 'success';
|
||||||
|
step: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Rule {
|
export interface Rule {
|
||||||
cooldownMs: number;
|
cooldownMs: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -355,6 +363,22 @@ export function cancelQqbotAccountScan(sessionId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getQqbotAccountScanEventsUrl(sessionId: string) {
|
||||||
|
return buildApiUrl(
|
||||||
|
`/qqbot/account/scan/events?sessionId=${encodeURIComponent(sessionId)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiUrl(path: string) {
|
||||||
|
const baseUrl = requestClient.getBaseUrl() || '';
|
||||||
|
if (!baseUrl) return path;
|
||||||
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
|
if (/^https?:\/\//i.test(baseUrl)) {
|
||||||
|
return new URL(path, baseUrl).toString();
|
||||||
|
}
|
||||||
|
return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getQqbotRuleList(params: QqbotApi.Query) {
|
export function getQqbotRuleList(params: QqbotApi.Query) {
|
||||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Rule>>(
|
return requestClient.get<QqbotApi.PageResult<QqbotApi.Rule>>(
|
||||||
'/qqbot/rule/list',
|
'/qqbot/rule/list',
|
||||||
|
|||||||
@ -14,7 +14,15 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
|||||||
import { Plus } from '@vben/icons';
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
import { useQRCode } from '@vueuse/integrations/useQRCode';
|
import { useQRCode } from '@vueuse/integrations/useQRCode';
|
||||||
import { Alert, Button, message, Space, Tag, Typography } from 'antdv-next';
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Space,
|
||||||
|
Steps,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import {
|
import {
|
||||||
@ -22,6 +30,7 @@ import {
|
|||||||
createQqbotAccount,
|
createQqbotAccount,
|
||||||
deleteQqbotAccount,
|
deleteQqbotAccount,
|
||||||
getQqbotAccountList,
|
getQqbotAccountList,
|
||||||
|
getQqbotAccountScanEventsUrl,
|
||||||
getQqbotAccountScanStatus,
|
getQqbotAccountScanStatus,
|
||||||
kickQqbotAccount,
|
kickQqbotAccount,
|
||||||
refreshQqbotAccountScanQrcode,
|
refreshQqbotAccountScanQrcode,
|
||||||
@ -33,6 +42,7 @@ import { KtTable, useKtTable } from '#/components/ktTable';
|
|||||||
|
|
||||||
const AKtTable = KtTable as any;
|
const AKtTable = KtTable as any;
|
||||||
const AButton = Button as any;
|
const AButton = Button as any;
|
||||||
|
const ASteps = Steps as any;
|
||||||
const ATypographyLink = Typography.Link as any;
|
const ATypographyLink = Typography.Link as any;
|
||||||
const ATypographyText = Typography.Text as any;
|
const ATypographyText = Typography.Text as any;
|
||||||
|
|
||||||
@ -45,6 +55,7 @@ export default defineComponent({
|
|||||||
const scanQrcodeImageFailed = ref(false);
|
const scanQrcodeImageFailed = ref(false);
|
||||||
const scanQrcodeRevision = ref(0);
|
const scanQrcodeRevision = ref(0);
|
||||||
const scanQrcodeText = ref('');
|
const scanQrcodeText = ref('');
|
||||||
|
const scanEvents = ref<QqbotApi.AccountScanEvent[]>([]);
|
||||||
const scanState = reactive<{
|
const scanState = reactive<{
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
@ -80,7 +91,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
return qrcode;
|
return qrcode;
|
||||||
});
|
});
|
||||||
|
const scanProgressItems = computed(() =>
|
||||||
|
scanEvents.value.map((event) => ({
|
||||||
|
description: formatEventTime(event.createdAt),
|
||||||
|
status: getScanStepStatus(event.status),
|
||||||
|
title: event.message,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const scanProgressCurrent = computed(() =>
|
||||||
|
Math.max(scanProgressItems.value.length - 1, 0),
|
||||||
|
);
|
||||||
let scanTimer: number | undefined;
|
let scanTimer: number | undefined;
|
||||||
|
let scanEventSessionId = '';
|
||||||
|
let scanEventSource: EventSource | undefined;
|
||||||
|
|
||||||
const [AccountForm, accountFormApi] = useVbenForm({
|
const [AccountForm, accountFormApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
@ -296,6 +319,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopScanPolling();
|
stopScanPolling();
|
||||||
|
stopScanEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openScanCreate() {
|
async function openScanCreate() {
|
||||||
@ -362,9 +386,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (result.status === 'pending') {
|
if (result.status === 'pending') {
|
||||||
startScanPolling();
|
startScanPolling();
|
||||||
|
startScanEvents(result.sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopScanPolling();
|
stopScanPolling();
|
||||||
|
stopScanEvents();
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
message.success(
|
message.success(
|
||||||
result.selfId ? `账号 ${result.selfId} 登录态已更新` : '账号已更新',
|
result.selfId ? `账号 ${result.selfId} 登录态已更新` : '账号已更新',
|
||||||
@ -412,8 +438,57 @@ export default defineComponent({
|
|||||||
scanTimer = undefined;
|
scanTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startScanEvents(sessionId?: string) {
|
||||||
|
if (!sessionId || scanEventSessionId === sessionId) return;
|
||||||
|
stopScanEvents();
|
||||||
|
scanEventSessionId = sessionId;
|
||||||
|
const source = new EventSource(getQqbotAccountScanEventsUrl(sessionId), {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
scanEventSource = source;
|
||||||
|
source.addEventListener('message', (event) => {
|
||||||
|
handleScanEvent(event.data);
|
||||||
|
});
|
||||||
|
source.addEventListener('error', () => {
|
||||||
|
stopScanEvents();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopScanEvents() {
|
||||||
|
if (scanEventSource) {
|
||||||
|
scanEventSource.close();
|
||||||
|
}
|
||||||
|
scanEventSource = undefined;
|
||||||
|
scanEventSessionId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScanEvent(payload: string) {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(payload) as QqbotApi.AccountScanEvent;
|
||||||
|
const index = scanEvents.value.findIndex(
|
||||||
|
(item) => item.step === event.step,
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
scanEvents.value.push(event);
|
||||||
|
} else {
|
||||||
|
scanEvents.value.splice(index, 1, event);
|
||||||
|
}
|
||||||
|
if (scanEvents.value.length > 20) {
|
||||||
|
scanEvents.value.splice(0, scanEvents.value.length - 20);
|
||||||
|
}
|
||||||
|
if (event.result) {
|
||||||
|
void applyScanResult(event.result, {
|
||||||
|
reloadQrcode: event.step === 'qrcode-ready',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed SSE chunks and wait for the next event.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetScanState(mode: 'create' | 'refresh') {
|
function resetScanState(mode: 'create' | 'refresh') {
|
||||||
stopScanPolling();
|
stopScanPolling();
|
||||||
|
stopScanEvents();
|
||||||
Object.assign(scanState, {
|
Object.assign(scanState, {
|
||||||
containerId: undefined,
|
containerId: undefined,
|
||||||
containerName: undefined,
|
containerName: undefined,
|
||||||
@ -428,11 +503,13 @@ export default defineComponent({
|
|||||||
scanQrcodeImageFailed.value = false;
|
scanQrcodeImageFailed.value = false;
|
||||||
scanQrcodeRevision.value = 0;
|
scanQrcodeRevision.value = 0;
|
||||||
scanQrcodeText.value = '';
|
scanQrcodeText.value = '';
|
||||||
|
scanEvents.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupScanSession() {
|
function cleanupScanSession() {
|
||||||
const sessionId = scanState.sessionId;
|
const sessionId = scanState.sessionId;
|
||||||
stopScanPolling();
|
stopScanPolling();
|
||||||
|
stopScanEvents();
|
||||||
if (sessionId && scanState.status === 'pending') {
|
if (sessionId && scanState.status === 'pending') {
|
||||||
void cancelQqbotAccountScan(sessionId);
|
void cancelQqbotAccountScan(sessionId);
|
||||||
}
|
}
|
||||||
@ -469,6 +546,19 @@ export default defineComponent({
|
|||||||
return '扫码登录请求失败';
|
return '扫码登录请求失败';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScanStepStatus(status: QqbotApi.AccountScanEvent['status']) {
|
||||||
|
if (status === 'error') return 'error';
|
||||||
|
if (status === 'processing') return 'process';
|
||||||
|
if (status === 'success') return 'finish';
|
||||||
|
return 'wait';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEventTime(value: number) {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleTimeString('zh-CN', { hour12: false });
|
||||||
|
}
|
||||||
|
|
||||||
function isQrcodeImageCandidate(value: string) {
|
function isQrcodeImageCandidate(value: string) {
|
||||||
return (
|
return (
|
||||||
/^data:image\//i.test(value) ||
|
/^data:image\//i.test(value) ||
|
||||||
@ -779,6 +869,14 @@ export default defineComponent({
|
|||||||
type="info"
|
type="info"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{scanProgressItems.value.length > 0 ? (
|
||||||
|
<ASteps
|
||||||
|
current={scanProgressCurrent.value}
|
||||||
|
direction="vertical"
|
||||||
|
items={scanProgressItems.value}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
{scanQrcodeText.value ? (
|
{scanQrcodeText.value ? (
|
||||||
<img
|
<img
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user