refactor: antdv desc comp

This commit is contained in:
xingyu4j
2025-10-21 11:38:02 +08:00
parent ad00cf839c
commit 284c47b721
3 changed files with 248 additions and 147 deletions

View File

@@ -1,82 +1,194 @@
<script lang="tsx">
import type { DescriptionsProps } from 'ant-design-vue';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { PropType } from 'vue';
import type { CSSProperties, PropType, Slots } from 'vue';
import type { DescriptionItemSchema, DescriptionsOptions } from './typing';
import type { DescriptionItemSchema, DescriptionProps } from './typing';
import { defineComponent } from 'vue';
import { computed, defineComponent, ref, toRefs, unref, useAttrs } from 'vue';
import { get } from '@vben/utils';
import { ContentWrap } from '@vben/common-ui';
import { get, isFunction } from '@vben/utils';
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
import { Descriptions } from 'ant-design-vue';
/** 对 Descriptions 进行二次封装 */
const Description = defineComponent({
name: 'Descriptions',
props: {
data: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
schema: {
type: Array as PropType<DescriptionItemSchema[]>,
default: () => [],
},
// Descriptions 原生 props
componentProps: {
type: Object as PropType<DescriptionsProps>,
default: () => ({}),
const props = {
bordered: { default: true, type: Boolean },
column: {
default: () => {
return { lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 };
},
type: [Number, Object],
},
data: { type: Object },
schema: {
default: () => [],
type: Array as PropType<DescriptionItemSchema[]>,
},
size: {
default: 'small',
type: String,
validator: (v: string) =>
['default', 'middle', 'small', undefined].includes(v),
},
title: { default: '', type: String },
useCard: { default: true, type: Boolean },
};
setup(props: DescriptionsOptions) {
// TODO @xingyu每个 field 的 slot 的考虑
// TODO @xingyufrom 5.0extra: () => getSlot(slots, 'extra')
/** 过滤掉不需要展示的 */
const shouldShowItem = (item: DescriptionItemSchema) => {
if (item.hidden === undefined) return true;
return typeof item.hidden === 'function'
? !item.hidden(props.data)
: !item.hidden;
};
/** 渲染内容 */
const renderContent = (item: DescriptionItemSchema) => {
if (item.content) {
return typeof item.content === 'function'
? item.content(props.data)
: item.content;
function getSlot(slots: Slots, slot: string, data?: any) {
if (!slots || !Reflect.has(slots, slot)) {
return null;
}
if (!isFunction(slots[slot])) {
console.error(`${slot} is not a function!`);
return null;
}
const slotFn = slots[slot];
if (!slotFn) return null;
return slotFn({ data });
}
export default defineComponent({
name: 'Description',
props,
setup(props, { slots }) {
const propsRef = ref<null | Partial<DescriptionProps>>(null);
const prefixCls = 'description';
const attrs = useAttrs();
// Custom title component: get title
const getMergeProps = computed(() => {
return {
...props,
...(unref(propsRef) as any),
} as DescriptionProps;
});
const getProps = computed(() => {
const opt = {
...unref(getMergeProps),
title: undefined,
};
return opt as DescriptionProps;
});
const useWrapper = computed(() => !!unref(getMergeProps).title);
const getDescriptionsProps = computed(() => {
return { ...unref(attrs), ...unref(getProps) } as DescriptionsProps;
});
// 防止换行
function renderLabel({
label,
labelMinWidth,
labelStyle,
}: DescriptionItemSchema) {
if (!labelStyle && !labelMinWidth) {
return label;
}
return item.field ? get(props.data, item.field) : null;
};
return () => (
<Descriptions
{...props}
bordered={props.componentProps?.bordered}
colon={props.componentProps?.colon}
column={props.componentProps?.column}
extra={props.componentProps?.extra}
layout={props.componentProps?.layout}
size={props.componentProps?.size}
title={props.componentProps?.title}
>
{props.schema?.filter(shouldShowItem).map((item) => (
<DescriptionsItem
contentStyle={item.contentStyle}
key={item.field || String(item.label)}
label={item.label}
labelStyle={item.labelStyle}
span={item.span}
>
{renderContent(item)}
</DescriptionsItem>
))}
</Descriptions>
);
const labelStyles: CSSProperties = {
...labelStyle,
minWidth: `${labelMinWidth}px `,
};
return <div style={labelStyles}>{label}</div>;
}
function renderItem() {
const { data, schema } = unref(getProps);
return unref(schema)
.map((item) => {
const { contentMinWidth, field, render, show, span } = item;
if (show && isFunction(show) && !show(data)) {
return null;
}
function getContent() {
const _data = unref(getProps)?.data;
if (!_data) {
return null;
}
const getField = get(_data, field);
if (
getField &&
!Object.prototype.hasOwnProperty.call(toRefs(_data), field)
) {
return isFunction(render) ? render!('', _data) : '';
}
return isFunction(render)
? render!(getField, _data)
: (getField ?? '');
}
const width = contentMinWidth;
return (
<Descriptions.Item
key={field}
label={renderLabel(item)}
span={span}
>
{() => {
if (item.slot) {
const slotContent = getSlot(slots, item.slot, data);
return slotContent;
}
if (!contentMinWidth) {
return getContent();
}
const style: CSSProperties = {
minWidth: `${width}px`,
};
return <div style={style}>{getContent()}</div>;
}}
</Descriptions.Item>
);
})
.filter((item) => !!item);
}
function renderDesc() {
return (
<Descriptions
class={`${prefixCls}`}
{...(unref(getDescriptionsProps) as any)}
>
{renderItem()}
</Descriptions>
);
}
function renderCard() {
const content = props.useCard ? renderDesc() : <div>{renderDesc()}</div>;
// Reduce the dom level
if (!props.useCard) {
return content;
}
const { title } = unref(getMergeProps);
const extraSlot = getSlot(slots, 'extra');
return (
<ContentWrap
class="text-base"
headerClass={props.bordered ? 'p-4' : 'border-none p-4'}
>
{{
default: () => content,
title: () => (
<div class="mb-2 flex w-full items-center justify-between text-base">
<div>{title}</div>
{extraSlot && <div>{extraSlot}</div>}
</div>
),
}}
</ContentWrap>
);
}
return () => (unref(useWrapper) ? renderCard() : renderDesc());
},
});
// TODO @xingyufrom 5.0emits: ['register'] 事件
export default Description;
</script>