前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,2 @@
export { useJVxeCompProps, useJVxeComponent } from './src/hooks/useJVxeComponent';
export { useResolveComponent } from './src/hooks/useData';

View File

@ -0,0 +1,4 @@
export { default as JVxeTable } from './src/JVxeTable';
export { registerJVxeTable } from './src/install';
export { deleteComponent } from './src/componentMap';
export { registerComponent, registerAsyncComponent, registerASyncComponentReal } from './src/utils/registerUtils';

View File

@ -0,0 +1,82 @@
import { defineComponent, h, nextTick, ref, useSlots } from 'vue';
import { vxeEmits, vxeProps } from './vxe.data';
import { useData, useRefs, useResolveComponent as rc } from './hooks/useData';
import { useColumns } from './hooks/useColumns';
import { useColumnsCache } from './hooks/useColumnsCache';
import { useMethods } from './hooks/useMethods';
import { useDataSource } from './hooks/useDataSource';
import { useDragSort } from './hooks/useDragSort';
import { useRenderComponents } from './hooks/useRenderComponents';
import { useFinallyProps } from './hooks/useFinallyProps';
import { JVxeTableProps } from './types';
import './style/index.less';
export default defineComponent({
name: 'JVxeTable',
inheritAttrs: false,
props: vxeProps(),
emits: [...vxeEmits],
setup(props: JVxeTableProps, context) {
const instanceRef = ref();
const refs = useRefs();
const slots = useSlots();
const data = useData(props);
const { methods, publicMethods, created } = useMethods(props, context, data, refs, instanceRef);
created();
useColumns(props, data, methods, slots);
useDataSource(props, data, methods, refs);
useDragSort(props, methods);
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-8566】JVXETable无法记住列设置
const { initSetting } = useColumnsCache({ cacheColumnsKey: props.cacheColumnsKey });
initSetting(props);
// update-end--author:liaozhiyang---date:20240321---for【QQYUN-8566】JVXETable无法记住列设置
// 最终传入到 template 里的 props
const finallyProps = useFinallyProps(props, data, methods);
// 渲染子组件
const renderComponents = useRenderComponents(props, data, methods, slots);
return {
instanceRef,
...refs,
...publicMethods,
...finallyProps,
...renderComponents,
vxeDataSource: data.vxeDataSource,
};
},
render() {
return h(
'div',
{
class: this.$attrs.class,
style: this.$attrs.style,
},
h(
rc('a-spin'),
{
spinning: this.loading,
wrapperClassName: this.prefixCls,
},
{
default: () => [
this.renderSubPopover(),
this.renderToolbar(),
this.renderToolbarAfterSlot(),
h(
rc('vxe-grid'),
{
...this.vxeProps,
data: this.vxeDataSource,
},
this.$slots
),
this.renderPagination(),
this.renderDetailsModal(),
],
}
)
);
},
created() {
this.instanceRef = this;
},
});

View File

@ -0,0 +1,97 @@
import type { JVxeVueComponent } from './types';
import { JVxeTypes } from './types/JVxeTypes';
import JVxeSlotCell from './components/cells/JVxeSlotCell';
import JVxeNormalCell from './components/cells/JVxeNormalCell.vue';
import JVxeDragSortCell from './components/cells/JVxeDragSortCell.vue';
import JVxeInputCell from './components/cells/JVxeInputCell.vue';
import JVxeDateCell from './components/cells/JVxeDateCell.vue';
import JVxeTimeCell from './components/cells/JVxeTimeCell.vue';
import JVxeSelectCell from './components/cells/JVxeSelectCell.vue';
import JVxeRadioCell from './components/cells/JVxeRadioCell.vue';
import JVxeCheckboxCell from './components/cells/JVxeCheckboxCell.vue';
import JVxeUploadCell from './components/cells/JVxeUploadCell.vue';
// import { TagsInputCell, TagsSpanCell } from './components/cells/JVxeTagsCell.vue'
import JVxeProgressCell from './components/cells/JVxeProgressCell.vue';
import JVxeTextareaCell from './components/cells/JVxeTextareaCell.vue';
// import JVxeDepartSelectCell from './components/cells/JVxeDepartSelectCell.vue'
// import JVxeUserSelectCell from './components/cells/JVxeUserSelectCell.vue'
let componentMap = new Map<JVxeTypes | string, JVxeVueComponent>();
// update-begin--author:liaozhiyang---date:20231208---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
const JVxeComponents = 'JVxeComponents__';
if (import.meta.env.DEV && componentMap.size === 0 && window[JVxeComponents] && window[JVxeComponents].size > 0) {
componentMap = window[JVxeComponents];
}
// update-end--author:liaozhiyang---date:20231027---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
/** span 组件结尾 */
export const spanEnds: string = ':span';
/** 定义不能用于注册的关键字 */
export const excludeKeywords: Array<JVxeTypes> = [
JVxeTypes.hidden,
JVxeTypes.rowNumber,
JVxeTypes.rowCheckbox,
JVxeTypes.rowRadio,
JVxeTypes.rowExpand,
];
/**
* 注册组件
*
* @param type 组件 type
* @param component Vue组件
* @param spanComponent 显示组件,可空,默认为 JVxeNormalCell 组件
*/
export function addComponent(type: JVxeTypes, component: JVxeVueComponent, spanComponent?: JVxeVueComponent) {
if (excludeKeywords.includes(type)) {
throw new Error(`【addComponent】不能使用"${type}"作为组件的name因为这是关键字。`);
}
if (componentMap.has(type)) {
throw new Error(`【addComponent】组件"${type}"已存在`);
}
componentMap.set(type, component);
if (spanComponent) {
componentMap.set(type + spanEnds, spanComponent);
}
// update-begin--author:liaozhiyang---date:20231208---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
// update-end--author:liaozhiyang---date:20231208---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
}
export function deleteComponent(type: JVxeTypes) {
componentMap.delete(type);
componentMap.delete(type + spanEnds);
// update-begin--author:liaozhiyang---date:20231208---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
// update-end--author:liaozhiyang---date:20231208---for【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
}
/** 定义内置自定义组件 */
export function definedComponent() {
addComponent(JVxeTypes.slot, JVxeSlotCell);
addComponent(JVxeTypes.normal, JVxeNormalCell);
addComponent(JVxeTypes.rowDragSort, JVxeDragSortCell);
addComponent(JVxeTypes.input, JVxeInputCell);
addComponent(JVxeTypes.inputNumber, JVxeInputCell);
addComponent(JVxeTypes.radio, JVxeRadioCell);
addComponent(JVxeTypes.checkbox, JVxeCheckboxCell);
addComponent(JVxeTypes.select, JVxeSelectCell);
addComponent(JVxeTypes.selectSearch, JVxeSelectCell); // 下拉搜索
addComponent(JVxeTypes.selectMultiple, JVxeSelectCell); // 下拉多选
addComponent(JVxeTypes.date, JVxeDateCell);
addComponent(JVxeTypes.datetime, JVxeDateCell);
addComponent(JVxeTypes.time, JVxeTimeCell);
addComponent(JVxeTypes.upload, JVxeUploadCell);
addComponent(JVxeTypes.textarea, JVxeTextareaCell);
// addComponent(JVxeTypes.tags, TagsInputCell, TagsSpanCell)
addComponent(JVxeTypes.progress, JVxeProgressCell);
// addComponent(JVxeTypes.departSelect, JVxeDepartSelectCell)
// addComponent(JVxeTypes.userSelect, JVxeUserSelectCell)
}
export { componentMap };

View File

@ -0,0 +1,78 @@
<template>
<BasicModal @register="registerModel" title="详细信息" :width="1200" :keyboard="true" @ok="handleOk" @cancel="close">
<transition name="fade">
<div v-if="getVisible">
<slot name="mainForm" :row="row" :column="column" />
</div>
</transition>
</BasicModal>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
import { cloneDeep } from 'lodash-es';
import { useModal } from '/@/components/Modal/src/hooks/useModal';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({
components: {
BasicModal: createAsyncComponent(() => import('/@/components/Modal/src/BasicModal.vue'), {
loading: true,
}),
},
props: {
trigger: {
type: Function,
required: true,
},
},
setup(props) {
const row = ref(null);
const column = ref(null);
const [registerModel, { openModal, closeModal, getVisible }] = useModal();
function open(event) {
let { row: $row, column: $column } = event;
row.value = cloneDeep($row);
column.value = $column;
openModal();
}
function close() {
closeModal();
}
function handleOk() {
props.trigger('detailsConfirm', {
row: row.value,
column: column.value,
callback: (success) => {
success ? closeModal() : openModal();
},
});
}
return {
getVisible,
row,
column,
open,
close,
handleOk,
registerModel,
};
},
});
</script>
<style scoped lang="less">
.fade-enter-active,
.fade-leave-active {
opacity: 1;
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,93 @@
import { defineComponent, h, ref, watch } from 'vue';
import { randomString } from '/@/utils/common/compUtils';
import '../style/reload-effect.less';
// 修改数据特效
export default defineComponent({
props: {
vNode: null,
// 是否启用特效
effect: Boolean,
},
emits: ['effectBegin', 'effectEnd'],
setup(props, { emit }) {
// vNode: null,
const innerEffect = ref(props.effect);
// 应付同时多个特效
const effectIdx = ref(0);
const effectList = ref<any[]>([]);
watch(
() => props.effect,
() => (innerEffect.value = props.effect)
);
watch(
() => props.vNode,
(_vNode, old) => {
if (props.effect && old != null) {
let topLayer = renderSpan(old, 'top');
effectList.value.push(topLayer);
}
},
{ deep: true, immediate: true }
);
// 条件渲染内容 span
function renderVNode() {
if (props.vNode == null) {
return null;
}
let bottom = renderSpan(props.vNode, 'bottom');
// 启用了特效,并且有旧数据,就渲染特效顶层
if (innerEffect.value && effectList.value.length > 0) {
emit('effectBegin');
// 1.4s 以后关闭特效
window.setTimeout(() => {
let item = effectList.value[effectIdx.value];
if (item && item.elm) {
// 特效结束后,展示先把 display 设为 none而不是直接删掉该元素
// 目的是为了防止页面重新渲染,导致动画重置
item.elm.style.display = 'none';
}
// 当所有的层级动画都结束时,再删掉所有元素
if (++effectIdx.value === effectList.value.length) {
innerEffect.value = false;
effectIdx.value = 0;
effectList.value = [];
emit('effectEnd');
}
}, 1400);
return [effectList.value, bottom];
} else {
return bottom;
}
}
// 渲染内容 span
function renderSpan(vNode, layer) {
let options = {
key: layer + effectIdx.value + randomString(6),
class: ['j-vxe-reload-effect-span', `layer-${layer}`],
style: {},
// update-begin--author:liaozhiyang---date:20240424---for【issues/1175】解决vxetable鼠标hover之后title显示不对的问题
title: vNode,
// update-end--author:liaozhiyang---date:20240424---for【issues/1175】解决vxetable鼠标hover之后title显示不对的问题
};
if (layer === 'top') {
// 最新渲染的在下面
options.style['z-index'] = 9999 - effectIdx.value;
}
return h('span', options, [vNode]);
}
return () =>
h(
'div',
{
class: ['j-vxe-reload-effect-box'],
},
[renderVNode()]
);
},
});

View File

@ -0,0 +1,207 @@
<template>
<a-popover :open="visible" :placement="placement" overlayClassName="j-vxe-popover-overlay" :overlayStyle="overlayStyle">
<template #title>
<div class="j-vxe-popover-title">
<div>子表</div>
<div class="j-vxe-popover-title-close" @click="close">
<a-icon type="close" />
</div>
</div>
</template>
<template #content>
<transition name="fade">
<slot v-if="visible" name="subForm" :row="row" :column="column" />
</transition>
</template>
<div ref="divRef" class="j-vxe-popover-div"></div>
</a-popover>
</template>
<script lang="ts">
import { ref, reactive, nextTick, defineComponent } from 'vue';
import domAlign from 'dom-align';
import { getParentNodeByTagName } from '../utils/vxeUtils';
import { triggerWindowResizeEvent } from '/@/utils/common/compUtils';
import { cloneDeep } from 'lodash-es';
import { useMessage } from '/@/hooks/web/useMessage';
import { isString } from '/@/utils/is';
export default defineComponent({
name: 'JVxeSubPopover',
setup() {
const visible = ref(false);
const row = ref<any>(null);
const column = ref<any>(null);
const overlayStyle = reactive<{
width?: number | string;
maxWidth?: number | string;
zIndex: number;
}>({
zIndex: 100,
});
const placement = ref('bottom');
const divRef = ref<HTMLElement>();
const { createMessage } = useMessage();
function toggle(event) {
if (document.body.clientHeight - event.$event.clientY > 350) {
placement.value = 'bottom';
} else {
placement.value = 'top';
}
if (row.value == null) {
open(event);
} else {
row.value.id === event.row.id ? close() : reopen(event);
}
}
function open(event, level = 0) {
if (level > 3) {
createMessage.error('打开子表失败');
console.warn('【JVxeSubPopover】打开子表失败');
return;
}
let {
row: $row,
column: $column,
$table,
$event: { target },
} = event;
row.value = cloneDeep($row);
column.value = $column;
let className = target.className || '';
className = isString(className) ? className : className.toString();
// 获取 td 父级
let td = getParentNodeByTagName(target, 'td');
// 点击的是拖拽排序列,不做处理
if (td && td.querySelector('.j-vxe-drag-box')) {
return;
}
// 点击的是expand不做处理
if (className.includes('vxe-table--expand-btn')) {
return;
}
// 点击的是checkbox不做处理
if (className.includes('vxe-checkbox--icon') || className.includes('vxe-cell--checkbox')) {
return;
}
// 点击的是radio不做处理
if (className.includes('vxe-radio--icon') || className.includes('vxe-cell--radio')) {
return;
}
let parentElem = $table.getParentElem();
let tr = getParentNodeByTagName(target, 'tr');
if (parentElem && tr) {
let clientWidth = parentElem.clientWidth;
let clientHeight = tr.clientHeight;
divRef.value!.style.width = clientWidth + 'px';
divRef.value!.style.height = clientHeight + 'px';
overlayStyle.width = Number.parseInt(`${clientWidth - clientWidth * 0.04}`) + 'px';
overlayStyle.maxWidth = overlayStyle.width;
//let realTable = getParentNodeByTagName(tr, 'table')
//let left = realTable.parentNode.scrollLeft
let h = event.$event.clientY;
if (h) {
h = h - 140;
}
let toolbar = divRef.value!.nextElementSibling;
domAlign(divRef.value, toolbar, {
points: ['tl', 'tl'],
offset: [0, h],
overflow: {
alwaysByViewport: true,
},
});
nextTick(() => {
visible.value = true;
nextTick(() => {
triggerWindowResizeEvent();
});
});
} else {
let num = ++level;
console.warn('【JVxeSubPopover】table or tr 获取失败,正在进行第 ' + num + '次重试', {
event,
table: parentElem,
tr,
});
window.setTimeout(() => open(event, num), 100);
}
}
function close() {
if (visible.value) {
row.value = null;
visible.value = false;
}
}
function reopen(event) {
open(event);
}
return {
divRef,
row,
column,
visible,
placement,
overlayStyle,
close,
toggle,
};
},
});
</script>
<style scoped lang="less">
.j-vxe-popover-title {
.j-vxe-popover-title-close {
position: absolute;
right: 0;
top: 0;
width: 31px;
height: 31px;
text-align: center;
line-height: 31px;
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
transition: color 300ms;
&:hover {
color: rgba(0, 0, 0, 0.8);
}
}
}
.j-vxe-popover-div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 31px;
z-index: -1;
}
</style>
<style lang="less">
.j-vxe-popover-overlay.ant-popover {
.ant-popover-title {
position: relative;
}
}
.fade-enter-active,
.fade-leave-active {
opacity: 1;
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div :class="boxClass">
<vxe-toolbar ref="xToolbarRef" :custom="custom">
<!-- 工具按钮 -->
<template #buttons>
<div :class="`${prefixCls}-button div`" :size="btnSize">
<slot v-if="showPrefix" name="toolbarPrefix" :size="btnSize" />
<a-button v-if="showAdd" type="primary" preIcon="ant-design:plus-outlined" :disabled="disabled" @click="trigger('add')">
<span>新增</span>
</a-button>
<a-button v-if="showSave" preIcon="ant-design:save-outlined" :disabled="disabled" @click="trigger('save')">
<span>保存</span>
</a-button>
<template v-if="selectedRowIds.length > 0">
<Popconfirm v-if="showRemove" :title="`确定要删除这 ${selectedRowIds.length} 项吗?`" @confirm="trigger('remove')">
<a-button preIcon="ant-design:minus-outlined" :disabled="disabled">删除</a-button>
</Popconfirm>
<template v-if="showClearSelection">
<a-button preIcon="ant-design:delete-outlined" @click="trigger('clearSelection')">清空选择</a-button>
</template>
</template>
<slot v-if="showSuffix" name="toolbarSuffix" :size="btnSize" />
<a v-if="showCollapse" style="margin-left: 4px" @click="toggleCollapse">
<span>{{ collapsed ? '展开' : '收起' }}</span>
<Icon :icon="collapsed ? 'ant-design:down-outlined' : 'ant-design:up-outlined'" />
</a>
</div>
</template>
</vxe-toolbar>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, ref, onMounted } from 'vue';
// noinspection ES6UnusedImports
import { Popconfirm } from 'ant-design-vue';
import { VxeToolbarInstance } from 'vxe-table';
import { Icon } from '/@/components/Icon';
import { propTypes } from '/@/utils/propTypes';
const props = defineProps({
size: propTypes.string,
disabled: propTypes.bool.def(false),
custom: propTypes.bool.def(false),
toolbarConfig: propTypes.object,
disabledRows: propTypes.object,
hasBtnAuth: propTypes.func,
selectedRowIds: propTypes.array.def(() => []),
});
const emit = defineEmits(['save', 'add', 'remove', 'clearSelection', 'register']);
const xToolbarRef = ref({} as VxeToolbarInstance);
const prefixCls = `${inject('prefixCls')}-toolbar`;
const boxClass = computed(() => [
prefixCls,
{
[`${prefixCls}-collapsed`]: collapsed.value,
},
]);
// 是否收起
const collapsed = ref(true);
// 配置的按钮
const btns = computed(() => {
let { btn, btns } = props.toolbarConfig || {};
btns = btn || btns || ['add', 'remove', 'clearSelection'];
// 排除掉没有授权的按钮
return btns.filter((btn) => {
// 系统默认的批量删除编码配置为 batch_delete 此处需要兼容一下
if (btn === 'remove') {
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
return hasBtnAuth(btn) && hasBtnAuth('batch_delete');
//update-end-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
}
return hasBtnAuth(btn);
});
});
const showAdd = computed(() => btns.value.includes('add'));
const showSave = computed(() => btns.value.includes('save'));
const showRemove = computed(() => btns.value.includes('remove'));
// 配置的插槽
const slots = computed(() => props.toolbarConfig?.slot || ['prefix', 'suffix']);
const showPrefix = computed(() => slots.value.includes('prefix'));
const showSuffix = computed(() => slots.value.includes('suffix'));
// 是否显示清除选择按钮
const showClearSelection = computed(() => {
if (btns.value.includes('clearSelection')) {
// 有禁用行时才显示清空选择按钮
// 因为禁用行会阻止选择行,导致无法取消全选
// return Object.keys(props.disabledRows).length > 0
}
return false;
});
// 是否显示展开收起按钮
const showCollapse = computed(() => btns.value.includes('collapse'));
// 按钮 size
const btnSize = computed(() => (props.size === 'tiny' ? 'small' : null));
onMounted(() => {
// 注册 vxe-toolbar
emit('register', {
xToolbarRef,
});
});
// 判断按钮是否已授权
function hasBtnAuth(key: string) {
return props.hasBtnAuth ? props.hasBtnAuth(key) : true;
}
/** 触发事件 */
function trigger(name) {
emit(name);
}
// 切换展开收起
function toggleCollapse() {
collapsed.value = !collapsed.value;
}
</script>

View File

@ -0,0 +1,116 @@
<template>
<div :class="boxClass" :style="boxStyle" title="">
<a-checkbox :checked="innerValue" v-bind="cellProps" @change="handleChange" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { isArray, isBoolean } from '/@/utils/is';
export default defineComponent({
name: 'JVxeCheckboxCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon } = useJVxeComponent(props);
// 是否启用边框
const bordered = computed(() => !!props.renderOptions.bordered);
// box 类名
const boxClass = computed(() => {
return {
'j-vxe-checkbox': true,
'no-animation': scrolling.value,
};
});
// box 行内样式
const boxStyle = computed(() => {
const style = {};
// 如果有边框且未设置align属性就强制居中
if (bordered.value && !originColumn.value.align) {
style['text-align'] = 'center';
}
return style;
});
// onChange 事件
function handleChange(event) {
handleChangeCommon(event.target.checked);
}
return {
cellProps,
innerValue,
boxClass,
boxStyle,
handleChange,
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
switches: {
visible: true,
},
getValue(value, ctx) {
let { context } = ctx!;
let { originColumn } = context;
// 处理 customValue
if (isArray(originColumn.value.customValue)) {
let customValue = getCustomValue(originColumn.value);
if (isBoolean(value)) {
return value ? customValue[0] : customValue[1];
} else {
return value;
}
} else {
return value;
}
},
setValue(value, ctx) {
let { context } = ctx!;
let { originColumn } = context;
// 判断是否设定了customValue自定义值
if (isArray(originColumn.value.customValue)) {
let customValue = getCustomValue(originColumn.value);
return neverNull(value).toString() === customValue[0].toString();
} else {
return !!value;
}
},
createValue(_defaultValue, ctx) {
let { context } = ctx!;
let {
column: { params: col },
} = context;
if (isArray(col.customValue)) {
let customValue = getCustomValue(col);
return col.defaultChecked ? customValue[0] : customValue[1];
} else {
return !!col.defaultChecked;
}
},
} as JVxeComponent.EnhancedPartial,
});
function neverNull(value, def?) {
return value == null ? neverNull(def, '') : value;
}
function getCustomValue(col) {
let customTrue = neverNull(col.customValue[0], true);
let customFalse = neverNull(col.customValue[1], false);
return [customTrue, customFalse];
}
</script>
<style lang="less">
// 关闭动画,防止滚动时动态赋值出现问题
.j-vxe-checkbox.no-animation {
.ant-checkbox-inner,
.ant-checkbox-inner::after {
transition: none !important;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<a-date-picker
:value="innerDateValue"
allowClear
:format="picker ? null : dateFormat"
:showTime="isDatetime"
:valueFormat="picker ? dateFormat : null"
popupClassName="j-vxe-date-picker"
style="min-width: 0"
v-model:open="openPicker"
v-bind="cellProps"
:picker="picker"
@change="handleChange"
/>
</template>
<script lang="ts">
import { ref, computed, watch, defineComponent } from 'vue';
import dayjs from 'dayjs';
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { isEmpty } from '/@/utils/is';
import { getWeekMonthQuarterYear } from '/@/utils';
export default defineComponent({
name: 'JVxeDateCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
const innerDateValue = ref<any>(null);
const isDatetime = computed(() => props.type === JVxeTypes.datetime);
const dateFormat = computed(() => {
let format = originColumn.value.format;
return format ? format : isDatetime.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
});
const openPicker = ref(true);
// update-begin--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
const picker = computed(() => {
const picker = originColumn.value.picker;
return picker ? picker : null;
});
// update-end--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
watch(
innerValue,
(val) => {
if (val == null || isEmpty(val)) {
innerDateValue.value = null;
} else {
innerDateValue.value = dayjs(val, dateFormat.value);
}
},
{ immediate: true }
);
function handleChange(_mom, dateStr) {
// update-begin--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
if (picker.value) {
handleChangeCommon(_mom);
} else {
handleChangeCommon(dateStr);
}
// update-begin--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
}
return {
cellProps,
isDatetime,
dateFormat,
innerDateValue,
openPicker,
handleChange,
picker,
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
aopEvents: {
},
// update-begin--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
translate: {
enabled: true,
handler(value, ctx) {
let { props, context } = ctx!;
let { row, originColumn } = context;
if (originColumn.value.picker && value) {
return getWeekMonthQuarterYear(value)[originColumn.value.picker];
}
return value;
},
},
// update-end--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,117 @@
<template>
<div class="j-vxe-drag-box">
<span v-if="!isAllowDrag"><span class="not-drag-btn"> <Icon icon="mi:drag" /> </span
></span>
<a-dropdown v-else :trigger="['click']" >
<span
><span class="drag-btn"> <Icon icon="mi:drag" /> </span
></span>
<template #overlay >
<a-menu>
<a-menu-item key="0" :disabled="disabledMoveUp" @click="handleRowMoveUp">向上移</a-menu-item>
<a-menu-item key="1" :disabled="disabledMoveDown" @click="handleRowMoveDown">向下移</a-menu-item>
<a-menu-divider />
<a-menu-item key="3" @click="handleRowInsertDown">插入一行</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { Icon } from '/@/components/Icon';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
export default defineComponent({
name: 'JVxeDragSortCell',
components: { Icon },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { rowIndex, originColumn, fullDataLength, trigger } = useJVxeComponent(props);
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
const isAllowDrag = computed(() => {
const notAllowDrag = originColumn.value.notAllowDrag;
if (notAllowDrag.length) {
const row = props.params.row;
const find = notAllowDrag.find((item: any) => {
const { key, value } = item;
return row[key] == value;
});
return !find;
} else {
return true;
}
});
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
const disabledMoveUp = computed(() => rowIndex.value === 0);
const disabledMoveDown = computed(() => rowIndex.value === fullDataLength.value - 1);
/** 向上移 */
function handleRowMoveUp() {
if (!disabledMoveUp.value) {
trigger('rowResort', {
oldIndex: rowIndex.value,
newIndex: rowIndex.value - 1,
});
}
}
/** 向下移 */
function handleRowMoveDown() {
if (!disabledMoveDown.value) {
trigger('rowResort', {
oldIndex: rowIndex.value,
newIndex: rowIndex.value + 1,
});
}
}
/** 插入一行 */
function handleRowInsertDown() {
trigger('rowInsertDown', rowIndex.value);
}
return {
disabledMoveUp,
disabledMoveDown,
handleRowMoveUp,
handleRowMoveDown,
handleRowInsertDown,
isAllowDrag
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
// 【功能开关】
switches: {
editRender: false,
},
} as JVxeComponent.EnhancedPartial,
});
</script>
<style lang="less">
.j-vxe-drag-box {
.app-iconify {
cursor: move;
}
}
.vxe-table--fixed-wrapper {
.j-vxe-drag-box {
.app-iconify {
cursor: pointer;
}
}
}
</style>
<style scoped>
.not-drag-btn {
opacity: 0.5;
.app-iconify {
cursor: not-allowed;
}
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<a-input ref="input" :value="innerValue" v-bind="cellProps" @blur="handleBlur" @change="handleChange" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { isString } from '/@/utils/is';
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
const NumberRegExp = /^-?\d+\.?\d*$/;
export default defineComponent({
name: 'JVxeInputCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
/** 处理change事件 */
function handleChange(event) {
let { target } = event;
let { value, selectionStart } = target;
let change = true;
if (props.type === JVxeTypes.inputNumber) {
// 判断输入的值是否匹配数字正则表达式,不匹配就还原
if (!NumberRegExp.test(value) && value !== '' && value !== '-') {
change = false;
value = innerValue.value;
target.value = value || '';
if (typeof selectionStart === 'number') {
target.selectionStart = selectionStart - 1;
target.selectionEnd = selectionStart - 1;
}
} else {
// update-begin--author:liaozhiyang---date:20240227---for【QQYUN-8347】小数点后大于两位且最后一位是0输入框不可输入了
// 例如41.1 -> 41.10, 100.1 -> 100.10 不执行handleChangeCommon 函数。
if (value.indexOf('.') != -1) {
const result = value.split('.').pop();
if (result && result.length >= 2 && result.substr(-1) === '0') {
change = false;
innerValue.value = value;
}
}
// update-end--author:liaozhiyang---date:20240227---for【QQYUN-8347】小数点后大于两位且最后一位是0输入框不可输入了
}
}
// 触发事件,存储输入的值
if (change) {
handleChangeCommon(value);
}
}
/** 处理blur失去焦点事件 */
function handleBlur(event) {
let { target } = event;
// 判断输入的值是否匹配数字正则表达式,不匹配就置空
if (props.type === JVxeTypes.inputNumber) {
if (!NumberRegExp.test(target.value)) {
target.value = '';
} else {
target.value = Number.parseFloat(target.value);
}
}
handleChangeCommon(target.value, true);
handleBlurCommon(target.value);
}
return {
innerValue,
cellProps,
handleChange,
handleBlur,
};
},
enhanced: {
installOptions: {
autofocus: '.ant-input',
},
getValue(value, ctx) {
if (ctx?.props?.type === JVxeTypes.inputNumber && isString(value)) {
if (NumberRegExp.test(value)) {
// 【issues/I5IHN7】修复无法输入小数点的bug
if (/\.0*$/.test(value)) {
return value;
}
return Number.parseFloat(value);
}
}
return value;
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,53 @@
<template>
<JVxeReloadEffect :vNode="innerValue" :effect="isEffect" @effectEnd="handleEffectEnd" />
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
import JVxeReloadEffect from '../JVxeReloadEffect';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { watch } from 'vue';
export default defineComponent({
name: 'JVxeNormalCell',
components: { JVxeReloadEffect },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const setup = useJVxeComponent(props);
const { innerValue, row } = setup;
const reloadEffect = props.renderOptions.reloadEffect;
const isEffect = ref<boolean>(false);
watch(
innerValue,
() => {
if (reloadEffect.enabled) {
if (reloadEffect.isEffect(row.value.id)) {
isEffect.value = true;
}
}
},
{ immediate: true }
);
// 特效结束
function handleEffectEnd() {
isEffect.value = false;
reloadEffect.removeEffect(row.value.id);
}
return {
innerValue,
isEffect,
handleEffectEnd,
};
},
enhanced: {
switches: { editRender: false },
} as JVxeComponent.EnhancedPartial,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,52 @@
<template>
<Progress :class="clazz" :percent="innerValue" size="small" v-bind="cellProps" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { Progress } from 'ant-design-vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
export default defineComponent({
name: 'JVxeCheckboxCell',
components: { Progress },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, scrolling } = useJVxeComponent(props);
const clazz = computed(() => {
return {
'j-vxe-progress': true,
'no-animation': scrolling.value,
};
});
return { innerValue, cellProps, clazz };
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
switches: {
editRender: false,
},
setValue(value) {
try {
if (typeof value !== 'number') {
return Number.parseFloat(value);
} else {
return value;
}
} catch {
return 0;
}
},
} as JVxeComponent.EnhancedPartial,
});
</script>
<style scoped lang="less">
// 关闭进度条的动画,防止滚动时动态赋值出现问题
.j-vxe-progress.no-animation {
:deep(.ant-progress-bg) {
transition: none !important;
}
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<a-radio-group :class="clazz" :value="innerValue" v-bind="cellProps" @change="(e) => handleChangeCommon(e.target.value)">
<a-radio v-for="item of originColumn.options" :key="item.value" :value="item.value" @click="(e) => handleRadioClick(item, e)"
>{{ item.text || item.label || item.title || item.value }}
</a-radio>
</a-radio-group>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
export default defineComponent({
name: 'JVxeRadioCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
const scrolling = computed(() => !!props.renderOptions.scrolling);
const clazz = computed(() => {
return {
'j-vxe-radio': true,
'no-animation': scrolling.value,
};
});
function handleRadioClick(item) {
if (originColumn.value.allowClear === true) {
// 取消选择
if (item.value === innerValue.value) {
handleChangeCommon(null);
}
}
}
return {
clazz,
innerValue,
originColumn,
cellProps,
handleRadioClick,
handleChangeCommon,
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
switches: { visible: true },
} as JVxeComponent.EnhancedPartial,
});
</script>
<style lang="less">
// 关闭动画,防止滚动时动态赋值出现问题
.j-vxe-radio.no-animation {
.ant-radio-inner,
.ant-radio-inner::after {
transition: none !important;
}
}
</style>

View File

@ -0,0 +1,240 @@
<template>
<a-select :value="innerValue" v-bind="selectProps">
<template v-if="loading" #notFoundContent>
<LoadingOutlined />
<span>&nbsp;加载中</span>
</template>
<template v-for="option of selectOptions" :key="option.value">
<a-select-option :value="option.value" :title="option.text || option.label || option.title" :disabled="option.disabled">
<span>{{ option.text || option.label || option.title || option.value }}</span>
</a-select-option>
</template>
</a-select>
</template>
<script lang="ts">
import { ref, computed, defineComponent } from 'vue';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
import { isPromise } from '/@/utils/is';
export default defineComponent({
name: 'JVxeSelectCell',
components: { LoadingOutlined },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, row, originColumn, scrolling, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
const loading = ref(false);
// 异步加载的options用于多级联动
const asyncOptions = ref<any[] | null>(null);
// 下拉框 props
const selectProps = computed(() => {
let selProps = {
...cellProps.value,
allowClear: true,
autofocus: true,
defaultOpen: !scrolling.value,
style: { width: '100%' },
filterOption: handleSelectFilterOption,
onBlur: handleBlur,
onChange: handleChange,
};
// 判断select是否允许输入
let { allowSearch, allowInput } = originColumn.value;
if (allowInput === true || allowSearch === true) {
selProps['showSearch'] = true;
selProps['onSearch'] = handleSearchSelect;
}
return selProps;
});
// 下拉选项
const selectOptions = computed(() => {
if (asyncOptions.value) {
return asyncOptions.value;
}
let { linkage } = props.renderOptions;
if (linkage) {
let { getLinkageOptionsSibling, config } = linkage;
let res = getLinkageOptionsSibling(row.value, originColumn.value, config, true);
// 当返回Promise时说明是多级联动
if (res instanceof Promise) {
loading.value = true;
res
.then((opt) => {
asyncOptions.value = opt;
loading.value = false;
})
.catch((e) => {
console.error(e);
loading.value = false;
});
} else {
asyncOptions.value = null;
return res;
}
}
return originColumn.value.options;
});
// --------- created ---------
// 多选、搜索type
let multipleTypes = [JVxeTypes.selectMultiple, 'list_multi'];
let searchTypes = [JVxeTypes.selectSearch, 'sel_search'];
if (multipleTypes.includes(props.type)) {
// 处理多选
let props = originColumn.value.props || {};
props['mode'] = 'multiple';
props['maxTagCount'] = 1;
//update-begin-author:taoyan date:2022-12-5 for: issues/271 Online表单主子表单下拉多选无法搜索
originColumn.value.allowSearch = true;
//update-end-author:taoyan date:2022-12-5 for: issues/271 Online表单主子表单下拉多选无法搜索
originColumn.value.props = props;
} else if (searchTypes.includes(props.type)) {
// 处理搜索
originColumn.value.allowSearch = true;
}
/** 处理 change 事件 */
function handleChange(value) {
// 处理下级联动
let linkage = props.renderOptions.linkage;
if (linkage) {
linkage.handleLinkageSelectChange(row.value, originColumn.value, linkage.config, value);
}
handleChangeCommon(value);
}
/** 处理blur失去焦点事件 */
function handleBlur(value) {
let { allowInput, options } = originColumn.value;
if (allowInput === true) {
// 删除无用的因搜索(用户输入)而创建的项
if (typeof value === 'string') {
let indexes: number[] = [];
options.forEach((option, index) => {
if (option.value.toLocaleString() === value.toLocaleString()) {
delete option.searchAdd;
} else if (option.searchAdd === true) {
indexes.push(index);
}
});
// 翻转删除数组中的项
for (let index of indexes.reverse()) {
options.splice(index, 1);
}
}
}
handleBlurCommon(value);
}
/** 用于搜索下拉框中的内容 */
function handleSelectFilterOption(input, option) {
let { allowSearch, allowInput } = originColumn.value;
if (allowSearch === true || allowInput === true) {
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-5806】js增强改变下拉搜索options (防止option.title为null报错)
if (option.title == null) return false;
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-5806】js增强改变下拉搜索options (防止option.title为null报错)
// update-begin--author:liaozhiyang---date:20230904---for【issues/5305】JVxeTypes.select 无法按照预期进行搜索
return option.title.toLowerCase().indexOf(input.toLowerCase()) >= 0;
// update-begin--author:liaozhiyang---date:20230904---for【issues/5305】JVxeTypes.select 无法按照预期进行搜索
}
return true;
}
/** select 搜索时的事件用于动态添加options */
function handleSearchSelect(value) {
let { allowSearch, allowInput, options } = originColumn.value;
if (allowSearch !== true && allowInput === true) {
// 是否找到了对应的项,找不到则添加这一项
let flag = false;
for (let option of options) {
if (option.value.toLocaleString() === value.toLocaleString()) {
flag = true;
break;
}
}
// !!value :不添加空值
if (!flag && !!value) {
// searchAdd 是否是通过搜索添加的
options.push({ title: value, value: value, searchAdd: true });
}
}
}
return {
loading,
innerValue,
selectProps,
selectOptions,
handleChange,
handleBlur,
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
aopEvents: {
editActived({ $event, row, column }) {
dispatchEvent({
$event,
row,
column,
props: this.props,
instance: this,
className: '.ant-select .ant-select-selection-search-input',
isClick: false,
handler: (el) => el.focus(),
});
},
},
translate: {
enabled: true,
async handler(value, ctx) {
let { props, context } = ctx!;
let { row, originColumn } = context;
let options;
let linkage = props?.renderOptions.linkage;
// 判断是否是多级联动,如果是就通过接口异步翻译
if (linkage) {
let { getLinkageOptionsSibling, config } = linkage;
let linkageOptions = getLinkageOptionsSibling(row.value, originColumn.value, config, true);
options = isPromise(linkageOptions) ? await linkageOptions : linkageOptions;
} else if (isPromise(originColumn.value.optionsPromise)) {
options = await originColumn.value.optionsPromise;
} else {
options = originColumn.value.options;
}
return filterDictText(options, value);
},
},
getValue(value) {
if (Array.isArray(value)) {
return value.join(',');
} else {
return value;
}
},
setValue(value, ctx) {
let { context } = ctx!;
let { originColumn } = context;
// 判断是否是多选
if ((originColumn.value.props || {})['mode'] === 'multiple') {
originColumn.value.props['maxTagCount'] = 1;
}
if (value != null && value !== '') {
if (typeof value === 'string') {
return value === '' ? [] : value.split(',');
}
return value;
} else {
return undefined;
}
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,41 @@
import { computed, defineComponent, h } from 'vue';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/src/hooks/useJVxeComponent';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/src/types/JVxeComponent';
export default defineComponent({
name: 'JVxeSlotCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const data = useJVxeComponent(props);
const slotProps = computed(() => {
return {
value: data.innerValue.value,
row: data.row.value,
column: data.originColumn.value,
params: props.params,
$table: props.params.$table,
rowId: props.params.rowid,
index: props.params.rowIndex,
rowIndex: props.params.rowIndex,
columnIndex: props.params.columnIndex,
scrolling: props.renderOptions.scrolling,
reloadEffect: props.renderOptions.reloadEffect.enabled,
triggerChange: (v) => data.handleChangeCommon(v),
};
});
return () => {
let { slot } = props.renderOptions;
if (slot) {
return h('div', {}, slot(slotProps.value));
} else {
return h('div');
}
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
switches: {
editRender: false,
},
} as JVxeComponent.EnhancedPartial,
});

View File

@ -0,0 +1,57 @@
<template>
<JInputPop
:value="innerValue"
:width="300"
:height="210"
:pop-container="getPopupContainer"
v-bind="cellProps"
style="width: 100%"
@blur="handleBlurCommon"
@change="handleChangeCommon"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import JInputPop from '/@/components/Form/src/jeecg/components/JInputPop.vue';
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
export default defineComponent({
name: 'JVxeTextareaCell',
components: { JInputPop },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
function getPopupContainer() {
return document.body;
}
return { innerValue, cellProps, handleChangeCommon, handleBlurCommon, getPopupContainer };
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
installOptions: {
autofocus: '.ant-input',
},
aopEvents: {
editActived({ $event, row, column }) {
// 是否默认打开右侧弹窗
if (column.params.defaultOpen ?? false) {
dispatchEvent({
$event,
row,
column,
props: this.props,
instance: this,
className: '.ant-input-suffix .app-iconify',
isClick: true,
});
}
},
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,64 @@
<template>
<TimePicker
:value="innerTimeValue"
allowClear
:format="format"
popupClassName="j-vxe-time-picker"
style="min-width: 0"
v-model:open="openPicker"
v-bind="cellProps"
@change="handleChange"
/>
</template>
<script lang="ts">
import { ref, computed, watch, defineComponent } from 'vue';
import dayjs from 'dayjs';
import { TimePicker } from 'ant-design-vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { isEmpty } from '/@/utils/is';
export default defineComponent({
name: 'JVxeTimeCell',
components: { TimePicker },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
const innerTimeValue = ref<any>(null);
const format = computed(() => {
let format = originColumn.value.format;
return format ? format : 'HH:mm:ss';
});
const openPicker = ref(true);
watch(
innerValue,
(val) => {
if (val == null || isEmpty(val)) {
innerTimeValue.value = null;
} else {
innerTimeValue.value = dayjs(val, format.value);
}
},
{ immediate: true }
);
function handleChange(_mom, dateStr) {
handleChangeCommon(dateStr);
}
return {
cellProps,
format,
innerTimeValue,
openPicker,
handleChange,
};
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
aopEvents: {
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,77 @@
<template>
<div>
<template v-if="hasFile" v-for="(file, fileKey) of [innerFile || {}]" :key="fileKey">
<a-input :readOnly="true" :value="file.name">
<template #addonBefore style="width: 30px">
<a-tooltip v-if="file.status === 'uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
<LoadingOutlined />
</a-tooltip>
<a-tooltip v-else-if="file.status === 'done'" title="上传完成">
<Icon icon="ant-design:check-circle-outlined" style="color: #00db00" />
</a-tooltip>
<a-tooltip v-else :title="file.message || '上传失败'">
<Icon icon="ant-design:exclamation-circle-outlined" style="color: red" />
</a-tooltip>
</template>
<span v-if="file.status === 'uploading'" slot="addonAfter">{{ Math.floor(file.percent) }}%</span>
<template v-if="originColumn.allowDownload !== false || originColumn.allowRemove !== false" #addonAfter>
<Dropdown :trigger="['click']" placement="bottomRight">
<a-tooltip title="操作">
<Icon icon="ant-design:setting-outlined" style="cursor: pointer" />
</a-tooltip>
<template #overlay>
<a-menu>
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
<span><Icon icon="ant-design:download-outlined" />&nbsp;下载</span>
</a-menu-item>
<a-menu-item v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
<span><Icon icon="ant-design:delete-outlined" />&nbsp;删除</span>
</a-menu-item>
</a-menu>
</template>
</Dropdown>
</template>
</a-input>
</template>
<a-upload
v-if="!cellProps.disabledTable"
v-show="!hasFile"
name="file"
:data="{ isup: 1 }"
:multiple="false"
:action="originColumn.action"
:headers="uploadHeaders"
:showUploadList="false"
v-bind="cellProps"
@change="handleChangeUpload"
>
<a-button preIcon="ant-design:upload-outlined">{{ originColumn.btnText || '点击上传' }}</a-button>
</a-upload>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Icon } from '/@/components/Icon';
import { Dropdown } from 'ant-design-vue';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { useJVxeUploadCell, fileGetValue, fileSetValue } from '../../hooks/cells/useJVxeUploadCell';
export default defineComponent({
name: 'JVxeUploadCell',
components: { Icon, Dropdown, LoadingOutlined },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const setup = useJVxeUploadCell(props);
return { ...setup };
},
// 【组件增强】注释详见JVxeComponent.Enhanced
enhanced: {
switches: { visible: true },
getValue: (value) => fileGetValue(value),
setValue: (value) => fileSetValue(value),
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@ -0,0 +1,139 @@
import { ref, computed, watch } from 'vue';
import {getTenantId, getToken} from '/@/utils/auth';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { JVxeComponent } from '../../types/JVxeComponent';
import { useJVxeComponent } from '../useJVxeComponent';
/**
* use 公共上传组件
* @param props
* @param options 组件选项token默认是否传递tokenaction默认上传路径multiple是否允许多文件
*/
export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
const setup = useJVxeComponent(props);
const { innerValue, originColumn, handleChangeCommon } = setup;
const innerFile = ref<any>(null);
/** upload headers */
const uploadHeaders = computed(() => {
let headers = {};
if ((originColumn.value.token ?? options?.token ?? false) === true) {
headers['X-Access-Token'] = getToken();
}
let tenantId = getTenantId();
headers['X-Tenant-Id'] = tenantId ? tenantId : '0';
return headers;
});
/** 上传请求地址 */
const uploadAction = computed(() => {
if (!originColumn.value.action) {
return options?.action ?? '';
} else {
return originColumn.value.action;
}
});
const hasFile = computed(() => innerFile.value != null);
const responseName = computed(() => originColumn.value.responseName ?? 'message');
watch(
innerValue,
(val) => {
if (val) {
innerFile.value = val;
} else {
innerFile.value = null;
}
},
{ immediate: true }
);
function handleChangeUpload(info) {
let { file } = info;
let value = {
name: file.name,
type: file.type,
size: file.size,
status: file.status,
percent: file.percent,
path: innerFile.value?.path ?? '',
};
if (file.response) {
value['responseName'] = file.response[responseName.value];
}
let paths: string[] = [];
if (options?.multiple && innerFile.value && innerFile.value.path) {
paths = innerFile.value.path.split(',');
}
if (file.status === 'done') {
if (typeof file.response.success === 'boolean') {
if (file.response.success) {
paths.push(file.response[responseName.value]);
value['path'] = paths.join(',');
handleChangeCommon(value);
} else {
value['status'] = 'error';
value['message'] = file.response.message || '未知错误';
}
} else {
// 考虑到如果设置action上传路径为非jeecg-boot后台可能不会返回 success 属性的情况,就默认为成功
paths.push(file.response[responseName.value]);
value['path'] = paths.join(',');
handleChangeCommon(value);
}
} else if (file.status === 'error') {
value['message'] = file.response.message || '未知错误';
}
innerFile.value = value;
}
function handleClickDownloadFile() {
let { url, path } = innerFile.value || {};
if (!url || url.length === 0) {
if (path && path.length > 0) {
url = getFileAccessHttpUrl(path.split(',')[0]);
}
}
if (url) {
window.open(url);
}
}
function handleClickDeleteFile() {
handleChangeCommon(null);
}
return {
...setup,
innerFile,
uploadAction,
uploadHeaders,
hasFile,
responseName,
handleChangeUpload,
handleClickDownloadFile,
handleClickDeleteFile,
};
}
export function fileGetValue(value) {
if (value && value.path) {
return value.path;
}
return value;
}
export function fileSetValue(value) {
if (value) {
let first = value.split(',')[0];
let name = first.substring(first.lastIndexOf('/') + 1);
return {
name: name,
path: value,
status: 'done',
};
}
return value;
}

View File

@ -0,0 +1,420 @@
import type { JVxeColumn, JVxeDataProps, JVxeTableProps } from '../types';
import { computed, nextTick } from 'vue';
import { isArray, isEmpty, isPromise } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
import { initDictOptions } from '/@/utils/dict';
import { pushIfNotExist } from '/@/utils/common/compUtils';
import { getEnhanced } from '../utils/enhancedUtils';
import { isRegistered } from '../utils/registerUtils';
import { JVxeComponent } from '../types/JVxeComponent';
import { useValidateRules } from './useValidateRules';
import { JVxeTableMethods } from '../types';
// handle 方法参数
export interface HandleArgs {
props: JVxeTableProps;
slots: any;
data: JVxeDataProps;
methods: JVxeTableMethods;
col?: JVxeColumn;
columns: JVxeColumn[];
renderOptions?: any;
enhanced?: JVxeComponent.Enhanced;
}
export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
data.vxeColumns = computed(() => {
let columns: JVxeColumn[] = [];
if (isArray(props.columns)) {
// handle 方法参数
const args: HandleArgs = { props, slots, data, methods, columns };
let seqColumn, selectionColumn, expandColumn, dragSortColumn;
props.columns.forEach((column: JVxeColumn) => {
// 排除未授权的列 1 = 显示/隐藏; 2 = 禁用
let auth = methods.getColAuth(column.key);
if (auth?.type == '1' && !auth.isAuth) {
return;
} else if (auth?.type == '2' && !auth.isAuth) {
column.disabled = true;
}
// type 不填,默认为 normal
if (column.type == null || isEmpty(column.type)) {
column.type = JVxeTypes.normal;
}
let col: JVxeColumn = cloneDeep(column);
// 处理隐藏列
if (col.type === JVxeTypes.hidden) {
return handleInnerColumn(args, col, handleHiddenColumn);
}
// 组件未注册,自动设置为 normal
if (!isRegistered(col.type)) {
col.type = JVxeTypes.normal;
}
args.enhanced = getEnhanced(col.type);
args.col = col;
args.renderOptions = {
bordered: props.bordered,
disabled: props.disabled,
scrolling: data.scrolling,
isDisabledRow: methods.isDisabledRow,
listeners: {
trigger: (name, event) => methods.trigger(name, event),
valueChange: (event) => methods.trigger('valueChange', event),
/** 重新排序行 */
rowResort: (event) => {
methods.doSort(event.oldIndex, event.newIndex);
methods.trigger('dragged', event);
},
/** 在当前行下面插入一行 */
rowInsertDown: (rowIndex) => methods.insertRows({}, rowIndex + 1),
},
};
if (col.type === JVxeTypes.rowNumber) {
seqColumn = col;
columns.push(col);
} else if (col.type === JVxeTypes.rowRadio || col.type === JVxeTypes.rowCheckbox) {
selectionColumn = col;
columns.push(col);
} else if (col.type === JVxeTypes.rowExpand) {
expandColumn = col;
columns.push(col);
} else if (col.type === JVxeTypes.rowDragSort) {
dragSortColumn = col;
columns.push(col);
} else {
col.params = column;
handlerCol(args);
}
});
handleInnerColumn(args, seqColumn, handleSeqColumn);
handleInnerColumn(args, selectionColumn, handleSelectionColumn);
handleInnerColumn(args, expandColumn, handleExpandColumn);
handleInnerColumn(args, dragSortColumn, handleDragSortColumn, true);
// update-begin--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
customComponentAddStar(columns);
// update-end--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
}
return columns;
});
}
/**
* 2024-05-30
* liaozhiyang
* 不可编辑组件必填通过title人为加*号
*/
function customComponentAddStar(columns) {
columns.forEach((column) => {
const { params } = column;
if (params) {
const { validateRules, type } = params;
if (
validateRules?.length &&
[
JVxeTypes.checkbox,
JVxeTypes.radio,
JVxeTypes.upload,
JVxeTypes.progress,
JVxeTypes.departSelect,
JVxeTypes.userSelect,
JVxeTypes.image,
JVxeTypes.file,
].includes(type)
) {
if (validateRules.find((item) => item.required)) {
column.title = ` * ${column.title}`;
}
}
}
});
}
/** 处理内置列 */
function handleInnerColumn(args: HandleArgs, col: JVxeColumn, handler: (args: HandleArgs) => void, assign?: boolean) {
let renderOptions = col?.editRender || col?.cellRender;
return handler({
...args,
col: col,
renderOptions: assign ? Object.assign({}, args.renderOptions, renderOptions) : renderOptions,
});
}
/**
* 处理隐藏列
*/
function handleHiddenColumn({ col, columns }: HandleArgs) {
col!.params = cloneDeep(col);
delete col!.type;
col!.field = col!.key;
col!.visible = false;
columns.push(col!);
}
/**
* 处理行号列
*/
function handleSeqColumn({ props, col, columns }: HandleArgs) {
// 判断是否开启了行号列
if (props.rowNumber) {
let column = {
type: 'seq',
title: '#',
width: 60,
// 【QQYUN-8405】
fixed: props.rowNumberFixed,
align: 'center',
};
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8405】vxetable支持序号是否固定移动端需要
if (props.rowNumberFixed === 'none') {
delete column.fixed;
}
// update-end--author:liaozhiyang---date:20240306---forQQYUN-8405】vxetable支持序号是否固定移动端需要
if (col) {
Object.assign(col, column);
} else {
columns.unshift(column as any);
}
}
}
/**
* 处理可选择列
*/
function handleSelectionColumn({ props, data, col, columns }: HandleArgs) {
// 判断是否开启了可选择行
if (props.rowSelection) {
let width = 45;
if (data.statistics.has && !props.rowExpand && !props.dragSort) {
width = 60;
}
let column: any = {
type: props.rowSelectionType,
width: width,
fixed: 'left',
align: 'center',
};
// update-begin--author:liaozhiyang---date:20240509---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
if (props.rowSelectionFixed === 'none') {
delete column.fixed;
}
// update-end--author:liaozhiyang---date:20240509---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
if (col) {
Object.assign(col, column);
} else {
columns.unshift(column as any);
}
}
}
/**
* 处理可展开行
*/
function handleExpandColumn({ props, data, col, columns }: HandleArgs) {
// 是否可展开行
if (props.rowExpand) {
let width = 40;
if (data.statistics.has && !props.dragSort) {
width = 60;
}
let column = {
type: 'expand',
title: '',
width: width,
fixed: 'left',
align: 'center',
slots: { content: 'expandContent' },
};
if (col) {
Object.assign(col, column);
} else {
columns.unshift(column as any);
}
}
}
/** 处理可排序列 */
function handleDragSortColumn({ props, data, col, columns, renderOptions }: HandleArgs) {
// 是否可拖动排序
if (props.dragSort) {
let width = 40;
if (data.statistics.has) {
width = 60;
}
let column: any = {
title: '',
width: width,
fixed: 'left',
align: 'center',
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
params: {
notAllowDrag: props.notAllowDrag,
...col?.params,
},
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
};
// update-begin--author:liaozhiyang---date:20240506---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
if (props.dragSortFixed === 'none') {
delete column.fixed;
}
// update-end--author:liaozhiyang---date:20240506---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
let cellRender = {
name: JVxeTypePrefix + JVxeTypes.rowDragSort,
sortKey: props.sortKey,
};
if (renderOptions) {
column.cellRender = Object.assign(renderOptions, cellRender);
} else {
column.cellRender = cellRender;
}
if (col) {
Object.assign(col, column);
} else {
columns.unshift(column);
}
}
}
/** 处理自定义组件列 */
function handlerCol(args: HandleArgs) {
const { props, col, columns, enhanced } = args;
if (!col) return;
let { type } = col;
col.field = col.key;
delete col.type;
let renderName = 'cellRender';
// 渲染选项
let $renderOptions: any = { name: JVxeTypePrefix + type };
if (enhanced?.switches.editRender) {
if (!(enhanced.switches.visible || props.alwaysEdit)) {
renderName = 'editRender';
}
// $renderOptions.type = (enhanced.switches.visible || props.alwaysEdit) ? 'visible' : 'default'
}
col[renderName] = $renderOptions;
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-5806】js增强改变下拉搜索options添加customOptions为true不读字典走自己的options
!col.params.customOptions && handleDict(args);
// update-end--author:liaozhiyang---date:20240321---for【QQYUN-5806】js增强改变下拉搜索options添加customOptions为true不读字典走自己的options
handleRules(args);
handleStatistics(args);
handleSlots(args);
handleLinkage(args);
handleReloadEffect(args);
if (col.editRender) {
Object.assign(col.editRender, args.renderOptions);
}
if (col.cellRender) {
Object.assign(col.cellRender, args.renderOptions);
}
columns.push(col);
}
/**
* 处理字典
*/
async function handleDict({ col, methods }: HandleArgs) {
if (col && col.params.dictCode) {
/** 加载数据字典并合并到 options */
try {
// 查询字典
if (!isPromise(col.params.optionsPromise)) {
col.params.optionsPromise = new Promise(async (resolve) => {
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1180 【代码生成】子表不支持带条件?
let dictCodeString = col.params.dictCode;
if (dictCodeString) {
dictCodeString = encodeURI(dictCodeString);
}
const dictOptions: any = await initDictOptions(dictCodeString);
//update-end-author:taoyan date:2022-6-1 for: VUEN-1180 【代码生成】子表不支持带条件?
let options = col.params.options ?? [];
dictOptions.forEach((dict) => {
// 过滤重复数据
if (options.findIndex((o) => o.value === dict.value) === -1) {
options.push(dict);
}
});
resolve(options);
});
}
col.params.options = await col.params.optionsPromise;
await nextTick();
await methods.getXTable().updateData();
} catch (e) {
console.group(`[JVxeTable] 查询字典 "${col.params.dictCode}" 时发生异常!`);
console.warn(e);
console.groupEnd();
}
}
}
/**
* 处理校验
*/
function handleRules(args: HandleArgs) {
if (isArray(args.col?.validateRules)) {
useValidateRules(args);
}
}
/**
* 处理统计列
*/
function handleStatistics({ col, data }: HandleArgs) {
// sum = 求和、average = 平均值
if (col && isArray(col.statistics)) {
data.statistics.has = true;
col.statistics.forEach((item) => {
if (!isEmpty(item)) {
let arr = data.statistics[(item as string).toLowerCase()];
if (isArray(arr)) {
pushIfNotExist(arr, col.key);
}
}
});
}
}
/**
* 处理插槽
*/
function handleSlots({ slots, col, renderOptions }: HandleArgs) {
// slot 组件特殊处理
if (col && col.params.type === JVxeTypes.slot) {
if (!isEmpty(col.slotName) && slots.hasOwnProperty(col.slotName)) {
renderOptions.slot = slots[col.slotName];
}
}
}
/** 处理联动列 */
function handleLinkage({ data, col, renderOptions, methods }: HandleArgs) {
// 处理联动列,联动列只能作用于 select 组件
if (col && col.params.type === JVxeTypes.select && data.innerLinkageConfig != null) {
// 判断当前列是否是联动列
if (data.innerLinkageConfig.has(col.key)) {
renderOptions.linkage = {
config: data.innerLinkageConfig.get(col.key),
getLinkageOptionsAsync: methods.getLinkageOptionsAsync,
getLinkageOptionsSibling: methods.getLinkageOptionsSibling,
handleLinkageSelectChange: methods.handleLinkageSelectChange,
};
}
}
}
function handleReloadEffect({ props, data, renderOptions }: HandleArgs) {
renderOptions.reloadEffect = {
enabled: props.reloadEffect,
getMap() {
return data.reloadEffectRowKeysMap;
},
isEffect(rowId) {
return data.reloadEffectRowKeysMap[rowId] === true;
},
removeEffect(rowId) {
return (data.reloadEffectRowKeysMap[rowId] = false);
},
};
}

View File

@ -0,0 +1,105 @@
import { computed } from 'vue';
import { router } from '/@/router';
import { createLocalStorage } from '/@/utils/cache';
import { useMessage } from '/@/hooks/web/useMessage';
export function useColumnsCache({ cacheColumnsKey, refs }: any) {
const $ls = createLocalStorage();
const { createMessage: $message } = useMessage();
const cacheKey = computed(() => {
const path = router.currentRoute.value.fullPath;
let key = path.replace(/[\/\\]/g, '_');
if (cacheColumnsKey) {
key += ':' + cacheColumnsKey;
}
return 'vxe-columnCache:' + key;
});
const initSetting = (props) => {
const columnCache = $ls.get(cacheKey.value);
if (columnCache) {
columnCache.forEach((key) => {
const column = props.columns.find((item) => item.key === key);
if (column) {
column.visible = false;
}
});
}
};
// const initSetting = (refs) => {
// let columnCache = $ls.get(cacheKey.value);
// if (columnCache) {
// const $grid = refs.gridRef.value!.getRefMaps().refTable.value;
// console.log('refs.gridRef', $grid);
// const { fullColumn } = $grid.getTableColumn();
// const hideColumns = getHideColumn(fullColumn, columnCache);
// if (hideColumns?.length) {
// hideColumns.forEach((column) => {
// $grid.hideColumn(column);
// });
// }
// }
// console.log(columnCache);
// };
function saveSetting($grid: any) {
console.log($grid);
const { fullColumn, visibleColumn } = $grid.getTableColumn();
const hideColumnKey = getHideColumnKey(fullColumn, visibleColumn);
if (hideColumnKey.length) {
$ls.set(cacheKey.value, hideColumnKey);
$message.success('保存成功');
}
}
const resetSetting = ($grid) => {
const columnCache = $ls.get(cacheKey.value);
if (columnCache) {
const { fullColumn } = $grid.getTableColumn();
const hideColumns = getHideColumn(fullColumn, columnCache);
if (hideColumns?.length) {
hideColumns.forEach((column) => {
if (columnCache.includes(column?.params?.key)) {
$grid.showColumn(column);
}
});
}
}
$ls.remove(cacheKey.value);
$message.success('重置成功');
};
const getHideColumn = (fullColumn, columnCache) => {
const result: any = [];
if (columnCache?.length) {
console.log('--fullColumn:',fullColumn);
columnCache.forEach((key) => {
const column = fullColumn.find((item) => item?.params?.key === key);
if (column) {
result.push(column);
}
});
}
return result;
};
const getHideColumnKey = (fullColumn, visibleColumn) => {
const reuslt: any = [];
if (fullColumn.length === visibleColumn.length) {
return reuslt;
} else {
fullColumn.forEach((item) => {
const fKey = item?.params?.key;
if (fKey) {
const vItem = visibleColumn.find((item) => {
return item?.params?.key === fKey;
});
if (!vItem) {
reuslt.push(fKey);
}
}
});
return reuslt;
}
};
return {
initSetting,
resetSetting,
saveSetting,
};
}

View File

@ -0,0 +1,102 @@
import { ref, reactive, provide, resolveComponent } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { JVxeDataProps, JVxeRefs, JVxeTableProps } from '../types';
import { VxeGridInstance } from 'vxe-table';
import { randomString } from '/@/utils/common/compUtils';
export function useData(props: JVxeTableProps): JVxeDataProps {
const { prefixCls } = useDesign('j-vxe-table');
provide('prefixCls', prefixCls);
return {
prefixCls: prefixCls,
caseId: `j-vxe-${randomString(8)}`,
vxeDataSource: ref([]),
scroll: reactive({ top: 0, left: 0 }),
scrolling: ref(false),
defaultVxeProps: reactive({
// update-begin--author:liaozhiyang---date:20240607---for【TV360X-327】vxetable警告
// rowId: props.rowKey,
rowConfig: {
keyField: props.rowKey,
},
// update-end--author:liaozhiyang---date:20240607---for【TV360X-327】vxetable警告
// 高亮hover的行
highlightHoverRow: true,
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
// 溢出隐藏并显示tooltip
showOverflow: "title",
// 表头溢出隐藏并显示tooltip
showHeaderOverflow: "title",
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
showFooterOverflow: true,
// 可编辑配置
editConfig: {
trigger: 'click',
mode: 'cell',
// update-begin--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
//activeMethod: () => !props.disabled,
beforeEditMethod: () => !props.disabled,
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
},
expandConfig: {
iconClose: 'ant-table-row-expand-icon ant-table-row-expand-icon-collapsed',
iconOpen: 'ant-table-row-expand-icon ant-table-row-expand-icon-expanded',
},
// 虚拟滚动配置y轴大于xx条数据时启用虚拟滚动
scrollY: {
gt: 30,
},
scrollX: {
gt: 20,
// 暂时关闭左右虚拟滚动
enabled: false,
},
radioConfig: { highlight: true },
checkboxConfig: { highlight: true },
mouseConfig: { selected: false },
keyboardConfig: {
// 删除键功能
isDel: false,
// Esc键关闭编辑功能
isEsc: true,
// Tab 键功能
isTab: true,
// 任意键进入编辑(功能键除外)
isEdit: true,
// 方向键功能
isArrow: true,
// 回车键功能
isEnter: true,
// 如果功能被支持,用于 column.type=checkbox|radio开启空格键切换复选框或单选框状态功能
isChecked: true,
},
}),
selectedRows: ref<any[]>([]),
selectedRowIds: ref<string[]>([]),
disabledRowIds: [],
statistics: reactive({
has: false,
sum: [],
average: [],
}),
authsMap: ref(null),
innerEditRules: {},
innerLinkageConfig: new Map<string, any>(),
reloadEffectRowKeysMap: reactive({}),
};
}
export function useRefs(): JVxeRefs {
return {
gridRef: ref<VxeGridInstance>(),
subPopoverRef: ref<any>(),
detailsModalRef: ref<any>(),
};
}
export function useResolveComponent(...t: any[]): any {
// @ts-ignore
return resolveComponent(...t);
}

View File

@ -0,0 +1,36 @@
import { nextTick, watch } from 'vue';
import { JVxeDataProps, JVxeRefs, JVxeTableMethods } from '../types';
import { cloneDeep } from 'lodash-es';
export function useDataSource(props, data: JVxeDataProps, methods: JVxeTableMethods, refs: JVxeRefs) {
watch(
() => props.dataSource,
async () => {
data.disabledRowIds = [];
data.vxeDataSource.value = cloneDeep(props.dataSource);
data.vxeDataSource.value.forEach((row, rowIndex) => {
// 判断是否是禁用行
if (methods.isDisabledRow(row, rowIndex)) {
data.disabledRowIds.push(row.id);
}
// 处理联动回显数据
methods.handleLinkageBackData(row);
});
await waitRef(refs.gridRef);
methods.recalcSortNumber();
},
{ immediate: true }
);
}
function waitRef($ref) {
return new Promise<any>((resolve) => {
(function next() {
if ($ref.value) {
resolve($ref);
} else {
nextTick(() => next());
}
})();
});
}

View File

@ -0,0 +1,79 @@
import { onMounted, onUnmounted, nextTick } from 'vue';
import { JVxeTableMethods, JVxeTableProps } from '/@/components/jeecg/JVxeTable/src/types';
import Sortable from 'sortablejs';
export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
if (props.dragSort) {
let sortable2: Sortable;
let initTime: any;
onMounted(() => {
// 加载完成之后再绑定拖动事件
initTime = setTimeout(createSortable, 300);
});
onUnmounted(() => {
clearTimeout(initTime);
if (sortable2) {
sortable2.destroy();
}
});
function createSortable() {
let xTable = methods.getXTable();
// let dom = xTable.$el.querySelector('.vxe-table--fixed-wrapper .vxe-table--body tbody')
let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
let startChildren = [];
sortable2 = Sortable.create(dom as HTMLElement, {
handle: '.drag-btn',
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
filter: '.not-allow-drag',
draggable: ".allow-drag",
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
direction: 'vertical',
animation: 300,
onStart(e) {
let from = e.from;
// @ts-ignore
startChildren = [...from.children];
},
onEnd(e) {
let oldIndex = e.oldIndex as number;
let newIndex = e.newIndex as number;
if (oldIndex === newIndex) {
return;
}
// 【VUEN-2505】获取当前行数据
let rowNode = xTable.getRowNode(e.item);
if (!rowNode) {
return;
}
let from = e.from;
let element = startChildren[oldIndex];
let target = null;
if (oldIndex > newIndex) {
// 向上移动
if (oldIndex + 1 < startChildren.length) {
target = startChildren[oldIndex + 1];
}
} else {
// 向下移动
target = startChildren[oldIndex + 1];
}
from.removeChild(element);
from.insertBefore(element, target);
nextTick(() => {
// 【VUEN-2505】算出因虚拟滚动导致的偏移量
let diffIndex = rowNode!.index - oldIndex;
if (diffIndex > 0) {
oldIndex = oldIndex + diffIndex;
newIndex = newIndex + diffIndex;
}
methods.doSort(oldIndex, newIndex);
methods.trigger('dragged', { oldIndex, newIndex });
});
},
});
}
}
}

View File

@ -0,0 +1,121 @@
import { unref, computed } from 'vue';
import { merge } from 'lodash-es';
import { isArray } from '/@/utils/is';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { useKeyboardEdit } from '../hooks/useKeyboardEdit';
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
export function useFinallyProps(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods) {
const attrs = useAttrs();
// vxe 键盘操作配置
const { keyboardEditConfig } = useKeyboardEdit(props);
// vxe 最终 editRules
const vxeEditRules = computed(() => merge({}, props.editRules, data.innerEditRules));
// vxe 最终 events
const vxeEvents = computed(() => {
let listeners = { ...unref(attrs) };
let events = {
onScroll: methods.handleVxeScroll,
onCellClick: methods.handleCellClick,
onEditClosed: methods.handleEditClosed,
onEditActived: methods.handleEditActived,
onRadioChange: methods.handleVxeRadioChange,
onCheckboxAll: methods.handleVxeCheckboxAll,
onCheckboxChange: methods.handleVxeCheckboxChange,
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-8566】JVXETable无法记住列设置
onCustom: methods.handleCustom,
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-8566】JVXETable无法记住列设置
};
// 用户传递的事件,进行合并操作
Object.keys(listeners).forEach((key) => {
let listen = listeners[key];
if (events.hasOwnProperty(key)) {
if (isArray(listen)) {
listen.push(events[key]);
} else {
listen = [events[key], listen];
}
}
events[key] = listen;
});
return events;
});
// vxe 最终 props
const vxeProps = computed(() => {
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
let rowClass = {};
if (props.dragSort) {
rowClass = {
rowClassName: (params) => {
let { row } = params;
const find = props.notAllowDrag?.find((item:any) => {
const {key, value} = item;
return row[key] == value;
});
// 业务传进的来的rowClassName
const popsRowClassName = props.rowClassName ?? '';
let outClass = '';
if(typeof popsRowClassName==='string'){
popsRowClassName && (outClass = popsRowClassName);
}else if(typeof popsRowClassName==='function'){
outClass = popsRowClassName(params)
}
return find ? `not-allow-drag ${outClass}` : `allow-drag ${outClass}`;
},
};
}
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
return merge(
{},
data.defaultVxeProps,
{
showFooter: data.statistics.has,
},
unref(attrs),
{
ref: 'gridRef',
size: props.size,
loading: false,
disabled: props.disabled,
columns: unref(data.vxeColumns),
editRules: unref(vxeEditRules),
height: props.height === 'auto' ? null : props.height,
maxHeight: props.maxHeight,
// update-begin--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
scrollY: props.scrollY,
scrollX: props.scrollX,
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
border: props.bordered,
footerMethod: methods.handleFooterMethod,
// 展开行配置
expandConfig: {
toggleMethod: methods.handleExpandToggleMethod,
},
// 可编辑配置
editConfig: {
// update-begin--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
//activeMethod: methods.handleActiveMethod,
beforeEditMethod: methods.handleActiveMethod,
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
},
radioConfig: {
checkMethod: methods.handleCheckMethod,
},
checkboxConfig: {
checkMethod: methods.handleCheckMethod,
},
...rowClass
// rowClassName:(params)=>{
// const { row } = params;
// return row.dbFieldName=='id'?"not-allow-drag":"allow-drag"
// }
},
unref(vxeEvents),
unref(keyboardEditConfig)
);
});
return {
vxeProps,
prefixCls: data.prefixCls,
};
}

View File

@ -0,0 +1,298 @@
import { computed, nextTick, ref, unref, watch } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign';
import { getEnhanced, replaceProps } from '../utils/enhancedUtils';
import { vModel } from '/@/components/jeecg/JVxeTable/utils';
import { JVxeRenderType } from '../types/JVxeTypes';
import { isBoolean, isFunction, isObject, isPromise } from '/@/utils/is';
import { JVxeComponent } from '../types/JVxeComponent';
import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
export function useJVxeCompProps() {
return {
// 组件类型
type: propTypes.string,
// 渲染类型
renderType: propTypes.string.def('default'),
// 渲染参数
params: propTypes.object,
// 渲染自定义选项
renderOptions: propTypes.object,
};
}
export function useJVxeComponent(props: JVxeComponent.Props) {
const value = computed(() => {
// update-begin--author:liaozhiyang---date:20240430---for【QQYUN-9125】oracle数据库日期类型字段会默认带上时分秒
const val = props.params.row[props.params.column.property];
if (props.type === 'date' && typeof val === 'string') {
return val.split(' ').shift();
} else {
return val;
}
// update-end--author:liaozhiyang---date:20240430---for【QQYUN-9125】oracle数据库日期类型字段会默认带上时分秒
});
const innerValue = ref(value.value);
const row = computed(() => props.params.row);
const rows = computed(() => props.params.data);
const column = computed(() => props.params.column);
// 用户配置的原始 column
const originColumn = computed(() => column.value.params);
const rowIndex = computed(() => props.params._rowIndex);
const columnIndex = computed(() => props.params._columnIndex);
// 表格数据长度
const fullDataLength = computed(() => props.params.$table.internalData.tableFullData.length);
// 是否正在滚动中
const scrolling = computed(() => !!props.renderOptions.scrolling);
const cellProps = computed(() => {
let renderOptions = props.renderOptions;
let col = originColumn.value;
let cellProps = {};
// 输入占位符
cellProps['placeholder'] = replaceProps(col, col.placeholder);
// 解析props
if (isObject(col.props)) {
Object.keys(col.props).forEach((key) => {
cellProps[key] = replaceProps(col, col.props[key]);
});
}
// 判断是否是禁用的列
cellProps['disabled'] = isBoolean(col['disabled']) ? col['disabled'] : cellProps['disabled'];
// 判断是否禁用行
if (renderOptions.isDisabledRow(row.value, rowIndex.value)) {
cellProps['disabled'] = true;
}
// update-begin--author:liaozhiyang---date:20240528---for【TV360X-291】没勾选同步数据库禁用排序功能
if (col.props && col.props.isDisabledCell) {
if (col.props.isDisabledCell({ row: row.value, rowIndex: rowIndex.value, column: col, columnIndex: columnIndex.value })) {
cellProps['disabled'] = true;
}
}
// update-end--author:liaozhiyang---date:20240528---for【TV360X-291】没勾选同步数据库禁用排序功能
// 判断是否禁用所有组件
if (renderOptions.disabled === true) {
cellProps['disabled'] = true;
// update-begin--author:liaozhiyang---date:20240607---for【TV360X-1068】行编辑整体禁用时上传按钮不显示
cellProps['disabledTable'] = true;
// update-end--author:liaozhiyang---date:20240607---for【TV360X-1068】行编辑整体禁用时上传按钮不显示
}
//update-begin-author:taoyan date:2022-5-25 for: VUEN-1111 一对多子表 部门选择 不应该级联
if (col.checkStrictly === true) {
cellProps['checkStrictly'] = true;
}
//update-end-author:taoyan date:2022-5-25 for: VUEN-1111 一对多子表 部门选择 不应该级联
//update-begin-author:taoyan date:2022-5-27 for: 用户组件 控制单选多选新的参数配置
if (col.isRadioSelection === true) {
cellProps['isRadioSelection'] = true;
} else if (col.isRadioSelection === false) {
cellProps['isRadioSelection'] = false;
}
//update-end-author:taoyan date:2022-5-27 for: 用户组件 控制单选多选新的参数配置
return cellProps;
});
const listeners = computed(() => {
let listeners = Object.assign({}, props.renderOptions.listeners || {});
// 默认change事件
if (!listeners.change) {
listeners.change = async (event) => {
vModel(event.value, row, column);
await nextTick();
// 处理 change 事件相关逻辑(例如校验)
props.params.$table.updateStatus(props.params);
};
}
return listeners;
});
const context = {
innerValue,
row,
rows,
rowIndex,
column,
columnIndex,
originColumn,
fullDataLength,
cellProps,
scrolling,
handleChangeCommon,
handleBlurCommon,
};
const ctx = { props, context };
// 获取组件增强
const enhanced = getEnhanced(props.type);
watch(
value,
(newValue) => {
// 验证值格式
let getValue = enhanced.getValue(newValue, ctx);
if (newValue !== getValue) {
// 值格式不正确,重新赋值
newValue = getValue;
vModel(newValue, row, column);
}
innerValue.value = enhanced.setValue(newValue, ctx);
// update-begin--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
if (props.type === 'date' && props.renderType === JVxeRenderType.spaner && enhanced.translate.enabled === true) {
if (isFunction(enhanced.translate.handler)) {
innerValue.value = enhanced.translate.handler(newValue, ctx);
}
return;
}
// update-end--author:liaozhiyang---date:20240509---for【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
// 判断是否启用翻译
if (props.renderType === JVxeRenderType.spaner && enhanced.translate.enabled === true) {
if (isFunction(enhanced.translate.handler)) {
let res = enhanced.translate.handler(newValue, ctx);
// 异步翻译,可解决字典查询慢的问题
if (isPromise(res)) {
res.then((v) => (innerValue.value = v));
} else {
innerValue.value = res;
}
}
}
},
{ immediate: true }
);
/** 通用处理 change 事件 */
function handleChangeCommon($value, force = false) {
const newValue = enhanced.getValue($value, ctx);
const oldValue = value.value;
// update-begin--author:liaozhiyang---date:20230718---for【issues-5025】JVueTable的事件 @valueChange重复触发问题
const execute = force ? true : newValue !== oldValue;
if (execute) {
trigger('change', { value: newValue });
// 触发valueChange事件
parentTrigger('valueChange', {
type: props.type,
value: newValue,
oldValue: oldValue,
col: originColumn.value,
rowIndex: rowIndex.value,
columnIndex: columnIndex.value,
});
}
// update-end--author:liaozhiyang---date:20230718---for【issues-5025】JVueTable的事件 @valueChange重复触发问题
}
/** 通用处理 blur 事件 */
function handleBlurCommon($value) {
// update-begin--author:liaozhiyang---date:20230817---for【issues/636】JVxeTable加上blur事件
const newValue = enhanced.getValue($value, ctx);
const oldValue = value.value;
//trigger('blur', { value });
// 触发blur事件
parentTrigger('blur', {
type: props.type,
value: newValue,
oldValue: oldValue,
col: originColumn.value,
rowIndex: rowIndex.value,
columnIndex: columnIndex.value,
});
// update-end--author:liaozhiyang---date:20230817---for【issues/636】JVxeTable加上blur事件
}
/**
* 如果事件存在的话,就触发
* @param name 事件名
* @param event 事件参数
* @param args 其他附带参数
*/
function trigger(name, event?, args: any[] = []) {
let listener = listeners.value[name];
if (isFunction(listener)) {
if (isObject(event)) {
event = packageEvent(name, event);
}
listener(event, ...args);
}
}
function parentTrigger(name, event, args: any[] = []) {
args.unshift(packageEvent(name, event));
trigger('trigger', name, args);
}
function packageEvent(name, event: any = {}) {
event.row = row.value;
event.column = column.value;
// online增强参数兼容
event.column['key'] = column.value['property'];
// event.cellTarget = this
if (!event.type) {
event.type = name;
}
if (!event.cellType) {
event.cellType = props.type;
}
// 是否校验表单默认为true
if (isBoolean(event.validate)) {
event.validate = true;
}
return event;
}
/**
* 防样式冲突类名生成器
* @param scope
*/
function useCellDesign(scope: string) {
return useDesign(`vxe-cell-${scope}`);
}
return {
...context,
enhanced,
trigger,
useCellDesign,
};
}
/**
* 获取组件默认增强
*/
export function useDefaultEnhanced(): JVxeComponent.EnhancedPartial {
return {
installOptions: {
autofocus: '',
},
interceptor: {
'event.clearActived': () => true,
'event.clearActived.className': () => true,
},
switches: {
editRender: true,
visible: false,
},
aopEvents: {
editActived() {},
editClosed() {},
activeMethod: () => true,
},
translate: {
enabled: false,
handler(value, ctx) {
// 默认翻译方法
if (ctx) {
return filterDictText(unref(ctx.context.column).params.options, value);
} else {
return value;
}
},
},
getValue: (value) => value,
setValue: (value) => value,
createValue: (defaultValue) => defaultValue,
} as JVxeComponent.Enhanced;
}

View File

@ -0,0 +1,37 @@
/*
* JVxeTable 键盘操作
*/
import type { VxeTablePropTypes } from 'vxe-table';
import type { JVxeTableProps } from '../types';
import { computed } from 'vue';
/**
* JVxeTable 键盘操作
*
* @param props
*/
export function useKeyboardEdit(props: JVxeTableProps) {
// 是否开启了键盘操作
const enabledKeyboard = computed(() => props.keyboardEdit ?? false);
// 重写 keyboardConfig
const keyboardConfig: VxeTablePropTypes.KeyboardConfig = {
editMethod({ row, column, $table }) {
// 重写默认的覆盖式,改为追加式
$table.setActiveCell(row, column);
return true;
},
};
// 键盘操作配置
const keyboardEditConfig = computed(() => {
return {
mouseConfig: {
selected: enabledKeyboard.value,
},
keyboardConfig,
};
});
return {
keyboardEditConfig,
};
}

View File

@ -0,0 +1,145 @@
import { watch } from 'vue';
import { isFunction, isPromise, isArray } from '/@/utils/is';
import { JVxeColumn, JVxeDataProps, JVxeTableProps, JVxeLinkageConfig } from '../types';
/**
* 多级联动
*/
export function useLinkage(props: JVxeTableProps, data: JVxeDataProps, methods) {
// 整理多级联动配置
watch(
() => props.linkageConfig,
(linkageConfig: JVxeLinkageConfig[]) => {
data.innerLinkageConfig.clear();
if (isArray(linkageConfig) && linkageConfig.length > 0) {
linkageConfig.forEach((config) => {
let keys = getLinkageKeys(config.key, []);
// 多个key共享一个引用地址
let configItem = {
...config,
keys,
optionsMap: new Map(),
};
keys.forEach((k) => data.innerLinkageConfig.set(k, configItem));
});
}
},
{ immediate: true }
);
// 获取联动的key顺序
function getLinkageKeys(key: string, keys: string[]): string[] {
let col = props.columns?.find((col: JVxeColumn) => col.key === key) as JVxeColumn;
if (col) {
keys.push(col.key);
// 寻找下级
if (col.linkageKey) {
return getLinkageKeys(col.linkageKey, keys);
}
}
return keys;
}
// 处理联动回显数据
function handleLinkageBackData(row) {
if (data.innerLinkageConfig.size > 0) {
for (let configItem of data.innerLinkageConfig.values()) {
autoSetLinkageOptionsByData(row, '', configItem, 0);
}
}
}
/** 【多级联动】获取同级联动下拉选项 */
function getLinkageOptionsSibling(row, col, config, request) {
// 如果当前列不是顶级列
let key = '';
if (col.key !== config.key) {
// 就找出联动上级列
let idx = config.keys.findIndex((k) => col.key === k);
let parentKey = config.keys[idx - 1];
key = row[parentKey];
// 如果联动上级列没有选择数据,就直接返回空数组
if (key === '' || key == null) {
return [];
}
} else {
key = 'root';
}
let options = config.optionsMap.get(key);
if (!Array.isArray(options)) {
if (request) {
let parent = key === 'root' ? '' : key;
return getLinkageOptionsAsync(config, parent);
} else {
options = [];
}
}
return options;
}
/** 【多级联动】获取联动下拉选项(异步) */
function getLinkageOptionsAsync(config, parent) {
return new Promise((resolve) => {
let key = parent ? parent : 'root';
let options;
if (config.optionsMap.has(key)) {
options = config.optionsMap.get(key);
if (isPromise(options)) {
options.then((opt) => {
config.optionsMap.set(key, opt);
resolve(opt);
});
} else {
resolve(options);
}
} else if (isFunction(config.requestData)) {
// 调用requestData方法通过传入parent来获取子级
// noinspection JSVoidFunctionReturnValueUsed,TypeScriptValidateJSTypes
let promise = config.requestData(parent);
config.optionsMap.set(key, promise);
promise.then((opt) => {
config.optionsMap.set(key, opt);
resolve(opt);
});
} else {
resolve([]);
}
});
}
// 【多级联动】 用于回显数据,自动填充 optionsMap
function autoSetLinkageOptionsByData(data, parent, config, level) {
if (level === 0) {
getLinkageOptionsAsync(config, '');
} else {
getLinkageOptionsAsync(config, parent);
}
if (config.keys.length - 1 > level) {
let value = data[config.keys[level]];
if (value) {
autoSetLinkageOptionsByData(data, value, config, level + 1);
}
}
}
// 【多级联动】联动组件change时清空下级组件
function handleLinkageSelectChange(row, col, config, value) {
if (col.linkageKey) {
getLinkageOptionsAsync(config, value);
let idx = config.keys.findIndex((k) => k === col.key);
let values = {};
for (let i = idx; i < config.keys.length; i++) {
values[config.keys[i]] = '';
}
// 清空后几列的数据
methods.setValues([{ rowKey: row.id, values }]);
}
}
return {
getLinkageOptionsAsync,
getLinkageOptionsSibling,
handleLinkageSelectChange,
handleLinkageBackData,
};
}

View File

@ -0,0 +1,886 @@
import { Ref, watch } from 'vue';
import XEUtils from 'xe-utils';
import { simpleDebounce } from '/@/utils/common/compUtils';
import { JVxeDataProps, JVxeRefs, JVxeTableProps, JVxeTypes } from '../types';
import { getEnhanced } from '../utils/enhancedUtils';
import { VxeTableInstance, VxeTablePrivateMethods } from 'vxe-table';
import { cloneDeep } from 'lodash-es';
import { isArray, isEmpty, isNull, isString } from '/@/utils/is';
import { useLinkage } from './useLinkage';
import { useWebSocket } from './useWebSocket';
import { getPrefix, getJVxeAuths } from '../utils/authUtils';
import { excludeKeywords } from '../componentMap';
import { useColumnsCache } from './useColumnsCache';
export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps, refs: JVxeRefs, instanceRef: Ref) {
let xTableTemp: VxeTableInstance & VxeTablePrivateMethods;
function getXTable() {
if (!xTableTemp) {
// !. 为 typescript 的非空断言
xTableTemp = refs.gridRef.value!.getRefMaps().refTable.value;
}
return xTableTemp;
}
// noinspection JSUnusedGlobalSymbols
const hookMethods = {
getXTable,
addRows,
pushRows,
insertRows,
addOrInsert,
setValues,
getValues,
getTableData,
getNewData,
getNewDataWithId,
getIfRowById,
getNewRowById,
getDeleteData,
getSelectionData,
getSelectedData,
removeRows,
removeRowsById,
removeSelection,
resetScrollTop,
validateTable,
fullValidateTable,
clearSelection,
filterNewRows,
isDisabledRow,
recalcDisableRows,
rowResort,
};
// 多级联动
const linkageMethods = useLinkage(props, data, hookMethods);
// WebSocket 无痕刷新
const socketMethods = useWebSocket(props, data, hookMethods);
// 可显式供外部调用的方法
const publicMethods = {
...hookMethods,
...linkageMethods,
...socketMethods,
};
/** 监听vxe滚动条位置 */
function handleVxeScroll(event) {
let { scroll } = data;
// 记录滚动条的位置
scroll.top = event.scrollTop;
scroll.left = event.scrollLeft;
refs.subPopoverRef.value?.close();
data.scrolling.value = true;
closeScrolling();
}
// 当手动勾选单选时触发的事件
function handleVxeRadioChange(event) {
let row = event.$table.getRadioRecord();
data.selectedRows.value = row ? [row] : [];
handleSelectChange('radio', data.selectedRows.value, event);
}
// 当手动勾选全选时触发的事件
function handleVxeCheckboxAll(event) {
data.selectedRows.value = event.$table.getCheckboxRecords();
handleSelectChange('checkbox-all', data.selectedRows.value, event);
}
// 当手动勾选并且值发生改变时触发的事件
function handleVxeCheckboxChange(event) {
data.selectedRows.value = event.$table.getCheckboxRecords();
handleSelectChange('checkbox', data.selectedRows.value, event);
}
// 行选择change事件
function handleSelectChange(type, selectedRows, $event) {
let action;
if (type === 'radio') {
action = 'selected';
} else if (type === 'checkbox') {
action = selectedRows.includes($event.row) ? 'selected' : 'unselected';
} else {
action = 'selected-all';
}
data.selectedRowIds.value = selectedRows.map((row) => row.id);
trigger('selectRowChange', {
type: type,
action: action,
$event: $event,
row: $event.row,
selectedRows: data.selectedRows.value,
selectedRowIds: data.selectedRowIds.value,
});
}
// 点击单元格时触发的事件
function handleCellClick(event) {
let { row, column, $event, $table } = event;
// 点击了可编辑的
if (column.editRender) {
refs.subPopoverRef.value?.close();
return;
}
// 显示详细信息
if (column.params?.showDetails) {
refs.detailsModalRef.value?.open(event);
} else if (refs.subPopoverRef.value) {
refs.subPopoverRef.value.toggle(event);
} else if (props.clickSelectRow) {
let className = $event.target.className || '';
className = isString(className) ? className : className.toString();
// 点击的是expand不做处理
if (className.includes('vxe-table--expand-btn')) {
return;
}
// 点击的是checkbox不做处理
if (className.includes('vxe-checkbox--icon') || className.includes('vxe-cell--checkbox')) {
return;
}
// 点击的是radio不做处理
if (className.includes('vxe-radio--icon') || className.includes('vxe-cell--radio')) {
return;
}
if (props.rowSelectionType === 'radio') {
$table.setRadioRow(row);
handleVxeRadioChange(event);
} else {
$table.toggleCheckboxRow(row);
handleVxeCheckboxChange(event);
}
}
}
// 单元格被激活编辑时会触发该事件
function handleEditActived({ column }) {
// 执行增强
getEnhanced(column.params.type).aopEvents.editActived!.apply(instanceRef.value, arguments as any);
}
// 单元格编辑状态下被关闭时会触发该事件
function handleEditClosed({ column }) {
// 执行增强
getEnhanced(column.params.type).aopEvents.editClosed!.apply(instanceRef.value, arguments as any);
}
// 返回值决定行是否可选中
function handleCheckMethod({ row }) {
if (props.disabled) {
return false;
}
return !data.disabledRowIds.includes(row.id);
}
// 返回值决定单元格是否可以编辑
function handleActiveMethod({ row, column }) {
let flag = (() => {
if (props.disabled) {
return false;
}
if (data.disabledRowIds.includes(row.id)) {
return false;
}
if (column.params?.disabled) {
return false;
}
// 执行增强
return getEnhanced(column.params.type).aopEvents.activeMethod!.apply(instanceRef.value, arguments as any) ?? true;
})();
if (!flag) {
getXTable().clearActived();
}
return flag;
}
/**
* 判断是否是禁用行
* @param row 行数据
* @param rowIndex 行号
* @param force 是否强制判断
*/
function isDisabledRow(row, rowIndex: number | boolean = -1, force = true) {
if(typeof rowIndex === 'boolean'){
force = rowIndex;
rowIndex = -1;
}
if (!force) {
return !data.disabledRowIds.includes(row.id);
}
if (props.disabledRows == null || isEmpty(props.disabledRows)) {
return false;
}
let disabled: boolean = false;
let keys: string[] = Object.keys(props.disabledRows);
for (const key of keys) {
// 判断是否有该属性
if (row.hasOwnProperty(key)) {
let value = row[key];
let temp: any = props.disabledRows![key];
// 禁用规则可以是一个函数
if (typeof temp === 'function') {
disabled = temp(value, row, rowIndex);
} else if (isArray(temp)) {
// 禁用规则可以是一个数组
disabled = temp.includes(value);
} else {
// 禁用规则可以是一个具体值
disabled = temp === value;
}
if (disabled) {
break;
}
}
}
return disabled;
}
// 重新计算禁用行
function recalcDisableRows() {
let xTable = getXTable();
data.disabledRowIds = [];
const { tableFullData } = xTable.internalData;
tableFullData.forEach((row, rowIndex) => {
// 判断是否是禁用行
if (isDisabledRow(row, rowIndex)) {
data.disabledRowIds.push(row.id);
}
});
xTable.updateData();
}
// 监听 disabledRows更改时重新计算禁用行
watch(
() => props.disabledRows,
() => recalcDisableRows()
);
// 返回值决定是否允许展开、收起行
function handleExpandToggleMethod({ expanded }) {
return !(expanded && props.disabled);
}
// 设置 data.scrolling 防抖模式
const closeScrolling = simpleDebounce(function () {
data.scrolling.value = false;
}, 100);
/** 表尾数据处理方法,用于显示统计信息 */
function handleFooterMethod({ columns, data: $data }) {
const { statistics } = data;
let footers: any[] = [];
if (statistics.has) {
if (statistics.sum.length > 0) {
footers.push(
getFooterStatisticsMap({
columns: columns,
title: '合计',
checks: statistics.sum,
method: (column) => XEUtils.sum($data, column.property),
})
);
}
if (statistics.average.length > 0) {
footers.push(
getFooterStatisticsMap({
columns: columns,
title: '平均',
checks: statistics.average,
method: (column) => XEUtils.mean($data, column.property),
})
);
}
}
return footers;
}
/** 获取底部统计Map */
function getFooterStatisticsMap({ columns, title, checks, method }) {
return columns.map((column, columnIndex) => {
if (columnIndex === 0) {
return title;
}
if (checks.includes(column.property)) {
return method(column, columnIndex);
}
return null;
});
}
// 创建新行,自动添加默认值
function createRow(record: Recordable = {}) {
let xTable = getXTable();
// 添加默认值
xTable.internalData.tableFullColumn.forEach((column) => {
let col = column.params;
// 不能被注册的列不获取增强
if (col && !excludeKeywords.includes(col.type)) {
if (col.key && (record[col.key] == null || record[col.key] === '')) {
// 设置默认值
let createValue = getEnhanced(col.type).createValue;
let defaultValue = col.defaultValue ?? '';
let ctx = { context: { row: record, column, $table: xTable } };
record[col.key] = createValue(defaultValue, ctx);
}
// 处理联动列
if (col.type === JVxeTypes.select && data.innerLinkageConfig.size > 0) {
// 判断当前列是否是联动列
if (data.innerLinkageConfig.has(col.key)) {
let configItem = data.innerLinkageConfig.get(col.key);
linkageMethods.getLinkageOptionsAsync(configItem, '');
}
}
} else if (col?.type === JVxeTypes.hidden) {
record[col.key] = col.defaultValue ?? '';
}
});
return record;
}
async function addOrInsert(rows: Recordable | Recordable[] = {}, index, triggerName, options?: IAddRowsOptions) {
let xTable = getXTable();
let records;
if (isArray(rows)) {
records = rows;
} else {
records = [rows];
}
// 遍历添加默认值
records.forEach((record) => createRow(record));
let setActive = options?.setActive ?? props.addSetActive ?? true;
let result = await pushRows(records, { index: index, setActive });
// 遍历插入的行
// online js增强时以传过来值为准不再赋默认值
if (!(options?.isOnlineJS ?? false)) {
if (triggerName != null) {
for (let i = 0; i < result.rows.length; i++) {
let row = result.rows[i];
trigger(triggerName, {
row: row,
rows: result.rows,
insertIndex: index,
$table: xTable,
target: instanceRef.value,
isModalData: options?.isModalData
});
}
}
}
return result;
}
// 新增、插入一行时的可选参数
interface IAddRowsOptions {
// 是否是 onlineJS增强 触发的
isOnlineJS?: boolean;
// 是否激活编辑状态
setActive?: boolean;
//是否需要触发change事件
emitChange?:boolean
// 是否是modal弹窗添加的数据
isModalData?:boolean
}
/**
* 添加一行或多行
*
* @param rows
* @param options 参数
* @return
*/
async function addRows(rows: Recordable | Recordable[] = {}, options?: IAddRowsOptions) {
//update-begin-author:taoyan date:2022-8-12 for: VUEN-1892【online子表弹框】有主从关联js时子表弹框修改了数据主表字段未修改
let result = await addOrInsert(rows, -1, 'added', options);
if(options && options!.emitChange==true){
trigger('valueChange', {column: 'all', row: result.row})
}
// update-begin--author:liaozhiyang---date:20240607---for【TV360X-279】行编辑添加新字段滚动对应位置
let xTable = getXTable();
setTimeout(() => {
xTable.scrollToRow(result.row);
}, 0);
// update-end--author:liaozhiyang---date:20240607---for【TV360X-279】行编辑添加新字段滚动对应位置
return result;
//update-end-author:taoyan date:2022-8-12 for: VUEN-1892【online子表弹框】有主从关联js时子表弹框修改了数据主表字段未修改
}
/**
* 添加一行或多行临时数据,不会填充默认值,传什么就添加进去什么
* @param rows
* @param options 选项
* @param options.setActive 是否激活最后一行的编辑模式
*/
async function pushRows(rows: Recordable | Recordable[] = {}, options = { setActive: false, index: -1 }) {
let xTable = getXTable();
let { setActive, index } = options;
index = index === -1 ? index : xTable.internalData.tableFullData[index];
// 插入行
let result = await xTable.insertAt(rows, index);
if (setActive) {
// 激活最后一行的编辑模式
xTable.setActiveRow(result.rows[result.rows.length - 1]);
}
await recalcSortNumber();
return result;
}
/**
* 插入一行或多行临时数据
*
* @param rows
* @param index 添加下标,数字,必填
* @param options 参数
* @return
*/
function insertRows(rows: Recordable | Recordable[] = {}, index: number, options?: IAddRowsOptions) {
if (index < 0) {
console.warn(`【JVxeTable】insertRowsindex必须传递数字且大于-1`);
return;
}
return addOrInsert(rows, index, 'inserted', options);
}
/** 获取表格表单里的值 */
function getValues(callback, rowIds) {
let tableData = getTableData({ rowIds: rowIds });
callback('', tableData);
}
/** 获取表格数据 */
function getTableData(options: any = {}) {
let { rowIds } = options;
let tableData;
// 仅查询指定id的行
if (isArray(rowIds) && rowIds.length > 0) {
tableData = [];
rowIds.forEach((rowId) => {
let { row } = getIfRowById(rowId);
if (row) {
tableData.push(row);
}
});
} else {
// 查询所有行
tableData = getXTable().getTableData().fullData;
}
return filterNewRows(tableData, false);
}
/** 仅获取新增的数据 */
function getNewData() {
let newData = getNewDataWithId();
newData.forEach((row) => delete row.id);
return newData;
}
/** 仅获取新增的数据,带有id */
function getNewDataWithId() {
let xTable = getXTable();
return cloneDeep(xTable.getInsertRecords());
}
/** 根据ID获取行新增的行也能查出来 */
function getIfRowById(id) {
let xTable = getXTable();
let row = xTable.getRowById(id),
isNew = false;
if (!row) {
row = getNewRowById(id);
if (!row) {
console.warn(`JVxeTable.getIfRowById没有找到id为"${id}"的行`);
return { row: null };
}
isNew = true;
}
return { row, isNew };
}
/** 通过临时ID获取新增的行 */
function getNewRowById(id) {
let records = getXTable().getInsertRecords();
for (let record of records) {
if (record.id === id) {
return record;
}
}
return null;
}
/**
* 过滤添加的行
* @param rows 要筛选的行数据
* @param remove true = 删除新增false=只删除id
* @param handler function
*/
function filterNewRows(rows, remove = true, handler?: Fn) {
let insertRecords = getXTable().getInsertRecords();
let records: Recordable[] = [];
for (let row of rows) {
let item = cloneDeep(row);
if (insertRecords.includes(row)) {
handler ? handler({ item, row, insertRecords }) : null;
if (remove) {
continue;
}
delete item.id;
}
records.push(item);
}
return records;
}
/**
* 重置滚动条Top位置
* @param top 新top位置留空则滚动到上次记录的位置用于解决切换tab选项卡时导致白屏以及自动将滚动条滚动到顶部的问题
*/
function resetScrollTop(top?) {
let xTable = getXTable();
xTable.scrollTo(null, top == null || top === '' ? data.scroll.top : top);
}
/** 校验table失败返回errMap成功返回null */
async function validateTable(rows?) {
let xTable = getXTable();
const errMap = await xTable.validate(rows ?? true).catch((errMap) => errMap);
return errMap ? errMap : null;
}
/** 完整校验 */
async function fullValidateTable(rows?) {
let xTable = getXTable();
const errMap = await xTable.fullValidate(rows ?? true).catch((errMap) => errMap);
return errMap ? errMap : null;
}
type setValuesParam = { rowKey: string; values: Recordable };
/**
* 设置某行某列的值
*
* @param values
* @return 返回受影响的单元格数量
*/
function setValues(values: setValuesParam[]): number {
if (!isArray(values)) {
console.warn(`[JVxeTable] setValues 必须传递数组`);
return 0;
}
let xTable = getXTable();
let count = 0;
values.forEach((item) => {
let { rowKey, values: record } = item;
let { row } = getIfRowById(rowKey);
if (!row) {
return;
}
Object.keys(record).forEach((colKey) => {
let column = xTable.getColumnByField(colKey);
if (column) {
let oldValue = row[colKey];
let newValue = record[colKey];
if (newValue !== oldValue) {
row[colKey] = newValue;
// 触发 valueChange 事件
trigger('valueChange', {
type: column.params.type,
value: newValue,
oldValue: oldValue,
col: column.params,
column: column,
isSetValues: true,
row: {...row}
});
count++;
}
} else {
console.warn(`[JVxeTable] setValues 没有找到key为"${colKey}"的列`);
}
});
});
if (count > 0) {
xTable.updateData();
}
return count;
}
/** 清空选择行 */
async function clearSelection() {
const xTable = getXTable();
let event = { $table: xTable, target: instanceRef.value };
if (props.rowSelectionType === JVxeTypes.rowRadio) {
await xTable.clearRadioRow();
handleVxeRadioChange(event);
} else {
await xTable.clearCheckboxRow();
handleVxeCheckboxChange(event);
}
}
/**
* 获取选中数据
* @param isFull 如果 isFull=true 则获取全表已选中的数据
*/
function getSelectionData(isFull?: boolean) {
const xTable = getXTable();
if (props.rowSelectionType === JVxeTypes.rowRadio) {
let row = xTable.getRadioRecord(isFull);
if (isNull(row)) {
return [];
}
return filterNewRows([row], false);
} else {
return filterNewRows(xTable.getCheckboxRecords(isFull), false);
}
}
/** 仅获取被删除的数据(新增又被删除的数据不会被获取到) */
function getDeleteData() {
return filterNewRows(getXTable().getRemoveRecords(), false);
}
/** 删除一行或多行数据 */
async function removeRows(rows, asyncRemove = false) {
// update-begin--author:liaozhiyang---date:20231123---forvxe-table removeRows方法加上异步删除
const xTable = getXTable();
const removeEvent: any = { deleteRows: rows, $table: xTable };
if (asyncRemove) {
const selectedRows = Array.isArray(rows) ? rows : [rows];
const deleteOldRows = filterNewRows(selectedRows);
if (deleteOldRows.length) {
return new Promise((resolve) => {
// 确认删除,只有调用这个方法才会真删除
removeEvent.confirmRemove = async () => {
const insertRecords = xTable.getInsertRecords();
selectedRows.forEach((item) => {
// 删除新添加的数据id
if (insertRecords.includes(item)) {
delete item.id;
}
});
const res = await xTable.remove(rows);
await recalcSortNumber();
resolve(res);
};
trigger('removed', removeEvent);
});
} else {
// 全新的行立马删除,不等待。
const res = await xTable.remove(rows);
removeEvent.confirmRemove = () => {};
trigger('removed', removeEvent);
await recalcSortNumber();
return res;
}
} else {
const res = await xTable.remove(rows);
trigger('removed', removeEvent);
await recalcSortNumber();
return res;
}
// update-end--author:liaozhiyang---date:20231123---forvxe-table removeRows方法加上异步删除
}
/** 根据id删除一行或多行 */
function removeRowsById(rowId) {
let rowIds;
if (isArray(rowId)) {
rowIds = rowId;
} else {
rowIds = [rowId];
}
let rows = rowIds
.map((id) => {
let { row } = getIfRowById(id);
if (!row) {
return;
}
if (row) {
return row;
} else {
console.warn(`【JVxeTable】removeRowsById${id}不存在`);
return null;
}
})
.filter((row) => row != null);
return removeRows(rows);
}
// 删除选中的数据
async function removeSelection() {
let xTable = getXTable();
let res;
if (props.rowSelectionType === JVxeTypes.rowRadio) {
res = await xTable.removeRadioRow();
} else {
res = await xTable.removeCheckboxRow();
}
await clearSelection();
await recalcSortNumber();
return res;
}
/** 重新计算排序字段的数值 */
async function recalcSortNumber(force = false) {
if (props.dragSort || force) {
let xTable = getXTable();
let sortKey = props.sortKey ?? 'orderNum';
let sortBegin = props.sortBegin ?? 0;
xTable.internalData.tableFullData.forEach((data) => (data[sortKey] = sortBegin++));
// update-begin--author:liaozhiyang---date:20231011---for【QQYUN-5133】JVxeTable 行编辑升级
// 4.1.0
//await xTable.updateCache();
// 4.1.1
await xTable.cacheRowMap()
// update-end--author:liaozhiyang---date:20231011---for【QQYUN-5133】JVxeTable 行编辑升级
return await xTable.updateData();
}
}
/**
* 排序表格
* @param oldIndex
* @param newIndex
* @param force 强制排序
*/
async function doSort(oldIndex: number, newIndex: number, force = false) {
if (props.dragSort || force) {
let xTable = getXTable();
let sort = (array) => {
// 存储old数据并删除该项
let row = array.splice(oldIndex, 1)[0];
// 向newIndex处添加old数据
array.splice(newIndex, 0, row);
};
sort(xTable.internalData.tableFullData);
if (xTable.keepSource) {
sort(xTable.internalData.tableSourceData);
}
return await recalcSortNumber(force);
}
}
/** 行重新排序 */
function rowResort(oldIndex: number, newIndex: number) {
return doSort(oldIndex, newIndex, true);
}
// ---------------- begin 权限控制 ----------------
// 加载权限
function loadAuthsMap() {
if (!props.authPre || props.authPre.length == 0) {
data.authsMap.value = null;
} else {
data.authsMap.value = getJVxeAuths(props.authPre);
}
}
/**
* 根据 权限code 获取权限
* @param authCode
*/
function getAuth(authCode) {
if (data.authsMap.value != null && props.authPre) {
let prefix = getPrefix(props.authPre);
return data.authsMap.value.get(prefix + authCode);
}
return null;
}
// 获取列权限
function getColAuth(key: string) {
return getAuth(key);
}
// 判断按钮权限
function hasBtnAuth(key: string) {
return getAuth('btn:' + key)?.isAuth ?? true;
}
// ---------------- end 权限控制 ----------------
/* --- 辅助方法 ---*/
function created() {
loadAuthsMap();
}
// 触发事件
function trigger(name, event: any = {}) {
event.$target = instanceRef.value;
event.$table = getXTable();
//online增强参数兼容
event.target = instanceRef.value;
emit(name, event);
}
/**
* 获取选中的行-和 getSelectionData 区别在于对于新增的行也会返回ID
* 用于onlinePopForm
* @param isFull
*/
function getSelectedData(isFull?: boolean) {
const xTable = getXTable();
let rows:any[] = []
if (props.rowSelectionType === JVxeTypes.rowRadio) {
let row = xTable.getRadioRecord(isFull);
if (isNull(row)) {
return [];
}
rows = [row]
} else {
rows = xTable.getCheckboxRecords(isFull)
}
let records: Recordable[] = [];
for (let row of rows) {
let item = cloneDeep(row);
records.push(item);
}
return records;
}
/**
* 2024-03-21
* liaozhiyang
* VXETable列设置保存缓存字段名
* */
function handleCustom({ type, $grid }) {
const { saveSetting, resetSetting } = useColumnsCache({ cacheColumnsKey: props.cacheColumnsKey });
if (type === 'confirm') {
saveSetting($grid);
} else if (type == 'reset') {
resetSetting($grid);
}
}
return {
methods: {
trigger,
...publicMethods,
closeScrolling,
doSort,
recalcSortNumber,
handleVxeScroll,
handleVxeRadioChange,
handleVxeCheckboxAll,
handleVxeCheckboxChange,
handleFooterMethod,
handleCellClick,
handleEditActived,
handleEditClosed,
handleCheckMethod,
handleActiveMethod,
handleExpandToggleMethod,
getColAuth,
hasBtnAuth,
handleCustom,
},
publicMethods,
created,
};
}

View File

@ -0,0 +1,66 @@
import { computed, reactive, h } from 'vue';
import { JVxeTableMethods, JVxeTableProps } from '/@/components/jeecg/JVxeTable/src/types';
import { isEmpty } from '/@/utils/is';
import { Pagination } from 'ant-design-vue';
export function usePagination(props: JVxeTableProps, methods: JVxeTableMethods) {
const innerPagination = reactive({
current: 1,
pageSize: 10,
pageSizeOptions: ['10', '20', '30'],
showTotal: (total, range) => {
return range[0] + '-' + range[1] + ' 共 ' + total + ' 条';
},
showQuickJumper: true,
showSizeChanger: true,
total: 100,
});
const bindProps = computed(() => {
return {
...innerPagination,
...props.pagination,
size: props.size === 'tiny' ? 'small' : '',
};
});
const boxClass = computed(() => {
return {
'j-vxe-pagination': true,
'show-quick-jumper': !!bindProps.value.showQuickJumper,
};
});
function handleChange(current, pageSize) {
innerPagination.current = current;
methods.trigger('pageChange', { current, pageSize });
}
function handleShowSizeChange(current, pageSize) {
innerPagination.pageSize = pageSize;
methods.trigger('pageChange', { current, pageSize });
}
/** 渲染分页器 */
function renderPagination() {
if (props.pagination && !isEmpty(props.pagination)) {
return h(
'div',
{
class: boxClass.value,
},
[
h(Pagination, {
...bindProps.value,
disabled: props.disabled,
onChange: handleChange,
onShowSizeChange: handleShowSizeChange,
}),
]
);
}
return null;
}
return { renderPagination };
}

View File

@ -0,0 +1,61 @@
import { h } from 'vue';
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
import JVxeSubPopover from '../components/JVxeSubPopover.vue';
import JVxeDetailsModal from '../components/JVxeDetailsModal.vue';
import { useToolbar } from '/@/components/jeecg/JVxeTable/src/hooks/useToolbar';
import { usePagination } from '/@/components/jeecg/JVxeTable/src/hooks/usePagination';
export function useRenderComponents(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
// 渲染 toolbar
const { renderToolbar } = useToolbar(props, data, methods, slots);
// 渲染分页器
const { renderPagination } = usePagination(props, methods);
// 渲染 toolbarAfter 插槽
function renderToolbarAfterSlot() {
if (slots['toolbarAfter']) {
return slots['toolbarAfter']();
}
return null;
}
// 渲染点击时弹出的子表
function renderSubPopover() {
if (props.clickRowShowSubForm && slots.subForm) {
return h(
JVxeSubPopover,
{
ref: 'subPopoverRef',
},
{
subForm: slots.subForm,
}
);
}
return null;
}
// 渲染点击时弹出的详细信息
function renderDetailsModal() {
if (props.clickRowShowMainForm && slots.mainForm) {
return h(
JVxeDetailsModal,
{
ref: 'detailsModalRef',
trigger: methods.trigger,
},
{
mainForm: slots.mainForm,
}
);
}
}
return {
renderToolbar,
renderPagination,
renderSubPopover,
renderDetailsModal,
renderToolbarAfterSlot,
};
}

View File

@ -0,0 +1,73 @@
import { h } from 'vue';
import JVxeToolbar from '../components/JVxeToolbar.vue';
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
export function useToolbar(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, $slots) {
/** 渲染工具栏 */
function renderToolbar() {
if (props.toolbar) {
return h(
JVxeToolbar,
{
size: props.size,
disabled: props.disabled,
toolbarConfig: props.toolbarConfig,
disabledRows: props.disabledRows,
hasBtnAuth: methods.hasBtnAuth,
selectedRowIds: data.selectedRowIds.value,
custom: props.custom,
// 新增事件
onAdd: () => {
// update-begin--author:liaozhiyang---date:20240521---for【TV360X-212】online新增字段就出校验提示
setTimeout(() => {
methods.addRows();
}, 0);
// update-end--author:liaozhiyang---date:20240521---for【TV360X-212】online新增字段就出校验提示
},
// 保存事件
onSave: () => methods.trigger('save'),
onRemove() {
const $table = methods.getXTable();
// update-begin--author:liaozhiyang---date:20231018---for【QQYUN-6805】修复asyncRemove字段不生效
// 触发删除事件
if (data.selectedRows.value.length > 0) {
const deleteOldRows = methods.filterNewRows(data.selectedRows.value);
const removeEvent: any = { deleteRows: data.selectedRows.value, $table };
const insertRecords = $table.getInsertRecords();
if (props.asyncRemove && deleteOldRows.length) {
data.selectedRows.value.forEach((item) => {
// 删除新添加的数据id
if (insertRecords.includes(item)) {
delete item.id;
}
});
// 确认删除,只有调用这个方法才会真删除
removeEvent.confirmRemove = () => methods.removeSelection();
} else {
if (props.asyncRemove) {
// asyncRemove删除的只有新增的数据时防止调用confirmRemove报错
removeEvent.confirmRemove = () => {};
}
methods.removeSelection();
}
methods.trigger('removed', removeEvent);
} else {
methods.removeSelection();
}
// update-end--author:liaozhiyang---date:20231018---for【QQYUN-6805】修复asyncRemove字段不生效
},
// 清除选择事件
onClearSelection: () => methods.clearSelection(),
onRegister: ({ xToolbarRef }) => methods.getXTable().connect(xToolbarRef.value),
},
{
toolbarPrefix: $slots.toolbarPrefix,
toolbarSuffix: $slots.toolbarSuffix,
}
);
}
return null;
}
return { renderToolbar };
}

View File

@ -0,0 +1,108 @@
import { VxeTablePropTypes } from 'vxe-table';
import { isArray } from '/@/utils/is';
import { HandleArgs } from './useColumns';
import { replaceProps } from '../utils/enhancedUtils';
export function useValidateRules(args: HandleArgs) {
const { data } = args;
const col = args.col!;
let rules: VxeTablePropTypes.EditRules[] = [];
if (isArray(col.validateRules)) {
for (let rule of col.validateRules) {
let replace = {
message: replaceProps(col, rule.message),
};
if (rule.unique || rule.pattern === 'only') {
// 唯一校验器
rule.validator = uniqueValidator(args);
} else if (rule.pattern) {
// 非空
if (rule.pattern === fooPatterns[0].value) {
rule.required = true;
delete rule.pattern;
} else {
// 兼容Online表单的特殊规则
for (let foo of fooPatterns) {
if (foo.value === rule.pattern) {
rule.pattern = foo.pattern;
break;
}
}
}
} else if (typeof rule.handler === 'function') {
// 自定义函数校验
rule.validator = handlerConvertToValidator;
}
rules.push(Object.assign({}, rule, replace));
}
}
data.innerEditRules[col.key] = rules;
}
/** 唯一校验器 */
function uniqueValidator({ methods }: HandleArgs) {
return function (event) {
const { cellValue, column, rule } = event;
// update-begin--author:liaozhiyang---date:20240522---for【TV360X-299】JVxetable组件中唯一校验过滤掉空字符串
if (cellValue == '') return Promise.resolve();
// update-end--author:liaozhiyang---date:20240522---for【TV360X-299】JVxetable组件中唯一校验过滤掉空字符串
let tableData = methods.getTableData();
let findCount = 0;
for (let rowData of tableData) {
if (rowData[column.params.key] === cellValue) {
if (++findCount >= 2) {
return Promise.reject(new Error(rule.message));
}
}
}
return Promise.resolve();
};
}
/** 旧版handler转为新版Validator */
function handlerConvertToValidator(event) {
const { column, rule } = event;
return new Promise((resolve, reject) => {
rule.handler(event, (flag, msg) => {
let message = rule.message;
if (typeof msg === 'string') {
message = replaceProps(column.params, msg);
}
if (flag == null) {
resolve(message);
} else if (!!flag) {
resolve(message);
} else {
reject(new Error(message));
}
});
});
}
// 兼容 online 的规则
const fooPatterns = [
{ title: '非空', value: '*', pattern: /^.+$/ },
{ title: '6到16位数字', value: 'n6-16', pattern: /^\d{6,16}$/ },
{ title: '6到16位任意字符', value: '*6-16', pattern: /^.{6,16}$/ },
{ title: '6到18位字母', value: 's6-18', pattern: /^[a-z|A-Z]{6,18}$/ },
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1160 对多子表,网址校验不正确
{
title: '网址',
value: 'url',
pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/,
},
//update-end-author:taoyan date:2022-6-1 for: VUEN-1160 对多子表,网址校验不正确
// update-begin--author:liaozhiyang---date:20240527---for【TV360X-466】邮箱跟一对第一校验规则一致
{ title: '电子邮件', value: 'e', pattern: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ },
// update-end--author:liaozhiyang---date:20240527---for【TV360X-466】邮箱跟一对第一校验规则一致
{ title: '手机号码', value: 'm', pattern: /^1[3456789]\d{9}$/ },
{ title: '邮政编码', value: 'p', pattern: /^\d{6}$/ },
{ title: '字母', value: 's', pattern: /^[A-Z|a-z]+$/ },
{ title: '数字', value: 'n', pattern: /^-?\d+(\.?\d+|\d?)$/ },
{ title: '整数', value: 'z', pattern: /^-?\d+$/ },
{
title: '金额',
value: 'money',
pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,5}))$/,
},
];

View File

@ -0,0 +1,236 @@
import { watch, onUnmounted } from 'vue';
import { buildUUID } from '/@/utils/uuid';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
import { isArray } from '/@/utils/is';
import { getToken } from '/@/utils/auth';
// vxe socket
const vs = {
// 页面唯一 id用于标识同一用户不同页面的websocket
pageId: buildUUID(),
// webSocket 对象
ws: null,
// 一些常量
constants: {
// 消息类型
TYPE: 'type',
// 消息数据
DATA: 'data',
// 消息类型:心跳检测
TYPE_HB: 'heart_beat',
// 消息类型更新vxe table数据
TYPE_UVT: 'update_vxe_table',
},
// 心跳检测
heartCheck: {
// 间隔时间,间隔多久发送一次心跳消息
interval: 10000,
// 心跳消息超时时间,心跳消息多久没有回复后重连
timeout: 6000,
timeoutTimer: -1,
clear() {
clearTimeout(this.timeoutTimer);
return this;
},
start() {
vs.sendMessage(vs.constants.TYPE_HB, '');
// 如果超过一定时间还没重置,说明后端主动断开了
this.timeoutTimer = window.setTimeout(() => {
vs.reconnect();
}, this.timeout);
return this;
},
// 心跳消息返回
back() {
this.clear();
window.setTimeout(() => this.start(), this.interval);
},
},
/** 初始化 WebSocket */
initialWebSocket() {
if (this.ws === null) {
const userId = useUserStore().getUserInfo?.id;
const domainURL = useGlobSetting().uploadUrl!;
const domain = domainURL.replace('https://', 'wss://').replace('http://', 'ws://');
const url = `${domain}/vxeSocket/${userId}/${this.pageId}`;
//update-begin-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
let token = (getToken() || '') as string;
this.ws = new WebSocket(url, [token]);
//update-end-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
this.ws.onopen = this.on.open.bind(this);
this.ws.onerror = this.on.error.bind(this);
this.ws.onmessage = this.on.message.bind(this);
this.ws.onclose = this.on.close.bind(this);
}
},
// 发送消息
sendMessage(type, message) {
try {
let ws = this.ws;
if (ws != null && ws.readyState === ws.OPEN) {
ws.send(
JSON.stringify({
type: type,
data: message,
})
);
}
} catch (err: any) {
console.warn('【JVxeWebSocket】发送消息失败(' + err.code + ')');
}
},
/** 绑定全局VXE表格 */
tableMap: new Map(),
/** 添加绑定 */
addBind(map, key, value: VmArgs) {
let binds = map.get(key);
if (isArray(binds)) {
binds.push(value);
} else {
map.set(key, [value]);
}
},
/** 移除绑定 */
removeBind(map, key, value: VmArgs) {
let binds = map.get(key);
if (isArray(binds)) {
for (let i = 0; i < binds.length; i++) {
let bind = binds[i];
if (bind === value) {
binds.splice(i, 1);
break;
}
}
if (binds.length === 0) {
map.delete(key);
}
} else {
map.delete(key);
}
},
// 呼叫绑定的表单
callBind(map, key, callback) {
let binds = map.get(key);
if (isArray(binds)) {
binds.forEach(callback);
}
},
lockReconnect: false,
/** 尝试重连 */
reconnect() {
if (this.lockReconnect) return;
this.lockReconnect = true;
setTimeout(() => {
if (this.ws && this.ws.close) {
this.ws.close();
}
this.ws = null;
console.info('【JVxeWebSocket】尝试重连...');
this.initialWebSocket();
this.lockReconnect = false;
}, 5000);
},
on: {
open() {
console.info('【JVxeWebSocket】连接成功');
this.heartCheck.start();
},
error(e) {
console.warn('【JVxeWebSocket】连接发生错误:', e);
this.reconnect();
},
message(e) {
// 解析消息
let json;
try {
json = JSON.parse(e.data);
} catch (e: any) {
console.warn('【JVxeWebSocket】收到无法解析的消息:', e.data);
return;
}
let type = json[this.constants.TYPE];
let data = json[this.constants.DATA];
switch (type) {
// 心跳检测
case this.constants.TYPE_HB:
this.heartCheck.back();
break;
// 更新form数据
case this.constants.TYPE_UVT:
this.callBind(this.tableMap, data.socketKey, (args) => this.onVM.onUpdateTable(args, ...data.args));
break;
default:
console.warn('【JVxeWebSocket】收到不识别的消息类型:' + type);
break;
}
},
close(e) {
console.info('【JVxeWebSocket】连接被关闭:', e);
this.reconnect();
},
},
onVM: {
/** 收到更新表格的消息 */
onUpdateTable({ props, data, methods }: VmArgs, row, caseId) {
if (data.caseId !== caseId) {
const tableRow = methods.getIfRowById(row.id).row;
// 局部保更新数据
if (tableRow) {
if (props.reloadEffect) {
data.reloadEffectRowKeysMap[row.id] = true;
}
Object.assign(tableRow, row, { id: tableRow.id });
methods.getXTable().reloadRow(tableRow);
}
}
},
},
} as {
ws: Nullable<WebSocket>;
} & Recordable;
type VmArgs = {
props: JVxeTableProps;
data: JVxeDataProps;
methods: JVxeTableMethods;
};
export function useWebSocket(props: JVxeTableProps, data: JVxeDataProps, methods) {
const args: VmArgs = { props, data, methods };
watch(
() => props.socketReload,
(socketReload: boolean) => {
if (socketReload) {
vs.initialWebSocket();
vs.addBind(vs.tableMap, props.socketKey, args);
} else {
vs.removeBind(vs.tableMap, props.socketKey, args);
}
},
{ immediate: true }
);
/** 发送socket消息更新行 */
function socketSendUpdateRow(row) {
vs.sendMessage(vs.constants.TYPE_UVT, {
socketKey: props.socketKey,
args: [row, data.caseId],
});
}
onUnmounted(() => {
vs.removeBind(vs.tableMap, props.socketKey, args);
});
return {
socketSendUpdateRow,
};
}

View File

@ -0,0 +1,75 @@
import type { App } from 'vue';
// 引入 vxe-table
import 'xe-utils';
import VXETable /*Grid*/ from 'vxe-table';
import VXETablePluginAntd from 'vxe-table-plugin-antd';
import 'vxe-table/lib/style.css';
import JVxeTable from './JVxeTable';
import { getEventPath } from '/@/utils/common/compUtils';
import { registerAllComponent } from './utils/registerUtils';
import { getEnhanced } from './utils/enhancedUtils';
export function registerJVxeTable(app: App) {
// VXETable 全局配置
const VXETableSettings = {
// z-index 起始值
zIndex: 1000,
table: {},
};
// 添加事件拦截器 event.clearActived
// 比如点击了某个组件的弹出层面板之后,此时被激活单元格不应该被自动关闭,通过返回 false 可以阻止默认的行为。
VXETable.interceptor.add('event.clearActived', preventClosingPopUp);
VXETable.interceptor.add('event.clearEdit', preventClosingPopUp);
// 注册插件
VXETable.use(VXETablePluginAntd);
// 注册自定义组件
registerAllComponent();
// 执行注册方法
app.use(VXETable, VXETableSettings);
app.component('JVxeTable', JVxeTable);
}
/**
* 阻止行编辑中关闭弹窗
* @param params
*/
function preventClosingPopUp(this: any, params) {
// 获取组件增强
let col = params.column.params;
let { $event } = params;
const interceptor = getEnhanced(col.type).interceptor;
// 执行增强
let flag = interceptor['event.clearActived']?.call(this, ...arguments);
if (flag === false) {
return false;
}
let path = getEventPath($event);
for (let p of path) {
let className: any = p.className || '';
className = typeof className === 'string' ? className : className.toString();
/* --- 特殊处理以下组件,点击以下标签时不清空编辑状态 --- */
// 点击的标签是JInputPop
if (className.includes('j-input-pop')) {
return false;
}
// 点击的标签是JPopup的弹出层、部门选择、用户选择
if (className.includes('j-popup-modal') || className.includes('j-depart-select-modal') || className.includes('j-user-select-modal')) {
return false;
}
// 点击的是日期选择器
if (className.includes('j-vxe-date-picker')) {
return false;
}
// 执行增强
let flag = interceptor['event.clearActived.className']?.call(this, className, ...arguments);
if (flag === false) {
return false;
}
}
}

View File

@ -0,0 +1,102 @@
@import 'vxe.const';
@import 'vxe.dark';
.@{prefix-cls} {
// 编辑按钮样式
.vxe-cell--edit-icon {
border-color: #606266;
}
.sort--active {
border-color: @primary-color;
}
// toolbar 样式
&-toolbar {
&-collapsed {
[data-collapse] {
display: none;
}
}
&-button.div .ant-btn {
margin-right: 8px;
}
}
// 分页器
.j-vxe-pagination {
margin-top: 8px;
text-align: right;
.ant-pagination-options-size-changer.ant-select {
margin-right: 0;
}
&.show-quick-jumper {
.ant-pagination-options-size-changer.ant-select {
margin-right: 8px;
}
}
}
// 更改 header 底色
.vxe-table.border--default .vxe-table--header-wrapper,
.vxe-table.border--full .vxe-table--header-wrapper,
.vxe-table.border--outer .vxe-table--header-wrapper {
//background-color: #FFFFFF;
}
// 更改 tooltip 校验失败的颜色
.vxe-table--tooltip-wrapper.vxe-table--valid-error {
background-color: #f5222d !important;
}
// 更改 输入框 校验失败的颜色
.col--valid-error > .vxe-cell > .ant-input,
.col--valid-error > .vxe-cell > .ant-select .ant-input,
.col--valid-error > .vxe-cell > .ant-select .ant-select-selection,
.col--valid-error > .vxe-cell > .ant-input-number,
.col--valid-error > .vxe-cell > .ant-cascader-picker .ant-cascader-input,
.col--valid-error > .vxe-cell > .ant-calendar-picker .ant-calendar-picker-input,
.col--valid-error > .vxe-tree-cell > .ant-input,
.col--valid-error > .vxe-tree-cell > .ant-select .ant-input,
.col--valid-error > .vxe-tree-cell > .ant-select .ant-select-selection,
.col--valid-error > .vxe-tree-cell > .ant-input-number,
.col--valid-error > .vxe-tree-cell > .ant-cascader-picker .ant-cascader-input,
.col--valid-error > .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input {
border-color: #f5222d !important;
}
.vxe-body--row.sortable-ghost,
.vxe-body--row.sortable-chosen {
background-color: #dfecfb;
}
// ----------- 【VUEN-1691】默认隐藏滚动条鼠标放上去才显示 -------------------------------------------
.vxe-table {
//.vxe-table--footer-wrapper.body--wrapper,
.vxe-table--body-wrapper.body--wrapper {
overflow-x: hidden;
}
&:hover {
//.vxe-table--footer-wrapper.body--wrapper,
.vxe-table--body-wrapper.body--wrapper {
overflow-x: auto;
}
}
}
// ----------- 【VUEN-1691】默认隐藏滚动条鼠标放上去才显示 -------------------------------------------
// 调整展开/收起图标样式
.vxe-table--render-default .vxe-table--expanded .vxe-table--expand-btn {
width: 17px;
height: 17px;
}
/*【美化表单】行编辑table的title字体改小一号*/
.vxe-header--column.col--ellipsis>.vxe-cell .vxe-cell--title{
font-size: 13px;
}
}

View File

@ -0,0 +1,44 @@
.j-vxe-reload-effect-box {
&,
.j-vxe-reload-effect-span {
display: inline;
height: 100%;
position: relative;
}
.j-vxe-reload-effect-span {
&.layer-top {
display: inline-block;
width: 100%;
position: absolute;
z-index: 2;
background-color: white;
transform-origin: 0 0;
animation: reload-effect 1.5s forwards;
}
&.layer-bottom {
z-index: 1;
}
}
// 定义动画
@keyframes reload-effect {
0% {
opacity: 1;
transform: rotateX(0);
}
10% {
opacity: 1;
}
90% {
opacity: 0;
}
100% {
opacity: 0;
transform: rotateX(180deg);
}
}
}

View File

@ -0,0 +1,2 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-j-vxe-table';

View File

@ -0,0 +1,124 @@
@import 'vxe.const';
// update-begin--author:liaozhiyang---date:20240313---for【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
html[data-theme='dark'] {
--vxe-table-body-background-color: #151515;
--vxe-table-footer-background-color: #151515;
--vxe-table-border-color: #606060;
--vxe-table-popup-border-color:#606060;
--vxe-table-row-hover-background-color:#1e1e1e;
--vxe-input-border-color: #606266;
}
// update-end--author:liaozhiyang---date:20240313---for【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
[data-theme='dark'] .@{prefix-cls} {
@fontColor: #c9d1d9;
@bgColor: #151515;
@borderColor: #606060;
.vxe-cell--item,
.vxe-cell--title,
.vxe-cell,
.vxe-body--expanded-cell {
color: @fontColor;
}
.vxe-toolbar {
// update-begin--author:liaozhiyang---date:20240313---for【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
background-color: #1f1f1f;
// update-end--author:liaozhiyang---date:20240313---for【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
}
.vxe-table--render-default .vxe-table--body-wrapper,
.vxe-table--render-default .vxe-table--footer-wrapper {
background-color: @bgColor;
}
// 外边框
.vxe-table--render-default .vxe-table--border-line {
border-color: @borderColor;
}
// header 下边框
.vxe-table .vxe-table--header-wrapper .vxe-table--header-border-line {
border-bottom-color: @borderColor;
}
// footer 上边框
.vxe-table--render-default .vxe-table--footer-wrapper {
border-top-color: @borderColor;
}
// 展开行 边框
.vxe-table--render-default .vxe-body--expanded-column {
border-bottom-color: @borderColor;
}
// 行斑马纹
.vxe-table--render-default .vxe-body--row.row--stripe {
background-color: #1e1e1e;
}
// 行hover
.vxe-table--render-default .vxe-body--row.row--hover {
background-color: #262626;
}
// 选中行
.vxe-table--render-default .vxe-body--row.row--checked {
background-color: #44403a;
&.row--hover {
background-color: #59524b;
}
}
.vxe-table--render-default.border--default .vxe-table--header-wrapper,
.vxe-table--render-default.border--full .vxe-table--header-wrapper,
.vxe-table--render-default.border--outer .vxe-table--header-wrapper {
background-color: #1d1d1d;
}
.vxe-table--render-default.border--default .vxe-body--column,
.vxe-table--render-default.border--default .vxe-footer--column,
.vxe-table--render-default.border--default .vxe-header--column,
.vxe-table--render-default.border--inner .vxe-body--column,
.vxe-table--render-default.border--inner .vxe-footer--column,
.vxe-table--render-default.border--inner .vxe-header--column {
background-image: linear-gradient(#1d1d1d, #1d1d1d);
}
// 列宽拖动
.vxe-header--column .vxe-resizable.is--line:before {
background-color: #505050;
}
// checkbox
.vxe-custom--option .vxe-checkbox--icon:before,
.vxe-export--panel-column-option .vxe-checkbox--icon:before,
.vxe-table--filter-option .vxe-checkbox--icon:before,
.vxe-table--render-default .vxe-cell--checkbox .vxe-checkbox--icon:before {
background-color: @bgColor;
border-color: @borderColor;
}
.vxe-toolbar .vxe-custom--option-wrapper {
background-color: @bgColor;
}
.vxe-button {
background-color: @bgColor;
border-color: @borderColor;
}
.vxe-button.type--button:not(.is--disabled):active {
background-color: @bgColor;
}
.vxe-toolbar .vxe-custom--wrapper.is--active > .vxe-button {
background-color: @bgColor;
}
.vxe-toolbar .vxe-custom--option-wrapper .vxe-custom--footer button {
color: @fontColor;
}
}

View File

@ -0,0 +1,87 @@
import { ComponentInternalInstance, ExtractPropTypes } from 'vue';
import { useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
export namespace JVxeComponent {
export type Props = ExtractPropTypes<ReturnType<typeof useJVxeCompProps>>;
interface EnhancedCtx {
props?: JVxeComponent.Props;
context?: any;
}
/** 组件增强类型 */
export interface Enhanced {
// 注册参数详见https://xuliangzhan_admin.gitee.io/vxe-table/v4/table/renderer/edit
installOptions: {
// 自动聚焦的 class 类名
autofocus?: string;
} & Recordable;
// 事件拦截器(用于兼容)
interceptor: {
// 已实现event.clearActived
// 说明:比如点击了某个组件的弹出层面板之后,此时被激活单元格不应该被自动关闭,通过返回 false 可以阻止默认的行为。
'event.clearActived'?: (params, event, target, ctx?: EnhancedCtx) => boolean;
// 自定义event.clearActived.className
// 说明比原生的多了一个参数className用于判断点击的元素的样式名递归到顶层
'event.clearActived.className'?: (params, event, target, ctx?: EnhancedCtx) => boolean;
};
// 【功能开关】
switches: {
// 是否使用 editRender 模式(仅当前组件,并非全局)
// 如果设为true则表头上方会出现一个可编辑的图标
editRender?: boolean;
// false = 组件触发后可视true = 组件一直可视
visible?: boolean;
};
// 【切面增强】切面事件处理,一般在某些方法执行后同步执行
aopEvents: {
// 单元格被激活编辑时会触发该事件
editActived?: (this: ComponentInternalInstance, ...args) => any;
// 单元格编辑状态下被关闭时会触发该事件
editClosed?: (this: ComponentInternalInstance, ...args) => any;
// 返回值决定单元格是否可以编辑
activeMethod?: (this: ComponentInternalInstance, ...args) => boolean;
};
// 【翻译增强】可以实现例如select组件保存的value但是span模式下需要显示成text
translate: {
// 是否启用翻译
enabled?: boolean;
/**
* 【翻译处理方法】如果handler留空则使用默认的翻译方法
*
* @param value 需要翻译的值
* @returns{*} 返回翻译后的数据
*/
handler?: (value, ctx?: EnhancedCtx) => any;
};
/**
* 【获取值增强】组件抛出的值
*
* @param value 保存到数据库里的值
* @returns{*} 返回处理后的值
*/
getValue: (value, ctx?: EnhancedCtx) => any;
/**
* 【设置值增强】设置给组件的值
*
* @param value 组件触发的值
* @returns{*} 返回处理后的值
*/
setValue: (value, ctx?: EnhancedCtx) => any;
/**
* 【新增行增强】在用户点击新增时触发的事件,返回新行的默认值
*
* @param defaultValue 默认值
* @param row 行数据
* @param column 列配置,.params 是用户配置的参数
* @param $table vxe 实例
* @param renderOptions 渲染选项
* @param params 可以在这里获取 $table
*
* @returns 返回新值
*/
createValue: (defaultValue: any, ctx?: EnhancedCtx) => any;
}
export type EnhancedPartial = Partial<Enhanced>;
}

View File

@ -0,0 +1,60 @@
/** 组件类型 */
export enum JVxeTypes {
// 行号列
rowNumber = 'row-number',
// 选择列
rowCheckbox = 'row-checkbox',
// 单选列
rowRadio = 'row-radio',
// 展开列
rowExpand = 'row-expand',
// 上下排序
rowDragSort = 'row-drag-sort',
input = 'input',
inputNumber = 'input-number',
textarea = 'textarea',
select = 'select',
date = 'date',
datetime = 'datetime',
time = 'time',
checkbox = 'checkbox',
upload = 'upload',
// 下拉搜索
selectSearch = 'select-search',
// 下拉多选
selectMultiple = 'select-multiple',
// 进度条
progress = 'progress',
//部门选择
departSelect = 'depart-select',
//用户选择
userSelect = 'user-select',
// 拖轮Tags暂无用
tags = 'tags', // TODO 待实现
slot = 'slot',
normal = 'normal',
hidden = 'hidden',
// 以下为自定义组件
popup = 'popup',
selectDictSearch = 'selectDictSearch',
radio = 'radio',
image = 'image',
file = 'file',
// 省市区
pca = 'pca',
}
// 为了防止和 vxe 内置的类型冲突,所以加上一个前缀
// 前缀是自动加的代码中直接用就行JVxeTypes.input
export const JVxeTypePrefix = 'j-';
/** VxeTable 渲染类型 */
export enum JVxeRenderType {
editer = 'editer',
spaner = 'spaner',
default = 'default',
}

View File

@ -0,0 +1,120 @@
import type { Component, Ref, ComputedRef, ExtractPropTypes } from 'vue';
import type { VxeColumnProps } from 'vxe-table/types/column';
import type { JVxeComponent } from './JVxeComponent';
import type { VxeGridInstance, VxeTablePropTypes } from 'vxe-table';
import { JVxeTypes } from './JVxeTypes';
import { vxeProps } from '../vxe.data';
import { useMethods } from '../hooks/useMethods';
import { getJVxeAuths } from '../utils/authUtils';
export type JVxeTableProps = Partial<ExtractPropTypes<ReturnType<typeof vxeProps>>>;
export type JVxeTableMethods = ReturnType<typeof useMethods>['methods'];
export type JVxeVueComponent = {
enhanced?: JVxeComponent.EnhancedPartial;
} & Component;
type statisticsTypes = 'sum' | 'average';
export type JVxeColumn = IJVxeColumn & Recordable;
/**
* JVxe 列配置项
*/
export interface IJVxeColumn extends VxeColumnProps {
type?: any;
// 行唯一标识
key: string;
// 表单预期值的提示信息,可以使用${...}变量替换文本
placeholder?: string;
// 默认值
defaultValue?: any;
// 是否禁用当前列默认false
disabled?: boolean;
// 校验规则 TODO 类型待定义
validateRules?: any;
// 联动下一级的字段key
linkageKey?: string;
// 自定义传入组件的其他属性
props?: Recordable;
allowClear?: boolean; // 允许清除
// 【inputNumber】是否是统计列只有 inputNumber 才能设置统计列。统计列sum 求和average 平均值
statistics?: boolean | [statisticsTypes, statisticsTypes?];
// 【select】
dictCode?: string; // 字典 code
options?: { title?: string; label?: string; text?: string; value: any; disabled?: boolean }[]; // 下拉选项列表
allowInput?: boolean; // 允许输入
allowSearch?: boolean; // 允许搜索
// 【slot】
slotName?: string; // 插槽名
// 【checkbox】
customValue?: [any, any]; // 自定义值
defaultChecked?: boolean; // 默认选中
// 【upload】 upload
btnText?: string; // 上传按钮文字
token?: boolean; // 是否传递 token
responseName?: string; // 返回取值名称
action?: string; // 上传地址
allowRemove?: boolean; // 是否允许删除
allowDownload?: boolean; // 是否允许下载
// 【下拉字典搜索】
dict?: string; // 字典表配置信息:数据库表名,显示字段名,存储字段名
async?: boolean; // 是否同步模式
tipsContent?: string;
// 【popup】
popupCode?: string;
field?: string;
orgFields?: string;
destFields?: string;
}
export interface JVxeRefs {
gridRef: Ref<VxeGridInstance | undefined>;
subPopoverRef: Ref<any>;
detailsModalRef: Ref<any>;
}
export interface JVxeDataProps {
prefixCls: string;
// vxe 实例ID
caseId: string;
// vxe 最终 columns
vxeColumns?: ComputedRef;
// vxe 最终 dataSource
vxeDataSource: Ref<Recordable[]>;
// 记录滚动条位置
scroll: { top: number; left: number };
// 当前是否正在滚动
scrolling: Ref<boolean>;
// vxe 默认配置
defaultVxeProps: object;
// 绑定左侧选择框
selectedRows: Ref<any[]>;
// 绑定左侧选择框已选择的id
selectedRowIds: Ref<string[]>;
disabledRowIds: string[];
// 统计列配置
statistics: {
has: boolean;
sum: string[];
average: string[];
};
// 所有和当前表格相关的授权信息
authsMap: Ref<Nullable<ReturnType<typeof getJVxeAuths>>>;
// 内置 EditRules
innerEditRules: Recordable<VxeTablePropTypes.EditRules[]>;
// 联动下拉选项(用于隔离不同的下拉选项)
// 内部联动配置map
innerLinkageConfig: Map<string, any>;
// 开启了数据刷新效果的行
reloadEffectRowKeysMap: Recordable;
}
export interface JVxeLinkageConfig {
// 联动第一级的 key
key: string;
// 获取数据的方法
requestData: (parent: string) => Promise<any>;
}
export { JVxeTypes };

View File

@ -0,0 +1,50 @@
/* JVxeTable 行编辑 权限 */
import { usePermissionStoreWithOut } from '/@/store/modules/permission';
const permissionStore = usePermissionStoreWithOut();
/**
* JVxe 专用,获取权限
* @param prefix
*/
export function getJVxeAuths(prefix) {
prefix = getPrefix(prefix);
let { authList, allAuthList } = permissionStore;
let authsMap = new Map<string, typeof allAuthList[0]>();
if (!prefix || prefix.length == 0) {
return authsMap;
}
// 将所有vxe用到的权限取出来
for (let auth of allAuthList) {
if (auth.status == '1' && (auth.action || '').startsWith(prefix)) {
authsMap.set(auth.action, { ...auth, isAuth: false });
}
}
// 设置是否已授权
for (let auth of authList) {
let getAuth = authsMap.get(auth.action);
if (getAuth != null) {
getAuth.isAuth = true;
}
}
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
let onlineButtonAuths = permissionStore.getOnlineSubTableAuth(prefix);
if (onlineButtonAuths && onlineButtonAuths.length > 0) {
for (let auth of onlineButtonAuths) {
authsMap.set(prefix + 'btn:' + auth, { action: auth, type: 1, status: 1, isAuth: false });
}
}
//update-end-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
return authsMap;
}
/**
* 获取前缀
* @param prefix
*/
export function getPrefix(prefix: string) {
if (prefix && !prefix.endsWith(':')) {
return prefix + ':';
}
return prefix;
}

View File

@ -0,0 +1,55 @@
import { useDefaultEnhanced } from '../hooks/useJVxeComponent';
import { isFunction, isObject, isString } from '/@/utils/is';
import { JVxeTypes } from '../types';
import { JVxeComponent } from '../types/JVxeComponent';
import { componentMap } from '../componentMap';
// 已注册的组件增强
const enhancedMap = new Map<JVxeTypes, JVxeComponent.Enhanced>();
/**
* 获取某个组件的增强
* @param type JVxeTypes
*/
export function getEnhanced(type: JVxeTypes | string): JVxeComponent.Enhanced {
let $type: JVxeTypes = <JVxeTypes>type;
if (!enhancedMap.has($type)) {
let defaultEnhanced = useDefaultEnhanced();
if (componentMap.has($type)) {
let enhanced = componentMap.get($type)?.enhanced ?? {};
if (isObject(enhanced)) {
Object.keys(defaultEnhanced).forEach((key) => {
let def = defaultEnhanced[key];
if (enhanced.hasOwnProperty(key)) {
// 方法如果存在就不覆盖
if (!isFunction(def) && !isString(def)) {
enhanced[key] = Object.assign({}, def, enhanced[key]);
}
} else {
enhanced[key] = def;
}
});
enhancedMap.set($type, <JVxeComponent.Enhanced>enhanced);
return <JVxeComponent.Enhanced>enhanced;
}
} else {
throw new Error(`[JVxeTable] ${$type} 组件尚未注册,获取增强失败`);
}
enhancedMap.set($type, <JVxeComponent.Enhanced>defaultEnhanced);
}
return <JVxeComponent.Enhanced>enhancedMap.get($type);
}
/** 辅助方法:替换${...}变量 */
export function replaceProps(col, value) {
if (value && typeof value === 'string') {
let text = value;
text = text.replace(/\${title}/g, col.title);
text = text.replace(/\${key}/g, col.key);
text = text.replace(/\${defaultValue}/g, col.defaultValue);
return text;
}
return value;
}

View File

@ -0,0 +1,143 @@
import type { Component } from 'vue';
import { h } from 'vue';
import VXETable from 'vxe-table';
import { definedComponent, addComponent, componentMap, spanEnds, excludeKeywords } from '../componentMap';
import { JVxeRenderType, JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
import { getEnhanced } from './enhancedUtils';
import { isFunction } from '/@/utils/is';
/**
* 判断某个组件是否已注册
* @param type
*/
export function isRegistered(type: JVxeTypes | string) {
if (excludeKeywords.includes(<JVxeTypes>type)) {
return true;
}
return componentMap.has(type);
}
/**
* 注册vxe自定义组件
*
* @param type
* @param component 编辑状态显示的组件
* @param spanComponent 非编辑状态显示的组件,可以为空
*/
export function registerComponent(type: JVxeTypes, component: Component, spanComponent?: Component) {
addComponent(type, component, spanComponent);
registerOneComponent(type);
}
/**
* 异步注册vxe自定义组件
*
* @param type
* @param promise
*/
export async function registerAsyncComponent(type: JVxeTypes, promise: Promise<any>) {
const result = await promise;
if (isFunction(result.installJVxe)) {
result.install((component: Component, spanComponent?: Component) => {
addComponent(type, component, spanComponent);
registerOneComponent(type);
});
} else {
addComponent(type, result.default);
registerOneComponent(type);
}
}
/**
* 2024-03-08
* liaozhiyang
* 异步注册vxe自定义组件
* 【QQYUN-8241】
* @param type
* @param promise
*/
export function registerASyncComponentReal(type: JVxeTypes, component) {
addComponent(type, component);
registerOneComponent(type);
}
/**
* 安装所有vxe组件
*/
export function registerAllComponent() {
definedComponent();
// 遍历所有组件批量注册
const components = [...componentMap.keys()];
components.forEach((type) => {
if (!type.endsWith(spanEnds)) {
registerOneComponent(<JVxeTypes>type);
}
});
}
/**
* 注册单个vxe组件
*
* @param type 组件 type
*/
export function registerOneComponent(type: JVxeTypes) {
const component = componentMap.get(type);
if (component) {
const switches = getEnhanced(type).switches;
if (switches.editRender && !switches.visible) {
createEditRender(type, component);
} else {
createCellRender(type, component);
}
} else {
throw new Error(`【registerOneComponent】"${type}"不存在于componentMap中`);
}
}
/** 注册可编辑组件 */
function createEditRender(type: JVxeTypes, component: Component, spanComponent?: Component) {
// 获取当前组件的增强
const enhanced = getEnhanced(type);
if (!spanComponent) {
if (componentMap.has(type + spanEnds)) {
spanComponent = componentMap.get(type + spanEnds);
} else {
// 默认的 span 组件为 normal
spanComponent = componentMap.get(JVxeTypes.normal);
}
}
// 添加渲染
VXETable.renderer.add(JVxeTypePrefix + type, {
// 可编辑模板
renderEdit: createRender(type, component, JVxeRenderType.editer),
// 显示模板
renderCell: createRender(type, spanComponent, JVxeRenderType.spaner),
// 增强注册
...enhanced.installOptions,
});
}
/** 注册普通组件 */
function createCellRender(type: JVxeTypes, component: Component = <Component>componentMap.get(JVxeTypes.normal)) {
// 获取当前组件的增强
const enhanced = getEnhanced(type);
VXETable.renderer.add(JVxeTypePrefix + type, {
// 默认显示模板
renderDefault: createRender(type, component, JVxeRenderType.default),
// 增强注册
...enhanced.installOptions,
});
}
function createRender(type, component, renderType) {
return function (renderOptions, params) {
return [
h(component, {
type: type,
params: params,
renderOptions: renderOptions,
renderType: renderType,
}),
];
};
}

View File

@ -0,0 +1,21 @@
/**
*
* 根据 tagName 获取父级节点
*
* @param dom 一级dom节点
* @param tagName 标签名,不区分大小写
*/
export function getParentNodeByTagName(dom: HTMLElement, tagName: string = 'body'): HTMLElement | null {
if (tagName === 'body') {
return document.body;
}
if (dom.parentElement) {
if (dom.parentElement.tagName.toLowerCase() === tagName.trim().toLowerCase()) {
return dom.parentElement;
} else {
return getParentNodeByTagName(dom.parentElement, tagName);
}
} else {
return null;
}
}

View File

@ -0,0 +1,118 @@
import { propTypes } from '/@/utils/propTypes';
export const vxeProps = () => ({
rowKey: propTypes.string.def('id'),
// 列信息
columns: {
type: Array,
required: true,
},
// 数据源
dataSource: {
type: Array,
required: true,
},
authPre: {
type: String,
required: false,
default: '',
},
// 是否显示工具栏
toolbar: propTypes.bool.def(false),
// 工具栏配置
toolbarConfig: propTypes.object.def(() => ({
// prefix 前缀suffix 后缀;
slots: ['prefix', 'suffix'],
// add 新增按钮remove 删除按钮clearSelection 清空选择按钮collapse 展开收起
btns: ['add', 'remove', 'clearSelection'],
})),
// 是否显示行号
rowNumber: propTypes.bool.def(false),
// 固定行号位置或者不固定 【QQYUN-8405】
rowNumberFixed: propTypes.oneOf(['left', 'none']).def('left'),
// update-begin--author:liaozhiyang---date:20240509---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
dragSortFixed: propTypes.oneOf(['left', 'none']).def('left'),
rowSelectionFixed: propTypes.oneOf(['left', 'none']).def('left'),
// update-end--author:liaozhiyang---date:20240509---for【issues/1162】JVxeTable列过长出现横向滚动条时无法拖拽排序
// 是否可选择行
rowSelection: propTypes.bool.def(false),
// 选择行类型
rowSelectionType: propTypes.oneOf(['checkbox', 'radio']).def('checkbox'),
// 是否可展开行
rowExpand: propTypes.bool.def(false),
// 展开行配置
expandConfig: propTypes.object.def(() => ({})),
// 页面是否在加载中
loading: propTypes.bool.def(false),
// 表格高度
height: propTypes.oneOfType([propTypes.number, propTypes.string]).def('auto'),
// 最大高度
maxHeight: {
type: Number,
default: () => null,
},
// 要禁用的行
disabledRows: propTypes.object.def(() => ({})),
// 是否禁用全部组件
disabled: propTypes.bool.def(false),
// 是否可拖拽排序(有固定列的情况下无法拖拽排序,仅可上下排序)
dragSort: propTypes.bool.def(false),
// 排序字段保存的Key
sortKey: propTypes.string.def('orderNum'),
// 排序序号开始值,默认为 0
sortBegin: propTypes.number.def(0),
// 大小可选值有medium、small、mini
size: propTypes.oneOf(['medium', 'small', 'mini']).def('medium'),
// 是否显示边框线
bordered: propTypes.bool.def(false),
// 自定义列配置 默认继承 setup.toolbar.custom
custom: propTypes.bool.def(false),
// 分页器参数,设置了即可显示分页器
pagination: propTypes.object.def(() => ({})),
// 点击行时是否显示子表单
clickRowShowSubForm: propTypes.bool.def(false),
// 点击行时是否显示主表单
clickRowShowMainForm: propTypes.bool.def(false),
// 是否点击选中行,优先级最低
clickSelectRow: propTypes.bool.def(false),
// 是否开启 reload 数据效果
reloadEffect: propTypes.bool.def(false),
// 校验规则
editRules: propTypes.object.def(() => ({})),
// 是否异步删除行,如果你要实现异步删除,那么需要把这个选项开启,
// 在remove事件里调用confirmRemove方法才会真正删除除非删除的全是新增的行
asyncRemove: propTypes.bool.def(false),
// 是否一直显示组件如果为false则只有点击的时候才出现组件
// 注:该参数不能动态修改;如果行、列字段多的情况下,会根据机器性能造成不同程度的卡顿。
// TODO 新版vxe-table取消了 visible 参数,导致无法实现该功能
alwaysEdit: propTypes.bool.def(false),
// 联动配置,数组,详情配置见文档
linkageConfig: propTypes.array.def(() => []),
// 是否开启使用 webSocket 无痕刷新
socketReload: propTypes.bool.def(false),
// 相同的socketKey更改时会互相刷新
socketKey: propTypes.string.def('vxe-default'),
// 新增行时切换行的激活状态
addSetActive: propTypes.bool.def(true),
// 是否开启键盘编辑
keyboardEdit: propTypes.bool.def(false),
// update-begin--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
// 横向虚拟滚动配置(不支持展开行)
// 【QQYUN-7676】x滚动条滚动时字典变成了id
scrollX: propTypes.object.def(() => ({ enabled: false })),
// 纵向虚拟滚动配置(不支持展开行)
scrollY: propTypes.object.def(() => ({ enabled: true })),
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
//【QQYUN-8566】缓存列设置的key路由页面内唯一
cacheColumnsKey: propTypes.string.def(''),
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
rowClassName: {
type: [String, Function],
default: null,
},
// 不允许拖拽的行 [{'key':field,'value':value}]
notAllowDrag: propTypes.array.def(() => []),
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
});
export const vxeEmits = ['save', 'added', 'removed', 'inserted', 'dragged', 'selectRowChange', 'pageChange', 'valueChange', 'blur'];

View File

@ -0,0 +1,6 @@
import JVxeTable from './src/JVxeTable';
export type { JVxeComponent } from './src/types/JVxeComponent';
export type { JVxeColumn, JVxeLinkageConfig } from './src/types';
export { JVxeTypes } from './src/types/JVxeTypes';
export type JVxeTableInstance = InstanceType<typeof JVxeTable>;

View File

@ -0,0 +1,109 @@
import type { Ref, ComponentInternalInstance } from 'vue';
import { unref, isRef } from 'vue';
import { isFunction } from '/@/utils/is';
type dispatchEventOptions = {
// JVxeTable 的 props
props;
// 触发的 event 事件对象
$event;
// 行、列
row?;
column?;
// JVxeTable的vue3实例
instance?: ComponentInternalInstance | any;
// 要寻找的className
className: string;
// 重写找到dom后的处理方法
handler?: Fn;
// 是否直接执行click方法而不是模拟click事件
isClick?: boolean;
};
/** 模拟触发事件 */
export function dispatchEvent(options: dispatchEventOptions) {
const { props, $event, row, column, instance, className, handler, isClick } = options;
if ((!$event || !$event.path) && !instance) {
return;
}
// alwaysEdit 下不模拟触发事件,否者会导致触发两次
if (props && props.alwaysEdit) {
return;
}
let getCell = () => {
let paths: HTMLElement[] = [...($event?.path ?? [])];
// 通过 instance 获取 cell dom对象
if (row && column) {
let selector = `table.vxe-table--body tbody tr[rowid='${row.id}'] td[colid='${column.id}']`;
let cellDom = instance!.vnode?.el?.querySelector(selector);
// -update-begin--author:liaozhiyang---date:20230830---for【QQYUN-6390】解决online新增字段警告兼容下
if (!cellDom) {
cellDom = instance!.$el?.querySelector(selector);
}
// -update-begin--author:liaozhiyang---date:20230830---for【QQYUN-6390】解决online新增字段警告兼容下
if (cellDom) {
paths.unshift(cellDom);
}
}
for (const el of paths) {
if (el.classList?.contains('vxe-body--column')) {
return el;
}
}
return null;
};
let cell = getCell();
if (cell) {
window.setTimeout(() => {
let getElement = () => {
let classList = className.split(' ');
if (classList.length > 0) {
const getClassName = (cls: string) => {
if (cls.startsWith('.')) {
return cls.substring(1, cls.length);
}
return cls;
};
let get = (target, className, idx = 0) => {
let elements = target.getElementsByClassName(getClassName(className));
if (elements && elements.length > 0) {
return elements[idx];
}
return null;
};
let element: HTMLElement = get(cell, classList[0]);
for (let i = 1; i < classList.length; i++) {
if (!element) {
break;
}
element = get(element, classList[i]);
}
return element;
}
return null;
};
let element = getElement();
if (element) {
if (isFunction(handler)) {
handler(element);
} else {
// 模拟触发点击事件
if (isClick) {
element.click();
} else {
element.dispatchEvent($event);
}
}
}
}, 10);
} else {
console.warn('【JVxeTable】dispatchEvent 获取 cell 失败');
}
}
/** 绑定 VxeTable 数据 */
export function vModel(value, row, column: Ref<any> | string) {
// @ts-ignore
let property = isRef(column) ? column.value.property : column;
unref(row)[property] = value;
}