Эх сурвалжийг харах

feat: add useCrudSchemas hook

陈凯龙 3 жил өмнө
parent
commit
00d947e2f8

+ 63 - 0
mock/dict/index.ts

@@ -0,0 +1,63 @@
+import { config } from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+
+const { result_code } = config
+
+const timeout = 1000
+
+const dictObj: Recordable = {
+  importance: [
+    {
+      value: 0,
+      label: 'tableDemo.commonly'
+    },
+    {
+      value: 1,
+      label: 'tableDemo.good'
+    },
+    {
+      value: 2,
+      label: 'tableDemo.important'
+    }
+  ]
+}
+
+export default [
+  // 字典接口
+  {
+    url: '/dict/list',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        code: result_code,
+        data: dictObj
+      }
+    }
+  },
+  // 获取某个字典
+  {
+    url: '/dict/one',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        code: result_code,
+        data: [
+          {
+            label: 'test1',
+            value: 0
+          },
+          {
+            label: 'test2',
+            value: 1
+          },
+          {
+            label: 'test3',
+            value: 2
+          }
+        ]
+      }
+    }
+  }
+] as MockMethod[]

+ 13 - 0
src/api/common/index.ts

@@ -0,0 +1,13 @@
+import { useAxios } from '@/hooks/web/useAxios'
+
+const request = useAxios()
+
+// 获取所有字典
+export const getDictApi = () => {
+  return request.get({ url: '/dict/list' })
+}
+
+// 模拟获取某个字典
+export const getDictOneApi = () => {
+  return request.get({ url: '/dict/one' })
+}

+ 1 - 1
src/components/ContentDetailWrap/src/ContentDetailWrap.vue

@@ -29,7 +29,7 @@ onMounted(() => {
       <div
         :class="[
           `${prefixCls}-header`,
-          'flex border-bottom-1 h-50px items-center text-center bg-white  pr-10px '
+          'flex border-bottom-1 h-50px items-center text-center bg-white pr-10px'
         ]"
       >
         <div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">

+ 217 - 0
src/hooks/web/useCrudSchemas.ts

@@ -0,0 +1,217 @@
+import { reactive } from 'vue'
+import { eachTree, treeMap, filter } from '@/utils/tree'
+import { findIndex } from '@/utils'
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { useI18n } from '@/hooks/web/useI18n'
+import type { AxiosPromise } from 'axios'
+
+export type CrudSchema = Omit<TableColumn, 'children'> & {
+  search?: CrudSearchParams
+  table?: CrudTableParams
+  form?: CrudFormParams
+  detail?: CrudDescriptionsParams
+  children?: CrudSchema[]
+}
+
+type CrudSearchParams = {
+  // 是否显示在查询项
+  show?: boolean
+  // 字典名称,会去取全局的字典
+  dictName?: string
+  // 接口路径
+  dictUrl?: string
+} & Omit<FormSchema, 'field'>
+
+type CrudTableParams = {
+  // 是否显示表头
+  show?: boolean
+} & Omit<FormSchema, 'field'>
+
+type CrudFormParams = {
+  // 是否显示表单项
+  show?: boolean
+} & Omit<FormSchema, 'field'>
+
+type CrudDescriptionsParams = {
+  // 是否显示表单项
+  show?: boolean
+} & Omit<DescriptionsSchema, 'field'>
+
+const dictStore = useDictStoreWithOut()
+
+const { t } = useI18n()
+
+interface AllSchemas {
+  searchSchema: FormSchema[]
+  tableColumns: TableColumn[]
+  formSchema: FormSchema[]
+  detailSchema: DescriptionsSchema[]
+}
+
+// 过滤所有结构
+export const useCrudSchemas = (
+  crudSchema: CrudSchema[]
+): {
+  allSchemas: AllSchemas
+} => {
+  // 所有结构数据
+  const allSchemas = reactive<AllSchemas>({
+    searchSchema: [],
+    tableColumns: [],
+    formSchema: [],
+    detailSchema: []
+  })
+
+  const searchSchema = filterSearchSchema(crudSchema, allSchemas)
+  allSchemas.searchSchema = searchSchema || []
+
+  const tableColumns = filterTableSchema(crudSchema)
+  allSchemas.tableColumns = tableColumns || []
+
+  const formSchema = filterFormSchema(crudSchema)
+  allSchemas.formSchema = formSchema
+
+  const detailSchema = filterDescriptionsSchema(crudSchema)
+  allSchemas.detailSchema = detailSchema
+
+  return {
+    allSchemas
+  }
+}
+
+// 过滤 Search 结构
+const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
+  const searchSchema: FormSchema[] = []
+
+  // 获取字典列表队列
+  const searchRequestTask: Array<() => Promise<void>> = []
+
+  eachTree(crudSchema, (schemaItem: CrudSchema) => {
+    // 判断是否显示
+    if (schemaItem?.search?.show) {
+      const searchSchemaItem = {
+        // 默认为 input
+        component: schemaItem.search.component || 'Input',
+        componentProps: {},
+        ...schemaItem.search,
+        field: schemaItem.field,
+        label: schemaItem.label
+      }
+
+      if (searchSchemaItem.dictName) {
+        // 如果有 dictName 则证明是从字典中获取数据
+        const dictArr = dictStore.getDictObj[searchSchemaItem.dictName]
+        searchSchemaItem.componentProps!.options = filterOptions(dictArr)
+      } else if (searchSchemaItem.api) {
+        searchRequestTask.push(async () => {
+          const res = await (searchSchemaItem.api as () => AxiosPromise)()
+          if (res) {
+            const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => {
+              return v.field === searchSchemaItem.field
+            })
+            if (index !== -1) {
+              allSchemas.searchSchema[index]!.componentProps!.options = filterOptions(
+                res.data,
+                searchSchemaItem.componentProps.optionsAlias?.labelField
+              )
+            }
+          }
+        })
+      }
+
+      // 删除不必要的字段
+      delete searchSchemaItem.show
+      delete searchSchemaItem.dictName
+
+      searchSchema.push(searchSchemaItem)
+    }
+  })
+
+  for (const task of searchRequestTask) {
+    task()
+  }
+
+  return searchSchema
+}
+
+// 过滤 table 结构
+const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
+  const tableColumns = treeMap<CrudSchema>(crudSchema, {
+    conversion: (schema: CrudSchema) => {
+      if (schema?.table?.show !== false) {
+        return {
+          ...schema.table,
+          ...schema
+        }
+      }
+    }
+  })
+
+  // 第一次过滤会有 undefined 所以需要二次过滤
+  return filter<TableColumn>(tableColumns as TableColumn[], (data) => {
+    if (data.children === void 0) {
+      delete data.children
+    }
+    return !!data.field
+  })
+}
+
+// 过滤 form 结构
+const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
+  const formSchema: FormSchema[] = []
+
+  eachTree(crudSchema, (schemaItem: CrudSchema) => {
+    // 判断是否显示
+    if (schemaItem?.form?.show !== false) {
+      const formSchemaItem = {
+        // 默认为 input
+        component: (schemaItem.form && schemaItem.form.component) || 'Input',
+        ...schemaItem.form,
+        field: schemaItem.field,
+        label: schemaItem.label
+      }
+
+      // 删除不必要的字段
+      delete formSchemaItem.show
+
+      formSchema.push(formSchemaItem)
+    }
+  })
+
+  return formSchema
+}
+
+// 过滤 descriptions 结构
+const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => {
+  const descriptionsSchema: FormSchema[] = []
+
+  eachTree(crudSchema, (schemaItem: CrudSchema) => {
+    // 判断是否显示
+    if (schemaItem?.detail?.show !== false) {
+      const descriptionsSchemaItem = {
+        ...schemaItem.detail,
+        field: schemaItem.field,
+        label: schemaItem.label
+      }
+
+      // 删除不必要的字段
+      delete descriptionsSchemaItem.show
+
+      descriptionsSchema.push(descriptionsSchemaItem)
+    }
+  })
+
+  return descriptionsSchema
+}
+
+// 给options添加国际化
+const filterOptions = (options: Recordable, labelField?: string) => {
+  return options.map((v: Recordable) => {
+    if (labelField) {
+      v['labelField'] = t(v.labelField)
+    } else {
+      v['label'] = t(v.label)
+    }
+    return v
+  })
+}

+ 13 - 0
src/permission.ts

@@ -5,12 +5,16 @@ import type { RouteRecordRaw } from 'vue-router'
 import { useTitle } from '@/hooks/web/useTitle'
 import { useNProgress } from '@/hooks/web/useNProgress'
 import { usePermissionStoreWithOut } from '@/store/modules/permission'
+import { useDictStoreWithOut } from '@/store/modules/dict'
 import { usePageLoading } from '@/hooks/web/usePageLoading'
+import { getDictApi } from '@/api/common'
 
 const permissionStore = usePermissionStoreWithOut()
 
 const appStore = useAppStoreWithOut()
 
+const dictStore = useDictStoreWithOut()
+
 const { wsCache } = useCache()
 
 const { start, done } = useNProgress()
@@ -31,6 +35,15 @@ router.beforeEach(async (to, from, next) => {
         return
       }
 
+      if (!dictStore.getIsSetDict) {
+        // 获取所有字典
+        const res = await getDictApi()
+        if (res) {
+          dictStore.setDictObj(res.data)
+          dictStore.setIsSetDict(true)
+        }
+      }
+
       // 开发者可根据实际情况进行修改
       const roleRouters = wsCache.get('roleRouters') || []
       const userInfo = wsCache.get(appStore.getUserInfo)

+ 38 - 0
src/store/modules/dict.ts

@@ -0,0 +1,38 @@
+import { defineStore } from 'pinia'
+import { store } from '../index'
+
+export interface DictState {
+  isSetDict: boolean
+  dictObj: Recordable
+}
+
+export const useDictStore = defineStore({
+  id: 'dict',
+  state: (): DictState => ({
+    isSetDict: false,
+    dictObj: {}
+  }),
+  persist: {
+    enabled: true
+  },
+  getters: {
+    getDictObj(): Recordable {
+      return this.dictObj
+    },
+    getIsSetDict(): boolean {
+      return this.isSetDict
+    }
+  },
+  actions: {
+    setDictObj(dictObj: Recordable) {
+      this.dictObj = dictObj
+    },
+    setIsSetDict(isSetDict: boolean) {
+      this.isSetDict = isSetDict
+    }
+  }
+})
+
+export const useDictStoreWithOut = () => {
+  return useDictStore(store)
+}

+ 92 - 20
src/views/Example/Dialog/ExampleDialog.vue

@@ -8,9 +8,10 @@ import { Table } from '@/components/Table'
 import { getTableListApi, saveTableApi, delTableListApi } from '@/api/table'
 import { useTable } from '@/hooks/web/useTable'
 import { TableData } from '@/api/table/types'
-import { h, reactive, ref, unref } from 'vue'
+import { h, ref, unref, reactive } from 'vue'
 import Write from './components/Write.vue'
 import Detail from './components/Detail.vue'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
 
 const { register, tableObject, methods } = useTable<
   {
@@ -33,24 +34,32 @@ getList()
 
 const { t } = useI18n()
 
-const searchData: FormSchema[] = [
-  {
-    label: t('exampleDemo.title'),
-    value: '',
-    component: 'Input',
-    field: 'title'
-  }
-]
-
-const columns = reactive<TableColumn[]>([
+const crudSchemas = reactive<CrudSchema[]>([
   {
     field: 'index',
     label: t('tableDemo.index'),
-    type: 'index'
+    type: 'index',
+    form: {
+      show: false
+    },
+    detail: {
+      show: false
+    }
   },
   {
     field: 'title',
-    label: t('tableDemo.title')
+    label: t('tableDemo.title'),
+    search: {
+      show: true
+    },
+    form: {
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24
+    }
   },
   {
     field: 'author',
@@ -58,7 +67,14 @@ const columns = reactive<TableColumn[]>([
   },
   {
     field: 'display_time',
-    label: t('tableDemo.displayTime')
+    label: t('tableDemo.displayTime'),
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'datetime',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss'
+      }
+    }
   },
   {
     field: 'importance',
@@ -76,19 +92,66 @@ const columns = reactive<TableColumn[]>([
             ? t('tableDemo.good')
             : t('tableDemo.commonly')
       )
+    },
+    form: {
+      component: 'Select',
+      componentProps: {
+        options: [
+          {
+            label: '重要',
+            value: 3
+          },
+          {
+            label: '良好',
+            value: 2
+          },
+          {
+            label: '一般',
+            value: 1
+          }
+        ]
+      }
     }
   },
   {
     field: 'pageviews',
-    label: t('tableDemo.pageviews')
+    label: t('tableDemo.pageviews'),
+    form: {
+      component: 'InputNumber',
+      value: 0
+    }
+  },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    table: {
+      show: false
+    },
+    form: {
+      component: 'Editor',
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24
+    }
   },
   {
     field: 'action',
     width: '260px',
-    label: t('tableDemo.action')
+    label: t('tableDemo.action'),
+    form: {
+      show: false
+    },
+    detail: {
+      show: false
+    }
   }
 ])
 
+const { allSchemas } = useCrudSchemas(crudSchemas)
+
 const dialogVisible = ref(false)
 
 const dialogTitle = ref('')
@@ -152,7 +215,7 @@ const save = async () => {
 
 <template>
   <ContentWrap>
-    <Search :schema="searchData" @search="setSearchParams" @reset="setSearchParams" />
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
 
     <div class="mb-10px">
       <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
@@ -164,7 +227,7 @@ const save = async () => {
     <Table
       v-model:pageSize="tableObject.pageSize"
       v-model:currentPage="tableObject.currentPage"
-      :columns="columns"
+      :columns="allSchemas.tableColumns"
       :data="tableObject.tableList"
       :loading="tableObject.loading"
       :pagination="{
@@ -187,9 +250,18 @@ const save = async () => {
   </ContentWrap>
 
   <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <Write v-if="actionType !== 'detail'" ref="writeRef" :current-row="tableObject.currentRow" />
+    <Write
+      v-if="actionType !== 'detail'"
+      ref="writeRef"
+      :form-schema="allSchemas.formSchema"
+      :current-row="tableObject.currentRow"
+    />
 
-    <Detail v-if="actionType === 'detail'" :current-row="tableObject.currentRow" />
+    <Detail
+      v-if="actionType === 'detail'"
+      :detail-schema="allSchemas.detailSchema"
+      :current-row="tableObject.currentRow"
+    />
 
     <template #footer>
       <ElButton v-if="actionType !== 'detail'" type="primary" :loading="loading" @click="save">

+ 6 - 31
src/views/Example/Dialog/components/Detail.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { PropType, reactive } from 'vue'
+import { PropType } from 'vue'
 import type { TableData } from '@/api/table/types'
 import { Descriptions } from '@/components/Descriptions'
 import { useI18n } from '@/hooks/web/useI18n'
@@ -11,41 +11,16 @@ defineProps({
   currentRow: {
     type: Object as PropType<Nullable<TableData>>,
     default: () => null
-  }
-})
-
-const schema = reactive<DescriptionsSchema[]>([
-  {
-    field: 'title',
-    label: t('exampleDemo.title'),
-    span: 24
-  },
-  {
-    field: 'author',
-    label: t('exampleDemo.author')
   },
-  {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime')
-  },
-  {
-    field: 'importance',
-    label: t('exampleDemo.importance')
-  },
-  {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews')
-  },
-  {
-    field: 'content',
-    label: t('exampleDemo.content'),
-    span: 24
+  detailSchema: {
+    type: Array as PropType<DescriptionsSchema[]>,
+    default: () => []
   }
-])
+})
 </script>
 
 <template>
-  <Descriptions :schema="schema" :data="currentRow || {}">
+  <Descriptions :schema="detailSchema" :data="currentRow || {}">
     <template #importance="{ row }: { row: TableData }">
       <ElTag :type="row.importance === 1 ? 'success' : row.importance === 2 ? 'warning' : 'danger'">
         {{

+ 6 - 97
src/views/Example/Dialog/components/Write.vue

@@ -3,102 +3,18 @@ import { Form } from '@/components/Form'
 import { useForm } from '@/hooks/web/useForm'
 import { PropType, reactive, watch } from 'vue'
 import { TableData } from '@/api/table/types'
-import { useI18n } from '@/hooks/web/useI18n'
 import { required } from '@/utils/formRules'
-import { IDomEditor } from '@wangeditor/editor'
 
 const props = defineProps({
   currentRow: {
     type: Object as PropType<Nullable<TableData>>,
     default: () => null
-  }
-})
-
-const { t } = useI18n()
-
-const schema = reactive<FormSchema[]>([
-  {
-    field: 'title',
-    label: t('exampleDemo.title'),
-    component: 'Input',
-    formItemProps: {
-      rules: [required]
-    },
-    colProps: {
-      span: 24
-    }
-  },
-  {
-    field: 'author',
-    label: t('exampleDemo.author'),
-    component: 'Input',
-    formItemProps: {
-      rules: [required]
-    }
   },
-  {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime'),
-    component: 'DatePicker',
-    componentProps: {
-      type: 'datetime',
-      valueFormat: 'YYYY-MM-DD HH:mm:ss'
-    },
-    formItemProps: {
-      rules: [required]
-    }
-  },
-  {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
-    component: 'Select',
-    formItemProps: {
-      rules: [required]
-    },
-    componentProps: {
-      options: [
-        {
-          label: '重要',
-          value: 3
-        },
-        {
-          label: '良好',
-          value: 2
-        },
-        {
-          label: '一般',
-          value: 1
-        }
-      ]
-    }
-  },
-  {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews'),
-    component: 'InputNumber',
-    value: 0,
-    formItemProps: {
-      rules: [required]
-    }
-  },
-  {
-    field: 'content',
-    component: 'Editor',
-    colProps: {
-      span: 24
-    },
-    componentProps: {
-      defaultHtml: '',
-      onChange: (edit: IDomEditor) => {
-        const { setValues } = methods
-        setValues({
-          content: edit.getHtml()
-        })
-      }
-    },
-    label: t('exampleDemo.content')
+  formSchema: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => []
   }
-])
+})
 
 const rules = reactive({
   title: [required],
@@ -110,22 +26,15 @@ const rules = reactive({
 })
 
 const { register, methods, elFormRef } = useForm({
-  schema
+  schema: props.formSchema
 })
 
 watch(
   () => props.currentRow,
   (currentRow) => {
     if (!currentRow) return
-    const { setValues, setSchema } = methods
+    const { setValues } = methods
     setValues(currentRow)
-    setSchema([
-      {
-        field: 'content',
-        path: 'componentProps.defaultHtml',
-        value: currentRow.content
-      }
-    ])
   },
   {
     deep: true,

+ 17 - 13
src/views/Example/Page/ExamplePage.vue

@@ -10,6 +10,7 @@ import { TableData } from '@/api/table/types'
 import { h, reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { useEmitt } from '@/hooks/web/useEmitt'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
 
 defineOptions({
   name: 'ExamplePage'
@@ -48,16 +49,7 @@ useEmitt({
 
 const { t } = useI18n()
 
-const searchData: FormSchema[] = [
-  {
-    label: t('exampleDemo.title'),
-    value: '',
-    component: 'Input',
-    field: 'title'
-  }
-]
-
-const columns = reactive<TableColumn[]>([
+const crudSchemas = reactive<CrudSchema[]>([
   {
     field: 'index',
     label: t('tableDemo.index'),
@@ -65,7 +57,10 @@ const columns = reactive<TableColumn[]>([
   },
   {
     field: 'title',
-    label: t('tableDemo.title')
+    label: t('tableDemo.title'),
+    search: {
+      show: true
+    }
   },
   {
     field: 'author',
@@ -97,6 +92,13 @@ const columns = reactive<TableColumn[]>([
     field: 'pageviews',
     label: t('tableDemo.pageviews')
   },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    table: {
+      show: false
+    }
+  },
   {
     field: 'action',
     width: '260px',
@@ -104,6 +106,8 @@ const columns = reactive<TableColumn[]>([
   }
 ])
 
+const { allSchemas } = useCrudSchemas(crudSchemas)
+
 const AddAction = () => {
   push('/example/example-add')
 }
@@ -130,7 +134,7 @@ const action = (row: TableData, type: string) => {
 
 <template>
   <ContentWrap>
-    <Search :schema="searchData" @search="setSearchParams" @reset="setSearchParams" />
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
 
     <div class="mb-10px">
       <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
@@ -142,7 +146,7 @@ const action = (row: TableData, type: string) => {
     <Table
       v-model:pageSize="tableObject.pageSize"
       v-model:currentPage="tableObject.currentPage"
-      :columns="columns"
+      :columns="allSchemas.tableColumns"
       :data="tableObject.tableList"
       :loading="tableObject.loading"
       :pagination="{

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

@@ -85,6 +85,8 @@ declare global {
     value?: FormValueType
     // 是否隐藏
     hidden?: boolean
+    // 远程加载下拉项
+    api?: <T = any>() => AxiosPromise<T>
   }
 
   declare type FormSetPropsType = {