refactor: 收敛Admin全域管理边界

This commit is contained in:
sunlei 2026-06-15 12:39:39 +08:00
parent 33ebdac389
commit 9f20988fbd
10 changed files with 258 additions and 56 deletions

View File

@ -41,4 +41,18 @@ describe('napcat login display helpers', () => {
'新设备二维码已扫码',
);
});
it('keeps the complete new-device progress key set in caller helpers', () => {
expect(
Object.keys(NAPCAT_LOGIN_PROGRESS_LABELS)
.filter((key) => key.startsWith('new-device-'))
.toSorted(),
).toEqual([
'new-device-confirming',
'new-device-qrcode-ready',
'new-device-required',
'new-device-scanned',
'new-device-verified',
]);
});
});

View File

@ -27,6 +27,7 @@ import {
qqbotRuleMatchOptions,
qqbotRuleTargetOptions,
} from '../../modules/options';
import { getQqbotStatusColor, getQqbotStatusLabel } from '../../modules/status';
const AKtTable = KtTable as any;
const ASpin = Spin as any;
@ -342,9 +343,10 @@ export default defineComponent({
};
const renderEnabledTag = (enabled: boolean) => {
const status = enabled ? 'enabled' : 'disabled';
return (
<Tag color={enabled ? 'success' : 'default'}>
{enabled ? '启用' : '停用'}
<Tag color={getQqbotStatusColor(status)}>
{getQqbotStatusLabel(status)}
</Tag>
);
};

View File

@ -32,6 +32,7 @@ import {
qqbotCommandParserOptions,
qqbotRuleTargetOptions,
} from '../modules/options';
import { getQqbotStatusColor, getQqbotStatusLabel } from '../modules/status';
const AKtTable = KtTable as any;
@ -562,9 +563,10 @@ export default defineComponent({
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.Command;
if (column.key === 'enabled') {
const status = row.enabled ? 'enabled' : 'disabled';
return (
<Tag color={row.enabled ? 'success' : 'default'}>
{row.enabled ? '启用' : '停用'}
<Tag color={getQqbotStatusColor(status)}>
{getQqbotStatusLabel(status)}
</Tag>
);
}

View File

@ -0,0 +1,84 @@
/* eslint-disable vue/one-component-per-file */
import { defineComponent, h, isVNode } from 'vue';
import { describe, expect, it, vi } from 'vitest';
vi.mock('antdv-next', () => ({
Button: defineComponent({
name: 'MockButton',
setup(_, { slots }) {
return () => h('button', slots.default?.());
},
}),
Popconfirm: defineComponent({
name: 'MockPopconfirm',
setup(_, { slots }) {
return () => h('span', slots.default?.());
},
}),
Space: defineComponent({
name: 'MockSpace',
setup(_, { slots }) {
return () => h('div', slots.default?.());
},
}),
}));
describe('qqbot shared action renderer', () => {
it('renders link buttons and preserves action props', async () => {
const { renderQqbotActions } = await import('./actions');
const TestIcon = defineComponent({
name: 'TestIcon',
setup: () => () => h('i'),
});
const onEdit = vi.fn();
const onDelete = vi.fn();
const vnode = renderQqbotActions([
{
disabled: true,
icon: TestIcon,
key: 'edit',
label: '编辑',
loading: true,
onClick: onEdit,
},
{
confirmText: '确认删除吗?',
danger: true,
key: 'delete',
label: '删除',
onClick: onDelete,
},
]);
expect(isVNode(vnode)).toBe(true);
const children = vnode.children as {
default?: () => unknown[];
};
const actionNodes = children.default?.().flat() as any[];
expect(actionNodes).toHaveLength(2);
const editButton = actionNodes[0].children[0];
expect(editButton.props).toMatchObject({
disabled: true,
loading: true,
size: 'small',
type: 'link',
});
expect(isVNode(editButton.props.icon)).toBe(true);
expect(editButton.props.onClick).toBe(onEdit);
const deleteConfirm = actionNodes[1];
expect(deleteConfirm.props.title).toBe('确认删除吗?');
expect(deleteConfirm.props.onConfirm).toBe(onDelete);
const confirmSlots = deleteConfirm.children as {
default?: () => unknown;
};
const deleteButton = confirmSlots.default?.() as any;
expect(deleteButton.props.danger).toBe(true);
expect(deleteButton.props.onClick).toBeUndefined();
});
});

View File

@ -0,0 +1,54 @@
import type { Component } from 'vue';
import { h } from 'vue';
import { Button, Popconfirm, Space } from 'antdv-next';
const AButton = Button as any;
const APopconfirm = Popconfirm as any;
const ASpace = Space as any;
export interface QqbotActionItem {
confirmText?: string;
danger?: boolean;
disabled?: boolean;
icon?: Component;
key: string;
label: string;
loading?: boolean;
onClick: () => Promise<void> | void;
}
export const renderQqbotActions = (actions: QqbotActionItem[]) => {
return (
<ASpace size="small">
{actions.map((action) => {
const button = (
<AButton
danger={action.danger}
disabled={action.disabled}
icon={action.icon ? h(action.icon) : undefined}
loading={action.loading}
onClick={action.confirmText ? undefined : action.onClick}
size="small"
type="link"
>
{action.label}
</AButton>
);
if (!action.confirmText) return <span key={action.key}>{button}</span>;
return (
<APopconfirm
key={action.key}
onConfirm={action.onClick}
title={action.confirmText}
>
{{ default: () => button }}
</APopconfirm>
);
})}
</ASpace>
);
};

View File

@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import {
getQqbotStatusColor,
getQqbotStatusLabel,
qqbotStatusLabels,
} from './status';
describe('qqbot shared status helpers', () => {
it('maps common QQBot status keys to Chinese labels', () => {
expect(qqbotStatusLabels).toMatchObject({
disabled: '已停用',
enabled: '已启用',
failed: '失败',
offline: '离线',
online: '在线',
pending: '处理中',
unknown: '未知',
});
expect(getQqbotStatusLabel(undefined)).toBe('未知');
expect(getQqbotStatusLabel('custom')).toBe('custom');
});
it('uses stable tag colors for common QQBot status keys', () => {
expect(getQqbotStatusColor('online')).toBe('success');
expect(getQqbotStatusColor('enabled')).toBe('success');
expect(getQqbotStatusColor('offline')).toBe('default');
expect(getQqbotStatusColor('disabled')).toBe('default');
expect(getQqbotStatusColor('failed')).toBe('error');
expect(getQqbotStatusColor('pending')).toBe('processing');
expect(getQqbotStatusColor(undefined)).toBe('default');
});
});

View File

@ -0,0 +1,28 @@
import type { TagProps } from 'antdv-next';
export const qqbotStatusLabels = {
disabled: '已停用',
enabled: '已启用',
failed: '失败',
offline: '离线',
online: '在线',
pending: '处理中',
unknown: '未知',
} as const;
export type QqbotStatusKey = keyof typeof qqbotStatusLabels;
export function getQqbotStatusLabel(status: string | undefined): string {
if (!status) return qqbotStatusLabels.unknown;
return qqbotStatusLabels[status as QqbotStatusKey] ?? status;
}
export function getQqbotStatusColor(
status: string | undefined,
): TagProps['color'] {
if (status === 'online' || status === 'enabled') return 'success';
if (status === 'offline' || status === 'disabled') return 'default';
if (status === 'failed') return 'error';
if (status === 'pending') return 'processing';
return 'default';
}

View File

@ -29,6 +29,7 @@ import {
getOptionLabel,
qqbotPermissionTargetOptions,
} from '../modules/options';
import { getQqbotStatusColor, getQqbotStatusLabel } from '../modules/status';
const AKtTable = KtTable as any;
const ASwitch = Switch as any;
@ -463,9 +464,10 @@ export default defineComponent({
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.Permission;
if (column.key === 'enabled') {
const status = row.enabled ? 'enabled' : 'disabled';
return (
<Tag color={row.enabled ? 'success' : 'default'}>
{row.enabled ? '启用' : '停用'}
<Tag color={getQqbotStatusColor(status)}>
{getQqbotStatusLabel(status)}
</Tag>
);
}

View File

@ -9,7 +9,7 @@ import { computed, defineComponent, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Drawer, message, Modal, Space, Tag } from 'antdv-next';
import { Drawer, message, Modal, Tag } from 'antdv-next';
import {
getQqbotPluginHealth,
@ -30,10 +30,11 @@ import {
import { KtTable, useKtTable } from '#/components/ktTable';
import { useDict } from '#/hooks/useDict';
const AButton = Button as any;
import { renderQqbotActions } from '../modules/actions';
import { getQqbotStatusColor, getQqbotStatusLabel } from '../modules/status';
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<
@ -301,23 +302,10 @@ export default defineComponent({
}
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>;
if (!status) return <Tag color="default">-</Tag>;
const color =
status === 'uninstalled' ? 'error' : getQqbotStatusColor(status);
return <Tag color={color}>{getQqbotStatusLabel(status)}</Tag>;
};
const renderDrawerContent = () => {
@ -354,9 +342,7 @@ export default defineComponent({
class="border-b border-solid border-gray-100 pb-3"
key={item.id}
>
<Tag color={item.enabled ? 'success' : 'default'}>
{item.enabled ? '已启用' : '已停用'}
</Tag>
{renderStatusTag(item.enabled ? 'enabled' : 'disabled')}
<span>
{item.pluginId} / {item.accountId}
</span>
@ -382,31 +368,26 @@ export default defineComponent({
{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>
{renderQqbotActions([
{
disabled: item.status === 'enabled',
key: 'enable',
label: '启用',
onClick: () => updateInstallationStatus(item, 'enable'),
},
{
disabled: item.status === 'disabled',
key: 'disable',
label: '禁用',
onClick: () => updateInstallationStatus(item, 'disable'),
},
{
danger: true,
key: 'uninstall',
label: '卸载',
onClick: () => updateInstallationStatus(item, 'uninstall'),
},
])}
</div>
))}
</div>

View File

@ -29,6 +29,7 @@ import {
qqbotRuleMatchOptions,
qqbotRuleTargetOptions,
} from '../modules/options';
import { getQqbotStatusColor, getQqbotStatusLabel } from '../modules/status';
const AKtTable = KtTable as any;
@ -299,9 +300,10 @@ export default defineComponent({
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.Rule;
if (column.key === 'enabled') {
const status = row.enabled ? 'enabled' : 'disabled';
return (
<Tag color={row.enabled ? 'success' : 'default'}>
{row.enabled ? '启用' : '停用'}
<Tag color={getQqbotStatusColor(status)}>
{getQqbotStatusLabel(status)}
</Tag>
);
}