feat: add vxe-table component (#4563)

* chore: wip vxe-table

* feat: add table demo

* chore: follow ci recommendations to adjust details

* chore: add custom-cell demo

* feat: add custom-cell table demo

* feat: add table from demo
This commit is contained in:
Vben
2024-10-04 23:05:28 +08:00
committed by GitHub
parent 46540a7329
commit 4173264805
80 changed files with 2426 additions and 80 deletions

View File

@@ -79,6 +79,7 @@
/* 遮罩颜色 */
--overlay: 0deg 0% 0% / 40%;
--overlay-content: 0deg 0% 0% / 40%;
/* 基本文字大小 */
--font-size-base: 16px;

View File

@@ -79,7 +79,7 @@
/* 遮罩颜色 */
--overlay: 0 0% 0% / 45%;
--overlay-light: 0 0% 95% / 45%;
--overlay-content: 0 0% 95% / 45%;
/* 基本文字大小 */
--font-size-base: 16px;

View File

@@ -3,5 +3,19 @@ import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
entries: [
{
builder: 'mkdist',
input: './src',
loaders: ['vue'],
pattern: ['**/*.vue'],
},
{
builder: 'mkdist',
format: 'esm',
input: './src',
loaders: ['js'],
pattern: ['**/*.ts'],
},
],
});

View File

@@ -0,0 +1,27 @@
<template>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd" transform="translate(0 1)">
<ellipse
cx="32"
cy="33"
fill="hsl(var(--background-deep))"
rx="32"
ry="7"
/>
<g fill-rule="nonzero" stroke="hsl(var(--heavy))">
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="hsl(var(--accent))"
/>
</g>
</g>
</svg>
</template>

View File

@@ -1,4 +1,5 @@
export { default as EmptyIcon } from './components/empty.vue';
export * from './create-icon';
export * from './lucide';
export * from './lucide';
export * from '@iconify/vue';

View File

@@ -1,9 +1,11 @@
/**
* @zh_CN 布局内容高度 css变量
* @en_US Layout content height
*/
/** layout content 组件的高度 */
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
/** layout content 组件的宽度 */
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
/** layout header 组件的高度 */
export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
/** layout footer 组件的高度 */
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
/**
* @zh_CN 默认命名空间

View File

@@ -85,3 +85,11 @@ export function needsScrollbar() {
// 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断
return doc.scrollHeight > window.innerHeight;
}
export function triggerWindowResize(): void {
// 创建一个新的 resize 事件
const resizeEvent = new Event('resize');
// 触发 window 的 resize 事件
window.dispatchEvent(resizeEvent);
}

View File

@@ -1,5 +1,5 @@
export * from './use-content-style';
export * from './use-is-mobile';
export * from './use-layout-style';
export * from './use-namespace';
export * from './use-priority-value';
export * from './use-scroll-lock';

View File

@@ -4,6 +4,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
} from '@vben-core/shared/constants';
import {
getElementVisibleRect,
@@ -15,7 +17,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
/**
* @zh_CN content style
*/
function useContentStyle() {
export function useLayoutContentStyle() {
let resizeObserver: null | ResizeObserver = null;
const contentElement = ref<HTMLDivElement | null>(null);
const visibleDomRect = ref<null | VisibleDomRect>(null);
@@ -40,7 +42,7 @@ function useContentStyle() {
contentHeight.value = `${visibleDomRect.value.height}px`;
contentWidth.value = `${visibleDomRect.value.width}px`;
},
100,
16,
);
onMounted(() => {
@@ -58,4 +60,28 @@ function useContentStyle() {
return { contentElement, overlayStyle, visibleDomRect };
}
export { useContentStyle };
export function useLayoutHeaderStyle() {
const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);
return {
getLayoutHeaderHeight: () => {
return Number.parseInt(`${headerHeight.value}`, 10);
},
setLayoutHeaderHeight: (height: number) => {
headerHeight.value = `${height}px`;
},
};
}
export function useLayoutFooterStyle() {
const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);
return {
getLayoutFooterHeight: () => {
return Number.parseInt(`${footerHeight.value}`, 10);
},
setLayoutFooterHeight: (height: number) => {
footerHeight.value = `${height}px`;
},
};
}

View File

@@ -39,7 +39,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"icpLink": "",
},
"footer": {
"enable": true,
"enable": false,
"fixed": false,
},
"header": {

View File

@@ -39,7 +39,7 @@ const defaultPreferences: Preferences = {
icpLink: '',
},
footer: {
enable: true,
enable: false,
fixed: false,
},
header: {

View File

@@ -28,6 +28,10 @@ function usePreferences() {
return isDarkTheme(preferences.theme.mode);
});
const locale = computed(() => {
return preferences.app.locale;
});
const isMobile = computed(() => {
return appPreferences.value.isMobile;
});
@@ -218,6 +222,7 @@ function usePreferences() {
isSideNav,
keepAlive,
layout,
locale,
preferencesButtonPosition,
sidebarCollapsed,
theme,

View File

@@ -109,7 +109,7 @@ describe('formApi', () => {
});
it('should unmount form and reset state', () => {
formApi.unmounted();
formApi.unmount();
expect(formApi.isMounted).toBe(false);
});

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, toRaw, unref } from 'vue';
import { computed, toRaw, unref, watch } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
import { cn, isFunction } from '@vben-core/shared/utils';
import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
import { COMPONENT_MAP } from '../config';
import { injectFormProps } from '../use-form-context';
@@ -65,6 +65,16 @@ async function handleReset(e: Event) {
form.resetForm();
}
}
watch(
() => collapsed.value,
() => {
const props = unref(rootProps);
if (props.collapseTriggerResize) {
triggerWindowResize();
}
},
);
</script>
<template>
<div

View File

@@ -24,9 +24,11 @@ function getDefaultState(): VbenFormProps {
actionWrapperClass: '',
collapsed: false,
collapsedRows: 1,
collapseTriggerResize: false,
commonConfig: {},
handleReset: undefined,
handleSubmit: undefined,
handleValuesChange: undefined,
layout: 'horizontal',
resetButtonOptions: {},
schema: [],
@@ -249,7 +251,7 @@ export class FormApi {
return rawValues;
}
unmounted() {
unmount() {
// this.state = null;
this.isMounted = false;
this.stateHandler.reset();

View File

@@ -244,6 +244,11 @@ export interface FormRenderProps<
* @default 1
*/
collapsedRows?: number;
/**
* 是否触发resize事件
* @default false
*/
collapseTriggerResize?: boolean;
/**
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
*/
@@ -302,6 +307,10 @@ export interface VbenFormProps<
* 表单提交回调
*/
handleSubmit?: HandleSubmitFn;
/**
* 表单值变化回调
*/
handleValuesChange?: (values: Record<string, any>) => void;
/**
* 重置按钮参数
*/

View File

@@ -24,7 +24,7 @@ export function useVbenForm<
const Form = defineComponent(
(props: VbenFormProps, { attrs, slots }) => {
onBeforeUnmount(() => {
api.unmounted();
api.unmount();
});
return () =>
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);

View File

@@ -1,7 +1,10 @@
<script setup lang="ts">
import type { ExtendedFormApi, VbenFormProps } from './types';
// import { toRaw, watch } from 'vue';
import { useForwardPriorityValues } from '@vben-core/composables';
// import { isFunction } from '@vben-core/shared/utils';
import FormActions from './components/form-actions.vue';
import {
@@ -31,6 +34,18 @@ props.formApi?.mount?.(form);
const handleUpdateCollapsed = (value: boolean) => {
props.formApi?.setState({ collapsed: !!value });
};
// if (isFunction(forward.value.handleValuesChange)) {
// watch(
// () => form.values,
// (val) => {
// forward.value.handleValuesChange?.(toRaw(val));
// },
// {
// deep: true,
// immediate: true,
// },
// );
// }
</script>
<template>

View File

@@ -4,7 +4,7 @@ import type { ContentCompactType } from '@vben-core/typings';
import type { CSSProperties } from 'vue';
import { computed } from 'vue';
import { useContentStyle } from '@vben-core/composables';
import { useLayoutContentStyle } from '@vben-core/composables';
import { Slot } from '@vben-core/shadcn-ui';
interface Props {
@@ -25,7 +25,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {});
const { contentElement, overlayStyle } = useContentStyle();
const { contentElement, overlayStyle } = useLayoutContentStyle();
const style = computed((): CSSProperties => {
const {

View File

@@ -4,7 +4,11 @@ import type { VbenLayoutProps } from './vben-layout';
import type { CSSProperties } from 'vue';
import { computed, ref, watch } from 'vue';
import { SCROLL_FIXED_CLASS } from '@vben-core/composables';
import {
SCROLL_FIXED_CLASS,
useLayoutFooterStyle,
useLayoutHeaderStyle,
} from '@vben-core/composables';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
@@ -74,6 +78,9 @@ const {
y: scrollY,
} = useScroll(document);
const { setLayoutHeaderHeight } = useLayoutHeaderStyle();
const { setLayoutFooterHeight } = useLayoutFooterStyle();
const { y: mouseY } = useMouse({ target: contentRef, type: 'client' });
const {
@@ -356,6 +363,26 @@ watch(
},
);
watch(
[() => headerWrapperHeight.value, () => isFullContent.value],
([height]) => {
setLayoutHeaderHeight(isFullContent.value ? 0 : height);
},
{
immediate: true,
},
);
watch(
() => props.footerHeight,
(height: number) => {
setLayoutFooterHeight(height);
},
{
immediate: true,
},
);
{
const mouseMove = () => {
mouseY.value > headerWrapperHeight.value

View File

@@ -15,13 +15,6 @@ export default defineBuildConfig({
loaders: ['vue'],
pattern: ['**/*.vue'],
},
// {
// builder: 'mkdist',
// format: 'cjs',
// input: './src',
// loaders: ['js'],
// pattern: ['**/*.ts'],
// },
{
builder: 'mkdist',
format: 'esm',

View File

@@ -16,13 +16,6 @@ export default defineBuildConfig({
loaders: ['vue'],
pattern: ['**/*.vue'],
},
// {
// builder: 'mkdist',
// format: 'cjs',
// input: './src',
// loaders: ['js'],
// pattern: ['**/*.ts'],
// },
{
builder: 'mkdist',
format: 'esm',

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, watch } from 'vue';
import { cn } from '@vben-core/shared/utils';
@@ -32,10 +32,13 @@ const {
showRowsPerPage = true,
showTotalText = true,
siblingCount = 1,
size = 'default',
size = 'small',
total = 500,
} = defineProps<Props>();
const emit = defineEmits<{
pageChange: [currentPage: number, pageSize: number];
}>();
const currentPage = defineModel<number>('currentPage', { default: 1 });
const itemPerPage = defineModel<number>('itemPerPage', { default: 20 });
@@ -53,6 +56,13 @@ const options = computed(() => {
function handleUpdateModelValue(value: string) {
itemPerPage.value = Number(value);
}
watch(
[() => itemPerPage.value, () => currentPage.value],
([itemPerPage, currentPage]) => {
emit('pageChange', currentPage, itemPerPage);
},
);
</script>
<template>

View File

@@ -69,7 +69,7 @@ function onTransitionEnd() {
<div
:class="
cn(
'z-100 dark:bg-overlay pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center bg-[hsl(var(--overlay-light))] transition-all duration-500',
'z-100 dark:bg-overlay bg-overlay-content pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
{
'invisible opacity-0': !showSpinner,
},

View File

@@ -63,7 +63,7 @@ function onTransitionEnd() {
<div
:class="
cn(
'flex-center z-100 dark:bg-overlay absolute left-0 top-0 size-full bg-[hsl(var(--overlay-light))] backdrop-blur-sm transition-all duration-500',
'flex-center z-100 bg-overlay-content absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500',
{
'invisible opacity-0': !showSpinner,
},