feat: 增加QQBot管理页面
This commit is contained in:
parent
14880dfcf1
commit
8644cf8eab
@ -16,6 +16,27 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
|
||||
'BlogTagCreate',
|
||||
'BlogTagDelete',
|
||||
'BlogTagEdit',
|
||||
'QqBot',
|
||||
'QqBotAccount',
|
||||
'QqBotAccountCreate',
|
||||
'QqBotAccountDelete',
|
||||
'QqBotAccountEdit',
|
||||
'QqBotAccountKick',
|
||||
'QqBotConversation',
|
||||
'QqBotDashboard',
|
||||
'QqBotMessage',
|
||||
'QqBotPermission',
|
||||
'QqBotPermissionCreate',
|
||||
'QqBotPermissionDelete',
|
||||
'QqBotPermissionEdit',
|
||||
'QqBotRule',
|
||||
'QqBotRuleCreate',
|
||||
'QqBotRuleDelete',
|
||||
'QqBotRuleEdit',
|
||||
'QqBotRuleToggle',
|
||||
'QqBotSendGroup',
|
||||
'QqBotSendLog',
|
||||
'QqBotSendPrivate',
|
||||
'System',
|
||||
'SystemDept',
|
||||
'SystemDeptCreate',
|
||||
|
||||
270
apps/web-antdv-next/src/api/qqbot/index.ts
Normal file
270
apps/web-antdv-next/src/api/qqbot/index.ts
Normal file
@ -0,0 +1,270 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace QqbotApi {
|
||||
export interface PageResult<T> {
|
||||
list: T[];
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface DashboardSummary {
|
||||
accountTotal: number;
|
||||
bus: {
|
||||
connected: boolean;
|
||||
mode: string;
|
||||
url: string;
|
||||
};
|
||||
conversationTotal: number;
|
||||
enabledRuleTotal: number;
|
||||
messageTotal: number;
|
||||
onlineTotal: number;
|
||||
runtime: {
|
||||
enabled: boolean;
|
||||
path: string;
|
||||
sessions: string[];
|
||||
};
|
||||
sendFailedTotal: number;
|
||||
sendSuccessTotal: number;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
clientRole?: string;
|
||||
connectStatus: 'offline' | 'online';
|
||||
connectionMode: 'reverse-ws';
|
||||
createTime?: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
lastConnectedAt?: string;
|
||||
lastError?: string;
|
||||
lastHeartbeatAt?: string;
|
||||
name: string;
|
||||
remark?: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
export interface AccountBody {
|
||||
accessToken?: string;
|
||||
connectionMode?: 'reverse-ws';
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
remark?: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
cooldownMs: number;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
keyword: string;
|
||||
lastHitAt?: string;
|
||||
matchType: 'equals' | 'keyword' | 'regex';
|
||||
name: string;
|
||||
priority: number;
|
||||
remark?: string;
|
||||
replyContent: string;
|
||||
targetType: 'all' | 'group' | 'private';
|
||||
}
|
||||
|
||||
export interface RuleBody {
|
||||
cooldownMs?: number;
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
keyword: string;
|
||||
matchType: 'equals' | 'keyword' | 'regex';
|
||||
name?: string;
|
||||
priority?: number;
|
||||
remark?: string;
|
||||
replyContent: string;
|
||||
targetType?: 'all' | 'group' | 'private';
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
createTime?: string;
|
||||
id: string;
|
||||
lastMessageText?: string;
|
||||
lastMessageTime?: string;
|
||||
messageCount: number;
|
||||
selfId: string;
|
||||
targetId: string;
|
||||
targetName?: string;
|
||||
targetType: 'group' | 'private';
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
direction: 'inbound' | 'outbound';
|
||||
eventTime: string;
|
||||
id: string;
|
||||
messageText: string;
|
||||
messageType: 'group' | 'private';
|
||||
senderNickname?: string;
|
||||
selfId: string;
|
||||
targetId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface SendLog {
|
||||
action: string;
|
||||
createTime?: string;
|
||||
errorMessage?: string;
|
||||
id: string;
|
||||
messageText: string;
|
||||
selfId: string;
|
||||
status: 'failed' | 'pending' | 'success';
|
||||
targetId: string;
|
||||
targetType: 'group' | 'private';
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
remark?: string;
|
||||
selfId?: string;
|
||||
targetId: string;
|
||||
targetType: 'all' | 'group' | 'private';
|
||||
}
|
||||
|
||||
export interface PermissionBody {
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
remark?: string;
|
||||
selfId?: string;
|
||||
targetId: string;
|
||||
targetType: 'all' | 'group' | 'private';
|
||||
}
|
||||
|
||||
export type Query = Recordable<any>;
|
||||
}
|
||||
|
||||
export function getQqbotDashboardSummary() {
|
||||
return requestClient.get<QqbotApi.DashboardSummary>(
|
||||
'/qqbot/dashboard/summary',
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotAccountList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Account>>(
|
||||
'/qqbot/account/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotEnabledAccounts() {
|
||||
return requestClient.get<QqbotApi.Account[]>('/qqbot/account/enabled');
|
||||
}
|
||||
|
||||
export function createQqbotAccount(data: QqbotApi.AccountBody) {
|
||||
return requestClient.post<string>('/qqbot/account/save', data);
|
||||
}
|
||||
|
||||
export function updateQqbotAccount(data: QqbotApi.AccountBody) {
|
||||
return requestClient.post<boolean>('/qqbot/account/update', data);
|
||||
}
|
||||
|
||||
export function deleteQqbotAccount(id: string) {
|
||||
return requestClient.post<boolean>(`/qqbot/account/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function kickQqbotAccount(selfId: string) {
|
||||
return requestClient.post<{ count: number }>(
|
||||
`/qqbot/account/kick?selfId=${selfId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotRuleList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Rule>>(
|
||||
'/qqbot/rule/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function createQqbotRule(data: QqbotApi.RuleBody) {
|
||||
return requestClient.post<string>('/qqbot/rule/save', data);
|
||||
}
|
||||
|
||||
export function updateQqbotRule(data: QqbotApi.RuleBody) {
|
||||
return requestClient.post<boolean>('/qqbot/rule/update', data);
|
||||
}
|
||||
|
||||
export function deleteQqbotRule(id: string) {
|
||||
return requestClient.post<boolean>(`/qqbot/rule/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function toggleQqbotRule(id: string, enabled: boolean) {
|
||||
return requestClient.post<boolean>(
|
||||
`/qqbot/rule/toggle?id=${id}&enabled=${enabled}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotConversationList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Conversation>>(
|
||||
'/qqbot/conversation/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotMessageList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Message>>(
|
||||
'/qqbot/message/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotSendLogList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.SendLog>>(
|
||||
'/qqbot/send/log/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function sendQqbotPrivate(data: {
|
||||
message: string;
|
||||
selfId?: string;
|
||||
userId: string;
|
||||
}) {
|
||||
return requestClient.post('/qqbot/send/private', data);
|
||||
}
|
||||
|
||||
export function sendQqbotGroup(data: {
|
||||
groupId: string;
|
||||
message: string;
|
||||
selfId?: string;
|
||||
}) {
|
||||
return requestClient.post('/qqbot/send/group', data);
|
||||
}
|
||||
|
||||
export function getQqbotPermissionList(
|
||||
kind: 'allowlist' | 'blocklist',
|
||||
params: QqbotApi.Query,
|
||||
) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Permission>>(
|
||||
`/qqbot/permission/${kind}`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function createQqbotPermission(
|
||||
kind: 'allowlist' | 'blocklist',
|
||||
data: QqbotApi.PermissionBody,
|
||||
) {
|
||||
return requestClient.post<string>(`/qqbot/permission/${kind}/save`, data);
|
||||
}
|
||||
|
||||
export function updateQqbotPermission(
|
||||
kind: 'allowlist' | 'blocklist',
|
||||
data: QqbotApi.PermissionBody,
|
||||
) {
|
||||
return requestClient.post<boolean>(`/qqbot/permission/${kind}/update`, data);
|
||||
}
|
||||
|
||||
export function deleteQqbotPermission(
|
||||
kind: 'allowlist' | 'blocklist',
|
||||
id: string,
|
||||
) {
|
||||
return requestClient.post<boolean>(
|
||||
`/qqbot/permission/${kind}/delete?id=${id}`,
|
||||
);
|
||||
}
|
||||
@ -44,6 +44,11 @@ const componentKeys: string[] = Object.keys({
|
||||
const path = v.replace('../../views/', '/');
|
||||
return path.replace(/\.(tsx|vue)$/, '');
|
||||
})
|
||||
.filter((path) => path.startsWith('/blog/') || path.startsWith('/system/'));
|
||||
.filter(
|
||||
(path) =>
|
||||
path.startsWith('/blog/') ||
|
||||
path.startsWith('/qqbot/') ||
|
||||
path.startsWith('/system/'),
|
||||
);
|
||||
|
||||
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||
|
||||
81
apps/web-antdv-next/src/router/routes/modules/qqbot.ts
Normal file
81
apps/web-antdv-next/src/router/routes/modules/qqbot.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:bot',
|
||||
order: 110,
|
||||
title: 'QQBot 管理',
|
||||
},
|
||||
name: 'QqBot',
|
||||
path: '/qqbot',
|
||||
redirect: '/qqbot/dashboard',
|
||||
children: [
|
||||
{
|
||||
component: () => import('#/views/qqbot/dashboard/list'),
|
||||
meta: {
|
||||
icon: 'lucide:gauge',
|
||||
title: '工作台',
|
||||
},
|
||||
name: 'QqBotDashboard',
|
||||
path: '/qqbot/dashboard',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/account/list'),
|
||||
meta: {
|
||||
icon: 'lucide:radio-receiver',
|
||||
title: '账号连接',
|
||||
},
|
||||
name: 'QqBotAccount',
|
||||
path: '/qqbot/account',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/rule/list'),
|
||||
meta: {
|
||||
icon: 'lucide:workflow',
|
||||
title: '自动回复规则',
|
||||
},
|
||||
name: 'QqBotRule',
|
||||
path: '/qqbot/rule',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/conversation/list'),
|
||||
meta: {
|
||||
icon: 'lucide:messages-square',
|
||||
title: '会话管理',
|
||||
},
|
||||
name: 'QqBotConversation',
|
||||
path: '/qqbot/conversation',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/message/list'),
|
||||
meta: {
|
||||
icon: 'lucide:message-square-text',
|
||||
title: '消息日志',
|
||||
},
|
||||
name: 'QqBotMessage',
|
||||
path: '/qqbot/message',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/sendLog/list'),
|
||||
meta: {
|
||||
icon: 'lucide:send',
|
||||
title: '发送日志',
|
||||
},
|
||||
name: 'QqBotSendLog',
|
||||
path: '/qqbot/sendLog',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/permission/list'),
|
||||
meta: {
|
||||
icon: 'lucide:shield-check',
|
||||
title: '权限名单',
|
||||
},
|
||||
name: 'QqBotPermission',
|
||||
path: '/qqbot/permission',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
305
apps/web-antdv-next/src/views/qqbot/account/list.tsx
Normal file
305
apps/web-antdv-next/src/views/qqbot/account/list.tsx
Normal file
@ -0,0 +1,305 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type {
|
||||
KtTableApi,
|
||||
KtTableButton,
|
||||
KtTableRowAction,
|
||||
} from '#/components/ktTable';
|
||||
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Form, FormItem, Input, message, Modal, Switch, Tag } from 'antdv-next';
|
||||
|
||||
import {
|
||||
createQqbotAccount,
|
||||
deleteQqbotAccount,
|
||||
getQqbotAccountList,
|
||||
kickQqbotAccount,
|
||||
updateQqbotAccount,
|
||||
} from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
const AInput = Input as any;
|
||||
const AModal = Modal as any;
|
||||
const ASwitch = Switch as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotAccountList',
|
||||
setup() {
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const editingId = ref<string>();
|
||||
const form = reactive<QqbotApi.AccountBody>({
|
||||
accessToken: '',
|
||||
connectionMode: 'reverse-ws',
|
||||
enabled: true,
|
||||
name: '',
|
||||
remark: '',
|
||||
selfId: '',
|
||||
});
|
||||
|
||||
const columns: Array<TableColumnType<QqbotApi.Account>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 160 },
|
||||
{ dataIndex: 'name', key: 'name', title: '账号名称', width: 180 },
|
||||
{
|
||||
dataIndex: 'connectStatus',
|
||||
key: 'connectStatus',
|
||||
title: '连接状态',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'clientRole',
|
||||
key: 'clientRole',
|
||||
title: '连接角色',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastHeartbeatAt',
|
||||
key: 'lastHeartbeatAt',
|
||||
title: '最后心跳',
|
||||
width: 190,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastError',
|
||||
key: 'lastError',
|
||||
title: '错误信息',
|
||||
width: 260,
|
||||
},
|
||||
];
|
||||
|
||||
const api: KtTableApi<QqbotApi.Account> = {
|
||||
list: async (params) => await getQqbotAccountList(params),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.Account>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新建账号',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Account:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const rowActions: Array<KtTableRowAction<QqbotApi.Account>> = [
|
||||
{
|
||||
disabled: (row) => row.connectStatus !== 'online',
|
||||
key: 'kick',
|
||||
label: '断开',
|
||||
onClick: async (row, context) => {
|
||||
await kickQqbotAccount(row.selfId);
|
||||
message.success('连接已断开');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:Account:Kick'],
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
onClick: openEdit,
|
||||
permissionCodes: ['QqBot:Account:Edit'],
|
||||
},
|
||||
{
|
||||
confirm: (row) => `确认删除账号「${row.selfId}」吗?`,
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
onClick: async (row, context) => {
|
||||
await deleteQqbotAccount(row.id);
|
||||
message.success('账号删除成功');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:Account:Delete'],
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotApi.Account>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: 'Self ID' },
|
||||
fieldName: 'selfId',
|
||||
label: 'Self ID',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '账号名称' },
|
||||
fieldName: 'name',
|
||||
label: '账号名称',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
],
|
||||
},
|
||||
fieldName: 'connectStatus',
|
||||
label: '连接状态',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions,
|
||||
tableTitle: 'QQBot 账号连接',
|
||||
});
|
||||
const modalTitle = computed(() =>
|
||||
editingId.value ? '编辑账号' : '新建账号',
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = undefined;
|
||||
Object.assign(form, {
|
||||
accessToken: '',
|
||||
connectionMode: 'reverse-ws',
|
||||
enabled: true,
|
||||
name: '',
|
||||
remark: '',
|
||||
selfId: '',
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: QqbotApi.Account) {
|
||||
editingId.value = row.id;
|
||||
Object.assign(form, {
|
||||
accessToken: '',
|
||||
connectionMode: row.connectionMode,
|
||||
enabled: row.enabled,
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
remark: row.remark || '',
|
||||
selfId: row.selfId,
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitAccount() {
|
||||
if (!form.selfId.trim()) {
|
||||
message.warning('请填写 Self ID');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
id: editingId.value,
|
||||
selfId: form.selfId.trim(),
|
||||
};
|
||||
if (!payload.accessToken) delete payload.accessToken;
|
||||
await (editingId.value
|
||||
? updateQqbotAccount(payload)
|
||||
: createQqbotAccount(payload));
|
||||
message.success('账号保存成功');
|
||||
modalOpen.value = false;
|
||||
await tableApi.reload();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Account;
|
||||
if (column.key === 'connectStatus') {
|
||||
return (
|
||||
<Tag
|
||||
color={
|
||||
row.connectStatus === 'online' ? 'success' : 'default'
|
||||
}
|
||||
>
|
||||
{row.connectStatus === 'online' ? '在线' : '离线'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitAccount}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="620px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="Self ID" required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="NapCat 当前登录 QQ"
|
||||
value={form.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="账号名称">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.name = value;
|
||||
},
|
||||
}}
|
||||
placeholder="便于后台识别"
|
||||
value={form.name}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Token">
|
||||
<AInput.Password
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.accessToken = value;
|
||||
},
|
||||
}}
|
||||
placeholder={
|
||||
editingId.value ? '留空表示不修改' : 'OneBot 反向 WS token'
|
||||
}
|
||||
value={form.accessToken}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.remark = value;
|
||||
},
|
||||
}}
|
||||
value={form.remark}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
107
apps/web-antdv-next/src/views/qqbot/conversation/list.tsx
Normal file
107
apps/web-antdv-next/src/views/qqbot/conversation/list.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type { KtTableApi } from '#/components/ktTable';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'antdv-next';
|
||||
|
||||
import { getQqbotConversationList } from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import { getOptionLabel, qqbotMessageTypeOptions } from '../modules/options';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotConversationList',
|
||||
setup() {
|
||||
const columns: Array<TableColumnType<QqbotApi.Conversation>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
|
||||
{
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
title: '会话类型',
|
||||
width: 110,
|
||||
},
|
||||
{ dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 160 },
|
||||
{ dataIndex: 'targetName', key: 'targetName', title: '名称', width: 160 },
|
||||
{
|
||||
dataIndex: 'lastMessageText',
|
||||
key: 'lastMessageText',
|
||||
title: '最后消息',
|
||||
width: 360,
|
||||
},
|
||||
{
|
||||
dataIndex: 'messageCount',
|
||||
key: 'messageCount',
|
||||
title: '消息数',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastMessageTime',
|
||||
key: 'lastMessageTime',
|
||||
title: '最后时间',
|
||||
width: 190,
|
||||
},
|
||||
];
|
||||
const api: KtTableApi<QqbotApi.Conversation> = {
|
||||
list: async (params) => await getQqbotConversationList(params),
|
||||
};
|
||||
const [registerTable] = useKtTable<QqbotApi.Conversation>({
|
||||
api,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: 'Self ID' },
|
||||
fieldName: 'selfId',
|
||||
label: 'Self ID',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotMessageTypeOptions,
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '会话类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '目标 ID' },
|
||||
fieldName: 'targetId',
|
||||
label: '目标 ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions: [],
|
||||
tableTitle: '会话管理',
|
||||
});
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Conversation;
|
||||
if (column.key === 'targetType') {
|
||||
return (
|
||||
<Tag color={row.targetType === 'group' ? 'blue' : 'green'}>
|
||||
{getOptionLabel(qqbotMessageTypeOptions, row.targetType)}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
129
apps/web-antdv-next/src/views/qqbot/dashboard/list.tsx
Normal file
129
apps/web-antdv-next/src/views/qqbot/dashboard/list.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import type { DescriptionsItemType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, Col, Descriptions, Row, Statistic, Tag } from 'antdv-next';
|
||||
|
||||
import { getQqbotDashboardSummary } from '#/api/qqbot';
|
||||
|
||||
const ACard = Card as any;
|
||||
const ACol = Col as any;
|
||||
const ADescriptions = Descriptions as any;
|
||||
const ARow = Row as any;
|
||||
const AStatistic = Statistic as any;
|
||||
const ATag = Tag as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotDashboardList',
|
||||
setup() {
|
||||
const loading = ref(false);
|
||||
const summary = ref<QqbotApi.DashboardSummary>();
|
||||
|
||||
async function loadSummary() {
|
||||
loading.value = true;
|
||||
try {
|
||||
summary.value = await getQqbotDashboardSummary();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSummary);
|
||||
|
||||
return () => {
|
||||
const data = summary.value;
|
||||
const runtimeItems: DescriptionsItemType[] = [
|
||||
{
|
||||
content: (
|
||||
<ATag color={data?.runtime.enabled ? 'success' : 'default'}>
|
||||
{data?.runtime.enabled ? '已启用' : '未启用'}
|
||||
</ATag>
|
||||
),
|
||||
key: 'runtime',
|
||||
label: 'QQBot Runtime',
|
||||
},
|
||||
{
|
||||
content: data?.runtime.path || '-',
|
||||
key: 'reverseWsPath',
|
||||
label: '反向 WS 路径',
|
||||
},
|
||||
{
|
||||
content: data?.runtime.sessions?.length || 0,
|
||||
key: 'sessions',
|
||||
label: '在线会话',
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<ATag color={data?.bus.connected ? 'success' : 'default'}>
|
||||
{data?.bus.mode || 'local'} /{' '}
|
||||
{data?.bus.connected ? '已连接' : '未连接'}
|
||||
</ATag>
|
||||
),
|
||||
key: 'mqtt',
|
||||
label: 'MQTT',
|
||||
},
|
||||
{
|
||||
content: data?.conversationTotal || 0,
|
||||
key: 'conversationTotal',
|
||||
label: '会话数',
|
||||
},
|
||||
{
|
||||
content: `${data?.sendSuccessTotal || 0}/${data?.sendFailedTotal || 0}`,
|
||||
key: 'sendResult',
|
||||
label: '发送成功/失败',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page autoContentHeight>
|
||||
<div style={{ display: 'grid', gap: '16px' }}>
|
||||
<ARow gutter={[16, 16]}>
|
||||
<ACol span={6}>
|
||||
<ACard loading={loading.value}>
|
||||
<AStatistic
|
||||
title="账号总数"
|
||||
value={data?.accountTotal || 0}
|
||||
/>
|
||||
</ACard>
|
||||
</ACol>
|
||||
<ACol span={6}>
|
||||
<ACard loading={loading.value}>
|
||||
<AStatistic title="在线账号" value={data?.onlineTotal || 0} />
|
||||
</ACard>
|
||||
</ACol>
|
||||
<ACol span={6}>
|
||||
<ACard loading={loading.value}>
|
||||
<AStatistic
|
||||
title="启用规则"
|
||||
value={data?.enabledRuleTotal || 0}
|
||||
/>
|
||||
</ACard>
|
||||
</ACol>
|
||||
<ACol span={6}>
|
||||
<ACard loading={loading.value}>
|
||||
<AStatistic
|
||||
title="消息总数"
|
||||
value={data?.messageTotal || 0}
|
||||
/>
|
||||
</ACard>
|
||||
</ACol>
|
||||
</ARow>
|
||||
|
||||
<ACard loading={loading.value} title="运行状态">
|
||||
<ADescriptions
|
||||
bordered
|
||||
column={2}
|
||||
items={runtimeItems}
|
||||
size="small"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
125
apps/web-antdv-next/src/views/qqbot/message/list.tsx
Normal file
125
apps/web-antdv-next/src/views/qqbot/message/list.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type { KtTableApi } from '#/components/ktTable';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'antdv-next';
|
||||
|
||||
import { getQqbotMessageList } from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import { getOptionLabel, qqbotMessageTypeOptions } from '../modules/options';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotMessageList',
|
||||
setup() {
|
||||
const columns: Array<TableColumnType<QqbotApi.Message>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
|
||||
{
|
||||
dataIndex: 'messageType',
|
||||
key: 'messageType',
|
||||
title: '消息类型',
|
||||
width: 110,
|
||||
},
|
||||
{ dataIndex: 'direction', key: 'direction', title: '方向', width: 100 },
|
||||
{ dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 150 },
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID', width: 150 },
|
||||
{
|
||||
dataIndex: 'senderNickname',
|
||||
key: 'senderNickname',
|
||||
title: '发送人',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'messageText',
|
||||
key: 'messageText',
|
||||
title: '消息内容',
|
||||
width: 420,
|
||||
},
|
||||
{
|
||||
dataIndex: 'eventTime',
|
||||
key: 'eventTime',
|
||||
title: '消息时间',
|
||||
width: 190,
|
||||
},
|
||||
];
|
||||
const api: KtTableApi<QqbotApi.Message> = {
|
||||
list: async (params) => await getQqbotMessageList(params),
|
||||
};
|
||||
const [registerTable] = useKtTable<QqbotApi.Message>({
|
||||
api,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '关键词' },
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: 'Self ID' },
|
||||
fieldName: 'selfId',
|
||||
label: 'Self ID',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotMessageTypeOptions,
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '消息类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '目标 ID' },
|
||||
fieldName: 'targetId',
|
||||
label: '目标 ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions: [],
|
||||
tableTitle: '消息日志',
|
||||
});
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Message;
|
||||
if (column.key === 'messageType') {
|
||||
return (
|
||||
<Tag color={row.messageType === 'group' ? 'blue' : 'green'}>
|
||||
{getOptionLabel(qqbotMessageTypeOptions, row.messageType)}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'direction') {
|
||||
return (
|
||||
<Tag
|
||||
color={
|
||||
row.direction === 'inbound' ? 'default' : 'processing'
|
||||
}
|
||||
>
|
||||
{row.direction === 'inbound' ? '接收' : '发送'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
44
apps/web-antdv-next/src/views/qqbot/modules/options.ts
Normal file
44
apps/web-antdv-next/src/views/qqbot/modules/options.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export const qqbotTargetTypeOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '私聊', value: 'private' },
|
||||
{ label: '群聊', value: 'group' },
|
||||
];
|
||||
|
||||
export const qqbotMessageTypeOptions = [
|
||||
{ label: '私聊', value: 'private' },
|
||||
{ label: '群聊', value: 'group' },
|
||||
];
|
||||
|
||||
export const qqbotRuleMatchOptions = [
|
||||
{ label: '关键词包含', value: 'keyword' },
|
||||
{ label: '完全相等', value: 'equals' },
|
||||
{ label: '正则匹配', value: 'regex' },
|
||||
];
|
||||
|
||||
export const qqbotRuleTargetOptions = qqbotTargetTypeOptions;
|
||||
|
||||
const qqbotDefaultSendStatusOption = {
|
||||
color: 'default',
|
||||
label: '等待中',
|
||||
value: 'pending',
|
||||
};
|
||||
|
||||
export const qqbotSendStatusOptions = [
|
||||
qqbotDefaultSendStatusOption,
|
||||
{ color: 'success', label: '成功', value: 'success' },
|
||||
{ color: 'error', label: '失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export function getOptionLabel(
|
||||
options: Array<{ label: string; value: string }>,
|
||||
value?: string,
|
||||
) {
|
||||
return options.find((item) => item.value === value)?.label || value || '-';
|
||||
}
|
||||
|
||||
export function getSendStatusOption(status?: string) {
|
||||
return (
|
||||
qqbotSendStatusOptions.find((item) => item.value === status) ||
|
||||
qqbotDefaultSendStatusOption
|
||||
);
|
||||
}
|
||||
297
apps/web-antdv-next/src/views/qqbot/permission/list.tsx
Normal file
297
apps/web-antdv-next/src/views/qqbot/permission/list.tsx
Normal file
@ -0,0 +1,297 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type {
|
||||
KtTableApi,
|
||||
KtTableButton,
|
||||
KtTableRowAction,
|
||||
} from '#/components/ktTable';
|
||||
|
||||
import { computed, defineComponent, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'antdv-next';
|
||||
|
||||
import {
|
||||
createQqbotPermission,
|
||||
deleteQqbotPermission,
|
||||
getQqbotPermissionList,
|
||||
updateQqbotPermission,
|
||||
} from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import { getOptionLabel, qqbotTargetTypeOptions } from '../modules/options';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
const AInput = Input as any;
|
||||
const AModal = Modal as any;
|
||||
const ASelect = Select as any;
|
||||
const ASwitch = Switch as any;
|
||||
const ATabs = Tabs as any;
|
||||
|
||||
type PermissionKind = 'allowlist' | 'blocklist';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotPermissionList',
|
||||
setup() {
|
||||
const activeKind = ref<PermissionKind>('allowlist');
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const editingId = ref<string>();
|
||||
const form = reactive<QqbotApi.PermissionBody>({
|
||||
enabled: true,
|
||||
remark: '',
|
||||
selfId: '',
|
||||
targetId: '',
|
||||
targetType: 'private',
|
||||
});
|
||||
const columns: Array<TableColumnType<QqbotApi.Permission>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
|
||||
{
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
title: '目标类型',
|
||||
width: 110,
|
||||
},
|
||||
{ dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 160 },
|
||||
{ dataIndex: 'enabled', key: 'enabled', title: '状态', width: 100 },
|
||||
{ dataIndex: 'remark', key: 'remark', title: '备注', width: 260 },
|
||||
];
|
||||
const api: KtTableApi<QqbotApi.Permission> = {
|
||||
list: async (params) =>
|
||||
await getQqbotPermissionList(activeKind.value, params),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.Permission>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新增名单',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Permission:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const rowActions: Array<KtTableRowAction<QqbotApi.Permission>> = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
onClick: openEdit,
|
||||
permissionCodes: ['QqBot:Permission:Edit'],
|
||||
},
|
||||
{
|
||||
confirm: (row) =>
|
||||
`确认删除名单「${row.targetId || row.targetType}」吗?`,
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
onClick: async (row, context) => {
|
||||
await deleteQqbotPermission(activeKind.value, row.id);
|
||||
message.success('名单删除成功');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:Permission:Delete'],
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotApi.Permission>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: 'Self ID' },
|
||||
fieldName: 'selfId',
|
||||
label: 'Self ID',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotTargetTypeOptions,
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '目标类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '目标 ID' },
|
||||
fieldName: 'targetId',
|
||||
label: '目标 ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions,
|
||||
tableTitle: '权限名单',
|
||||
});
|
||||
const modalTitle = computed(
|
||||
() =>
|
||||
`${editingId.value ? '编辑' : '新增'}${activeKind.value === 'allowlist' ? '白名单' : '黑名单'}`,
|
||||
);
|
||||
|
||||
watch(activeKind, async () => {
|
||||
await tableApi.reset();
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = undefined;
|
||||
Object.assign(form, {
|
||||
enabled: true,
|
||||
remark: '',
|
||||
selfId: '',
|
||||
targetId: '',
|
||||
targetType: 'private',
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: QqbotApi.Permission) {
|
||||
editingId.value = row.id;
|
||||
Object.assign(form, { ...row });
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitPermission() {
|
||||
if (form.targetType !== 'all' && !form.targetId.trim()) {
|
||||
message.warning('请填写目标 ID');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await (editingId.value
|
||||
? updateQqbotPermission(activeKind.value, {
|
||||
...form,
|
||||
id: editingId.value,
|
||||
})
|
||||
: createQqbotPermission(activeKind.value, form));
|
||||
message.success('名单保存成功');
|
||||
modalOpen.value = false;
|
||||
await tableApi.reload();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<ATabs
|
||||
activeKey={activeKind.value}
|
||||
items={[
|
||||
{ key: 'allowlist', label: '白名单' },
|
||||
{ key: 'blocklist', label: '黑名单' },
|
||||
]}
|
||||
{...{
|
||||
'onUpdate:activeKey': (value: PermissionKind) => {
|
||||
activeKind.value = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Permission;
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'targetType') {
|
||||
return getOptionLabel(qqbotTargetTypeOptions, row.targetType);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitPermission}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="620px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="Self ID">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="留空代表全部账号"
|
||||
value={form.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标类型">
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (
|
||||
value: QqbotApi.PermissionBody['targetType'],
|
||||
) => {
|
||||
form.targetType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotTargetTypeOptions}
|
||||
value={form.targetType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标 ID">
|
||||
<AInput
|
||||
disabled={form.targetType === 'all'}
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.targetId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="私聊填 QQ 号,群聊填群号"
|
||||
value={form.targetId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.remark = value;
|
||||
},
|
||||
}}
|
||||
value={form.remark}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
349
apps/web-antdv-next/src/views/qqbot/rule/list.tsx
Normal file
349
apps/web-antdv-next/src/views/qqbot/rule/list.tsx
Normal file
@ -0,0 +1,349 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type {
|
||||
KtTableApi,
|
||||
KtTableButton,
|
||||
KtTableRowAction,
|
||||
} from '#/components/ktTable';
|
||||
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
Tag,
|
||||
TextArea,
|
||||
} from 'antdv-next';
|
||||
|
||||
import {
|
||||
createQqbotRule,
|
||||
deleteQqbotRule,
|
||||
getQqbotRuleList,
|
||||
toggleQqbotRule,
|
||||
updateQqbotRule,
|
||||
} from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import {
|
||||
getOptionLabel,
|
||||
qqbotRuleMatchOptions,
|
||||
qqbotRuleTargetOptions,
|
||||
} from '../modules/options';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
const AInput = Input as any;
|
||||
const AInputNumber = InputNumber as any;
|
||||
const AModal = Modal as any;
|
||||
const ASelect = Select as any;
|
||||
const ASwitch = Switch as any;
|
||||
const ATextArea = TextArea as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotRuleList',
|
||||
setup() {
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const editingId = ref<string>();
|
||||
const form = reactive<QqbotApi.RuleBody>({
|
||||
cooldownMs: 1500,
|
||||
enabled: true,
|
||||
keyword: '',
|
||||
matchType: 'keyword',
|
||||
name: '',
|
||||
priority: 0,
|
||||
replyContent: '',
|
||||
targetType: 'all',
|
||||
});
|
||||
|
||||
const columns: Array<TableColumnType<QqbotApi.Rule>> = [
|
||||
{ dataIndex: 'name', key: 'name', title: '规则名称', width: 180 },
|
||||
{ dataIndex: 'keyword', key: 'keyword', title: '关键词', width: 220 },
|
||||
{
|
||||
dataIndex: 'matchType',
|
||||
key: 'matchType',
|
||||
title: '匹配方式',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
title: '目标范围',
|
||||
width: 120,
|
||||
},
|
||||
{ dataIndex: 'enabled', key: 'enabled', title: '状态', width: 100 },
|
||||
{ dataIndex: 'priority', key: 'priority', title: '优先级', width: 100 },
|
||||
{
|
||||
dataIndex: 'lastHitAt',
|
||||
key: 'lastHitAt',
|
||||
title: '最后命中',
|
||||
width: 190,
|
||||
},
|
||||
];
|
||||
const api: KtTableApi<QqbotApi.Rule> = {
|
||||
list: async (params) => await getQqbotRuleList(params),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.Rule>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新建规则',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Rule:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const rowActions: Array<KtTableRowAction<QqbotApi.Rule>> = [
|
||||
{
|
||||
key: 'toggle',
|
||||
label: '启停',
|
||||
onClick: async (row, context) => {
|
||||
await toggleQqbotRule(row.id, !row.enabled);
|
||||
message.success(row.enabled ? '规则已停用' : '规则已启用');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:Rule:Toggle'],
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
onClick: openEdit,
|
||||
permissionCodes: ['QqBot:Rule:Edit'],
|
||||
},
|
||||
{
|
||||
confirm: (row) => `确认删除规则「${row.name || row.keyword}」吗?`,
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
onClick: async (row, context) => {
|
||||
await deleteQqbotRule(row.id);
|
||||
message.success('规则删除成功');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:Rule:Delete'],
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotApi.Rule>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '规则名称/关键词',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotRuleTargetOptions,
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '目标范围',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '停用', value: false },
|
||||
],
|
||||
},
|
||||
fieldName: 'enabled',
|
||||
label: '状态',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions,
|
||||
tableTitle: '自动回复规则',
|
||||
});
|
||||
const modalTitle = computed(() =>
|
||||
editingId.value ? '编辑规则' : '新建规则',
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = undefined;
|
||||
Object.assign(form, {
|
||||
cooldownMs: 1500,
|
||||
enabled: true,
|
||||
keyword: '',
|
||||
matchType: 'keyword',
|
||||
name: '',
|
||||
priority: 0,
|
||||
replyContent: '',
|
||||
targetType: 'all',
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: QqbotApi.Rule) {
|
||||
editingId.value = row.id;
|
||||
Object.assign(form, { ...row });
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitRule() {
|
||||
if (!form.keyword.trim() || !form.replyContent.trim()) {
|
||||
message.warning('请填写关键词和回复内容');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await (editingId.value
|
||||
? updateQqbotRule({ ...form, id: editingId.value })
|
||||
: createQqbotRule(form));
|
||||
message.success('规则保存成功');
|
||||
modalOpen.value = false;
|
||||
await tableApi.reload();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Rule;
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'matchType') {
|
||||
return getOptionLabel(qqbotRuleMatchOptions, row.matchType);
|
||||
}
|
||||
if (column.key === 'targetType') {
|
||||
return getOptionLabel(qqbotRuleTargetOptions, row.targetType);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitRule}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="720px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="规则名称">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.name = value;
|
||||
},
|
||||
}}
|
||||
value={form.name}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="匹配方式" required>
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (value: QqbotApi.RuleBody['matchType']) => {
|
||||
form.matchType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotRuleMatchOptions}
|
||||
value={form.matchType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="关键词" required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.keyword = value;
|
||||
},
|
||||
}}
|
||||
value={form.keyword}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标范围">
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (
|
||||
value: QqbotApi.RuleBody['targetType'],
|
||||
) => {
|
||||
form.targetType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotRuleTargetOptions}
|
||||
value={form.targetType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="回复内容" required>
|
||||
<ATextArea
|
||||
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.replyContent = value;
|
||||
},
|
||||
}}
|
||||
value={form.replyContent}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="优先级">
|
||||
<AInputNumber
|
||||
{...{
|
||||
'onUpdate:value': (value: number) => {
|
||||
form.priority = value || 0;
|
||||
},
|
||||
}}
|
||||
value={form.priority}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="冷却时间">
|
||||
<AInputNumber
|
||||
addonAfter="ms"
|
||||
min={0}
|
||||
{...{
|
||||
'onUpdate:value': (value: number) => {
|
||||
form.cooldownMs = value || 0;
|
||||
},
|
||||
}}
|
||||
value={form.cooldownMs}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
248
apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx
Normal file
248
apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotApi } from '#/api/qqbot';
|
||||
import type { KtTableApi, KtTableButton } from '#/components/ktTable';
|
||||
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Form, FormItem, Input, message, Modal, Select, Tag } from 'antdv-next';
|
||||
|
||||
import {
|
||||
getQqbotSendLogList,
|
||||
sendQqbotGroup,
|
||||
sendQqbotPrivate,
|
||||
} from '#/api/qqbot';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import {
|
||||
getOptionLabel,
|
||||
getSendStatusOption,
|
||||
qqbotMessageTypeOptions,
|
||||
qqbotSendStatusOptions,
|
||||
} from '../modules/options';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
const AInput = Input as any;
|
||||
const AModal = Modal as any;
|
||||
const ASelect = Select as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotSendLogList',
|
||||
setup() {
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const sendForm = reactive({
|
||||
message: '',
|
||||
selfId: '',
|
||||
targetId: '',
|
||||
targetType: 'private' as 'group' | 'private',
|
||||
});
|
||||
const columns: Array<TableColumnType<QqbotApi.SendLog>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
|
||||
{
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
title: '目标类型',
|
||||
width: 110,
|
||||
},
|
||||
{ dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 160 },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
||||
{
|
||||
dataIndex: 'messageText',
|
||||
key: 'messageText',
|
||||
title: '消息内容',
|
||||
width: 420,
|
||||
},
|
||||
{
|
||||
dataIndex: 'errorMessage',
|
||||
key: 'errorMessage',
|
||||
title: '错误信息',
|
||||
width: 260,
|
||||
},
|
||||
{
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
title: '发送时间',
|
||||
width: 190,
|
||||
},
|
||||
];
|
||||
const api: KtTableApi<QqbotApi.SendLog> = {
|
||||
list: async (params) => await getQqbotSendLogList(params),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.SendLog>> = [
|
||||
{
|
||||
key: 'send',
|
||||
label: '手动发送',
|
||||
onClick: openSend,
|
||||
permissionCodes: ['QqBot:Send:Private', 'QqBot:Send:Group'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotApi.SendLog>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: 'Self ID' },
|
||||
fieldName: 'selfId',
|
||||
label: 'Self ID',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotMessageTypeOptions,
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '目标类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: { allowClear: true, placeholder: '目标 ID' },
|
||||
fieldName: 'targetId',
|
||||
label: '目标 ID',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: qqbotSendStatusOptions,
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions: [],
|
||||
tableTitle: '发送日志',
|
||||
});
|
||||
const targetLabel = computed(() =>
|
||||
sendForm.targetType === 'group' ? '群号' : 'QQ 号',
|
||||
);
|
||||
|
||||
function openSend() {
|
||||
Object.assign(sendForm, {
|
||||
message: '',
|
||||
selfId: '',
|
||||
targetId: '',
|
||||
targetType: 'private',
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitSend() {
|
||||
if (!sendForm.targetId.trim() || !sendForm.message.trim()) {
|
||||
message.warning('请填写目标和消息内容');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await (sendForm.targetType === 'group'
|
||||
? sendQqbotGroup({
|
||||
groupId: sendForm.targetId,
|
||||
message: sendForm.message,
|
||||
selfId: sendForm.selfId || undefined,
|
||||
})
|
||||
: sendQqbotPrivate({
|
||||
message: sendForm.message,
|
||||
selfId: sendForm.selfId || undefined,
|
||||
userId: sendForm.targetId,
|
||||
}));
|
||||
message.success('消息已发送');
|
||||
modalOpen.value = false;
|
||||
await tableApi.reload();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.SendLog;
|
||||
if (column.key === 'targetType') {
|
||||
return getOptionLabel(qqbotMessageTypeOptions, row.targetType);
|
||||
}
|
||||
if (column.key === 'status') {
|
||||
const status = getSendStatusOption(row.status);
|
||||
return <Tag color={status.color}>{status.label}</Tag>;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitSend}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title="手动发送"
|
||||
width="620px"
|
||||
>
|
||||
<Form
|
||||
labelCol={{ span: 5 }}
|
||||
model={sendForm}
|
||||
wrapperCol={{ span: 18 }}
|
||||
>
|
||||
<FormItem label="Self ID">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="留空使用默认启用账号"
|
||||
value={sendForm.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标类型">
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (value: 'group' | 'private') => {
|
||||
sendForm.targetType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotMessageTypeOptions}
|
||||
value={sendForm.targetType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={targetLabel.value} required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.targetId = value;
|
||||
},
|
||||
}}
|
||||
value={sendForm.targetId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="消息内容" required>
|
||||
<AInput.TextArea
|
||||
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.message = value;
|
||||
},
|
||||
}}
|
||||
value={sendForm.message}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user