import type { TableColumnType } from 'antdv-next'; import type { PropType, VNodeChild } from 'vue'; import type { AccountMessagePushModalExposed } from './AccountMessagePushModal'; import type { QqbotMessagePushApi } from '#/api/qqbot/message-push'; import type { KtTableApi, KtTableButton, KtTableContext, KtTableRowAction, } from '#/components/ktTable'; import { defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { useAccess } from '@vben/access'; import { Plus } from '@vben/icons'; import { Space, Tag } from 'antdv-next'; import { deleteAccountMessagePushBinding, getAccountMessagePushBindings, getAccountMessagePushTargets, getMessageSubscriptionList, getMessageTemplateList, setAccountMessagePushBindingEnabled, } from '#/api/qqbot/message-push'; import { KtTable, useKtTable } from '#/components/ktTable'; import AccountMessagePushModal from './AccountMessagePushModal'; const AKtTable = KtTable as any; export interface AccountMessagePushPanelProps { headerControls: () => VNodeChild; selfId: string; title: () => VNodeChild; } const PERMISSIONS = { create: 'QqBot:Account:MessagePush:Create', delete: 'QqBot:Account:MessagePush:Delete', list: 'QqBot:Account:MessagePush:List', toggle: 'QqBot:Account:MessagePush:Toggle', update: 'QqBot:Account:MessagePush:Update', } as const; export default defineComponent({ name: 'AccountMessagePushPanel', props: { headerControls: { required: true, type: Function as PropType<() => VNodeChild>, }, selfId: { required: true, type: String, }, title: { required: true, type: Function as PropType<() => VNodeChild>, }, }, /** Owns one permission-gated account binding table and per-account option cache. */ setup(props) { const { hasAccessByCodes } = useAccess(); const canList = hasAccessByCodes([PERMISSIONS.list]); const canLoadTargets = hasAccessByCodes([PERMISSIONS.create]) || hasAccessByCodes([PERMISSIONS.update]); const modalRef = ref(); const subscriptions = ref( [], ); const templates = ref([]); const targetOptions = ref(); const targetOptionsLoading = ref(false); let loadRevision = 0; let latestBindingPage: { items: QqbotMessagePushApi.QqbotMessagePublishBindingView[]; total: number; } = { items: [], total: 0 }; const columns: Array< TableColumnType > = [ { dataIndex: 'subscriptionName', key: 'subscription', title: '消息订阅', width: 210, }, { key: 'source', title: '消息源', width: 260, }, { dataIndex: 'templateName', key: 'template', title: '消息模板', width: 200, }, { key: 'targets', title: '推送目标', width: 300, }, { dataIndex: 'enabled', key: 'enabled', title: '状态', width: 90, }, { dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180, }, ]; const api: KtTableApi = { /** * Adapts the account binding array while returning the latest page for stale requests. * @returns Strict KtTable page retaining every string identifier. */ list: async () => { const revision = loadRevision; const selfId = props.selfId; const bindings = await getAccountMessagePushBindings(selfId); if (revision !== loadRevision || selfId !== props.selfId) { return latestBindingPage; } latestBindingPage = { items: bindings, total: bindings.length, }; return latestBindingPage; }, }; const buttons: Array< KtTableButton > = [ { icon: , key: 'create', label: '新增推送', onClick: openCreate, permissionCodes: [PERMISSIONS.create], type: 'primary', }, { key: 'refresh', label: '刷新', operation: 'reload', permissionCodes: [PERMISSIONS.list], }, ]; const rowActions: Array< KtTableRowAction > = [ { key: 'edit', label: '编辑', onClick: openEdit, permissionCodes: [PERMISSIONS.update], }, { key: 'toggle', label: '启停', onClick: handleToggle, permissionCodes: [PERMISSIONS.toggle], }, { confirm: getDeleteConfirm, danger: true, key: 'delete', label: '解绑', onClick: handleDelete, permissionCodes: [PERMISSIONS.delete], }, ]; const [registerTable, tableApi] = useKtTable({ api, buttons, columns, immediate: false, rowActions, rowActionVisibleCount: 3, rowKey: 'id', showDefaultButtons: false, showFooter: false, showIndex: false, showPagination: false, showTableSetting: false, size: 'small', }); /** Opens a blank binding session for this panel's implicit account. */ function openCreate() { modalRef.value?.openCreate(); } /** * Opens one existing binding without deriving another account identity. * @param row - Binding selected from this account's KtTable. */ function openEdit(row: QqbotMessagePushApi.QqbotMessagePublishBindingView) { modalRef.value?.openEdit(row); } /** * Builds account-binding removal confirmation text. * @param row - Binding awaiting removal. * @returns Confirmation containing the global subscription name. */ function getDeleteConfirm( row: QqbotMessagePushApi.QqbotMessagePublishBindingView, ): string { return `确认解绑消息订阅「${row.subscriptionName}」吗?`; } /** * Toggles one binding and reloads only the mutable binding list after success. * @param row - Binding whose enabled state is inverted. * @param context - Registered KtTable action context. */ async function handleToggle( row: QqbotMessagePushApi.QqbotMessagePublishBindingView, context: KtTableContext, ) { await setAccountMessagePushBindingEnabled( props.selfId, row.id, !row.enabled, ); await context.reload(); } /** * Removes one binding and reloads only the mutable binding list after success. * @param row - Binding confirmed for account-scoped removal. * @param context - Registered KtTable action context. */ async function handleDelete( row: QqbotMessagePushApi.QqbotMessagePublishBindingView, context: KtTableContext, ) { await deleteAccountMessagePushBinding(props.selfId, row.id); await context.reload(); } /** Reloads only bindings after one successful modal persistence. */ async function handleModalSaved() { await tableApi.reload(); } /** * Loads the complete subscription/template collection in bounded 100-row pages. * @param loader - One strict page caller receiving pageNo/pageSize. * @returns Concatenated server rows until total is reached or progress stops. */ async function loadAllPages( loader: (params: { pageNo: number; pageSize: number; }) => Promise>, ): Promise { const rows: Row[] = []; for (let pageNo = 1; pageNo <= 1000; pageNo += 1) { const page = await loader({ pageNo, pageSize: 100 }); rows.push(...page.items); if (rows.length >= page.total || page.items.length === 0) break; } return rows; } /** * Loads per-account modal metadata once and applies only the latest revision. * @param selfId - Exact account identity captured for this load. * @param revision - Monotonic revision assigned by `loadAccount`. */ async function loadMetadata(selfId: string, revision: number) { targetOptionsLoading.value = canLoadTargets; const [subscriptionResult, templateResult, targetResult] = await Promise.allSettled([ loadAllPages((params) => getMessageSubscriptionList(params)), loadAllPages((params) => getMessageTemplateList(params)), canLoadTargets ? getAccountMessagePushTargets(selfId) : Promise.resolve(undefined), ]); if (revision !== loadRevision || selfId !== props.selfId) return; if (subscriptionResult.status === 'fulfilled') { subscriptions.value = subscriptionResult.value; } if (templateResult.status === 'fulfilled') { templates.value = templateResult.value; } if (targetResult.status === 'fulfilled') { targetOptions.value = targetResult.value; } targetOptionsLoading.value = false; } /** * Starts exactly one logical load for a new nonempty account identity. * @param selfId - Current implicit account identity. */ async function loadAccount(selfId: string) { const revision = ++loadRevision; latestBindingPage = { items: [], total: 0 }; subscriptions.value = []; templates.value = []; targetOptions.value = undefined; if (!selfId) { targetOptionsLoading.value = false; return; } await Promise.allSettled([ tableApi.reload(), loadMetadata(selfId, revision), ]); } /** Starts the one authorized initial account load after table registration. */ function activatePanel() { if (canList && props.selfId) void loadAccount(props.selfId); } /** Invalidates any pending request when this tab's panel unmounts. */ function invalidatePendingLoad() { loadRevision += 1; } /** * Renders source, targets, availability, and enabled state. * @param slot - KtTable body-cell slot payload. * @param slot.column - Current data column. * @param slot.record - Current account binding. * @returns Custom cell content or undefined for native field rendering. */ function renderBodyCell(slot: { column: TableColumnType; record: QqbotMessagePushApi.QqbotMessagePublishBindingView; }) { const { column, record } = slot; if (column.key === 'source') { return `${record.sourceName} · ${record.sourceKey}`; } if (column.key === 'targets') { return ( {record.targets.map((target) => ( {target.targetType === 'group' ? '群' : '私聊'} ·{' '} {target.targetName || target.targetId} ))} ); } if (column.key === 'enabled') { let color = 'warning'; let label = record.invalidReasonCode || 'unavailable'; if (record.available) { color = record.enabled ? 'success' : 'default'; label = record.enabled ? '启用' : '停用'; } return {label}; } return undefined; } onMounted(activatePanel); onBeforeUnmount(invalidatePendingLoad); watch( () => props.selfId, (selfId, previousSelfId) => { if (!canList || selfId === previousSelfId) return; void loadAccount(selfId); }, ); return () => ( ); }, });