21 changed files with 1138 additions and 199 deletions
@ -1,47 +0,0 @@ |
|||
import type { |
|||
VbenFormSchema as FormSchema, |
|||
VbenFormProps, |
|||
} from '@vben/common-ui'; |
|||
|
|||
import type { ComponentType } from './component'; |
|||
|
|||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
setupVbenForm<ComponentType>({ |
|||
config: { |
|||
// ant design vue组件库默认都是 v-model:value
|
|||
baseModelPropName: 'value', |
|||
|
|||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
|||
modelPropNameMap: { |
|||
Checkbox: 'checked', |
|||
Radio: 'checked', |
|||
Switch: 'checked', |
|||
Upload: 'fileList', |
|||
}, |
|||
}, |
|||
defineRules: { |
|||
// 输入项目必填国际化适配
|
|||
required: (value, _params, ctx) => { |
|||
if (value === undefined || value === null || value.length === 0) { |
|||
return $t('ui.formRules.required', [ctx.label]); |
|||
} |
|||
return true; |
|||
}, |
|||
// 选择项目必填国际化适配
|
|||
selectRequired: (value, _params, ctx) => { |
|||
if (value === undefined || value === null) { |
|||
return $t('ui.formRules.selectRequired', [ctx.label]); |
|||
} |
|||
return true; |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
const useVbenForm = useForm<ComponentType>; |
|||
|
|||
export { useVbenForm, z }; |
|||
|
|||
export type VbenFormSchema = FormSchema<ComponentType>; |
|||
export type { VbenFormProps }; |
|||
@ -1,71 +0,0 @@ |
|||
import { h } from 'vue'; |
|||
|
|||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; |
|||
|
|||
import { Button, Image } from 'ant-design-vue'; |
|||
|
|||
import { useVbenForm } from './form'; |
|||
|
|||
setupVbenVxeTable({ |
|||
configVxeTable: (vxeUI) => { |
|||
vxeUI.setConfig({ |
|||
grid: { |
|||
align: 'center', |
|||
border: false, |
|||
columnConfig: { |
|||
resizable: true, |
|||
}, |
|||
minHeight: 180, |
|||
formConfig: { |
|||
// 全局禁用vxe-table的表单配置,使用formOptions
|
|||
enabled: false, |
|||
}, |
|||
pagerConfig: { |
|||
pageSize: 10, |
|||
pageSizes: [10, 15, 25, 50, 100], |
|||
}, |
|||
proxyConfig: { |
|||
autoLoad: true, |
|||
response: { |
|||
result: 'items', |
|||
total: 'total', |
|||
list: 'items', |
|||
}, |
|||
showActiveMsg: true, |
|||
showResponseMsg: false, |
|||
}, |
|||
round: true, |
|||
showOverflow: true, |
|||
size: 'small', |
|||
}, |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|||
vxeUI.renderer.add('CellImage', { |
|||
renderTableDefault(_renderOpts, params) { |
|||
const { column, row } = params; |
|||
return h(Image, { src: row[column.field] }); |
|||
}, |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|||
vxeUI.renderer.add('CellLink', { |
|||
renderTableDefault(renderOpts) { |
|||
const { props } = renderOpts; |
|||
return h( |
|||
Button, |
|||
{ size: 'small', type: 'link' }, |
|||
{ default: () => props?.text }, |
|||
); |
|||
}, |
|||
}); |
|||
|
|||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|||
// vxeUI.formats.add
|
|||
}, |
|||
useVbenForm, |
|||
}); |
|||
|
|||
export { useVbenVxeGrid }; |
|||
|
|||
export type * from '@vben/plugins/vxe-table'; |
|||
@ -0,0 +1 @@ |
|||
export * from './notifications'; |
|||
@ -0,0 +1,84 @@ |
|||
import type { Dictionary } from '@abp/core'; |
|||
|
|||
export enum NotificationLifetime { |
|||
OnlyOne = 1, |
|||
Persistent = 0, |
|||
} |
|||
|
|||
export enum NotificationType { |
|||
Application = 0, |
|||
ServiceCallback = 30, |
|||
System = 10, |
|||
User = 20, |
|||
} |
|||
|
|||
export enum NotificationContentType { |
|||
Html = 1, |
|||
Json = 3, |
|||
Markdown = 2, |
|||
Text = 0, |
|||
} |
|||
|
|||
export enum NotificationSeverity { |
|||
Error = 30, |
|||
Fatal = 40, |
|||
Info = 10, |
|||
Success = 0, |
|||
Warn = 20, |
|||
} |
|||
|
|||
export enum NotificationReadState { |
|||
Read = 0, |
|||
UnRead = 1, |
|||
} |
|||
|
|||
interface NotificationData { |
|||
extraProperties: { [key: string]: any }; |
|||
type: string; |
|||
} |
|||
|
|||
interface UserIdentifier { |
|||
userId: string; |
|||
userName?: string; |
|||
} |
|||
|
|||
interface NotificationSendInput { |
|||
culture?: string; |
|||
data: Dictionary<string, any>; |
|||
name: string; |
|||
severity?: NotificationSeverity; |
|||
toUsers?: UserIdentifier[]; |
|||
} |
|||
|
|||
interface NotificationInfo { |
|||
contentType: NotificationContentType; |
|||
creationTime: Date; |
|||
data: NotificationData; |
|||
id: string; |
|||
lifetime: NotificationLifetime; |
|||
name: string; |
|||
severity: NotificationSeverity; |
|||
type: NotificationType; |
|||
} |
|||
|
|||
interface NotificationDto { |
|||
description: string; |
|||
displayName: string; |
|||
lifetime: NotificationLifetime; |
|||
name: string; |
|||
type: NotificationType; |
|||
} |
|||
|
|||
interface NotificationGroupDto { |
|||
displayName: string; |
|||
name: string; |
|||
notifications: NotificationDto[]; |
|||
} |
|||
|
|||
export type { |
|||
NotificationData, |
|||
NotificationDto, |
|||
NotificationGroupDto, |
|||
NotificationInfo, |
|||
NotificationSendInput, |
|||
}; |
|||
@ -1,69 +1,76 @@ |
|||
import { h } from 'vue'; |
|||
import type { VxeGridProps } from '../components/vxe-table/types'; |
|||
|
|||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; |
|||
import { h } from 'vue'; |
|||
|
|||
import { Button, Image } from 'ant-design-vue'; |
|||
|
|||
import { setupVbenVxeTable } from '../components/vxe-table'; |
|||
import { useVbenVxeGrid as useVxeGrid } from '../components/vxe-table/use-vxe-grid'; |
|||
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, |
|||
pagerConfig: { |
|||
pageSize: 10, |
|||
pageSizes: [10, 25, 50, 100], |
|||
}, |
|||
proxyConfig: { |
|||
autoLoad: true, |
|||
response: { |
|||
result: 'items', |
|||
total: 'total', |
|||
list: 'items', |
|||
function useVbenVxeGrid(options: VxeGridProps) { |
|||
setupVbenVxeTable({ |
|||
configVxeTable: (vxeUI) => { |
|||
vxeUI.setConfig({ |
|||
grid: { |
|||
align: 'center', |
|||
border: false, |
|||
columnConfig: { |
|||
resizable: true, |
|||
}, |
|||
showActiveMsg: true, |
|||
showResponseMsg: false, |
|||
formConfig: { |
|||
// 全局禁用vxe-table的表单配置,使用formOptions
|
|||
enabled: false, |
|||
}, |
|||
minHeight: 180, |
|||
pagerConfig: { |
|||
pageSize: 10, |
|||
pageSizes: [10, 25, 50, 100], |
|||
}, |
|||
proxyConfig: { |
|||
autoLoad: true, |
|||
response: { |
|||
result: 'items', |
|||
total: 'total', |
|||
list: 'items', |
|||
}, |
|||
showActiveMsg: true, |
|||
showResponseMsg: false, |
|||
}, |
|||
round: true, |
|||
showOverflow: true, |
|||
size: 'small', |
|||
}, |
|||
round: true, |
|||
showOverflow: true, |
|||
size: 'small', |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|||
vxeUI.renderer.add('CellImage', { |
|||
renderTableDefault(_renderOpts, params) { |
|||
const { column, row } = params; |
|||
return h(Image, { src: row[column.field] }); |
|||
}, |
|||
}); |
|||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|||
!vxeUI.renderer.get('CellImage') && |
|||
vxeUI.renderer.add('CellImage', { |
|||
renderTableDefault(_renderOpts, params) { |
|||
const { column, row } = params; |
|||
return h(Image, { src: row[column.field] }); |
|||
}, |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|||
vxeUI.renderer.add('CellLink', { |
|||
renderTableDefault(renderOpts) { |
|||
const { props } = renderOpts; |
|||
return h( |
|||
Button, |
|||
{ size: 'small', type: 'link' }, |
|||
{ default: () => props?.text }, |
|||
); |
|||
}, |
|||
}); |
|||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|||
!vxeUI.renderer.get('CellLink') && |
|||
vxeUI.renderer.add('CellLink', { |
|||
renderTableDefault(renderOpts) { |
|||
const { props } = renderOpts; |
|||
return h( |
|||
Button, |
|||
{ size: 'small', type: 'link' }, |
|||
{ default: () => props?.text }, |
|||
); |
|||
}, |
|||
}); |
|||
|
|||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|||
// vxeUI.formats.add
|
|||
}, |
|||
useVbenForm, |
|||
}); |
|||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|||
// vxeUI.formats.add
|
|||
}, |
|||
useVbenForm, |
|||
}); |
|||
return useVxeGrid(options); |
|||
} |
|||
|
|||
export { useVbenVxeGrid }; |
|||
|
|||
@ -0,0 +1,127 @@ |
|||
import type { ExtendedFormApi } from '@vben-core/form-ui'; |
|||
import type { VxeGridInstance } from 'vxe-table'; |
|||
|
|||
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: '', |
|||
formOptions: undefined, |
|||
gridClass: '', |
|||
gridEvents: {}, |
|||
gridOptions: {}, |
|||
showSearchForm: true, |
|||
}; |
|||
} |
|||
|
|||
export class VxeGridApi { |
|||
private isMounted = false; |
|||
|
|||
private stateHandler: StateHandler; |
|||
public formApi = {} as ExtendedFormApi; |
|||
|
|||
// private prevState: null | VxeGridProps = null;
|
|||
public grid = {} as VxeGridInstance; |
|||
|
|||
public state: null | VxeGridProps = null; |
|||
|
|||
public store: Store<VxeGridProps>; |
|||
|
|||
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) => Partial<VxeGridProps>) |
|||
| Partial<VxeGridProps>, |
|||
) { |
|||
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(); |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
import type { Recordable } from '@vben/types'; |
|||
import type { VxeGridProps, VxeUIExport } from 'vxe-table'; |
|||
|
|||
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.has('formatDate') && |
|||
vxeUI.formats.add('formatDate', { |
|||
tableCellFormatMethod({ cellValue }) { |
|||
return formatDate(cellValue); |
|||
}, |
|||
}); |
|||
|
|||
!vxeUI.formats.has('formatDateTime') && |
|||
vxeUI.formats.add('formatDateTime', { |
|||
tableCellFormatMethod({ cellValue }) { |
|||
return formatDateTime(cellValue); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export { setupVbenVxeTable } from './init'; |
|||
export type { VxeTableGridOptions } from './types'; |
|||
|
|||
export { default as VbenVxeGrid } from './use-vxe-grid.vue'; |
|||
export type { VxeGridListeners, VxeGridProps } from 'vxe-table'; |
|||
@ -0,0 +1,131 @@ |
|||
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 preference = usePreferences(); |
|||
|
|||
const localMap = { |
|||
'en-US': enUS, |
|||
'zh-CN': zhCN, |
|||
}; |
|||
|
|||
watch( |
|||
[() => preference.theme.value, () => preference.locale.value], |
|||
([theme, locale]) => { |
|||
VxeUI.setTheme(theme === 'dark' ? 'dark' : 'light'); |
|||
VxeUI.setI18n(locale, localMap[locale]); |
|||
VxeUI.setLanguage(locale); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
}, |
|||
); |
|||
|
|||
extendsDefaultFormatter(VxeUI); |
|||
|
|||
configVxeTable(VxeUI); |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
: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-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; |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
import type { ClassType, DeepPartial } from '@vben/types'; |
|||
import type { VbenFormProps } from '@vben-core/form-ui'; |
|||
import type { |
|||
VxeGridListeners, |
|||
VxeGridPropTypes, |
|||
VxeGridProps as VxeTableGridProps, |
|||
VxeUIExport, |
|||
} from 'vxe-table'; |
|||
|
|||
import type { VxeGridApi } from './api'; |
|||
|
|||
import type { Ref } from 'vue'; |
|||
|
|||
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 VxeGridProps { |
|||
/** |
|||
* 组件class |
|||
*/ |
|||
class?: ClassType; |
|||
/** |
|||
* 表单配置 |
|||
*/ |
|||
formOptions?: VbenFormProps; |
|||
/** |
|||
* vxe-grid class |
|||
*/ |
|||
gridClass?: ClassType; |
|||
/** |
|||
* vxe-grid 事件 |
|||
*/ |
|||
gridEvents?: DeepPartial<VxeGridListeners>; |
|||
/** |
|||
* vxe-grid 配置 |
|||
*/ |
|||
gridOptions?: DeepPartial<VxeTableGridOptions>; |
|||
/** |
|||
* 显示搜索表单 |
|||
*/ |
|||
showSearchForm?: boolean; |
|||
/** |
|||
* 标题 |
|||
*/ |
|||
tableTitle?: string; |
|||
/** |
|||
* 标题帮助 |
|||
*/ |
|||
tableTitleHelp?: string; |
|||
} |
|||
|
|||
export type ExtendedVxeGridApi = { |
|||
useStore: <T = NoInfer<VxeGridProps>>( |
|||
selector?: (state: NoInfer<VxeGridProps>) => T, |
|||
) => Readonly<Ref<T>>; |
|||
} & VxeGridApi; |
|||
|
|||
export interface SetupVxeTable { |
|||
configVxeTable: (ui: VxeUIExport) => void; |
|||
useVbenForm: typeof useVbenForm; |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
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'; |
|||
|
|||
export function useVbenVxeGrid(options: VxeGridProps) { |
|||
// const IS_REACTIVE = isReactive(options);
|
|||
const api = new VxeGridApi(options); |
|||
const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi; |
|||
extendedApi.useStore = (selector) => { |
|||
return useStore(api.store, selector); |
|||
}; |
|||
|
|||
const Grid = defineComponent( |
|||
(props: VxeGridProps, { attrs, slots }) => { |
|||
onBeforeUnmount(() => { |
|||
api.unmount(); |
|||
}); |
|||
api.setState({ ...props, ...attrs }); |
|||
return () => h(VxeGrid, { ...props, ...attrs, api: extendedApi }, slots); |
|||
}, |
|||
{ |
|||
inheritAttrs: false, |
|||
name: 'VbenVxeGrid', |
|||
}, |
|||
); |
|||
// Add reactivity support
|
|||
// if (IS_REACTIVE) {
|
|||
// watch(
|
|||
// () => options,
|
|||
// () => {
|
|||
// api.setState(options);
|
|||
// },
|
|||
// { immediate: true },
|
|||
// );
|
|||
// }
|
|||
|
|||
return [Grid, extendedApi] as const; |
|||
} |
|||
|
|||
export type UseVbenVxeGrid = typeof useVbenVxeGrid; |
|||
@ -0,0 +1,394 @@ |
|||
<script lang="ts" setup> |
|||
import type { VbenFormProps } from '@vben-core/form-ui'; |
|||
import type { |
|||
VxeGridDefines, |
|||
VxeGridInstance, |
|||
VxeGridListeners, |
|||
VxeGridPropTypes, |
|||
VxeGridProps as VxeTableGridProps, |
|||
VxeToolbarPropTypes, |
|||
} from 'vxe-table'; |
|||
|
|||
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, mergeWithArrayOverride } from '@vben/utils'; |
|||
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-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 gridRef = useTemplateRef<VxeGridInstance>('gridRef'); |
|||
|
|||
const state = props.api?.useStore?.(); |
|||
|
|||
const { |
|||
class: className, |
|||
formOptions, |
|||
gridClass, |
|||
gridEvents, |
|||
gridOptions, |
|||
showSearchForm, |
|||
tableTitle, |
|||
tableTitleHelp, |
|||
} = usePriorityValues(props, state); |
|||
|
|||
const { isMobile } = usePreferences(); |
|||
|
|||
const slots = useSlots(); |
|||
|
|||
const [Form, formApi] = useTableForm({ |
|||
commonConfig: { |
|||
componentProps: { |
|||
class: 'w-full', |
|||
}, |
|||
}, |
|||
compact: true, |
|||
handleReset: async () => { |
|||
await formApi.resetForm(); |
|||
const formValues = formApi.form.values; |
|||
formApi.setLatestSubmissionValues(formValues); |
|||
props.api.reload(formValues); |
|||
}, |
|||
handleSubmit: async () => { |
|||
const formValues = formApi.form.values; |
|||
formApi.setLatestSubmissionValues(toRaw(formValues)); |
|||
props.api.reload(formValues); |
|||
}, |
|||
showCollapseButton: true, |
|||
submitButtonOptions: { |
|||
content: $t('common.query'), |
|||
}, |
|||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', |
|||
}); |
|||
|
|||
const showTableTitle = computed(() => { |
|||
return !!slots.tableTitle?.() || 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 = { |
|||
circle: true, |
|||
code: 'search', |
|||
icon: 'vxe-icon--search', |
|||
status: showSearchForm.value ? 'primary' : undefined, |
|||
title: $t('common.search'), |
|||
}; |
|||
// 将搜索按钮合并到用户配置的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) { |
|||
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, |
|||
{ |
|||
background: true, |
|||
className: 'mt-2 w-full', |
|||
layouts: isMobile.value ? mobileLayouts : layouts, |
|||
pageSize: 20, |
|||
pageSizes: [10, 20, 30, 50, 100, 200], |
|||
size: 'mini' as const, |
|||
}, |
|||
); |
|||
} |
|||
if (mergedOptions.formConfig) { |
|||
mergedOptions.formConfig.enabled = false; |
|||
} |
|||
return mergedOptions; |
|||
}); |
|||
|
|||
function onToolbarToolClick(event: VxeGridDefines.ToolbarToolClickEventParams) { |
|||
if (event.code === 'search') { |
|||
props.api?.toggleSearchForm?.(); |
|||
} |
|||
( |
|||
gridEvents.value?.toolbarToolClick as VxeGridListeners['toolbarToolClick'] |
|||
)?.(event); |
|||
} |
|||
|
|||
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].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, '')); |
|||
}); |
|||
|
|||
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?.('_init', formApi.form?.values ?? {}); |
|||
// 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', |
|||
); |
|||
} |
|||
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> |
|||
|
|||
<!-- form表单 --> |
|||
<template #form> |
|||
<div |
|||
v-if="formOptions" |
|||
v-show="showSearchForm !== false" |
|||
:class="cn('relative rounded py-3', isCompactForm ? 'pb-6' : 'pb-4')" |
|||
> |
|||
<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 |
|||
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 #empty> |
|||
<slot name="empty"> |
|||
<EmptyIcon class="mx-auto" /> |
|||
<div class="mt-2">{{ $t('common.noData') }}</div> |
|||
</slot> |
|||
</template> |
|||
</VxeGrid> |
|||
</div> |
|||
</template> |
|||
Loading…
Reference in new issue