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
	
	 puhui999
						puhui999