feat: 新增支持 schema 模式的描述列表组件

pull/83/head
puhui999 2025-04-25 18:18:20 +08:00
parent b2011aea91
commit e519bff27c
5 changed files with 210 additions and 38 deletions

View File

@ -0,0 +1,71 @@
<script lang="tsx">
import type { DescriptionItemSchema, DescriptionsOptions } from './typing';
import type { DescriptionsProps } from 'ant-design-vue';
import type { PropType } from 'vue';
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
import { defineComponent } from '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: () => ({}),
},
},
setup(props: DescriptionsOptions) {
/** 过滤掉不需要展示的 */
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;
}
return item.field ? 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>
);
},
});
export default Description;
</script>

View File

@ -0,0 +1,3 @@
export { default as Description } from './description.vue';
export * from './typing';
export { useDescription } from './use-description';

View File

@ -0,0 +1,18 @@
import type { DescriptionsProps } from 'ant-design-vue';
import type { CSSProperties, VNode } from 'vue';
export interface DescriptionItemSchema {
label: string | VNode; // 内容的描述
field?: string; // 对应 data 中的字段名
content?: ((data: any) => string | VNode) | string | VNode; // 自定义需要展示的内容,比如说 dict-tag
span?: number; // 包含列的数量
labelStyle?: CSSProperties; // 自定义标签样式
contentStyle?: CSSProperties; // 自定义内容样式
hidden?: ((data: any) => boolean) | boolean; // 是否显示
}
export interface DescriptionsOptions {
data?: Record<string, any>; // 数据
schema?: DescriptionItemSchema[]; // 描述项配置
componentProps?: DescriptionsProps; // antd Descriptions 组件参数
}

View File

@ -0,0 +1,70 @@
import type { DescriptionsOptions } from './typing';
import { defineComponent, h, isReactive, reactive, watch } from 'vue';
import { Description } from './index';
/** 描述列表 api 定义 */
class DescriptionApi {
private state = reactive<Record<string, any>>({});
constructor(options: DescriptionsOptions) {
this.state = { ...options };
}
getState(): DescriptionsOptions {
return this.state as DescriptionsOptions;
}
setState(newState: Partial<DescriptionsOptions>) {
this.state = { ...this.state, ...newState };
}
}
export type ExtendedDescriptionApi = DescriptionApi;
export function useDescription(options: DescriptionsOptions) {
const IS_REACTIVE = isReactive(options);
const api = new DescriptionApi(options);
// 扩展API
const extendedApi: ExtendedDescriptionApi = api as never;
const Desc = defineComponent({
name: 'UseDescription',
inheritAttrs: false,
setup(_, { attrs, slots }) {
// 合并props和attrs到state
api.setState({ ...attrs });
return () =>
h(
Description,
{
...api.getState(),
...attrs,
},
slots,
);
},
});
// 响应式支持
if (IS_REACTIVE) {
watch(
() => options.schema,
(newSchema) => {
api.setState({ schema: newSchema });
},
{ immediate: true, deep: true },
);
watch(
() => options.data,
(newData) => {
api.setState({ data: newData });
},
{ immediate: true, deep: true },
);
}
return [Desc, extendedApi] as const;
}

View File

@ -1,18 +1,56 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { SystemNotifyMessageApi } from '#/api/system/notify/message'; import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { ref } from 'vue'; import { useDescription } from '#/components/description';
import { DictTag } from '#/components/dict-tag';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '#/utils/dict';
import { h, ref } from 'vue';
import { formatDateTime } from '@vben/utils'; import { formatDateTime } from '@vben/utils';
import { Descriptions } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
const formData = ref<SystemNotifyMessageApi.NotifyMessage>(); const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
const [Description, descApi] = useDescription({
componentProps: {
bordered: true,
column: 1,
size: 'middle',
class: 'mx-4',
},
schema: [
{
field: 'templateNickname',
label: '发送人',
},
{
field: 'createTime',
label: '发送时间',
content: (data) => formatDateTime(data?.createTime) as string,
},
{
field: 'templateType',
label: '消息类型',
content: (data) => h(DictTag, { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE, value: data?.templateType }),
},
{
field: 'readStatus',
label: '是否已读',
content: (data) => h(DictTag, { type: DICT_TYPE.INFRA_BOOLEAN_STRING, value: data?.readStatus }),
},
{
field: 'readTime',
label: '阅读时间',
content: (data) => formatDateTime(data?.readTime) as string,
},
{
field: 'templateContent',
label: '消息内容',
},
],
});
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
@ -27,6 +65,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(); modalApi.lock();
try { try {
formData.value = data; formData.value = data;
descApi.setState({ data });
} finally { } finally {
modalApi.lock(false); modalApi.lock(false);
} }
@ -35,36 +74,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal <Modal title="消息详情" :show-cancel-button="false" :show-confirm-button="false">
title="消息详情" <Description />
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions bordered :column="1" size="middle" class="mx-4">
<Descriptions.Item label="发送人">
{{ formData?.templateNickname }}
</Descriptions.Item>
<Descriptions.Item label="发送时间">
{{ formatDateTime(formData?.createTime) }}
</Descriptions.Item>
<Descriptions.Item label="消息类型">
<DictTag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="formData?.templateType"
/>
</Descriptions.Item>
<Descriptions.Item label="是否已读">
<DictTag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="formData?.readStatus"
/>
</Descriptions.Item>
<Descriptions.Item label="阅读时间">
{{ formatDateTime(formData?.readTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="消息内容">
{{ formData?.templateContent }}
</Descriptions.Item>
</Descriptions>
</Modal> </Modal>
</template> </template>