Kaynağa Gözat

feat: Add Descriptions component and add Descriptions demo

陈凯龙 3 yıl önce
ebeveyn
işleme
7ad46f828d

+ 3 - 0
src/components/Descriptions/index.ts

@@ -0,0 +1,3 @@
+import Descriptions from './src/Descriptions.vue'
+
+export { Descriptions }

+ 134 - 0
src/components/Descriptions/src/Descriptions.vue

@@ -0,0 +1,134 @@
+<script setup lang="ts">
+import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+import { propTypes } from '@/utils/propTypes'
+import { ref, unref, PropType, computed, useAttrs } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+
+const mobile = computed(() => appStore.getMobile)
+
+const attrs = useAttrs()
+
+const props = defineProps({
+  title: propTypes.string.def(''),
+  message: propTypes.string.def(''),
+  collapse: propTypes.bool.def(true),
+  schema: {
+    type: Array as PropType<DescriptionsSchema[]>,
+    default: () => []
+  },
+  data: {
+    type: Object as PropType<Recordable>,
+    default: () => ({})
+  }
+})
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('descriptions')
+
+const getBindValue = computed(() => {
+  const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
+  const obj = { ...attrs, ...props }
+  for (const key in obj) {
+    if (delArr.indexOf(key) !== -1) {
+      delete obj[key]
+    }
+  }
+  return obj
+})
+
+const getBindItemValue = (item: DescriptionsSchema) => {
+  const delArr: string[] = ['field']
+  const obj = { ...item }
+  for (const key in obj) {
+    if (delArr.indexOf(key) !== -1) {
+      delete obj[key]
+    }
+  }
+  return obj
+}
+
+// 折叠
+const show = ref(true)
+
+const toggleClick = () => {
+  if (props.collapse) {
+    show.value = !unref(show)
+  }
+}
+</script>
+
+<template>
+  <div :class="[prefixCls, 'bg-[var(--el-color-white)]']">
+    <div
+      v-if="title"
+      :class="[
+        `${prefixCls}-header`,
+        'h-50px flex justify-between items-center mb-10px border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer'
+      ]"
+      @click="toggleClick"
+    >
+      <div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
+        <div class="flex items-center">
+          {{ title }}
+          <ElTooltip v-if="message" :content="message" placement="right">
+            <Icon icon="ep:warning" class="ml-5px" />
+          </ElTooltip>
+        </div>
+      </div>
+      <Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
+    </div>
+
+    <ElCollapseTransition>
+      <div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
+        <ElDescriptions
+          :column="2"
+          border
+          :direction="mobile ? 'vertical' : 'horizontal'"
+          v-bind="getBindValue"
+        >
+          <ElDescriptionsItem
+            v-for="item in schema"
+            :key="item.field"
+            v-bind="getBindItemValue(item)"
+          >
+            <template #label>
+              <slot :name="`${item.field}-label`" :label="item.label">{{ item.label }}</slot>
+            </template>
+
+            <template #default>
+              <slot :name="item.field">{{ data[item.field] }}</slot>
+            </template>
+          </ElDescriptionsItem>
+        </ElDescriptions>
+      </div>
+    </ElCollapseTransition>
+  </div>
+</template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-descriptions';
+
+.@{prefix-cls}-header {
+  &__title {
+    &::after {
+      position: absolute;
+      top: 3px;
+      left: -10px;
+      width: 4px;
+      height: 70%;
+      background: var(--el-color-primary);
+      content: '';
+    }
+  }
+}
+
+.@{prefix-cls}-content {
+  :deep(.@{elNamespace}-descriptions__cell) {
+    width: 0;
+  }
+}
+</style>

+ 6 - 1
src/components/Form/src/Form.vue

@@ -246,7 +246,12 @@ export default defineComponent({
     }
 
     return () => (
-      <ElForm ref={elFormRef} {...getFormBindValue()} model={formModel} class={prefixCls}>
+      <ElForm
+        ref={elFormRef}
+        {...getFormBindValue()}
+        model={props.isCustom ? props.model : formModel}
+        class={prefixCls}
+      >
         {{
           // 如果需要自定义,就什么都不渲染,而是提供默认插槽
           default: () => {

+ 14 - 3
src/components/ImageViewer/src/ImageViewer.vue

@@ -1,9 +1,20 @@
 <script setup lang="ts">
 import { ElImageViewer } from 'element-plus'
-import { computed, ref } from 'vue'
-import { imageViewerProps } from './props'
+import { computed, ref, PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
 
-const props = defineProps(imageViewerProps)
+const props = defineProps({
+  urlList: {
+    type: Array as PropType<string[]>,
+    default: (): string[] => []
+  },
+  zIndex: propTypes.number.def(200),
+  initialIndex: propTypes.number.def(0),
+  infinite: propTypes.bool.def(true),
+  hideOnClickModal: propTypes.bool.def(false),
+  appendToBody: propTypes.bool.def(false),
+  show: propTypes.bool.def(false)
+})
 
 const getBindValue = computed(() => {
   const propsData: Recordable = { ...props }

+ 0 - 32
src/components/ImageViewer/src/props.ts

@@ -1,32 +0,0 @@
-import { PropType } from 'vue'
-
-export const imageViewerProps = {
-  urlList: {
-    type: Array as PropType<string[]>,
-    default: (): string[] => []
-  },
-  zIndex: {
-    type: Number as PropType<number>,
-    default: 2000
-  },
-  initialIndex: {
-    type: Number as PropType<number>,
-    default: 0
-  },
-  infinite: {
-    type: Boolean as PropType<boolean>,
-    default: true
-  },
-  hideOnClickModal: {
-    type: Boolean as PropType<boolean>,
-    default: false
-  },
-  appendToBody: {
-    type: Boolean as PropType<boolean>,
-    default: false
-  },
-  show: {
-    type: Boolean as PropType<boolean>,
-    default: false
-  }
-}

+ 12 - 1
src/locales/en.ts

@@ -106,7 +106,8 @@ export default {
     editor: 'Editor',
     richText: 'Rich text',
     dialog: 'Dialog',
-    imageViewer: 'Image viewer'
+    imageViewer: 'Image viewer',
+    descriptions: 'Descriptions'
   },
   analysis: {
     newUser: 'New user',
@@ -349,5 +350,15 @@ export default {
     open: 'Open',
     imageViewer: 'Image viewer',
     imageViewerDes: 'Secondary packaging of ImageViewer components based on ElementPlus'
+  },
+  descriptionsDemo: {
+    descriptions: 'Descriptions',
+    descriptionsDes: 'Secondary packaging of Descriptions components based on ElementPlus',
+    username: 'Username',
+    nickName: 'NickName',
+    phone: 'Phone',
+    email: 'Email',
+    addr: 'Address',
+    form: 'Combined with Form component'
   }
 }

+ 12 - 1
src/locales/zh-CN.ts

@@ -106,7 +106,8 @@ export default {
     editor: '编辑器',
     richText: '富文本',
     dialog: '弹窗',
-    imageViewer: '图片预览'
+    imageViewer: '图片预览',
+    descriptions: '描述'
   },
   analysis: {
     newUser: '新增用户',
@@ -346,5 +347,15 @@ export default {
     open: '打开',
     imageViewer: '图片预览',
     imageViewerDes: '基于 ElementPlus 的 ImageViewer 组件二次封装'
+  },
+  descriptionsDemo: {
+    descriptions: '描述',
+    descriptionsDes: '基于 ElementPlus 的 descriptions 组件二次封装',
+    username: '用户名',
+    nickName: '昵称',
+    phone: '联系电话',
+    email: '邮箱',
+    addr: '地址',
+    form: '与 Form 组件组合'
   }
 }

+ 8 - 0
src/router/index.ts

@@ -193,6 +193,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
           title: t('router.search')
         }
       },
+      {
+        path: 'descriptions',
+        component: () => import('@/views/Components/Descriptions.vue'),
+        name: 'Descriptions',
+        meta: {
+          title: t('router.descriptions')
+        }
+      },
       {
         path: 'image-viewer',
         component: () => import('@/views/Components/ImageViewer.vue'),

+ 140 - 0
src/views/Components/Descriptions.vue

@@ -0,0 +1,140 @@
+<script setup lang="ts">
+import { Descriptions } from '@/components/Descriptions'
+import { useI18n } from '@/hooks/web/useI18n'
+import { reactive, unref } from 'vue'
+import { Form } from '@/components/Form'
+import { ElFormItem, ElInput, ElButton } from 'element-plus'
+import { required } from '@/utils/formRules'
+import { useForm } from '@/hooks/web/useForm'
+
+const { t } = useI18n()
+
+const data = reactive({
+  username: 'chenkl',
+  nickName: '梦似花落。',
+  age: 26,
+  phone: '13655971xxxx',
+  email: '502431556@qq.com',
+  addr: '这是一个很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的地址',
+  sex: '男',
+  certy: '3505831994xxxxxxxx'
+})
+
+const schema = reactive<DescriptionsSchema[]>([
+  {
+    field: 'username',
+    label: t('descriptionsDemo.username')
+  },
+  {
+    field: 'nickName',
+    label: t('descriptionsDemo.nickName')
+  },
+  {
+    field: 'phone',
+    label: t('descriptionsDemo.phone')
+  },
+  {
+    field: 'email',
+    label: t('descriptionsDemo.email')
+  },
+  {
+    field: 'addr',
+    label: t('descriptionsDemo.addr'),
+    span: 24
+  }
+])
+
+const form = reactive({
+  username: '',
+  nickName: '',
+  phone: '',
+  email: '',
+  addr: ''
+})
+
+const rules = reactive({
+  username: [required],
+  nickName: [required],
+  phone: [required],
+  email: [required],
+  addr: [required]
+})
+
+const { register, elFormRef } = useForm()
+
+const formValidation = () => {
+  unref(elFormRef)
+    ?.validate()
+    ?.catch(() => {})
+}
+</script>
+
+<template>
+  <Descriptions
+    :title="t('descriptionsDemo.descriptions')"
+    :message="t('descriptionsDemo.descriptionsDes')"
+    :data="data"
+    :schema="schema"
+  />
+
+  <Form is-custom :model="form" :rules="rules" @register="register">
+    <Descriptions :title="t('descriptionsDemo.form')" :data="data" :schema="schema" class="mt-20px">
+      <template #username-label="scope">
+        <span class="is-required--item">{{ scope.label }}</span>
+      </template>
+      <template #nickName-label="scope">
+        <span class="is-required--item">{{ scope.label }}</span>
+      </template>
+      <template #phone-label="scope">
+        <span class="is-required--item">{{ scope.label }}</span>
+      </template>
+      <template #email-label="scope">
+        <span class="is-required--item">{{ scope.label }}</span>
+      </template>
+      <template #addr-label="scope">
+        <span class="is-required--item">{{ scope.label }}</span>
+      </template>
+
+      <template #username>
+        <ElFormItem prop="username">
+          <ElInput v-model="form.username" />
+        </ElFormItem>
+      </template>
+      <template #nickName>
+        <ElFormItem prop="nickName">
+          <ElInput v-model="form.nickName" />
+        </ElFormItem>
+      </template>
+      <template #phone>
+        <ElFormItem prop="phone">
+          <ElInput v-model="form.phone" />
+        </ElFormItem>
+      </template>
+      <template #email>
+        <ElFormItem prop="email">
+          <ElInput v-model="form.email" />
+        </ElFormItem>
+      </template>
+      <template #addr>
+        <ElFormItem prop="addr">
+          <ElInput v-model="form.addr" />
+        </ElFormItem>
+      </template>
+    </Descriptions>
+    <div class="text-center mt-10px">
+      <ElButton @click="formValidation"> {{ t('formDemo.formValidation') }} </ElButton>
+    </div>
+  </Form>
+</template>
+
+<style lang="less" scoped>
+.is-required--item {
+  position: relative;
+
+  &::before {
+    margin-right: 4px;
+    color: var(--el-color-danger);
+    content: '*';
+  }
+}
+</style>

+ 11 - 0
types/componentType/descriptions.d.ts

@@ -0,0 +1,11 @@
+declare interface DescriptionsSchema {
+  span?: number // 占多少分
+  field: string // 字段名
+  label?: string // label名
+  width?: string | number
+  minWidth?: string | number
+  align?: 'left' | 'center' | 'right'
+  labelAlign?: 'left' | 'center' | 'right'
+  className?: string
+  labelClassName?: string
+}