提交 07599ba6 作者: 方治民

feat: 新增 VxeTable 组件实现,完善在线表单数据表属性动态维护示例

上级 90d7de51
......@@ -26,6 +26,7 @@
- [x] 完善网络配置
- [x] 完善权限控制
- [ ] 适配上传/下载接口的自动化生成模板
- [ ] 集成 [Monaco Editor](https://github.com/Microsoft/monaco-editor) 代码编辑器组件
- [ ] 新增 Demo 模块,集成基础 Table 和 Form 组件示例,上传下载等
- [ ] 跟进 Vben 更新,修复 Bug 和新增功能(持续...)
......
......@@ -2,7 +2,7 @@
* Introduces component library styles on demand.
* https://github.com/anncwb/vite-plugin-style-import
*/
import { createStyleImportPlugin } from 'vite-plugin-style-import'
import { createStyleImportPlugin, VxeTableResolve } from 'vite-plugin-style-import'
export function configStyleImportPlugin(_isBuild: boolean) {
if (!_isBuild) {
......@@ -76,6 +76,7 @@ export function configStyleImportPlugin(_isBuild: boolean) {
},
},
],
resolves: [VxeTableResolve()],
})
return styleImportPlugin
}
......@@ -72,6 +72,7 @@
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"echarts": "^5.4.1",
"exceljs": "^4.3.0",
"intro.js": "^6.0.0",
"js-file-download": "^0.4.12",
"lodash-es": "^4.17.21",
......@@ -95,6 +96,9 @@
"vue-json-pretty": "^2.2.3",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2",
"vxe-table": "^4.3.10",
"vxe-table-plugin-export-xlsx": "^3.0.4",
"xe-utils": "^3.5.7",
"xlsx": "^0.18.5"
},
"devDependencies": {
......@@ -155,6 +159,7 @@
"rimraf": "^4.1.2",
"rollup": "^2.79.1",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.60.0",
"sort-package-json": "^2.4.1",
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.5",
......
......@@ -12,13 +12,13 @@
<Icon :icon="item.popConfirm.icon" />
</template>
<div>
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
<Icon :icon="item.icon" v-if="item.icon" :color="getColor(item.color)" />
<span class="ml-1" :style="{ color: getColor(item.color) }">{{ item.text }}</span>
</div>
</a-popconfirm>
<template v-else>
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
<Icon :icon="item.icon" v-if="item.icon" :color="getColor(item.color)" />
<span class="ml-1" :style="{ color: getColor(item.color) }">{{ item.text }}</span>
</template>
</a-menu-item>
<a-menu-divider v-if="item.divider" :key="`d-${item.event}`" />
......@@ -74,6 +74,19 @@
item.onClick?.()
}
function getColor(color: string) {
switch (color) {
case 'error':
return '#ff4d4f'
case 'success':
return '#52c41a'
case 'warning':
return '#e37f4c'
default:
return '#555'
}
}
const getPopConfirmAttrs = computed(() => {
return (attrs) => {
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon'])
......
......@@ -30,7 +30,9 @@
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { propTypes } from '/@/utils/propTypes'
type OptionsItem = { label: string; value: string; disabled?: boolean }
export default defineComponent({
name: 'ApiSelect',
components: {
......@@ -46,10 +48,7 @@
default: null,
},
// api params
params: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
params: propTypes.any.def({}),
// support xxx.xxx.xx
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
......@@ -65,31 +64,37 @@
const emitData = ref<any[]>([])
const attrs = useAttrs()
const { t } = useI18n()
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData)
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField]
const value = get(next, valueField)
prev.push({
...omit(next, [labelField, valueField]),
label: next[labelField],
label: get(next, labelField),
value: numberToString ? `${value}` : value,
})
}
return prev
}, [] as OptionsItem[])
})
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => state.value,
(v) => {
emit('update:value', v)
},
)
watch(
() => props.params,
() => {
......@@ -97,6 +102,7 @@
},
{ deep: true },
)
async function fetch() {
const api = props.api
if (!api || !isFunction(api)) return
......@@ -119,6 +125,7 @@
loading.value = false
}
}
async function handleFetch(visible) {
if (visible) {
if (props.alwaysLoad) {
......@@ -129,12 +136,15 @@
}
}
}
function emitChange() {
emit('options-change', unref(getOptions))
}
function handleChange(_, ...args) {
emitData.value = args
}
return { state, attrs, getOptions, loading, t, handleFetch, handleChange }
},
})
......
......@@ -30,7 +30,7 @@
</template>
<!-- 增加对antdv3.x兼容 -->
<template #bodyCell="data">
<slot name="bodyCell" v-bind="data || {}"></slot>
<slot name="bodyCell" v-bind="data || {}" style="background-color: red"></slot>
</template>
<!-- <template #[`header-${column.dataIndex}`] v-for="(column, index) in columns" :key="index">-->
<!-- <HeaderCell :column="column" />-->
......
......@@ -27,10 +27,11 @@
},
record: {
type: Object as PropType<EditRecordRow>,
default: (): EditRecordRow => ({}),
},
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
default: (): BasicColumn => ({}),
},
index: propTypes.number,
},
......@@ -71,7 +72,7 @@
}
upEditDynamicDisabled(record, column, value)
return {
size: 'small',
size: 'default',
getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
......@@ -263,10 +264,11 @@
if (props.column?.editable || unref(getRowEditable)) {
return
}
const component = unref(getComponent)
if (component.includes('Input')) {
handleCancel()
}
// const component = unref(getComponent)
// if (component.includes('Input')) {
// handleCancel()
// }
handleCancel()
}
// only ApiSelect or TreeSelect
function handleOptionsChange(options: LabelValueOptions) {
......@@ -378,7 +380,7 @@
onOptionsChange={this.handleOptionsChange}
onPressEnter={this.handleEnter}
/>
{!this.getRowEditable && (
{/* {!this.getRowEditable && (
<div class={`${this.prefixCls}__action`}>
<CheckOutlined
class={[`${this.prefixCls}__icon`, 'mx-2']}
......@@ -386,7 +388,7 @@
/>
<CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
</div>
)}
)} */}
</div>
</Spin>
)}
......
import { BasicColumn } from '@/components/Table'
import { ComponentType } from '../../types/componentType'
import { useI18n } from '/@/hooks/web/useI18n'
......@@ -6,12 +7,12 @@ const { t } = useI18n()
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
export function createPlaceholderMessage(component: ComponentType, column?: BasicColumn) {
if (component.includes('Input') || component.includes('AutoComplete')) {
return t('common.inputText')
return t('common.inputText') + (column?.customTitle || '')
}
if (component.includes('Picker')) {
return t('common.chooseText')
return t('common.chooseText') + (column?.customTitle || '')
}
if (
......@@ -22,7 +23,7 @@ export function createPlaceholderMessage(component: ComponentType) {
component.includes('DatePicker') ||
component.includes('TimePicker')
) {
return t('common.chooseText')
return t('common.chooseText') + (column?.customTitle || '')
}
return ''
}
......@@ -35,6 +35,9 @@ export function useCustomRow(
) {
const customRow = (record: Recordable, index: number) => {
return {
'v-click-outside': () => {
console.log(1)
},
onClick: (e: Event) => {
e?.stopPropagation()
function handleClick() {
......
import { withInstall } from '/@/utils'
import vxeBasicTable from './src/VxeBasicTable'
import { VXETable } from 'vxe-table'
import VXETablePluginAntd from './src/components'
import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
import './src/setting'
import '/@/components/VxeTable/src/css/index.scss'
export const VxeBasicTable = withInstall(vxeBasicTable)
export * from 'vxe-table'
export * from './src/types'
VXETable.use(VXETablePluginAntd).use(VXETablePluginExportXLSX)
import { defineComponent } from 'vue'
import { computed, ref } from 'vue'
import { BasicTableProps } from './types'
import { basicProps } from './props'
import { ignorePropKeys } from './const'
import { basicEmits } from './emits'
import XEUtils from 'xe-utils'
import type {
VxeGridInstance,
VxeGridEventProps,
GridMethods,
TableMethods,
TableEditMethods,
TableValidatorMethods,
} from 'vxe-table'
import { Grid as VxeGrid } from 'vxe-table'
import { extendSlots } from '/@/utils/helper/tsxHelper'
import { gridComponentMethodKeys } from './methods'
import { omit } from 'lodash-es'
export default defineComponent({
name: 'VxeBasicTable',
props: basicProps,
emits: basicEmits,
setup(props, { emit, attrs }) {
const tableElRef = ref<VxeGridInstance>()
const emitEvents: VxeGridEventProps = {}
const extendTableMethods = (methodKeys) => {
const funcs: any = {}
methodKeys.forEach((name) => {
funcs[name] = (...args: any[]) => {
const $vxegrid: any = tableElRef.value
if ($vxegrid && $vxegrid[name]) {
return $vxegrid[name](...args)
}
}
})
return funcs
}
const gridExtendTableMethods = extendTableMethods(gridComponentMethodKeys) as GridMethods &
TableMethods &
TableEditMethods &
TableValidatorMethods
basicEmits.forEach((name) => {
const type = XEUtils.camelCase(`on-${name}`) as keyof VxeGridEventProps
emitEvents[type] = (...args: any[]) => emit(name, ...args)
})
/**
* @description: 二次封装需要的所有属性
* 1.部分属性需要和全局属性进行合并
*/
const getBindValues = computed<BasicTableProps>(() => {
const propsData: BasicTableProps = {
...attrs,
...props,
}
return propsData
})
/**
* @description: Table 所有属性
*/
const getBindGridValues = computed(() => {
const omitProps = omit(getBindValues.value, ignorePropKeys)
return {
...omitProps,
...getBindGridEvent,
}
})
/**
* @description: 组件外层class
*/
const getWrapperClass = computed(() => {
return [attrs.class]
})
/**
* @description: 重写Vxe-table 方法
*/
const getBindGridEvent: VxeGridEventProps = {
...emitEvents,
}
return {
getWrapperClass,
getBindGridValues,
tableElRef,
...gridExtendTableMethods,
}
},
render() {
const { tableClass, tableStyle } = this.$props
return (
<div class={`h-full flex flex-col bg-white ${this.getWrapperClass}`}>
<VxeGrid
ref="tableElRef"
class={`vxe-grid_scrollbar ${tableClass}`}
style={tableStyle}
{...this.getBindGridValues}
>
{extendSlots(this.$slots)}
</VxeGrid>
</div>
)
},
})
import type { Component } from 'vue'
import type { ComponentType } from './componentType'
import { ApiSelect, ApiTreeSelect } from '/@/components/Form'
import {
Input,
Select,
Radio,
Checkbox,
AutoComplete,
Cascader,
DatePicker,
InputNumber,
Switch,
TimePicker,
TreeSelect,
Rate,
Empty,
} from 'ant-design-vue'
import { Button } from '/@/components/Button'
const componentMap = new Map<ComponentType, Component>()
componentMap.set('AButton', Button)
componentMap.set('AInput', Input)
componentMap.set('AInputSearch', Input.Search)
componentMap.set('AInputNumber', InputNumber)
componentMap.set('AAutoComplete', AutoComplete)
componentMap.set('ASelect', Select)
componentMap.set('ATreeSelect', TreeSelect)
componentMap.set('ASwitch', Switch)
componentMap.set('ARadioGroup', Radio.Group)
componentMap.set('ACheckboxGroup', Checkbox.Group)
componentMap.set('ACascader', Cascader)
componentMap.set('ARate', Rate)
componentMap.set('ADatePicker', DatePicker)
componentMap.set('AMonthPicker', DatePicker.MonthPicker)
componentMap.set('ARangePicker', DatePicker.RangePicker)
componentMap.set('AWeekPicker', DatePicker.WeekPicker)
componentMap.set('AYearPicker', DatePicker.YearPicker)
componentMap.set('ATimePicker', TimePicker)
componentMap.set('AApiSelect', ApiSelect)
componentMap.set('AApiTreeSelect', ApiTreeSelect)
componentMap.set('AEmpty', Empty)
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component)
}
export function del(compName: ComponentType) {
componentMap.delete(compName)
}
export { componentMap }
export type ComponentType =
| 'AInput'
| 'AInputNumber'
| 'ASelect'
| 'AApiSelect'
| 'ATreeSelect'
| 'AApiTreeSelect'
| 'ARadioGroup'
| 'ACheckboxGroup'
| 'AAutoComplete'
| 'ACascader'
| 'ADatePicker'
| 'AMonthPicker'
| 'ARangePicker'
| 'AWeekPicker'
| 'ATimePicker'
| 'AYearPicker'
| 'ASwitch'
| 'ARate'
| 'AInputSearch'
| 'AButton'
| 'AEmpty'
import XEUtils from 'xe-utils'
import { createDefaultRender, createEditRender, createFormItemRender } from './common'
export default {
renderDefault: createDefaultRender({}, (_, params) => {
return {
params: XEUtils.get(params, 'row'),
}
}),
renderEdit: createEditRender({}, (_, params) => {
return {
params: XEUtils.get(params, 'row'),
}
}),
renderItemContent: createFormItemRender({}, (_, params) => {
return {
params: XEUtils.get(params, 'row'),
}
}),
}
import {
createEditRender,
createDefaultRender,
createFilterRender,
createDefaultFilterRender,
createFormItemRender,
} from './common'
export default {
autofocus: 'input.ant-input',
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
}
import { h } from 'vue'
import { FormItemContentRenderParams, FormItemRenderOptions, VxeGlobalRendererHandles } from 'vxe-table'
import XEUtils from 'xe-utils'
import { cellText, createEvents, createProps, getComponent } from './common'
const COMPONENT_NAME = 'AButton'
export function createEditRender() {
return function (
renderOpts: VxeGlobalRendererHandles.RenderEditOptions,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { attrs } = renderOpts
const Component = getComponent(COMPONENT_NAME)
return [
h(Component, {
...attrs,
...createProps(renderOpts, null),
...createEvents(renderOpts, params),
}),
]
}
}
export function createDefaultRender() {
return function (
renderOpts: VxeGlobalRendererHandles.RenderEditOptions,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { attrs } = renderOpts
const Component = getComponent(COMPONENT_NAME)
return [
h(
Component,
{
...attrs,
...createProps(renderOpts, null),
...createEvents(renderOpts, params),
},
cellText(renderOpts.content),
),
]
}
}
export function createFormItemRender() {
return function (renderOpts: FormItemRenderOptions, params: FormItemContentRenderParams) {
const { attrs, content } = renderOpts
const { property, $form, data } = params
const props = createProps(renderOpts, null)
const Component = getComponent(COMPONENT_NAME)
return [
h(
Component,
{
...attrs,
...props,
...createEvents(
renderOpts,
params,
(value: any) => {
// 处理 model 值双向绑定
XEUtils.set(data, property, value)
},
() => {
// 处理 change 事件相关逻辑
$form.updateStatus({
...params,
field: property,
})
},
),
},
{
default: () => cellText(content || props.content),
},
),
]
}
}
function createToolbarButtonRender() {
return function (
renderOpts: VxeGlobalRendererHandles.RenderToolOptions,
params: VxeGlobalRendererHandles.RenderButtonParams,
) {
const { attrs } = renderOpts
const { button } = params
const props = createProps(renderOpts, null)
const Component = getComponent(COMPONENT_NAME)
return [
h(
Component,
{
...attrs,
...props,
...createEvents(renderOpts, params),
},
{
default: () => cellText(button?.content || props.content),
},
),
]
}
}
export default {
renderEdit: createEditRender(),
renderDefault: createDefaultRender(),
renderItemContent: createFormItemRender(),
renderToolbarButton: createToolbarButtonRender(),
}
import { FormItemContentRenderParams, FormItemRenderOptions, VxeGlobalRendererHandles } from 'vxe-table'
import { createDefaultRender, createEditRender, createFormItemRender } from './AButton'
function createEditButtonRender() {
return function (
renderOpts: VxeGlobalRendererHandles.RenderEditOptions,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const buttonEditRender = createEditRender()
const { children } = renderOpts
if (children) {
return children.map(
(childRenderOpts: VxeGlobalRendererHandles.RenderEditOptions) =>
buttonEditRender(childRenderOpts, params)[0],
)
}
return []
}
}
function createDefaultButtonRender() {
return function (
renderOpts: VxeGlobalRendererHandles.RenderDefaultOptions,
params: VxeGlobalRendererHandles.RenderDefaultParams,
) {
const buttonDefaultRender = createDefaultRender()
const { children } = renderOpts
if (children) {
return children.map(
(childRenderOpts: VxeGlobalRendererHandles.RenderDefaultOptions) =>
buttonDefaultRender(childRenderOpts, params)[0],
)
}
return []
}
}
function createButtonItemRender() {
return function (renderOpts: FormItemRenderOptions, params: FormItemContentRenderParams) {
const buttonItemRender = createFormItemRender()
const { children } = renderOpts
if (children) {
return children.map(
(childRenderOpts: FormItemRenderOptions) => buttonItemRender(childRenderOpts, params)[0],
)
}
return []
}
}
export default {
renderEdit: createEditButtonRender(),
renderDefault: createDefaultButtonRender(),
renderItemContent: createButtonItemRender(),
}
import { VxeGlobalRendererHandles } from 'vxe-table'
import XEUtils from 'xe-utils'
import { createEditRender, createCellRender, createFormItemRender, createExportMethod } from './common'
function matchCascaderData(index: number, list: any[], values: any[], labels: any[]) {
const val = values[index]
if (list && values.length > index) {
XEUtils.each(list, (item) => {
if (item.value === val) {
labels.push(item.label)
matchCascaderData(++index, item.children, values, labels)
}
})
}
}
function getCascaderCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams,
) {
const { props = {} } = renderOpts
const { row, column } = params
const cellValue = XEUtils.get(row, column.field as string)
const values = cellValue || []
const labels: Array<any> = []
matchCascaderData(0, props.options, values, labels)
return (props.showAllLevels === false ? labels.slice(labels.length - 1, labels.length) : labels).join(
` ${props.separator || '/'} `,
)
}
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getCascaderCellValue),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getCascaderCellValue),
}
import { createFormItemRender } from './common'
export default {
renderItemContent: createFormItemRender(),
}
import { VxeGlobalRendererHandles } from 'vxe-table'
import XEUtils from 'xe-utils'
import { createCellRender, createEditRender, createExportMethod, createFormItemRender } from './common'
export function getDatePickerCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams,
defaultFormat: string,
) {
const { props = {} } = renderOpts
const { row, column } = params
let cellValue = XEUtils.get(row, column.field as string)
if (cellValue) {
cellValue = cellValue.format(props.format || defaultFormat)
}
return cellValue
}
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getDatePickerCellValue, () => {
return ['YYYY-MM-DD']
}),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getDatePickerCellValue, () => {
return ['YYYY-MM-DD']
}),
}
import { h } from 'vue'
import { VxeGlobalRendererHandles } from 'vxe-table'
import { getComponent } from './common'
function createEmptyRender() {
return function (renderOpts: VxeGlobalRendererHandles.RenderEmptyOptions) {
const { name, attrs, props } = renderOpts
const Component = getComponent(name)
return [
h(
'div',
{
class: 'flex items-center justify-center',
},
h(Component, {
...attrs,
...props,
}),
),
]
}
}
export default {
renderEmpty: createEmptyRender(),
}
import {
createEditRender,
createDefaultRender,
createFilterRender,
createDefaultFilterRender,
createFormItemRender,
} from './common'
export default {
autofocus: 'input.ant-input',
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
}
import {
createEditRender,
createFilterRender,
createFormItemRender,
createDefaultFilterRender,
createDefaultRender,
} from './common'
export default {
autofocus: 'input.ant-input-number-input',
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
}
import {
createEditRender,
createDefaultRender,
createFilterRender,
createDefaultFilterRender,
createFormItemRender,
createToolbarToolRender,
} from './common'
export default {
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
renderToolbarTool: createToolbarToolRender(),
}
import { getDatePickerCellValue } from './ADatePicker'
import { createCellRender, createEditRender, createExportMethod, createFormItemRender } from './common'
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getDatePickerCellValue, () => {
return ['YYYY-MM']
}),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getDatePickerCellValue, () => {
return ['YYYY-MM']
}),
}
import { createFormItemRender } from './common'
export default {
renderItemContent: createFormItemRender(),
}
import { VxeColumnPropTypes, VxeGlobalRendererHandles } from 'vxe-table'
import XEUtils from 'xe-utils'
import { createCellRender, createEditRender, createExportMethod, createFormItemRender } from './common'
function getRangePickerCellValue(
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams,
) {
const { props = {} } = renderOpts
const { row, column } = params
let cellValue = XEUtils.get(row, column.field as string)
if (cellValue) {
cellValue = XEUtils.map(cellValue, (date: any) => date.format(props.format || 'YYYY-MM-DD')).join(' ~ ')
}
return cellValue
}
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getRangePickerCellValue),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getRangePickerCellValue),
}
import {
createEditRender,
createDefaultRender,
createFilterRender,
createDefaultFilterRender,
createFormItemRender,
} from './common'
export default {
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
}
import { h } from 'vue'
import XEUtils from 'xe-utils'
import {
createEditRender,
createDefaultRender,
createProps,
createEvents,
createDefaultFilterRender,
createFormItemRender,
getComponent,
} from './common'
export default {
renderDefault: createDefaultRender(),
renderEdit: createEditRender(),
renderFilter(renderOpts, params) {
const { column } = params
const { name, attrs } = renderOpts
const Component = getComponent(name)
return [
h(
'div',
{
class: 'vxe-table--filter-antd-wrapper',
},
column.filters.map((option, oIndex) => {
const optionValue = option.data
return h(Component, {
key: oIndex,
...attrs,
...createProps(renderOpts, optionValue),
...createEvents(
renderOpts,
params,
(value: any) => {
// 处理 model 值双向绑定
option.data = value
},
() => {
// 处理 change 事件相关逻辑
const { $panel } = params
$panel.changeOption(null, XEUtils.isBoolean(option.data), option)
},
),
})
}),
),
]
},
defaultFilterMethod: createDefaultFilterRender(),
renderItemContent: createFormItemRender(),
}
import { getDatePickerCellValue } from './ADatePicker'
import { createEditRender, createCellRender, createFormItemRender, createExportMethod } from './common'
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getDatePickerCellValue, () => {
return ['HH:mm:ss']
}),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getDatePickerCellValue, () => {
return ['HH:mm:ss']
}),
}
import { VxeGlobalRendererHandles } from 'vxe-table'
import XEUtils from 'xe-utils'
import { createEditRender, createCellRender, isEmptyValue, createFormItemRender, createExportMethod } from './common'
function getTreeSelectCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams,
) {
const { props = {} } = renderOpts
const { treeData, treeCheckable } = props
const { row, column } = params
const cellValue = XEUtils.get(row, column.field as string)
if (!isEmptyValue(cellValue)) {
return XEUtils.map(treeCheckable ? cellValue : [cellValue], (value) => {
const matchObj = XEUtils.findTree(treeData, (item: any) => item.value === value, {
children: 'children',
})
return matchObj ? matchObj.item.title : value
}).join(', ')
}
return cellValue
}
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getTreeSelectCellValue),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getTreeSelectCellValue),
}
import { getDatePickerCellValue } from './ADatePicker'
import { createEditRender, createCellRender, createFormItemRender, createExportMethod } from './common'
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getDatePickerCellValue, () => {
return ['YYYY-WW周']
}),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getDatePickerCellValue, () => {
return ['YYYY-WW周']
}),
}
import { getDatePickerCellValue } from './ADatePicker'
import { createEditRender, createCellRender, createFormItemRender, createExportMethod } from './common'
export default {
renderEdit: createEditRender(),
renderCell: createCellRender(getDatePickerCellValue, () => {
return ['YYYY']
}),
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getDatePickerCellValue, () => {
return ['YYYY']
}),
}
import { VXETableCore, VxeGlobalInterceptorHandles } from 'vxe-table'
import AAutoComplete from './AAutoComplete'
import AInput from './AInput'
import AInputNumber from './AInputNumber'
import ASelect from './ASelect'
import ACascader from './ACascader'
import ADatePicker from './ADatePicker'
import AMonthPicker from './AMonthPicker'
import ARangePicker from './ARangePicker'
import AWeekPicker from './AWeekPicker'
import ATreeSelect from './ATreeSelect'
import ATimePicker from './ATimePicker'
import ARate from './ARate'
import ASwitch from './ASwitch'
import ARadioGroup from './ARadioGroup'
import ACheckboxGroup from './ACheckboxGroup'
import AButton from './AButton'
import AButtonGroup from './AButtonGroup'
import AApiSelect from './AApiSelect'
import AEmpty from './AEmpty'
import AInputSearch from './AInputSearch'
import AYearPicker from './AYearPicker'
/**
* 检查触发源是否属于目标节点
*/
function getEventTargetNode(evnt: any, container: HTMLElement, className: string) {
let targetElem
let target = evnt.target
while (target && target.nodeType && target !== document) {
if (
className &&
target.className &&
target.className.split &&
target.className.split(' ').indexOf(className) > -1
) {
targetElem = target
} else if (target === container) {
return { flag: className ? !!targetElem : true, container, targetElem: targetElem }
}
target = target.parentNode
}
return { flag: false }
}
/**
* 事件兼容性处理
*/
function handleClearEvent(
params:
| VxeGlobalInterceptorHandles.InterceptorClearFilterParams
| VxeGlobalInterceptorHandles.InterceptorClearActivedParams
| VxeGlobalInterceptorHandles.InterceptorClearAreasParams,
) {
const { $event } = params
const bodyElem = document.body
if (
// 下拉框
getEventTargetNode($event, bodyElem, 'ant-select-dropdown').flag ||
// 级联
getEventTargetNode($event, bodyElem, 'ant-cascader-menus').flag ||
// 日期
getEventTargetNode($event, bodyElem, 'ant-calendar-picker-container').flag ||
// 时间选择
getEventTargetNode($event, bodyElem, 'ant-time-picker-panel').flag
) {
return false
}
}
/**
* 基于 vxe-table 表格的适配插件,用于兼容 ant-design-vue 组件库
*/
export const VXETablePluginAntd = {
install(vxetablecore: VXETableCore) {
const { interceptor, renderer } = vxetablecore
renderer.mixin({
AAutoComplete,
AInput,
AInputNumber,
ASelect,
ACascader,
ADatePicker,
AMonthPicker,
ARangePicker,
AWeekPicker,
ATimePicker,
ATreeSelect,
ARate,
ASwitch,
ARadioGroup,
ACheckboxGroup,
AButton,
AButtonGroup,
AApiSelect,
AEmpty,
AInputSearch,
AYearPicker,
})
interceptor.add('event.clearFilter', handleClearEvent)
interceptor.add('event.clearActived', handleClearEvent)
interceptor.add('event.clearAreas', handleClearEvent)
},
}
if (typeof window !== 'undefined' && window.VXETable && window.VXETable.use) {
window.VXETable.use(VXETablePluginAntd)
}
export default VXETablePluginAntd
/**
* @description: 传给vxe-table 时需要忽略的prop
*/
export const ignorePropKeys = ['tableClass', 'tableStyle']
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: initial;
}
.vxe-body--row {
.drag {
cursor: move;
}
+ .sortable-ghost,
+ .sortable-chosen {
background-color: #dfecfb !important;
}
}
%ResetBorder {
border: 0;
box-shadow: none;
}
%CompWidth {
& > .ant-input,
& > .ant-input-number,
& > .ant-select,
& > .ant-cascader-picker,
& > .ant-calendar-picker,
& > .ant-time-picker {
width: 100%;
}
}
.vxe-form {
.vxe-form--item-content {
@extend %CompWidth;
}
}
.vxe-table--filter-antd-wrapper {
& > .ant-input,
& > .ant-input-number,
& > .ant-select,
& > .ant-rate {
width: 180px;
}
}
.vxe-cell,
.vxe-tree-cell {
@extend %CompWidth;
& > .ant-rate {
vertical-align: bottom;
.anticon-star {
display: block;
}
}
}
.col--valid-error {
& > .vxe-cell,
& > .vxe-tree-cell {
& > .ant-input,
& > .ant-select .ant-input,
& > .ant-select .ant-select-selection,
& > .ant-input-number,
& > .ant-cascader-picker .ant-cascader-input,
& > .ant-calendar-picker .ant-calendar-picker-input {
// border-color: $vxe-table-validate-error-color;
box-shadow: none;
}
}
}
.vxe-table.cell--highlight {
.vxe-cell,
.vxe-tree-cell {
& > .ant-input,
& > .ant-input-number {
padding: 0;
@extend %ResetBorder;
}
& > .ant-select {
.ant-input {
padding: 0;
@extend %ResetBorder;
}
}
& > .ant-input-number {
.ant-input-number-input {
padding: 0;
}
.ant-input-number-handler-wrap,
.ant-input-number-handler-down {
@extend %ResetBorder;
}
}
& > .ant-select {
.ant-select-selection {
@extend %ResetBorder;
.ant-select-selection__rendered {
margin: 0;
}
}
}
& > .ant-cascader-picker {
.ant-input {
@extend %ResetBorder;
}
.ant-cascader-picker-label {
padding: 0;
}
}
& > .ant-calendar-picker {
.ant-calendar-picker-input {
padding: 0;
@extend %ResetBorder;
}
}
& > .ant-time-picker {
.ant-time-picker-input {
padding: 0;
@extend %ResetBorder;
}
}
}
}
@import './common.scss';
@import './variable.scss';
@import './scrollbar.scss';
@import './toolbar.scss';
@import './component.scss';
@import 'vxe-table/styles/index.scss';
.vxe-grid_scrollbar {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background-color: #ffffff;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 5px;
border: 1px solid #f1f1f1;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}
::-webkit-scrollbar-thumb:active {
background-color: #a8a8a8;
}
::-webkit-scrollbar-corner {
background-color: #ffffff;
}
}
.vxe-toolbar .vxe-custom--option-wrapper .vxe-custom--footer {
display: flex;
}
.vxe-toolbar .vxe-tools--wrapper,
.vxe-toolbar .vxe-tools--operate button:first-child {
margin: 0;
}
.vxe-toolbar .vxe-tools--wrapper,
.vxe-toolbar .vxe-tools--operate .vxe-button {
margin-left: 1px;
border-radius: 0 !important;
}
.vxe-toolbar .vxe-tools--wrapper,
.vxe-toolbar .vxe-tools--operate button:first-child {
margin-left: 10px;
}
.vxe-toolbar .vxe-tools--wrapper,
.vxe-toolbar .vxe-tools--operate .vxe-custom--wrapper {
margin-left: 1px;
border-radius: 0 !important;
}
.vxe-toolbar .vxe-tools--wrapper,
.vxe-toolbar .vxe-tools--operate .vxe-custom--wrapper .vxe-button {
margin-left: 1px;
}
$vxe-primary-color: rgb(9, 96, 189) !default;
$vxe-table-row-current-background-color: rgba(9, 96, 189, 0.3);
$vxe-table-row-hover-current-background-color: rgba(9, 96, 189, 0.2);
$vxe-table-column-hover-background-color: rgba(9, 96, 189, 0.3);
$vxe-table-column-current-background-color: rgba(9, 96, 189, 0.2);
$vxe-table-validate-error-color: #f56c6c;
import tableEmits from 'vxe-table/es/table/src/emits'
export const basicEmits = [
...tableEmits,
'page-change',
'form-submit',
'form-submit-invalid',
'form-reset',
'form-collapse',
'form-toggle-collapse',
'toolbar-button-click',
'toolbar-tool-click',
'zoom',
//... 如有缺少在此处追加
// xxx
]
import { ComponentType } from './componentType'
import { useI18n } from '/@/hooks/web/useI18n'
const { t } = useI18n()
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (!component) return
if (component.includes('RangePicker')) {
return [t('common.chooseText'), t('common.chooseText')]
}
if (component.includes('Input') || component.includes('Complete') || component.includes('Rate')) {
return t('common.inputText')
} else {
return t('common.chooseText')
}
}
import { GridMethods, TableMethods, TableEditMethods, TableValidatorMethods } from 'vxe-table'
export const gridComponentMethodKeys: (
| keyof GridMethods
| keyof TableMethods
| keyof TableEditMethods
| keyof TableValidatorMethods
)[] = [
// vxe-grid 部分
'dispatchEvent',
'commitProxy',
'getFormItems',
'getPendingRecords',
'zoom',
'isMaximized',
'maximize',
'revert',
'getProxyInfo',
// vxe-table和vxe-grid公共部分
'clearAll',
'syncData',
'updateData',
'loadData',
'reloadData',
'reloadRow',
'loadColumn',
'reloadColumn',
'getRowNode',
'getColumnNode',
'getRowIndex',
'getVTRowIndex',
'getVMRowIndex',
'getColumnIndex',
'getVTColumnIndex',
'getVMColumnIndex',
'createData',
'createRow',
'revertData',
'clearData',
'isInsertByRow',
'isUpdateByRow',
'getColumns',
'getColumnById',
'getColumnByField',
'getTableColumn',
'getData',
'getCheckboxRecords',
'getParentRow',
'getRowSeq',
'getRowById',
'getRowid',
'getTableData',
'hideColumn',
'showColumn',
'resetColumn',
'refreshColumn',
'refreshScroll',
'recalculate',
'closeTooltip',
'isAllCheckboxChecked',
'isAllCheckboxIndeterminate',
'getCheckboxIndeterminateRecords',
'setCheckboxRow',
'isCheckedByCheckboxRow',
'isIndeterminateByCheckboxRow',
'toggleCheckboxRow',
'setAllCheckboxRow',
'getRadioReserveRecord',
'clearRadioReserve',
'getCheckboxReserveRecords',
'clearCheckboxReserve',
'toggleAllCheckboxRow',
'clearCheckboxRow',
'setCurrentRow',
'isCheckedByRadioRow',
'setRadioRow',
'clearCurrentRow',
'clearRadioRow',
'getCurrentRecord',
'getRadioRecord',
'getCurrentColumn',
'setCurrentColumn',
'clearCurrentColumn',
'sort',
'clearSort',
'isSort',
'getSortColumns',
'closeFilter',
'isFilter',
'isRowExpandLoaded',
'clearRowExpandLoaded',
'reloadRowExpand',
'reloadRowExpand',
'toggleRowExpand',
'setAllRowExpand',
'setRowExpand',
'isExpandByRow',
'clearRowExpand',
'clearRowExpandReserve',
'getRowExpandRecords',
'getTreeExpandRecords',
'isTreeExpandLoaded',
'clearTreeExpandLoaded',
'reloadTreeExpand',
'reloadTreeChilds',
'toggleTreeExpand',
'setAllTreeExpand',
'setTreeExpand',
'isTreeExpandByRow',
'clearTreeExpand',
'clearTreeExpandReserve',
'getScroll',
'scrollTo',
'scrollToRow',
'scrollToColumn',
'clearScroll',
'updateFooter',
'updateStatus',
'setMergeCells',
'removeInsertRow',
'removeMergeCells',
'getMergeCells',
'clearMergeCells',
'setMergeFooterItems',
'removeMergeFooterItems',
'getMergeFooterItems',
'clearMergeFooterItems',
'openTooltip',
'focus',
'blur',
'connect',
// vxe-table-edit部分
'insert',
'insertAt',
'remove',
'removeCheckboxRow',
'removeRadioRow',
'removeCurrentRow',
'getRecordset',
'getInsertRecords',
'getRemoveRecords',
'getUpdateRecords',
'getEditRecord',
'getSelectedCell',
'clearSelected',
'isEditByRow',
'setEditRow',
'setEditCell',
'setSelectCell',
// vxe-table-validator
'clearValidate',
'fullValidate',
'validate',
//... 如有缺少在此处追加
// xxx
]
import { VxeGridPropTypes, VxeTablePropTypes } from 'vxe-table'
import tableProps from 'vxe-table/es/table/src/props'
import { CSSProperties } from 'vue'
/**
* @description: table二次开发需要后,需要接受的所有prop属性
*/
export const basicProps = {
...tableProps,
columns: Array as PropType<VxeGridPropTypes.Columns>,
// proxyConfig: {
// type: Object as PropType<VxeGridPropTypes.ProxyConfig>,
// default: () => ({}),
// },
pagerConfig: {
type: Object as PropType<VxeGridPropTypes.PagerConfig>,
default: () => ({}),
},
toolbarConfig: {
type: Object as PropType<VxeGridPropTypes.ToolbarConfig>,
default: () => ({}),
},
formConfig: {
type: Object as PropType<VxeGridPropTypes.FormConfig>,
default: () => ({}),
},
zoomConfig: {
type: Object as PropType<VxeGridPropTypes.ZoomConfig>,
default: () => ({}),
},
printConfig: {
type: Object as PropType<VxeTablePropTypes.PrintConfig>,
default: () => ({}),
},
exportConfig: {
type: Object as PropType<VxeTablePropTypes.ExportConfig>,
default: () => ({}),
},
importConfig: {
type: Object as PropType<VxeTablePropTypes.ImportConfig>,
default: () => ({}),
},
size: String as PropType<VxeGridPropTypes.Size>,
tableClass: {
type: String,
default: '',
},
tableStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
}
import { VXETable } from '..'
import componentSetting from '/@/settings/componentSetting'
VXETable.setup(componentSetting.vxeTable)
import { CSSProperties } from 'vue'
import { VxeGridProps } from 'vxe-table'
export type BasicTableProps = VxeGridProps & {
tableClass?: string
tableStyle?: CSSProperties
}
import type { App } from 'vue'
import { Button } from './Button'
import { Input, Layout } from 'ant-design-vue'
import { Input, Layout, Select } from 'ant-design-vue'
// 注入 vxe-table
import VXETable from 'vxe-table'
export function registerGlobComp(app: App) {
app.use(Input).use(Button).use(Layout)
app.use(Input).use(Button).use(Select).use(Layout).use(VXETable)
}
......@@ -205,3 +205,7 @@ html[data-theme='light'] {
width: 172px;
}
}
.ant-tabs-tab .anticon {
margin-right: 6px;
}
......@@ -42,6 +42,52 @@ export default {
return data
},
},
vxeTable: {
table: {
border: true,
stripe: true,
columnConfig: {
resizable: true,
isCurrent: false,
isHover: false,
},
rowConfig: {
isCurrent: false,
isHover: true,
},
emptyRender: {
name: 'AEmpty',
},
printConfig: {},
exportConfig: {},
customConfig: {
storage: true,
},
},
grid: {
toolbarConfig: {
enabled: false,
export: false,
zoom: false,
print: false,
refresh: false,
custom: false,
},
pagerConfig: {
pageSizes: [20, 50, 100, 500],
pageSize: 20,
autoHidden: true,
},
proxyConfig: {
form: true,
props: {
result: 'items',
total: 'total',
},
},
zoomConfig: {},
},
},
// scrollbar setting
scrollbar: {
// Whether to use native scroll bar
......
import { Tag } from 'ant-design-vue'
import { Checkbox, SelectProps, Tag } from 'ant-design-vue'
import { BasicColumn, FormProps } from '@/components/Table'
export interface TypeOption {
label: string
value: string
disabled?: boolean
}
import { VxeGridPropTypes } from '@/components/VxeTable'
import { Icon } from '@/components/Icon'
/**
* 支持的数据源类型
*/
export const typeOptions: TypeOption[] = [
{
label: '单表',
value: 'Single',
},
export const databaseTypeOptions: SelectProps['options'] = [
{
label: '主表',
value: 'Master',
disabled: true,
label: '常规表',
value: 'General',
options: [
{
label: '常规单表',
value: 'SingleGeneral',
},
{
label: '常规主表',
value: 'MasterGeneral',
disabled: true,
},
{
label: '常规从表',
value: 'SlaveGeneral',
disabled: true,
},
],
},
{
label: '从表',
value: 'Slave',
disabled: true,
label: '时序表',
value: 'TimeSeries',
options: [
{
label: '时序单表',
value: 'SingleTimeSeries',
disabled: true,
},
{
label: '业务时序表',
value: 'BusinessTimeSeries',
disabled: true,
},
],
},
]
......@@ -90,7 +108,7 @@ export function getFormConfig(): Partial<FormProps> {
label: `表类型`,
component: 'Select',
componentProps: {
options: typeOptions,
options: databaseTypeOptions,
mode: 'multiple',
style: {
width: '180px',
......@@ -114,3 +132,314 @@ export function getFormConfig(): Partial<FormProps> {
},
}
}
// ================================
export type FieldType =
| 'Integer'
| 'Float'
| 'Double'
| 'BigDecimal'
| 'Char'
| 'String'
| 'Text'
| 'Boolean'
| 'LocalDate'
| 'LocalTime'
| 'LocalDateTime'
| 'Point'
| 'Line'
| 'Polygon'
| 'Geometry'
| 'Blob'
| 'JSONObject'
| 'JSONArray'
/**
* 支持的数据源类型
*/
export const fieldTypeOptions: SelectProps['options'] = [
{
label: '数值类型',
value: 'Number',
options: [
{
label: '整数',
value: 'Integer',
},
{
label: '单精度小数',
value: 'Float',
},
{
label: '双精度小数',
value: 'Double',
},
{
label: '高精度小数',
value: 'BigDecimal',
},
],
},
{
label: '字符串类型',
value: 'Varchar',
options: [
{
label: '字符',
value: 'Char',
},
{
label: '字符串',
value: 'String',
},
{
label: '大文本',
value: 'Text',
},
],
},
{
label: '布尔类型',
value: 'Bool',
options: [
{
label: '布尔',
value: 'Boolean',
},
],
},
{
label: '日期类型',
value: 'Date',
options: [
{
label: '日期',
value: 'LocalDate',
},
{
label: '时间',
value: 'LocalTime',
},
{
label: '日期时间',
value: 'LocalDateTime',
},
],
},
{
label: '空间类型',
value: 'GIS',
options: [
{
label: '坐标点',
value: 'Point',
},
{
label: '空间线',
value: 'Line',
},
{
label: '空间面',
value: 'Polygon',
},
{
label: '空间几何',
value: 'Geometry',
},
],
},
{
label: '其他',
value: 'Other',
options: [
{
label: 'Blob 对象',
value: 'Blob',
},
{
label: 'JSON 对象',
value: 'JSONObject',
},
{
label: 'JSON 数组',
value: 'JSONArray',
},
],
},
]
export interface DataSourceSchema {
field?: string
comment?: string
type?: FieldType
len?: number
decimal?: number
pk?: boolean
empty?: boolean
transient?: boolean
editable?: boolean
disabled?: boolean
[key: string]: any
}
/**
* 获取数据源字段列表
* @returns Column[]
*/
export function getDataSourceSchemaColumns(): VxeGridPropTypes.Columns {
return [
{
type: 'html',
align: 'center',
width: 50,
className: 'drag',
slots: {
default: () => {
return [<Icon icon="mdi:sort" />]
},
},
},
{
type: 'checkbox',
align: 'center',
width: 50,
},
{
type: 'seq',
align: 'center',
width: 50,
},
{
title: '字段名称',
field: 'field',
editRender: {
name: 'AInput',
placeholder: '请点击输入',
},
},
{
title: '字段备注',
field: 'comment',
editRender: {
name: 'AInput',
placeholder: '请点击输入',
},
},
{
title: '字段类型',
field: 'type',
editRender: {
name: 'ASelect',
placeholder: '请点击选择',
optionGroups: fieldTypeOptions,
},
},
{
title: '字段长度',
field: 'len',
editRender: {
name: 'AInputNumber',
},
},
{
title: '小数点',
field: 'decimal',
editRender: {
name: 'AInputNumber',
},
},
{
title: '是否主键',
field: 'pk',
align: 'center',
slots: {
default: ({ row }) => {
return [<Checkbox v-model:checked={row.pk} disabled={row.disabled || row.disablePK} />]
},
},
},
{
title: '允许空值',
field: 'empty',
align: 'center',
slots: {
default: ({ row }) => {
return [<Checkbox v-model:checked={row.empty} disabled={row.disabled || row.disableEmpty} />]
},
},
},
{
title: '是否同步数据库',
field: 'transient',
align: 'center',
slots: {
default: ({ row }) => {
return [
<Checkbox v-model:checked={row.transient} disabled={row.disabled || row.disableTransient} />,
]
},
},
},
]
}
export function getDefaultSchemaFieldDataSourcesByType(type: string): DataSourceSchema[] {
if (type === 'SingleGeneral') {
return [
{
id: 'id',
field: 'id',
comment: '主键',
type: 'String',
len: 32,
pk: true,
empty: false,
transient: true,
disabled: true,
},
{
id: 'createBy',
field: 'createBy',
comment: '创建人',
type: 'String',
len: 32,
pk: false,
empty: false,
transient: true,
},
{
id: 'createTime',
field: 'createTime',
comment: '创建时间',
type: 'LocalDateTime',
len: 0,
decimal: 0,
pk: false,
empty: false,
transient: true,
},
{
id: 'updateBy',
field: 'updateBy',
comment: '修改人',
type: 'String',
len: 32,
pk: false,
empty: false,
transient: true,
},
{
id: 'updateTime',
field: 'updateTime',
comment: '修改时间',
type: 'LocalDateTime',
len: 0,
decimal: 0,
pk: false,
empty: false,
transient: true,
},
]
}
return []
}
<script lang="ts" setup>
import { nanoid } from 'nanoid'
import { Button, Space, Tabs, TabPane } from 'ant-design-vue'
import { Icon } from '@/components/Icon'
import { BasicForm, useForm } from '/@/components/Form'
import { BasicTable, EditRecordRow, useTable } from '/@/components/Table'
import { BasicModal, useModalInner } from '/@/components/Modal'
// @ts-ignore
import vClickOutside from '/@/directives/clickOutside'
import { databaseTypeOptions, getDataSourceSchemaColumns, getDefaultSchemaFieldDataSourcesByType } from './data'
const [registerModal] = useModalInner((data) => {
console.log(data)
// TODO:
// 1. 根据传递的 ID 参数,查询表配置详细信息
// 2. 赋值填充到表单对象中
})
const defaultTableType = databaseTypeOptions[0].options[0].value
const [registerEditForm] = useForm({
colon: true,
labelWidth: 150,
showActionButtonGroup: false,
schemas: [
{
field: 'name',
label: '表名',
required: true,
component: 'Input',
colProps: {
span: 8,
},
},
{
field: 'describe',
label: '表描述',
required: true,
component: 'Input',
colProps: {
span: 8,
},
},
{
field: 'type',
label: '表类型',
required: true,
component: 'Select',
componentProps: {
options: databaseTypeOptions,
defaultValue: defaultTableType,
},
colProps: {
span: 8,
},
},
],
})
const [registerDatabaseSchemaTable, { insertTableDataRecord, updateTableDataRecord, getDataSource }] = useTable({
rowKey: 'id',
bordered: true,
showTableSetting: true,
tableSetting: { fullScreen: true },
pagination: false,
maxHeight: 400,
columns: getDataSourceSchemaColumns(),
dataSource: getDefaultSchemaFieldDataSourcesByType(defaultTableType),
})
const insertField = () => {
insertTableDataRecord({
id: nanoid(),
field: null,
type: null,
comment: null,
len: null,
decimal: null,
pk: false,
empty: false,
transient: true,
editable: true,
})
onRowClick(getDataSource().slice(-1)[0])
}
const editFieldChange = (item: Recordable) => {
if (item.column?.dataIndex === 'type') {
const alter = { decimal: null, len: null }
if (item.value === 'Char') {
alter.len = 1
} else if (item.value === 'String') {
alter.len = 256
} else if (['Integer', 'Float', 'Double', 'BigDecimal'].includes(item.value)) {
alter.len = 10
item.value !== 'Integer' && (alter.decimal = 0)
}
updateTableDataRecord(item.record.id, {
...alter,
})
}
if (item.column?.dataIndex === 'field') {
const disabled = item.value === 'id'
updateTableDataRecord(item.record.id, {
empty: disabled ? false : item.record.empty,
disabled,
})
}
}
const onRowClick = (record?: EditRecordRow) => {
const records = getDataSource()
records.forEach((item) => {
if (item.id !== record?.id) {
item.editable = false
}
})
if (record?.editable === false) {
record.editable = true
}
}
</script>
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose defaultFullscreen :canFullscreen="false">
<div class="pr-20px">
<BasicForm @register="registerEditForm" />
</div>
<div class="p-20px">
<Tabs>
<TabPane key="db-schema">
<template #tab>
<Icon icon="carbon:schematics" />
数据表属性
</template>
<!-- -->
<BasicTable
@register="registerDatabaseSchemaTable"
@edit-change="editFieldChange"
@row-click="onRowClick"
@row-db-click="(record: EditRecordRow) => (record.editable = false)"
v-click-outside="onRowClick"
>
<template #tableTitle>
<Space>
<Button type="primary" @click="insertField">
<template #icon>
<Icon icon="ant-design:plus-outlined" />
</template>
新增
</Button>
</Space>
</template>
</BasicTable>
</TabPane>
<TabPane key="db-foreign-key">
<template #tab>
<Icon icon="mdi:foreign-key" />
外键
</template>
<!-- -->
外键
</TabPane>
<TabPane key="db-index">
<template #tab>
<Icon icon="ant-design:node-index-outlined" />
索引
</template>
<!-- -->
索引
</TabPane>
<TabPane key="page-schema">
<template #tab>
<Icon icon="ri:page-separator" />
页面属性
</template>
<!-- -->
页面属性
</TabPane>
<TabPane key="schema-rule">
<template #tab>
<Icon icon="material-symbols:rule" />
规则校验
</template>
<!-- -->
规则校验
</TabPane>
</Tabs>
</div>
</BasicModal>
</template>
<style lang="less" scoped>
/** ... */
</style>
<script lang="ts" setup>
import { nanoid } from 'nanoid'
import { Tabs, TabPane } from 'ant-design-vue'
import { Icon } from '@/components/Icon'
import { BasicForm, useForm } from '/@/components/Form'
import { BasicTableProps, VxeBasicTable, VxeGridInstance } from '/@/components/VxeTable'
import { BasicModal, useModalInner } from '/@/components/Modal'
import { useSortable } from '/@/hooks/web/useSortable'
import { typeOptions } from './data'
import { databaseTypeOptions, getDataSourceSchemaColumns, getDefaultSchemaFieldDataSourcesByType } from './data'
const [registerModal] = useModalInner((data) => {
console.log(data)
......@@ -12,8 +15,24 @@
// TODO:
// 1. 根据传递的 ID 参数,查询表配置详细信息
// 2. 赋值填充到表单对象中
nextTick(() => {
const table = databaseSchemaTable.value
const { initSortable } = useSortable(table?.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
handle: '.drag',
onEnd: (sortableEvent) => {
const newIndex = sortableEvent.newIndex as number
const oldIndex = sortableEvent.oldIndex as number
const currRow = table.data.splice(oldIndex, 1)[0]
table.data.splice(newIndex, 0, currRow)
databaseSchemaTableOptions.data = [...table.data]
},
})
initSortable()
})
})
const defaultTableType = databaseTypeOptions[0].options[0].value
const [registerEditForm] = useForm({
colon: true,
labelWidth: 150,
......@@ -43,8 +62,8 @@
required: true,
component: 'Select',
componentProps: {
options: typeOptions,
defaultValue: typeOptions[0].value,
options: databaseTypeOptions,
defaultValue: defaultTableType,
},
colProps: {
span: 8,
......@@ -52,10 +71,99 @@
},
],
})
const databaseSchemaTable = ref<VxeGridInstance>()
const databaseSchemaTableOptions = reactive<BasicTableProps>({
id: nanoid(),
autoResize: true,
stripe: false,
keepSource: true,
showOverflow: true,
rowConfig: { useKey: true, keyField: 'id' },
checkboxConfig: { checkMethod: ({ row }) => !row.disabled },
columns: getDataSourceSchemaColumns(),
data: getDefaultSchemaFieldDataSourcesByType(defaultTableType),
editConfig: { trigger: 'click', mode: 'cell', showStatus: false, beforeEditMethod: ({ row }) => !row.disabled },
editRules: {
field: [{ required: true, message: '请输入字段名称' }],
type: [{ required: true, message: '请选择字段类型' }],
},
toolbarConfig: {
buttons: [
{
content: '新增',
buttonRender: {
name: 'AButton',
props: {
type: 'primary',
preIcon: 'mdi:table-row-plus-before',
},
events: {
click: async () => {
const table = databaseSchemaTable.value
const { row } = await table?.insertAt(
{
id: nanoid(),
field: null,
type: null,
comment: null,
len: null,
decimal: null,
pk: false,
empty: false,
transient: true,
},
-1,
)
databaseSchemaTableOptions.data.push(row)
await table?.updateData()
await table?.setEditCell(row, 'field')
},
},
},
},
{
content: '删除',
buttonRender: {
name: 'AButton',
props: {
type: 'danger',
preIcon: 'mdi:table-row-remove',
disabled: true,
},
events: {
click: async () => {
const table = databaseSchemaTable.value
const checkedRecords = table?.getCheckboxRecords()
if (!checkedRecords?.length) {
return
}
// 更新数据
databaseSchemaTableOptions.data = databaseSchemaTableOptions.data.filter(
(item) => !checkedRecords?.some((record) => record.id === item.id),
)
await table?.removeCheckboxRow()
await table?.updateData()
onCheckboxChange()
},
},
},
},
],
},
})
const onCheckboxChange = () => {
const checkedRecords = databaseSchemaTable.value?.getCheckboxRecords()
databaseSchemaTableOptions.toolbarConfig.buttons[1].buttonRender.props.disabled = !checkedRecords?.length
}
</script>
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose defaultFullscreen>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose defaultFullscreen :canFullscreen="false">
<div class="pr-20px">
<BasicForm @register="registerEditForm" />
</div>
......@@ -68,7 +176,12 @@
</template>
<!-- -->
数据表属性
<VxeBasicTable
ref="databaseSchemaTable"
v-bind="databaseSchemaTableOptions"
@checkbox-change="onCheckboxChange"
@checkbox-all="onCheckboxChange"
/>
</TabPane>
<TabPane key="db-foreign-key">
......@@ -77,6 +190,8 @@
外键
</template>
<!-- -->
外键
</TabPane>
<TabPane key="db-index">
......@@ -85,14 +200,18 @@
索引
</template>
<!-- -->
索引
</TabPane>
<TabPane key="page-schema">
<template #tab>
<Icon icon="fluent-emoji-high-contrast:pager" />
<Icon icon="ri:page-separator" />
页面属性
</template>
<!-- -->
页面属性
</TabPane>
<TabPane key="schema-rule">
......
......@@ -13,12 +13,12 @@
const { createConfirm } = useMessage()
const [registerTable] = useTable({
rowKey: 'id',
size: 'small',
bordered: true,
canResize: true,
columns: getColumns(),
dataSource: getDataSource(),
rowKey: (record) => record.id,
useSearchForm: true,
formConfig: getFormConfig(),
showTableSetting: true,
......@@ -42,6 +42,7 @@
return [
{
label: '同步数据库',
icon: 'mdi:database-sync',
onClick: () => {
const submiting = ref(false)
const syncType = ref('update')
......@@ -81,26 +82,31 @@
},
{
label: '功能测试',
icon: 'fluent-mdl2:test-plan',
onClick: () => console.log(record),
ifShow: record.sync === true,
},
{
label: '地址配置',
icon: 'ph:link-bold',
onClick: () => console.log(record),
ifShow: record.sync === true,
},
{
label: '权限配置',
icon: 'mdi:filter-menu',
onClick: () => console.log(record),
ifShow: record.sync === true,
},
{
label: '授权配置',
icon: 'carbon:user-role',
onClick: () => console.log(record),
ifShow: record.sync === true,
},
{
label: '复制表',
icon: 'ph:copy-duotone',
onClick: () => {
const submiting = ref(false)
const tableName = ref((record.name ?? '') + '_copy')
......@@ -140,15 +146,21 @@
},
{
label: '移除',
icon: 'material-symbols:bookmark-remove',
color: 'warning',
popConfirm: {
title: '是否移除',
placement: 'left',
confirm: () => console.log(record),
},
},
{
label: '删除',
icon: 'material-symbols:delete-outline',
color: 'error',
popConfirm: {
title: '是否删除',
placement: 'left',
confirm: () => console.log(record),
},
},
......@@ -160,7 +172,7 @@
// 新增/编辑打开窗口
const editHandler = (data?: Recordable) => {
setModalProps({ title: data ? '编辑' : '新增' })
openModal(true, data)
openModal(true, data || {})
}
</script>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论