diff --git a/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss b/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss new file mode 100644 index 0000000..118ebec --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss @@ -0,0 +1,107 @@ +.qqbot-napcat-webui { + display: flex; + flex-direction: column; + gap: 12px; + height: var(--vben-content-height, 100%); + min-height: 0; + overflow: hidden; + color: hsl(var(--foreground)); + background: hsl(var(--background)); + + &__header { + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + min-height: 44px; + padding-bottom: 12px; + border-bottom: 1px solid hsl(var(--border)); + } + + &__identity { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + min-width: 0; + } + + &__back { + display: inline-flex; + gap: 6px; + align-items: center; + padding-inline: 0; + } + + &__back-icon { + width: 16px; + height: 16px; + } + + &__title { + display: inline-flex; + gap: 8px; + align-items: center; + min-width: 0; + font-size: 16px; + font-weight: 600; + } + + &__actions { + flex: 0 0 auto; + } + + &__meta { + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 12px; + min-width: 0; + font-size: 12px; + color: hsl(var(--muted-foreground)); + } + + &__content { + display: flex; + flex: 1 1 0; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + + &__center, + &__message { + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: 12px; + align-items: center; + justify-content: center; + min-height: 0; + } + + &__message { + align-items: stretch; + max-width: 560px; + margin: 0 auto; + } + + &__iframe-shell { + flex: 1 1 0; + min-height: 0; + overflow: hidden; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + } + + &__iframe { + display: block; + width: 100%; + height: 100%; + background: hsl(var(--background)); + border: 0; + } +} diff --git a/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx b/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx new file mode 100644 index 0000000..d4a24f6 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx @@ -0,0 +1,224 @@ +import type { NapcatWebuiGatewaySessionState } from './useNapcatWebuiGatewaySession'; + +import { computed, defineComponent, watch } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; + +import { Page } from '@vben/common-ui'; +import { ArrowLeft } from '@vben/icons'; + +import { Alert, Button, Space, Spin, Tag } from 'antdv-next'; + +import { useNapcatWebuiGatewaySession } from './useNapcatWebuiGatewaySession'; + +import './index.scss'; + +const AAlert = Alert as any; +const AButton = Button as any; +const ASpace = Space as any; +const ASpin = Spin as any; +const ATag = Tag as any; + +export default defineComponent({ + name: 'QqBotAccountNapcatWebui', + /** + * Wires route account identity to the page-owned NapCat WebUI session. + */ + setup() { + const route = useRoute(); + const router = useRouter(); + const routeAccountId = computed(() => + normalizeRouteParam(route.params.accountId), + ); + const session = useNapcatWebuiGatewaySession(routeAccountId); + const accountTitle = computed(() => { + const account = session.account.value; + if (!account) return 'NapCat WebUI'; + if (account.name) return `${account.name}(${account.selfId})`; + return account.selfId; + }); + const expiresAtText = computed(() => + formatGatewayExpiresAt(session.expiresAt.value), + ); + const statusMeta = computed(() => getStatusMeta(session.state.value)); + + watch( + routeAccountId, + () => { + void session.open(); + }, + { immediate: true }, + ); + + /** + * Navigates back to the account list route. + */ + function goBack() { + void router.push({ name: 'QqBotAccount' }); + } + + /** + * Reopens the gateway session for the current route account. + */ + function reopen() { + void session.open(); + } + + /** + * Closes the current gateway session while staying on this page. + */ + function closeSession() { + void session.revoke(); + } + + /** + * Renders the main state area for loading, error, revoked, and ready states. + * + * @returns TSX content for the current gateway state. + */ + const renderBody = () => { + if (session.state.value === 'ready' && session.iframeUrl.value) { + return ( +
+