feat: 新增支持 schema 模式的描述列表组件
parent
b2011aea91
commit
e519bff27c
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as Description } from './description.vue';
|
||||||
|
export * from './typing';
|
||||||
|
export { useDescription } from './use-description';
|
|
@ -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 组件参数
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue