feat: 支持 QQBot 扫码容器信息展示
This commit is contained in:
parent
fa32ba9ca5
commit
cd49c8e5da
@ -45,10 +45,12 @@
|
|||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "catalog:",
|
"@vueuse/core": "catalog:",
|
||||||
|
"@vueuse/integrations": "catalog:",
|
||||||
"antdv-next": "catalog:",
|
"antdv-next": "catalog:",
|
||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"json-bigint": "catalog:",
|
"json-bigint": "catalog:",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
|
"qrcode": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-router": "catalog:"
|
"vue-router": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,20 @@ export namespace QqbotApi {
|
|||||||
selfId: string;
|
selfId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountScanResult {
|
||||||
|
accountId?: string;
|
||||||
|
containerId?: string;
|
||||||
|
containerName?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
mode: 'create' | 'refresh';
|
||||||
|
qrcode?: string;
|
||||||
|
selfId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
status: 'error' | 'expired' | 'pending' | 'success';
|
||||||
|
webuiPort?: null | number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Rule {
|
export interface Rule {
|
||||||
cooldownMs: number;
|
cooldownMs: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -174,6 +188,37 @@ export function kickQqbotAccount(selfId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function startQqbotAccountScanCreate() {
|
||||||
|
return requestClient.post<QqbotApi.AccountScanResult>(
|
||||||
|
'/qqbot/account/scan/create',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startQqbotAccountScanRefresh(id: string) {
|
||||||
|
return requestClient.post<QqbotApi.AccountScanResult>(
|
||||||
|
`/qqbot/account/scan/refresh?id=${id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQqbotAccountScanStatus(sessionId: string) {
|
||||||
|
return requestClient.get<QqbotApi.AccountScanResult>(
|
||||||
|
'/qqbot/account/scan/status',
|
||||||
|
{ params: { sessionId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshQqbotAccountScanQrcode(sessionId: string) {
|
||||||
|
return requestClient.post<QqbotApi.AccountScanResult>(
|
||||||
|
`/qqbot/account/scan/qrcode/refresh?sessionId=${sessionId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelQqbotAccountScan(sessionId: string) {
|
||||||
|
return requestClient.post<boolean>(
|
||||||
|
`/qqbot/account/scan/cancel?sessionId=${sessionId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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',
|
||||||
|
|||||||
@ -7,26 +7,46 @@ import type {
|
|||||||
KtTableRowAction,
|
KtTableRowAction,
|
||||||
} from '#/components/ktTable';
|
} from '#/components/ktTable';
|
||||||
|
|
||||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
import { computed, defineComponent, onBeforeUnmount, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { Plus } from '@vben/icons';
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
import { Form, FormItem, Input, message, Modal, Switch, Tag } from 'antdv-next';
|
import { useQRCode } from '@vueuse/integrations/useQRCode';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
cancelQqbotAccountScan,
|
||||||
createQqbotAccount,
|
createQqbotAccount,
|
||||||
deleteQqbotAccount,
|
deleteQqbotAccount,
|
||||||
getQqbotAccountList,
|
getQqbotAccountList,
|
||||||
|
getQqbotAccountScanStatus,
|
||||||
kickQqbotAccount,
|
kickQqbotAccount,
|
||||||
|
refreshQqbotAccountScanQrcode,
|
||||||
|
startQqbotAccountScanCreate,
|
||||||
|
startQqbotAccountScanRefresh,
|
||||||
updateQqbotAccount,
|
updateQqbotAccount,
|
||||||
} from '#/api/qqbot';
|
} from '#/api/qqbot';
|
||||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||||
|
|
||||||
const AKtTable = KtTable as any;
|
const AKtTable = KtTable as any;
|
||||||
|
const AButton = Button as any;
|
||||||
const AInput = Input as any;
|
const AInput = Input as any;
|
||||||
const AModal = Modal as any;
|
const AModal = Modal as any;
|
||||||
const ASwitch = Switch as any;
|
const ASwitch = Switch as any;
|
||||||
|
const ATypographyLink = Typography.Link as any;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'QqBotAccountList',
|
name: 'QqBotAccountList',
|
||||||
@ -34,6 +54,29 @@ export default defineComponent({
|
|||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const modalOpen = ref(false);
|
const modalOpen = ref(false);
|
||||||
const editingId = ref<string>();
|
const editingId = ref<string>();
|
||||||
|
const scanLoading = ref(false);
|
||||||
|
const scanModalOpen = ref(false);
|
||||||
|
const scanQrcodeText = ref('');
|
||||||
|
const scanState = reactive<{
|
||||||
|
containerId?: string;
|
||||||
|
containerName?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
mode: 'create' | 'refresh';
|
||||||
|
selfId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
status: 'error' | 'expired' | 'idle' | 'pending' | 'success';
|
||||||
|
webuiPort?: null | number;
|
||||||
|
}>({
|
||||||
|
mode: 'create',
|
||||||
|
status: 'idle',
|
||||||
|
});
|
||||||
|
const scanQrcode = useQRCode(scanQrcodeText, {
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
margin: 2,
|
||||||
|
scale: 8,
|
||||||
|
});
|
||||||
|
let scanTimer: number | undefined;
|
||||||
const form = reactive<QqbotApi.AccountBody>({
|
const form = reactive<QqbotApi.AccountBody>({
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
connectionMode: 'reverse-ws',
|
connectionMode: 'reverse-ws',
|
||||||
@ -78,14 +121,26 @@ export default defineComponent({
|
|||||||
const buttons: Array<KtTableButton<QqbotApi.Account>> = [
|
const buttons: Array<KtTableButton<QqbotApi.Account>> = [
|
||||||
{
|
{
|
||||||
icon: <Plus class="kt-table__button-icon" />,
|
icon: <Plus class="kt-table__button-icon" />,
|
||||||
key: 'create',
|
key: 'scanCreate',
|
||||||
label: '新建账号',
|
label: '扫码新增账号',
|
||||||
onClick: openCreate,
|
onClick: openScanCreate,
|
||||||
permissionCodes: ['QqBot:Account:Create'],
|
permissionCodes: ['QqBot:Account:Create'],
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'manualCreate',
|
||||||
|
label: '手动维护',
|
||||||
|
onClick: openCreate,
|
||||||
|
permissionCodes: ['QqBot:Account:Create'],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const rowActions: Array<KtTableRowAction<QqbotApi.Account>> = [
|
const rowActions: Array<KtTableRowAction<QqbotApi.Account>> = [
|
||||||
|
{
|
||||||
|
key: 'refreshLogin',
|
||||||
|
label: '更新登录',
|
||||||
|
onClick: openScanRefresh,
|
||||||
|
permissionCodes: ['QqBot:Account:RefreshLogin'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
disabled: (row) => row.connectStatus !== 'online',
|
disabled: (row) => row.connectStatus !== 'online',
|
||||||
key: 'kick',
|
key: 'kick',
|
||||||
@ -154,6 +209,162 @@ export default defineComponent({
|
|||||||
const modalTitle = computed(() =>
|
const modalTitle = computed(() =>
|
||||||
editingId.value ? '编辑账号' : '新建账号',
|
editingId.value ? '编辑账号' : '新建账号',
|
||||||
);
|
);
|
||||||
|
const scanTitle = computed(() =>
|
||||||
|
scanState.mode === 'refresh' ? '更新账号登录' : '扫码新增账号',
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopScanPolling();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openScanCreate() {
|
||||||
|
await startScan('create');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openScanRefresh(row: QqbotApi.Account) {
|
||||||
|
await startScan('refresh', row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startScan(
|
||||||
|
mode: 'create' | 'refresh',
|
||||||
|
row?: QqbotApi.Account,
|
||||||
|
) {
|
||||||
|
resetScanState(mode);
|
||||||
|
scanModalOpen.value = true;
|
||||||
|
scanLoading.value = true;
|
||||||
|
try {
|
||||||
|
if (mode === 'create') {
|
||||||
|
await applyScanResult(await startQqbotAccountScanCreate());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!row) {
|
||||||
|
message.warning('请选择需要更新登录的账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyScanResult(await startQqbotAccountScanRefresh(row.id));
|
||||||
|
} catch (error) {
|
||||||
|
stopScanPolling();
|
||||||
|
scanState.status = 'error';
|
||||||
|
scanState.errorMessage = getErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
scanLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyScanResult(result: QqbotApi.AccountScanResult) {
|
||||||
|
scanState.containerId = result.containerId;
|
||||||
|
scanState.containerName = result.containerName;
|
||||||
|
scanState.errorMessage = result.errorMessage;
|
||||||
|
scanState.expiresAt = result.expiresAt;
|
||||||
|
scanState.mode = result.mode;
|
||||||
|
scanState.selfId = result.selfId;
|
||||||
|
scanState.sessionId = result.sessionId;
|
||||||
|
scanState.status = result.status;
|
||||||
|
scanState.webuiPort = result.webuiPort;
|
||||||
|
scanQrcodeText.value = result.qrcode || '';
|
||||||
|
|
||||||
|
if (result.status === 'pending') {
|
||||||
|
startScanPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopScanPolling();
|
||||||
|
if (result.status === 'success') {
|
||||||
|
message.success(
|
||||||
|
result.selfId ? `账号 ${result.selfId} 登录态已更新` : '账号已更新',
|
||||||
|
);
|
||||||
|
scanModalOpen.value = false;
|
||||||
|
await tableApi.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollScanStatus() {
|
||||||
|
if (!scanState.sessionId || scanLoading.value) return;
|
||||||
|
scanLoading.value = true;
|
||||||
|
try {
|
||||||
|
await applyScanResult(
|
||||||
|
await getQqbotAccountScanStatus(scanState.sessionId),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
scanLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshScanQrcode() {
|
||||||
|
if (!scanState.sessionId) return;
|
||||||
|
scanLoading.value = true;
|
||||||
|
try {
|
||||||
|
await applyScanResult(
|
||||||
|
await refreshQqbotAccountScanQrcode(scanState.sessionId),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
scanLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startScanPolling() {
|
||||||
|
if (scanTimer) return;
|
||||||
|
scanTimer = window.setInterval(() => {
|
||||||
|
void pollScanStatus();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopScanPolling() {
|
||||||
|
if (!scanTimer) return;
|
||||||
|
window.clearInterval(scanTimer);
|
||||||
|
scanTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScanState(mode: 'create' | 'refresh') {
|
||||||
|
stopScanPolling();
|
||||||
|
Object.assign(scanState, {
|
||||||
|
containerId: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
errorMessage: undefined,
|
||||||
|
expiresAt: undefined,
|
||||||
|
mode,
|
||||||
|
selfId: undefined,
|
||||||
|
sessionId: undefined,
|
||||||
|
status: 'idle',
|
||||||
|
webuiPort: undefined,
|
||||||
|
});
|
||||||
|
scanQrcodeText.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeScanModal() {
|
||||||
|
const sessionId = scanState.sessionId;
|
||||||
|
stopScanPolling();
|
||||||
|
scanModalOpen.value = false;
|
||||||
|
if (sessionId && scanState.status === 'pending') {
|
||||||
|
void cancelQqbotAccountScan(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScanAlertType() {
|
||||||
|
if (scanState.status === 'success') return 'success';
|
||||||
|
if (scanState.status === 'error') return 'error';
|
||||||
|
if (scanState.status === 'expired') return 'warning';
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScanMessage() {
|
||||||
|
if (scanState.status === 'success') return '扫码登录成功';
|
||||||
|
if (scanState.status === 'error') {
|
||||||
|
return scanState.errorMessage || '扫码登录失败';
|
||||||
|
}
|
||||||
|
if (scanState.status === 'expired') return '二维码已过期,请刷新二维码';
|
||||||
|
if (scanState.errorMessage) return scanState.errorMessage;
|
||||||
|
return '请使用目标 QQ 扫码登录,页面会自动轮询登录结果';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown) {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
if (typeof error === 'string') return error;
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const record = error as Record<string, unknown>;
|
||||||
|
return `${record.msg || record.message || record.err || '扫码登录请求失败'}`;
|
||||||
|
}
|
||||||
|
return '扫码登录请求失败';
|
||||||
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
editingId.value = undefined;
|
editingId.value = undefined;
|
||||||
@ -229,6 +440,97 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<AModal
|
||||||
|
destroyOnClose
|
||||||
|
footer={[
|
||||||
|
<AButton key="close" onClick={closeScanModal}>
|
||||||
|
关闭
|
||||||
|
</AButton>,
|
||||||
|
<AButton
|
||||||
|
disabled={!scanState.sessionId}
|
||||||
|
key="refresh"
|
||||||
|
loading={scanLoading.value}
|
||||||
|
onClick={refreshScanQrcode}
|
||||||
|
>
|
||||||
|
刷新二维码
|
||||||
|
</AButton>,
|
||||||
|
<AButton
|
||||||
|
disabled={!scanState.sessionId}
|
||||||
|
key="check"
|
||||||
|
loading={scanLoading.value}
|
||||||
|
onClick={pollScanStatus}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
检查状态
|
||||||
|
</AButton>,
|
||||||
|
]}
|
||||||
|
onCancel={closeScanModal}
|
||||||
|
{...{
|
||||||
|
'onUpdate:open': (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
scanModalOpen.value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeScanModal();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
open={scanModalOpen.value}
|
||||||
|
title={scanTitle.value}
|
||||||
|
width="520px"
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
message={getScanMessage()}
|
||||||
|
showIcon
|
||||||
|
type={getScanAlertType() as any}
|
||||||
|
/>
|
||||||
|
{scanState.containerName ? (
|
||||||
|
<Alert
|
||||||
|
message={`NapCat 容器:${scanState.containerName}${
|
||||||
|
scanState.webuiPort
|
||||||
|
? `,WebUI 端口:${scanState.webuiPort}`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
{scanQrcodeText.value ? (
|
||||||
|
<img
|
||||||
|
alt="qqbot-login-qrcode"
|
||||||
|
src={scanQrcode.value}
|
||||||
|
style={{
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '240px',
|
||||||
|
padding: '12px',
|
||||||
|
width: '240px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
alignItems: 'center',
|
||||||
|
border: '1px dashed var(--border-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
height: '240px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '240px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
二维码生成中
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{scanQrcodeText.value ? (
|
||||||
|
<ATypographyLink href={scanQrcodeText.value} target="_blank">
|
||||||
|
打开扫码链接
|
||||||
|
</ATypographyLink>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</AModal>
|
||||||
<AModal
|
<AModal
|
||||||
confirmLoading={saving.value}
|
confirmLoading={saving.value}
|
||||||
onOk={submitAccount}
|
onOk={submitAccount}
|
||||||
|
|||||||
@ -587,6 +587,9 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 14.2.0(vue@3.5.27(typescript@5.9.3))
|
version: 14.2.0(vue@3.5.27(typescript@5.9.3))
|
||||||
|
'@vueuse/integrations':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 14.2.0(async-validator@4.2.5)(axios@1.13.5)(change-case@5.4.4)(focus-trap@7.8.0)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.6)(vue@3.5.27(typescript@5.9.3))
|
||||||
antdv-next:
|
antdv-next:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.0.2(date-fns@4.1.0)(vue@3.5.27(typescript@5.9.3))
|
version: 1.0.2(date-fns@4.1.0)(vue@3.5.27(typescript@5.9.3))
|
||||||
@ -599,6 +602,9 @@ importers:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||||
|
qrcode:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.5.4
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.27
|
specifier: ^3.5.27
|
||||||
version: 3.5.27(typescript@5.9.3)
|
version: 3.5.27(typescript@5.9.3)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user