Browse Source

!435 form-create:字典选择器分离,重新封装 api 选择器。扩展字体图标。
Merge pull request !435 from puhui999/dev-crm

芋道源码 10 months ago
parent
commit
41cbabcf88

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

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

+ 0 - 46
src/components/DictSelect/src/DictSelect.vue

@@ -1,46 +0,0 @@
-<!-- 数据字典 Select 选择器 -->
-<template>
-  <el-select class="w-1/1" v-bind="attrs">
-    <template v-if="valueType === 'int'">
-      <el-option
-        v-for="(dict, index) in getIntDictOptions(dictType)"
-        :key="index"
-        :label="dict.label"
-        :value="dict.value"
-      />
-    </template>
-    <template v-if="valueType === 'str'">
-      <el-option
-        v-for="(dict, index) in getStrDictOptions(dictType)"
-        :key="index"
-        :label="dict.label"
-        :value="dict.value"
-      />
-    </template>
-    <template v-if="valueType === 'bool'">
-      <el-option
-        v-for="(dict, index) in getBoolDictOptions(dictType)"
-        :key="index"
-        :label="dict.label"
-        :value="dict.value"
-      />
-    </template>
-  </el-select>
-</template>
-
-<script lang="ts" setup>
-import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-
-// 接受父组件参数
-interface Props {
-  dictType: string // 字典类型
-  valueType: string // 字典值类型
-}
-
-withDefaults(defineProps<Props>(), {
-  dictType: '',
-  valueType: 'str'
-})
-const attrs = useAttrs()
-defineOptions({ name: 'DictSelect' })
-</script>

+ 2 - 1
src/components/FormCreate/index.ts

@@ -1,3 +1,4 @@
 import { useFormCreateDesigner } from './src/useFormCreateDesigner'
+import { useApiSelect } from './src/components/useApiSelect'
 
-export { useFormCreateDesigner }
+export { useFormCreateDesigner, useApiSelect }

+ 59 - 0
src/components/FormCreate/src/components/DictSelect.vue

@@ -0,0 +1,59 @@
+<!-- 数据字典 Select 选择器 -->
+<template>
+  <el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
+    <el-option
+      v-for="(dict, index) in getDictOptions"
+      :key="index"
+      :label="dict.label"
+      :value="dict.value"
+    />
+  </el-select>
+  <el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
+    <el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
+      {{ dict.label }}
+    </el-radio>
+  </el-radio-group>
+  <el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
+    <el-checkbox
+      v-for="(dict, index) in getDictOptions"
+      :key="index"
+      :label="dict.label"
+      :value="dict.value"
+    />
+  </el-checkbox-group>
+</template>
+
+<script lang="ts" setup>
+import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'DictSelect' })
+
+const attrs = useAttrs()
+
+// 接受父组件参数
+interface Props {
+  dictType: string // 字典类型
+  valueType?: 'str' | 'int' | 'bool' // 字典值类型
+  selectType?: 'select' | 'radio' | 'checkbox' // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
+  formCreateInject?: any
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  valueType: 'str',
+  selectType: 'select'
+})
+
+// 获得字典配置
+const getDictOptions = computed(() => {
+  switch (props.valueType) {
+    case 'str':
+      return getStrDictOptions(props.dictType)
+    case 'int':
+      return getIntDictOptions(props.dictType)
+    case 'bool':
+      return getBoolDictOptions(props.dictType)
+    default:
+      return []
+  }
+})
+</script>

+ 143 - 0
src/components/FormCreate/src/components/useApiSelect.tsx

@@ -0,0 +1,143 @@
+import request from '@/config/axios'
+import { isEmpty } from '@/utils/is'
+import { ApiSelectProps } from '@/components/FormCreate/src/type'
+import { jsonParse } from '@/utils'
+
+export const useApiSelect = (option: ApiSelectProps) => {
+  return defineComponent({
+    name: option.name,
+    props: {
+      // 选项标签
+      labelField: {
+        type: String,
+        default: () => option.labelField ?? 'label'
+      },
+      // 选项的值
+      valueField: {
+        type: String,
+        default: () => option.valueField ?? 'value'
+      },
+      // api 接口
+      url: {
+        type: String,
+        default: () => option.url ?? ''
+      },
+      // 请求类型
+      method: {
+        type: String,
+        default: 'GET'
+      },
+      // 请求参数
+      data: {
+        type: String,
+        default: ''
+      },
+      // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
+      selectType: {
+        type: String,
+        default: 'select'
+      },
+      // 是否多选
+      multiple: {
+        type: Boolean,
+        default: false
+      }
+    },
+    setup(props) {
+      const attrs = useAttrs()
+      const options = ref<any[]>([]) // 下拉数据
+      const getOptions = async () => {
+        options.value = []
+        // 接口选择器
+        if (isEmpty(props.url)) {
+          return
+        }
+        let data = []
+        switch (props.method) {
+          case 'GET':
+            data = await request.get({ url: props.url })
+            break
+          case 'POST':
+            data = await request.post({ url: props.url, data: jsonParse(props.data) })
+            break
+        }
+
+        if (Array.isArray(data)) {
+          options.value = data.map((item: any) => ({
+            label: item[props.labelField],
+            value: item[props.valueField]
+          }))
+          return
+        }
+        console.log(`接口[${props.url}] 返回结果不是一个数组`)
+      }
+
+      onMounted(async () => {
+        await getOptions()
+      })
+
+      const buildSelect = () => {
+        if (props.multiple) {
+          // fix:多写此步是为了解决 multiple 属性问题
+          return (
+            <el-select class="w-1/1" {...attrs} multiple>
+              {options.value.map((item, index) => (
+                <el-option key={index} label={item.label} value={item.value} />
+              ))}
+            </el-select>
+          )
+        }
+        return (
+          <el-select class="w-1/1" {...attrs}>
+            {options.value.map((item, index) => (
+              <el-option key={index} label={item.label} value={item.value} />
+            ))}
+          </el-select>
+        )
+      }
+      const buildCheckbox = () => {
+        if (isEmpty(options.value)) {
+          options.value = [
+            { label: '选项1', value: '选项1' },
+            { label: '选项2', value: '选项2' }
+          ]
+        }
+        return (
+          <el-checkbox-group class="w-1/1" {...attrs}>
+            {options.value.map((item, index) => (
+              <el-checkbox key={index} label={item.label} value={item.value} />
+            ))}
+          </el-checkbox-group>
+        )
+      }
+      const buildRadio = () => {
+        if (isEmpty(options.value)) {
+          options.value = [
+            { label: '选项1', value: '选项1' },
+            { label: '选项2', value: '选项2' }
+          ]
+        }
+        return (
+          <el-radio-group class="w-1/1" {...attrs}>
+            {options.value.map((item, index) => (
+              <el-radio key={index} value={item.value}>
+                {item.label}
+              </el-radio>
+            ))}
+          </el-radio-group>
+        )
+      }
+      return () => (
+        <>
+          {props.selectType === 'select'
+            ? buildSelect()
+            : props.selectType === 'radio'
+              ? buildRadio()
+              : props.selectType === 'checkbox'
+                ? buildCheckbox()
+                : buildSelect()}
+        </>
+      )
+    }
+  })
+}

+ 3 - 3
src/components/FormCreate/src/config/index.ts

@@ -2,14 +2,14 @@ import { useUploadFileRule } from './useUploadFileRule'
 import { useUploadImgRule } from './useUploadImgRule'
 import { useUploadImgsRule } from './useUploadImgsRule'
 import { useDictSelectRule } from './useDictSelectRule'
-import { useUserSelectRule } from './useUserSelectRule'
 import { useEditorRule } from './useEditorRule'
+import { useSelectRule } from './useSelectRule'
 
 export {
   useUploadFileRule,
   useUploadImgRule,
   useUploadImgsRule,
   useDictSelectRule,
-  useUserSelectRule,
-  useEditorRule
+  useEditorRule,
+  useSelectRule
 }

+ 77 - 1
src/components/FormCreate/src/config/selectRule.ts

@@ -1,4 +1,24 @@
 const selectRule = [
+  {
+    type: 'select',
+    field: 'selectType',
+    title: '选择器类型',
+    value: 'select',
+    options: [
+      { label: '下拉框', value: 'select' },
+      { label: '单选框', value: 'radio' },
+      { label: '多选框', value: 'checkbox' }
+    ],
+    // 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
+    control: [
+      {
+        value: 'select',
+        condition: '=',
+        method: 'hidden',
+        rule: ['multiple']
+      }
+    ]
+  },
   { type: 'switch', field: 'multiple', title: '是否多选' },
   {
     type: 'switch',
@@ -68,4 +88,60 @@ const selectRule = [
   }
 ]
 
-export default selectRule
+const apiSelectRule = [
+  {
+    type: 'input',
+    field: 'url',
+    title: 'url 地址',
+    props: {
+      placeholder: '/system/user/simple-list'
+    }
+  },
+  {
+    type: 'select',
+    field: 'method',
+    title: '请求类型',
+    value: 'GET',
+    options: [
+      { label: 'GET', value: 'GET' },
+      { label: 'POST', value: 'POST' }
+    ],
+    control: [
+      {
+        value: 'GET',
+        condition: '!=',
+        method: 'hidden',
+        rule: [
+          {
+            type: 'input',
+            field: 'data',
+            title: '请求参数 JSON 格式',
+            props: {
+              autosize: true,
+              type: 'textarea',
+              placeholder: '{"type": 1}'
+            }
+          }
+        ]
+      }
+    ]
+  },
+  {
+    type: 'input',
+    field: 'labelField',
+    title: 'label 属性',
+    props: {
+      placeholder: 'nickname'
+    }
+  },
+  {
+    type: 'input',
+    field: 'valueField',
+    title: 'value 属性',
+    props: {
+      placeholder: 'id'
+    }
+  }
+]
+
+export { selectRule, apiSelectRule }

+ 6 - 3
src/components/FormCreate/src/config/useDictSelectRule.ts

@@ -1,8 +1,11 @@
 import { generateUUID } from '@/utils'
 import * as DictDataApi from '@/api/system/dict/dict.type'
 import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
-import selectRule from '@/components/FormCreate/src/config/selectRule'
+import { selectRule } from '@/components/FormCreate/src/config/selectRule'
 
+/**
+ * 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule
+ */
 export const useDictSelectRule = () => {
   const label = '字典选择器'
   const name = 'DictSelect'
@@ -19,7 +22,7 @@ export const useDictSelectRule = () => {
       })) ?? []
   })
   return {
-    icon: 'icon-select',
+    icon: 'icon-doc-text',
     label,
     name,
     rule() {
@@ -43,7 +46,7 @@ export const useDictSelectRule = () => {
         },
         {
           type: 'select',
-          field: 'valueType',
+          field: 'dictValueType',
           title: '字典值类型',
           value: 'str',
           options: [

+ 33 - 0
src/components/FormCreate/src/config/useSelectRule.ts

@@ -0,0 +1,33 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+import { selectRule } from '@/components/FormCreate/src/config/selectRule'
+import { SelectRuleOption } from '@/components/FormCreate/src/type'
+
+/**
+ * 通用选择器规则 hook
+ * @param option 规则配置
+ */
+export const useSelectRule = (option: SelectRuleOption) => {
+  const label = option.label
+  const name = option.name
+  return {
+    icon: option.icon,
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      if (!option.props) {
+        option.props = []
+      }
+      return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule])
+    }
+  }
+}

+ 0 - 25
src/components/FormCreate/src/config/useUserSelectRule.ts

@@ -1,25 +0,0 @@
-import { generateUUID } from '@/utils'
-import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
-import selectRule from '@/components/FormCreate/src/config/selectRule'
-
-export const useUserSelectRule = () => {
-  const label = '用户选择器'
-  const name = 'UserSelect'
-  return {
-    icon: 'icon-select',
-    label,
-    name,
-    rule() {
-      return {
-        type: name,
-        field: generateUUID(),
-        title: label,
-        info: '',
-        $required: false
-      }
-    },
-    props(_, { t }) {
-      return localeProps(t, name + '.props', [makeRequiredRule(), ...selectRule])
-    }
-  }
-}

+ 50 - 0
src/components/FormCreate/src/type/index.ts

@@ -0,0 +1,50 @@
+import { Rule } from '@form-create/element-ui' //左侧拖拽按钮
+
+// 左侧拖拽按钮
+export interface MenuItem {
+  label: string
+  name: string
+  icon: string
+}
+
+// 左侧拖拽按钮分类
+export interface Menu {
+  title: string
+  name: string
+  list: MenuItem[]
+}
+
+export interface MenuList extends Array<Menu> {}
+
+// 拖拽组件的规则
+export interface DragRule {
+  icon: string
+  name: string
+  label: string
+  children?: string
+  inside?: true
+  drag?: true | String
+  dragBtn?: false
+  mask?: false
+
+  rule(): Rule
+
+  props(v: any, v1: any): Rule[]
+}
+
+// 通用下拉组件 Props 类型
+export interface ApiSelectProps {
+  name: string // 组件名称
+  labelField?: string // 选项标签
+  valueField?: string // 选项的值
+  url?: string // url 接口
+  isDict?: boolean // 是否字典选择器
+}
+
+// 选择组件规则配置类型
+export interface SelectRuleOption {
+  label: string // label 名称
+  name: string // 组件名称
+  icon: string // 组件图标
+  props?: any[] // 组件规则
+}

+ 62 - 15
src/components/FormCreate/src/useFormCreateDesigner.ts

@@ -1,12 +1,14 @@
 import {
   useDictSelectRule,
   useEditorRule,
+  useSelectRule,
   useUploadFileRule,
   useUploadImgRule,
-  useUploadImgsRule,
-  useUserSelectRule
+  useUploadImgsRule
 } from './config'
 import { Ref } from 'vue'
+import { Menu } from '@/components/FormCreate/src/type'
+import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
 
 /**
  * 表单设计器增强 hook
@@ -15,30 +17,25 @@ import { Ref } from 'vue'
  * - 单图上传
  * - 多图上传
  * - 字典选择器
- * - 系统用户选择器
+ * - 用户选择器
+ * - 部门选择器
  * - 富文本
  */
-export const useFormCreateDesigner = (designer: Ref) => {
+export const useFormCreateDesigner = async (designer: Ref) => {
   const editorRule = useEditorRule()
   const uploadFileRule = useUploadFileRule()
   const uploadImgRule = useUploadImgRule()
   const uploadImgsRule = useUploadImgsRule()
-  const dictSelectRule = useDictSelectRule()
-  const userSelectRule = useUserSelectRule()
 
-  onMounted(() => {
+  /**
+   * 构建表单组件
+   */
+  const buildFormComponents = () => {
     // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
     designer.value?.removeMenuItem('upload')
     // 移除自带的富文本组件规则,使用 editorRule 替代
     designer.value?.removeMenuItem('fc-editor')
-    const components = [
-      editorRule,
-      uploadFileRule,
-      uploadImgRule,
-      uploadImgsRule,
-      dictSelectRule,
-      userSelectRule
-    ]
+    const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
     components.forEach((component) => {
       // 插入组件规则
       designer.value?.addComponent(component)
@@ -49,5 +46,55 @@ export const useFormCreateDesigner = (designer: Ref) => {
         label: component.label
       })
     })
+  }
+
+  const userSelectRule = useSelectRule({
+    name: 'UserSelect',
+    label: '用户选择器',
+    icon: 'icon-user-o'
+  })
+  const deptSelectRule = useSelectRule({
+    name: 'DeptSelect',
+    label: '部门选择器',
+    icon: 'icon-address-card-o'
+  })
+  const dictSelectRule = useDictSelectRule()
+  const apiSelectRule0 = useSelectRule({
+    name: 'ApiSelect',
+    label: '接口选择器',
+    icon: 'icon-server',
+    props: [...apiSelectRule]
+  })
+
+  /**
+   * 构建系统字段菜单
+   */
+  const buildSystemMenu = () => {
+    // 移除自带的下拉选择器组件,使用 currencySelectRule 替代
+    designer.value?.removeMenuItem('select')
+    designer.value?.removeMenuItem('radio')
+    designer.value?.removeMenuItem('checkbox')
+    const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
+    const menu: Menu = {
+      name: 'system',
+      title: '系统字段',
+      list: components.map((component) => {
+        // 插入组件规则
+        designer.value?.addComponent(component)
+        // 插入拖拽按钮到 `system` 分类下
+        return {
+          icon: component.icon,
+          name: component.name,
+          label: component.label
+        }
+      })
+    }
+    designer.value?.addMenu(menu)
+  }
+
+  onMounted(async () => {
+    await nextTick()
+    buildFormComponents()
+    buildSystemMenu()
   })
 }

+ 20 - 2
src/plugins/formCreate/index.ts

@@ -19,9 +19,25 @@ import formCreate from '@form-create/element-ui'
 import install from '@form-create/element-ui/auto-import'
 //======================= 自定义组件 =======================
 import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
-import { DictSelect } from '@/components/DictSelect'
-import UserSelect from '@/views/system/user/components/UserSelect.vue'
+import { useApiSelect } from '@/components/FormCreate'
 import { Editor } from '@/components/Editor'
+import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
+
+const UserSelect = useApiSelect({
+  name: 'UserSelect',
+  labelField: 'nickname',
+  valueField: 'id',
+  url: '/system/user/simple-list'
+})
+const DeptSelect = useApiSelect({
+  name: 'DeptSelect',
+  labelField: 'name',
+  valueField: 'id',
+  url: '/system/dept/simple-list'
+})
+const ApiSelect = useApiSelect({
+  name: 'ApiSelect'
+})
 
 const components = [
   ElAside,
@@ -41,6 +57,8 @@ const components = [
   UploadFile,
   DictSelect,
   UserSelect,
+  DeptSelect,
+  ApiSelect,
   Editor
 ]
 

BIN
src/styles/FormCreate/fonts/fontello.woff


+ 22 - 0
src/styles/FormCreate/index.scss

@@ -0,0 +1,22 @@
+// 使用字体图标来源 https://fontello.com/
+
+@font-face {
+  font-family: 'fc-icon';
+  src: url('@/styles/FormCreate/fonts/fontello.woff') format('woff');
+}
+
+.icon-doc-text:before {
+  content: '\f0f6';
+}
+
+.icon-server:before {
+  content: '\f233';
+}
+
+.icon-address-card-o:before {
+  content: '\f2bc';
+}
+
+.icon-user-o:before {
+  content: '\f2c0';
+}

+ 1 - 0
src/styles/index.scss

@@ -1,4 +1,5 @@
 @import './var.css';
+@import './FormCreate/index.scss';
 @import 'element-plus/theme-chalk/dark/css-vars.css';
 
 .reset-margin [class*='el-icon'] + span {

+ 14 - 0
src/utils/index.ts

@@ -435,3 +435,17 @@ export const areaReplace = (areaName: string) => {
     .replace('自治区', '')
     .replace('省', '')
 }
+
+/**
+ * 解析 JSON 字符串
+ *
+ * @param str
+ */
+export function jsonParse(str: string) {
+  try {
+    return JSON.parse(str)
+  } catch (e) {
+    console.log(`str[${str}] 不是一个 JSON 字符串`)
+    return ''
+  }
+}

+ 2 - 4
src/views/infra/build/index.vue

@@ -8,11 +8,9 @@
           <el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
         </div>
       </el-col>
-      <!-- 表单设计器 -->
-      <el-col>
-        <FcDesigner ref="designer" height="780px" />
-      </el-col>
     </el-row>
+    <!-- 表单设计器 -->
+    <FcDesigner ref="designer" height="780px" />
   </ContentWrap>
 
   <!-- 弹窗:表单预览 -->

+ 0 - 28
src/views/system/user/components/UserSelect.vue

@@ -1,28 +0,0 @@
-<!-- TODO puhui999: 先单独一个后面封装成通用选择组件 -->
-<template>
-  <el-select class="w-1/1" v-bind="attrs">
-    <el-option
-      v-for="(dict, index) in userOptions"
-      :key="index"
-      :label="dict.nickname"
-      :value="dict.id"
-    />
-  </el-select>
-</template>
-
-<script lang="ts" setup>
-import * as UserApi from '@/api/system/user'
-
-defineOptions({ name: 'UserSelect' })
-
-const attrs = useAttrs()
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户下拉数据
-
-onMounted(async () => {
-  const data = await UserApi.getSimpleUserList()
-  if (!data || data.length === 0) {
-    return
-  }
-  userOptions.value = data
-})
-</script>