Browse Source

处理路由取值

王飞 1 year ago
parent
commit
8e5dab4eb4

+ 107 - 0
mock/role/index.ts

@@ -39,6 +39,113 @@ const adminList = [
       }
     ]
   },
+  {
+    path: '/manage',
+    component: '#',
+    redirect: '/manage/news-page',
+    name: 'Manage',
+    meta: {
+      title: '模块管理',
+      icon: 'ep:menu',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'news-page',
+        component: 'views/Manage/News/NewsPage',
+        name: 'NewsPage',
+        meta: {
+          title: '新闻管理'
+        }
+      },
+      {
+        path: 'news-add',
+        component: 'views/Manage/News/NewsAdd',
+        name: 'NewsAdd',
+        meta: {
+          title: '新增新闻',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
+        }
+      },
+      {
+        path: 'news-edit',
+        component: 'views/Manage/News/NewsEdit',
+        name: 'NewsEdit',
+        meta: {
+          title: '编辑新闻',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
+        }
+      },
+      {
+        path: 'news-detail',
+        component: 'views/Manage/News/NewsDetail',
+        name: 'NewsDetail',
+        meta: {
+          title: '新闻详情',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
+        }
+      },
+      {
+        path: 'product-page',
+        component: 'views/Manage/Product/ProductPage',
+        name: 'ProductPage',
+        meta: {
+          title: '产品管理'
+        }
+      },
+      {
+        path: 'product-add',
+        component: 'views/Manage/Product/ProductAdd',
+        name: 'ProductAdd',
+        meta: {
+          title: '新增产品',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/product-page'
+        }
+      },
+      {
+        path: 'product-edit',
+        component: 'views/Manage/Product/ProductEdit',
+        name: 'ProductEdit',
+        meta: {
+          title: '编辑产品',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/product-page'
+        }
+      },
+      {
+        path: 'product-detail',
+        component: 'views/Manage/Product/ProductDetail',
+        name: 'ProductDetail',
+        meta: {
+          title: '产品详情',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/product-page'
+        }
+      }
+    ]
+  },
   {
     path: '/example',
     component: '#',

+ 3 - 3
src/components/UserInfo/src/UserInfo.vue

@@ -53,9 +53,9 @@ const lockScreen = () => {
   dialogVisible.value = true
 }
 
-const toDocument = () => {
-  window.open('https://element-plus-admin-doc.cn/')
-}
+// const toDocument = () => {
+//   window.open('https://element-plus-admin-doc.cn/')
+// }
 </script>
 
 <template>

+ 2 - 0
src/config/axios/config.ts

@@ -74,6 +74,7 @@ const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
     config.params = {}
     config.url = url
   }
+  console.log(config)
   return config
 }
 ;(error: AxiosError) => {
@@ -82,6 +83,7 @@ const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
 }
 
 const defaultResponseInterceptors = (response: AxiosResponse<any>) => {
+  console.log(response)
   if (response?.config?.responseType === 'blob') {
     // 如果是文件流,直接过
     return response

+ 1 - 1
src/layout/Layout.vue

@@ -2,7 +2,7 @@
 import { computed, defineComponent, unref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
 import { Backtop } from '@/components/Backtop'
-import { Setting } from '@/components/Setting'
+// import { Setting } from '@/components/Setting'
 import { useRenderLayout } from './components/useRenderLayout'
 import { useDesign } from '@/hooks/web/useDesign'
 

+ 56 - 374
src/router/index.ts

@@ -88,428 +88,110 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
       }
     ]
   },
+
   {
-    path: '/external-link',
-    component: Layout,
-    meta: {},
-    name: 'ExternalLink',
-    children: [
-      {
-        path: 'https://element-plus-admin-doc.cn/',
-        name: 'DocumentLink',
-        meta: {
-          title: t('router.document'),
-          icon: 'clarity:document-solid'
-        }
-      }
-    ]
-  },
-  {
-    path: '/guide',
-    component: Layout,
-    name: 'Guide',
-    meta: {},
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/Guide/Guide.vue'),
-        name: 'GuideDemo',
-        meta: {
-          title: t('router.guide'),
-          icon: 'cib:telegram-plane'
-        }
-      }
-    ]
-  },
-  {
-    path: '/components',
+    path: '/manage',
     component: Layout,
-    name: 'ComponentsDemo',
+    redirect: '/manage/news-page',
+    name: 'Manage',
     meta: {
-      title: t('router.component'),
-      icon: 'bx:bxs-component',
+      title: '模块管理',
+      icon: 'ep:menu',
       alwaysShow: true
     },
     children: [
       {
-        path: 'form',
-        component: getParentLayout(),
-        redirect: '/components/form/default-form',
-        name: 'Form',
-        meta: {
-          title: t('router.form'),
-          alwaysShow: true
-        },
-        children: [
-          {
-            path: 'default-form',
-            component: () => import('@/views/Components/Form/DefaultForm.vue'),
-            name: 'DefaultForm',
-            meta: {
-              title: t('router.defaultForm')
-            }
-          },
-          {
-            path: 'use-form',
-            component: () => import('@/views/Components/Form/UseFormDemo.vue'),
-            name: 'UseForm',
-            meta: {
-              title: 'UseForm'
-            }
-          }
-        ]
-      },
-      {
-        path: 'table',
-        component: getParentLayout(),
-        redirect: '/components/table/default-table',
-        name: 'TableDemo',
-        meta: {
-          title: t('router.table'),
-          alwaysShow: true
-        },
-        children: [
-          {
-            path: 'default-table',
-            component: () => import('@/views/Components/Table/DefaultTable.vue'),
-            name: 'DefaultTable',
-            meta: {
-              title: t('router.defaultTable')
-            }
-          },
-          {
-            path: 'use-table',
-            component: () => import('@/views/Components/Table/UseTableDemo.vue'),
-            name: 'UseTable',
-            meta: {
-              title: 'UseTable'
-            }
-          },
-          {
-            path: 'tree-table',
-            component: () => import('@/views/Components/Table/TreeTable.vue'),
-            name: 'TreeTable',
-            meta: {
-              title: t('router.treeTable')
-            }
-          },
-          {
-            path: 'table-image-preview',
-            component: () => import('@/views/Components/Table/TableImagePreview.vue'),
-            name: 'TableImagePreview',
-            meta: {
-              title: t('router.PicturePreview')
-            }
-          }
-        ]
-      },
-      {
-        path: 'editor-demo',
-        component: getParentLayout(),
-        redirect: '/components/editor-demo/editor',
-        name: 'EditorDemo',
-        meta: {
-          title: t('router.editor'),
-          alwaysShow: true
-        },
-        children: [
-          {
-            path: 'editor',
-            component: () => import('@/views/Components/Editor/Editor.vue'),
-            name: 'Editor',
-            meta: {
-              title: t('router.richText')
-            }
-          }
-        ]
-      },
-      {
-        path: 'search',
-        component: () => import('@/views/Components/Search.vue'),
-        name: 'Search',
-        meta: {
-          title: t('router.search')
-        }
-      },
-      {
-        path: 'descriptions',
-        component: () => import('@/views/Components/Descriptions.vue'),
-        name: 'Descriptions',
+        path: 'news-page',
+        component: () => import('@/views/Manage/News/NewsPage.vue'),
+        name: 'NewsPage',
         meta: {
-          title: t('router.descriptions')
+          title: '新闻管理'
         }
       },
       {
-        path: 'image-viewer',
-        component: () => import('@/views/Components/ImageViewer.vue'),
-        name: 'ImageViewer',
+        path: 'news-add',
+        component: () => import('@/views/Manage/News/NewsAdd.vue'),
+        name: 'NewsAdd',
         meta: {
-          title: t('router.imageViewer')
-        }
-      },
-      {
-        path: 'dialog',
-        component: () => import('@/views/Components/Dialog.vue'),
-        name: 'Dialog',
-        meta: {
-          title: t('router.dialog')
-        }
-      },
-      {
-        path: 'icon',
-        component: () => import('@/views/Components/Icon.vue'),
-        name: 'Icon',
-        meta: {
-          title: t('router.icon')
-        }
-      },
-      {
-        path: 'echart',
-        component: () => import('@/views/Components/Echart.vue'),
-        name: 'Echart',
-        meta: {
-          title: t('router.echart')
-        }
-      },
-      {
-        path: 'count-to',
-        component: () => import('@/views/Components/CountTo.vue'),
-        name: 'CountTo',
-        meta: {
-          title: t('router.countTo')
-        }
-      },
-      {
-        path: 'qrcode',
-        component: () => import('@/views/Components/Qrcode.vue'),
-        name: 'Qrcode',
-        meta: {
-          title: t('router.qrcode')
-        }
-      },
-      {
-        path: 'highlight',
-        component: () => import('@/views/Components/Highlight.vue'),
-        name: 'Highlight',
-        meta: {
-          title: t('router.highlight')
-        }
-      },
-      {
-        path: 'infotip',
-        component: () => import('@/views/Components/Infotip.vue'),
-        name: 'Infotip',
-        meta: {
-          title: t('router.infotip')
+          title: '新增新闻',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
         }
       },
       {
-        path: 'input-password',
-        component: () => import('@/views/Components/InputPassword.vue'),
-        name: 'InputPassword',
-        meta: {
-          title: t('router.inputPassword')
-        }
-      }
-    ]
-  },
-  {
-    path: '/hooks',
-    component: Layout,
-    redirect: '/hooks/useWatermark',
-    name: 'Hooks',
-    meta: {
-      title: 'hooks',
-      icon: 'ic:outline-webhook',
-      alwaysShow: true
-    },
-    children: [
-      {
-        path: 'useWatermark',
-        component: () => import('@/views/hooks/useWatermark.vue'),
-        name: 'UseWatermark',
+        path: 'news-edit',
+        component: () => import('@/views/Manage/News/NewsEdit.vue'),
+        name: 'NewsEdit',
         meta: {
-          title: 'useWatermark'
+          title: '编辑新闻',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
         }
-      }
-      // {
-      //   path: 'useOpenTab',
-      //   component: () => import('@/views/hooks/useOpenTab.vue'),
-      //   name: 'UseOpenTab',
-      //   meta: {
-      //     title: 'useOpenTab'
-      //   }
-      // }
-      // {
-      //   path: 'useCrudSchemas',
-      //   component: () => import('@/views/hooks/useCrudSchemas.vue'),
-      //   name: 'UseCrudSchemas',
-      //   meta: {
-      //     title: 'useCrudSchemas'
-      //   }
-      // }
-    ]
-  },
-  {
-    path: '/level',
-    component: Layout,
-    redirect: '/level/menu1/menu1-1/menu1-1-1',
-    name: 'Level',
-    meta: {
-      title: t('router.level'),
-      icon: 'carbon:skill-level-advanced'
-    },
-    children: [
-      {
-        path: 'menu1',
-        name: 'Menu1',
-        component: getParentLayout(),
-        redirect: '/level/menu1/menu1-1/menu1-1-1',
-        meta: {
-          title: t('router.menu1')
-        },
-        children: [
-          {
-            path: 'menu1-1',
-            name: 'Menu11',
-            component: getParentLayout(),
-            redirect: '/level/menu1/menu1-1/menu1-1-1',
-            meta: {
-              title: t('router.menu11'),
-              alwaysShow: true
-            },
-            children: [
-              {
-                path: 'menu1-1-1',
-                name: 'Menu111',
-                component: () => import('@/views/Level/Menu111.vue'),
-                meta: {
-                  title: t('router.menu111')
-                }
-              }
-            ]
-          },
-          {
-            path: 'menu1-2',
-            name: 'Menu12',
-            component: () => import('@/views/Level/Menu12.vue'),
-            meta: {
-              title: t('router.menu12')
-            }
-          }
-        ]
       },
       {
-        path: 'menu2',
-        name: 'Menu2',
-        component: () => import('@/views/Level/Menu2.vue'),
+        path: 'news-detail',
+        component: () => import('@/views/Manage/News/NewsDetail.vue'),
+        name: 'NewsDetail',
         meta: {
-          title: t('router.menu2')
-        }
-      }
-    ]
-  },
-  {
-    path: '/example',
-    component: Layout,
-    redirect: '/example/example-dialog',
-    name: 'Example',
-    meta: {
-      title: t('router.example'),
-      icon: 'ep:management',
-      alwaysShow: true
-    },
-    children: [
-      {
-        path: 'example-dialog',
-        component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
-        name: 'ExampleDialog',
-        meta: {
-          title: t('router.exampleDialog')
+          title: '新闻详情',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/manage/news-page'
         }
       },
       {
-        path: 'example-page',
-        component: () => import('@/views/Example/Page/ExamplePage.vue'),
-        name: 'ExamplePage',
+        path: 'product-page',
+        component: () => import('@/views/Manage/Product/ProductPage.vue'),
+        name: 'ProductPage',
         meta: {
-          title: t('router.examplePage')
+          title: '产品管理'
         }
       },
       {
-        path: 'example-add',
-        component: () => import('@/views/Example/Page/ExampleAdd.vue'),
-        name: 'ExampleAdd',
+        path: 'product-add',
+        component: () => import('@/views/Manage/Product/ProductAdd.vue'),
+        name: 'ProductAdd',
         meta: {
-          title: t('router.exampleAdd'),
+          title: '新增产品',
           noTagsView: true,
           noCache: true,
           hidden: true,
           canTo: true,
-          activeMenu: '/example/example-page'
+          activeMenu: '/manage/product-page'
         }
       },
       {
-        path: 'example-edit',
-        component: () => import('@/views/Example/Page/ExampleEdit.vue'),
-        name: 'ExampleEdit',
+        path: 'product-edit',
+        component: () => import('@/views/Manage/Product/ProductEdit.vue'),
+        name: 'ProductEdit',
         meta: {
-          title: t('router.exampleEdit'),
+          title: '编辑产品',
           noTagsView: true,
           noCache: true,
           hidden: true,
           canTo: true,
-          activeMenu: '/example/example-page'
+          activeMenu: '/manage/product-page'
         }
       },
       {
-        path: 'example-detail',
-        component: () => import('@/views/Example/Page/ExampleDetail.vue'),
-        name: 'ExampleDetail',
+        path: 'product-detail',
+        component: () => import('@/views/Manage/Product/ProductDetail.vue'),
+        name: 'ProductDetail',
         meta: {
-          title: t('router.exampleDetail'),
+          title: '产品详情',
           noTagsView: true,
           noCache: true,
           hidden: true,
           canTo: true,
-          activeMenu: '/example/example-page'
-        }
-      }
-    ]
-  },
-  {
-    path: '/error',
-    component: Layout,
-    redirect: '/error/404',
-    name: 'Error',
-    meta: {
-      title: t('router.errorPage'),
-      icon: 'ci:error',
-      alwaysShow: true
-    },
-    children: [
-      {
-        path: '404-demo',
-        component: () => import('@/views/Error/404.vue'),
-        name: '404Demo',
-        meta: {
-          title: '404'
-        }
-      },
-      {
-        path: '403-demo',
-        component: () => import('@/views/Error/403.vue'),
-        name: '403Demo',
-        meta: {
-          title: '403'
-        }
-      },
-      {
-        path: '500-demo',
-        component: () => import('@/views/Error/500.vue'),
-        name: '500Demo',
-        meta: {
-          title: '500'
+          activeMenu: '/manage/product-page'
         }
       }
     ]

+ 1 - 1
src/store/modules/app.ts

@@ -57,7 +57,7 @@ export const useAppStore = defineStore('app', {
       fixedHeader: true, // 固定toolheader
       footer: false, // 显示页脚
       greyMode: false, // 是否开始灰色模式,用于特殊悼念日
-      dynamicRouter: getStorage('dynamicRouter') || true, // 是否动态路由
+      dynamicRouter: false, // 是否动态路由
       fixedMenu: getStorage('fixedMenu') || true, // 是否固定菜单
 
       layout: getStorage('layout') || 'classic', // layout布局

+ 53 - 0
src/views/Manage/News/NewsAdd.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import Write from './components/Write.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref, unref } from 'vue'
+import { ElButton } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter } from 'vue-router'
+import { saveTableApi } from '@/api/table'
+import { useEmitt } from '@/hooks/event/useEmitt'
+
+const { emitter } = useEmitt()
+
+const { push, go } = useRouter()
+
+const { t } = useI18n()
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+
+const loading = ref(false)
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    loading.value = true
+    const res = await saveTableApi(formData)
+      .catch(() => {})
+      .finally(() => {
+        loading.value = false
+      })
+    if (res) {
+      emitter.emit('getList', 'add')
+      push('/manage/news-page')
+    }
+  }
+}
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.add')" @back="push('/manage/news-page')">
+    <Write ref="writeRef" />
+
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+      <ElButton type="primary" :loading="loading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+    </template>
+  </ContentDetailWrap>
+</template>
+@/hooks/event/useEmitt

+ 38 - 0
src/views/Manage/News/NewsDetail.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import Detail from './components/Detail.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter, useRoute } from 'vue-router'
+import { getTableDetApi } from '@/api/table'
+import { TableData } from '@/api/table/types'
+import { ElButton } from 'element-plus'
+
+const { push, go } = useRouter()
+
+const { query } = useRoute()
+
+const { t } = useI18n()
+
+const currentRow = ref<Nullable<TableData>>(null)
+
+const getTableDet = async () => {
+  const res = await getTableDetApi(query.id as string)
+  if (res) {
+    currentRow.value = res.data
+  }
+}
+
+getTableDet()
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.detail')" @back="push('/manage/news-page')">
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+    </template>
+    <Detail :current-row="currentRow" />
+  </ContentDetailWrap>
+</template>

+ 67 - 0
src/views/Manage/News/NewsEdit.vue

@@ -0,0 +1,67 @@
+<script setup lang="ts">
+import Write from './components/Write.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref, unref } from 'vue'
+import { ElButton } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter, useRoute } from 'vue-router'
+import { saveTableApi, getTableDetApi } from '@/api/table'
+import { TableData } from '@/api/table/types'
+import { useEmitt } from '@/hooks/event/useEmitt'
+
+const { emitter } = useEmitt()
+
+const { push, go } = useRouter()
+
+const { query } = useRoute()
+
+const { t } = useI18n()
+
+const currentRow = ref<Nullable<TableData>>(null)
+
+const getTableDet = async () => {
+  const res = await getTableDetApi(query.id as string)
+  if (res) {
+    currentRow.value = res.data
+  }
+}
+
+getTableDet()
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+
+const loading = ref(false)
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    loading.value = true
+    const res = await saveTableApi(formData)
+      .catch(() => {})
+      .finally(() => {
+        loading.value = false
+      })
+    if (res) {
+      emitter.emit('getList', 'editor')
+      push('/manage/news-page')
+    }
+  }
+}
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.edit')" @back="push('/manage/news-page')">
+    <Write ref="writeRef" :current-row="currentRow" />
+
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+      <ElButton type="primary" :loading="loading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+    </template>
+  </ContentDetailWrap>
+</template>
+@/hooks/event/useEmitt

+ 347 - 0
src/views/Manage/News/NewsPage.vue

@@ -0,0 +1,347 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTag } from 'element-plus'
+import { Table } from '@/components/Table'
+import { getTableListApi, delTableListApi } from '@/api/table'
+import { useTable } from '@/hooks/web/useTable'
+import { TableData } from '@/api/table/types'
+import { ref, unref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useEmitt } from '@/hooks/event/useEmitt'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+import { TableSetting } from '@/components/TableSetting'
+import { usePageStore } from '@/store/modules/page'
+import { set } from 'lodash-es'
+
+defineOptions({
+  name: 'NewsPage'
+})
+
+const { push } = useRouter()
+
+const ids = ref<string[]>([])
+
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  searchParams.value = params
+  getList()
+}
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { currentPage, pageSize } = tableState
+    const res = await getTableListApi({
+      pageIndex: unref(currentPage),
+      pageSize: unref(pageSize),
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: res.data.total
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(ids))
+    return !!res
+  }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+const { getList, getElTableExpose, delList, setColumn } = tableMethods
+
+getList()
+
+useEmitt({
+  name: 'getList',
+  callback: (type: string) => {
+    if (type === 'add') {
+      currentPage.value = 1
+    }
+    getList()
+  }
+})
+
+const { t } = useI18n()
+const appStore = usePageStore()
+const crudSchemas: CrudSchema[] = [
+  {
+    field: 'selection',
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      type: 'selection',
+      hidden: false
+    }
+  },
+  {
+    field: 'index',
+    label: t('tableDemo.index'),
+    type: 'index',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    }
+  },
+  {
+    field: 'title',
+    label: t('tableDemo.title'),
+    search: {
+      component: 'Input'
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'Input',
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24
+    }
+  },
+  {
+    field: 'author',
+    label: t('tableDemo.author'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    }
+  },
+  {
+    field: 'display_time',
+    label: t('tableDemo.displayTime'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'datetime',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss'
+      }
+    }
+  },
+  {
+    field: 'importance',
+    label: t('tableDemo.importance'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'Select',
+      componentProps: {
+        style: {
+          width: '100%'
+        },
+        options: [
+          {
+            label: '重要',
+            value: 3
+          },
+          {
+            label: '良好',
+            value: 2
+          },
+          {
+            label: '一般',
+            value: 1
+          }
+        ]
+      }
+    },
+    detail: {
+      slots: {
+        default: (data: any) => {
+          return (
+            <ElTag
+              type={
+                data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'
+              }
+            >
+              {data.importance === 1
+                ? t('tableDemo.important')
+                : data.importance === 2
+                ? t('tableDemo.good')
+                : t('tableDemo.commonly')}
+            </ElTag>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'pageviews',
+    label: t('tableDemo.pageviews'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'InputNumber',
+      value: 0
+    }
+  },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: true
+    },
+    form: {
+      component: 'Editor',
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24,
+      slots: {
+        default: (data: any) => {
+          return <div innerHTML={data.content}></div>
+        }
+      }
+    }
+  },
+  {
+    field: 'action',
+    width: '260px',
+    label: t('tableDemo.action'),
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      hidden: false,
+      slots: {
+        default: (data: any) => {
+          return (
+            <>
+              <ElButton type="primary" onClick={() => action(data[0].row, 'edit')}>
+                {t('exampleDemo.edit')}
+              </ElButton>
+              <ElButton type="success" onClick={() => action(data[0].row, 'detail')}>
+                {t('exampleDemo.detail')}
+              </ElButton>
+              <ElButton type="danger" onClick={() => delData(data[0].row)}>
+                {t('exampleDemo.del')}
+              </ElButton>
+            </>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @ts-ignore
+const getSchemas = () => {
+  let localSchemas = appStore.getPageData['NewsPage']
+  if (localSchemas && localSchemas.schemas) {
+    let localSchemasArr = localSchemas.schemas
+    for (let i = 0; i < localSchemasArr.length; i++) {
+      let item = localSchemasArr[i]
+      let index = crudSchemas.findIndex((e) => {
+        return e.field == item.field
+      })
+      if (index > 0) {
+        set(crudSchemas[index], 'table.hidden', item.table.hidden)
+      }
+    }
+  }
+}
+getSchemas()
+let allSchemas = useCrudSchemas(crudSchemas).allSchemas
+// 修改列设置后调用
+const setSchemas = (schemas: CrudSchema[]) => {
+  let arr = schemas.map((item) => {
+    return {
+      field: item.field,
+      path: 'hidden',
+      value: item.table ? item.table.hidden : false
+    }
+  })
+  setColumn(arr)
+}
+
+const AddAction = () => {
+  push('/manage/news-add')
+}
+
+const delLoading = ref(false)
+
+const delData = async (row: TableData | null) => {
+  const elTableExpose = await getElTableExpose()
+  ids.value = row ? [row.id] : elTableExpose?.getSelectionRows().map((v: TableData) => v.id) || []
+  delLoading.value = true
+  await delList(unref(ids).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const action = (row: TableData, type: string) => {
+  push(`/manage/news-${type}?id=${row.id}`)
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
+      <ElButton :loading="delLoading" type="danger" @click="delData(null)">
+        {{ t('exampleDemo.del') }}
+      </ElButton>
+      <TableSetting page="NewsPage" :data="crudSchemas" @set-schemas="setSchemas" />
+    </div>
+
+    <Table
+      v-model:pageSize="pageSize"
+      v-model:currentPage="currentPage"
+      :columns="allSchemas.tableColumns"
+      :data="dataList"
+      :loading="loading"
+      :pagination="{
+        total: total
+      }"
+      @register="tableRegister"
+    />
+  </ContentWrap>
+</template>
+@/hooks/event/useEmitt

+ 69 - 0
src/views/Manage/News/components/Detail.vue

@@ -0,0 +1,69 @@
+<script setup lang="tsx">
+import { PropType, reactive } from 'vue'
+import type { TableData } from '@/api/table/types'
+import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElTag } from 'element-plus'
+
+const { t } = useI18n()
+
+defineProps({
+  currentRow: {
+    type: Object as PropType<Nullable<TableData>>,
+    default: () => null
+  }
+})
+
+const schema = reactive<DescriptionsSchema[]>([
+  {
+    field: 'title',
+    label: '新闻标题',
+    span: 24
+  },
+  {
+    field: 'author',
+    label: t('exampleDemo.author')
+  },
+  {
+    field: 'display_time',
+    label: t('exampleDemo.displayTime')
+  },
+  {
+    field: 'importance',
+    label: t('exampleDemo.importance'),
+    slots: {
+      default: (data: any) => {
+        return (
+          <ElTag
+            type={data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'}
+          >
+            {data.importance === 1
+              ? t('tableDemo.important')
+              : data.importance === 2
+              ? t('tableDemo.good')
+              : t('tableDemo.commonly')}
+          </ElTag>
+        )
+      }
+    }
+  },
+  {
+    field: 'pageviews',
+    label: t('exampleDemo.pageviews')
+  },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    span: 24,
+    slots: {
+      default: (data: any) => {
+        return <div innerHTML={data.content}></div>
+      }
+    }
+  }
+])
+</script>
+
+<template>
+  <Descriptions :schema="schema" :data="currentRow || {}" />
+</template>

+ 174 - 0
src/views/Manage/News/components/Write.vue

@@ -0,0 +1,174 @@
+<script setup lang="tsx">
+import { Form, FormSchema } 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 { useValidator } from '@/hooks/web/useValidator'
+import { IDomEditor } from '@wangeditor/editor'
+import { ElButton } from 'element-plus'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<Nullable<TableData>>,
+    default: () => null
+  }
+})
+
+const { t } = useI18n()
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
+
+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()]
+    },
+    componentProps: {
+      placeholder: '请输入作者'
+    }
+  },
+  {
+    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: 'imageUrl',
+    component: 'Upload',
+    label: '图片',
+    componentProps: {
+      action: 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15',
+      showFileList: false,
+      onSuccess: (_response, uploadFile) => {
+        console.log(uploadFile)
+      },
+      slots: {
+        default: () => <ElButton type="primary">Click to upload</ElButton>,
+        tip: () => <div class="el-upload__tip">jpg/png files with a size less than 500KB.</div>
+      }
+    }
+  },
+  {
+    field: 'content',
+    component: 'Editor',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      defaultHtml: '',
+      // @ts-ignore
+      onChange: (edit: IDomEditor) => {
+        setValues({
+          content: edit.getHtml()
+        })
+      }
+    },
+    label: t('exampleDemo.content')
+  }
+])
+
+const rules = reactive({
+  title: [required()],
+  author: [required()],
+  importance: [required()],
+  pageviews: [required()],
+  display_time: [required()],
+  content: [required()]
+})
+
+const submit = async () => {
+  const elForm = await getElFormExpose()
+  const valid = await elForm?.validate().catch((err) => {
+    console.log(err)
+  })
+  if (valid) {
+    const formData = await getFormData()
+    return formData
+  }
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+    setSchema([
+      {
+        field: 'content',
+        path: 'componentProps.defaultHtml',
+        value: currentRow.content
+      }
+    ])
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="schema" />
+</template>

+ 53 - 0
src/views/Manage/Product/ProductAdd.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import Write from './components/Write.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref, unref } from 'vue'
+import { ElButton } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter } from 'vue-router'
+import { saveTableApi } from '@/api/table'
+import { useEmitt } from '@/hooks/event/useEmitt'
+
+const { emitter } = useEmitt()
+
+const { push, go } = useRouter()
+
+const { t } = useI18n()
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+
+const loading = ref(false)
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    loading.value = true
+    const res = await saveTableApi(formData)
+      .catch(() => {})
+      .finally(() => {
+        loading.value = false
+      })
+    if (res) {
+      emitter.emit('getList', 'add')
+      push('/manage/product-page')
+    }
+  }
+}
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.add')" @back="push('/manage/product-page')">
+    <Write ref="writeRef" />
+
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+      <ElButton type="primary" :loading="loading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+    </template>
+  </ContentDetailWrap>
+</template>
+@/hooks/event/useEmitt

+ 38 - 0
src/views/Manage/Product/ProductDetail.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import Detail from './components/Detail.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter, useRoute } from 'vue-router'
+import { getTableDetApi } from '@/api/table'
+import { TableData } from '@/api/table/types'
+import { ElButton } from 'element-plus'
+
+const { push, go } = useRouter()
+
+const { query } = useRoute()
+
+const { t } = useI18n()
+
+const currentRow = ref<Nullable<TableData>>(null)
+
+const getTableDet = async () => {
+  const res = await getTableDetApi(query.id as string)
+  if (res) {
+    currentRow.value = res.data
+  }
+}
+
+getTableDet()
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.detail')" @back="push('/manage/product-page')">
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+    </template>
+    <Detail :current-row="currentRow" />
+  </ContentDetailWrap>
+</template>

+ 67 - 0
src/views/Manage/Product/ProductEdit.vue

@@ -0,0 +1,67 @@
+<script setup lang="ts">
+import Write from './components/Write.vue'
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+import { ref, unref } from 'vue'
+import { ElButton } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter, useRoute } from 'vue-router'
+import { saveTableApi, getTableDetApi } from '@/api/table'
+import { TableData } from '@/api/table/types'
+import { useEmitt } from '@/hooks/event/useEmitt'
+
+const { emitter } = useEmitt()
+
+const { push, go } = useRouter()
+
+const { query } = useRoute()
+
+const { t } = useI18n()
+
+const currentRow = ref<Nullable<TableData>>(null)
+
+const getTableDet = async () => {
+  const res = await getTableDetApi(query.id as string)
+  if (res) {
+    currentRow.value = res.data
+  }
+}
+
+getTableDet()
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+
+const loading = ref(false)
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    loading.value = true
+    const res = await saveTableApi(formData)
+      .catch(() => {})
+      .finally(() => {
+        loading.value = false
+      })
+    if (res) {
+      emitter.emit('getList', 'editor')
+      push('/manage/product-page')
+    }
+  }
+}
+</script>
+
+<template>
+  <ContentDetailWrap :title="t('exampleDemo.edit')" @back="push('/manage/product-page')">
+    <Write ref="writeRef" :current-row="currentRow" />
+
+    <template #header>
+      <ElButton @click="go(-1)">
+        {{ t('common.back') }}
+      </ElButton>
+      <ElButton type="primary" :loading="loading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+    </template>
+  </ContentDetailWrap>
+</template>
+@/hooks/event/useEmitt

+ 347 - 0
src/views/Manage/Product/ProductPage.vue

@@ -0,0 +1,347 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTag } from 'element-plus'
+import { Table } from '@/components/Table'
+import { getTableListApi, delTableListApi } from '@/api/table'
+import { useTable } from '@/hooks/web/useTable'
+import { TableData } from '@/api/table/types'
+import { ref, unref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useEmitt } from '@/hooks/event/useEmitt'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+import { TableSetting } from '@/components/TableSetting'
+import { usePageStore } from '@/store/modules/page'
+import { set } from 'lodash-es'
+
+defineOptions({
+  name: 'ProductPage'
+})
+
+const { push } = useRouter()
+
+const ids = ref<string[]>([])
+
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  searchParams.value = params
+  getList()
+}
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { currentPage, pageSize } = tableState
+    const res = await getTableListApi({
+      pageIndex: unref(currentPage),
+      pageSize: unref(pageSize),
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: res.data.total
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(ids))
+    return !!res
+  }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+const { getList, getElTableExpose, delList, setColumn } = tableMethods
+
+getList()
+
+useEmitt({
+  name: 'getList',
+  callback: (type: string) => {
+    if (type === 'add') {
+      currentPage.value = 1
+    }
+    getList()
+  }
+})
+
+const { t } = useI18n()
+const appStore = usePageStore()
+const crudSchemas: CrudSchema[] = [
+  {
+    field: 'selection',
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      type: 'selection',
+      hidden: false
+    }
+  },
+  {
+    field: 'index',
+    label: t('tableDemo.index'),
+    type: 'index',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    }
+  },
+  {
+    field: 'title',
+    label: t('tableDemo.title'),
+    search: {
+      component: 'Input'
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'Input',
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24
+    }
+  },
+  {
+    field: 'author',
+    label: t('tableDemo.author'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    }
+  },
+  {
+    field: 'display_time',
+    label: t('tableDemo.displayTime'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'datetime',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss'
+      }
+    }
+  },
+  {
+    field: 'importance',
+    label: t('tableDemo.importance'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'Select',
+      componentProps: {
+        style: {
+          width: '100%'
+        },
+        options: [
+          {
+            label: '重要',
+            value: 3
+          },
+          {
+            label: '良好',
+            value: 2
+          },
+          {
+            label: '一般',
+            value: 1
+          }
+        ]
+      }
+    },
+    detail: {
+      slots: {
+        default: (data: any) => {
+          return (
+            <ElTag
+              type={
+                data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'
+              }
+            >
+              {data.importance === 1
+                ? t('tableDemo.important')
+                : data.importance === 2
+                ? t('tableDemo.good')
+                : t('tableDemo.commonly')}
+            </ElTag>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'pageviews',
+    label: t('tableDemo.pageviews'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      component: 'InputNumber',
+      value: 0
+    }
+  },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: true
+    },
+    form: {
+      component: 'Editor',
+      colProps: {
+        span: 24
+      }
+    },
+    detail: {
+      span: 24,
+      slots: {
+        default: (data: any) => {
+          return <div innerHTML={data.content}></div>
+        }
+      }
+    }
+  },
+  {
+    field: 'action',
+    width: '260px',
+    label: t('tableDemo.action'),
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      hidden: false,
+      slots: {
+        default: (data: any) => {
+          return (
+            <>
+              <ElButton type="primary" onClick={() => action(data[0].row, 'edit')}>
+                {t('exampleDemo.edit')}
+              </ElButton>
+              <ElButton type="success" onClick={() => action(data[0].row, 'detail')}>
+                {t('exampleDemo.detail')}
+              </ElButton>
+              <ElButton type="danger" onClick={() => delData(data[0].row)}>
+                {t('exampleDemo.del')}
+              </ElButton>
+            </>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @ts-ignore
+const getSchemas = () => {
+  let localSchemas = appStore.getPageData['ProductPage']
+  if (localSchemas && localSchemas.schemas) {
+    let localSchemasArr = localSchemas.schemas
+    for (let i = 0; i < localSchemasArr.length; i++) {
+      let item = localSchemasArr[i]
+      let index = crudSchemas.findIndex((e) => {
+        return e.field == item.field
+      })
+      if (index > 0) {
+        set(crudSchemas[index], 'table.hidden', item.table.hidden)
+      }
+    }
+  }
+}
+getSchemas()
+let allSchemas = useCrudSchemas(crudSchemas).allSchemas
+// 修改列设置后调用
+const setSchemas = (schemas: CrudSchema[]) => {
+  let arr = schemas.map((item) => {
+    return {
+      field: item.field,
+      path: 'hidden',
+      value: item.table ? item.table.hidden : false
+    }
+  })
+  setColumn(arr)
+}
+
+const AddAction = () => {
+  push('/manage/product-add')
+}
+
+const delLoading = ref(false)
+
+const delData = async (row: TableData | null) => {
+  const elTableExpose = await getElTableExpose()
+  ids.value = row ? [row.id] : elTableExpose?.getSelectionRows().map((v: TableData) => v.id) || []
+  delLoading.value = true
+  await delList(unref(ids).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const action = (row: TableData, type: string) => {
+  push(`/manage/product-${type}?id=${row.id}`)
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
+      <ElButton :loading="delLoading" type="danger" @click="delData(null)">
+        {{ t('exampleDemo.del') }}
+      </ElButton>
+      <TableSetting page="ProductPage" :data="crudSchemas" @set-schemas="setSchemas" />
+    </div>
+
+    <Table
+      v-model:pageSize="pageSize"
+      v-model:currentPage="currentPage"
+      :columns="allSchemas.tableColumns"
+      :data="dataList"
+      :loading="loading"
+      :pagination="{
+        total: total
+      }"
+      @register="tableRegister"
+    />
+  </ContentWrap>
+</template>
+@/hooks/event/useEmitt

+ 69 - 0
src/views/Manage/Product/components/Detail.vue

@@ -0,0 +1,69 @@
+<script setup lang="tsx">
+import { PropType, reactive } from 'vue'
+import type { TableData } from '@/api/table/types'
+import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElTag } from 'element-plus'
+
+const { t } = useI18n()
+
+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'),
+    slots: {
+      default: (data: any) => {
+        return (
+          <ElTag
+            type={data.importance === 1 ? 'success' : data.importance === 2 ? 'warning' : 'danger'}
+          >
+            {data.importance === 1
+              ? t('tableDemo.important')
+              : data.importance === 2
+              ? t('tableDemo.good')
+              : t('tableDemo.commonly')}
+          </ElTag>
+        )
+      }
+    }
+  },
+  {
+    field: 'pageviews',
+    label: t('exampleDemo.pageviews')
+  },
+  {
+    field: 'content',
+    label: t('exampleDemo.content'),
+    span: 24,
+    slots: {
+      default: (data: any) => {
+        return <div innerHTML={data.content}></div>
+      }
+    }
+  }
+])
+</script>
+
+<template>
+  <Descriptions :schema="schema" :data="currentRow || {}" />
+</template>

+ 157 - 0
src/views/Manage/Product/components/Write.vue

@@ -0,0 +1,157 @@
+<script setup lang="ts">
+import { Form, FormSchema } 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 { useValidator } from '@/hooks/web/useValidator'
+import { IDomEditor } from '@wangeditor/editor'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<Nullable<TableData>>,
+    default: () => null
+  }
+})
+
+const { t } = useI18n()
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
+
+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()]
+    },
+    componentProps: {
+      placeholder: '请输入作者'
+    }
+  },
+  {
+    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: '',
+      // @ts-ignore
+      onChange: (edit: IDomEditor) => {
+        setValues({
+          content: edit.getHtml()
+        })
+      }
+    },
+    label: t('exampleDemo.content')
+  }
+])
+
+const rules = reactive({
+  title: [required()],
+  author: [required()],
+  importance: [required()],
+  pageviews: [required()],
+  display_time: [required()],
+  content: [required()]
+})
+
+const submit = async () => {
+  const elForm = await getElFormExpose()
+  const valid = await elForm?.validate().catch((err) => {
+    console.log(err)
+  })
+  if (valid) {
+    const formData = await getFormData()
+    return formData
+  }
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+    setSchema([
+      {
+        field: 'content',
+        path: 'componentProps.defaultHtml',
+        value: currentRow.content
+      }
+    ])
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="schema" />
+</template>