Răsfoiți Sursa

wip(VForm): VForm component development

陈凯龙 3 ani în urmă
părinte
comite
69909e2832

+ 203 - 11
src/App.vue

@@ -1,27 +1,58 @@
 <script setup lang="ts">
-import { reactive } from 'vue'
+import { reactive, ref, onMounted } from 'vue'
 import { ElConfigProvider, ElIcon } from 'element-plus'
 import zhCn from 'element-plus/lib/locale/lang/zh-cn'
 // import en from 'element-plus/lib/locale/lang/en'
 import { VFrom } from '@/components/Form'
 import Calendar from '~icons/ep/calendar'
+import { useI18n } from '@/hooks/web/useI18n'
+const { t } = useI18n()
+
+const restaurants = ref<Recordable[]>([])
+const querySearch = (queryString: string, cb: Fn) => {
+  const results = queryString
+    ? restaurants.value.filter(createFilter(queryString))
+    : restaurants.value
+  // call callback function to return suggestions
+  cb(results)
+}
+const createFilter = (queryString: string) => {
+  return (restaurant: Recordable) => {
+    return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
+  }
+}
+const loadAll = () => {
+  return [
+    { value: 'vue', link: 'https://github.com/vuejs/vue' },
+    { value: 'element', link: 'https://github.com/ElemeFE/element' },
+    { value: 'cooking', link: 'https://github.com/ElemeFE/cooking' },
+    { value: 'mint-ui', link: 'https://github.com/ElemeFE/mint-ui' },
+    { value: 'vuex', link: 'https://github.com/vuejs/vuex' },
+    { value: 'vue-router', link: 'https://github.com/vuejs/vue-router' },
+    { value: 'babel', link: 'https://github.com/babel/babel' }
+  ]
+}
+const handleSelect = (item: Recordable) => {
+  console.log(item)
+}
+onMounted(() => {
+  restaurants.value = loadAll()
+})
 
 const schema = reactive<VFormSchema[]>([
   {
     field: 'field1',
-    component: 'Divider',
-    componentProps: {
-      text: 'Input'
-    }
+    label: t('formDemo.input'),
+    component: 'Divider'
   },
   {
     field: 'field2',
-    label: 'default',
+    label: t('formDemo.default'),
     component: 'Input'
   },
   {
     field: 'field3',
-    label: 'input-icon1',
+    label: `${t('formDemo.icon')}1`,
     component: 'Input',
     componentProps: {
       suffixIcon: Calendar,
@@ -30,7 +61,7 @@ const schema = reactive<VFormSchema[]>([
   },
   {
     field: 'field4',
-    label: 'input-icon2',
+    label: `${t('formDemo.icon')}2`,
     component: 'Input',
     componentProps: {
       slots: {
@@ -41,7 +72,7 @@ const schema = reactive<VFormSchema[]>([
   },
   {
     field: 'field5',
-    label: 'input-mixed',
+    label: t('formDemo.mixed'),
     component: 'Input',
     componentProps: {
       slots: {
@@ -52,7 +83,7 @@ const schema = reactive<VFormSchema[]>([
   },
   {
     field: 'field6',
-    label: 'textarea',
+    label: t('formDemo.textarea'),
     component: 'Input',
     componentProps: {
       type: 'textarea',
@@ -61,10 +92,152 @@ const schema = reactive<VFormSchema[]>([
   },
   {
     field: 'field7',
+    label: t('formDemo.autocomplete'),
+    component: 'Divider'
+  },
+  {
+    field: 'field8',
+    label: t('formDemo.default'),
+    component: 'Autocomplete',
+    componentProps: {
+      fetchSuggestions: querySearch,
+      onSelect: handleSelect
+    }
+  },
+  {
+    field: 'field9',
+    label: t('formDemo.slot'),
+    component: 'Autocomplete',
+    componentProps: {
+      fetchSuggestions: querySearch,
+      onSelect: handleSelect,
+      slots: {
+        default: true
+      }
+    }
+  },
+  {
+    field: 'field10',
     component: 'Divider',
     componentProps: {
-      text: 'Autocomplete'
+      text: t('formDemo.inputNumber')
     }
+  },
+  {
+    field: 'field11',
+    label: t('formDemo.default'),
+    component: 'InputNumber'
+  },
+  {
+    field: 'field11',
+    label: t('formDemo.position'),
+    component: 'InputNumber',
+    componentProps: {
+      controlsPosition: 'right'
+    }
+  },
+  {
+    field: 'field12',
+    label: t('formDemo.select'),
+    component: 'Divider'
+  },
+  {
+    field: 'field13',
+    label: t('formDemo.default'),
+    component: 'Select',
+    options: [
+      {
+        label: '选项1',
+        value: '1'
+      },
+      {
+        label: '选项2',
+        value: '2'
+      }
+    ]
+  },
+  {
+    field: 'field14',
+    label: t('formDemo.slot'),
+    component: 'Select',
+    options: [
+      {
+        label: '选项1',
+        value: '1'
+      },
+      {
+        label: '选项2',
+        value: '2'
+      }
+    ],
+    optionsSlot: true
+  },
+  {
+    field: 'field15',
+    label: t('formDemo.group'),
+    component: 'Select',
+    options: [
+      {
+        label: '选项1',
+        options: [
+          {
+            label: '选项1-1',
+            value: '1-1'
+          },
+          {
+            label: '选项1-2',
+            value: '1-2'
+          }
+        ]
+      },
+      {
+        label: '选项2',
+        options: [
+          {
+            label: '选项2-1',
+            value: '2-1'
+          },
+          {
+            label: '选项2-2',
+            value: '2-2'
+          }
+        ]
+      }
+    ]
+  },
+  {
+    field: 'field16',
+    label: `${t('formDemo.group')}${t('formDemo.slot')}`,
+    component: 'Select',
+    options: [
+      {
+        label: '选项1',
+        options: [
+          {
+            label: '选项1-1',
+            value: '1-1'
+          },
+          {
+            label: '选项1-2',
+            value: '1-2'
+          }
+        ]
+      },
+      {
+        label: '选项2',
+        options: [
+          {
+            label: '选项2-1',
+            value: '2-1'
+          },
+          {
+            label: '选项2-2',
+            value: '2-2'
+          }
+        ]
+      }
+    ],
+    optionsSlot: true
   }
 ])
 </script>
@@ -81,6 +254,25 @@ const schema = reactive<VFormSchema[]>([
 
       <template #field5-prepend> Http:// </template>
       <template #field5-append> .com </template>
+
+      <template #field9-default="{ item }">
+        <div class="value">{{ item.value }}</div>
+        <span class="link">{{ item.link }}</span>
+      </template>
+
+      <template #field14-option="item">
+        <span style="float: left">{{ item.label }}</span>
+        <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{
+          item.value
+        }}</span>
+      </template>
+
+      <template #field16-option="item">
+        <span style="float: left">{{ item.label }}</span>
+        <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{
+          item.value
+        }}</span>
+      </template>
     </VFrom>
   </ElConfigProvider>
 </template>

+ 54 - 25
src/components/Form/src/VForm.vue

@@ -1,7 +1,7 @@
 <script lang="tsx">
-import { PropType, defineComponent, ref, computed, unref, watch } from 'vue'
-import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus'
-import { COMPONENT_MAP } from './componentMap'
+import { PropType, defineComponent, ref, computed, unref } from 'vue'
+import { ElForm, ElFormItem, ElRow, ElCol, ElOption, ElOptionGroup } from 'element-plus'
+import { componentMap } from './componentMap'
 import { propTypes } from '@/utils/propTypes'
 import { getSlot } from '@/utils/tsxHelper'
 import { setTextPlaceholder, setGridProp, setComponentProps, setItemComponentSlots } from './helper'
@@ -33,14 +33,6 @@ export default defineComponent({
     const formRef = ref<ComponentRef<typeof ElForm>>()
     const getProps = computed(() => props)
     const { schema, isCol, isCustom, autoSetPlaceholder } = unref(getProps)
-    const test = ref('')
-
-    watch(
-      () => test.value,
-      (val) => {
-        console.log(val)
-      }
-    )
 
     // 渲染包裹标签,是否使用栅格布局
     function renderWrap() {
@@ -60,11 +52,9 @@ export default defineComponent({
         .map((item) => {
           // 如果是 Divider 组件,需要自己占用一行
           const isDivider = item.component === 'Divider'
-          const Com = COMPONENT_MAP['Divider'] as ReturnType<typeof defineComponent>
+          const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
           return isDivider ? (
-            <Com {...{ contentPosition: 'left', ...item.componentProps }}>
-              {item?.componentProps?.text}
-            </Com>
+            <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
           ) : isCol ? (
             // 如果需要栅格,需要包裹 ElCol
             <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
@@ -79,17 +69,14 @@ export default defineComponent({
       return (
         <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label}>
           {() => {
-            const Com = COMPONENT_MAP[item.component as string] as ReturnType<
-              typeof defineComponent
-            >
+            const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
             return (
               <Com
-                vModel={test.value}
                 {...(autoSetPlaceholder && setTextPlaceholder(item))}
                 {...setComponentProps(item.componentProps)}
               >
                 {{
-                  default: () => (item.options ? renderOptions() : null),
+                  default: () => (item.options ? renderOptions(item) : null),
                   ...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
                 }}
               </Com>
@@ -100,9 +87,53 @@ export default defineComponent({
     }
 
     // 渲染options
-    function renderOptions() {
-      // const optionsMap = ['Radio', 'Checkbox', 'Select']
-      return <div>2222</div>
+    function renderOptions(item: VFormSchema) {
+      switch (item.component) {
+        case 'Select':
+          return renderSelectOptions(item)
+        default:
+          break
+      }
+    }
+
+    // 渲染 select options
+    function renderSelectOptions(item: VFormSchema) {
+      // 如果有别名,就取别名
+      const labelAlias = item.optionsField?.labelField
+      return item.options?.map((option) => {
+        if (option?.options?.length) {
+          return (
+            <ElOptionGroup label={labelAlias ? option[labelAlias] : option['label']}>
+              {() => {
+                return option?.options?.map((v) => {
+                  return renderSelectOptionItem(item, v)
+                })
+              }}
+            </ElOptionGroup>
+          )
+        } else {
+          return renderSelectOptionItem(item, option)
+        }
+      })
+    }
+
+    // 渲染 select option item
+    function renderSelectOptionItem(item: VFormSchema, option: FormOptions) {
+      // 如果有别名,就取别名
+      const labelAlias = item.optionsField?.labelField
+      const valueAlias = item.optionsField?.valueField
+      return (
+        <ElOption
+          label={labelAlias ? option[labelAlias] : option['label']}
+          value={valueAlias ? option[valueAlias] : option['value']}
+        >
+          {{
+            default: () =>
+              // option 插槽名规则,{field}-option
+              item.optionsSlot ? getSlot(slots, `${item.field}-option`, option) : null
+          }}
+        </ElOption>
+      )
     }
 
     // 过滤传入Form组件的属性
@@ -129,5 +160,3 @@ export default defineComponent({
   }
 })
 </script>
-
-<style lang="less" scoped></style>

+ 2 - 2
src/components/Form/src/componentMap.ts

@@ -19,7 +19,7 @@ import {
   ElDivider
 } from 'element-plus'
 
-const COMPONENT_MAP: Recordable<Component, ComponentName> = {
+const componentMap: Recordable<Component, ComponentName> = {
   Radio: ElRadioGroup,
   Checkbox: ElCheckboxGroup,
   Input: ElInput,
@@ -39,4 +39,4 @@ const COMPONENT_MAP: Recordable<Component, ComponentName> = {
   SelectV2: ElSelectV2
 }
 
-export { COMPONENT_MAP }
+export { componentMap }

+ 0 - 0
src/components/Form/src/components/Checkbox.vue


+ 0 - 0
src/components/Form/src/components/Radio.vue


+ 0 - 0
src/components/Form/src/components/Select.vue


+ 2 - 2
src/components/Form/src/helper.ts

@@ -110,8 +110,8 @@ export function setItemComponentSlots(
   for (const key in slotsProps) {
     if (slotsProps[key]) {
       // 由于组件有可能重复,需要有一个唯一的前缀
-      slotObj[key] = () => {
-        return getSlot(slots, `${field}-${key}`)
+      slotObj[key] = (data: Recordable) => {
+        return getSlot(slots, `${field}-${key}`, data)
       }
     }
   }

+ 13 - 0
src/locales/en.ts

@@ -4,5 +4,18 @@ export default {
     selectText: 'Please select',
     startTimeText: 'Start time',
     endTimeText: 'End time'
+  },
+  formDemo: {
+    input: 'Input',
+    inputNumber: 'InputNumber',
+    default: 'Default',
+    icon: 'Icon',
+    mixed: 'Mixed',
+    textarea: 'Textarea',
+    slot: 'Slot',
+    position: 'Position',
+    autocomplete: 'Autocomplete',
+    select: 'Select',
+    group: 'Select Group'
   }
 }

+ 13 - 0
src/locales/zh-CN.ts

@@ -4,5 +4,18 @@ export default {
     selectText: '请选择',
     startTimeText: '开始时间',
     endTimeText: '结束时间'
+  },
+  formDemo: {
+    input: '输入框',
+    inputNumber: '数字输入框',
+    default: '默认',
+    icon: '图标',
+    mixed: '复合型',
+    textarea: '多行文本',
+    slot: '插槽',
+    position: '位置',
+    autocomplete: '自动补全',
+    select: '选择器',
+    group: '选项分组'
   }
 }

+ 4 - 0
src/main.ts

@@ -5,6 +5,10 @@ import { createApp } from 'vue'
 import App from './App.vue'
 const app = createApp(App)
 
+// 引入element-plus
+import { setupElementPlus } from '@/plugins/elementPlus'
+setupElementPlus(app)
+
 // 引入状态管理
 import { setupStore } from '@/store'
 setupStore(app)

+ 16 - 0
src/plugins/elementPlus/index.ts

@@ -0,0 +1,16 @@
+import type { App } from 'vue'
+
+// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题
+import { ElLoading, ElScrollbar } from 'element-plus'
+
+const plugins = [ElLoading]
+const components = [ElScrollbar]
+
+export function setupElementPlus(app: App) {
+  plugins.forEach((plugin) => {
+    app.use(plugin)
+  })
+  components.forEach((component) => {
+    app.component(component.name, component)
+  })
+}

+ 2 - 0
src/types/componentType.d.ts

@@ -58,6 +58,7 @@ declare global {
     disabled?: boolean
     key?: string | number
     children?: FormOptions[]
+    options?: FormOptions[]
     [key: string]: any
   }
 
@@ -76,6 +77,7 @@ declare global {
     value?: FormValueTypes
     options?: FormOptions[]
     optionsField?: FormOptionsAlias
+    optionsSlot?: boolean
     hidden?: boolean
   }