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> | ||||
| 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({ | ||||
|   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({ | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|  | @ -27,6 +65,7 @@ const [Modal, modalApi] = useVbenModal({ | |||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = data; | ||||
|       descApi.setState({ data }); | ||||
|     } finally { | ||||
|       modalApi.lock(false); | ||||
|     } | ||||
|  | @ -35,36 +74,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> | ||||
|       <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> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 puhui999
						puhui999