feat: 完善QQBot账号配置交互

This commit is contained in:
sunlei 2026-06-02 19:24:24 +08:00
parent 77270c51eb
commit c30d7d04a6
13 changed files with 1073 additions and 17 deletions

View File

@ -0,0 +1,14 @@
import { requestClient } from '#/api/request';
export namespace DictApi {
export interface Option {
label: string;
value: number | string;
}
}
export function getDictByKey(dictKey: string) {
return requestClient.get<DictApi.Option[]>('/dict/getDictByKey', {
params: { dictKey },
});
}

View File

@ -1,4 +1,5 @@
export * from './auth';
export * from './dict';
export * from './menu';
export * from './timezone';
export * from './user';

View File

@ -18,6 +18,8 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
'BlogTagEdit',
'QqBot',
'QqBotAccount',
'QqBotAccountConfig',
'QqBotAccountConfigButton',
'QqBotAccountCreate',
'QqBotAccountDelete',
'QqBotAccountEdit',
@ -75,9 +77,11 @@ function filterSupportedAdminMenus(
const children = menu.children
? filterSupportedAdminMenus(menu.children)
: undefined;
const menuWithoutChildren = { ...menu };
delete menuWithoutChildren.children;
return {
...menu,
...menuWithoutChildren,
...(children && children.length > 0 ? { children } : {}),
};
})

View File

@ -3,6 +3,8 @@ import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace QqbotApi {
export type PluginTriggerMode = 'command' | 'event';
export interface PageResult<T> {
list: T[];
pageNo?: number;
@ -212,6 +214,7 @@ export namespace QqbotApi {
key: string;
name: string;
operationCount: number;
triggerMode: PluginTriggerMode;
version: string;
}
@ -223,12 +226,29 @@ export namespace QqbotApi {
name: string;
outputSchema?: Recordable<any>;
pluginKey: string;
triggerMode: PluginTriggerMode;
}
export interface PluginHealth {
checkedAt: string;
message?: string;
name?: string;
pluginKey?: string;
status: 'degraded' | 'healthy' | 'offline';
triggerMode?: PluginTriggerMode;
}
export interface EventPlugin {
accountName?: string;
bound: boolean;
connectStatus?: string;
description?: string;
key: string;
name: string;
remark?: string;
selfId: string;
triggerType: 'message';
version: string;
}
export type Query = Recordable<any>;
@ -265,6 +285,26 @@ export function deleteQqbotAccount(id: string) {
);
}
export function bindQqbotAccountCommand(selfId: string, commandId: string) {
const params = new URLSearchParams({ commandId, selfId });
return requestClient.post<boolean>(`/qqbot/account/bind/command?${params}`);
}
export function unbindQqbotAccountCommand(selfId: string, commandId: string) {
const params = new URLSearchParams({ commandId, selfId });
return requestClient.post<boolean>(`/qqbot/account/unbind/command?${params}`);
}
export function bindQqbotAccountRule(selfId: string, ruleId: string) {
const params = new URLSearchParams({ ruleId, selfId });
return requestClient.post<boolean>(`/qqbot/account/bind/rule?${params}`);
}
export function unbindQqbotAccountRule(selfId: string, ruleId: string) {
const params = new URLSearchParams({ ruleId, selfId });
return requestClient.post<boolean>(`/qqbot/account/unbind/rule?${params}`);
}
export function kickQqbotAccount(selfId: string) {
return requestClient.post<{ count: number }>(
`/qqbot/account/kick?selfId=${selfId}`,
@ -451,19 +491,47 @@ export function testQqbotCommand(data: {
);
}
export function getQqbotPluginList() {
return requestClient.get<QqbotApi.Plugin[]>('/qqbot/plugin/list');
export function getQqbotPluginList(triggerMode?: QqbotApi.PluginTriggerMode) {
return requestClient.get<QqbotApi.Plugin[]>('/qqbot/plugin/list', {
params: { triggerMode },
});
}
export function getQqbotPluginOperationList(pluginKey?: string) {
export function getQqbotPluginOperationList(
pluginKey?: string,
triggerMode?: QqbotApi.PluginTriggerMode,
) {
return requestClient.get<QqbotApi.PluginOperation[]>(
'/qqbot/plugin/operation/list',
{ params: { pluginKey } },
{ params: { pluginKey, triggerMode } },
);
}
export function getQqbotPluginHealth(pluginKey?: string) {
export function getQqbotPluginHealth(
pluginKey?: string,
triggerMode?: QqbotApi.PluginTriggerMode,
) {
return requestClient.get<QqbotApi.PluginHealth[]>('/qqbot/plugin/health', {
params: { pluginKey },
params: { pluginKey, triggerMode },
});
}
export function getQqbotEventPluginList(params?: { selfId?: string }) {
return requestClient.get<QqbotApi.EventPlugin[]>('/qqbot/plugin/event/list', {
params,
});
}
export function bindQqbotEventPlugin(selfId: string, pluginKey: string) {
const params = new URLSearchParams({ pluginKey, selfId });
return requestClient.post<QqbotApi.EventPlugin>(
`/qqbot/plugin/event/bind?${params.toString()}`,
);
}
export function unbindQqbotEventPlugin(selfId: string, pluginKey: string) {
const params = new URLSearchParams({ pluginKey, selfId });
return requestClient.post<boolean>(
`/qqbot/plugin/event/unbind?${params.toString()}`,
);
}

View File

@ -100,9 +100,11 @@ function filterSupportedSystemMenus(
const children = menu.children
? filterSupportedSystemMenus(menu.children)
: undefined;
const menuWithoutChildren = { ...menu };
delete menuWithoutChildren.children;
return {
...menu,
...menuWithoutChildren,
...(children && children.length > 0 ? { children } : {}),
};
})

View File

@ -0,0 +1,198 @@
import type { DictApi } from '#/api/core';
import { computed, readonly, ref, shallowRef } from 'vue';
import { getDictByKey } from '#/api/core';
export interface DictOption<TValue = number | string> {
label: string;
raw?: DictApi.Option;
value: TValue;
}
export interface LoadDictOptions<TValue = number | string> {
fallbackOptions?: Array<DictOption<TValue>>;
refresh?: boolean;
}
export interface UseDictOptions<
TValue = number | string,
> extends LoadDictOptions<TValue> {
immediate?: boolean;
}
const DICT_CACHE = new Map<string, Array<DictOption>>();
const DICT_PENDING = new Map<string, Promise<Array<DictOption>>>();
/**
* @param dictKey admin_dict.dict_code
* @param options refresh true
*/
export async function loadDictOptions<TValue = number | string>(
dictKey: string,
options: LoadDictOptions<TValue> = {},
): Promise<Array<DictOption<TValue>>> {
if (!dictKey) {
return normalizeFallbackOptions(options.fallbackOptions);
}
if (!options.refresh && DICT_CACHE.has(dictKey)) {
return DICT_CACHE.get(dictKey) as Array<DictOption<TValue>>;
}
if (!options.refresh && DICT_PENDING.has(dictKey)) {
return DICT_PENDING.get(dictKey) as Promise<Array<DictOption<TValue>>>;
}
const pending = getDictByKey(dictKey)
.then((list) => {
const normalized = normalizeDictOptions<TValue>(list);
const nextOptions =
normalized.length > 0
? normalized
: normalizeFallbackOptions(options.fallbackOptions);
DICT_CACHE.set(dictKey, nextOptions as Array<DictOption>);
return nextOptions;
})
.catch((error) => {
const fallback = normalizeFallbackOptions(options.fallbackOptions);
if (fallback.length > 0) {
DICT_CACHE.set(dictKey, fallback as Array<DictOption>);
return fallback;
}
throw error;
})
.finally(() => {
DICT_PENDING.delete(dictKey);
});
DICT_PENDING.set(dictKey, pending as Promise<Array<DictOption>>);
return pending;
}
/**
* @param dictKey admin_dict.dict_code
*/
export function getCachedDictOptions<TValue = number | string>(
dictKey: string,
) {
return (DICT_CACHE.get(dictKey) || []) as Array<DictOption<TValue>>;
}
/**
* @param dictKey
*/
export function clearDictCache(dictKey?: string) {
if (dictKey) {
DICT_CACHE.delete(dictKey);
DICT_PENDING.delete(dictKey);
return;
}
DICT_CACHE.clear();
DICT_PENDING.clear();
}
/**
* @param options
* @param value
* @param fallback
*/
export function getDictLabel(
options: Array<DictOption>,
value: unknown,
fallback?: string,
) {
const valueKey = getDictValueKey(value);
const matched = options.find(
(item) => getDictValueKey(item.value) === valueKey,
);
return matched?.label ?? fallback ?? valueKey;
}
/**
* @param dictKey admin_dict.dict_code
* @param options immediate true
*/
export function useDict<TValue = number | string>(
dictKey: string,
options: UseDictOptions<TValue> = {},
) {
const dictOptions = shallowRef<Array<DictOption<TValue>>>(
normalizeFallbackOptions(options.fallbackOptions),
);
const error = ref<unknown>();
const loading = ref(false);
const optionMap = computed(() => {
const map: Record<string, DictOption<TValue>> = {};
for (const item of dictOptions.value) {
map[getDictValueKey(item.value)] = item;
}
return map;
});
async function reload(refresh = false) {
loading.value = true;
error.value = undefined;
try {
dictOptions.value = await loadDictOptions<TValue>(dictKey, {
fallbackOptions: options.fallbackOptions,
refresh,
});
return dictOptions.value;
} catch (currentError) {
error.value = currentError;
dictOptions.value = normalizeFallbackOptions(options.fallbackOptions);
return dictOptions.value;
} finally {
loading.value = false;
}
}
function labelOf(value: unknown, fallback?: string) {
const valueKey = getDictValueKey(value);
return optionMap.value[valueKey]?.label ?? fallback ?? valueKey;
}
if (options.immediate !== false) {
void reload();
}
return {
error: readonly(error),
labelOf,
loading: readonly(loading),
options: dictOptions,
reload,
};
}
/**
* @param list
*/
function normalizeDictOptions<TValue = number | string>(
list: DictApi.Option[] = [],
): Array<DictOption<TValue>> {
return list
.filter((item) => item && item.label && item.value !== undefined)
.map((item) => ({
label: item.label,
raw: item,
value: item.value as TValue,
}));
}
/**
* @param options
*/
function normalizeFallbackOptions<TValue = number | string>(
options: Array<DictOption<TValue>> = [],
) {
return options.map((item) => ({ ...item }));
}
/**
* @param value /
*/
function getDictValueKey(value: unknown) {
return value === undefined || value === null ? '' : String(value);
}

View File

@ -29,6 +29,16 @@ const routes: RouteRecordRaw[] = [
name: 'QqBotAccount',
path: '/qqbot/account',
},
{
component: () => import('#/views/qqbot/account/config'),
meta: {
activePath: '/qqbot/account',
hideInMenu: true,
title: '账号功能配置',
},
name: 'QqBotAccountConfig',
path: '/qqbot/account/config',
},
{
component: () => import('#/views/qqbot/rule/list'),
meta: {

View File

@ -0,0 +1,489 @@
import type { PropType } from 'vue';
import type { QqbotApi } from '#/api/qqbot';
import { computed, defineComponent, ref, watch } from 'vue';
import {
Button,
message,
Popconfirm,
Space,
Table,
Tabs,
Tag,
} from 'antdv-next';
import {
bindQqbotAccountCommand,
bindQqbotAccountRule,
bindQqbotEventPlugin,
getQqbotCommandList,
getQqbotEventPluginList,
getQqbotRuleList,
unbindQqbotAccountCommand,
unbindQqbotAccountRule,
unbindQqbotEventPlugin,
} from '#/api/qqbot';
import {
getOptionLabel,
qqbotRuleMatchOptions,
qqbotRuleTargetOptions,
} from '../../modules/options';
const AButton = Button as any;
const APopconfirm = Popconfirm as any;
const ASpace = Space as any;
const ATable = Table as any;
const ATabs = Tabs as any;
export default defineComponent({
name: 'QqBotAccountConfigPanel',
props: {
account: {
default: undefined,
type: Object as PropType<QqbotApi.Account | undefined>,
},
},
setup(props) {
const activeTab = ref('command');
const boundCommands = ref<QqbotApi.Command[]>([]);
const boundRules = ref<QqbotApi.Rule[]>([]);
const commandTemplates = ref<QqbotApi.Command[]>([]);
const eventPlugins = ref<QqbotApi.EventPlugin[]>([]);
const loading = ref(false);
const ruleTemplates = ref<QqbotApi.Rule[]>([]);
const currentSelfId = computed(() => props.account?.selfId || '');
const boundCommandIds = computed(
() => new Set(boundCommands.value.map((item) => item.id)),
);
const boundRuleIds = computed(
() => new Set(boundRules.value.map((item) => item.id)),
);
const mergedCommandTemplates = computed(() =>
mergeById(commandTemplates.value, boundCommands.value),
);
const mergedRuleTemplates = computed(() =>
mergeById(ruleTemplates.value, boundRules.value),
);
const commandColumns = [
{ dataIndex: 'name', key: 'name', title: '命令模板', width: 160 },
{ dataIndex: 'code', key: 'code', title: '命令编码', width: 140 },
{ dataIndex: 'aliases', key: 'aliases', title: '别名', width: 200 },
{ dataIndex: 'pluginKey', key: 'pluginKey', title: '插件', width: 140 },
{
dataIndex: 'targetType',
key: 'targetType',
title: '目标范围',
width: 100,
},
{ dataIndex: 'enabled', key: 'enabled', title: '模板状态', width: 100 },
{ dataIndex: 'bound', key: 'bound', title: '绑定状态', width: 100 },
{
dataIndex: 'action',
fixed: 'right',
key: 'action',
title: '操作',
width: 100,
},
];
const eventColumns = [
{ dataIndex: 'name', key: 'name', title: '插件模板', width: 160 },
{ dataIndex: 'key', key: 'key', title: '插件 Key', width: 160 },
{
dataIndex: 'triggerType',
key: 'triggerType',
title: '触发类型',
width: 100,
},
{
dataIndex: 'description',
key: 'description',
title: '说明',
width: 320,
},
{ dataIndex: 'bound', key: 'bound', title: '绑定状态', width: 100 },
{
dataIndex: 'action',
fixed: 'right',
key: 'action',
title: '操作',
width: 100,
},
];
const ruleColumns = [
{ dataIndex: 'name', key: 'name', title: '规则模板', width: 160 },
{ dataIndex: 'keyword', key: 'keyword', title: '关键词', width: 180 },
{
dataIndex: 'matchType',
key: 'matchType',
title: '匹配方式',
width: 110,
},
{
dataIndex: 'targetType',
key: 'targetType',
title: '目标范围',
width: 100,
},
{
dataIndex: 'replyContent',
key: 'replyContent',
title: '回复模板',
width: 320,
},
{ dataIndex: 'enabled', key: 'enabled', title: '模板状态', width: 100 },
{ dataIndex: 'bound', key: 'bound', title: '绑定状态', width: 100 },
{
dataIndex: 'action',
fixed: 'right',
key: 'action',
title: '操作',
width: 100,
},
];
watch(
currentSelfId,
(selfId) => {
if (!selfId) {
boundCommands.value = [];
boundRules.value = [];
commandTemplates.value = [];
eventPlugins.value = [];
ruleTemplates.value = [];
return;
}
void refreshAll();
},
{ immediate: true },
);
async function refreshAll() {
loading.value = true;
try {
await Promise.all([
refreshCommandTemplates(),
refreshEventPlugins(),
refreshRuleTemplates(),
]);
} finally {
loading.value = false;
}
}
async function refreshCommandTemplates() {
const [templateResult, boundResult] = await Promise.all([
getQqbotCommandList({ pageNo: 1, pageSize: 500 }),
getQqbotCommandList({
pageNo: 1,
pageSize: 500,
selfId: currentSelfId.value,
}),
]);
commandTemplates.value = templateResult.list || [];
boundCommands.value = boundResult.list || [];
}
async function refreshCommandBindings() {
const result = await getQqbotCommandList({
pageNo: 1,
pageSize: 500,
selfId: currentSelfId.value,
});
boundCommands.value = result.list || [];
}
async function refreshEventPlugins() {
eventPlugins.value = await getQqbotEventPluginList({
selfId: currentSelfId.value,
});
}
async function refreshRuleTemplates() {
const [templateResult, boundResult] = await Promise.all([
getQqbotRuleList({ pageNo: 1, pageSize: 500 }),
getQqbotRuleList({
pageNo: 1,
pageSize: 500,
selfId: currentSelfId.value,
}),
]);
ruleTemplates.value = templateResult.list || [];
boundRules.value = boundResult.list || [];
}
async function refreshRuleBindings() {
const result = await getQqbotRuleList({
pageNo: 1,
pageSize: 500,
selfId: currentSelfId.value,
});
boundRules.value = result.list || [];
}
async function handleCommandBind(row: QqbotApi.Command) {
if (!ensureSelfId()) return;
await bindQqbotAccountCommand(currentSelfId.value, row.id);
message.success('命令已绑定到当前账号');
await refreshCommandBindings();
}
async function handleCommandUnbind(row: QqbotApi.Command) {
if (!ensureSelfId()) return;
await unbindQqbotAccountCommand(currentSelfId.value, row.id);
message.success('命令已从当前账号解绑');
await refreshCommandBindings();
}
async function handleEventBind(row: QqbotApi.EventPlugin) {
if (!ensureSelfId()) return;
await bindQqbotEventPlugin(currentSelfId.value, row.key);
message.success('事件插件已绑定到当前账号');
await refreshEventPlugins();
}
async function handleEventUnbind(row: QqbotApi.EventPlugin) {
if (!ensureSelfId()) return;
await unbindQqbotEventPlugin(currentSelfId.value, row.key);
message.success('事件插件已从当前账号解绑');
await refreshEventPlugins();
}
async function handleRuleBind(row: QqbotApi.Rule) {
if (!ensureSelfId()) return;
await bindQqbotAccountRule(currentSelfId.value, row.id);
message.success('规则已绑定到当前账号');
await refreshRuleBindings();
}
async function handleRuleUnbind(row: QqbotApi.Rule) {
if (!ensureSelfId()) return;
await unbindQqbotAccountRule(currentSelfId.value, row.id);
message.success('规则已从当前账号解绑');
await refreshRuleBindings();
}
function ensureSelfId() {
if (currentSelfId.value) return true;
message.warning('缺少账号 Self ID请从账号连接列表进入配置页');
return false;
}
function mergeById<T extends { id: string }>(templates: T[], bound: T[]) {
const map = new Map<string, T>();
templates.forEach((item) => map.set(item.id, item));
bound.forEach((item) => {
if (!map.has(item.id)) map.set(item.id, item);
});
return [...map.values()];
}
function renderBindAction(options: {
bound: boolean;
name: string;
onBind: () => Promise<void>;
onUnbind: () => Promise<void>;
}) {
if (!options.bound) {
return (
<AButton onClick={options.onBind} type="link">
</AButton>
);
}
return (
<APopconfirm
onConfirm={options.onUnbind}
title={`确认从当前账号解绑「${options.name}」吗?`}
>
<AButton danger type="link">
</AButton>
</APopconfirm>
);
}
function renderBoundTag(bound: boolean) {
return (
<Tag color={bound ? 'success' : 'default'}>
{bound ? '已绑定' : '未绑定'}
</Tag>
);
}
function renderEnabledTag(enabled: boolean) {
return (
<Tag color={enabled ? 'success' : 'default'}>
{enabled ? '启用' : '停用'}
</Tag>
);
}
function renderCommandTable() {
return (
<ATable
columns={commandColumns}
dataSource={mergedCommandTemplates.value}
loading={loading.value}
pagination={false}
rowKey="id"
scroll={{ x: 1200, y: 420 }}
size="small"
v-slots={{
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.Command;
const bound = boundCommandIds.value.has(row.id);
if (column.key === 'aliases') {
return row.aliases?.join(' / ') || '-';
}
if (column.key === 'targetType') {
return getOptionLabel(qqbotRuleTargetOptions, row.targetType);
}
if (column.key === 'enabled') {
return renderEnabledTag(row.enabled);
}
if (column.key === 'bound') {
return renderBoundTag(bound);
}
if (column.key === 'action') {
return (
<ASpace>
{renderBindAction({
bound,
name: row.name || row.code,
onBind: () => handleCommandBind(row),
onUnbind: () => handleCommandUnbind(row),
})}
</ASpace>
);
}
return undefined;
},
}}
/>
);
}
function renderEventTable() {
return (
<ATable
columns={eventColumns}
dataSource={eventPlugins.value}
loading={loading.value}
pagination={false}
rowKey={(row: QqbotApi.EventPlugin) =>
`${currentSelfId.value}:${row.key}`
}
scroll={{ x: 960, y: 420 }}
size="small"
v-slots={{
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.EventPlugin;
if (column.key === 'triggerType') {
return row.triggerType === 'message'
? '消息事件'
: row.triggerType;
}
if (column.key === 'bound') {
return renderBoundTag(row.bound);
}
if (column.key === 'action') {
return (
<ASpace>
{renderBindAction({
bound: row.bound,
name: row.name,
onBind: () => handleEventBind(row),
onUnbind: () => handleEventUnbind(row),
})}
</ASpace>
);
}
return undefined;
},
}}
/>
);
}
function renderRuleTable() {
return (
<ATable
columns={ruleColumns}
dataSource={mergedRuleTemplates.value}
loading={loading.value}
pagination={false}
rowKey="id"
scroll={{ x: 1200, y: 420 }}
size="small"
v-slots={{
bodyCell: ({ column, record }: any) => {
const row = record as QqbotApi.Rule;
const bound = boundRuleIds.value.has(row.id);
if (column.key === 'matchType') {
return getOptionLabel(qqbotRuleMatchOptions, row.matchType);
}
if (column.key === 'targetType') {
return getOptionLabel(qqbotRuleTargetOptions, row.targetType);
}
if (column.key === 'replyContent') {
return (
<span class="qqbot-account-config-panel__ellipsis">
{row.replyContent || '-'}
</span>
);
}
if (column.key === 'enabled') {
return renderEnabledTag(row.enabled);
}
if (column.key === 'bound') {
return renderBoundTag(bound);
}
if (column.key === 'action') {
return (
<ASpace>
{renderBindAction({
bound,
name: row.name || row.keyword,
onBind: () => handleRuleBind(row),
onUnbind: () => handleRuleUnbind(row),
})}
</ASpace>
);
}
return undefined;
},
}}
/>
);
}
return () => (
<div class="qqbot-account-config-panel">
<div class="qqbot-account-config-panel__account">
<Tag color="processing">Self ID{currentSelfId.value || '-'}</Tag>
{props.account?.name ? <Tag>{props.account.name}</Tag> : null}
</div>
<ATabs
class="qqbot-account-config-panel__tabs"
items={[
{ key: 'command', label: '在线命令' },
{ key: 'event', label: '事件触发' },
{ key: 'rule', label: '自动回复规则' },
]}
v-model:activeKey={activeTab.value}
/>
<div class="qqbot-account-config-panel__content">
{activeTab.value === 'command' ? renderCommandTable() : null}
{activeTab.value === 'event' ? renderEventTable() : null}
{activeTab.value === 'rule' ? renderRuleTable() : null}
</div>
</div>
);
},
});

View File

@ -0,0 +1,83 @@
.qqbot-account-config {
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
min-height: 0;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 40px;
}
&__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;
}
&__card {
flex: 1;
min-height: 0;
}
&__card,
&__card > .ant-card-body {
display: flex;
flex-direction: column;
min-height: 0;
}
&__card > .ant-card-body {
flex: 1;
}
}
.qqbot-account-config-panel {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
&__account {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
&__tabs {
flex: none;
}
&__content {
flex: 1;
min-height: 0;
}
&__ellipsis {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
}

View File

@ -0,0 +1,126 @@
import type { QqbotApi } from '#/api/qqbot';
import { computed, defineComponent, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { ArrowLeft } from '@vben/icons';
import { Alert, Button, Card, Spin, Tag } from 'antdv-next';
import { getQqbotAccountList } from '#/api/qqbot';
import AccountConfigPanel from './components/AccountConfigPanel';
import './config.scss';
const AButton = Button as any;
const ACard = Card as any;
const ASpin = Spin as any;
export default defineComponent({
name: 'QqBotAccountConfig',
setup() {
const route = useRoute();
const router = useRouter();
const account = ref<QqbotApi.Account>();
const errorMessage = ref('');
const loading = ref(false);
const selfId = computed(() => normalizeQueryValue(route.query.selfId));
const accountTitle = computed(() => {
if (!account.value) return '账号功能配置';
return account.value.name
? `${account.value.name}${account.value.selfId}`
: account.value.selfId;
});
watch(
selfId,
() => {
void loadAccount();
},
{ immediate: true },
);
async function loadAccount() {
const currentSelfId = selfId.value;
account.value = undefined;
errorMessage.value = '';
if (!currentSelfId) {
errorMessage.value = '缺少账号 Self ID请从账号连接列表进入配置页。';
return;
}
loading.value = true;
try {
const result = await getQqbotAccountList({
pageNo: 1,
pageSize: 20,
selfId: currentSelfId,
});
const matched = (result.list || []).find(
(item) => item.selfId === currentSelfId,
);
if (!matched) {
errorMessage.value = `未找到账号 ${currentSelfId},请返回账号连接列表确认账号状态。`;
return;
}
account.value = matched;
} finally {
loading.value = false;
}
}
function normalizeQueryValue(value: unknown) {
if (Array.isArray(value)) return `${value[0] || ''}`.trim();
return `${value || ''}`.trim();
}
function goBack() {
void router.push({ name: 'QqBotAccount' });
}
return () => (
<Page autoContentHeight>
<div class="qqbot-account-config">
<div class="qqbot-account-config__header">
<AButton
class="qqbot-account-config__back"
onClick={goBack}
type="text"
>
<ArrowLeft class="qqbot-account-config__back-icon" />
</AButton>
<div class="qqbot-account-config__title">
<span>{accountTitle.value}</span>
{account.value ? (
<Tag
color={
account.value.connectStatus === 'online'
? 'success'
: 'default'
}
>
{account.value.connectStatus === 'online' ? '在线' : '离线'}
</Tag>
) : null}
</div>
</div>
<ACard bordered={false} class="qqbot-account-config__card">
<ASpin spinning={loading.value}>
{errorMessage.value ? (
<Alert message={errorMessage.value} showIcon type="warning" />
) : (
<AccountConfigPanel account={account.value} />
)}
</ASpin>
</ACard>
</div>
</Page>
);
},
});

View File

@ -8,6 +8,7 @@ import type {
} from '#/components/ktTable';
import { computed, defineComponent, onBeforeUnmount, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
@ -38,6 +39,7 @@ export default defineComponent({
name: 'QqBotAccountList',
setup() {
const editingId = ref<string>();
const router = useRouter();
const scanLoading = ref(false);
const scanQrcodeText = ref('');
const scanState = reactive<{
@ -158,6 +160,12 @@ export default defineComponent({
},
];
const rowActions: Array<KtTableRowAction<QqbotApi.Account>> = [
{
key: 'config',
label: '配置',
onClick: openConfig,
permissionCodes: ['QqBot:Account:Config'],
},
{
key: 'refreshLogin',
label: '更新登录',
@ -267,7 +275,6 @@ export default defineComponent({
void resetAccountForm(values || getAccountFormDefaults());
},
});
onBeforeUnmount(() => {
stopScanPolling();
});
@ -446,6 +453,15 @@ export default defineComponent({
accountModalApi.setData({ values: getAccountFormDefaults() }).open();
}
function openConfig(row: QqbotApi.Account) {
void router.push({
name: 'QqBotAccountConfig',
query: {
selfId: row.selfId,
},
});
}
function openEdit(row: QqbotApi.Account) {
editingId.value = row.id;
accountModalApi

View File

@ -390,8 +390,8 @@ export default defineComponent({
async function loadPlugins() {
const [plugins, operations] = await Promise.all([
getQqbotPluginList(),
getQqbotPluginOperationList(),
getQqbotPluginList('command'),
getQqbotPluginOperationList(undefined, 'command'),
]);
pluginOptions.value = plugins.map((item) => ({
label: `${item.name} (${item.key})`,

View File

@ -2,6 +2,7 @@ import type { TableColumnType } from 'antdv-next';
import type { QqbotApi } from '#/api/qqbot';
import type { KtTableApi, KtTableButton } from '#/components/ktTable';
import type { DictOption } from '#/hooks/useDict';
import { defineComponent, onMounted, ref } from 'vue';
@ -15,17 +16,39 @@ import {
getQqbotPluginOperationList,
} from '#/api/qqbot';
import { KtTable, useKtTable } from '#/components/ktTable';
import { useDict } from '#/hooks/useDict';
const AKtTable = KtTable as any;
const QQBOT_PLUGIN_TRIGGER_MODE_DICT = 'QQBOT_PLUGIN_TRIGGER_MODE';
const qqbotPluginTriggerModeFallback: Array<
DictOption<QqbotApi.PluginTriggerMode>
> = [
{ label: '命令', value: 'command' },
{ label: '事件', value: 'event' },
];
export default defineComponent({
name: 'QqBotPluginList',
setup() {
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
const {
labelOf: getTriggerModeLabel,
options: triggerModeOptions,
reload: reloadTriggerModeDict,
} = useDict<QqbotApi.PluginTriggerMode>(QQBOT_PLUGIN_TRIGGER_MODE_DICT, {
fallbackOptions: qqbotPluginTriggerModeFallback,
immediate: false,
});
const columns: Array<TableColumnType<QqbotApi.PluginOperation>> = [
{ dataIndex: 'pluginKey', key: 'pluginKey', title: '插件', width: 160 },
{
dataIndex: 'triggerMode',
key: 'triggerMode',
title: '触发方式',
width: 120,
},
{ dataIndex: 'key', key: 'key', title: '能力 Key', width: 220 },
{ dataIndex: 'name', key: 'name', title: '能力名称', width: 160 },
{
@ -43,7 +66,7 @@ export default defineComponent({
];
const api: KtTableApi<QqbotApi.PluginOperation> = {
list: async (params) =>
await getQqbotPluginOperationList(params.pluginKey),
await getQqbotPluginOperationList(params.pluginKey, params.triggerMode),
};
const buttons: Array<KtTableButton<QqbotApi.PluginOperation>> = [
{
@ -52,7 +75,10 @@ export default defineComponent({
onClick: async () => {
const health = await getQqbotPluginHealth();
const content = health
.map((item) => `${item.status}: ${item.message || 'OK'}`)
.map(
(item) =>
`${getTriggerModeLabel(item.triggerMode, '-')} ${item.name || item.pluginKey || ''}: ${item.status}${item.message ? ` ${item.message}` : ''}`,
)
.join('');
message.success(content || '插件健康检查完成');
},
@ -64,6 +90,15 @@ export default defineComponent({
columns,
formOptions: {
schema: [
{
component: 'Select',
componentProps: () => ({
allowClear: true,
options: triggerModeOptions.value,
}),
fieldName: 'triggerMode',
label: '触发方式',
},
{
component: 'Select',
componentProps: () => ({
@ -80,18 +115,21 @@ export default defineComponent({
});
onMounted(() => {
void loadPlugins();
void loadMetadata();
});
async function loadPlugins() {
const plugins = await getQqbotPluginList();
async function loadMetadata() {
const [plugins] = await Promise.all([
getQqbotPluginList(),
reloadTriggerModeDict(),
]);
const nextPluginMap: Record<string, QqbotApi.Plugin> = {};
for (const item of plugins) {
nextPluginMap[item.key] = item;
}
pluginMap.value = nextPluginMap;
pluginOptions.value = plugins.map((item) => ({
label: `${item.name} (${item.key})`,
label: `${item.name} (${item.key} / ${getTriggerModeLabel(item.triggerMode, '-')})`,
value: item.key,
}));
}
@ -113,6 +151,13 @@ export default defineComponent({
row.pluginKey
);
}
if (column.key === 'triggerMode') {
return (
<Tag color={row.triggerMode === 'event' ? 'warning' : 'blue'}>
{getTriggerModeLabel(row.triggerMode, '-')}
</Tag>
);
}
if (column.key === 'cacheTtlMs') {
return row.cacheTtlMs ? `${row.cacheTtlMs} ms` : '-';
}