commit
						e4267573e9
					
				|  | @ -45,6 +45,7 @@ | ||||||
|     "@vben/utils": "workspace:*", |     "@vben/utils": "workspace:*", | ||||||
|     "@vueuse/core": "catalog:", |     "@vueuse/core": "catalog:", | ||||||
|     "ant-design-vue": "catalog:", |     "ant-design-vue": "catalog:", | ||||||
|  |     "vxe-table": "catalog:", | ||||||
|     "cropperjs": "catalog:", |     "cropperjs": "catalog:", | ||||||
|     "crypto-js": "catalog:", |     "crypto-js": "catalog:", | ||||||
|     "dayjs": "catalog:", |     "dayjs": "catalog:", | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import { preferences } from '@vben/preferences'; | ||||||
| import { initStores } from '@vben/stores'; | import { initStores } from '@vben/stores'; | ||||||
| import '@vben/styles'; | import '@vben/styles'; | ||||||
| import '@vben/styles/antd'; | import '@vben/styles/antd'; | ||||||
|  | import 'vxe-table/styles/cssvar.scss'; | ||||||
| 
 | 
 | ||||||
| import { useTitle } from '@vueuse/core'; | import { useTitle } from '@vueuse/core'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { CSSProperties } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { Card } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
|  | defineOptions({ name: 'ContentWrap' }); | ||||||
|  | 
 | ||||||
|  | withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     bodyStyle?: CSSProperties; | ||||||
|  |     title?: string; | ||||||
|  |   }>(), | ||||||
|  |   { | ||||||
|  |     bodyStyle: () => ({ padding: '10px' }), | ||||||
|  |     title: '', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <Card :body-style="bodyStyle" :title="title" class="mb-4"> | ||||||
|  |     <slot></slot> | ||||||
|  |   </Card> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default as ContentWrap } from './content-wrap.vue'; | ||||||
|  | @ -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; | ||||||
|  | } | ||||||
|  | @ -64,7 +64,7 @@ function getDictObj(dictType: string, value: any) { | ||||||
| function getDictOptions( | function getDictOptions( | ||||||
|   dictType: string, |   dictType: string, | ||||||
|   valueType: 'boolean' | 'number' | 'string' = 'string', |   valueType: 'boolean' | 'number' | 'string' = 'string', | ||||||
| ) { | ): any[] { | ||||||
|   const dictStore = useDictStore(); |   const dictStore = useDictStore(); | ||||||
|   const dictOpts = dictStore.getDictOptions(dictType); |   const dictOpts = dictStore.getDictOptions(dictType); | ||||||
|   const dictOptions: DefaultOptionType = []; |   const dictOptions: DefaultOptionType = []; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,223 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; | ||||||
|  | 
 | ||||||
|  | import { ContentWrap } from '#/components/content-wrap'; | ||||||
|  | import { DictTag } from '#/components/dict-tag'; | ||||||
|  | import Demo01ContactForm from './modules/form.vue'; | ||||||
|  | import { Page, useVbenModal } from '@vben/common-ui'; | ||||||
|  | import { Download, Plus, RefreshCw, Search } from '@vben/icons'; | ||||||
|  | import { Button, Form, Input, message, Pagination, RangePicker, Select } from 'ant-design-vue'; | ||||||
|  | import { VxeColumn, VxeTable } from 'vxe-table'; | ||||||
|  | 
 | ||||||
|  | import { deleteDemo01Contact, exportDemo01Contact, getDemo01ContactPage } from '#/api/infra/demo/demo01'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | import { getRangePickerDefaultProps } from '#/utils/date'; | ||||||
|  | import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||||
|  | import { downloadByData } from '#/utils/download'; | ||||||
|  | import { h, onMounted, reactive, ref } from 'vue'; | ||||||
|  | import {cloneDeep, formatDateTime} from '@vben/utils'; | ||||||
|  | 
 | ||||||
|  | const loading = ref(true); // 列表的加载中 | ||||||
|  | const list = ref<Demo01ContactApi.Demo01Contact[]>([]); // 列表的数据 | ||||||
|  | const total = ref(0); // 列表的总页数 | ||||||
|  | const queryParams = reactive({ | ||||||
|  |   pageNo: 1, | ||||||
|  |   pageSize: 10, | ||||||
|  |   name: undefined, | ||||||
|  |   sex: undefined, | ||||||
|  |   createTime: undefined, | ||||||
|  | }); | ||||||
|  | const queryFormRef = ref(); // 搜索的表单 | ||||||
|  | const exportLoading = ref(false); // 导出的加载中 | ||||||
|  | 
 | ||||||
|  | /** 查询列表 */ | ||||||
|  | const getList = async () => { | ||||||
|  |   loading.value = true; | ||||||
|  |   try { | ||||||
|  |     const params = cloneDeep(queryParams) as any | ||||||
|  |     if (params.createTime && Array.isArray(params.createTime)) { | ||||||
|  |       params.createTime = (params.createTime as string[]).join(',') | ||||||
|  |     } | ||||||
|  |     const data = await getDemo01ContactPage(params); | ||||||
|  |     list.value = data.list; | ||||||
|  |     total.value = data.total; | ||||||
|  |   } finally { | ||||||
|  |     loading.value = false; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** 搜索按钮操作 */ | ||||||
|  | const handleQuery = () => { | ||||||
|  |   queryParams.pageNo = 1; | ||||||
|  |   getList(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** 重置按钮操作 */ | ||||||
|  | const resetQuery = () => { | ||||||
|  |   queryFormRef.value.resetFields(); | ||||||
|  |   handleQuery(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const [FormModal, formModalApi] = useVbenModal({ | ||||||
|  |   connectedComponent: Demo01ContactForm, | ||||||
|  |   destroyOnClose: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | /** 创建示例联系人 */ | ||||||
|  | function onCreate() { | ||||||
|  |   formModalApi.setData({}).open(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 编辑示例联系人 */ | ||||||
|  | function onEdit(row: Demo01ContactApi.Demo01Contact) { | ||||||
|  |   formModalApi.setData(row).open(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 删除示例联系人 */ | ||||||
|  | async function onDelete(row: Demo01ContactApi.Demo01Contact) { | ||||||
|  |   const hideLoading = message.loading({ | ||||||
|  |     content: $t('ui.actionMessage.deleting', [row.id]), | ||||||
|  |     duration: 0, | ||||||
|  |     key: 'action_process_msg', | ||||||
|  |   }); | ||||||
|  |   try { | ||||||
|  |     await deleteDemo01Contact(row.id as number); | ||||||
|  |     message.success({ | ||||||
|  |       content: $t('ui.actionMessage.deleteSuccess', [row.id]), | ||||||
|  |       key: 'action_process_msg', | ||||||
|  |     }); | ||||||
|  |     await getList(); | ||||||
|  |   } catch { | ||||||
|  |     hideLoading(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 导出表格 */ | ||||||
|  | async function onExport() { | ||||||
|  |   try { | ||||||
|  |     exportLoading.value = true; | ||||||
|  |     const data = await exportDemo01Contact(queryParams); | ||||||
|  |     downloadByData(data, '示例联系人.xls'); | ||||||
|  |   } finally { | ||||||
|  |     exportLoading.value = false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 初始化 */ | ||||||
|  | onMounted(() => { | ||||||
|  |   getList(); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <Page auto-content-height> | ||||||
|  |     <FormModal @success="getList" /> | ||||||
|  | 
 | ||||||
|  |     <ContentWrap> | ||||||
|  |       <!-- 搜索工作栏 --> | ||||||
|  |       <Form class="-mb-15px" :model="queryParams" ref="queryFormRef" layout="inline"> | ||||||
|  |         <Form.Item label="名字" name="name"> | ||||||
|  |           <Input | ||||||
|  |             v-model:value="queryParams.name" | ||||||
|  |             placeholder="请输入名字" | ||||||
|  |             allow-clear | ||||||
|  |             @press-enter="handleQuery" | ||||||
|  |             class="!w-240px" | ||||||
|  |           /> | ||||||
|  |         </Form.Item> | ||||||
|  |         <Form.Item label="性别" name="sex"> | ||||||
|  |           <Select v-model:value="queryParams.sex" placeholder="请选择性别" allow-clear class="!w-240px"> | ||||||
|  |             <Select.Option | ||||||
|  |               v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')" | ||||||
|  |               :key="dict.value" | ||||||
|  |               :label="dict.label" | ||||||
|  |               :value="dict.value" | ||||||
|  |             /> | ||||||
|  |           </Select> | ||||||
|  |         </Form.Item> | ||||||
|  |         <Form.Item label="创建时间" name="createTime"> | ||||||
|  |           <RangePicker v-model:value="queryParams.createTime" v-bind="getRangePickerDefaultProps()" class="!w-220px" /> | ||||||
|  |         </Form.Item> | ||||||
|  |         <Form.Item> | ||||||
|  |           <Button class="ml-2" @click="handleQuery" :icon="h(Search)">搜索</Button> | ||||||
|  |           <Button class="ml-2" @click="resetQuery" :icon="h(RefreshCw)">重置</Button> | ||||||
|  |           <Button | ||||||
|  |             class="ml-2" | ||||||
|  |             :icon="h(Plus)" | ||||||
|  |             type="primary" | ||||||
|  |             @click="onCreate" | ||||||
|  |             v-access:code="['infra:demo01-contact:create']" | ||||||
|  |           > | ||||||
|  |             {{ $t('ui.actionTitle.create', ['示例联系人']) }} | ||||||
|  |           </Button> | ||||||
|  |           <Button | ||||||
|  |             :icon="h(Download)" | ||||||
|  |             type="primary" | ||||||
|  |             class="ml-2" | ||||||
|  |             :loading="exportLoading" | ||||||
|  |             @click="onExport" | ||||||
|  |             v-access:code="['infra:demo01-contact:export']" | ||||||
|  |           > | ||||||
|  |             {{ $t('ui.actionTitle.export') }} | ||||||
|  |           </Button> | ||||||
|  |         </Form.Item> | ||||||
|  |       </Form> | ||||||
|  |     </ContentWrap> | ||||||
|  | 
 | ||||||
|  |     <!-- 列表 --> | ||||||
|  |     <ContentWrap> | ||||||
|  |       <VxeTable :data="list" show-overflow :loading="loading"> | ||||||
|  |         <VxeColumn field="id" title="编号" align="center" /> | ||||||
|  |         <VxeColumn field="name" title="名字" align="center" /> | ||||||
|  |         <VxeColumn field="sex" title="性别" align="center"> | ||||||
|  |           <template #default="{ row }"> | ||||||
|  |             <DictTag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" /> | ||||||
|  |           </template> | ||||||
|  |         </VxeColumn> | ||||||
|  |         <VxeColumn field="birthday" title="出生年" align="center"> | ||||||
|  |           <template #default="{ row }"> | ||||||
|  |             {{ formatDateTime(row.birthday) }} | ||||||
|  |           </template> | ||||||
|  |         </VxeColumn> | ||||||
|  |         <VxeColumn field="description" title="简介" align="center" /> | ||||||
|  |         <VxeColumn field="avatar" title="头像" align="center" /> | ||||||
|  |         <VxeColumn field="createTime" title="创建时间" align="center"> | ||||||
|  |           <template #default="{ row }"> | ||||||
|  |             {{ formatDateTime(row.createTime) }} | ||||||
|  |           </template> | ||||||
|  |         </VxeColumn> | ||||||
|  |         <VxeColumn field="operation" title="操作" align="center"> | ||||||
|  |           <template #default="{ row }"> | ||||||
|  |             <Button | ||||||
|  |               size="small" | ||||||
|  |               type="link" | ||||||
|  |               @click="onEdit(row as any)" | ||||||
|  |               v-access:code="['infra:demo01-contact:update']" | ||||||
|  |             > | ||||||
|  |               {{ $t('ui.actionTitle.edit') }} | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               size="small" | ||||||
|  |               type="link" | ||||||
|  |               class="ml-2" | ||||||
|  |               @click="onDelete(row as any)" | ||||||
|  |               v-access:code="['infra:demo01-contact:delete']" | ||||||
|  |             > | ||||||
|  |               {{ $t('ui.actionTitle.delete') }} | ||||||
|  |             </Button> | ||||||
|  |           </template> | ||||||
|  |         </VxeColumn> | ||||||
|  |       </VxeTable> | ||||||
|  |       <!-- 分页 --> | ||||||
|  |       <div class="mt-2 flex justify-end"> | ||||||
|  |         <Pagination | ||||||
|  |           :total="total" | ||||||
|  |           v-model:current="queryParams.pageNo" | ||||||
|  |           v-model:page-size="queryParams.pageSize" | ||||||
|  |           show-size-changer | ||||||
|  |           @change="getList" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </ContentWrap> | ||||||
|  |   </Page> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,121 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; | ||||||
|  | import type { Rule } from 'ant-design-vue/es/form'; | ||||||
|  | 
 | ||||||
|  | import { Tinymce as RichTextarea } from '#/components/tinymce'; | ||||||
|  | import { ImageUpload } from '#/components/upload'; | ||||||
|  | import { useVbenModal } from '@vben/common-ui'; | ||||||
|  | import { DatePicker, Form, Input, message, Radio, RadioGroup } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
|  | import { createDemo01Contact, getDemo01Contact, updateDemo01Contact } from '#/api/infra/demo/demo01'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const emit = defineEmits(['success']); | ||||||
|  | const formRef = ref(); | ||||||
|  | const labelCol = { span: 5 }; | ||||||
|  | const wrapperCol = { span: 13 }; | ||||||
|  | const formData = ref<Partial<Demo01ContactApi.Demo01Contact>>({ | ||||||
|  |   id: undefined, | ||||||
|  |   name: undefined, | ||||||
|  |   sex: undefined, | ||||||
|  |   birthday: undefined, | ||||||
|  |   description: undefined, | ||||||
|  |   avatar: undefined, | ||||||
|  | }); | ||||||
|  | const rules: Record<string, Rule[]> = { | ||||||
|  |   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }], | ||||||
|  |   sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }], | ||||||
|  |   birthday: [{ required: true, message: '出生年不能为空', trigger: 'blur' }], | ||||||
|  |   description: [{ required: true, message: '简介不能为空', trigger: 'blur' }], | ||||||
|  | }; | ||||||
|  | const getTitle = computed(() => { | ||||||
|  |   return formData.value?.id ? $t('ui.actionTitle.edit', ['示例联系人']) : $t('ui.actionTitle.create', ['示例联系人']); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | /** 重置表单 */ | ||||||
|  | const resetForm = () => { | ||||||
|  |   formData.value = { | ||||||
|  |     id: undefined, | ||||||
|  |     name: undefined, | ||||||
|  |     sex: undefined, | ||||||
|  |     birthday: undefined, | ||||||
|  |     description: undefined, | ||||||
|  |     avatar: undefined, | ||||||
|  |   }; | ||||||
|  |   formRef.value?.resetFields(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const [Modal, modalApi] = useVbenModal({ | ||||||
|  |   async onConfirm() { | ||||||
|  |     await formRef.value?.validate(); | ||||||
|  |     modalApi.lock(); | ||||||
|  |     // 提交表单 | ||||||
|  |     const data = formData.value as Demo01ContactApi.Demo01Contact; | ||||||
|  |     try { | ||||||
|  |       await (formData.value?.id ? updateDemo01Contact(data) : createDemo01Contact(data)); | ||||||
|  |       // 关闭并提示 | ||||||
|  |       await modalApi.close(); | ||||||
|  |       emit('success'); | ||||||
|  |       message.success({ | ||||||
|  |         content: $t('ui.actionMessage.operationSuccess'), | ||||||
|  |         key: 'action_process_msg', | ||||||
|  |       }); | ||||||
|  |     } finally { | ||||||
|  |       modalApi.lock(false); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   async onOpenChange(isOpen: boolean) { | ||||||
|  |     if (!isOpen) { | ||||||
|  |       resetForm(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 加载数据 | ||||||
|  |     let data = modalApi.getData<Demo01ContactApi.Demo01Contact>(); | ||||||
|  |     if (!data) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (data.id) { | ||||||
|  |       modalApi.lock(); | ||||||
|  |       try { | ||||||
|  |         data = await getDemo01Contact(data.id); | ||||||
|  |       } finally { | ||||||
|  |         modalApi.lock(false); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     formData.value = data; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <Modal :title="getTitle"> | ||||||
|  |     <Form ref="formRef" :model="formData" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||||
|  |       <Form.Item label="名字" name="name"> | ||||||
|  |         <Input v-model:value="formData.name" placeholder="请输入名字" /> | ||||||
|  |       </Form.Item> | ||||||
|  |       <Form.Item label="性别" name="sex"> | ||||||
|  |         <RadioGroup v-model:value="formData.sex"> | ||||||
|  |           <Radio | ||||||
|  |             v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')" | ||||||
|  |             :key="dict.value" | ||||||
|  |             :value="dict.value" | ||||||
|  |           > | ||||||
|  |             {{ dict.label }} | ||||||
|  |           </Radio> | ||||||
|  |         </RadioGroup> | ||||||
|  |       </Form.Item> | ||||||
|  |       <Form.Item label="出生年" name="birthday"> | ||||||
|  |         <DatePicker v-model:value="formData.birthday" value-format="x" placeholder="选择出生年" /> | ||||||
|  |       </Form.Item> | ||||||
|  |       <Form.Item label="简介" name="description"> | ||||||
|  |         <RichTextarea v-model="formData.description" height="500px" /> | ||||||
|  |       </Form.Item> | ||||||
|  |       <Form.Item label="头像" name="avatar"> | ||||||
|  |         <ImageUpload v-model:value="formData.avatar" /> | ||||||
|  |       </Form.Item> | ||||||
|  |     </Form> | ||||||
|  |   </Modal> | ||||||
|  | </template> | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -69,4 +69,5 @@ export { | ||||||
|   Upload, |   Upload, | ||||||
|   UserRoundPen, |   UserRoundPen, | ||||||
|   X, |   X, | ||||||
|  |   RefreshCw, | ||||||
| } from 'lucide-vue-next'; | } from 'lucide-vue-next'; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 芋道源码
						芋道源码