diff --git a/apps/web-antdv-next/src/adapter/component/index.ts b/apps/web-antdv-next/src/adapter/component/index.ts index 7bb6cda..1a9cc92 100644 --- a/apps/web-antdv-next/src/adapter/component/index.ts +++ b/apps/web-antdv-next/src/adapter/component/index.ts @@ -137,7 +137,22 @@ const withDefaultPlaceholder = ( return () => h( component, - { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, + { + ...componentProps, + placeholder, + ...props, + ...attrs, + ref: innerRef, + style: + type === 'select' + ? [ + { width: '100%' }, + componentProps.style, + props?.style, + attrs?.style, + ] + : (attrs?.style ?? props?.style ?? componentProps.style), + }, slots, ); }, diff --git a/apps/web-antdv-next/src/adapter/vxe-table.ts b/apps/web-antdv-next/src/adapter/vxe-table.ts deleted file mode 100644 index 617aa01..0000000 --- a/apps/web-antdv-next/src/adapter/vxe-table.ts +++ /dev/null @@ -1,298 +0,0 @@ -import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; -import type { Recordable } from '@vben/types'; - -import type { ComponentType } from './component'; - -import { h } from 'vue'; - -import { IconifyIcon } from '@vben/icons'; -import { $te } from '@vben/locales'; -import { - setupVbenVxeTable, - useVbenVxeGrid as useGrid, -} from '@vben/plugins/vxe-table'; -import { get, isFunction, isString } from '@vben/utils'; - -import { objectOmit } from '@vueuse/core'; -import { Button, Image, Popconfirm, Switch, Tag } from 'antdv-next'; - -import { $t } from '#/locales'; - -import { useVbenForm } from './form'; - -setupVbenVxeTable({ - configVxeTable: (vxeUI) => { - vxeUI.setConfig({ - grid: { - align: 'center', - border: false, - columnConfig: { - resizable: true, - }, - - formConfig: { - // 全局禁用vxe-table的表单配置,使用formOptions - enabled: false, - }, - minHeight: 180, - proxyConfig: { - autoLoad: true, - response: { - result: 'items', - total: 'total', - list: '', - }, - showActiveMsg: true, - showResponseMsg: false, - }, - round: true, - showOverflow: true, - size: 'small', - } as VxeTableGridOptions, - }); - - /** - * 解决vxeTable在热更新时可能会出错的问题 - */ - vxeUI.renderer.forEach((_item, key) => { - if (key.startsWith('Cell')) { - vxeUI.renderer.delete(key); - } - }); - - // 表格配置项可以用 cellRender: { name: 'CellImage' }, - vxeUI.renderer.add('CellImage', { - renderTableDefault(renderOpts, params) { - const { props } = renderOpts; - const { column, row } = params; - return h(Image, { src: row[column.field], ...props }); - }, - }); - - // 表格配置项可以用 cellRender: { name: 'CellLink' }, - vxeUI.renderer.add('CellLink', { - renderTableDefault(renderOpts) { - const { props } = renderOpts; - return h( - Button, - { size: 'small', type: 'link' }, - { default: () => props?.text }, - ); - }, - }); - - // 单元格渲染: Tag - vxeUI.renderer.add('CellTag', { - renderTableDefault({ options, props }, { column, row }) { - const value = get(row, column.field); - const tagOptions = options ?? [ - { color: 'success', label: $t('common.enabled'), value: 1 }, - { color: 'error', label: $t('common.disabled'), value: 0 }, - ]; - const tagItem = tagOptions.find((item) => item.value === value); - return h( - Tag, - { - ...props, - ...objectOmit(tagItem ?? {}, ['label']), - }, - { default: () => tagItem?.label ?? value }, - ); - }, - }); - - vxeUI.renderer.add('CellSwitch', { - renderTableDefault({ attrs, props }, { column, row }) { - const loadingKey = `__loading_${column.field}`; - const finallyProps = { - checkedChildren: $t('common.enabled'), - checkedValue: 1, - unCheckedChildren: $t('common.disabled'), - unCheckedValue: 0, - ...props, - checked: row[column.field], - loading: row[loadingKey] ?? false, - 'onUpdate:checked': onChange, - }; - async function onChange(newVal: any) { - row[loadingKey] = true; - try { - const result = await attrs?.beforeChange?.(newVal, row); - if (result !== false) { - row[column.field] = newVal; - } - } finally { - row[loadingKey] = false; - } - } - return h(Switch, finallyProps); - }, - }); - - /** - * 注册表格的操作按钮渲染器 - */ - vxeUI.renderer.add('CellOperation', { - renderTableDefault({ attrs, options, props }, { column, row }) { - const defaultProps = { size: 'small', type: 'link', ...props }; - let align = 'end'; - switch (column.align) { - case 'center': { - align = 'center'; - break; - } - case 'left': { - align = 'start'; - break; - } - default: { - align = 'end'; - break; - } - } - const presets: Recordable> = { - delete: { - danger: true, - text: $t('common.delete'), - }, - edit: { - text: $t('common.edit'), - }, - }; - const operations: Array> = ( - options || ['edit', 'delete'] - ) - .map((opt) => { - if (isString(opt)) { - return presets[opt] - ? { code: opt, ...presets[opt], ...defaultProps } - : { - code: opt, - text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt, - ...defaultProps, - }; - } else { - return { ...defaultProps, ...presets[opt.code], ...opt }; - } - }) - .map((opt) => { - const optBtn: Recordable = {}; - Object.keys(opt).forEach((key) => { - optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key]; - }); - return optBtn; - }) - .filter((opt) => opt.show !== false); - - function renderBtn(opt: Recordable, listen = true) { - return h( - Button, - { - ...props, - ...opt, - icon: undefined, - onClick: listen - ? () => - attrs?.onClick?.({ - code: opt.code, - row, - }) - : undefined, - }, - { - default: () => { - const content = []; - if (opt.icon) { - content.push( - h(IconifyIcon, { class: 'size-5', icon: opt.icon }), - ); - } - content.push(opt.text); - return content; - }, - }, - ); - } - - function renderConfirm(opt: Recordable) { - let viewportWrapper: HTMLElement | null = null; - return h( - Popconfirm, - { - /** - * 当popconfirm用在固定列中时,将固定列作为弹窗的容器时可能会因为固定列较窄而无法容纳弹窗 - * 将表格主体区域作为弹窗容器时又会因为固定列的层级较高而遮挡弹窗 - * 将body或者表格视口区域作为弹窗容器时又会导致弹窗无法跟随表格滚动。 - * 鉴于以上各种情况,一种折中的解决方案是弹出层展示时,禁止操作表格的滚动条。 - * 这样既解决了弹窗的遮挡问题,又不至于让弹窗随着表格的滚动而跑出视口区域。 - */ - getPopupContainer(el) { - viewportWrapper = el.closest('.vxe-table--viewport-wrapper'); - return document.body; - }, - placement: 'topLeft', - title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), - ...props, - ...opt, - icon: undefined, - onOpenChange: (open: boolean) => { - // 当弹窗打开时,禁止表格的滚动 - if (open) { - viewportWrapper?.style.setProperty('pointer-events', 'none'); - } else { - viewportWrapper?.style.removeProperty('pointer-events'); - } - }, - onConfirm: () => { - attrs?.onClick?.({ - code: opt.code, - row, - }); - }, - }, - { - default: () => renderBtn({ ...opt }, false), - description: () => - h( - 'div', - { class: 'truncate' }, - $t('ui.actionMessage.deleteConfirm', [ - row[attrs?.nameField || 'name'], - ]), - ), - }, - ); - } - - const btns = operations.map((opt) => - opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt), - ); - return h( - 'div', - { - class: 'flex table-operations', - style: { justifyContent: align }, - }, - btns, - ); - }, - }); - - // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化 - // vxeUI.formats.add - }, - useVbenForm, -}); - -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); - -export type OnActionClickParams> = { - code: string; - row: T; -}; -export type OnActionClickFn> = ( - params: OnActionClickParams, -) => void; -export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-antdv-next/src/api/core/menu.ts b/apps/web-antdv-next/src/api/core/menu.ts index 742a167..9df0d45 100644 --- a/apps/web-antdv-next/src/api/core/menu.ts +++ b/apps/web-antdv-next/src/api/core/menu.ts @@ -22,6 +22,12 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([ 'QqBotAccountDelete', 'QqBotAccountEdit', 'QqBotAccountKick', + 'QqBotCommand', + 'QqBotCommandCreate', + 'QqBotCommandDelete', + 'QqBotCommandEdit', + 'QqBotCommandTest', + 'QqBotCommandToggle', 'QqBotConversation', 'QqBotDashboard', 'QqBotMessage', @@ -29,6 +35,7 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([ 'QqBotPermissionCreate', 'QqBotPermissionDelete', 'QqBotPermissionEdit', + 'QqBotPlugin', 'QqBotRule', 'QqBotRuleCreate', 'QqBotRuleDelete', diff --git a/apps/web-antdv-next/src/api/qqbot/index.ts b/apps/web-antdv-next/src/api/qqbot/index.ts index 3b5461a..bce013f 100644 --- a/apps/web-antdv-next/src/api/qqbot/index.ts +++ b/apps/web-antdv-next/src/api/qqbot/index.ts @@ -159,6 +159,78 @@ export namespace QqbotApi { userId?: string; } + export interface Command { + aliases: string[]; + code: string; + cooldownMs: number; + defaultParams?: Recordable; + enabled: boolean; + errorTemplate?: string; + id: string; + lastHitAt?: string; + name: string; + operationKey: string; + parserKey: 'ff14Price' | 'plain'; + pluginKey: string; + prefixes: string[]; + priority: number; + remark?: string; + replyTemplate?: string; + targetType: 'all' | 'channel' | 'group' | 'private'; + } + + export interface CommandBody { + aliases?: string | string[]; + code: string; + cooldownMs?: number; + defaultParams?: Recordable | string; + enabled?: boolean; + errorTemplate?: string; + id?: string; + name: string; + operationKey: string; + parserKey?: 'ff14Price' | 'plain'; + pluginKey: string; + prefixes?: string | string[]; + priority?: number; + remark?: string; + replyTemplate?: string; + targetType?: 'all' | 'channel' | 'group' | 'private'; + } + + export interface CommandTestResult { + command?: Command; + input?: Recordable; + matched: boolean; + message?: string; + output?: Recordable; + replyText?: string; + } + + export interface Plugin { + description?: string; + key: string; + name: string; + operationCount: number; + version: string; + } + + export interface PluginOperation { + cacheTtlMs?: number; + description?: string; + inputSchema?: Recordable; + key: string; + name: string; + outputSchema?: Recordable; + pluginKey: string; + } + + export interface PluginHealth { + checkedAt: string; + message?: string; + status: 'degraded' | 'healthy' | 'offline'; + } + export type Query = Recordable; } @@ -339,3 +411,59 @@ export function deleteQqbotPermission( `/qqbot/permission/${kind}/delete?id=${id}`, ); } + +export function getQqbotCommandList(params: QqbotApi.Query) { + return requestClient.get>( + '/qqbot/command/list', + { params }, + ); +} + +export function createQqbotCommand(data: QqbotApi.CommandBody) { + return requestClient.post('/qqbot/command/save', data); +} + +export function updateQqbotCommand(data: QqbotApi.CommandBody) { + return requestClient.post('/qqbot/command/update', data); +} + +export function deleteQqbotCommand(id: string) { + return requestClient.post(`/qqbot/command/delete?id=${id}`); +} + +export function toggleQqbotCommand(id: string, enabled: boolean) { + return requestClient.post( + `/qqbot/command/toggle?id=${id}&enabled=${enabled}`, + ); +} + +export function testQqbotCommand(data: { + commandId?: string; + selfId?: string; + targetId?: string; + targetType?: 'channel' | 'group' | 'private'; + text: string; + userId?: string; +}) { + return requestClient.post( + '/qqbot/command/test', + data, + ); +} + +export function getQqbotPluginList() { + return requestClient.get('/qqbot/plugin/list'); +} + +export function getQqbotPluginOperationList(pluginKey?: string) { + return requestClient.get( + '/qqbot/plugin/operation/list', + { params: { pluginKey } }, + ); +} + +export function getQqbotPluginHealth(pluginKey?: string) { + return requestClient.get('/qqbot/plugin/health', { + params: { pluginKey }, + }); +} diff --git a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx index 17b57c2..51b4685 100644 --- a/apps/web-antdv-next/src/components/ktTable/KtTable.tsx +++ b/apps/web-antdv-next/src/components/ktTable/KtTable.tsx @@ -870,6 +870,7 @@ export default defineComponent({ {props.showHeader ? ( {{ + controls: () => slots.headerControls?.(context), settings: renderHeaderSettings, title: () => slots.title?.(), toolbar: renderHeaderButtons, diff --git a/apps/web-antdv-next/src/components/ktTable/components/KtTableHeader.tsx b/apps/web-antdv-next/src/components/ktTable/components/KtTableHeader.tsx index 5284431..ea714dd 100644 --- a/apps/web-antdv-next/src/components/ktTable/components/KtTableHeader.tsx +++ b/apps/web-antdv-next/src/components/ktTable/components/KtTableHeader.tsx @@ -5,19 +5,28 @@ import { Divider } from 'antdv-next'; const ADivider = Divider as any; /** - * 判断标题插槽是否只包含空节点。 + * 判断插槽是否只包含空节点。 * - * @param title 标题插槽渲染结果。 + * @param content 插槽渲染结果。 */ -function isEmptyTitleSlot(title: unknown) { +function isEmptySlot(content: unknown) { return ( - Array.isArray(title) && - title.every( + Array.isArray(content) && + content.every( (item) => item === null || (isVNode(item) && item.type === Comment), ) ); } +/** + * 解析插槽渲染结果,过滤空注释节点。 + * + * @param content 插槽渲染结果。 + */ +function resolveSlotContent(content: unknown) { + return !content || isEmptySlot(content) ? null : content; +} + export default defineComponent({ name: 'KtTableHeader', props: { @@ -31,30 +40,50 @@ export default defineComponent({ * * @param props 表格头部 props,目前用于接收默认标题。 * @param slots Vue setup context。 - * @param slots.slots 头部标题、按钮和设置区插槽。 + * @param slots.slots 头部标题、控制区、按钮和设置区插槽。 */ setup(props, { slots }) { return () => { - const slotTitle = slots.title?.(); - const title = - !slotTitle || isEmptyTitleSlot(slotTitle) ? props.title : slotTitle; - const toolbar = slots.toolbar?.(); - const settings = slots.settings?.(); + const slotTitle = resolveSlotContent(slots.title?.()); + const title = slotTitle || props.title; + const controls = resolveSlotContent(slots.controls?.()); + const toolbar = resolveSlotContent(slots.toolbar?.()); + const settings = resolveSlotContent(slots.settings?.()); - if (!title && !toolbar && !settings) return null; + if (!title && !controls && !toolbar && !settings) return null; return (
-
-
{title}
-
{toolbar}
-
- {settings ? ( -
- - {settings} +
+
+ {title ? ( +
+
{title}
+
+ ) : null} + {controls ? ( +
{controls}
+ ) : null}
- ) : null} + {toolbar || settings ? ( +
+ {toolbar ? ( +
{toolbar}
+ ) : null} + {settings ? ( +
+ {toolbar ? ( + + ) : null} + {settings} +
+ ) : null} +
+ ) : null} +
); }; diff --git a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx index 89a562c..a1e2276 100644 --- a/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx +++ b/apps/web-antdv-next/src/components/ktTable/components/KtTableResizableTitle.tsx @@ -1,6 +1,6 @@ import type { PropType } from 'vue'; -import { defineComponent, h, onBeforeUnmount, ref } from 'vue'; +import { defineComponent, onBeforeUnmount, ref } from 'vue'; export interface KtTableResizeInfo { size: { @@ -13,6 +13,10 @@ type KtTableResizableTitleProps = { width?: number; }; +type KtTableClickHandler = + | ((event: MouseEvent) => void) + | Array<(event: MouseEvent) => void>; + export default defineComponent({ name: 'KtTableResizableTitle', inheritAttrs: false, @@ -164,12 +168,32 @@ export default defineComponent({ } /** - * 阻止拖拽后的冒泡点击,避免误触发表头排序。 + * 触发表头原始点击事件。 * - * @param event 表头捕获阶段点击事件。 + * @param event 表头点击事件。 */ - function handleClickCapture(event: MouseEvent) { - if (!stopNextClick.value) return; + function runOriginalClick(event: MouseEvent) { + const handler = attrs.onClick as KtTableClickHandler | undefined; + if (!handler) return; + + if (Array.isArray(handler)) { + handler.forEach((item) => item(event)); + return; + } + + handler(event); + } + + /** + * 处理表头点击,拖拽结束后的点击会被阻止,避免误触发表头排序。 + * + * @param event 表头点击事件。 + */ + function handleHeaderClick(event: MouseEvent) { + if (!stopNextClick.value) { + runOriginalClick(event); + return; + } event.stopPropagation(); event.preventDefault(); @@ -183,41 +207,39 @@ export default defineComponent({ return () => { if (!props.width) { - return h('th', attrs, slots.default?.()); + return {slots.default?.()}; } if (!props.onResize) { - return h( - 'th', - { - ...attrs, - style: { + return ( + | undefined), width: `${props.width}px`, - }, - }, - slots.default?.(), + }} + > + {slots.default?.()} + ); } - return h( - 'th', - { - ...attrs, - class: ['kt-table__resizable-title', attrs.class], - onClickCapture: handleClickCapture, - style: { + return ( + | undefined), width: `${props.width}px`, - }, - }, - [ - slots.default?.(), - h('span', { - class: 'kt-table__resizable-handle', - onMousedown: handleMouseDown, - }), - ], + }} + > + {slots.default?.()} + + ); }; }, diff --git a/apps/web-antdv-next/src/components/ktTable/config/constants.ts b/apps/web-antdv-next/src/components/ktTable/config/constants.ts index 623f4ea..db96963 100644 --- a/apps/web-antdv-next/src/components/ktTable/config/constants.ts +++ b/apps/web-antdv-next/src/components/ktTable/config/constants.ts @@ -12,7 +12,7 @@ export const KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT = 140; export const KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT = 40; -export const KT_TABLE_ROW_ACTION_VISIBLE_COUNT = 2; +export const KT_TABLE_ROW_ACTION_VISIBLE_COUNT = 1; export const KT_TABLE_DEFAULT_PAGE_SIZE = 10; diff --git a/apps/web-antdv-next/src/components/ktTable/styles/header.scss b/apps/web-antdv-next/src/components/ktTable/styles/header.scss index 59ee4ab..136c7b9 100644 --- a/apps/web-antdv-next/src/components/ktTable/styles/header.scss +++ b/apps/web-antdv-next/src/components/ktTable/styles/header.scss @@ -2,20 +2,31 @@ @include kt.block { &__header { - display: flex; + display: block; flex-shrink: 0; - gap: 12px; - align-items: center; - justify-content: space-between; margin-bottom: 12px; } - &__header-align { + &__header-layout { display: flex; flex: 1 1 0; gap: 12px; + align-items: flex-end; + justify-content: space-between; + min-width: 0; + } + + &__header-content { + display: grid; + flex: 1 1 auto; + gap: 8px; + min-width: 0; + } + + &__header-title-row { + display: flex; + gap: 12px; align-items: center; - justify-content: flex-start; min-width: 0; } @@ -25,6 +36,56 @@ font-weight: 500; } + &__header-controls { + display: grid; + gap: 6px; + align-items: start; + min-width: 0; + } + + &__header-control-group { + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + gap: 8px 12px; + align-items: center; + min-width: 0; + } + + &__header-control-group--grow { + flex: 0 1 auto; + } + + &__header-control-label { + flex: 0 0 auto; + font-weight: 500; + line-height: 32px; + } + + &__header-control-muted { + flex: 0 0 auto; + line-height: 32px; + color: hsl(var(--muted-foreground)); + } + + &__header-tabs { + min-width: 0; + + .ant-tabs-nav { + margin: 0; + } + } + + &__header-actions { + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 8px; + align-items: center; + justify-content: flex-end; + min-height: 32px; + } + &__header-button { display: flex; flex-shrink: 0; diff --git a/apps/web-antdv-next/src/locales/langs/en-US/examples.json b/apps/web-antdv-next/src/locales/langs/en-US/examples.json index 0503548..a6230fb 100644 --- a/apps/web-antdv-next/src/locales/langs/en-US/examples.json +++ b/apps/web-antdv-next/src/locales/langs/en-US/examples.json @@ -26,18 +26,6 @@ "crop-image": "Crop image", "upload-image": "Click to upload image" }, - "vxeTable": { - "title": "Vxe Table", - "basic": "Basic Table", - "remote": "Remote Load", - "tree": "Tree Table", - "fixed": "Fixed Header/Column", - "virtual": "Virtual Scroll", - "editCell": "Edit Cell", - "editRow": "Edit Row", - "custom-cell": "Custom Cell", - "form": "Form Table" - }, "captcha": { "title": "Captcha", "pointSelection": "Point Selection Captcha", diff --git a/apps/web-antdv-next/src/locales/langs/zh-CN/examples.json b/apps/web-antdv-next/src/locales/langs/zh-CN/examples.json index 3b0d934..7c2eb4a 100644 --- a/apps/web-antdv-next/src/locales/langs/zh-CN/examples.json +++ b/apps/web-antdv-next/src/locales/langs/zh-CN/examples.json @@ -29,18 +29,6 @@ "crop-image": "裁剪图片", "upload-image": "点击上传图片" }, - "vxeTable": { - "title": "Vxe 表格", - "basic": "基础表格", - "remote": "远程加载", - "tree": "树形表格", - "fixed": "固定表头/列", - "virtual": "虚拟滚动", - "editCell": "单元格编辑", - "editRow": "行编辑", - "custom-cell": "自定义单元格", - "form": "搜索表单" - }, "captcha": { "title": "验证码", "pointSelection": "点选验证", diff --git a/apps/web-antdv-next/src/router/routes/modules/examples.ts b/apps/web-antdv-next/src/router/routes/modules/examples.ts index 017b2c2..f3687cb 100644 --- a/apps/web-antdv-next/src/router/routes/modules/examples.ts +++ b/apps/web-antdv-next/src/router/routes/modules/examples.ts @@ -96,89 +96,6 @@ const routes: RouteRecordRaw[] = [ }, ], }, - { - name: 'VxeTableExample', - path: '/examples/vxe-table', - meta: { - icon: 'lucide:table', - title: $t('examples.vxeTable.title'), - }, - children: [ - { - name: 'VxeTableBasicExample', - path: '/examples/vxe-table/basic', - component: () => import('#/views/examples/vxe-table/basic.vue'), - meta: { - title: $t('examples.vxeTable.basic'), - }, - }, - { - name: 'VxeTableRemoteExample', - path: '/examples/vxe-table/remote', - component: () => import('#/views/examples/vxe-table/remote.vue'), - meta: { - title: $t('examples.vxeTable.remote'), - }, - }, - { - name: 'VxeTableTreeExample', - path: '/examples/vxe-table/tree', - component: () => import('#/views/examples/vxe-table/tree.vue'), - meta: { - title: $t('examples.vxeTable.tree'), - }, - }, - { - name: 'VxeTableFixedExample', - path: '/examples/vxe-table/fixed', - component: () => import('#/views/examples/vxe-table/fixed.vue'), - meta: { - title: $t('examples.vxeTable.fixed'), - }, - }, - { - name: 'VxeTableCustomCellExample', - path: '/examples/vxe-table/custom-cell', - component: () => - import('#/views/examples/vxe-table/custom-cell.vue'), - meta: { - title: $t('examples.vxeTable.custom-cell'), - }, - }, - { - name: 'VxeTableFormExample', - path: '/examples/vxe-table/form', - component: () => import('#/views/examples/vxe-table/form.vue'), - meta: { - title: $t('examples.vxeTable.form'), - }, - }, - { - name: 'VxeTableEditCellExample', - path: '/examples/vxe-table/edit-cell', - component: () => import('#/views/examples/vxe-table/edit-cell.vue'), - meta: { - title: $t('examples.vxeTable.editCell'), - }, - }, - { - name: 'VxeTableEditRowExample', - path: '/examples/vxe-table/edit-row', - component: () => import('#/views/examples/vxe-table/edit-row.vue'), - meta: { - title: $t('examples.vxeTable.editRow'), - }, - }, - { - name: 'VxeTableVirtualExample', - path: '/examples/vxe-table/virtual', - component: () => import('#/views/examples/vxe-table/virtual.vue'), - meta: { - title: $t('examples.vxeTable.virtual'), - }, - }, - ], - }, { name: 'CaptchaExample', path: '/examples/captcha', diff --git a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts index 8a24dd8..2ddbf43 100644 --- a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts +++ b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts @@ -38,6 +38,24 @@ const routes: RouteRecordRaw[] = [ name: 'QqBotRule', path: '/qqbot/rule', }, + { + component: () => import('#/views/qqbot/command/list'), + meta: { + icon: 'lucide:square-terminal', + title: '在线命令', + }, + name: 'QqBotCommand', + path: '/qqbot/command', + }, + { + component: () => import('#/views/qqbot/plugin/list'), + meta: { + icon: 'lucide:plug', + title: '插件能力', + }, + name: 'QqBotPlugin', + path: '/qqbot/plugin', + }, { component: () => import('#/views/qqbot/conversation/list'), meta: { diff --git a/apps/web-antdv-next/src/views/blog/article/list.tsx b/apps/web-antdv-next/src/views/blog/article/list.tsx index f47af6e..01ed3cc 100644 --- a/apps/web-antdv-next/src/views/blog/article/list.tsx +++ b/apps/web-antdv-next/src/views/blog/article/list.tsx @@ -8,30 +8,14 @@ import type { KtTableRowAction, } from '#/components/ktTable'; -import { - computed, - defineComponent, - onActivated, - onMounted, - reactive, - ref, -} from 'vue'; +import { computed, defineComponent, onActivated, onMounted, ref } from 'vue'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; -import { - Form, - FormItem, - Input, - message, - Modal, - Select, - Switch, - Tag, - TextArea, -} from 'antdv-next'; +import { message, Tag } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { createArticle, deleteArticle, @@ -57,11 +41,6 @@ type ArticleSearchValues = { }; 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 ATextArea = TextArea as any; const articleStatusOptions = [ { color: 'success', label: '已发布', value: 'publish' }, @@ -73,26 +52,106 @@ const articleStatusOptions = [ export default defineComponent({ name: 'BlogArticleList', setup() { - const saving = ref(false); - const modalOpen = ref(false); const editingId = ref(); const categoryOptions = ref([]); const tagOptions = ref([]); - const form = reactive({ - categories: [], - content: '', - excerpt: '', - slug: '', - status: 'draft', - sticky: false, - tags: [], - title: '', + const [ArticleForm, articleFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-20', + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { + placeholder: '请输入文章标题', + }, + fieldName: 'title', + label: '标题', + rules: 'required', + }, + { + component: 'Select', + componentProps: { + options: articleStatusOptions, + }, + fieldName: 'status', + label: '状态', + }, + { + component: 'Input', + componentProps: { + placeholder: '可选,WordPress slug', + }, + fieldName: 'slug', + label: '别名', + }, + { + component: 'Select', + componentProps: () => ({ + mode: 'multiple', + options: categoryOptions.value, + placeholder: '选择分类', + }), + fieldName: 'categories', + label: '分类', + }, + { + component: 'Select', + componentProps: () => ({ + mode: 'multiple', + options: tagOptions.value, + placeholder: '选择标签', + }), + fieldName: 'tags', + label: '标签', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 4, minRows: 2 }, + placeholder: '可选,文章摘要', + }, + fieldName: 'excerpt', + label: '摘要', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 12, minRows: 6 }, + placeholder: '支持 HTML 内容', + }, + fieldName: 'content', + label: '内容', + }, + { + component: 'Switch', + fieldName: 'sticky', + label: '置顶', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const modalTitle = computed(() => editingId.value ? '编辑文章' : '新建文章', ); + const [ArticleModal, articleModalApi] = useVbenModal({ + class: 'w-[760px]', + fullscreenButton: false, + async onConfirm() { + await submitArticle(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = articleModalApi.getData<{ + values?: WordpressBlogApi.ArticleBody; + }>(); + void resetArticleForm(values || getArticleFormDefaults()); + }, + }); const columns: Array> = [ { dataIndex: 'title', key: 'title', title: '标题', width: 280 }, { dataIndex: 'status', key: 'status', title: '状态', width: 110 }, @@ -310,15 +369,10 @@ export default defineComponent({ await tableApi.search(); } - async function openCreate( - context?: KtTableContext, - ) { - const searchValues = context - ? await context.getSearchValues() - : await tableApi.getSearchValues(); - - editingId.value = undefined; - Object.assign(form, { + function getArticleFormDefaults( + searchValues: ArticleSearchValues = {}, + ): WordpressBlogApi.ArticleBody { + return { categories: [...(searchValues.categories || [])], content: '', excerpt: '', @@ -327,47 +381,74 @@ export default defineComponent({ sticky: false, tags: [...(searchValues.tags || [])], title: '', - }); - modalOpen.value = true; + }; + } + + async function resetArticleForm(values: WordpressBlogApi.ArticleBody) { + await articleFormApi.resetForm(); + await articleFormApi.setValues(values); + await articleFormApi.resetValidate(); + } + + async function openCreate( + context?: KtTableContext, + ) { + const searchValues = context + ? await context.getSearchValues() + : await tableApi.getSearchValues(); + + editingId.value = undefined; + articleModalApi + .setData({ values: getArticleFormDefaults(searchValues) }) + .open(); } function openEdit(row: WordpressBlogApi.Article) { editingId.value = row.id; - Object.assign(form, { - categories: row.categories || [], - content: getRenderedText(row.content), - excerpt: getRenderedText(row.excerpt), - id: row.id, - slug: row.slug || '', - status: row.status || 'draft', - sticky: !!row.sticky, - tags: row.tags || [], - title: getRenderedText(row.title), - }); - modalOpen.value = true; + articleModalApi + .setData({ + values: { + categories: row.categories || [], + content: getRenderedText(row.content), + excerpt: getRenderedText(row.excerpt), + id: row.id, + slug: row.slug || '', + status: row.status || 'draft', + sticky: !!row.sticky, + tags: row.tags || [], + title: getRenderedText(row.title), + }, + }) + .open(); } async function submitArticle() { - if (!form.title.trim()) { + const { valid } = await articleFormApi.validate(); + if (!valid) return; + + const values = + await articleFormApi.getValues(); + const title = values.title?.trim(); + if (!title) { message.warning('请填写文章标题'); return; } - saving.value = true; + articleModalApi.lock(); try { const payload = { - ...form, + ...values, id: editingId.value, - title: form.title.trim(), + title, }; await (editingId.value ? updateArticle(payload) : createArticle(payload)); message.success('文章保存成功'); - modalOpen.value = false; + await articleModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + articleModalApi.unlock(); } } @@ -457,96 +538,9 @@ export default defineComponent({ }} /> - { - modalOpen.value = value; - }} - open={modalOpen.value} - title={modalTitle.value} - width="760px" - > -
- - { - form.title = value; - }} - placeholder="请输入文章标题" - value={form.title} - /> - - - { - form.status = value; - }} - options={articleStatusOptions} - value={form.status} - /> - - - { - form.slug = value; - }} - placeholder="可选,WordPress slug" - value={form.slug} - /> - - - { - form.categories = value; - }} - options={categoryOptions.value} - placeholder="选择分类" - value={form.categories} - /> - - - { - form.tags = value; - }} - options={tagOptions.value} - placeholder="选择标签" - value={form.tags} - /> - - - { - form.excerpt = value; - }} - placeholder="可选,文章摘要" - value={form.excerpt} - /> - - - { - form.content = value; - }} - placeholder="支持 HTML 内容" - value={form.content} - /> - - - { - form.sticky = value; - }} - /> - -
-
+ + + ); }, diff --git a/apps/web-antdv-next/src/views/blog/modules/term-management.tsx b/apps/web-antdv-next/src/views/blog/modules/term-management.tsx index 8e28b08..727dbb5 100644 --- a/apps/web-antdv-next/src/views/blog/modules/term-management.tsx +++ b/apps/web-antdv-next/src/views/blog/modules/term-management.tsx @@ -9,29 +9,15 @@ import type { KtTableRowAction, } from '#/components/ktTable'; -import { - computed, - defineComponent, - onMounted, - reactive, - ref, - watch, -} from 'vue'; +import { computed, defineComponent, onMounted, ref, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; -import { - Form, - FormItem, - Input, - message, - Modal, - Select, - TextArea, -} from 'antdv-next'; +import { message } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { createCategory, createTag, @@ -51,10 +37,6 @@ type TermSearchValues = { }; const AKtTable = KtTable as any; -const AInput = Input as any; -const AModal = Modal as any; -const ASelect = Select as any; -const ATextArea = TextArea as any; export default defineComponent({ name: 'BlogTermManagement', @@ -72,21 +54,82 @@ export default defineComponent({ const route = useRoute(); const router = useRouter(); - const saving = ref(false); - const modalOpen = ref(false); const editingId = ref(); const tableRows = ref([]); + const parentOptions = computed(() => + tableRows.value + .filter((item) => item.id !== editingId.value) + .map((item) => ({ label: item.name, value: item.id })), + ); - const form = reactive({ - description: '', - name: '', - parent: undefined, - slug: '', + const [TermForm, termFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: () => ({ + placeholder: `请输入${props.title}名称`, + }), + fieldName: 'name', + label: '名称', + rules: 'required', + }, + { + component: 'Input', + componentProps: { + placeholder: '可选,WordPress slug', + }, + fieldName: 'slug', + label: '别名', + }, + { + component: 'Select', + componentProps: () => ({ + allowClear: true, + options: parentOptions.value, + placeholder: '选择父级分类', + }), + dependencies: { + if: () => props.kind === 'category', + triggerFields: ['name'], + }, + fieldName: 'parent', + label: '父级分类', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 6, minRows: 3 }, + placeholder: '可选', + }, + fieldName: 'description', + label: '描述', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const modalTitle = computed(() => editingId.value ? `编辑${props.title}` : `新建${props.title}`, ); + const [TermModal, termModalApi] = useVbenModal({ + class: 'w-[620px]', + fullscreenButton: false, + async onConfirm() { + await submitTerm(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = termModalApi.getData<{ + values?: WordpressBlogApi.TermBody; + }>(); + void resetTermForm(values || getTermFormDefaults()); + }, + }); const permissionModule = computed(() => props.kind === 'category' ? 'Blog:Category' : 'Blog:Tag', ); @@ -103,12 +146,6 @@ export default defineComponent({ }, ], ); - const parentOptions = computed(() => - tableRows.value - .filter((item) => item.id !== editingId.value) - .map((item) => ({ label: item.name, value: item.id })), - ); - const api: KtTableApi = { list: async (params) => { const requestParams = { @@ -225,41 +262,58 @@ export default defineComponent({ }); } - function openCreate() { - editingId.value = undefined; - Object.assign(form, { + function getTermFormDefaults(): WordpressBlogApi.TermBody { + return { description: '', name: '', parent: undefined, slug: '', - }); - modalOpen.value = true; + }; + } + + async function resetTermForm(values: WordpressBlogApi.TermBody) { + await termFormApi.resetForm(); + await termFormApi.setValues(values); + await termFormApi.resetValidate(); + } + + function openCreate() { + editingId.value = undefined; + termModalApi.setData({ values: getTermFormDefaults() }).open(); } function openEdit(row: WordpressBlogApi.Term) { editingId.value = row.id; - Object.assign(form, { - description: row.description || '', - id: row.id, - name: row.name, - parent: row.parent || undefined, - slug: row.slug || '', - }); - modalOpen.value = true; + termModalApi + .setData({ + values: { + description: row.description || '', + id: row.id, + name: row.name, + parent: row.parent || undefined, + slug: row.slug || '', + }, + }) + .open(); } async function submitTerm() { - if (!form.name.trim()) { + const { valid } = await termFormApi.validate(); + if (!valid) return; + + const values = await termFormApi.getValues(); + const name = values.name?.trim(); + if (!name) { message.warning(`请填写${props.title}名称`); return; } - saving.value = true; + termModalApi.lock(); try { const payload = { - ...form, + ...values, id: editingId.value, - name: form.name.trim(), + name, }; if (props.kind === 'category') { await (editingId.value @@ -269,10 +323,10 @@ export default defineComponent({ await (editingId.value ? updateTag(payload) : createTag(payload)); } message.success(`${props.title}保存成功`); - modalOpen.value = false; + await termModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + termModalApi.unlock(); } } @@ -323,60 +377,9 @@ export default defineComponent({ }} /> - { - modalOpen.value = value; - }} - open={modalOpen.value} - title={modalTitle.value} - width="620px" - > -
- - { - form.name = value; - }} - placeholder={`请输入${props.title}名称`} - value={form.name} - /> - - - { - form.slug = value; - }} - placeholder="可选,WordPress slug" - value={form.slug} - /> - - {props.kind === 'category' ? ( - - { - form.parent = value; - }} - options={parentOptions.value} - placeholder="选择父级分类" - value={form.parent} - /> - - ) : null} - - { - form.description = value; - }} - placeholder="可选" - value={form.description} - /> - -
-
+ + + ); }, diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/basic.vue b/apps/web-antdv-next/src/views/examples/vxe-table/basic.vue deleted file mode 100644 index 3f9630e..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/basic.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/custom-cell.vue b/apps/web-antdv-next/src/views/examples/vxe-table/custom-cell.vue deleted file mode 100644 index 95b015a..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/custom-cell.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/edit-cell.vue b/apps/web-antdv-next/src/views/examples/vxe-table/edit-cell.vue deleted file mode 100644 index 9aebde8..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/edit-cell.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/edit-row.vue b/apps/web-antdv-next/src/views/examples/vxe-table/edit-row.vue deleted file mode 100644 index 31ebadf..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/edit-row.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/fixed.vue b/apps/web-antdv-next/src/views/examples/vxe-table/fixed.vue deleted file mode 100644 index a979132..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/fixed.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/form.vue b/apps/web-antdv-next/src/views/examples/vxe-table/form.vue deleted file mode 100644 index ec9e4b5..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/form.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/remote.vue b/apps/web-antdv-next/src/views/examples/vxe-table/remote.vue deleted file mode 100644 index de2faf8..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/remote.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/table-data.ts b/apps/web-antdv-next/src/views/examples/vxe-table/table-data.ts deleted file mode 100644 index b4eb5ed..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/table-data.ts +++ /dev/null @@ -1,172 +0,0 @@ -interface TableRowData { - address: string; - age: number; - id: number; - name: string; - nickname: string; - role: string; -} - -const roles = ['User', 'Admin', 'Manager', 'Guest']; - -export const MOCK_TABLE_DATA: TableRowData[] = (() => { - const data: TableRowData[] = []; - for (let i = 0; i < 40; i++) { - data.push({ - address: `New York${i}`, - age: i + 1, - id: i, - name: `Test${i}`, - nickname: `Test${i}`, - role: roles[Math.floor(Math.random() * roles.length)] as string, - }); - } - return data; -})(); - -export const MOCK_TREE_TABLE_DATA = [ - { - date: '2020-08-01', - id: 10_000, - name: 'Test1', - parentId: null, - size: 1024, - type: 'mp3', - }, - { - date: '2021-04-01', - id: 10_050, - name: 'Test2', - parentId: null, - size: 0, - type: 'mp4', - }, - { - date: '2020-03-01', - id: 24_300, - name: 'Test3', - parentId: 10_050, - size: 1024, - type: 'avi', - }, - { - date: '2021-04-01', - id: 20_045, - name: 'Test4', - parentId: 24_300, - size: 600, - type: 'html', - }, - { - date: '2021-04-01', - id: 10_053, - name: 'Test5', - parentId: 24_300, - size: 0, - type: 'avi', - }, - { - date: '2021-10-01', - id: 24_330, - name: 'Test6', - parentId: 10_053, - size: 25, - type: 'txt', - }, - { - date: '2020-01-01', - id: 21_011, - name: 'Test7', - parentId: 10_053, - size: 512, - type: 'pdf', - }, - { - date: '2021-06-01', - id: 22_200, - name: 'Test8', - parentId: 10_053, - size: 1024, - type: 'js', - }, - { - date: '2020-11-01', - id: 23_666, - name: 'Test9', - parentId: null, - size: 2048, - type: 'xlsx', - }, - { - date: '2021-06-01', - id: 23_677, - name: 'Test10', - parentId: 23_666, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 23_671, - name: 'Test11', - parentId: 23_677, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 23_672, - name: 'Test12', - parentId: 23_677, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 23_688, - name: 'Test13', - parentId: 23_666, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 23_681, - name: 'Test14', - parentId: 23_688, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 23_682, - name: 'Test15', - parentId: 23_688, - size: 1024, - type: 'js', - }, - { - date: '2020-10-01', - id: 24_555, - name: 'Test16', - parentId: null, - size: 224, - type: 'avi', - }, - { - date: '2021-06-01', - id: 24_566, - name: 'Test17', - parentId: 24_555, - size: 1024, - type: 'js', - }, - { - date: '2021-06-01', - id: 24_577, - name: 'Test18', - parentId: 24_555, - size: 1024, - type: 'js', - }, -]; diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/tree.vue b/apps/web-antdv-next/src/views/examples/vxe-table/tree.vue deleted file mode 100644 index 1a28dee..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/tree.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/examples/vxe-table/virtual.vue b/apps/web-antdv-next/src/views/examples/vxe-table/virtual.vue deleted file mode 100644 index f35a691..0000000 --- a/apps/web-antdv-next/src/views/examples/vxe-table/virtual.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/apps/web-antdv-next/src/views/qqbot/account/list.tsx b/apps/web-antdv-next/src/views/qqbot/account/list.tsx index 755bca2..c1173ac 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/list.tsx @@ -9,24 +9,13 @@ import type { import { computed, defineComponent, onBeforeUnmount, reactive, ref } from 'vue'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; import { useQRCode } from '@vueuse/integrations/useQRCode'; -import { - Alert, - Button, - Form, - FormItem, - Input, - message, - Modal, - Space, - Switch, - Tag, - Typography, -} from 'antdv-next'; +import { Alert, Button, message, Space, Tag, Typography } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { cancelQqbotAccountScan, createQqbotAccount, @@ -43,19 +32,13 @@ import { KtTable, useKtTable } from '#/components/ktTable'; const AKtTable = KtTable as any; const AButton = Button as any; -const AInput = Input as any; -const AModal = Modal as any; -const ASwitch = Switch as any; const ATypographyLink = Typography.Link as any; export default defineComponent({ name: 'QqBotAccountList', setup() { - const saving = ref(false); - const modalOpen = ref(false); const editingId = ref(); const scanLoading = ref(false); - const scanModalOpen = ref(false); const scanQrcodeText = ref(''); const scanState = reactive<{ containerId?: string; @@ -77,13 +60,53 @@ export default defineComponent({ scale: 8, }); let scanTimer: number | undefined; - const form = reactive({ - accessToken: '', - connectionMode: 'reverse-ws', - enabled: true, - name: '', - remark: '', - selfId: '', + + const [AccountForm, accountFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { + placeholder: 'NapCat 当前登录 QQ', + }, + fieldName: 'selfId', + label: 'Self ID', + rules: 'required', + }, + { + component: 'Input', + componentProps: { + placeholder: '便于后台识别', + }, + fieldName: 'name', + label: '账号名称', + }, + { + component: 'InputPassword', + componentProps: () => ({ + placeholder: editingId.value + ? '留空表示不修改' + : 'OneBot 反向 WS token', + }), + fieldName: 'accessToken', + label: 'Token', + }, + { + component: 'Switch', + fieldName: 'enabled', + label: '启用', + }, + { + component: 'Input', + fieldName: 'remark', + label: '备注', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const columns: Array> = [ @@ -218,6 +241,33 @@ export default defineComponent({ scanState.mode === 'refresh' ? '更新账号登录' : '扫码新增账号', ); + const [ScanModal, scanModalApi] = useVbenModal({ + class: 'w-[520px]', + fullscreenButton: false, + onBeforeClose() { + cleanupScanSession(); + return true; + }, + onCancel() { + closeScanModal(); + }, + }); + + const [AccountModal, accountModalApi] = useVbenModal({ + class: 'w-[620px]', + fullscreenButton: false, + async onConfirm() { + await submitAccount(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = accountModalApi.getData<{ + values?: QqbotApi.AccountBody; + }>(); + void resetAccountForm(values || getAccountFormDefaults()); + }, + }); + onBeforeUnmount(() => { stopScanPolling(); }); @@ -235,7 +285,7 @@ export default defineComponent({ row?: QqbotApi.Account, ) { resetScanState(mode); - scanModalOpen.value = true; + scanModalApi.setState({ title: scanTitle.value }).open(); scanLoading.value = true; try { if (mode === 'create') { @@ -277,7 +327,7 @@ export default defineComponent({ message.success( result.selfId ? `账号 ${result.selfId} 登录态已更新` : '账号已更新', ); - scanModalOpen.value = false; + await scanModalApi.close(); await tableApi.reload(); } } @@ -335,15 +385,18 @@ export default defineComponent({ scanQrcodeText.value = ''; } - function closeScanModal() { + function cleanupScanSession() { const sessionId = scanState.sessionId; stopScanPolling(); - scanModalOpen.value = false; if (sessionId && scanState.status === 'pending') { void cancelQqbotAccountScan(sessionId); } } + function closeScanModal() { + void scanModalApi.close(); + } + function getScanAlertType() { if (scanState.status === 'success') return 'success'; if (scanState.status === 'error') return 'error'; @@ -371,55 +424,72 @@ export default defineComponent({ return '扫码登录请求失败'; } - function openCreate() { - editingId.value = undefined; - Object.assign(form, { + function getAccountFormDefaults(): QqbotApi.AccountBody { + return { accessToken: '', connectionMode: 'reverse-ws', enabled: true, name: '', remark: '', selfId: '', - }); - modalOpen.value = true; + }; + } + + async function resetAccountForm(values: QqbotApi.AccountBody) { + await accountFormApi.resetForm(); + await accountFormApi.setValues(values); + await accountFormApi.resetValidate(); + } + + function openCreate() { + editingId.value = undefined; + accountModalApi.setData({ values: getAccountFormDefaults() }).open(); } 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; + accountModalApi + .setData({ + values: { + accessToken: '', + connectionMode: row.connectionMode, + enabled: row.enabled, + id: row.id, + name: row.name, + remark: row.remark || '', + selfId: row.selfId, + }, + }) + .open(); } async function submitAccount() { - if (!form.selfId.trim()) { + const { valid } = await accountFormApi.validate(); + if (!valid) return; + + const values = await accountFormApi.getValues(); + const selfId = values.selfId?.trim(); + if (!selfId) { message.warning('请填写 Self ID'); return; } - saving.value = true; + accountModalApi.lock(); try { const payload = { - ...form, + ...values, id: editingId.value, - selfId: form.selfId.trim(), + selfId, }; if (!payload.accessToken) delete payload.accessToken; await (editingId.value ? updateQqbotAccount(payload) : createQqbotAccount(payload)); message.success('账号保存成功'); - modalOpen.value = false; + await accountModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + accountModalApi.unlock(); } } @@ -445,43 +515,32 @@ export default defineComponent({ }, }} /> - - 关闭 - , - - 刷新二维码 - , - - 检查状态 - , - ]} - onCancel={closeScanModal} - {...{ - 'onUpdate:open': (value: boolean) => { - if (value) { - scanModalOpen.value = value; - return; - } - closeScanModal(); - }, - }} - open={scanModalOpen.value} + [ + + 关闭 + , + + 刷新二维码 + , + + 检查状态 + , + ], + }} > ) : null} - - { - modalOpen.value = value; - }, - }} - open={modalOpen.value} - title={modalTitle.value} - width="620px" - > -
- - { - form.selfId = value; - }, - }} - placeholder="NapCat 当前登录 QQ" - value={form.selfId} - /> - - - { - form.name = value; - }, - }} - placeholder="便于后台识别" - value={form.name} - /> - - - { - form.accessToken = value; - }, - }} - placeholder={ - editingId.value ? '留空表示不修改' : 'OneBot 反向 WS token' - } - value={form.accessToken} - /> - - - { - form.enabled = value; - }, - }} - /> - - - { - form.remark = value; - }, - }} - value={form.remark} - /> - -
-
+ + + + ); }, diff --git a/apps/web-antdv-next/src/views/qqbot/command/list.tsx b/apps/web-antdv-next/src/views/qqbot/command/list.tsx new file mode 100644 index 0000000..b899f32 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/command/list.tsx @@ -0,0 +1,612 @@ +import type { TableColumnType } from 'antdv-next'; + +import type { QqbotApi } from '#/api/qqbot'; +import type { + KtTableApi, + KtTableButton, + KtTableRowAction, +} from '#/components/ktTable'; + +import { computed, defineComponent, onMounted, ref } from 'vue'; + +import { Page, useVbenModal } from '@vben/common-ui'; +import { Plus } from '@vben/icons'; + +import { message, Tag } from 'antdv-next'; + +import { useVbenForm } from '#/adapter/form'; +import { + createQqbotCommand, + deleteQqbotCommand, + getQqbotCommandList, + getQqbotPluginList, + getQqbotPluginOperationList, + testQqbotCommand, + toggleQqbotCommand, + updateQqbotCommand, +} from '#/api/qqbot'; +import { KtTable, useKtTable } from '#/components/ktTable'; + +import { + getOptionLabel, + qqbotCommandParserOptions, + qqbotRuleTargetOptions, +} from '../modules/options'; + +const AKtTable = KtTable as any; + +export default defineComponent({ + name: 'QqBotCommandList', + setup() { + const editingId = ref(); + const pluginOptions = ref>([]); + const pluginOperations = ref([]); + const pluginMetadataLoaded = ref(false); + const selectedPluginKey = ref(''); + const testResult = ref(); + let pluginMetadataPromise: Promise | undefined; + let isRestoringCommandForm = false; + + const operationOptions = computed(() => + pluginOperations.value + .filter((item) => item.pluginKey === selectedPluginKey.value) + .map((item) => ({ + label: `${item.name} (${item.key})`, + value: item.key, + })), + ); + const modalTitle = computed(() => + editingId.value ? '编辑命令' : '新建命令', + ); + + const [CommandForm, commandFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + handleValuesChange(values, fieldsChanged) { + if (fieldsChanged.includes('pluginKey')) { + selectedPluginKey.value = values.pluginKey || ''; + if (!isRestoringCommandForm) { + void commandFormApi.setFieldValue('operationKey', undefined); + } + } + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { placeholder: '如 ff14_price' }, + fieldName: 'code', + label: '命令编码', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'name', + label: '命令名称', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 3, minRows: 2 }, + placeholder: '逗号分隔,如 查价,price,ff14price', + }, + fieldName: 'aliases', + label: '命令别名', + }, + { + component: 'Input', + componentProps: { + placeholder: '逗号分隔,如 /,!,!', + }, + fieldName: 'prefixes', + label: '命令前缀', + }, + { + component: 'Select', + componentProps: () => ({ + options: pluginOptions.value, + }), + fieldName: 'pluginKey', + label: '插件', + rules: 'selectRequired', + }, + { + component: 'Select', + componentProps: () => ({ + options: operationOptions.value, + }), + fieldName: 'operationKey', + label: '插件能力', + rules: 'selectRequired', + }, + { + component: 'Select', + componentProps: { + options: qqbotCommandParserOptions, + }, + fieldName: 'parserKey', + label: '解析器', + }, + { + component: 'Select', + componentProps: { + options: qqbotRuleTargetOptions, + }, + fieldName: 'targetType', + label: '目标范围', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 8, minRows: 4 }, + placeholder: '{\n "world": "中国",\n "language": "zh"\n}', + }, + fieldName: 'defaultParams', + label: '默认参数', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 5, minRows: 3 }, + placeholder: + '留空时使用插件返回的 replyText;可用 {{output.xxx}} / {{input.xxx}}', + }, + fieldName: 'replyTemplate', + label: '回复模板', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 4, minRows: 2 }, + placeholder: '如 FF14 查价失败:{{error}}', + }, + fieldName: 'errorTemplate', + label: '错误模板', + }, + { + component: 'InputNumber', + fieldName: 'priority', + label: '优先级', + }, + { + component: 'InputNumber', + componentProps: { min: 0 }, + fieldName: 'cooldownMs', + label: '冷却时间', + suffix: () => 'ms', + }, + { + component: 'Switch', + fieldName: 'enabled', + label: '启用', + }, + { + component: 'Input', + fieldName: 'remark', + label: '备注', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', + }); + + const [TestForm, testFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { + placeholder: '如 /查价 魔匠药酒 莫古力 hq', + }, + fieldName: 'text', + label: '测试消息', + rules: 'required', + }, + { + component: 'Select', + componentProps: { + options: [ + { label: '私聊', value: 'private' }, + { label: '群聊', value: 'group' }, + { label: '频道', value: 'channel' }, + ], + }, + fieldName: 'targetType', + label: '消息类型', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', + }); + + const columns: Array> = [ + { dataIndex: 'code', key: 'code', title: '命令编码', width: 150 }, + { dataIndex: 'name', key: 'name', title: '命令名称', width: 150 }, + { dataIndex: 'aliases', key: 'aliases', title: '别名', width: 220 }, + { dataIndex: 'pluginKey', key: 'pluginKey', title: '插件', width: 140 }, + { + dataIndex: 'operationKey', + key: 'operationKey', + title: '能力', + width: 180, + }, + { + dataIndex: 'parserKey', + key: 'parserKey', + 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 = { + list: async (params) => await getQqbotCommandList(params), + }; + const buttons: Array> = [ + { + icon: , + key: 'create', + label: '新建命令', + onClick: openCreate, + permissionCodes: ['QqBot:Command:Create'], + type: 'primary', + }, + ]; + const rowActions: Array> = [ + { + key: 'toggle', + label: '启停', + onClick: async (row, context) => { + await toggleQqbotCommand(row.id, !row.enabled); + message.success(row.enabled ? '命令已停用' : '命令已启用'); + await context.reload(); + }, + permissionCodes: ['QqBot:Command:Toggle'], + }, + { + key: 'test', + label: '测试', + onClick: openTest, + permissionCodes: ['QqBot:Command:Test'], + }, + { + key: 'edit', + label: '编辑', + onClick: openEdit, + permissionCodes: ['QqBot:Command:Edit'], + }, + { + confirm: (row) => `确认删除命令「${row.name || row.code}」吗?`, + danger: true, + key: 'delete', + label: '删除', + onClick: async (row, context) => { + await deleteQqbotCommand(row.id); + message.success('命令删除成功'); + await context.reload(); + }, + permissionCodes: ['QqBot:Command:Delete'], + }, + ]; + const [registerTable, tableApi] = useKtTable({ + api, + buttons, + columns, + formOptions: { + schema: [ + { + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '命令编码/名称/别名', + }, + fieldName: 'keyword', + label: '关键词', + }, + { + component: 'Select', + componentProps: () => ({ + allowClear: true, + options: pluginOptions.value, + }), + fieldName: 'pluginKey', + 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 [CommandModal, commandModalApi] = useVbenModal({ + class: 'w-[820px]', + fullscreenButton: false, + async onConfirm() { + await submitCommand(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = commandModalApi.getData<{ + values?: QqbotApi.CommandBody; + }>(); + void resetCommandForm(values || getCommandFormDefaults()); + }, + }); + const [TestModal, testModalApi] = useVbenModal({ + class: 'w-[680px]', + fullscreenButton: false, + async onConfirm() { + await submitTest(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + testResult.value = undefined; + const { row } = testModalApi.getData<{ row?: QqbotApi.Command }>(); + void resetTestForm(row); + }, + }); + + onMounted(() => { + void ensurePluginMetadata(); + }); + + async function loadPlugins() { + const [plugins, operations] = await Promise.all([ + getQqbotPluginList(), + getQqbotPluginOperationList(), + ]); + pluginOptions.value = plugins.map((item) => ({ + label: `${item.name} (${item.key})`, + value: item.key, + })); + pluginOperations.value = operations; + pluginMetadataLoaded.value = true; + } + + async function ensurePluginMetadata() { + if (pluginMetadataLoaded.value) { + return; + } + pluginMetadataPromise ||= loadPlugins().finally(() => { + pluginMetadataPromise = undefined; + }); + await pluginMetadataPromise; + } + + function getCommandFormDefaults(): QqbotApi.CommandBody { + return { + aliases: '', + code: '', + cooldownMs: 1500, + defaultParams: '{\n "language": "zh",\n "world": "中国"\n}', + enabled: true, + errorTemplate: '命令执行失败:{{error}}', + name: '', + operationKey: '', + parserKey: 'plain', + pluginKey: '', + prefixes: '/,!,!', + priority: 0, + replyTemplate: '', + targetType: 'all', + }; + } + + async function resetCommandForm(values: QqbotApi.CommandBody) { + await ensurePluginMetadata(); + isRestoringCommandForm = true; + selectedPluginKey.value = values.pluginKey || ''; + try { + await commandFormApi.resetForm(); + await commandFormApi.setValues({ + ...values, + aliases: normalizeListText(values.aliases), + defaultParams: normalizeJsonText(values.defaultParams), + prefixes: normalizeListText(values.prefixes), + }); + await commandFormApi.resetValidate(); + } finally { + isRestoringCommandForm = false; + } + } + + async function resetTestForm(row?: QqbotApi.Command) { + await testFormApi.resetForm(); + await testFormApi.setValues({ + targetType: 'private', + text: row?.aliases?.[0] ? `/${row.aliases[0]} ` : '', + }); + await testFormApi.resetValidate(); + } + + function openCreate() { + editingId.value = undefined; + commandModalApi.setData({ values: getCommandFormDefaults() }).open(); + } + + function openEdit(row: QqbotApi.Command) { + editingId.value = row.id; + commandModalApi.setData({ values: { ...row } }).open(); + } + + function openTest(row: QqbotApi.Command) { + testModalApi.setData({ row }).open(); + } + + async function submitCommand() { + const { valid } = await commandFormApi.validate(); + if (!valid) return; + + const values = await commandFormApi.getValues(); + const payload = normalizeCommandPayload(values); + commandModalApi.lock(); + try { + await (editingId.value + ? updateQqbotCommand({ ...payload, id: editingId.value }) + : createQqbotCommand(payload)); + message.success('命令保存成功'); + await commandModalApi.close(); + await tableApi.reload(); + } finally { + commandModalApi.unlock(); + } + } + + async function submitTest() { + const { valid } = await testFormApi.validate(); + if (!valid) return; + const values = await testFormApi.getValues<{ + targetType: 'channel' | 'group' | 'private'; + text: string; + }>(); + const { row } = testModalApi.getData<{ row?: QqbotApi.Command }>(); + testModalApi.lock(); + try { + testResult.value = await testQqbotCommand({ + commandId: row?.id, + targetType: values.targetType || 'private', + text: values.text, + }); + } finally { + testModalApi.unlock(); + } + } + + function normalizeCommandPayload( + values: QqbotApi.CommandBody, + ): QqbotApi.CommandBody { + return { + ...values, + aliases: normalizeList(values.aliases), + code: values.code.trim(), + cooldownMs: values.cooldownMs || 0, + defaultParams: parseJsonText(values.defaultParams), + name: values.name.trim(), + prefixes: normalizeList(values.prefixes), + priority: values.priority || 0, + }; + } + + function normalizeList(value?: string | string[]) { + if (Array.isArray(value)) return value; + return `${value || ''}` + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + } + + function normalizeListText(value?: string | string[]) { + return Array.isArray(value) ? value.join(',') : value || ''; + } + + function normalizeJsonText(value?: Record | string) { + if (!value) return ''; + return typeof value === 'string' ? value : JSON.stringify(value, null, 2); + } + + function parseJsonText(value?: Record | string) { + if (!value || typeof value !== 'string') return value || {}; + const source = value.trim(); + if (!source) return {}; + try { + return JSON.parse(source); + } catch { + message.warning('默认参数必须是合法 JSON'); + throw new Error('默认参数必须是合法 JSON'); + } + } + + return () => ( + + { + const row = record as QqbotApi.Command; + if (column.key === 'enabled') { + return ( + + {row.enabled ? '启用' : '停用'} + + ); + } + if (column.key === 'aliases') { + return row.aliases?.join(' / ') || '-'; + } + if (column.key === 'parserKey') { + return getOptionLabel(qqbotCommandParserOptions, row.parserKey); + } + if (column.key === 'targetType') { + return getOptionLabel(qqbotRuleTargetOptions, row.targetType); + } + return undefined; + }, + }} + /> + + + + +
+ + {testResult.value ? ( +
+
+ 匹配结果:{testResult.value.matched ? '已匹配' : '未匹配'} +
+ {testResult.value.replyText ? ( +
+                    {testResult.value.replyText}
+                  
+ ) : null} + {testResult.value.message ? ( +
+ {testResult.value.message} +
+ ) : null} +
+ ) : null} +
+
+
+ ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/modules/options.ts b/apps/web-antdv-next/src/views/qqbot/modules/options.ts index cfc448c..569c0ec 100644 --- a/apps/web-antdv-next/src/views/qqbot/modules/options.ts +++ b/apps/web-antdv-next/src/views/qqbot/modules/options.ts @@ -19,6 +19,11 @@ export const qqbotRuleMatchOptions = [ export const qqbotRuleTargetOptions = qqbotTargetTypeOptions; +export const qqbotCommandParserOptions = [ + { label: '普通文本', value: 'plain' }, + { label: 'FF14 查价', value: 'ff14Price' }, +]; + export const qqbotPermissionTargetOptions = [ { label: 'QQ号', value: 'qq' }, { label: '群聊', value: 'group' }, diff --git a/apps/web-antdv-next/src/views/qqbot/permission/list.tsx b/apps/web-antdv-next/src/views/qqbot/permission/list.tsx index b19c1cc..2f18db8 100644 --- a/apps/web-antdv-next/src/views/qqbot/permission/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/permission/list.tsx @@ -7,31 +7,14 @@ import type { KtTableRowAction, } from '#/components/ktTable'; -import { - computed, - defineComponent, - onMounted, - reactive, - ref, - watch, -} from 'vue'; +import { computed, defineComponent, onMounted, ref, watch } from 'vue'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; -import { - Button, - Form, - FormItem, - Input, - message, - Modal, - Select, - Switch, - Tabs, - Tag, -} from 'antdv-next'; +import { message, Switch, Tabs, Tag } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { createQqbotPermission, deleteQqbotPermission, @@ -48,10 +31,6 @@ import { } from '../modules/options'; const AKtTable = KtTable as any; -const AButton = Button 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; @@ -68,21 +47,85 @@ export default defineComponent({ const activeKind = ref('allowlist'); const activeTargetType = ref('qq'); const configSaving = ref(false); - const saving = ref(false); - const modalOpen = ref(false); const editingId = ref(); - const permissionConfig = reactive({ + const permissionConfig = ref({ allowlistEnabled: false, blocklistEnabled: true, }); - const form = reactive({ - enabled: true, - preciseUser: false, - remark: '', - selfId: '', - targetId: '', - targetType: 'qq', - userId: '', + const [PermissionForm, permissionFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + handleValuesChange(values, fieldsChanged) { + if (fieldsChanged.includes('preciseUser') && !values.preciseUser) { + void permissionFormApi.setFieldValue('userId', ''); + } + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { + placeholder: '留空代表全部账号', + }, + fieldName: 'selfId', + label: 'Self ID', + }, + { + component: 'Select', + componentProps: { + disabled: true, + options: qqbotPermissionTargetOptions, + }, + fieldName: 'targetType', + label: '目标类型', + }, + { + component: 'Input', + componentProps: () => ({ + placeholder: `请填写${targetIdLabel.value}`, + }), + fieldName: 'targetId', + label: () => targetIdLabel.value, + rules: 'required', + }, + { + component: 'Switch', + dependencies: { + if: () => isPreciseAvailable(), + triggerFields: ['targetType'], + }, + fieldName: 'preciseUser', + label: '精确 QQ', + }, + { + component: 'Input', + componentProps: { + placeholder: '请填写需要精确匹配的 QQ 号', + }, + dependencies: { + if(values) { + return isPreciseAvailable() && !!values.preciseUser; + }, + triggerFields: ['preciseUser', 'targetType'], + }, + fieldName: 'userId', + label: 'QQ 号', + rules: 'required', + }, + { + component: 'Switch', + fieldName: 'enabled', + label: '启用', + }, + { + component: 'Input', + fieldName: 'remark', + label: '备注', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const columns: Array> = [ { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 }, @@ -110,16 +153,6 @@ export default defineComponent({ targetType: activeTargetType.value, }), }; - const buttons: Array> = [ - { - icon: , - key: 'create', - label: '新增名单', - onClick: openCreate, - permissionCodes: ['QqBot:Permission:Create'], - type: 'primary', - }, - ]; const rowActions: Array> = [ { key: 'edit', @@ -141,6 +174,16 @@ export default defineComponent({ permissionCodes: ['QqBot:Permission:Delete'], }, ]; + const buttons: Array> = [ + { + icon: , + key: 'create', + label: '新增名单', + onClick: openCreate, + permissionCodes: ['QqBot:Permission:Create'], + type: 'primary', + }, + ]; const [registerTable, tableApi] = useKtTable({ api, buttons, @@ -168,9 +211,14 @@ export default defineComponent({ ], }, rowActions, - tableTitle: '权限名单', }); const activeTargetLabel = computed(() => getPermissionTargetLabel()); + const permissionModeChecked = computed({ + get: () => permissionConfig.value.allowlistEnabled, + set: (checked: boolean) => { + void handlePermissionModeChange(checked); + }, + }); const modalTitle = computed( () => `${editingId.value ? '编辑' : '新增'}${activeTargetLabel.value}${activeKind.value === 'allowlist' ? '白名单' : '黑名单'}`, @@ -181,6 +229,21 @@ export default defineComponent({ return 'QQ 号'; }); + const [PermissionModal, permissionModalApi] = useVbenModal({ + class: 'w-[620px]', + fullscreenButton: false, + async onConfirm() { + await submitPermission(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = permissionModalApi.getData<{ + values?: QqbotApi.PermissionBody; + }>(); + void resetPermissionForm(values || getPermissionFormDefaults()); + }, + }); + onMounted(() => { void loadConfig(); }); @@ -190,25 +253,12 @@ export default defineComponent({ }); async function loadConfig() { - Object.assign(permissionConfig, await getQqbotPermissionConfig()); + const config = await getQqbotPermissionConfig(); + permissionConfig.value = normalizePermissionConfig(config); } - async function saveConfig() { - configSaving.value = true; - try { - Object.assign( - permissionConfig, - await updateQqbotPermissionConfig(permissionConfig), - ); - message.success('权限配置保存成功'); - } finally { - configSaving.value = false; - } - } - - function openCreate() { - editingId.value = undefined; - Object.assign(form, { + function getPermissionFormDefaults(): QqbotApi.PermissionBody { + return { enabled: true, preciseUser: false, remark: '', @@ -216,50 +266,85 @@ export default defineComponent({ targetId: '', targetType: activeTargetType.value, userId: '', - }); - modalOpen.value = true; + }; + } + + async function resetPermissionForm(values: QqbotApi.PermissionBody) { + await permissionFormApi.resetForm(); + await permissionFormApi.setValues(values); + await permissionFormApi.resetValidate(); + } + + function openCreate() { + editingId.value = undefined; + permissionModalApi + .setData({ values: getPermissionFormDefaults() }) + .open(); } function openEdit(row: QqbotApi.Permission) { editingId.value = row.id; activeTargetType.value = normalizePermissionTargetType(row.targetType); - Object.assign(form, { - ...row, - preciseUser: !!row.preciseUser, - targetType: activeTargetType.value, - userId: row.userId || '', - }); - modalOpen.value = true; + permissionModalApi + .setData({ + values: { + ...row, + preciseUser: !!row.preciseUser, + targetType: activeTargetType.value, + userId: row.userId || '', + }, + }) + .open(); } async function submitPermission() { - form.targetType = activeTargetType.value; - if (!form.targetId.trim()) { + const { valid } = await permissionFormApi.validate(); + if (!valid) return; + + const values = + await permissionFormApi.getValues(); + const targetId = values.targetId?.trim(); + if (!targetId) { message.warning(`请填写${targetIdLabel.value}`); return; } - if (isPreciseAvailable() && form.preciseUser && !form.userId?.trim()) { + if ( + isPreciseAvailable() && + values.preciseUser && + !values.userId?.trim() + ) { message.warning('开启精确到 QQ 号后必须填写 QQ 号'); return; } + + const payload: QqbotApi.PermissionBody = { + ...values, + preciseUser: isPreciseAvailable() ? !!values.preciseUser : false, + targetId, + targetType: activeTargetType.value, + userId: + isPreciseAvailable() && values.preciseUser + ? values.userId?.trim() + : '', + }; if (!isPreciseAvailable()) { - form.preciseUser = false; - form.userId = ''; + payload.preciseUser = false; + payload.userId = ''; } - saving.value = true; + permissionModalApi.lock(); try { await (editingId.value ? updateQqbotPermission(activeKind.value, { - ...form, + ...payload, id: editingId.value, }) - : createQqbotPermission(activeKind.value, form)); + : createQqbotPermission(activeKind.value, payload)); message.success('名单保存成功'); - modalOpen.value = false; + await permissionModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + permissionModalApi.unlock(); } } @@ -283,187 +368,128 @@ export default defineComponent({ return 'qq'; } - return () => ( - -
-
-
- - 白名单过滤: - { - permissionConfig.allowlistEnabled = value; - }, - }} - /> - - - 黑名单过滤: - { - permissionConfig.blocklistEnabled = value; - }, - }} - /> - -
- - 保存配置 - + /** + * 切换权限名单过滤模式,并立刻保存互斥后的配置。 + * + * @param checked switch 选中状态;true 表示白名单,false 表示黑名单。 + */ + async function handlePermissionModeChange(checked: boolean) { + const nextKind: PermissionKind = checked ? 'allowlist' : 'blocklist'; + const nextConfig = { + allowlistEnabled: nextKind === 'allowlist', + blocklistEnabled: nextKind === 'blocklist', + }; + + configSaving.value = true; + try { + Object.assign( + permissionConfig.value, + normalizePermissionConfig( + await updateQqbotPermissionConfig(nextConfig), + ), + ); + activeKind.value = nextKind; + message.success('权限配置已更新'); + } finally { + configSaving.value = false; + } + } + + /** + * 归一化后端配置,保证页面展示始终只有一种名单过滤模式。 + * + * @param config 后端返回的权限配置。 + */ + function normalizePermissionConfig( + config: QqbotApi.PermissionConfig, + ): QqbotApi.PermissionConfig { + const allowlistEnabled = !!config.allowlistEnabled; + + return { + allowlistEnabled, + blocklistEnabled: !allowlistEnabled, + }; + } + + /** + * 渲染 KtTable 表头控制区。 + */ + function renderHeaderControls() { + return ( + <> +
+
- { - activeKind.value = value; - }, - }} - /> - { - activeTargetType.value = value; - }, - }} - /> - { - const row = record as QqbotApi.Permission; - if (column.key === 'enabled') { - return ( - - {row.enabled ? '启用' : '停用'} - - ); - } - if (column.key === 'targetType') { - return getPermissionTargetLabel(row.targetType); - } - if (column.key === 'preciseUser') { - if (row.targetType === 'qq' || row.targetType === 'private') { - return '-'; - } - return row.preciseUser ? '是' : '否'; - } - if (column.key === 'userId') { - return row.preciseUser ? row.userId || '-' : '-'; - } - return undefined; - }, - }} +
+ +
+ + ); + } + + /** + * 渲染 KtTable 按钮区里的权限过滤模式。 + */ + function renderPermissionModeToolbar() { + return ( +
+ 过滤模式 +
- { - modalOpen.value = value; + ); + } + + return () => ( + + { + const row = record as QqbotApi.Permission; + if (column.key === 'enabled') { + return ( + + {row.enabled ? '启用' : '停用'} + + ); + } + if (column.key === 'targetType') { + return getPermissionTargetLabel(row.targetType); + } + if (column.key === 'preciseUser') { + if (row.targetType === 'qq' || row.targetType === 'private') { + return '-'; + } + return row.preciseUser ? '是' : '否'; + } + if (column.key === 'userId') { + return row.preciseUser ? row.userId || '-' : '-'; + } + return undefined; }, + headerControls: renderHeaderControls, + toolbar: renderPermissionModeToolbar, }} - open={modalOpen.value} - title={modalTitle.value} - width="620px" - > -
- - { - form.selfId = value; - }, - }} - placeholder="留空代表全部账号" - value={form.selfId} - /> - - - - - - { - form.targetId = value; - }, - }} - placeholder={`请填写${targetIdLabel.value}`} - value={form.targetId} - /> - - {isPreciseAvailable() && ( - <> - - { - form.preciseUser = value; - if (!value) form.userId = ''; - }, - }} - /> - - {form.preciseUser && ( - - { - form.userId = value; - }, - }} - placeholder="请填写需要精确匹配的 QQ 号" - value={form.userId} - /> - - )} - - )} - - { - form.enabled = value; - }, - }} - /> - - - { - form.remark = value; - }, - }} - value={form.remark} - /> - -
-
+ /> + + + ); }, diff --git a/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx b/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx new file mode 100644 index 0000000..f0ad5f8 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx @@ -0,0 +1,126 @@ +import type { TableColumnType } from 'antdv-next'; + +import type { QqbotApi } from '#/api/qqbot'; +import type { KtTableApi, KtTableButton } from '#/components/ktTable'; + +import { defineComponent, onMounted, ref } from 'vue'; + +import { Page } from '@vben/common-ui'; + +import { message, Tag } from 'antdv-next'; + +import { + getQqbotPluginHealth, + getQqbotPluginList, + getQqbotPluginOperationList, +} from '#/api/qqbot'; +import { KtTable, useKtTable } from '#/components/ktTable'; + +const AKtTable = KtTable as any; + +export default defineComponent({ + name: 'QqBotPluginList', + setup() { + const pluginOptions = ref>([]); + const pluginMap = ref>({}); + + const columns: Array> = [ + { dataIndex: 'pluginKey', key: 'pluginKey', title: '插件', width: 160 }, + { dataIndex: 'key', key: 'key', title: '能力 Key', width: 220 }, + { dataIndex: 'name', key: 'name', title: '能力名称', width: 160 }, + { + dataIndex: 'description', + key: 'description', + title: '说明', + width: 360, + }, + { + dataIndex: 'cacheTtlMs', + key: 'cacheTtlMs', + title: '建议缓存', + width: 120, + }, + ]; + const api: KtTableApi = { + list: async (params) => + await getQqbotPluginOperationList(params.pluginKey), + }; + const buttons: Array> = [ + { + key: 'health', + label: '健康检查', + onClick: async () => { + const health = await getQqbotPluginHealth(); + const content = health + .map((item) => `${item.status}: ${item.message || 'OK'}`) + .join(';'); + message.success(content || '插件健康检查完成'); + }, + }, + ]; + const [registerTable] = useKtTable({ + api, + buttons, + columns, + formOptions: { + schema: [ + { + component: 'Select', + componentProps: () => ({ + allowClear: true, + options: pluginOptions.value, + }), + fieldName: 'pluginKey', + label: '插件', + }, + ], + }, + showSelection: false, + tableTitle: '插件能力', + }); + + onMounted(() => { + void loadPlugins(); + }); + + async function loadPlugins() { + const plugins = await getQqbotPluginList(); + const nextPluginMap: Record = {}; + for (const item of plugins) { + nextPluginMap[item.key] = item; + } + pluginMap.value = nextPluginMap; + pluginOptions.value = plugins.map((item) => ({ + label: `${item.name} (${item.key})`, + value: item.key, + })); + } + + return () => ( + + { + const row = record as QqbotApi.PluginOperation; + if (column.key === 'pluginKey') { + const plugin = pluginMap.value[row.pluginKey]; + return plugin ? ( + + {plugin.name} v{plugin.version} + + ) : ( + row.pluginKey + ); + } + if (column.key === 'cacheTtlMs') { + return row.cacheTtlMs ? `${row.cacheTtlMs} ms` : '-'; + } + return undefined; + }, + }} + /> + + ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/rule/list.tsx b/apps/web-antdv-next/src/views/qqbot/rule/list.tsx index 1ebeed7..5719302 100644 --- a/apps/web-antdv-next/src/views/qqbot/rule/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/rule/list.tsx @@ -7,24 +7,14 @@ import type { KtTableRowAction, } from '#/components/ktTable'; -import { computed, defineComponent, reactive, ref } from 'vue'; +import { computed, defineComponent, ref } from 'vue'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; -import { - Form, - FormItem, - Input, - InputNumber, - message, - Modal, - Select, - Switch, - Tag, - TextArea, -} from 'antdv-next'; +import { message, Tag } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { createQqbotRule, deleteQqbotRule, @@ -41,28 +31,76 @@ import { } 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(); - const form = reactive({ - cooldownMs: 1500, - enabled: true, - keyword: '', - matchType: 'keyword', - name: '', - priority: 0, - replyContent: '', - targetType: 'all', + const [RuleForm, ruleFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + fieldName: 'name', + label: '规则名称', + }, + { + component: 'Select', + componentProps: { + options: qqbotRuleMatchOptions, + }, + fieldName: 'matchType', + label: '匹配方式', + rules: 'selectRequired', + }, + { + component: 'Input', + fieldName: 'keyword', + label: '关键词', + rules: 'required', + }, + { + component: 'Select', + componentProps: { + options: qqbotRuleTargetOptions, + }, + fieldName: 'targetType', + label: '目标范围', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 6, minRows: 3 }, + }, + fieldName: 'replyContent', + label: '回复内容', + rules: 'required', + }, + { + component: 'InputNumber', + fieldName: 'priority', + label: '优先级', + }, + { + component: 'InputNumber', + componentProps: { + min: 0, + }, + fieldName: 'cooldownMs', + label: '冷却时间', + suffix: () => 'ms', + }, + { + component: 'Switch', + fieldName: 'enabled', + label: '启用', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const columns: Array> = [ @@ -177,9 +215,23 @@ export default defineComponent({ editingId.value ? '编辑规则' : '新建规则', ); - function openCreate() { - editingId.value = undefined; - Object.assign(form, { + const [RuleModal, ruleModalApi] = useVbenModal({ + class: 'w-[720px]', + fullscreenButton: false, + async onConfirm() { + await submitRule(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + const { values } = ruleModalApi.getData<{ + values?: QqbotApi.RuleBody; + }>(); + void resetRuleForm(values || getRuleFormDefaults()); + }, + }); + + function getRuleFormDefaults(): QqbotApi.RuleBody { + return { cooldownMs: 1500, enabled: true, keyword: '', @@ -188,32 +240,54 @@ export default defineComponent({ priority: 0, replyContent: '', targetType: 'all', - }); - modalOpen.value = true; + }; + } + + async function resetRuleForm(values: QqbotApi.RuleBody) { + await ruleFormApi.resetForm(); + await ruleFormApi.setValues(values); + await ruleFormApi.resetValidate(); + } + + function openCreate() { + editingId.value = undefined; + ruleModalApi.setData({ values: getRuleFormDefaults() }).open(); } function openEdit(row: QqbotApi.Rule) { editingId.value = row.id; - Object.assign(form, { ...row }); - modalOpen.value = true; + ruleModalApi.setData({ values: { ...row } }).open(); } async function submitRule() { - if (!form.keyword.trim() || !form.replyContent.trim()) { + const { valid } = await ruleFormApi.validate(); + if (!valid) return; + + const values = await ruleFormApi.getValues(); + const keyword = values.keyword?.trim(); + const replyContent = values.replyContent?.trim(); + if (!keyword || !replyContent) { message.warning('请填写关键词和回复内容'); return; } - saving.value = true; + ruleModalApi.lock(); try { + const payload: QqbotApi.RuleBody = { + ...values, + cooldownMs: values.cooldownMs || 0, + keyword, + priority: values.priority || 0, + replyContent, + }; await (editingId.value - ? updateQqbotRule({ ...form, id: editingId.value }) - : createQqbotRule(form)); + ? updateQqbotRule({ ...payload, id: editingId.value }) + : createQqbotRule(payload)); message.success('规则保存成功'); - modalOpen.value = false; + await ruleModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + ruleModalApi.unlock(); } } @@ -241,108 +315,9 @@ export default defineComponent({ }, }} /> - { - modalOpen.value = value; - }, - }} - open={modalOpen.value} - title={modalTitle.value} - width="720px" - > -
- - { - form.name = value; - }, - }} - value={form.name} - /> - - - { - form.matchType = value; - }, - }} - options={qqbotRuleMatchOptions} - value={form.matchType} - /> - - - { - form.keyword = value; - }, - }} - value={form.keyword} - /> - - - { - form.targetType = value; - }, - }} - options={qqbotRuleTargetOptions} - value={form.targetType} - /> - - - { - form.replyContent = value; - }, - }} - value={form.replyContent} - /> - - - { - form.priority = value || 0; - }, - }} - value={form.priority} - /> - - - { - form.cooldownMs = value || 0; - }, - }} - value={form.cooldownMs} - /> - - - { - form.enabled = value; - }, - }} - /> - -
-
+ + + ); }, diff --git a/apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx b/apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx index e27040e..4e86d4c 100644 --- a/apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/sendLog/list.tsx @@ -3,12 +3,13 @@ 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 { computed, defineComponent, ref } from 'vue'; -import { Page } from '@vben/common-ui'; +import { Page, useVbenModal } from '@vben/common-ui'; -import { Form, FormItem, Input, message, Modal, Select, Tag } from 'antdv-next'; +import { message, Tag } from 'antdv-next'; +import { useVbenForm } from '#/adapter/form'; import { getQqbotSendLogList, sendQqbotGroup, @@ -24,20 +25,57 @@ import { } 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 sendTargetType = ref<'group' | 'private'>('private'); + const [SendForm, sendFormApi] = useVbenForm({ + commonConfig: { + labelClass: 'w-24', + }, + handleValuesChange(values, fieldsChanged) { + if (fieldsChanged.includes('targetType')) { + sendTargetType.value = + values.targetType === 'group' ? 'group' : 'private'; + } + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { + placeholder: '留空使用默认启用账号', + }, + fieldName: 'selfId', + label: 'Self ID', + }, + { + component: 'Select', + componentProps: { + options: qqbotMessageTypeOptions, + }, + fieldName: 'targetType', + label: '目标类型', + }, + { + component: 'Input', + fieldName: 'targetId', + label: () => targetLabel.value, + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { maxRows: 6, minRows: 3 }, + }, + fieldName: 'message', + label: '消息内容', + rules: 'required', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', }); const columns: Array> = [ { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 }, @@ -122,43 +160,73 @@ export default defineComponent({ tableTitle: '发送日志', }); const targetLabel = computed(() => - sendForm.targetType === 'group' ? '群号' : 'QQ 号', + sendTargetType.value === 'group' ? '群号' : 'QQ 号', ); - function openSend() { - Object.assign(sendForm, { + const [SendModal, sendModalApi] = useVbenModal({ + class: 'w-[620px]', + fullscreenButton: false, + async onConfirm() { + await submitSend(); + }, + onOpenChange(isOpen: boolean) { + if (!isOpen) return; + void resetSendForm(); + }, + }); + + async function resetSendForm() { + const values = { message: '', selfId: '', targetId: '', targetType: 'private', - }); - modalOpen.value = true; + }; + sendTargetType.value = values.targetType as 'private'; + await sendFormApi.resetForm(); + await sendFormApi.setValues(values); + await sendFormApi.resetValidate(); + } + + function openSend() { + sendModalApi.open(); } async function submitSend() { - if (!sendForm.targetId.trim() || !sendForm.message.trim()) { + const { valid } = await sendFormApi.validate(); + if (!valid) return; + + const values = await sendFormApi.getValues<{ + message: string; + selfId: string; + targetId: string; + targetType: 'group' | 'private'; + }>(); + const targetId = values.targetId?.trim(); + const messageText = values.message?.trim(); + if (!targetId || !messageText) { message.warning('请填写目标和消息内容'); return; } - saving.value = true; + sendModalApi.lock(); try { - await (sendForm.targetType === 'group' + await (values.targetType === 'group' ? sendQqbotGroup({ - groupId: sendForm.targetId, - message: sendForm.message, - selfId: sendForm.selfId || undefined, + groupId: targetId, + message: messageText, + selfId: values.selfId || undefined, }) : sendQqbotPrivate({ - message: sendForm.message, - selfId: sendForm.selfId || undefined, - userId: sendForm.targetId, + message: messageText, + selfId: values.selfId || undefined, + userId: targetId, })); message.success('消息已发送'); - modalOpen.value = false; + await sendModalApi.close(); await tableApi.reload(); } finally { - saving.value = false; + sendModalApi.unlock(); } } @@ -180,68 +248,9 @@ export default defineComponent({ }, }} /> - { - modalOpen.value = value; - }, - }} - open={modalOpen.value} - title="手动发送" - width="620px" - > -
- - { - sendForm.selfId = value; - }, - }} - placeholder="留空使用默认启用账号" - value={sendForm.selfId} - /> - - - { - sendForm.targetType = value; - }, - }} - options={qqbotMessageTypeOptions} - value={sendForm.targetType} - /> - - - { - sendForm.targetId = value; - }, - }} - value={sendForm.targetId} - /> - - - { - sendForm.message = value; - }, - }} - value={sendForm.message} - /> - -
-
+ + + ); }, diff --git a/apps/web-antdv-next/src/views/system/dept/data.ts b/apps/web-antdv-next/src/views/system/dept/data.ts index d2ad728..cf216b6 100644 --- a/apps/web-antdv-next/src/views/system/dept/data.ts +++ b/apps/web-antdv-next/src/views/system/dept/data.ts @@ -1,17 +1,9 @@ -import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; - import type { VbenFormSchema } from '#/adapter/form'; -import type { OnActionClickFn } from '#/adapter/vxe-table'; -import type { SystemDeptApi } from '#/api/system/dept'; import { z } from '#/adapter/form'; import { getDeptList } from '#/api/system/dept'; import { $t } from '#/locales'; -type PermissionOptions = { - canAccess?: (code: string) => boolean; -}; - /** * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量 */ @@ -72,76 +64,3 @@ export function useSchema(): VbenFormSchema[] { }, ]; } - -/** - * 获取表格列配置 - * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头 - * @param onActionClick 表格操作按钮点击事件 - */ -export function useColumns( - onActionClick?: OnActionClickFn, - options: PermissionOptions = {}, -): VxeTableGridOptions['columns'] { - const canAccess = options.canAccess || (() => true); - - return [ - { - align: 'left', - field: 'name', - fixed: 'left', - title: $t('system.dept.deptName'), - treeNode: true, - width: 150, - }, - { - cellRender: { name: 'CellTag' }, - field: 'status', - title: $t('system.dept.status'), - width: 100, - }, - { - field: 'createTime', - title: $t('system.dept.createTime'), - width: 180, - }, - { - field: 'remark', - title: $t('system.dept.remark'), - }, - { - align: 'right', - cellRender: { - attrs: { - nameField: 'name', - nameTitle: $t('system.dept.name'), - onClick: onActionClick, - }, - name: 'CellOperation', - options: [ - { - code: 'append', - show: () => canAccess('System:Dept:Create'), - text: '新增下级', - }, - { - code: 'edit', - show: () => canAccess('System:Dept:Edit'), - }, - { - code: 'delete', // 默认的删除按钮 - disabled: (row: SystemDeptApi.SystemDept) => { - return !!(row.children && row.children.length > 0); - }, - show: () => canAccess('System:Dept:Delete'), - }, - ], - }, - field: 'operation', - fixed: 'right', - headerAlign: 'center', - showOverflow: false, - title: $t('system.dept.operation'), - width: 200, - }, - ]; -} diff --git a/apps/web-antdv-next/src/views/system/menu/data.ts b/apps/web-antdv-next/src/views/system/menu/data.ts index 69185f8..dfb9123 100644 --- a/apps/web-antdv-next/src/views/system/menu/data.ts +++ b/apps/web-antdv-next/src/views/system/menu/data.ts @@ -1,12 +1,5 @@ -import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; -import type { SystemMenuApi } from '#/api/system/menu'; - import { $t } from '#/locales'; -type PermissionOptions = { - canAccess?: (code: string) => boolean; -}; - export function getMenuTypeOptions() { return [ { @@ -24,100 +17,3 @@ export function getMenuTypeOptions() { { color: 'warning', label: $t('system.menu.typeLink'), value: 'link' }, ]; } - -export function useColumns( - onActionClick: OnActionClickFn, - options: PermissionOptions = {}, -): VxeTableGridOptions['columns'] { - const canAccess = options.canAccess || (() => true); - - return [ - { - align: 'left', - field: 'meta.title', - fixed: 'left', - slots: { default: 'title' }, - title: $t('system.menu.menuTitle'), - treeNode: true, - width: 250, - }, - { - align: 'center', - cellRender: { name: 'CellTag', options: getMenuTypeOptions() }, - field: 'type', - title: $t('system.menu.type'), - width: 100, - }, - { - field: 'authCode', - title: $t('system.menu.authCode'), - width: 200, - }, - { - align: 'left', - field: 'path', - title: $t('system.menu.path'), - width: 200, - }, - - { - align: 'left', - field: 'component', - formatter: ({ row }) => { - switch (row.type) { - case 'catalog': - case 'menu': { - return row.component ?? ''; - } - case 'embedded': { - return row.meta?.iframeSrc ?? ''; - } - case 'link': { - return row.meta?.link ?? ''; - } - } - return ''; - }, - minWidth: 200, - title: $t('system.menu.component'), - }, - { - cellRender: { name: 'CellTag' }, - field: 'status', - title: $t('system.menu.status'), - width: 100, - }, - - { - align: 'right', - cellRender: { - attrs: { - nameField: 'name', - onClick: onActionClick, - }, - name: 'CellOperation', - options: [ - { - code: 'append', - show: () => canAccess('System:Menu:Create'), - text: '新增下级', - }, - { - code: 'edit', - show: () => canAccess('System:Menu:Edit'), - }, - { - code: 'delete', - show: () => canAccess('System:Menu:Delete'), - }, - ], - }, - field: 'operation', - fixed: 'right', - headerAlign: 'center', - showOverflow: false, - title: $t('system.menu.operation'), - width: 200, - }, - ]; -} diff --git a/apps/web-antdv-next/src/views/system/role/data.ts b/apps/web-antdv-next/src/views/system/role/data.ts index e9f2d40..3df0b47 100644 --- a/apps/web-antdv-next/src/views/system/role/data.ts +++ b/apps/web-antdv-next/src/views/system/role/data.ts @@ -1,13 +1,7 @@ import type { VbenFormSchema } from '#/adapter/form'; -import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; -import type { SystemRoleApi } from '#/api'; import { $t } from '#/locales'; -type PermissionOptions = { - canAccess?: (code: string) => boolean; -}; - export function useFormSchema(): VbenFormSchema[] { return [ { @@ -77,68 +71,3 @@ export function useGridFormSchema(): VbenFormSchema[] { }, ]; } - -export function useColumns( - onActionClick: OnActionClickFn, - onStatusChange?: (newStatus: any, row: T) => PromiseLike, - options: PermissionOptions = {}, -): VxeTableGridOptions['columns'] { - const canAccess = options.canAccess || (() => true); - - return [ - { - field: 'name', - title: $t('system.role.roleName'), - width: 200, - }, - { - field: 'id', - title: $t('system.role.id'), - width: 200, - }, - { - cellRender: { - attrs: { beforeChange: onStatusChange }, - name: onStatusChange ? 'CellSwitch' : 'CellTag', - }, - field: 'status', - title: $t('system.role.status'), - width: 100, - }, - { - field: 'remark', - minWidth: 100, - title: $t('system.role.remark'), - }, - { - field: 'createTime', - title: $t('system.role.createTime'), - width: 200, - }, - { - align: 'center', - cellRender: { - attrs: { - nameField: 'name', - nameTitle: $t('system.role.name'), - onClick: onActionClick, - }, - name: 'CellOperation', - options: [ - { - code: 'edit', - show: () => canAccess('System:Role:Edit'), - }, - { - code: 'delete', - show: () => canAccess('System:Role:Delete'), - }, - ], - }, - field: 'operation', - fixed: 'right', - title: $t('system.role.operation'), - width: 130, - }, - ]; -} diff --git a/internal/vite-config/package.json b/internal/vite-config/package.json index 445cbf2..1619046 100644 --- a/internal/vite-config/package.json +++ b/internal/vite-config/package.json @@ -53,7 +53,6 @@ "vite": "catalog:", "vite-plugin-compression": "catalog:", "vite-plugin-dts": "catalog:", - "vite-plugin-html": "catalog:", - "vite-plugin-lazy-import": "catalog:" + "vite-plugin-html": "catalog:" } } diff --git a/internal/vite-config/src/config/application.ts b/internal/vite-config/src/config/application.ts index a5d33c6..3a1fab2 100644 --- a/internal/vite-config/src/config/application.ts +++ b/internal/vite-config/src/config/application.ts @@ -48,7 +48,6 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) { }, pwa: true, pwaOptions: getDefaultPwaOptions(appTitle), - vxeTableLazyImport: true, ...envConfig, ...application, }); diff --git a/internal/vite-config/src/plugins/index.ts b/internal/vite-config/src/plugins/index.ts index 3f91b94..e84eaca 100644 --- a/internal/vite-config/src/plugins/index.ts +++ b/internal/vite-config/src/plugins/index.ts @@ -25,7 +25,6 @@ import { viteMetadataPlugin } from './inject-metadata'; import { viteLicensePlugin } from './license'; import { viteNitroMockPlugin } from './nitro-mock'; import { vitePrintPlugin } from './print'; -import { viteVxeTableImportsPlugin } from './vxe-table'; /** * 获取条件成立的 vite 插件 @@ -112,7 +111,6 @@ async function loadApplicationPlugins( printInfoMap, pwa, pwaOptions, - vxeTableLazyImport, ...commonOptions } = options; @@ -138,12 +136,6 @@ async function loadApplicationPlugins( return [await vitePrintPlugin({ infoMap: printInfoMap })]; }, }, - { - condition: vxeTableLazyImport, - plugins: async () => { - return [await viteVxeTableImportsPlugin()]; - }, - }, { condition: nitroMock, plugins: async () => { @@ -259,5 +251,4 @@ export { viteDtsPlugin, viteHtmlPlugin, viteVisualizerPlugin, - viteVxeTableImportsPlugin, }; diff --git a/internal/vite-config/src/plugins/vxe-table.ts b/internal/vite-config/src/plugins/vxe-table.ts deleted file mode 100644 index cc226b5..0000000 --- a/internal/vite-config/src/plugins/vxe-table.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PluginOption } from 'vite'; - -import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import'; - -async function viteVxeTableImportsPlugin(): Promise { - return [ - lazyImport({ - resolvers: [ - VxeResolver({ - libraryName: 'vxe-table', - }), - VxeResolver({ - libraryName: 'vxe-pc-ui', - }), - ], - }), - ] as unknown as PluginOption; -} - -export { viteVxeTableImportsPlugin }; diff --git a/internal/vite-config/src/typing.ts b/internal/vite-config/src/typing.ts index 4fddb7d..b63bee9 100644 --- a/internal/vite-config/src/typing.ts +++ b/internal/vite-config/src/typing.ts @@ -278,11 +278,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions { * PWA 插件配置 */ pwaOptions?: Partial; - /** - * 是否开启 VXE Table 懒加载 - * @default false - */ - vxeTableLazyImport?: boolean; } /** diff --git a/packages/effects/plugins/package.json b/packages/effects/plugins/package.json index 3e508c1..f89a6fc 100644 --- a/packages/effects/plugins/package.json +++ b/packages/effects/plugins/package.json @@ -18,10 +18,6 @@ "types": "./src/echarts/index.ts", "default": "./src/echarts/index.ts" }, - "./vxe-table": { - "types": "./src/vxe-table/index.ts", - "default": "./src/vxe-table/index.ts" - }, "./motion": { "types": "./src/motion/index.ts", "default": "./src/motion/index.ts" @@ -40,8 +36,6 @@ "@vueuse/core": "catalog:", "@vueuse/motion": "catalog:", "echarts": "catalog:", - "vue": "catalog:", - "vxe-pc-ui": "catalog:", - "vxe-table": "catalog:" + "vue": "catalog:" } } diff --git a/packages/effects/plugins/src/vxe-table/api.ts b/packages/effects/plugins/src/vxe-table/api.ts deleted file mode 100644 index 2b60d60..0000000 --- a/packages/effects/plugins/src/vxe-table/api.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { VxeGridInstance } from 'vxe-table'; - -import type { ExtendedFormApi } from '@vben-core/form-ui'; - -import type { VxeGridProps } from './types'; - -import { toRaw } from 'vue'; - -import { Store } from '@vben-core/shared/store'; -import { - bindMethods, - isBoolean, - isFunction, - mergeWithArrayOverride, - StateHandler, -} from '@vben-core/shared/utils'; - -function getDefaultState(): VxeGridProps { - return { - class: '', - gridClass: '', - gridOptions: {}, - gridEvents: {}, - formOptions: undefined, - showSearchForm: true, - }; -} - -export class VxeGridApi = any> { - public formApi = {} as ExtendedFormApi; - - // private prevState: null | VxeGridProps = null; - public grid = {} as VxeGridInstance; - public state: null | VxeGridProps = null; - - public store: Store>; - - private isMounted = false; - - private stateHandler: StateHandler; - - constructor(options: VxeGridProps = {}) { - const storeState = { ...options }; - - const defaultState = getDefaultState(); - this.store = new Store( - mergeWithArrayOverride(storeState, defaultState), - { - onUpdate: () => { - // this.prevState = this.state; - this.state = this.store.state; - }, - }, - ); - - this.state = this.store.state; - this.stateHandler = new StateHandler(); - bindMethods(this); - } - - mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) { - if (!this.isMounted && instance) { - this.grid = instance; - this.formApi = formApi; - this.stateHandler.setConditionTrue(); - this.isMounted = true; - } - } - - async query(params: Record = {}) { - try { - await this.grid.commitProxy('query', toRaw(params)); - } catch (error) { - console.error('Error occurred while querying:', error); - } - } - - async reload(params: Record = {}) { - try { - await this.grid.commitProxy('reload', toRaw(params)); - } catch (error) { - console.error('Error occurred while reloading:', error); - } - } - - setGridOptions(options: Partial) { - this.setState({ - gridOptions: options, - }); - } - - setLoading(isLoading: boolean) { - this.setState({ - gridOptions: { - loading: isLoading, - }, - }); - } - - setState( - stateOrFn: - | ((prev: VxeGridProps) => Partial>) - | Partial>, - ) { - if (isFunction(stateOrFn)) { - this.store.setState((prev) => { - return mergeWithArrayOverride(stateOrFn(prev), prev); - }); - } else { - this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev)); - } - } - - toggleSearchForm(show?: boolean) { - this.setState({ - showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm, - }); - // nextTick(() => { - // this.grid.recalculate(); - // }); - return this.state?.showSearchForm; - } - - unmount() { - this.isMounted = false; - this.stateHandler.reset(); - } -} diff --git a/packages/effects/plugins/src/vxe-table/extends.ts b/packages/effects/plugins/src/vxe-table/extends.ts deleted file mode 100644 index a6cf4ca..0000000 --- a/packages/effects/plugins/src/vxe-table/extends.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { VxeGridProps, VxeUIExport } from 'vxe-table'; - -import type { Recordable } from '@vben/types'; - -import type { VxeGridApi } from './api'; - -import { formatDate, formatDateTime, isFunction } from '@vben/utils'; - -export function extendProxyOptions( - api: VxeGridApi, - options: VxeGridProps, - getFormValues: () => Recordable, -) { - [ - 'query', - 'querySuccess', - 'queryError', - 'queryAll', - 'queryAllSuccess', - 'queryAllError', - ].forEach((key) => { - extendProxyOption(key, api, options, getFormValues); - }); -} - -function extendProxyOption( - key: string, - api: VxeGridApi, - options: VxeGridProps, - getFormValues: () => Recordable, -) { - const { proxyConfig } = options; - const configFn = (proxyConfig?.ajax as Recordable)?.[key]; - if (!isFunction(configFn)) { - return options; - } - - const wrapperFn = async ( - params: Recordable, - customValues: Recordable, - ...args: Recordable[] - ) => { - const formValues = getFormValues(); - const data = await configFn( - params, - { - /** - * 开启toolbarConfig.refresh功能 - * 点击刷新按钮 这里的值为PointerEvent 会携带错误参数 - */ - ...(customValues instanceof PointerEvent ? {} : customValues), - ...formValues, - }, - ...args, - ); - return data; - }; - api.setState({ - gridOptions: { - proxyConfig: { - ajax: { - [key]: wrapperFn, - }, - }, - }, - }); -} - -export function extendsDefaultFormatter(vxeUI: VxeUIExport) { - vxeUI.formats.add('formatDate', { - tableCellFormatMethod({ cellValue }) { - return formatDate(cellValue); - }, - }); - - vxeUI.formats.add('formatDateTime', { - tableCellFormatMethod({ cellValue }) { - return formatDateTime(cellValue); - }, - }); -} diff --git a/packages/effects/plugins/src/vxe-table/index.ts b/packages/effects/plugins/src/vxe-table/index.ts deleted file mode 100644 index a19f2b8..0000000 --- a/packages/effects/plugins/src/vxe-table/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { setupVbenVxeTable } from './init'; -export type { VxeTableGridOptions } from './types'; -export * from './use-vxe-grid'; - -export { default as VbenVxeGrid } from './use-vxe-grid.vue'; -export type { - VxeGridListeners, - VxeGridProps, - VxeGridPropTypes, -} from 'vxe-table'; diff --git a/packages/effects/plugins/src/vxe-table/init.ts b/packages/effects/plugins/src/vxe-table/init.ts deleted file mode 100644 index 00e9d8a..0000000 --- a/packages/effects/plugins/src/vxe-table/init.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { SetupVxeTable } from './types'; - -import { defineComponent, watch } from 'vue'; - -import { usePreferences } from '@vben/preferences'; - -import { useVbenForm } from '@vben-core/form-ui'; - -import { - VxeButton, - VxeCheckbox, - - // VxeFormGather, - // VxeForm, - // VxeFormItem, - VxeIcon, - VxeInput, - VxeLoading, - VxeModal, - VxeNumberInput, - VxePager, - // VxeList, - // VxeModal, - // VxeOptgroup, - // VxeOption, - // VxePulldown, - // VxeRadio, - // VxeRadioButton, - VxeRadioGroup, - VxeSelect, - VxeTooltip, - VxeUI, - VxeUpload, - // VxeSwitch, - // VxeTextarea, -} from 'vxe-pc-ui'; -import enUS from 'vxe-pc-ui/lib/language/en-US'; -// 导入默认的语言 -import zhCN from 'vxe-pc-ui/lib/language/zh-CN'; -import { - VxeColgroup, - VxeColumn, - VxeGrid, - VxeTable, - VxeToolbar, -} from 'vxe-table'; - -import { extendsDefaultFormatter } from './extends'; - -// 是否加载过 -let isInit = false; - -// eslint-disable-next-line import/no-mutable-exports -export let useTableForm: typeof useVbenForm; - -// 部分组件,如果没注册,vxe-table 会报错,这里实际没用组件,只是为了不报错,同时可以减少打包体积 -const createVirtualComponent = (name = '') => { - return defineComponent({ - name, - }); -}; - -export function initVxeTable() { - if (isInit) { - return; - } - - VxeUI.component(VxeTable); - VxeUI.component(VxeColumn); - VxeUI.component(VxeColgroup); - VxeUI.component(VxeGrid); - VxeUI.component(VxeToolbar); - - VxeUI.component(VxeButton); - // VxeUI.component(VxeButtonGroup); - VxeUI.component(VxeCheckbox); - // VxeUI.component(VxeCheckboxGroup); - VxeUI.component(createVirtualComponent('VxeForm')); - // VxeUI.component(VxeFormGather); - // VxeUI.component(VxeFormItem); - VxeUI.component(VxeIcon); - VxeUI.component(VxeInput); - // VxeUI.component(VxeList); - VxeUI.component(VxeLoading); - VxeUI.component(VxeModal); - VxeUI.component(VxeNumberInput); - // VxeUI.component(VxeOptgroup); - // VxeUI.component(VxeOption); - VxeUI.component(VxePager); - // VxeUI.component(VxePulldown); - // VxeUI.component(VxeRadio); - // VxeUI.component(VxeRadioButton); - VxeUI.component(VxeRadioGroup); - VxeUI.component(VxeSelect); - // VxeUI.component(VxeSwitch); - // VxeUI.component(VxeTextarea); - VxeUI.component(VxeTooltip); - VxeUI.component(VxeUpload); - - isInit = true; -} - -export function setupVbenVxeTable(setupOptions: SetupVxeTable) { - const { configVxeTable, useVbenForm } = setupOptions; - - initVxeTable(); - useTableForm = useVbenForm; - - const { isDark, locale } = usePreferences(); - - const localMap = { - 'zh-CN': zhCN, - 'en-US': enUS, - }; - - watch( - [() => isDark.value, () => locale.value], - ([isDarkValue, localeValue]) => { - VxeUI.setTheme(isDarkValue ? 'dark' : 'light'); - VxeUI.setI18n(localeValue, localMap[localeValue]); - VxeUI.setLanguage(localeValue); - }, - { - immediate: true, - }, - ); - - extendsDefaultFormatter(VxeUI); - - configVxeTable(VxeUI); -} diff --git a/packages/effects/plugins/src/vxe-table/style.css b/packages/effects/plugins/src/vxe-table/style.css deleted file mode 100644 index 5b47fa2..0000000 --- a/packages/effects/plugins/src/vxe-table/style.css +++ /dev/null @@ -1,117 +0,0 @@ -:root .vxe-grid { - --vxe-ui-font-color: hsl(var(--foreground)); - --vxe-ui-font-primary-color: hsl(var(--primary)); - - /* --vxe-ui-font-lighten-color: #babdc0; - --vxe-ui-font-darken-color: #86898e; */ - --vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%); - - /* base */ - --vxe-ui-base-popup-border-color: hsl(var(--border)); - --vxe-ui-input-disabled-color: hsl(var(--border) / 60%); - - /* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */ - - /* layout */ - --vxe-ui-layout-background-color: hsl(var(--background)); - --vxe-ui-table-resizable-line-color: hsl(var(--heavy)); - - /* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent)); - --vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */ - - /* input */ - --vxe-ui-input-border-color: hsl(var(--border)); - - /* --vxe-ui-input-placeholder-color: #8d9095; */ - - /* --vxe-ui-input-disabled-background-color: #262727; */ - - /* loading */ - --vxe-ui-loading-background-color: hsl(var(--overlay-content)); - - /* table */ - --vxe-ui-table-header-background-color: hsl(var(--accent)); - --vxe-ui-table-border-color: hsl(var(--border)); - --vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover)); - --vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%); - --vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent)); - --vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent)); - --vxe-ui-table-row-hover-radio-checked-background-color: hsl( - var(--accent-hover) - ); - --vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent)); - --vxe-ui-table-row-hover-checkbox-checked-background-color: hsl( - var(--accent-hover) - ); - --vxe-ui-table-row-current-background-color: hsl(var(--accent)); - --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover)); - --vxe-ui-font-primary-tinge-color: hsl(var(--primary)); - --vxe-ui-font-primary-lighten-color: hsl(var(--primary) / 60%); - --vxe-ui-font-primary-darken-color: hsl(var(--primary)); - - height: auto !important; - - /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */ -} - -.vxe-pager { - .vxe-pager--prev-btn:not(.is--disabled):active, - .vxe-pager--next-btn:not(.is--disabled):active, - .vxe-pager--num-btn:not(.is--disabled):active, - .vxe-pager--jump-prev:not(.is--disabled):active, - .vxe-pager--jump-next:not(.is--disabled):active, - .vxe-pager--prev-btn:not(.is--disabled):focus, - .vxe-pager--next-btn:not(.is--disabled):focus, - .vxe-pager--num-btn:not(.is--disabled):focus, - .vxe-pager--jump-prev:not(.is--disabled):focus, - .vxe-pager--jump-next:not(.is--disabled):focus { - color: hsl(var(--accent-foreground)); - background-color: hsl(var(--accent)); - border: 1px solid hsl(var(--border)); - box-shadow: 0 0 0 1px hsl(var(--border)); - } - - .vxe-pager--wrapper { - display: flex; - align-items: center; - } - - .vxe-pager--sizes { - margin-right: auto; - } -} - -.vxe-pager--wrapper { - @apply justify-center md:justify-end; -} - -.vxe-tools--operate { - margin-right: 0.25rem; - margin-left: 0.75rem; -} - -.vxe-table-custom--checkbox-option:hover { - background: none !important; -} - -.vxe-toolbar { - padding: 0; -} - -.vxe-buttons--wrapper:not(:empty), -.vxe-tools--operate:not(:empty), -.vxe-tools--wrapper:not(:empty) { - padding: 0.6em 0; -} - -.vxe-tools--operate:not(:has(button)) { - margin-left: 0; -} - -.vxe-grid--layout-header-wrapper { - overflow: visible; -} - -.vxe-grid--layout-body-content-wrapper { - overflow: hidden; -} diff --git a/packages/effects/plugins/src/vxe-table/types.ts b/packages/effects/plugins/src/vxe-table/types.ts deleted file mode 100644 index 8b9aea4..0000000 --- a/packages/effects/plugins/src/vxe-table/types.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { - VxeGridListeners, - VxeGridPropTypes, - VxeGridProps as VxeTableGridProps, - VxeUIExport, -} from 'vxe-table'; - -import type { Ref } from 'vue'; - -import type { ClassType, DeepPartial } from '@vben/types'; - -import type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui'; - -import type { VxeGridApi } from './api'; - -import { useVbenForm } from '@vben-core/form-ui'; - -export interface VxePaginationInfo { - currentPage: number; - pageSize: number; - total: number; -} - -interface ToolbarConfigOptions extends VxeGridPropTypes.ToolbarConfig { - /** 是否显示切换搜索表单的按钮 */ - search?: boolean; -} - -export interface VxeTableGridOptions extends VxeTableGridProps { - /** 工具栏配置 */ - toolbarConfig?: ToolbarConfigOptions; -} - -export interface SeparatorOptions { - show?: boolean; - backgroundColor?: string; -} - -export interface VxeGridProps< - T extends Record = any, - D extends BaseFormComponentType = BaseFormComponentType, -> { - /** - * 标题 - */ - tableTitle?: string; - /** - * 标题帮助 - */ - tableTitleHelp?: string; - /** - * 组件class - */ - class?: ClassType; - /** - * vxe-grid class - */ - gridClass?: ClassType; - /** - * vxe-grid 配置 - */ - gridOptions?: DeepPartial>; - /** - * vxe-grid 事件 - */ - gridEvents?: DeepPartial>; - /** - * 表单配置 - */ - formOptions?: VbenFormProps; - /** - * 显示搜索表单 - */ - showSearchForm?: boolean; - /** - * 搜索表单与表格主体之间的分隔条 - */ - separator?: boolean | SeparatorOptions; -} - -export type ExtendedVxeGridApi< - D extends Record = any, - F extends BaseFormComponentType = BaseFormComponentType, -> = VxeGridApi & { - useStore: >>( - selector?: (state: NoInfer>) => T, - ) => Readonly>; -}; - -export interface SetupVxeTable { - configVxeTable: (ui: VxeUIExport) => void; - useVbenForm: typeof useVbenForm; -} diff --git a/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts b/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts deleted file mode 100644 index 6ab8769..0000000 --- a/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table'; - -import type { SlotsType } from 'vue'; - -import type { BaseFormComponentType } from '@vben-core/form-ui'; - -import type { ExtendedVxeGridApi, VxeGridProps } from './types'; - -import { defineComponent, h, onBeforeUnmount } from 'vue'; - -import { useStore } from '@vben-core/shared/store'; - -import { VxeGridApi } from './api'; -import VxeGrid from './use-vxe-grid.vue'; - -type FilteredSlots = { - [K in keyof VxeGridSlots as K extends 'form' - ? never - : K]: VxeGridSlots[K]; -}; - -export function useVbenVxeGrid< - T extends Record = any, - D extends BaseFormComponentType = BaseFormComponentType, ->(options: VxeGridProps) { - // const IS_REACTIVE = isReactive(options); - const api = new VxeGridApi(options); - const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi; - extendedApi.useStore = (selector) => { - return useStore(api.store, selector); - }; - - const Grid = defineComponent( - (props: VxeGridProps, { attrs, slots }) => { - onBeforeUnmount(() => { - api.unmount(); - }); - api.setState({ ...props, ...attrs }); - return () => h(VxeGrid, { ...props, ...attrs, api: extendedApi }, slots); - }, - { - name: 'VbenVxeGrid', - inheritAttrs: false, - slots: Object as SlotsType< - { - // 表格标题 - 'table-title': undefined; - // 工具栏左侧部分 - 'toolbar-actions': VxeGridSlotTypes.DefaultSlotParams; - // 工具栏右侧部分 - 'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams; - } & FilteredSlots - >, - }, - ); - // Add reactivity support - // if (IS_REACTIVE) { - // watch( - // () => options, - // () => { - // api.setState(options); - // }, - // { immediate: true }, - // ); - // } - - return [Grid, extendedApi] as const; -} - -export type UseVbenVxeGrid = typeof useVbenVxeGrid; diff --git a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue deleted file mode 100644 index 3fcea78..0000000 --- a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue +++ /dev/null @@ -1,481 +0,0 @@ - - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 984e3e8..9aa3675 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -390,9 +390,6 @@ catalogs: vite-plugin-html: specifier: ^3.2.2 version: 3.2.2 - vite-plugin-lazy-import: - specifier: ^1.0.7 - version: 1.0.7 vite-plugin-pwa: specifier: ^1.2.0 version: 1.2.0 @@ -420,12 +417,6 @@ catalogs: vue-tsc: specifier: ^3.2.4 version: 3.2.4 - vxe-pc-ui: - specifier: ^4.12.16 - version: 4.12.35 - vxe-table: - specifier: ^4.17.46 - version: 4.17.48 watermark-js-plus: specifier: ^1.6.3 version: 1.6.3 @@ -922,9 +913,6 @@ importers: vite-plugin-html: specifier: 'catalog:' version: 3.2.2(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) - vite-plugin-lazy-import: - specifier: 'catalog:' - version: 1.0.7 packages/@core/base/design: {} @@ -1410,12 +1398,6 @@ importers: vue: specifier: ^3.5.27 version: 3.5.27(typescript@5.9.3) - vxe-pc-ui: - specifier: 'catalog:' - version: 4.12.35(vue@3.5.27(typescript@5.9.3)) - vxe-table: - specifier: 'catalog:' - version: 4.17.48(vue@3.5.27(typescript@5.9.3)) packages/effects/request: dependencies: @@ -4091,11 +4073,6 @@ packages: peerDependencies: vue: ^3.5.27 - '@vxe-ui/core@4.3.1': - resolution: {integrity: sha512-sr2WdFDWM3IKID02HbSaDxxRDvj1LZ5ZkOnH2POvGkkCfCWItkx3avkizfRUk8RtjNU+wXozaPbYTNha5kjSdg==} - peerDependencies: - vue: ^3.5.27 - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4890,9 +4867,6 @@ packages: dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dom-zindex@1.0.6: - resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==} - domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} @@ -8449,9 +8423,6 @@ packages: '@nuxt/kit': optional: true - vite-plugin-lazy-import@1.0.7: - resolution: {integrity: sha512-mE6oAObOb4wqso4AoUGi9cLjdR+4vay1RCaKJvziBuFPlziZl7J0aw2hsqRTokLVRx3bli0a0VyjMOwsNDv58A==} - vite-plugin-pwa@1.2.0: resolution: {integrity: sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==} engines: {node: '>=16.0.0'} @@ -8601,12 +8572,6 @@ packages: typescript: optional: true - vxe-pc-ui@4.12.35: - resolution: {integrity: sha512-Hzmz8fhi3osQbxRAZ4mxdX+BgZjaGczl2O3Xhqp14+VUKuU4/S6UXU7uIvZqTO4nWl2w6DVdZzXE+UpXDs2zEg==} - - vxe-table@4.17.48: - resolution: {integrity: sha512-hd2j3FMA5vu3Qc3wyCavwMdsaT5uEq1GCux3eV5VKPkd/MoSdh9DIyMzmqDKh/0QnpVPsoQuIZPJi/govnw2Iw==} - watermark-js-plus@1.6.3: resolution: {integrity: sha512-iCLOGf70KacIwjGF9MDViYxQcRiVwOH7l42qDHLeE2HeUsQD1EQuUC9cKRG/4SErTUmdqV3yf5WnKk2dRARHPQ==} @@ -8751,9 +8716,6 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} - xe-utils@3.9.1: - resolution: {integrity: sha512-Ujk5UmoH6Iaqhgz3oGwfCXVcMdUJKlXnfvLABdnMyseMG0eHsX2mcCvLd/8sGlIXtfwsprI9bW7vgcVognLmqQ==} - xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -11703,12 +11665,6 @@ snapshots: dependencies: vue: 3.5.27(typescript@5.9.3) - '@vxe-ui/core@4.3.1(vue@3.5.27(typescript@5.9.3))': - dependencies: - dom-zindex: 1.0.6 - vue: 3.5.27(typescript@5.9.3) - xe-utils: 3.9.1 - abbrev@2.0.0: {} abbrev@3.0.1: {} @@ -12551,8 +12507,6 @@ snapshots: domhandler: 5.0.3 entities: 4.5.0 - dom-zindex@1.0.6: {} - domelementtype@2.3.0: {} domhandler@4.3.1: @@ -16470,13 +16424,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-lazy-import@1.0.7: - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - es-module-lexer: 1.7.0 - rollup: 4.57.1 - xe-utils: 3.9.1 - vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0): dependencies: debug: 4.4.3 @@ -16694,18 +16641,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - vxe-pc-ui@4.12.35(vue@3.5.27(typescript@5.9.3)): - dependencies: - '@vxe-ui/core': 4.3.1(vue@3.5.27(typescript@5.9.3)) - transitivePeerDependencies: - - vue - - vxe-table@4.17.48(vue@3.5.27(typescript@5.9.3)): - dependencies: - vxe-pc-ui: 4.12.35(vue@3.5.27(typescript@5.9.3)) - transitivePeerDependencies: - - vue - watermark-js-plus@1.6.3: {} web-streams-polyfill@3.3.3: {} @@ -16935,8 +16870,6 @@ snapshots: dependencies: is-wsl: 3.1.0 - xe-utils@3.9.1: {} - xml-name-validator@4.0.0: {} y18n@4.0.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 33ac861..c1db0fe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -158,7 +158,6 @@ catalog: vite-plugin-compression: ^0.5.1 vite-plugin-dts: ^4.5.4 vite-plugin-html: ^3.2.2 - vite-plugin-lazy-import: ^1.0.7 vite-plugin-pwa: ^1.2.0 vite-plugin-vue-devtools: ^8.0.5 vitest: ^3.2.4 @@ -169,8 +168,6 @@ catalog: vue-router: ^4.6.4 vue-tippy: ^6.7.1 vue-tsc: ^3.2.4 - vxe-pc-ui: ^4.12.16 - vxe-table: ^4.17.46 watermark-js-plus: ^1.6.3 yaml-eslint-parser: ^1.3.2 zod: ^3.25.76 diff --git a/skills/SKILL.md b/skills/SKILL.md index 1099b57..f1bf522 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -279,7 +279,7 @@ import { useAuthStore } from '#/store'; ### 业务组件 (references/components/business/) - **Page页面**: `references/components/business/page.md` - 页面布局容器、标题区、内容区 - **表单组件**: `references/components/business/form.md` - Vben Form表单配置、校验、联动 -- **表格组件**: `references/components/business/table.md` - Vben Vxe Table表格配置、搜索、远程加载 +- **表格组件**: `references/components/business/table.md` - KtTable 表格配置、搜索、远程加载、按钮权限 - **模态框**: `references/components/business/modal.md` - Vben Modal配置、拖拽、全屏 - **抽屉**: `references/components/business/drawer.md` - Vben Drawer配置、组件抽离 - **轻量提示框**: `references/components/business/alert.md` - alert、confirm、prompt调用 diff --git a/skills/references/components/business/table.md b/skills/references/components/business/table.md index 2c83bbe..a7da8ae 100644 --- a/skills/references/components/business/table.md +++ b/skills/references/components/business/table.md @@ -1,266 +1,166 @@ -# Vben Vxe Table 表格 +# KtTable 表格 -基于 [vxe-table](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 和 `Vben Form` 做了二次封装,用于构建带搜索表单的列表页面。 +当前项目表格统一使用 `KtTable + Antdv Next Table + Vben Form`。不要再引入旧表格适配器或额外表格依赖。 ## 基础用法 -```vue - +}; - +const buttons: Array> = [ + { + icon: () => h(Plus, { class: 'kt-table__button-icon' }), + key: 'create', + label: '新增', + onClick: onCreate, + type: 'primary', + }, +]; + +const [registerTable, tableApi] = useKtTable({ + api, + buttons, + columns, + rowActions, +}); ``` -## 远程加载 - -```vue - + }} +/> ``` ## 搜索表单 -```vue - -``` - -## 树形表格 - -```ts -const [Grid, gridApi] = useVbenVxeGrid({ - gridOptions: { - columns: [...], - treeConfig: { - transform: true, - parentField: 'parentId', - rowField: 'id', - }, - }, }); ``` -## 固定列 +## 操作按钮 + +按钮完全由业务页面注册,组件里不写死新增、编辑、删除等业务逻辑。 ```ts -const columns = [ - { field: 'name', title: '名称', fixed: 'left', width: 100 }, - { field: 'age', title: '年龄' }, - { field: 'address', title: '地址' }, - { field: 'action', title: '操作', fixed: 'right', width: 100 }, -]; -``` - -## 单元格编辑 - -```ts -const [Grid, gridApi] = useVbenVxeGrid({ - gridOptions: { - editConfig: { - mode: 'cell', // 或 'row' - trigger: 'click', - }, - columns: [ - { - field: 'name', - title: '名称', - editRender: { name: 'input' }, - }, - ], - }, -}); -``` - -## 自定义渲染器 - -```ts -// 适配器配置 -import { h } from 'vue'; -import { Image, Button } from 'ant-design-vue'; - -vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { - const { column, row } = params; - return h(Image, { src: row[column.field] }); - }, -}); - -vxeUI.renderer.add('CellLink', { - renderTableDefault(renderOpts) { - const { props } = renderOpts; - return h(Button, { size: 'small', type: 'link' }, { - default: () => props?.text, - }); - }, -}); - -// 使用 -const columns = [ +const rowActions = [ { - field: 'avatar', - title: '头像', - cellRender: { name: 'CellImage' }, + key: 'edit', + label: '编辑', + onClick: onEdit, + permissionCodes: ['System:Role:Edit'], }, { - field: 'link', - title: '链接', - cellRender: { name: 'CellLink', props: { text: '查看' } }, + confirm: (row) => `确认删除「${row.name}」吗?`, + danger: true, + key: 'delete', + label: '删除', + onClick: onDelete, + permissionCodes: ['System:Role:Delete'], }, ]; ``` -## GridApi 方法 - -| 方法名 | 描述 | 类型 | -|--------|------|------| -| setLoading | 设置loading状态 | `(loading: boolean) => void` | -| setGridOptions | 更新gridOptions | `(options) => void` | -| reload | 重新加载,重置分页 | `(params?) => void` | -| query | 重新查询,保留分页 | `(params?) => void` | -| grid | vxe-grid实例 | `VxeGridInstance` | -| formApi | 搜索表单API | `FormApi` | -| toggleSearchForm | 切换搜索表单状态 | `(show?: boolean) => boolean` | - -## Props 属性 - -| 属性名 | 描述 | 类型 | -|--------|------|------| -| tableTitle | 表格标题 | `string` | -| tableTitleHelp | 表格标题帮助信息 | `string` | -| class | 外层容器的class | `string` | -| gridClass | vxe-grid的class | `string` | -| gridOptions | vxe-grid配置 | `VxeTableGridOptions` | -| gridEvents | vxe-grid事件 | `VxeGridListeners` | -| formOptions | 搜索表单配置 | `VbenFormProps` | -| showSearchForm | 是否显示搜索表单 | `boolean` | -| separator | 搜索表单与表格的分隔条 | `boolean \| SeparatorOptions` | - -## 插槽 - -| 插槽名 | 描述 | -|--------|------| -| toolbar-actions | 工具栏左侧区域 | -| toolbar-tools | 工具栏右侧区域 | -| table-title | 自定义表格标题 | -| form-* | 搜索表单插槽转发 | - -## 适配器配置 +## 可插拔模块 ```ts -// src/adapter/vxe-table.ts -import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; -import { useVbenForm } from './form'; +import { defineKtTableHook, defineKtTableModule } from '#/components/ktTable'; -setupVbenVxeTable({ - configVxeTable: (vxeUI) => { - vxeUI.setConfig({ - grid: { - align: 'center', - border: false, - columnConfig: { - resizable: true, - }, - minHeight: 180, - proxyConfig: { - autoLoad: true, - response: { - result: 'items', - total: 'total', - list: 'items', - }, - }, - showOverflow: true, - size: 'small', - }, - }); +const requestLogger = defineKtTableHook({ + name: 'requestLogger', + onBeforeFetch(params) { + console.log(params); }, - useVbenForm, }); -export { useVbenVxeGrid }; +const statusModule = defineKtTableModule({ + columns: [{ dataIndex: 'status', key: 'status', title: '状态', width: 100 }], + hooks: [requestLogger], + name: 'statusModule', +}); + +const [registerTable] = useKtTable({ + columns, + modules: [statusModule], +}); ``` + +## 常用配置 + +| 属性 | 说明 | +| --- | --- | +| `api.list` | 远程数据接口,组件自动带分页和搜索参数 | +| `columns` | Antdv Next `TableColumnType[]` | +| `formOptions` | Vben Form 搜索表单配置 | +| `buttons` | 表格头部按钮 | +| `rowActions` | 行操作按钮,超过可见数量自动折叠 | +| `statistics` | 行列级统计,固定在表格底部 | +| `showIndex` | 是否显示序号列,默认显示 | +| `showSelection` | 是否显示选择列 | +| `showPagination` | 是否显示分页 | +| `rowResizable` | 是否允许调整单行行高 | + +## 约束 + +- 表格列使用 Antdv Next 原生 `TableColumnType`。 +- 自定义单元格使用 `bodyCell` 插槽或页面内 TSX 渲染。 +- 搜索表单必须使用 Vben Form,不在外部维护独立 `searchValue`。 +- 业务按钮、权限码和请求逻辑都由页面通过 `useKtTable` 注册。