feat: 优化后台表格并接入 QQBot 插件能力
This commit is contained in:
parent
6221487991
commit
77270c51eb
@ -137,7 +137,22 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
|
||||
@ -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<Recordable<any>> = {
|
||||
delete: {
|
||||
danger: true,
|
||||
text: $t('common.delete'),
|
||||
},
|
||||
edit: {
|
||||
text: $t('common.edit'),
|
||||
},
|
||||
};
|
||||
const operations: Array<Recordable<any>> = (
|
||||
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<any> = {};
|
||||
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<any>, 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<any>) {
|
||||
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 = <T extends Record<string, any>>(
|
||||
...rest: Parameters<typeof useGrid<T, ComponentType>>
|
||||
) => useGrid<T, ComponentType>(...rest);
|
||||
|
||||
export type OnActionClickParams<T = Recordable<any>> = {
|
||||
code: string;
|
||||
row: T;
|
||||
};
|
||||
export type OnActionClickFn<T = Recordable<any>> = (
|
||||
params: OnActionClickParams<T>,
|
||||
) => void;
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
@ -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',
|
||||
|
||||
@ -159,6 +159,78 @@ export namespace QqbotApi {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
aliases: string[];
|
||||
code: string;
|
||||
cooldownMs: number;
|
||||
defaultParams?: Recordable<any>;
|
||||
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<any> | 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<any>;
|
||||
matched: boolean;
|
||||
message?: string;
|
||||
output?: Recordable<any>;
|
||||
replyText?: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
description?: string;
|
||||
key: string;
|
||||
name: string;
|
||||
operationCount: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface PluginOperation {
|
||||
cacheTtlMs?: number;
|
||||
description?: string;
|
||||
inputSchema?: Recordable<any>;
|
||||
key: string;
|
||||
name: string;
|
||||
outputSchema?: Recordable<any>;
|
||||
pluginKey: string;
|
||||
}
|
||||
|
||||
export interface PluginHealth {
|
||||
checkedAt: string;
|
||||
message?: string;
|
||||
status: 'degraded' | 'healthy' | 'offline';
|
||||
}
|
||||
|
||||
export type Query = Recordable<any>;
|
||||
}
|
||||
|
||||
@ -339,3 +411,59 @@ export function deleteQqbotPermission(
|
||||
`/qqbot/permission/${kind}/delete?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotCommandList(params: QqbotApi.Query) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotApi.Command>>(
|
||||
'/qqbot/command/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function createQqbotCommand(data: QqbotApi.CommandBody) {
|
||||
return requestClient.post<string>('/qqbot/command/save', data);
|
||||
}
|
||||
|
||||
export function updateQqbotCommand(data: QqbotApi.CommandBody) {
|
||||
return requestClient.post<boolean>('/qqbot/command/update', data);
|
||||
}
|
||||
|
||||
export function deleteQqbotCommand(id: string) {
|
||||
return requestClient.post<boolean>(`/qqbot/command/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function toggleQqbotCommand(id: string, enabled: boolean) {
|
||||
return requestClient.post<boolean>(
|
||||
`/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<QqbotApi.CommandTestResult>(
|
||||
'/qqbot/command/test',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotPluginList() {
|
||||
return requestClient.get<QqbotApi.Plugin[]>('/qqbot/plugin/list');
|
||||
}
|
||||
|
||||
export function getQqbotPluginOperationList(pluginKey?: string) {
|
||||
return requestClient.get<QqbotApi.PluginOperation[]>(
|
||||
'/qqbot/plugin/operation/list',
|
||||
{ params: { pluginKey } },
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotPluginHealth(pluginKey?: string) {
|
||||
return requestClient.get<QqbotApi.PluginHealth[]>('/qqbot/plugin/health', {
|
||||
params: { pluginKey },
|
||||
});
|
||||
}
|
||||
|
||||
@ -870,6 +870,7 @@ export default defineComponent({
|
||||
{props.showHeader ? (
|
||||
<KtTableHeader title={props.tableTitle}>
|
||||
{{
|
||||
controls: () => slots.headerControls?.(context),
|
||||
settings: renderHeaderSettings,
|
||||
title: () => slots.title?.(),
|
||||
toolbar: renderHeaderButtons,
|
||||
|
||||
@ -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 (
|
||||
<div class="kt-table__header">
|
||||
<div class="kt-table__header-align">
|
||||
<div class="kt-table__header-title">{title}</div>
|
||||
<div class="kt-table__header-button">{toolbar}</div>
|
||||
</div>
|
||||
{settings ? (
|
||||
<div class="kt-table__header-toolbar">
|
||||
<ADivider class="kt-table__header-divider" type="vertical" />
|
||||
{settings}
|
||||
<div class="kt-table__header-layout">
|
||||
<div class="kt-table__header-content">
|
||||
{title ? (
|
||||
<div class="kt-table__header-title-row">
|
||||
<div class="kt-table__header-title">{title}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{controls ? (
|
||||
<div class="kt-table__header-controls">{controls}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{toolbar || settings ? (
|
||||
<div class="kt-table__header-actions">
|
||||
{toolbar ? (
|
||||
<div class="kt-table__header-button">{toolbar}</div>
|
||||
) : null}
|
||||
{settings ? (
|
||||
<div class="kt-table__header-toolbar">
|
||||
{toolbar ? (
|
||||
<ADivider
|
||||
class="kt-table__header-divider"
|
||||
type="vertical"
|
||||
/>
|
||||
) : null}
|
||||
{settings}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 <th {...attrs}>{slots.default?.()}</th>;
|
||||
}
|
||||
|
||||
if (!props.onResize) {
|
||||
return h(
|
||||
'th',
|
||||
{
|
||||
...attrs,
|
||||
style: {
|
||||
return (
|
||||
<th
|
||||
{...attrs}
|
||||
style={{
|
||||
...(attrs.style as Record<string, unknown> | undefined),
|
||||
width: `${props.width}px`,
|
||||
},
|
||||
},
|
||||
slots.default?.(),
|
||||
}}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
'th',
|
||||
{
|
||||
...attrs,
|
||||
class: ['kt-table__resizable-title', attrs.class],
|
||||
onClickCapture: handleClickCapture,
|
||||
style: {
|
||||
return (
|
||||
<th
|
||||
{...attrs}
|
||||
class={['kt-table__resizable-title', attrs.class]}
|
||||
onClick={handleHeaderClick}
|
||||
style={{
|
||||
...(attrs.style as Record<string, unknown> | undefined),
|
||||
width: `${props.width}px`,
|
||||
},
|
||||
},
|
||||
[
|
||||
slots.default?.(),
|
||||
h('span', {
|
||||
class: 'kt-table__resizable-handle',
|
||||
onMousedown: handleMouseDown,
|
||||
}),
|
||||
],
|
||||
}}
|
||||
>
|
||||
{slots.default?.()}
|
||||
<span
|
||||
class="kt-table__resizable-handle"
|
||||
onMousedown={handleMouseDown}
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -29,18 +29,6 @@
|
||||
"crop-image": "裁剪图片",
|
||||
"upload-image": "点击上传图片"
|
||||
},
|
||||
"vxeTable": {
|
||||
"title": "Vxe 表格",
|
||||
"basic": "基础表格",
|
||||
"remote": "远程加载",
|
||||
"tree": "树形表格",
|
||||
"fixed": "固定表头/列",
|
||||
"virtual": "虚拟滚动",
|
||||
"editCell": "单元格编辑",
|
||||
"editRow": "行编辑",
|
||||
"custom-cell": "自定义单元格",
|
||||
"form": "搜索表单"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "验证码",
|
||||
"pointSelection": "点选验证",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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<number>();
|
||||
const categoryOptions = ref<TermOption[]>([]);
|
||||
const tagOptions = ref<TermOption[]>([]);
|
||||
|
||||
const form = reactive<WordpressBlogApi.ArticleBody>({
|
||||
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<TableColumnType<WordpressBlogApi.Article>> = [
|
||||
{ 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<WordpressBlogApi.Article, ArticleSearchValues>,
|
||||
) {
|
||||
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<WordpressBlogApi.Article, ArticleSearchValues>,
|
||||
) {
|
||||
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<WordpressBlogApi.ArticleBody>();
|
||||
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({
|
||||
}}
|
||||
/>
|
||||
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitArticle}
|
||||
onUpdate:open={(value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="760px"
|
||||
>
|
||||
<Form labelCol={{ span: 4 }} model={form} wrapperCol={{ span: 19 }}>
|
||||
<FormItem label="标题" required>
|
||||
<AInput
|
||||
onUpdate:value={(value: string) => {
|
||||
form.title = value;
|
||||
}}
|
||||
placeholder="请输入文章标题"
|
||||
value={form.title}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<ASelect
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.status = value;
|
||||
}}
|
||||
options={articleStatusOptions}
|
||||
value={form.status}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="别名">
|
||||
<AInput
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.slug = value;
|
||||
}}
|
||||
placeholder="可选,WordPress slug"
|
||||
value={form.slug}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="分类">
|
||||
<ASelect
|
||||
mode="multiple"
|
||||
onUpdate:value={(value: number[] | undefined) => {
|
||||
form.categories = value;
|
||||
}}
|
||||
options={categoryOptions.value}
|
||||
placeholder="选择分类"
|
||||
value={form.categories}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="标签">
|
||||
<ASelect
|
||||
mode="multiple"
|
||||
onUpdate:value={(value: number[] | undefined) => {
|
||||
form.tags = value;
|
||||
}}
|
||||
options={tagOptions.value}
|
||||
placeholder="选择标签"
|
||||
value={form.tags}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="摘要">
|
||||
<ATextArea
|
||||
autoSize={{ maxRows: 4, minRows: 2 }}
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.excerpt = value;
|
||||
}}
|
||||
placeholder="可选,文章摘要"
|
||||
value={form.excerpt}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="内容">
|
||||
<ATextArea
|
||||
autoSize={{ maxRows: 12, minRows: 6 }}
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.content = value;
|
||||
}}
|
||||
placeholder="支持 HTML 内容"
|
||||
value={form.content}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="置顶">
|
||||
<ASwitch
|
||||
checked={form.sticky}
|
||||
onUpdate:checked={(value: boolean) => {
|
||||
form.sticky = value;
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
<ArticleModal title={modalTitle.value}>
|
||||
<ArticleForm class="mx-2" />
|
||||
</ArticleModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
@ -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<number>();
|
||||
const tableRows = ref<WordpressBlogApi.Term[]>([]);
|
||||
const parentOptions = computed(() =>
|
||||
tableRows.value
|
||||
.filter((item) => item.id !== editingId.value)
|
||||
.map((item) => ({ label: item.name, value: item.id })),
|
||||
);
|
||||
|
||||
const form = reactive<WordpressBlogApi.TermBody>({
|
||||
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<WordpressBlogApi.Term, TermSearchValues> = {
|
||||
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<WordpressBlogApi.TermBody>();
|
||||
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({
|
||||
}}
|
||||
/>
|
||||
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitTerm}
|
||||
onUpdate:open={(value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="620px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="名称" required>
|
||||
<AInput
|
||||
onUpdate:value={(value: string) => {
|
||||
form.name = value;
|
||||
}}
|
||||
placeholder={`请输入${props.title}名称`}
|
||||
value={form.name}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="别名">
|
||||
<AInput
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.slug = value;
|
||||
}}
|
||||
placeholder="可选,WordPress slug"
|
||||
value={form.slug}
|
||||
/>
|
||||
</FormItem>
|
||||
{props.kind === 'category' ? (
|
||||
<FormItem label="父级分类">
|
||||
<ASelect
|
||||
allowClear
|
||||
onUpdate:value={(value: number | undefined) => {
|
||||
form.parent = value;
|
||||
}}
|
||||
options={parentOptions.value}
|
||||
placeholder="选择父级分类"
|
||||
value={form.parent}
|
||||
/>
|
||||
</FormItem>
|
||||
) : null}
|
||||
<FormItem label="描述">
|
||||
<ATextArea
|
||||
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||
onUpdate:value={(value: string | undefined) => {
|
||||
form.description = value;
|
||||
}}
|
||||
placeholder="可选"
|
||||
value={form.description}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
<TermModal title={modalTitle.value}>
|
||||
<TermForm class="mx-2" />
|
||||
</TermModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
import { MOCK_TABLE_DATA } from './table-data';
|
||||
|
||||
interface RowType {
|
||||
address: string;
|
||||
age: number;
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'name', title: 'Name' },
|
||||
{ field: 'age', sortable: true, title: 'Age' },
|
||||
{ field: 'nickname', title: 'Nickname' },
|
||||
{ field: 'role', title: 'Role' },
|
||||
{ field: 'address', showOverflow: true, title: 'Address' },
|
||||
],
|
||||
data: MOCK_TABLE_DATA,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
sortConfig: {
|
||||
multiple: true,
|
||||
},
|
||||
};
|
||||
|
||||
const gridEvents: VxeGridListeners<RowType> = {
|
||||
cellClick: ({ row }) => {
|
||||
message.info(`cell-click: ${row.name}`);
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const [Grid, gridApi] = useVbenVxeGrid<RowType>({
|
||||
// 放开注释查看表单组件的类型
|
||||
// formOptions: {
|
||||
// schema: [
|
||||
// {
|
||||
// component: 'Switch',
|
||||
// fieldName: 'name',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
gridEvents,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 放开注释查看当前表格实例的类型
|
||||
// gridApi.grid
|
||||
|
||||
const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
|
||||
const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
|
||||
|
||||
function changeBorder() {
|
||||
gridApi.setGridOptions({
|
||||
border: !showBorder.value,
|
||||
});
|
||||
}
|
||||
|
||||
function changeStripe() {
|
||||
gridApi.setGridOptions({
|
||||
stripe: !showStripe.value,
|
||||
});
|
||||
}
|
||||
|
||||
function changeLoading() {
|
||||
gridApi.setLoading(true);
|
||||
setTimeout(() => {
|
||||
gridApi.setLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="表格组件常用于快速开发数据展示与交互界面,示例数据为静态数据。该组件是对vxe-table进行简单的二次封装,大部分属性与方法与vxe-table保持一致。"
|
||||
title="表格基础示例"
|
||||
>
|
||||
<template #extra>
|
||||
<DocButton path="/components/common-ui/vben-vxe-table" />
|
||||
</template>
|
||||
<Grid table-title="基础列表" table-title-help="提示">
|
||||
<!-- <template #toolbar-actions>
|
||||
<Button class="mr-2" type="primary">左侧插槽</Button>
|
||||
</template> -->
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="changeBorder">
|
||||
{{ showBorder ? '隐藏' : '显示' }}边框
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="changeLoading">
|
||||
显示loading
|
||||
</Button>
|
||||
<Button type="primary" @click="changeStripe">
|
||||
{{ showStripe ? '隐藏' : '显示' }}斑马纹
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,108 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, Switch, Tag } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
open: boolean;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
status: 'error' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'category', title: 'Category', width: 100 },
|
||||
{
|
||||
field: 'imageUrl',
|
||||
slots: { default: 'image-url' },
|
||||
title: 'Image',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellImage' },
|
||||
field: 'imageUrl2',
|
||||
title: 'Render Image',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
field: 'open',
|
||||
slots: { default: 'open' },
|
||||
title: 'Open',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
slots: { default: 'status' },
|
||||
title: 'Status',
|
||||
width: 100,
|
||||
},
|
||||
{ field: 'color', title: 'Color', width: 100 },
|
||||
{ field: 'productName', title: 'Product Name', width: 200 },
|
||||
{ field: 'price', title: 'Price', width: 100 },
|
||||
{
|
||||
field: 'releaseDate',
|
||||
formatter: 'formatDateTime',
|
||||
title: 'Date',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellLink', props: { text: '编辑' } },
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #image-url="{ row }">
|
||||
<Image :src="row.imageUrl" height="30" width="30" />
|
||||
</template>
|
||||
<template #open="{ row }">
|
||||
<Switch v-model="row.open" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.color">{{ row.status }}</Tag>
|
||||
</template>
|
||||
<template #action>
|
||||
<Button type="link">编辑</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,57 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
|
||||
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
|
||||
{
|
||||
editRender: { name: 'input' },
|
||||
field: 'productName',
|
||||
title: 'Product Name',
|
||||
},
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
],
|
||||
editConfig: {
|
||||
mode: 'cell',
|
||||
trigger: 'click',
|
||||
},
|
||||
height: 'auto',
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: true,
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,94 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
|
||||
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
|
||||
{
|
||||
editRender: { name: 'input' },
|
||||
field: 'productName',
|
||||
title: 'Product Name',
|
||||
},
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
{ slots: { default: 'action' }, title: '操作' },
|
||||
],
|
||||
editConfig: {
|
||||
mode: 'row',
|
||||
trigger: 'click',
|
||||
},
|
||||
height: 'auto',
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: true,
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function hasEditStatus(row: RowType) {
|
||||
return gridApi.grid?.isEditByRow(row);
|
||||
}
|
||||
|
||||
function editRowEvent(row: RowType) {
|
||||
gridApi.grid?.setEditRow(row);
|
||||
}
|
||||
|
||||
async function saveRowEvent(row: RowType) {
|
||||
await gridApi.grid?.clearEdit();
|
||||
|
||||
gridApi.setLoading(true);
|
||||
setTimeout(() => {
|
||||
gridApi.setLoading(false);
|
||||
message.success({
|
||||
content: `保存成功!category=${row.category}`,
|
||||
});
|
||||
}, 600);
|
||||
}
|
||||
|
||||
const cancelRowEvent = (_row: RowType) => {
|
||||
gridApi.grid?.clearEdit();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<template v-if="hasEditStatus(row)">
|
||||
<Button type="link" @click="saveRowEvent(row)">保存</Button>
|
||||
<Button type="link" @click="cancelRowEvent(row)">取消</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button type="link" @click="editRowEvent(row)">编辑</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,69 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ fixed: 'left', title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'category', title: 'Category', width: 300 },
|
||||
{ field: 'color', title: 'Color', width: 300 },
|
||||
{ field: 'productName', title: 'Product Name', width: 300 },
|
||||
{ field: 'price', title: 'Price', width: 300 },
|
||||
{
|
||||
field: 'releaseDate',
|
||||
formatter: 'formatDateTime',
|
||||
title: 'DateTime',
|
||||
width: 500,
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 120,
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #action>
|
||||
<Button type="link">编辑</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,127 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
fieldMappingTime: [['date', ['start', 'end']]],
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
defaultValue: '1',
|
||||
fieldName: 'category',
|
||||
label: 'Category',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'productName',
|
||||
label: 'ProductName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'price',
|
||||
label: 'Price',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Color1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'Color2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'color',
|
||||
label: 'Color',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
defaultValue: [dayjs().subtract(7, 'days'), dayjs()],
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
|
||||
const gridOptions: VxeTableGridOptions<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
|
||||
{ field: 'category', title: 'Category' },
|
||||
{ field: 'color', title: 'Color' },
|
||||
{ field: 'productName', title: 'Product Name' },
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
],
|
||||
exportConfig: {},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
message.success(`Query params: ${JSON.stringify(formValues)}`);
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
refresh: true,
|
||||
resizable: true,
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,81 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
|
||||
{ field: 'category', sortable: true, title: 'Category' },
|
||||
{ field: 'color', sortable: true, title: 'Color' },
|
||||
{ field: 'productName', sortable: true, title: 'Product Name' },
|
||||
{ field: 'price', sortable: true, title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime' },
|
||||
],
|
||||
exportConfig: {},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sortBy: sort.field,
|
||||
sortOrder: sort.order,
|
||||
});
|
||||
},
|
||||
},
|
||||
sort: true,
|
||||
},
|
||||
sortConfig: {
|
||||
defaultSort: { field: 'category', order: 'desc' },
|
||||
remote: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="数据列表" table-title-help="提示">
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="() => gridApi.query()">
|
||||
刷新当前页面
|
||||
</Button>
|
||||
<Button type="primary" @click="() => gridApi.reload()">
|
||||
刷新并返回第一页
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -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',
|
||||
},
|
||||
];
|
||||
@ -1,62 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { MOCK_TREE_TABLE_DATA } from './table-data';
|
||||
|
||||
interface RowType {
|
||||
date: string;
|
||||
id: number;
|
||||
name: string;
|
||||
parentId: null | number;
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 70 },
|
||||
{ field: 'name', minWidth: 300, title: 'Name', treeNode: true },
|
||||
{ field: 'size', title: 'Size' },
|
||||
{ field: 'type', title: 'Type' },
|
||||
{ field: 'date', title: 'Date' },
|
||||
],
|
||||
data: MOCK_TREE_TABLE_DATA,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
const expandAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(true);
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Grid table-title="数据列表" table-title-help="提示">
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="expandAll">
|
||||
展开全部
|
||||
</Button>
|
||||
<Button type="primary" @click="collapseAll"> 折叠全部 </Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,66 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
interface RowType {
|
||||
id: number;
|
||||
name: string;
|
||||
role: string;
|
||||
sex: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 70 },
|
||||
{ field: 'name', title: 'Name' },
|
||||
{ field: 'role', title: 'Role' },
|
||||
{ field: 'sex', title: 'Sex' },
|
||||
],
|
||||
data: [],
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
showOverflow: true,
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
// 模拟行数据
|
||||
const loadList = (size = 200) => {
|
||||
try {
|
||||
const dataList: RowType[] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
dataList.push({
|
||||
id: 10_000 + i,
|
||||
name: `Test${i}`,
|
||||
role: 'Developer',
|
||||
sex: '男',
|
||||
});
|
||||
}
|
||||
gridApi.setGridOptions({ data: dataList });
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
// Implement user-friendly error handling
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadList(1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
@ -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<string>();
|
||||
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<QqbotApi.AccountBody>({
|
||||
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<TableColumnType<QqbotApi.Account>> = [
|
||||
@ -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<QqbotApi.AccountBody>();
|
||||
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({
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<AButton key="close" onClick={closeScanModal}>
|
||||
关闭
|
||||
</AButton>,
|
||||
<AButton
|
||||
disabled={!scanState.sessionId}
|
||||
key="refresh"
|
||||
loading={scanLoading.value}
|
||||
onClick={refreshScanQrcode}
|
||||
>
|
||||
刷新二维码
|
||||
</AButton>,
|
||||
<AButton
|
||||
disabled={!scanState.sessionId}
|
||||
key="check"
|
||||
loading={scanLoading.value}
|
||||
onClick={pollScanStatus}
|
||||
type="primary"
|
||||
>
|
||||
检查状态
|
||||
</AButton>,
|
||||
]}
|
||||
onCancel={closeScanModal}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
if (value) {
|
||||
scanModalOpen.value = value;
|
||||
return;
|
||||
}
|
||||
closeScanModal();
|
||||
},
|
||||
}}
|
||||
open={scanModalOpen.value}
|
||||
<ScanModal
|
||||
title={scanTitle.value}
|
||||
width="520px"
|
||||
v-slots={{
|
||||
footer: () => [
|
||||
<AButton key="close" onClick={closeScanModal}>
|
||||
关闭
|
||||
</AButton>,
|
||||
<AButton
|
||||
disabled={!scanState.sessionId}
|
||||
key="refresh"
|
||||
loading={scanLoading.value}
|
||||
onClick={refreshScanQrcode}
|
||||
>
|
||||
刷新二维码
|
||||
</AButton>,
|
||||
<AButton
|
||||
disabled={!scanState.sessionId}
|
||||
key="check"
|
||||
loading={scanLoading.value}
|
||||
onClick={pollScanStatus}
|
||||
type="primary"
|
||||
>
|
||||
检查状态
|
||||
</AButton>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
@ -535,77 +594,10 @@ export default defineComponent({
|
||||
</ATypographyLink>
|
||||
) : null}
|
||||
</Space>
|
||||
</AModal>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitAccount}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="620px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="Self ID" required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="NapCat 当前登录 QQ"
|
||||
value={form.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="账号名称">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.name = value;
|
||||
},
|
||||
}}
|
||||
placeholder="便于后台识别"
|
||||
value={form.name}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Token">
|
||||
<AInput.Password
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.accessToken = value;
|
||||
},
|
||||
}}
|
||||
placeholder={
|
||||
editingId.value ? '留空表示不修改' : 'OneBot 反向 WS token'
|
||||
}
|
||||
value={form.accessToken}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.remark = value;
|
||||
},
|
||||
}}
|
||||
value={form.remark}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
</ScanModal>
|
||||
<AccountModal title={modalTitle.value}>
|
||||
<AccountForm class="mx-2" />
|
||||
</AccountModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
612
apps/web-antdv-next/src/views/qqbot/command/list.tsx
Normal file
612
apps/web-antdv-next/src/views/qqbot/command/list.tsx
Normal file
@ -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<string>();
|
||||
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const pluginOperations = ref<QqbotApi.PluginOperation[]>([]);
|
||||
const pluginMetadataLoaded = ref(false);
|
||||
const selectedPluginKey = ref('');
|
||||
const testResult = ref<QqbotApi.CommandTestResult>();
|
||||
let pluginMetadataPromise: Promise<void> | 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<TableColumnType<QqbotApi.Command>> = [
|
||||
{ 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<QqbotApi.Command> = {
|
||||
list: async (params) => await getQqbotCommandList(params),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.Command>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新建命令',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Command:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const rowActions: Array<KtTableRowAction<QqbotApi.Command>> = [
|
||||
{
|
||||
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<QqbotApi.Command>({
|
||||
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<QqbotApi.CommandBody>();
|
||||
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, any> | string) {
|
||||
if (!value) return '';
|
||||
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function parseJsonText(value?: Record<string, any> | 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 () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Command;
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
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;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<CommandModal title={modalTitle.value}>
|
||||
<CommandForm class="mx-2" />
|
||||
</CommandModal>
|
||||
<TestModal title="测试命令">
|
||||
<div class="mx-2">
|
||||
<TestForm />
|
||||
{testResult.value ? (
|
||||
<div class="mt-4 rounded border border-border p-3 text-sm">
|
||||
<div>
|
||||
匹配结果:{testResult.value.matched ? '已匹配' : '未匹配'}
|
||||
</div>
|
||||
{testResult.value.replyText ? (
|
||||
<pre class="mt-2 whitespace-pre-wrap">
|
||||
{testResult.value.replyText}
|
||||
</pre>
|
||||
) : null}
|
||||
{testResult.value.message ? (
|
||||
<div class="mt-2 text-warning">
|
||||
{testResult.value.message}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TestModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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' },
|
||||
|
||||
@ -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<PermissionKind>('allowlist');
|
||||
const activeTargetType = ref<PermissionTargetType>('qq');
|
||||
const configSaving = ref(false);
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const editingId = ref<string>();
|
||||
const permissionConfig = reactive<QqbotApi.PermissionConfig>({
|
||||
const permissionConfig = ref<QqbotApi.PermissionConfig>({
|
||||
allowlistEnabled: false,
|
||||
blocklistEnabled: true,
|
||||
});
|
||||
const form = reactive<QqbotApi.PermissionBody>({
|
||||
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<TableColumnType<QqbotApi.Permission>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
|
||||
@ -110,16 +153,6 @@ export default defineComponent({
|
||||
targetType: activeTargetType.value,
|
||||
}),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.Permission>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新增名单',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Permission:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const rowActions: Array<KtTableRowAction<QqbotApi.Permission>> = [
|
||||
{
|
||||
key: 'edit',
|
||||
@ -141,6 +174,16 @@ export default defineComponent({
|
||||
permissionCodes: ['QqBot:Permission:Delete'],
|
||||
},
|
||||
];
|
||||
const buttons: Array<KtTableButton<QqbotApi.Permission>> = [
|
||||
{
|
||||
icon: <Plus class="kt-table__button-icon" />,
|
||||
key: 'create',
|
||||
label: '新增名单',
|
||||
onClick: openCreate,
|
||||
permissionCodes: ['QqBot:Permission:Create'],
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotApi.Permission>({
|
||||
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<QqbotApi.PermissionBody>();
|
||||
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 () => (
|
||||
<Page autoContentHeight>
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '20px' }}>
|
||||
<span>
|
||||
白名单过滤:
|
||||
<ASwitch
|
||||
checked={permissionConfig.allowlistEnabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
permissionConfig.allowlistEnabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
黑名单过滤:
|
||||
<ASwitch
|
||||
checked={permissionConfig.blocklistEnabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
permissionConfig.blocklistEnabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<AButton loading={configSaving.value} onClick={saveConfig}>
|
||||
保存配置
|
||||
</AButton>
|
||||
/**
|
||||
* 切换权限名单过滤模式,并立刻保存互斥后的配置。
|
||||
*
|
||||
* @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 (
|
||||
<>
|
||||
<div class="kt-table__header-control-group">
|
||||
<ATabs
|
||||
class="kt-table__header-tabs"
|
||||
items={[
|
||||
{ key: 'allowlist', label: '白名单' },
|
||||
{ key: 'blocklist', label: '黑名单' },
|
||||
]}
|
||||
v-model:activeKey={activeKind.value}
|
||||
/>
|
||||
</div>
|
||||
<ATabs
|
||||
activeKey={activeKind.value}
|
||||
items={[
|
||||
{ key: 'allowlist', label: '白名单' },
|
||||
{ key: 'blocklist', label: '黑名单' },
|
||||
]}
|
||||
{...{
|
||||
'onUpdate:activeKey': (value: PermissionKind) => {
|
||||
activeKind.value = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ATabs
|
||||
activeKey={activeTargetType.value}
|
||||
items={permissionTargetTabItems}
|
||||
{...{
|
||||
'onUpdate:activeKey': (value: PermissionTargetType) => {
|
||||
activeTargetType.value = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Permission;
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'targetType') {
|
||||
return 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;
|
||||
},
|
||||
}}
|
||||
<div class="kt-table__header-control-group kt-table__header-control-group--grow">
|
||||
<ATabs
|
||||
class="kt-table__header-tabs"
|
||||
items={permissionTargetTabItems}
|
||||
v-model:activeKey={activeTargetType.value}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 KtTable 按钮区里的权限过滤模式。
|
||||
*/
|
||||
function renderPermissionModeToolbar() {
|
||||
return (
|
||||
<div class="kt-table__header-control-group">
|
||||
<span class="kt-table__header-control-muted">过滤模式</span>
|
||||
<ASwitch
|
||||
checkedChildren="白名单"
|
||||
loading={configSaving.value}
|
||||
unCheckedChildren="黑名单"
|
||||
v-model:checked={permissionModeChecked.value}
|
||||
/>
|
||||
</div>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitPermission}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
);
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Permission;
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'targetType') {
|
||||
return 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 labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="Self ID">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="留空代表全部账号"
|
||||
value={form.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标类型">
|
||||
<ASelect
|
||||
disabled
|
||||
options={qqbotPermissionTargetOptions}
|
||||
value={activeTargetType.value}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={targetIdLabel.value}>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.targetId = value;
|
||||
},
|
||||
}}
|
||||
placeholder={`请填写${targetIdLabel.value}`}
|
||||
value={form.targetId}
|
||||
/>
|
||||
</FormItem>
|
||||
{isPreciseAvailable() && (
|
||||
<>
|
||||
<FormItem label="精确 QQ">
|
||||
<ASwitch
|
||||
checked={form.preciseUser}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.preciseUser = value;
|
||||
if (!value) form.userId = '';
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
{form.preciseUser && (
|
||||
<FormItem label="QQ 号">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.userId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="请填写需要精确匹配的 QQ 号"
|
||||
value={form.userId}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.remark = value;
|
||||
},
|
||||
}}
|
||||
value={form.remark}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
/>
|
||||
<PermissionModal title={modalTitle.value}>
|
||||
<PermissionForm class="mx-2" />
|
||||
</PermissionModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
126
apps/web-antdv-next/src/views/qqbot/plugin/list.tsx
Normal file
126
apps/web-antdv-next/src/views/qqbot/plugin/list.tsx
Normal file
@ -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<Array<{ label: string; value: string }>>([]);
|
||||
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
|
||||
|
||||
const columns: Array<TableColumnType<QqbotApi.PluginOperation>> = [
|
||||
{ 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<QqbotApi.PluginOperation> = {
|
||||
list: async (params) =>
|
||||
await getQqbotPluginOperationList(params.pluginKey),
|
||||
};
|
||||
const buttons: Array<KtTableButton<QqbotApi.PluginOperation>> = [
|
||||
{
|
||||
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<QqbotApi.PluginOperation>({
|
||||
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<string, QqbotApi.Plugin> = {};
|
||||
for (const item of plugins) {
|
||||
nextPluginMap[item.key] = item;
|
||||
}
|
||||
pluginMap.value = nextPluginMap;
|
||||
pluginOptions.value = plugins.map((item) => ({
|
||||
label: `${item.name} (${item.key})`,
|
||||
value: item.key,
|
||||
}));
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.PluginOperation;
|
||||
if (column.key === 'pluginKey') {
|
||||
const plugin = pluginMap.value[row.pluginKey];
|
||||
return plugin ? (
|
||||
<Tag color="processing">
|
||||
{plugin.name} v{plugin.version}
|
||||
</Tag>
|
||||
) : (
|
||||
row.pluginKey
|
||||
);
|
||||
}
|
||||
if (column.key === 'cacheTtlMs') {
|
||||
return row.cacheTtlMs ? `${row.cacheTtlMs} ms` : '-';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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<string>();
|
||||
const form = reactive<QqbotApi.RuleBody>({
|
||||
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<TableColumnType<QqbotApi.Rule>> = [
|
||||
@ -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<QqbotApi.RuleBody>();
|
||||
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({
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitRule}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title={modalTitle.value}
|
||||
width="720px"
|
||||
>
|
||||
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||
<FormItem label="规则名称">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.name = value;
|
||||
},
|
||||
}}
|
||||
value={form.name}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="匹配方式" required>
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (value: QqbotApi.RuleBody['matchType']) => {
|
||||
form.matchType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotRuleMatchOptions}
|
||||
value={form.matchType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="关键词" required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.keyword = value;
|
||||
},
|
||||
}}
|
||||
value={form.keyword}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标范围">
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (
|
||||
value: QqbotApi.RuleBody['targetType'],
|
||||
) => {
|
||||
form.targetType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotRuleTargetOptions}
|
||||
value={form.targetType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="回复内容" required>
|
||||
<ATextArea
|
||||
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
form.replyContent = value;
|
||||
},
|
||||
}}
|
||||
value={form.replyContent}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="优先级">
|
||||
<AInputNumber
|
||||
{...{
|
||||
'onUpdate:value': (value: number) => {
|
||||
form.priority = value || 0;
|
||||
},
|
||||
}}
|
||||
value={form.priority}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="冷却时间">
|
||||
<AInputNumber
|
||||
addonAfter="ms"
|
||||
min={0}
|
||||
{...{
|
||||
'onUpdate:value': (value: number) => {
|
||||
form.cooldownMs = value || 0;
|
||||
},
|
||||
}}
|
||||
value={form.cooldownMs}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="启用">
|
||||
<ASwitch
|
||||
checked={form.enabled}
|
||||
{...{
|
||||
'onUpdate:checked': (value: boolean) => {
|
||||
form.enabled = value;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
<RuleModal title={modalTitle.value}>
|
||||
<RuleForm class="mx-2" />
|
||||
</RuleModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
@ -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<TableColumnType<QqbotApi.SendLog>> = [
|
||||
{ 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({
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
onOk={submitSend}
|
||||
{...{
|
||||
'onUpdate:open': (value: boolean) => {
|
||||
modalOpen.value = value;
|
||||
},
|
||||
}}
|
||||
open={modalOpen.value}
|
||||
title="手动发送"
|
||||
width="620px"
|
||||
>
|
||||
<Form
|
||||
labelCol={{ span: 5 }}
|
||||
model={sendForm}
|
||||
wrapperCol={{ span: 18 }}
|
||||
>
|
||||
<FormItem label="Self ID">
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.selfId = value;
|
||||
},
|
||||
}}
|
||||
placeholder="留空使用默认启用账号"
|
||||
value={sendForm.selfId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标类型">
|
||||
<ASelect
|
||||
{...{
|
||||
'onUpdate:value': (value: 'group' | 'private') => {
|
||||
sendForm.targetType = value;
|
||||
},
|
||||
}}
|
||||
options={qqbotMessageTypeOptions}
|
||||
value={sendForm.targetType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={targetLabel.value} required>
|
||||
<AInput
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.targetId = value;
|
||||
},
|
||||
}}
|
||||
value={sendForm.targetId}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="消息内容" required>
|
||||
<AInput.TextArea
|
||||
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||
{...{
|
||||
'onUpdate:value': (value: string) => {
|
||||
sendForm.message = value;
|
||||
},
|
||||
}}
|
||||
value={sendForm.message}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</AModal>
|
||||
<SendModal title="手动发送">
|
||||
<SendForm class="mx-2" />
|
||||
</SendModal>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
|
||||
@ -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<SystemDeptApi.SystemDept>,
|
||||
options: PermissionOptions = {},
|
||||
): VxeTableGridOptions<SystemDeptApi.SystemDept>['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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -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<SystemMenuApi.SystemMenu>,
|
||||
options: PermissionOptions = {},
|
||||
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -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<T = SystemRoleApi.SystemRole>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,6 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
||||
},
|
||||
pwa: true,
|
||||
pwaOptions: getDefaultPwaOptions(appTitle),
|
||||
vxeTableLazyImport: true,
|
||||
...envConfig,
|
||||
...application,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
|
||||
|
||||
async function viteVxeTableImportsPlugin(): Promise<PluginOption> {
|
||||
return [
|
||||
lazyImport({
|
||||
resolvers: [
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-table',
|
||||
}),
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-pc-ui',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
] as unknown as PluginOption;
|
||||
}
|
||||
|
||||
export { viteVxeTableImportsPlugin };
|
||||
@ -278,11 +278,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
|
||||
* PWA 插件配置
|
||||
*/
|
||||
pwaOptions?: Partial<PwaPluginOptions>;
|
||||
/**
|
||||
* 是否开启 VXE Table 懒加载
|
||||
* @default false
|
||||
*/
|
||||
vxeTableLazyImport?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T extends Record<string, any> = any> {
|
||||
public formApi = {} as ExtendedFormApi;
|
||||
|
||||
// private prevState: null | VxeGridProps = null;
|
||||
public grid = {} as VxeGridInstance<T>;
|
||||
public state: null | VxeGridProps<T> = null;
|
||||
|
||||
public store: Store<VxeGridProps<T>>;
|
||||
|
||||
private isMounted = false;
|
||||
|
||||
private stateHandler: StateHandler;
|
||||
|
||||
constructor(options: VxeGridProps = {}) {
|
||||
const storeState = { ...options };
|
||||
|
||||
const defaultState = getDefaultState();
|
||||
this.store = new Store<VxeGridProps>(
|
||||
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<string, any> = {}) {
|
||||
try {
|
||||
await this.grid.commitProxy('query', toRaw(params));
|
||||
} catch (error) {
|
||||
console.error('Error occurred while querying:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async reload(params: Record<string, any> = {}) {
|
||||
try {
|
||||
await this.grid.commitProxy('reload', toRaw(params));
|
||||
} catch (error) {
|
||||
console.error('Error occurred while reloading:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setGridOptions(options: Partial<VxeGridProps['gridOptions']>) {
|
||||
this.setState({
|
||||
gridOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(isLoading: boolean) {
|
||||
this.setState({
|
||||
gridOptions: {
|
||||
loading: isLoading,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setState(
|
||||
stateOrFn:
|
||||
| ((prev: VxeGridProps<T>) => Partial<VxeGridProps<T>>)
|
||||
| Partial<VxeGridProps<T>>,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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<any>,
|
||||
) {
|
||||
[
|
||||
'query',
|
||||
'querySuccess',
|
||||
'queryError',
|
||||
'queryAll',
|
||||
'queryAllSuccess',
|
||||
'queryAllError',
|
||||
].forEach((key) => {
|
||||
extendProxyOption(key, api, options, getFormValues);
|
||||
});
|
||||
}
|
||||
|
||||
function extendProxyOption(
|
||||
key: string,
|
||||
api: VxeGridApi,
|
||||
options: VxeGridProps,
|
||||
getFormValues: () => Recordable<any>,
|
||||
) {
|
||||
const { proxyConfig } = options;
|
||||
const configFn = (proxyConfig?.ajax as Recordable<any>)?.[key];
|
||||
if (!isFunction(configFn)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const wrapperFn = async (
|
||||
params: Recordable<any>,
|
||||
customValues: Recordable<any>,
|
||||
...args: Recordable<any>[]
|
||||
) => {
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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';
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<T = any> extends VxeTableGridProps<T> {
|
||||
/** 工具栏配置 */
|
||||
toolbarConfig?: ToolbarConfigOptions;
|
||||
}
|
||||
|
||||
export interface SeparatorOptions {
|
||||
show?: boolean;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export interface VxeGridProps<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
> {
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
tableTitle?: string;
|
||||
/**
|
||||
* 标题帮助
|
||||
*/
|
||||
tableTitleHelp?: string;
|
||||
/**
|
||||
* 组件class
|
||||
*/
|
||||
class?: ClassType;
|
||||
/**
|
||||
* vxe-grid class
|
||||
*/
|
||||
gridClass?: ClassType;
|
||||
/**
|
||||
* vxe-grid 配置
|
||||
*/
|
||||
gridOptions?: DeepPartial<VxeTableGridOptions<T>>;
|
||||
/**
|
||||
* vxe-grid 事件
|
||||
*/
|
||||
gridEvents?: DeepPartial<VxeGridListeners<T>>;
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
formOptions?: VbenFormProps<D>;
|
||||
/**
|
||||
* 显示搜索表单
|
||||
*/
|
||||
showSearchForm?: boolean;
|
||||
/**
|
||||
* 搜索表单与表格主体之间的分隔条
|
||||
*/
|
||||
separator?: boolean | SeparatorOptions;
|
||||
}
|
||||
|
||||
export type ExtendedVxeGridApi<
|
||||
D extends Record<string, any> = any,
|
||||
F extends BaseFormComponentType = BaseFormComponentType,
|
||||
> = VxeGridApi<D> & {
|
||||
useStore: <T = NoInfer<VxeGridProps<D, F>>>(
|
||||
selector?: (state: NoInfer<VxeGridProps<any, any>>) => T,
|
||||
) => Readonly<Ref<T>>;
|
||||
};
|
||||
|
||||
export interface SetupVxeTable {
|
||||
configVxeTable: (ui: VxeUIExport) => void;
|
||||
useVbenForm: typeof useVbenForm;
|
||||
}
|
||||
@ -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<T> = {
|
||||
[K in keyof VxeGridSlots<T> as K extends 'form'
|
||||
? never
|
||||
: K]: VxeGridSlots<T>[K];
|
||||
};
|
||||
|
||||
export function useVbenVxeGrid<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
>(options: VxeGridProps<T, D>) {
|
||||
// const IS_REACTIVE = isReactive(options);
|
||||
const api = new VxeGridApi(options);
|
||||
const extendedApi: ExtendedVxeGridApi<T, D> = api as ExtendedVxeGridApi<T, D>;
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
};
|
||||
|
||||
const Grid = defineComponent(
|
||||
(props: VxeGridProps<T>, { 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<T>;
|
||||
// 工具栏右侧部分
|
||||
'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||
} & FilteredSlots<T>
|
||||
>,
|
||||
},
|
||||
);
|
||||
// Add reactivity support
|
||||
// if (IS_REACTIVE) {
|
||||
// watch(
|
||||
// () => options,
|
||||
// () => {
|
||||
// api.setState(options);
|
||||
// },
|
||||
// { immediate: true },
|
||||
// );
|
||||
// }
|
||||
|
||||
return [Grid, extendedApi] as const;
|
||||
}
|
||||
|
||||
export type UseVbenVxeGrid = typeof useVbenVxeGrid;
|
||||
@ -1,481 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
VxeGridDefines,
|
||||
VxeGridInstance,
|
||||
VxeGridListeners,
|
||||
VxeGridPropTypes,
|
||||
VxeGridProps as VxeTableGridProps,
|
||||
VxeToolbarPropTypes,
|
||||
} from 'vxe-table';
|
||||
|
||||
import type { SetupContext } from 'vue';
|
||||
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
|
||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
toRaw,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { usePriorityValues } from '@vben/hooks';
|
||||
import { EmptyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
import {
|
||||
cloneDeep,
|
||||
cn,
|
||||
isBoolean,
|
||||
isEqual,
|
||||
mergeWithArrayOverride,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { VxeButton } from 'vxe-pc-ui';
|
||||
import { VxeGrid, VxeUI } from 'vxe-table';
|
||||
|
||||
import { extendProxyOptions } from './extends';
|
||||
import { useTableForm } from './init';
|
||||
|
||||
import 'vxe-table/styles/cssvar.scss';
|
||||
import 'vxe-pc-ui/styles/cssvar.scss';
|
||||
import './style.css';
|
||||
|
||||
interface Props extends VxeGridProps {
|
||||
api: ExtendedVxeGridApi;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const FORM_SLOT_PREFIX = 'form-';
|
||||
|
||||
const TOOLBAR_ACTIONS = 'toolbar-actions';
|
||||
const TOOLBAR_TOOLS = 'toolbar-tools';
|
||||
const TABLE_TITLE = 'table-title';
|
||||
|
||||
const gridRef = useTemplateRef<VxeGridInstance>('gridRef');
|
||||
|
||||
const state = props.api?.useStore?.();
|
||||
|
||||
const {
|
||||
gridOptions,
|
||||
class: className,
|
||||
gridClass,
|
||||
gridEvents,
|
||||
formOptions,
|
||||
tableTitle,
|
||||
tableTitleHelp,
|
||||
showSearchForm,
|
||||
separator,
|
||||
} = usePriorityValues(props, state);
|
||||
|
||||
const { isMobile } = usePreferences();
|
||||
const isSeparator = computed(() => {
|
||||
if (
|
||||
!formOptions.value ||
|
||||
showSearchForm.value === false ||
|
||||
separator.value === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (separator.value === true || separator.value === undefined) {
|
||||
return true;
|
||||
}
|
||||
return separator.value.show !== false;
|
||||
});
|
||||
const separatorBg = computed(() => {
|
||||
return !separator.value ||
|
||||
isBoolean(separator.value) ||
|
||||
!separator.value.backgroundColor
|
||||
? undefined
|
||||
: separator.value.backgroundColor;
|
||||
});
|
||||
const slots: SetupContext['slots'] = useSlots();
|
||||
|
||||
const [Form, formApi] = useTableForm({
|
||||
compact: true,
|
||||
handleSubmit: async () => {
|
||||
const formValues = await formApi.getValues();
|
||||
formApi.setLatestSubmissionValues(toRaw(formValues));
|
||||
props.api.reload(formValues);
|
||||
},
|
||||
handleReset: async () => {
|
||||
const prevValues = await formApi.getValues();
|
||||
await formApi.resetForm();
|
||||
const formValues = await formApi.getValues();
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
// 如果值发生了变化,submitOnChange会触发刷新。所以只在submitOnChange为false或者值没有发生变化时,手动刷新
|
||||
if (isEqual(prevValues, formValues) || !formOptions.value?.submitOnChange) {
|
||||
props.api.reload(formValues);
|
||||
}
|
||||
},
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: computed(() => $t('common.search')),
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
const showTableTitle = computed(() => {
|
||||
return !!slots[TABLE_TITLE]?.() || tableTitle.value;
|
||||
});
|
||||
|
||||
const showToolbar = computed(() => {
|
||||
return (
|
||||
!!slots[TOOLBAR_ACTIONS]?.() ||
|
||||
!!slots[TOOLBAR_TOOLS]?.() ||
|
||||
showTableTitle.value
|
||||
);
|
||||
});
|
||||
|
||||
const toolbarOptions = computed(() => {
|
||||
const slotActions = slots[TOOLBAR_ACTIONS]?.();
|
||||
const slotTools = slots[TOOLBAR_TOOLS]?.();
|
||||
const searchBtn: VxeToolbarPropTypes.ToolConfig = {
|
||||
code: 'search',
|
||||
icon: 'vxe-icon-search',
|
||||
circle: true,
|
||||
status: showSearchForm.value ? 'primary' : undefined,
|
||||
title: showSearchForm.value
|
||||
? $t('common.hideSearchPanel')
|
||||
: $t('common.showSearchPanel'),
|
||||
};
|
||||
// 将搜索按钮合并到用户配置的toolbarConfig.tools中
|
||||
const toolbarConfig: VxeGridPropTypes.ToolbarConfig = {
|
||||
tools: (gridOptions.value?.toolbarConfig?.tools ??
|
||||
[]) as VxeToolbarPropTypes.ToolConfig[],
|
||||
};
|
||||
if (gridOptions.value?.toolbarConfig?.search && !!formOptions.value) {
|
||||
toolbarConfig.tools = Array.isArray(toolbarConfig.tools)
|
||||
? [...toolbarConfig.tools, searchBtn]
|
||||
: [searchBtn];
|
||||
}
|
||||
|
||||
if (!showToolbar.value) {
|
||||
toolbarConfig.enabled = false;
|
||||
return { toolbarConfig };
|
||||
}
|
||||
|
||||
// 强制使用固定的toolbar配置,不允许用户自定义
|
||||
// 减少配置的复杂度,以及后续维护的成本
|
||||
toolbarConfig.slots = {
|
||||
...(slotActions || showTableTitle.value
|
||||
? { buttons: TOOLBAR_ACTIONS }
|
||||
: {}),
|
||||
...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),
|
||||
};
|
||||
return { toolbarConfig };
|
||||
});
|
||||
|
||||
const options = computed(() => {
|
||||
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
|
||||
|
||||
const mergedOptions: VxeTableGridProps = cloneDeep(
|
||||
mergeWithArrayOverride(
|
||||
{},
|
||||
toRaw(toolbarOptions.value),
|
||||
toRaw(gridOptions.value),
|
||||
globalGridConfig,
|
||||
),
|
||||
);
|
||||
|
||||
if (mergedOptions.proxyConfig) {
|
||||
const { ajax } = mergedOptions.proxyConfig;
|
||||
mergedOptions.proxyConfig.enabled = !!ajax;
|
||||
// 不自动加载数据, 由组件控制
|
||||
mergedOptions.proxyConfig.autoLoad = false;
|
||||
}
|
||||
|
||||
if (mergedOptions.pagerConfig) {
|
||||
const mobileLayouts = [
|
||||
'PrevJump',
|
||||
'PrevPage',
|
||||
'Number',
|
||||
'NextPage',
|
||||
'NextJump',
|
||||
] as any;
|
||||
const layouts = [
|
||||
'Total',
|
||||
'Sizes',
|
||||
'Home',
|
||||
...mobileLayouts,
|
||||
'End',
|
||||
] as readonly string[];
|
||||
mergedOptions.pagerConfig = mergeWithArrayOverride(
|
||||
{},
|
||||
mergedOptions.pagerConfig,
|
||||
{
|
||||
pageSize: 20,
|
||||
background: true,
|
||||
pageSizes: [10, 20, 30, 50, 100, 200],
|
||||
className: 'mt-2 w-full',
|
||||
layouts: isMobile.value ? mobileLayouts : layouts,
|
||||
size: 'mini' as const,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (mergedOptions.formConfig) {
|
||||
mergedOptions.formConfig.enabled = false;
|
||||
}
|
||||
return mergedOptions;
|
||||
});
|
||||
|
||||
function onToolbarToolClick(event: VxeGridDefines.ToolbarToolClickEventParams) {
|
||||
if (event.code === 'search') {
|
||||
onSearchBtnClick();
|
||||
}
|
||||
(
|
||||
gridEvents.value?.toolbarToolClick as VxeGridListeners['toolbarToolClick']
|
||||
)?.(event);
|
||||
}
|
||||
|
||||
function onSearchBtnClick() {
|
||||
props.api?.toggleSearchForm?.();
|
||||
}
|
||||
|
||||
const events = computed(() => {
|
||||
return {
|
||||
...gridEvents.value,
|
||||
toolbarToolClick: onToolbarToolClick,
|
||||
};
|
||||
});
|
||||
|
||||
const delegatedSlots = computed(() => {
|
||||
const resultSlots: string[] = [];
|
||||
|
||||
for (const key of Object.keys(slots)) {
|
||||
if (
|
||||
!['empty', 'form', 'loading', TOOLBAR_ACTIONS, TOOLBAR_TOOLS].includes(
|
||||
key,
|
||||
)
|
||||
) {
|
||||
resultSlots.push(key);
|
||||
}
|
||||
}
|
||||
return resultSlots;
|
||||
});
|
||||
|
||||
const delegatedFormSlots = computed(() => {
|
||||
const resultSlots: string[] = [];
|
||||
|
||||
for (const key of Object.keys(slots)) {
|
||||
if (key.startsWith(FORM_SLOT_PREFIX)) {
|
||||
resultSlots.push(key);
|
||||
}
|
||||
}
|
||||
return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
|
||||
});
|
||||
|
||||
const showDefaultEmpty = computed(() => {
|
||||
// 检查是否有原生的 VXE Table 空状态配置
|
||||
const hasEmptyText = options.value.emptyText !== undefined;
|
||||
const hasEmptyRender = options.value.emptyRender !== undefined;
|
||||
|
||||
// 如果有原生配置,就不显示默认的空状态
|
||||
return !hasEmptyText && !hasEmptyRender;
|
||||
});
|
||||
|
||||
async function init() {
|
||||
await nextTick();
|
||||
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
|
||||
const defaultGridOptions: VxeTableGridProps = mergeWithArrayOverride(
|
||||
{},
|
||||
toRaw(gridOptions.value),
|
||||
toRaw(globalGridConfig),
|
||||
);
|
||||
// 内部主动加载数据,防止form的默认值影响
|
||||
const autoLoad = defaultGridOptions.proxyConfig?.autoLoad;
|
||||
const enableProxyConfig = options.value.proxyConfig?.enabled;
|
||||
if (enableProxyConfig && autoLoad) {
|
||||
props.api.grid.commitProxy?.(
|
||||
'query',
|
||||
formOptions.value ? ((await formApi.getValues()) ?? {}) : {},
|
||||
);
|
||||
// props.api.reload(formApi.form?.values ?? {});
|
||||
}
|
||||
|
||||
// form 由 vben-form代替,所以不适配formConfig,这里给出警告
|
||||
const formConfig = gridOptions.value?.formConfig;
|
||||
// 处理某个页面加载多个Table时,第2个之后的Table初始化报出警告
|
||||
// 因为第一次初始化之后会把defaultGridOptions和gridOptions合并后缓存进State
|
||||
if (formConfig && formConfig.enabled) {
|
||||
console.warn(
|
||||
'[Vben Vxe Table]: The formConfig in the grid is not supported, please use the `formOptions` props',
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
props.api?.setState?.({ gridOptions: defaultGridOptions });
|
||||
// form 由 vben-form 代替,所以需要保证query相关事件可以拿到参数
|
||||
extendProxyOptions(props.api, defaultGridOptions, () =>
|
||||
formApi.getLatestSubmissionValues(),
|
||||
);
|
||||
}
|
||||
|
||||
// formOptions支持响应式
|
||||
watch(
|
||||
formOptions,
|
||||
() => {
|
||||
formApi.setState((prev) => {
|
||||
const finalFormOptions: VbenFormProps = mergeWithArrayOverride(
|
||||
{},
|
||||
formOptions.value,
|
||||
prev,
|
||||
);
|
||||
return {
|
||||
...finalFormOptions,
|
||||
collapseTriggerResize: !!finalFormOptions.showCollapseButton,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const isCompactForm = computed(() => {
|
||||
return formApi.getState()?.compact;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
props.api?.mount?.(gridRef.value, formApi);
|
||||
init();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
formApi?.unmount?.();
|
||||
props.api?.unmount?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('bg-card h-full rounded-md', className)">
|
||||
<VxeGrid
|
||||
ref="gridRef"
|
||||
:class="
|
||||
cn(
|
||||
'p-2',
|
||||
{
|
||||
'pt-0': showToolbar && !formOptions,
|
||||
},
|
||||
gridClass,
|
||||
)
|
||||
"
|
||||
v-bind="options"
|
||||
v-on="events"
|
||||
>
|
||||
<!-- 左侧操作区域或者title -->
|
||||
<template v-if="showToolbar" #toolbar-actions="slotProps">
|
||||
<slot v-if="showTableTitle" name="table-title">
|
||||
<div class="mr-1 pl-1 text-[1rem]">
|
||||
{{ tableTitle }}
|
||||
<VbenHelpTooltip v-if="tableTitleHelp" trigger-class="pb-1">
|
||||
{{ tableTitleHelp }}
|
||||
</VbenHelpTooltip>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="toolbar-actions" v-bind="slotProps"> </slot>
|
||||
</template>
|
||||
|
||||
<!-- 继承默认的slot -->
|
||||
<template
|
||||
v-for="slotName in delegatedSlots"
|
||||
:key="slotName"
|
||||
#[slotName]="slotProps"
|
||||
>
|
||||
<slot :name="slotName" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
<template #toolbar-tools="slotProps">
|
||||
<slot name="toolbar-tools" v-bind="slotProps"></slot>
|
||||
<VxeButton
|
||||
icon="vxe-icon-search"
|
||||
circle
|
||||
class="ml-2"
|
||||
v-if="gridOptions?.toolbarConfig?.search && !!formOptions"
|
||||
:status="showSearchForm ? 'primary' : undefined"
|
||||
:title="$t('common.search')"
|
||||
@click="onSearchBtnClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- form表单 -->
|
||||
<template #form>
|
||||
<div
|
||||
v-if="formOptions"
|
||||
v-show="showSearchForm !== false"
|
||||
:class="
|
||||
cn(
|
||||
'relative rounded py-3',
|
||||
isCompactForm
|
||||
? isSeparator
|
||||
? 'pb-8'
|
||||
: 'pb-4'
|
||||
: isSeparator
|
||||
? 'pb-4'
|
||||
: 'pb-0',
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot name="form">
|
||||
<Form>
|
||||
<template
|
||||
v-for="slotName in delegatedFormSlots"
|
||||
:key="slotName"
|
||||
#[slotName]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="`${FORM_SLOT_PREFIX}${slotName}`"
|
||||
v-bind="slotProps"
|
||||
></slot>
|
||||
</template>
|
||||
<template #reset-before="slotProps">
|
||||
<slot name="reset-before" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
<template #submit-before="slotProps">
|
||||
<slot name="submit-before" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
<template #expand-before="slotProps">
|
||||
<slot name="expand-before" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
<template #expand-after="slotProps">
|
||||
<slot name="expand-after" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</Form>
|
||||
</slot>
|
||||
<div
|
||||
v-if="isSeparator"
|
||||
:style="{
|
||||
...(separatorBg ? { backgroundColor: separatorBg } : undefined),
|
||||
}"
|
||||
class="bg-background-deep z-100 absolute -left-2 bottom-1 h-2 w-[calc(100%+1rem)] overflow-hidden md:bottom-2 md:h-3"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- loading -->
|
||||
<template #loading>
|
||||
<slot name="loading">
|
||||
<VbenLoading :spinning="true" />
|
||||
</slot>
|
||||
</template>
|
||||
<!-- 统一控状态 -->
|
||||
<template v-if="showDefaultEmpty" #empty>
|
||||
<slot name="empty">
|
||||
<EmptyIcon class="mx-auto" />
|
||||
<div class="mt-2">{{ $t('common.noData') }}</div>
|
||||
</slot>
|
||||
</template>
|
||||
</VxeGrid>
|
||||
</div>
|
||||
</template>
|
||||
@ -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: {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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调用
|
||||
|
||||
@ -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
|
||||
<script setup lang="ts">
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
```tsx
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50 },
|
||||
{ field: 'name', title: '名称' },
|
||||
{ field: 'age', title: '年龄' },
|
||||
],
|
||||
data: [
|
||||
{ name: '张三', age: 18 },
|
||||
{ name: '李四', age: 20 },
|
||||
],
|
||||
import type { KtTableApi, KtTableButton } from '#/components/ktTable';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
name: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const columns: Array<TableColumnType<Row>> = [
|
||||
{ dataIndex: 'name', key: 'name', title: '名称', width: 200 },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
||||
];
|
||||
|
||||
const api: KtTableApi<Row> = {
|
||||
list: async (params) => {
|
||||
return await getListApi({
|
||||
page: params.pageNo,
|
||||
pageSize: params.pageSize,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
};
|
||||
|
||||
<template>
|
||||
<Grid />
|
||||
</template>
|
||||
const buttons: Array<KtTableButton<Row>> = [
|
||||
{
|
||||
icon: () => h(Plus, { class: 'kt-table__button-icon' }),
|
||||
key: 'create',
|
||||
label: '新增',
|
||||
onClick: onCreate,
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
const [registerTable, tableApi] = useKtTable<Row>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
rowActions,
|
||||
});
|
||||
```
|
||||
|
||||
## 远程加载
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getUserListApi } from '#/api';
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50 },
|
||||
{ field: 'name', title: '名称' },
|
||||
{ field: 'age', title: '年龄' },
|
||||
],
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
const res = await getUserListApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
return {
|
||||
items: res.data.list,
|
||||
total: res.data.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
```tsx
|
||||
<KtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }) => {
|
||||
if (column.key === 'status') {
|
||||
return record.status === 1 ? '启用' : '停用';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## 搜索表单
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
```ts
|
||||
const [registerTable] = useKtTable<Row>({
|
||||
api,
|
||||
columns,
|
||||
formOptions: {
|
||||
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
|
||||
labelInInput: true,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
},
|
||||
{ component: 'Input', fieldName: 'keyword', label: '关键词' },
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
{ label: '停用', value: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
toolbarConfig: {
|
||||
search: true, // 显示搜索面板开关按钮
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const res = await getUserListApi({
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: [...],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 树形表格
|
||||
|
||||
```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<Row>({
|
||||
name: 'requestLogger',
|
||||
onBeforeFetch(params) {
|
||||
console.log(params);
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
const statusModule = defineKtTableModule<Row>({
|
||||
columns: [{ dataIndex: 'status', key: 'status', title: '状态', width: 100 }],
|
||||
hooks: [requestLogger],
|
||||
name: 'statusModule',
|
||||
});
|
||||
|
||||
const [registerTable] = useKtTable<Row>({
|
||||
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` 注册。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user