Compare commits
4 Commits
68e593237e
...
33ebdac389
| Author | SHA1 | Date | |
|---|---|---|---|
| 33ebdac389 | |||
| 15480ca705 | |||
| eceadc36f8 | |||
| 5f270cff5c |
@ -47,7 +47,8 @@ pnpm run build:antdv-next
|
||||
## 业务页面
|
||||
|
||||
- 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。
|
||||
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / qrcode 每步进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API。
|
||||
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / new-device / qrcode 每步中文进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API,新设备验证二维码和腾讯验证码分开展示。
|
||||
- QQBot / 插件平台页保留在线命令能力表,并提供 manifest 校验、本地插件安装、安装记录、运行事件和账号绑定抽屉,接口走 `/qqbot/plugin-platform/*`。
|
||||
|
||||
## 部署说明
|
||||
|
||||
|
||||
80
apps/web-antdv-next/src/api/blog/asset.ts
Normal file
80
apps/web-antdv-next/src/api/blog/asset.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BlogAssetApi {
|
||||
export interface BucketStatus {
|
||||
bucketName: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ListQuery {
|
||||
bucketName?: string;
|
||||
prefix?: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectItem {
|
||||
etag: string;
|
||||
lastModified?: string;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ObjectQuery {
|
||||
bucketName?: string;
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
export interface PresignedUrlQuery extends ObjectQuery {
|
||||
expiry?: number;
|
||||
}
|
||||
|
||||
export interface UploadOptions {
|
||||
bucketName?: string;
|
||||
objectName?: string;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
bucketName: string;
|
||||
etag: string;
|
||||
mimeType: string;
|
||||
objectName: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkAssetBucket(bucketName?: string) {
|
||||
return requestClient.get<BlogAssetApi.BucketStatus>('/minio/check', {
|
||||
params: { bucketName },
|
||||
});
|
||||
}
|
||||
|
||||
export function createAssetBucket(bucketName?: string) {
|
||||
return requestClient.post<string>('/minio/bucket', undefined, {
|
||||
params: { bucketName },
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadBlogAsset(
|
||||
file: Blob | File,
|
||||
options: BlogAssetApi.UploadOptions = {},
|
||||
) {
|
||||
return requestClient.upload<BlogAssetApi.UploadResult>('/minio/upload', {
|
||||
...options,
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAssetList(params: BlogAssetApi.ListQuery = {}) {
|
||||
return requestClient.get<BlogAssetApi.ObjectItem[]>('/minio/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAssetPresignedUrl(params: BlogAssetApi.PresignedUrlQuery) {
|
||||
return requestClient.get<string>('/minio/url', { params });
|
||||
}
|
||||
|
||||
export function removeBlogAsset(params: BlogAssetApi.ObjectQuery) {
|
||||
return requestClient.delete<boolean>('/minio/remove', { params });
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from './asset';
|
||||
export * from './wordpress';
|
||||
|
||||
@ -21,7 +21,7 @@ export namespace WordpressBlogApi {
|
||||
contentMarkdown?: string;
|
||||
date?: string;
|
||||
excerpt?: RenderedField | string;
|
||||
id: number | string;
|
||||
id: string;
|
||||
link?: string;
|
||||
modified?: string;
|
||||
slug?: string;
|
||||
@ -39,7 +39,7 @@ export namespace WordpressBlogApi {
|
||||
contentFormat?: 'html' | 'markdown';
|
||||
cover?: string;
|
||||
excerpt?: string;
|
||||
id?: number | string;
|
||||
id?: string;
|
||||
slug?: string;
|
||||
status?: string;
|
||||
sticky?: boolean;
|
||||
@ -119,17 +119,17 @@ export namespace WordpressBlogApi {
|
||||
export interface Term {
|
||||
count?: number;
|
||||
description?: string;
|
||||
id: number | string;
|
||||
id: string;
|
||||
name: string;
|
||||
parent?: number | string;
|
||||
parent?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface TermBody {
|
||||
description?: string;
|
||||
id?: number | string;
|
||||
id?: string;
|
||||
name: string;
|
||||
parent?: number | string;
|
||||
parent?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ export namespace WordpressBlogApi {
|
||||
hide_empty?: boolean;
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
parent?: number | string;
|
||||
parent?: string;
|
||||
search?: string;
|
||||
}
|
||||
}
|
||||
@ -148,7 +148,7 @@ export function getArticleList(params: WordpressBlogApi.ArticleQuery) {
|
||||
>('/blog/article/list', { params });
|
||||
}
|
||||
|
||||
export function getArticleDetail(id: number | string) {
|
||||
export function getArticleDetail(id: string) {
|
||||
return requestClient.get<WordpressBlogApi.Article>('/blog/article/detail', {
|
||||
params: { id },
|
||||
});
|
||||
@ -168,7 +168,7 @@ export function updateArticle(data: WordpressBlogApi.ArticleBody) {
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteArticle(id: number | string) {
|
||||
export function deleteArticle(id: string) {
|
||||
return requestClient.post<WordpressBlogApi.Article>(
|
||||
`/blog/article/remove?id=${id}`,
|
||||
);
|
||||
@ -234,7 +234,7 @@ export function updateCategory(data: WordpressBlogApi.TermBody) {
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCategory(id: number | string, force = true) {
|
||||
export function deleteCategory(id: string, force = true) {
|
||||
return requestClient.post<WordpressBlogApi.Term>(
|
||||
`/blog/category/remove?id=${id}&force=${force}`,
|
||||
);
|
||||
@ -255,7 +255,7 @@ export function updateTag(data: WordpressBlogApi.TermBody) {
|
||||
return requestClient.post<WordpressBlogApi.Term>('/blog/tag/update', data);
|
||||
}
|
||||
|
||||
export function deleteTag(id: number | string, force = true) {
|
||||
export function deleteTag(id: string, force = true) {
|
||||
return requestClient.post<WordpressBlogApi.Term>(
|
||||
`/blog/tag/remove?id=${id}&force=${force}`,
|
||||
);
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { NapcatLoginNewDeviceStatus } from './napcat';
|
||||
|
||||
import { encryptPassword } from '#/api/core/auth';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace QqbotApi {
|
||||
export type PluginTriggerMode = 'command' | 'event';
|
||||
export type OneBotStatus = 'offline' | 'online';
|
||||
export type WebuiStatus = 'offline' | 'online' | 'unknown';
|
||||
export type QqLoginStatus =
|
||||
| 'offline'
|
||||
| 'online'
|
||||
| 'qrcode_expired'
|
||||
| 'qrcode_pending'
|
||||
| 'unknown';
|
||||
|
||||
export interface PageResult<T> {
|
||||
list: T[];
|
||||
@ -37,6 +47,7 @@ export namespace QqbotApi {
|
||||
clientRole?: string;
|
||||
connectStatus: 'offline' | 'online';
|
||||
connectionMode: 'reverse-ws';
|
||||
containerStatus?: AccountNapcatRuntime['containerStatus'];
|
||||
createTime?: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
@ -45,8 +56,12 @@ export namespace QqbotApi {
|
||||
lastHeartbeatAt?: string;
|
||||
name: string;
|
||||
napcat?: AccountNapcatRuntime | null;
|
||||
oneBotStatus?: OneBotStatus;
|
||||
qqLoginMessage?: null | string;
|
||||
qqLoginStatus?: QqLoginStatus;
|
||||
remark?: string;
|
||||
selfId: string;
|
||||
webuiStatus?: WebuiStatus;
|
||||
}
|
||||
|
||||
export interface AccountNapcatRuntime {
|
||||
@ -61,12 +76,7 @@ export namespace QqbotApi {
|
||||
lastStartedAt?: string;
|
||||
oneBotOnline?: boolean;
|
||||
qqLoginMessage?: null | string;
|
||||
qqLoginStatus?:
|
||||
| 'offline'
|
||||
| 'online'
|
||||
| 'qrcode_expired'
|
||||
| 'qrcode_pending'
|
||||
| 'unknown';
|
||||
qqLoginStatus?: QqLoginStatus;
|
||||
webuiOnline?: boolean | null;
|
||||
webuiPort?: null | number;
|
||||
}
|
||||
@ -88,9 +98,12 @@ export namespace QqbotApi {
|
||||
captchaUrl?: string;
|
||||
containerId?: string;
|
||||
containerName?: string;
|
||||
deviceVerifyUrl?: string;
|
||||
errorMessage?: string;
|
||||
expiresAt?: number;
|
||||
mode: 'create' | 'refresh';
|
||||
newDeviceQrcode?: string;
|
||||
newDeviceStatus?: NapcatLoginNewDeviceStatus;
|
||||
qrcode?: string;
|
||||
selfId?: string;
|
||||
sessionId?: string;
|
||||
|
||||
44
apps/web-antdv-next/src/api/qqbot/napcat.spec.ts
Normal file
44
apps/web-antdv-next/src/api/qqbot/napcat.spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getNapcatNewDeviceStatusMessage,
|
||||
NAPCAT_LOGIN_PROGRESS_LABELS,
|
||||
resolveNapcatLoginDisplayQrcode,
|
||||
} from './napcat';
|
||||
|
||||
describe('napcat login display helpers', () => {
|
||||
it('keeps all Chinese progress labels required by the login flow', () => {
|
||||
expect(NAPCAT_LOGIN_PROGRESS_LABELS).toMatchObject({
|
||||
'captcha-submit': '验证码已提交,等待确认',
|
||||
'login-failed': '登录失败',
|
||||
'login-success': '登录成功',
|
||||
'manual-qr-required': '正在生成手动二维码',
|
||||
'new-device-confirming': '新设备确认中',
|
||||
'new-device-qrcode-ready': '新设备二维码待扫码',
|
||||
'new-device-required': '需要新设备验证二维码',
|
||||
'new-device-scanned': '新设备二维码已扫码',
|
||||
'new-device-verified': '新设备验证成功,继续登录',
|
||||
'password-login': '正在密码登录',
|
||||
'password-login-captcha': '需要验证码',
|
||||
'password-login-captcha-submit': '验证码已提交,等待确认',
|
||||
'password-login-failed': '登录失败',
|
||||
'password-login-start': '正在密码登录',
|
||||
'quick-login-fallback': '快速登录失败,进入密码登录',
|
||||
'quick-login-start': '正在快速登录',
|
||||
'runtime-cleanup-failed': '运行态清理失败',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows new-device QR separately before normal login QR or captcha', () => {
|
||||
expect(
|
||||
resolveNapcatLoginDisplayQrcode({
|
||||
captchaUrl: 'https://captcha.qq.com/',
|
||||
newDeviceQrcode: 'data:image/png;base64,new-device',
|
||||
qrcode: 'normal-login-qrcode',
|
||||
}),
|
||||
).toBe('data:image/png;base64,new-device');
|
||||
expect(getNapcatNewDeviceStatusMessage('scanned')).toBe(
|
||||
'新设备二维码已扫码',
|
||||
);
|
||||
});
|
||||
});
|
||||
50
apps/web-antdv-next/src/api/qqbot/napcat.ts
Normal file
50
apps/web-antdv-next/src/api/qqbot/napcat.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export type NapcatLoginNewDeviceStatus =
|
||||
| 'confirming'
|
||||
| 'expired'
|
||||
| 'failed'
|
||||
| 'qr-pending'
|
||||
| 'scanned'
|
||||
| 'verified';
|
||||
|
||||
export type NapcatLoginDisplayQrcodeSource = {
|
||||
captchaUrl?: string;
|
||||
newDeviceQrcode?: string;
|
||||
qrcode?: string;
|
||||
};
|
||||
|
||||
export const NAPCAT_LOGIN_PROGRESS_LABELS = {
|
||||
'captcha-submit': '验证码已提交,等待确认',
|
||||
'login-failed': '登录失败',
|
||||
'login-success': '登录成功',
|
||||
'manual-qr-required': '正在生成手动二维码',
|
||||
'new-device-confirming': '新设备确认中',
|
||||
'new-device-qrcode-ready': '新设备二维码待扫码',
|
||||
'new-device-required': '需要新设备验证二维码',
|
||||
'new-device-scanned': '新设备二维码已扫码',
|
||||
'new-device-verified': '新设备验证成功,继续登录',
|
||||
'password-login': '正在密码登录',
|
||||
'password-login-captcha': '需要验证码',
|
||||
'password-login-captcha-submit': '验证码已提交,等待确认',
|
||||
'password-login-failed': '登录失败',
|
||||
'password-login-start': '正在密码登录',
|
||||
'quick-login-fallback': '快速登录失败,进入密码登录',
|
||||
'quick-login-start': '正在快速登录',
|
||||
'runtime-cleanup-failed': '运行态清理失败',
|
||||
} as const;
|
||||
|
||||
export function resolveNapcatLoginDisplayQrcode(
|
||||
source: NapcatLoginDisplayQrcodeSource,
|
||||
) {
|
||||
return source.newDeviceQrcode || source.qrcode || '';
|
||||
}
|
||||
|
||||
export function getNapcatNewDeviceStatusMessage(
|
||||
status?: NapcatLoginNewDeviceStatus,
|
||||
) {
|
||||
if (status === 'scanned') return '新设备二维码已扫码';
|
||||
if (status === 'confirming') return '新设备确认中';
|
||||
if (status === 'verified') return '新设备验证成功,继续登录';
|
||||
if (status === 'expired') return '新设备二维码已过期';
|
||||
if (status === 'failed') return '新设备验证失败';
|
||||
return '新设备二维码待扫码';
|
||||
}
|
||||
164
apps/web-antdv-next/src/api/qqbot/plugin.ts
Normal file
164
apps/web-antdv-next/src/api/qqbot/plugin.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace QqbotPluginPlatformApi {
|
||||
export type InstallStatus =
|
||||
| 'disabled'
|
||||
| 'enabled'
|
||||
| 'failed'
|
||||
| 'installed'
|
||||
| 'uninstalled'
|
||||
| 'uploaded'
|
||||
| 'validated';
|
||||
|
||||
export type RuntimeStatus =
|
||||
| 'crashed'
|
||||
| 'healthy'
|
||||
| 'starting'
|
||||
| 'stopped'
|
||||
| 'unhealthy';
|
||||
|
||||
export interface ManifestValidationResult {
|
||||
manifest: Recordable<any>;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export interface Installation {
|
||||
createTime?: string;
|
||||
id: string;
|
||||
installedPath?: string;
|
||||
pluginId: string;
|
||||
runtimeStatus: RuntimeStatus;
|
||||
status: InstallStatus;
|
||||
updateTime?: string;
|
||||
versionId: string;
|
||||
}
|
||||
|
||||
export interface RuntimeEvent {
|
||||
createTime?: string;
|
||||
eventType: string;
|
||||
id: string;
|
||||
installationId?: null | string;
|
||||
level: 'error' | 'info' | 'warn';
|
||||
pluginId: string;
|
||||
safeSummary?: Recordable<any>;
|
||||
}
|
||||
|
||||
export interface AccountBinding {
|
||||
accountId: string;
|
||||
createTime?: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface ConfigBody {
|
||||
configKey: string;
|
||||
pluginId: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export interface PackageBody {
|
||||
manifest: Recordable<any>;
|
||||
packageHash?: string;
|
||||
packagePath?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getQqbotPluginPlatformInstallations() {
|
||||
return requestClient.get<QqbotPluginPlatformApi.Installation[]>(
|
||||
'/qqbot/plugin-platform/installations',
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadQqbotPluginPackage(
|
||||
data: QqbotPluginPlatformApi.PackageBody,
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
||||
'/qqbot/plugin-platform/upload',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function validateQqbotPluginManifest(
|
||||
manifest: QqbotPluginPlatformApi.PackageBody['manifest'],
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
||||
'/qqbot/plugin-platform/validate',
|
||||
{ manifest },
|
||||
);
|
||||
}
|
||||
|
||||
export function installQqbotPluginPackage(
|
||||
data: QqbotPluginPlatformApi.PackageBody,
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.Installation>(
|
||||
'/qqbot/plugin-platform/install',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function installLocalQqbotPluginPackage(
|
||||
data: QqbotPluginPlatformApi.PackageBody,
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.Installation>(
|
||||
'/qqbot/plugin-platform/install-local',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function enableQqbotPluginInstallation(id: string) {
|
||||
return requestClient.post<{ id: string; status: string }>(
|
||||
'/qqbot/plugin-platform/enable',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function disableQqbotPluginInstallation(id: string) {
|
||||
return requestClient.post<{ id: string; status: string }>(
|
||||
'/qqbot/plugin-platform/disable',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function upgradeQqbotPluginInstallation(id: string) {
|
||||
return requestClient.post<{ id: string; status: string }>(
|
||||
'/qqbot/plugin-platform/upgrade',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function uninstallQqbotPluginInstallation(id: string) {
|
||||
return requestClient.post<{ id: string; status: string }>(
|
||||
'/qqbot/plugin-platform/uninstall',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function updateQqbotPluginConfig(
|
||||
data: QqbotPluginPlatformApi.ConfigBody,
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.ConfigBody>(
|
||||
'/qqbot/plugin-platform/config',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotPluginRuntimeEvents(pluginId?: string) {
|
||||
return requestClient.get<QqbotPluginPlatformApi.RuntimeEvent[]>(
|
||||
'/qqbot/plugin-platform/runtime-events',
|
||||
{
|
||||
params: { pluginId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotPluginAccountBindings(pluginId?: string) {
|
||||
return requestClient.get<QqbotPluginPlatformApi.AccountBinding[]>(
|
||||
'/qqbot/plugin-platform/account-bindings',
|
||||
{
|
||||
params: { pluginId },
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -61,7 +61,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('#/views/qqbot/plugin/list'),
|
||||
meta: {
|
||||
icon: 'lucide:plug',
|
||||
title: '插件能力',
|
||||
title: '插件平台',
|
||||
},
|
||||
name: 'QqBotPlugin',
|
||||
path: '/qqbot/plugin',
|
||||
|
||||
@ -54,7 +54,7 @@ export default defineComponent({
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const editingId = ref<number | string>();
|
||||
const editingId = ref<string>();
|
||||
const tableRows = ref<WordpressBlogApi.Term[]>([]);
|
||||
const parentOptions = computed(() =>
|
||||
tableRows.value
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export interface BlogArticleFilters {
|
||||
categories?: Array<number | string>;
|
||||
tags?: Array<number | string>;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
let pendingFilters: BlogArticleFilters | null = null;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type { NapcatLoginNewDeviceStatus } from '#/api/qqbot/napcat';
|
||||
import type {
|
||||
KtTableApi,
|
||||
KtTableButton,
|
||||
@ -39,6 +40,10 @@ import {
|
||||
submitQqbotAccountScanCaptcha,
|
||||
updateQqbotAccount,
|
||||
} from '#/api/qqbot';
|
||||
import {
|
||||
getNapcatNewDeviceStatusMessage,
|
||||
resolveNapcatLoginDisplayQrcode,
|
||||
} from '#/api/qqbot/napcat';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
@ -87,9 +92,12 @@ export default defineComponent({
|
||||
captchaUrl?: string;
|
||||
containerId?: string;
|
||||
containerName?: string;
|
||||
deviceVerifyUrl?: string;
|
||||
errorMessage?: string;
|
||||
expiresAt?: number;
|
||||
mode: 'create' | 'refresh';
|
||||
newDeviceQrcode?: string;
|
||||
newDeviceStatus?: NapcatLoginNewDeviceStatus;
|
||||
selfId?: string;
|
||||
sessionId?: string;
|
||||
status: 'error' | 'expired' | 'idle' | 'pending' | 'success';
|
||||
@ -130,6 +138,9 @@ export default defineComponent({
|
||||
Math.max(scanProgressItems.value.length - 1, 0),
|
||||
);
|
||||
const scanQrcodePlaceholderText = computed(() => {
|
||||
if (scanState.newDeviceStatus) {
|
||||
return getNapcatNewDeviceStatusMessage(scanState.newDeviceStatus);
|
||||
}
|
||||
if (scanState.captchaUrl) {
|
||||
return '等待安全验证';
|
||||
}
|
||||
@ -294,7 +305,7 @@ export default defineComponent({
|
||||
permissionCodes: ['QqBot:Account:Delete'],
|
||||
},
|
||||
{
|
||||
disabled: (row) => row.connectStatus !== 'online',
|
||||
disabled: (row) => getOneBotStatus(row) !== 'online',
|
||||
key: 'kick',
|
||||
label: '断开',
|
||||
onClick: async (row, context) => {
|
||||
@ -429,14 +440,17 @@ export default defineComponent({
|
||||
scanState.captchaUrl = result.captchaUrl;
|
||||
scanState.containerId = result.containerId;
|
||||
scanState.containerName = result.containerName;
|
||||
scanState.deviceVerifyUrl = result.deviceVerifyUrl;
|
||||
scanState.errorMessage = result.errorMessage;
|
||||
scanState.expiresAt = result.expiresAt;
|
||||
scanState.mode = result.mode;
|
||||
scanState.newDeviceQrcode = result.newDeviceQrcode;
|
||||
scanState.newDeviceStatus = result.newDeviceStatus;
|
||||
scanState.selfId = result.selfId;
|
||||
scanState.sessionId = result.sessionId;
|
||||
scanState.status = result.status;
|
||||
scanState.webuiPort = result.webuiPort;
|
||||
const nextQrcode = result.qrcode || '';
|
||||
const nextQrcode = resolveNapcatLoginDisplayQrcode(result);
|
||||
const qrcodeChanged = nextQrcode !== scanQrcodeText.value;
|
||||
if (qrcodeChanged) {
|
||||
scanQrcodeImageFailed.value = false;
|
||||
@ -572,7 +586,9 @@ export default defineComponent({
|
||||
}
|
||||
if (event.result) {
|
||||
void applyScanResult(event.result, {
|
||||
reloadQrcode: event.step === 'qrcode-ready',
|
||||
reloadQrcode: ['new-device-qrcode-ready', 'qrcode-ready'].includes(
|
||||
event.step,
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@ -587,9 +603,12 @@ export default defineComponent({
|
||||
captchaUrl: undefined,
|
||||
containerId: undefined,
|
||||
containerName: undefined,
|
||||
deviceVerifyUrl: undefined,
|
||||
errorMessage: undefined,
|
||||
expiresAt: undefined,
|
||||
mode,
|
||||
newDeviceQrcode: undefined,
|
||||
newDeviceStatus: undefined,
|
||||
selfId: undefined,
|
||||
sessionId: undefined,
|
||||
status: 'idle',
|
||||
@ -621,6 +640,12 @@ export default defineComponent({
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function getNewDeviceAlertType(status?: NapcatLoginNewDeviceStatus) {
|
||||
if (status === 'failed') return 'error';
|
||||
if (status === 'verified') return 'success';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
function getScanMessage() {
|
||||
if (scanState.status === 'success') return '扫码登录成功';
|
||||
if (scanState.status === 'error') {
|
||||
@ -696,7 +721,7 @@ export default defineComponent({
|
||||
if (!row.enabled) {
|
||||
return <Tag color="default">已停用</Tag>;
|
||||
}
|
||||
const online = row.napcat?.oneBotOnline ?? row.connectStatus === 'online';
|
||||
const online = getOneBotStatus(row) === 'online';
|
||||
return (
|
||||
<Tag color={online ? 'success' : 'default'}>
|
||||
{online ? 'OneBot 在线' : 'OneBot 离线'}
|
||||
@ -705,14 +730,13 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const renderQqLoginStatus = (row: QqbotApi.Account) => {
|
||||
const meta = getQqLoginStatusMeta(row.napcat);
|
||||
const meta = getQqLoginStatusMeta(row);
|
||||
const message = getQqLoginMessage(row);
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
{row.napcat?.qqLoginMessage ? (
|
||||
<ATypographyText type="secondary">
|
||||
{row.napcat.qqLoginMessage}
|
||||
</ATypographyText>
|
||||
{message ? (
|
||||
<ATypographyText type="secondary">{message}</ATypographyText>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
@ -720,8 +744,8 @@ export default defineComponent({
|
||||
|
||||
const renderNapcatRuntime = (row: QqbotApi.Account) => {
|
||||
const napcat = row.napcat;
|
||||
const meta = getNapcatStatusMeta(napcat);
|
||||
const webuiMeta = getNapcatWebuiMeta(napcat);
|
||||
const meta = getNapcatStatusMeta(row);
|
||||
const webuiMeta = getNapcatWebuiMeta(row);
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Space size={4} wrap>
|
||||
@ -762,13 +786,12 @@ export default defineComponent({
|
||||
);
|
||||
};
|
||||
|
||||
function getNapcatStatusMeta(
|
||||
napcat?: null | QqbotApi.AccountNapcatRuntime,
|
||||
) {
|
||||
if (!napcat) {
|
||||
function getNapcatStatusMeta(row: QqbotApi.Account) {
|
||||
const status = row.containerStatus || row.napcat?.containerStatus;
|
||||
if (!row.napcat && !status) {
|
||||
return { color: 'default', label: '未绑定专属容器' };
|
||||
}
|
||||
if (!napcat.containerStatus) {
|
||||
if (!status) {
|
||||
return { color: 'warning', label: '容器记录缺失' };
|
||||
}
|
||||
const statusMap: Record<
|
||||
@ -780,26 +803,29 @@ export default defineComponent({
|
||||
running: { color: 'success', label: '容器运行中' },
|
||||
stopped: { color: 'default', label: '容器已停止' },
|
||||
};
|
||||
return statusMap[napcat.containerStatus];
|
||||
return statusMap[status];
|
||||
}
|
||||
|
||||
function getNapcatWebuiMeta(napcat?: null | QqbotApi.AccountNapcatRuntime) {
|
||||
if (!napcat) return { color: 'default', label: 'WebUI 未绑定' };
|
||||
if (napcat.webuiOnline === true) {
|
||||
function getNapcatWebuiMeta(row: QqbotApi.Account) {
|
||||
const status = getWebuiStatus(row);
|
||||
if (!row.napcat && status === 'unknown') {
|
||||
return { color: 'default', label: 'WebUI 未绑定' };
|
||||
}
|
||||
if (status === 'online') {
|
||||
return { color: 'success', label: 'WebUI 可用' };
|
||||
}
|
||||
if (napcat.webuiOnline === false) {
|
||||
if (status === 'offline') {
|
||||
return { color: 'error', label: 'WebUI 不可用' };
|
||||
}
|
||||
return { color: 'default', label: 'WebUI 未检查' };
|
||||
}
|
||||
|
||||
function getQqLoginStatusMeta(
|
||||
napcat?: null | QqbotApi.AccountNapcatRuntime,
|
||||
) {
|
||||
if (!napcat) return { color: 'default', label: '未绑定容器' };
|
||||
function getQqLoginStatusMeta(row: QqbotApi.Account) {
|
||||
if (!row.napcat && !row.qqLoginStatus) {
|
||||
return { color: 'default', label: '未绑定容器' };
|
||||
}
|
||||
const statusMap: Record<
|
||||
NonNullable<QqbotApi.AccountNapcatRuntime['qqLoginStatus']>,
|
||||
QqbotApi.QqLoginStatus,
|
||||
{ color: string; label: string }
|
||||
> = {
|
||||
offline: { color: 'error', label: 'QQ 离线' },
|
||||
@ -808,7 +834,30 @@ export default defineComponent({
|
||||
qrcode_pending: { color: 'processing', label: '等待扫码' },
|
||||
unknown: { color: 'default', label: '状态未知' },
|
||||
};
|
||||
return statusMap[napcat.qqLoginStatus || 'unknown'];
|
||||
return statusMap[getQqLoginStatus(row)];
|
||||
}
|
||||
|
||||
function getOneBotStatus(row: QqbotApi.Account): QqbotApi.OneBotStatus {
|
||||
if (row.oneBotStatus) return row.oneBotStatus;
|
||||
if (row.napcat?.oneBotOnline !== undefined) {
|
||||
return row.napcat.oneBotOnline ? 'online' : 'offline';
|
||||
}
|
||||
return row.connectStatus === 'online' ? 'online' : 'offline';
|
||||
}
|
||||
|
||||
function getWebuiStatus(row: QqbotApi.Account): QqbotApi.WebuiStatus {
|
||||
if (row.webuiStatus) return row.webuiStatus;
|
||||
if (row.napcat?.webuiOnline === true) return 'online';
|
||||
if (row.napcat?.webuiOnline === false) return 'offline';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function getQqLoginStatus(row: QqbotApi.Account): QqbotApi.QqLoginStatus {
|
||||
return row.qqLoginStatus || row.napcat?.qqLoginStatus || 'unknown';
|
||||
}
|
||||
|
||||
function getQqLoginMessage(row: QqbotApi.Account) {
|
||||
return row.qqLoginMessage ?? row.napcat?.qqLoginMessage;
|
||||
}
|
||||
|
||||
function getRecentActivity(row: QqbotApi.Account) {
|
||||
@ -836,31 +885,35 @@ export default defineComponent({
|
||||
if (row.lastError) {
|
||||
return { level: 'warning', text: `账号异常:${row.lastError}` };
|
||||
}
|
||||
if (row.napcat?.qqLoginMessage) {
|
||||
const qqLoginMessage = getQqLoginMessage(row);
|
||||
const qqLoginStatus = getQqLoginStatus(row);
|
||||
const containerStatus =
|
||||
row.containerStatus || row.napcat?.containerStatus;
|
||||
if (qqLoginMessage) {
|
||||
return {
|
||||
level: 'warning',
|
||||
text: `QQ 登录:${row.napcat.qqLoginMessage}`,
|
||||
text: `QQ 登录:${qqLoginMessage}`,
|
||||
};
|
||||
}
|
||||
if (row.napcat?.lastError) {
|
||||
return { level: 'warning', text: `NapCat:${row.napcat.lastError}` };
|
||||
}
|
||||
if (row.napcat?.qqLoginStatus === 'qrcode_expired') {
|
||||
if (qqLoginStatus === 'qrcode_expired') {
|
||||
return { level: 'warning', text: '二维码已过期,点击更新登录' };
|
||||
}
|
||||
if (row.connectStatus === 'online') {
|
||||
if (getOneBotStatus(row) === 'online') {
|
||||
return { level: 'normal', text: '消息链路可用' };
|
||||
}
|
||||
if (row.napcat?.qqLoginStatus === 'online') {
|
||||
if (qqLoginStatus === 'online') {
|
||||
return { level: 'warning', text: 'QQ 在线,等待 OneBot 连接' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'running') {
|
||||
if (containerStatus === 'running') {
|
||||
return { level: 'warning', text: 'NapCat 运行中,等待 OneBot 连接' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'creating') {
|
||||
if (containerStatus === 'creating') {
|
||||
return { level: 'warning', text: '容器创建中' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'stopped') {
|
||||
if (containerStatus === 'stopped') {
|
||||
return { level: 'warning', text: '容器已停止' };
|
||||
}
|
||||
if (!row.napcat) {
|
||||
@ -993,7 +1046,11 @@ export default defineComponent({
|
||||
关闭
|
||||
</AButton>,
|
||||
<AButton
|
||||
disabled={!scanState.sessionId}
|
||||
disabled={
|
||||
!scanState.sessionId ||
|
||||
!!scanState.captchaUrl ||
|
||||
!!scanState.newDeviceStatus
|
||||
}
|
||||
key="refresh"
|
||||
loading={scanLoading.value}
|
||||
onClick={refreshScanQrcode}
|
||||
@ -1029,7 +1086,32 @@ export default defineComponent({
|
||||
type="info"
|
||||
/>
|
||||
) : null}
|
||||
{scanState.captchaUrl ? (
|
||||
{scanState.newDeviceStatus ? (
|
||||
<Alert
|
||||
description={
|
||||
<Space direction="vertical">
|
||||
<ATypographyText>
|
||||
请使用手机 QQ
|
||||
扫描下方新设备验证二维码,并在手机端确认登录。
|
||||
</ATypographyText>
|
||||
{scanState.deviceVerifyUrl ? (
|
||||
<ATypographyLink
|
||||
href={scanState.deviceVerifyUrl}
|
||||
target="_blank"
|
||||
>
|
||||
打开新设备验证链接
|
||||
</ATypographyLink>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
message={getNapcatNewDeviceStatusMessage(
|
||||
scanState.newDeviceStatus,
|
||||
)}
|
||||
showIcon
|
||||
type={getNewDeviceAlertType(scanState.newDeviceStatus)}
|
||||
/>
|
||||
) : null}
|
||||
{scanState.captchaUrl && !scanState.newDeviceStatus ? (
|
||||
<Alert
|
||||
description={
|
||||
<Space>
|
||||
|
||||
@ -1,23 +1,39 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type { QqbotPluginPlatformApi } from '#/api/qqbot/plugin';
|
||||
import type { KtTableApi, KtTableButton } from '#/components/ktTable';
|
||||
import type { DictOption } from '#/hooks/useDict';
|
||||
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message, Tag } from 'antdv-next';
|
||||
import { Button, Drawer, message, Modal, Space, Tag } from 'antdv-next';
|
||||
|
||||
import {
|
||||
getQqbotPluginHealth,
|
||||
getQqbotPluginList,
|
||||
getQqbotPluginOperationList,
|
||||
} from '#/api/qqbot';
|
||||
import {
|
||||
disableQqbotPluginInstallation,
|
||||
enableQqbotPluginInstallation,
|
||||
getQqbotPluginAccountBindings,
|
||||
getQqbotPluginPlatformInstallations,
|
||||
getQqbotPluginRuntimeEvents,
|
||||
installLocalQqbotPluginPackage,
|
||||
uninstallQqbotPluginInstallation,
|
||||
uploadQqbotPluginPackage,
|
||||
validateQqbotPluginManifest,
|
||||
} from '#/api/qqbot/plugin';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
import { useDict } from '#/hooks/useDict';
|
||||
|
||||
const AButton = Button as any;
|
||||
const ADrawer = Drawer as any;
|
||||
const AModal = Modal as any;
|
||||
const ASpace = Space as any;
|
||||
const AKtTable = KtTable as any;
|
||||
const QQBOT_PLUGIN_TRIGGER_MODE_DICT = 'QQBOT_PLUGIN_TRIGGER_MODE';
|
||||
const qqbotPluginTriggerModeFallback: Array<
|
||||
@ -26,12 +42,59 @@ const qqbotPluginTriggerModeFallback: Array<
|
||||
{ label: '命令', value: 'command' },
|
||||
{ label: '事件', value: 'event' },
|
||||
];
|
||||
const defaultManifest = {
|
||||
assets: [],
|
||||
configSchema: { type: 'object' },
|
||||
entry: 'src/index.ts',
|
||||
events: [],
|
||||
minApiSdkVersion: '1.0.0',
|
||||
name: 'Demo Plugin',
|
||||
operations: [
|
||||
{
|
||||
handlerName: 'echo',
|
||||
key: 'demo-plugin.echo',
|
||||
name: 'Echo',
|
||||
permissions: ['qqbot.send'],
|
||||
timeoutMs: 3000,
|
||||
},
|
||||
],
|
||||
permissions: ['qqbot.send'],
|
||||
pluginKey: 'demo-plugin',
|
||||
runtime: {
|
||||
maxConcurrency: 1,
|
||||
memoryMb: 128,
|
||||
timeoutMs: 5000,
|
||||
workerType: 'node-worker',
|
||||
},
|
||||
version: '0.1.0',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotPluginList',
|
||||
setup() {
|
||||
const accountBindings = ref<QqbotPluginPlatformApi.AccountBinding[]>([]);
|
||||
const drawerMode = ref<'bindings' | 'events' | 'installations'>(
|
||||
'installations',
|
||||
);
|
||||
const drawerOpen = ref(false);
|
||||
const installations = ref<QqbotPluginPlatformApi.Installation[]>([]);
|
||||
const manifestMode = ref<'install' | 'upload' | 'validate'>('validate');
|
||||
const manifestModalOpen = ref(false);
|
||||
const manifestText = ref(JSON.stringify(defaultManifest, null, 2));
|
||||
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
|
||||
const platformLoading = ref(false);
|
||||
const runtimeEvents = ref<QqbotPluginPlatformApi.RuntimeEvent[]>([]);
|
||||
const drawerTitle = computed(() => {
|
||||
if (drawerMode.value === 'events') return '插件运行事件';
|
||||
if (drawerMode.value === 'bindings') return '插件账号绑定';
|
||||
return '插件安装记录';
|
||||
});
|
||||
const manifestModalTitle = computed(() => {
|
||||
if (manifestMode.value === 'install') return '安装插件 Manifest';
|
||||
if (manifestMode.value === 'upload') return '上传插件 Manifest';
|
||||
return '校验插件 Manifest';
|
||||
});
|
||||
const {
|
||||
labelOf: getTriggerModeLabel,
|
||||
options: triggerModeOptions,
|
||||
@ -69,6 +132,36 @@ export default defineComponent({
|
||||
await getQqbotPluginOperationList(params.pluginKey, params.triggerMode),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.PluginOperation>> = [
|
||||
{
|
||||
key: 'manifestValidate',
|
||||
label: '校验 Manifest',
|
||||
onClick: () => openManifestModal('validate'),
|
||||
},
|
||||
{
|
||||
key: 'manifestUpload',
|
||||
label: '上传插件',
|
||||
onClick: () => openManifestModal('upload'),
|
||||
},
|
||||
{
|
||||
key: 'manifestInstall',
|
||||
label: '本地安装',
|
||||
onClick: () => openManifestModal('install'),
|
||||
},
|
||||
{
|
||||
key: 'installations',
|
||||
label: '安装记录',
|
||||
onClick: () => void loadInstallations(),
|
||||
},
|
||||
{
|
||||
key: 'runtimeEvents',
|
||||
label: '运行事件',
|
||||
onClick: () => void loadRuntimeEvents(),
|
||||
},
|
||||
{
|
||||
key: 'accountBindings',
|
||||
label: '账号绑定',
|
||||
onClick: () => void loadAccountBindings(),
|
||||
},
|
||||
{
|
||||
key: 'health',
|
||||
label: '健康检查',
|
||||
@ -134,6 +227,194 @@ export default defineComponent({
|
||||
}));
|
||||
}
|
||||
|
||||
function openManifestModal(mode: typeof manifestMode.value) {
|
||||
manifestMode.value = mode;
|
||||
manifestText.value = JSON.stringify(defaultManifest, null, 2);
|
||||
manifestModalOpen.value = true;
|
||||
}
|
||||
|
||||
function parseManifestText() {
|
||||
try {
|
||||
return JSON.parse(manifestText.value);
|
||||
} catch {
|
||||
message.error('Manifest JSON 格式不正确');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitManifest() {
|
||||
const manifest = parseManifestText();
|
||||
if (!manifest) return;
|
||||
|
||||
platformLoading.value = true;
|
||||
try {
|
||||
if (manifestMode.value === 'upload') {
|
||||
await uploadQqbotPluginPackage({ manifest });
|
||||
message.success('插件包上传校验通过');
|
||||
} else if (manifestMode.value === 'install') {
|
||||
await installLocalQqbotPluginPackage({ manifest });
|
||||
message.success('插件已安装');
|
||||
await loadInstallations(false);
|
||||
} else {
|
||||
await validateQqbotPluginManifest(manifest);
|
||||
message.success('Manifest 校验通过');
|
||||
}
|
||||
manifestModalOpen.value = false;
|
||||
} finally {
|
||||
platformLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInstallations(openDrawer = true) {
|
||||
installations.value = await getQqbotPluginPlatformInstallations();
|
||||
drawerMode.value = 'installations';
|
||||
drawerOpen.value = openDrawer || drawerOpen.value;
|
||||
}
|
||||
|
||||
async function loadRuntimeEvents() {
|
||||
runtimeEvents.value = await getQqbotPluginRuntimeEvents();
|
||||
drawerMode.value = 'events';
|
||||
drawerOpen.value = true;
|
||||
}
|
||||
|
||||
async function loadAccountBindings() {
|
||||
accountBindings.value = await getQqbotPluginAccountBindings();
|
||||
drawerMode.value = 'bindings';
|
||||
drawerOpen.value = true;
|
||||
}
|
||||
|
||||
async function updateInstallationStatus(
|
||||
row: QqbotPluginPlatformApi.Installation,
|
||||
action: 'disable' | 'enable' | 'uninstall',
|
||||
) {
|
||||
if (action === 'enable') {
|
||||
await enableQqbotPluginInstallation(row.id);
|
||||
message.success('插件已启用');
|
||||
} else if (action === 'disable') {
|
||||
await disableQqbotPluginInstallation(row.id);
|
||||
message.success('插件已禁用');
|
||||
} else {
|
||||
await uninstallQqbotPluginInstallation(row.id);
|
||||
message.success('插件已卸载');
|
||||
}
|
||||
await loadInstallations(false);
|
||||
}
|
||||
|
||||
const renderStatusTag = (status?: string) => {
|
||||
let color = 'processing';
|
||||
switch (status) {
|
||||
case 'disabled': {
|
||||
color = 'warning';
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
color = 'success';
|
||||
break;
|
||||
}
|
||||
case 'failed':
|
||||
case 'uninstalled': {
|
||||
color = 'error';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return <Tag color={color}>{status || '-'}</Tag>;
|
||||
};
|
||||
|
||||
const renderDrawerContent = () => {
|
||||
if (drawerMode.value === 'events') {
|
||||
return runtimeEvents.value.length > 0 ? (
|
||||
<div class="space-y-3">
|
||||
{runtimeEvents.value.map((item) => (
|
||||
<div
|
||||
class="border-b border-solid border-gray-100 pb-3"
|
||||
key={item.id}
|
||||
>
|
||||
<div>
|
||||
<Tag color={item.level === 'error' ? 'error' : 'processing'}>
|
||||
{item.level}
|
||||
</Tag>
|
||||
<span>{item.eventType}</span>
|
||||
</div>
|
||||
<pre class="mt-2 whitespace-pre-wrap text-xs">
|
||||
{JSON.stringify(item.safeSummary || {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>暂无运行事件</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (drawerMode.value === 'bindings') {
|
||||
return accountBindings.value.length > 0 ? (
|
||||
<div class="space-y-3">
|
||||
{accountBindings.value.map((item) => (
|
||||
<div
|
||||
class="border-b border-solid border-gray-100 pb-3"
|
||||
key={item.id}
|
||||
>
|
||||
<Tag color={item.enabled ? 'success' : 'default'}>
|
||||
{item.enabled ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
<span>
|
||||
插件 {item.pluginId} / 账号 {item.accountId}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>暂无账号绑定</span>
|
||||
);
|
||||
}
|
||||
|
||||
return installations.value.length > 0 ? (
|
||||
<div class="space-y-3">
|
||||
{installations.value.map((item) => (
|
||||
<div
|
||||
class="border-b border-solid border-gray-100 pb-3"
|
||||
key={item.id}
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
{renderStatusTag(item.status)}
|
||||
<Tag>{item.runtimeStatus || '-'}</Tag>
|
||||
<span>
|
||||
插件 {item.pluginId} / 版本 {item.versionId}
|
||||
</span>
|
||||
</div>
|
||||
<ASpace>
|
||||
<AButton
|
||||
disabled={item.status === 'enabled'}
|
||||
onClick={() => void updateInstallationStatus(item, 'enable')}
|
||||
size="small"
|
||||
>
|
||||
启用
|
||||
</AButton>
|
||||
<AButton
|
||||
disabled={item.status === 'disabled'}
|
||||
onClick={() => void updateInstallationStatus(item, 'disable')}
|
||||
size="small"
|
||||
>
|
||||
禁用
|
||||
</AButton>
|
||||
<AButton
|
||||
danger
|
||||
onClick={() =>
|
||||
void updateInstallationStatus(item, 'uninstall')
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
卸载
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>暂无安装记录</span>
|
||||
);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
@ -165,6 +446,35 @@ export default defineComponent({
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={platformLoading.value}
|
||||
onCancel={() => {
|
||||
manifestModalOpen.value = false;
|
||||
}}
|
||||
onOk={() => void submitManifest()}
|
||||
open={manifestModalOpen.value}
|
||||
title={manifestModalTitle.value}
|
||||
width={760}
|
||||
>
|
||||
<textarea
|
||||
class="w-full resize-y rounded border border-solid border-gray-200 p-3 font-mono text-sm outline-none"
|
||||
onInput={(event) => {
|
||||
manifestText.value = (event.target as HTMLTextAreaElement).value;
|
||||
}}
|
||||
rows={18}
|
||||
value={manifestText.value}
|
||||
/>
|
||||
</AModal>
|
||||
<ADrawer
|
||||
onClose={() => {
|
||||
drawerOpen.value = false;
|
||||
}}
|
||||
open={drawerOpen.value}
|
||||
title={drawerTitle.value}
|
||||
width={760}
|
||||
>
|
||||
{renderDrawerContent()}
|
||||
</ADrawer>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user