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

pull/80/head
puhui999 2025-04-23 12:18:26 +08:00
parent a6f25d477b
commit b80b106fdd
5 changed files with 210 additions and 39 deletions

View File

@ -0,0 +1,71 @@
<script lang="tsx">
import type { DescriptionItemSchema, DescriptionProps } 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: 'Description',
props: {
data: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
schema: {
type: Array as PropType<DescriptionItemSchema[]>,
default: () => [],
},
/** 原生 Descriptions 的参数 */
descriptionsProps: {
type: Object as PropType<DescriptionsProps>,
default: () => ({}),
},
},
setup(props: DescriptionProps) {
/** 过滤掉不需要展示的 */
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.descriptionsProps?.bordered}
colon={props.descriptionsProps?.colon}
column={props.descriptionsProps?.column}
extra={props.descriptionsProps?.extra}
layout={props.descriptionsProps?.layout}
size={props.descriptionsProps?.size}
title={props.descriptionsProps?.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 DescriptionProps {
data?: Record<string, any>; // 数据
schema?: DescriptionItemSchema[]; // 描述项配置
descriptionsProps?: DescriptionsProps; // 原生 Descriptions 的参数
}

View File

@ -0,0 +1,70 @@
import type { DescriptionProps } 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: DescriptionProps) {
this.state = { ...options };
}
getState(): DescriptionProps {
return this.state as DescriptionProps;
}
setState(newState: Partial<DescriptionProps>) {
this.state = { ...this.state, ...newState };
}
}
export type ExtendedDescriptionApi = DescriptionApi;
export function useDescription(options: DescriptionProps) {
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>
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 { DICT_TYPE } from '#/utils/dict';
import { h, ref } from 'vue';
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 [Description, descApi] = useDescription({
descriptionsProps: {
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({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
@ -26,6 +64,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
formData.value = data;
descApi.setState({ data });
} finally {
modalApi.lock(false);
}
@ -34,37 +73,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal
title="消息详情"
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions bordered :column="1" size="middle" class="mx-4">
<Descriptions.Item label="发送人">
{{ formData?.templateNickname }}
</Descriptions.Item>
<!-- TODO @芋艿报错 -->
<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 title="消息详情" :show-cancel-button="false" :show-confirm-button="false">
<Description />
</Modal>
</template>