10 Комити d9b59e54dc ... d6b27dd54f

Аутор SHA1 Порука Датум
  王飞 d6b27dd54f 调整 文件管理,图片管理 пре 1 година
  王飞 4adda2eec3 Update Detail.vue пре 1 година
  王飞 a1b56fe5a3 添加字典管理页面 пре 1 година
  王飞 6a06a62c1d 处理报错 пре 1 година
  王飞 fc21d69c0c 添加公司管理、图片管理 пре 1 година
  王飞 ce65532928 产品管理调整页面 пре 1 година
  王飞 11da2af2f0 文件管理模块 пре 1 година
  王飞 b805e9f8df Merge branch 'v2.0' into websiteManagement пре 1 година
  王飞 256f762954 处理 产品管理页面 пре 1 година
  王飞 52e8061627 用户接口测试 пре 1 година
55 измењених фајлова са 3419 додато и 978 уклоњено
  1. 1 1
      .env.base
  2. 1 1
      .env.dev
  3. 1 1
      .env.gitee
  4. 1 1
      .env.pro
  5. 1 1
      .env.test
  6. 4 0
      src/api/common/index.ts
  7. 28 1
      src/api/department/index.ts
  8. 32 2
      src/api/department/types.ts
  9. 3 3
      src/api/login/index.ts
  10. 2 2
      src/api/login/types.ts
  11. 18 0
      src/api/manage/company.ts
  12. 22 0
      src/api/manage/dict.ts
  13. 18 0
      src/api/manage/file.ts
  14. 18 0
      src/api/manage/img.ts
  15. 22 0
      src/api/manage/news.ts
  16. 22 0
      src/api/manage/product.ts
  17. 76 0
      src/api/manage/types.ts
  18. 13 0
      src/api/table/types.ts
  19. 1 0
      src/components/Backtop/src/Backtop.vue
  20. 8 2
      src/components/TableSetting/src/TableSetting.vue
  21. 10 7
      src/components/UserInfo/src/UserInfo.vue
  22. 30 7
      src/config/axios/config.ts
  23. 1 1
      src/config/axios/service.ts
  24. 1 1
      src/config/axios/types/index.ts
  25. 0 1
      src/hooks/web/useTable.ts
  26. 70 48
      src/router/index.ts
  27. 314 0
      src/views/Authorization/Dict/Dict.vue
  28. 278 0
      src/views/Authorization/Dict/components/Detail.vue
  29. 58 0
      src/views/Authorization/Dict/components/Write.vue
  30. 120 137
      src/views/Authorization/User/User.vue
  31. 5 8
      src/views/Authorization/User/components/Write.vue
  32. 10 233
      src/views/Dashboard/Workplace.vue
  33. 4 6
      src/views/Login/Login.vue
  34. 60 64
      src/views/Login/components/LoginForm.vue
  35. 469 0
      src/views/Manage/Company/CompanyPage.vue
  36. 66 0
      src/views/Manage/Company/components/Write.vue
  37. 453 0
      src/views/Manage/File/FilePage.vue
  38. 66 0
      src/views/Manage/File/components/Write.vue
  39. 419 0
      src/views/Manage/Img/ImgPage.vue
  40. 80 0
      src/views/Manage/Img/components/Write.vue
  41. 1 1
      src/views/Manage/News/NewsAdd.vue
  42. 3 3
      src/views/Manage/News/NewsDetail.vue
  43. 4 4
      src/views/Manage/News/NewsEdit.vue
  44. 123 124
      src/views/Manage/News/NewsPage.vue
  45. 22 26
      src/views/Manage/News/components/Detail.vue
  46. 191 54
      src/views/Manage/News/components/Write.vue
  47. 1 1
      src/views/Manage/Product/ProductAdd.vue
  48. 3 3
      src/views/Manage/Product/ProductDetail.vue
  49. 4 4
      src/views/Manage/Product/ProductEdit.vue
  50. 50 126
      src/views/Manage/Product/ProductPage.vue
  51. 32 30
      src/views/Manage/Product/components/Detail.vue
  52. 160 56
      src/views/Manage/Product/components/Write.vue
  53. 3 3
      src/views/hooks/useTagsView.vue
  54. 2 1
      types/global.d.ts
  55. 14 14
      vite.config.ts

+ 1 - 1
.env.base

@@ -8,4 +8,4 @@ VITE_API_BASE_PATH=base
 VITE_BASE_PATH=/
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=网站后台管理

+ 1 - 1
.env.dev

@@ -20,4 +20,4 @@ VITE_SOURCEMAP=true
 VITE_OUT_DIR=dist-dev
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=网站后台管理

+ 1 - 1
.env.gitee

@@ -20,4 +20,4 @@ VITE_SOURCEMAP=false
 VITE_OUT_DIR=dist-pro
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=网站后台管理

+ 1 - 1
.env.pro

@@ -20,4 +20,4 @@ VITE_SOURCEMAP=false
 VITE_OUT_DIR=dist-pro
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=网站后台管理

+ 1 - 1
.env.test

@@ -20,4 +20,4 @@ VITE_SOURCEMAP=true
 VITE_OUT_DIR=dist-test
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=网站后台管理

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

@@ -9,3 +9,7 @@ export const getDictApi = () => {
 export const getDictOneApi = async () => {
   return request.get({ url: '/dict/one' })
 }
+
+export const uploadFile = async (data: any) => {
+  return request.post({ url: '/api/sysApk/uploadFile', headersType: 'multipart/form-data', data })
+}

+ 28 - 1
src/api/department/index.ts

@@ -1,5 +1,12 @@
 import request from '@/config/axios'
-import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types'
+import {
+  DepartmentListResponse,
+  DepartmentUserParams,
+  DepartmentUserResponse,
+  UserParams,
+  SysRole,
+  SysUser
+} from './types'
 
 export const getDepartmentApi = () => {
   return request.get<DepartmentListResponse>({ url: '/department/list' })
@@ -28,3 +35,23 @@ export const deleteDepartmentApi = (ids: string[] | number[]) => {
 export const getDepartmentTableApi = (params: any) => {
   return request.get({ url: '/department/table/list', params })
 }
+
+export const getUserList = (data: UserParams) => {
+  return request.post<DepartmentUserResponse>({ url: '/api/sysUser/page', data })
+}
+
+export const deleteUserById = (id: number) => {
+  return request.delete({ url: `/api/sysUser/${id}` })
+}
+
+export const getRoleApi = () => {
+  return request.post<SysRole[]>({ url: '/api/sysRole/list' })
+}
+
+export const addUser = (data: any) => {
+  return request.post({ url: '/api/sysUser/add', data })
+}
+
+export const updataUser = (data: any) => {
+  return request.put({ url: '/api/sysUser', data })
+}

+ 32 - 2
src/api/department/types.ts

@@ -17,7 +17,7 @@ export interface DepartmentUserParams {
 }
 
 export interface DepartmentUserItem {
-  id: string
+  id: number
   username: string
   account: string
   email: string
@@ -28,5 +28,35 @@ export interface DepartmentUserItem {
 
 export interface DepartmentUserResponse {
   list: DepartmentUserItem[]
-  total: number
+  totalCount: number
+}
+
+export interface UserParams {
+  pageSize: number
+  pageNum: number
+  name?: string
+  account?: string
+}
+
+
+export interface UserResponse {
+  list: DepartmentUserItem[]
+  totalCount: number
+  totalPage: number
+  pageSize: number
+  currPage: number
+}
+
+export interface SysRole {
+  id: number
+  roleName: string
+  createTime: string
+  roleDesc: string
+}
+
+export interface SysUser {
+  id?: number
+  account: string
+  name: string
+  password: string
 }

+ 3 - 3
src/api/login/index.ts

@@ -6,11 +6,11 @@ interface RoleParams {
 }
 
 export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
-  return request.post({ url: '/user/login', data })
+  return request.post({ url: '/api/login/login', data, headersType: 'application/json' })
 }
 
-export const loginOutApi = (): Promise<IResponse> => {
-  return request.get({ url: '/user/loginOut' })
+export const loginOutApi = (userId: string): Promise<IResponse> => {
+  return request.get({ url: `/api/login/logout/${userId}` })
 }
 
 export const getUserListApi = ({ params }: AxiosConfig) => {

+ 2 - 2
src/api/login/types.ts

@@ -1,10 +1,10 @@
 export type UserLoginType = {
-  username: string
+  account: string
   password: string
 }
 
 export type UserType = {
-  username: string
+  account: string
   password: string
   role: string
   roleId: string

+ 18 - 0
src/api/manage/company.ts

@@ -0,0 +1,18 @@
+import request from '@/config/axios'
+// import type { FileData } from './types'
+
+export const getTableListApi = (data: any) => {
+  return request.post({ url: '/api/company/page', data })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.delete({ url: `/api/company/${id}` })
+}
+
+export const saveTableApi = (data: any) => {
+  return request.post({ url: '/api/company', data })
+}
+
+export const updateTableApi = (data: any) => {
+  return request.put({ url: '/api/company', data })
+}

+ 22 - 0
src/api/manage/dict.ts

@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+// import type { FileData } from './types'
+
+export const getTree = (data?: any) => {
+  return request.post({ url: '/api/sysDictionary/showTree', data })
+}
+
+export const getDict = (data: any) => {
+  return request.post({ url: '/api/sysDictionary/page', data })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.delete({ url: `/api/sysDictionary/${id}` })
+}
+
+export const saveTableApi = (data: any) => {
+  return request.post({ url: '/api/sysDictionary', data })
+}
+
+export const updateTableApi = (data: any) => {
+  return request.put({ url: '/api/sysDictionary', data })
+}

+ 18 - 0
src/api/manage/file.ts

@@ -0,0 +1,18 @@
+import request from '@/config/axios'
+// import type { FileData } from './types'
+
+export const getTableListApi = (data: any) => {
+  return request.post({ url: '/api/sysApk/page', data })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.post({ url: `/api/sysApk/deleteApkById/${id}` })
+}
+
+export const saveTableApi = (data: any) => {
+  return request.post({ url: '/api/sysApk/addSysApk', data })
+}
+
+export const updateTableApi = (data: any) => {
+  return request.post({ url: '/api/sysApk/updateSysApk', data })
+}

+ 18 - 0
src/api/manage/img.ts

@@ -0,0 +1,18 @@
+import request from '@/config/axios'
+// import type { FileData } from './types'
+
+export const getTableListApi = (data: any) => {
+  return request.post({ url: '/api/banner/page', data })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.delete({ url: `/api/banner/${id}` })
+}
+
+export const saveTableApi = (data: any) => {
+  return request.post({ url: '/api/banner', data })
+}
+
+export const updateTableApi = (data: any) => {
+  return request.put({ url: '/api/banner', data })
+}

+ 22 - 0
src/api/manage/news.ts

@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+import type { NewsTableData } from './types'
+
+export const getTableListApi = (data: any) => {
+  return request.post({ url: '/api/article/page', data })
+}
+
+export const saveTableApi = (data: Partial<NewsTableData>): Promise<IResponse> => {
+  return request.post({ url: '/api/article', data })
+}
+
+export const updateTableApi = (data: Partial<NewsTableData>): Promise<IResponse> => {
+  return request.put({ url: '/api/article', data })
+}
+
+export const getTableDetApi = (id: string): Promise<IResponse<NewsTableData>> => {
+  return request.get({ url: `/api/article/${id}` })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.delete({ url: `/api/article/${id}` })
+}

+ 22 - 0
src/api/manage/product.ts

@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+import type { ProductTableData } from './types'
+
+export const getTableListApi = (data: any) => {
+  return request.post({ url: '/api/product/page', data })
+}
+
+export const saveTableApi = (data: Partial<ProductTableData>): Promise<IResponse> => {
+  return request.post({ url: '/api/product', data })
+}
+
+export const updateTableApi = (data: Partial<ProductTableData>): Promise<IResponse> => {
+  return request.put({ url: '/api/product', data })
+}
+
+export const getTableDetApi = (id: string): Promise<IResponse<ProductTableData>> => {
+  return request.get({ url: `/api/product/${id}` })
+}
+
+export const delTableListApi = (id: string | number): Promise<IResponse> => {
+  return request.delete({ url: `/api/product/${id}` })
+}

+ 76 - 0
src/api/manage/types.ts

@@ -0,0 +1,76 @@
+export type FileData = {
+  id?: string
+  virtualPath: string
+  updateTime?: string
+  fileName?: string
+  filePath?: string
+}
+
+export type ProductTableData = {
+  id?: string
+  productName: string
+  productPic: string
+  productSpecifications?: string
+  productIntroduction?: string
+  productDetailed?: string
+  productCharacteristic?: string
+  productPrice: number
+  productStock: number
+  remark: string
+}
+
+export type NewsTableData = {
+  id?: string
+  articleContent: string
+  articlePic: string
+  articleTitle: string
+  categoryId?: string
+  articleViews: number
+  introduce: string
+  remark: string
+  updateTime: string
+  videoUrl: string
+  weight: number
+}
+
+export type CompanyData = {
+  id?: string
+  companyAddress?: string
+  companyBrief?: string
+  companyEmail?: string
+  companyName: string
+  companyPhone?: string
+  isActive?: string
+  name?: string
+  phone?: string
+  wechat?: string
+  wechatImg: string
+}
+
+export type ImgData = {
+  id?: string
+  bannerUrl: string
+  updateTime?: string
+  bannerType?: string
+  bannerTarget?: string
+}
+
+export type DictData = {
+  id?: string
+  code?: string
+  name?: string
+  pid?: string
+  showText?: string
+  description?: number
+  isActive?: string
+}
+
+export type DictRowData = {
+  id: string
+  code: string
+  name: string
+  pid?: string
+  showText?: string
+  description?: number
+  isActive: string
+}

+ 13 - 0
src/api/table/types.ts

@@ -7,3 +7,16 @@ export type TableData = {
   display_time: string
   pageviews: number
 }
+
+export type ProductTableData = {
+  id?: string
+  productName: string
+  productPic: string
+  productSpecifications: string
+  productIntroduction: string
+  productDetailed: string
+  productCharacteristic: string
+  productPrice: number
+  productStock: number
+  remark: string
+}

+ 1 - 0
src/components/Backtop/src/Backtop.vue

@@ -10,6 +10,7 @@ const prefixCls = getPrefixCls('backtop')
 <template>
   <ElBacktop
     :class="`${prefixCls}-backtop`"
+    style="z-index: 100"
     :target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
   />
 </template>

+ 8 - 2
src/components/TableSetting/src/TableSetting.vue

@@ -26,10 +26,16 @@ const columns: TableColumn[] = [
   },
   {
     field: 'table.hidden',
-    label: '隐藏',
+    label: '展示',
     slots: {
       default: (data: any) => {
-        return <ElSwitch v-model={data[0].row.table.hidden}></ElSwitch>
+        return (
+          <ElSwitch
+            v-model={data.row.table.hidden}
+            activeValue={false}
+            inactiveValue={true}
+          ></ElSwitch>
+        )
       }
     }
   }

+ 10 - 7
src/components/UserInfo/src/UserInfo.vue

@@ -11,6 +11,7 @@ import LockDialog from './components/LockDialog.vue'
 import { ref, computed } from 'vue'
 import LockPage from './components/LockPage.vue'
 import { useLockStore } from '@/store/modules/lock'
+import { useAppStore } from '@/store/modules/app'
 
 const lockStore = useLockStore()
 
@@ -21,13 +22,13 @@ const tagsViewStore = useTagsViewStore()
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('user-info')
-
+const appStore = useAppStore()
 const { t } = useI18n()
 
-const { clear } = useStorage()
+const { clear, getStorage } = useStorage()
 
 const { replace } = useRouter()
-
+const userInfo = ref(getStorage(appStore.userInfo))
 const loginOut = () => {
   ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
     confirmButtonText: t('common.ok'),
@@ -35,7 +36,7 @@ const loginOut = () => {
     type: 'warning'
   })
     .then(async () => {
-      const res = await loginOutApi().catch(() => {})
+      const res = await loginOutApi(userInfo.value.id).catch(() => {})
       if (res) {
         clear()
         tagsViewStore.delAllViews()
@@ -61,12 +62,14 @@ const lockScreen = () => {
 <template>
   <ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
     <div class="flex items-center">
-      <img
+      <!-- <img
         src="@/assets/imgs/avatar.jpg"
         alt=""
         class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
-      />
-      <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">Archer</span>
+      /> -->
+      <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
+        userInfo.name
+      }}</span>
     </div>
     <template #dropdown>
       <ElDropdownMenu>

+ 30 - 7
src/config/axios/config.ts

@@ -5,8 +5,15 @@ import {
   AxiosError,
   InternalAxiosRequestConfig
 } from './types'
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import qs from 'qs'
+import { useStorage } from '@/hooks/web/useStorage'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { resetRouter } from '@/router'
+import router from '@/router'
+
+const { getStorage, clear } = useStorage()
+const tagsViewStore = useTagsViewStore()
 
 const config: AxiosConfig = {
   /**
@@ -20,7 +27,7 @@ const config: AxiosConfig = {
     dev: '',
 
     // 打包生产环境接口前缀
-    pro: '',
+    pro: 'http://api.dacundianzi.com',
 
     // 打包测试环境接口前缀
     test: ''
@@ -29,7 +36,7 @@ const config: AxiosConfig = {
   /**
    * 接口成功返回状态码
    */
-  code: 0,
+  code: '200',
 
   /**
    * 接口请求超时时间
@@ -40,7 +47,7 @@ const config: AxiosConfig = {
    * 默认接口请求类型
    * 可选值:application/x-www-form-urlencoded multipart/form-data
    */
-  defaultHeaders: 'application/json',
+  defaultHeaders: 'application/x-www-form-urlencoded',
 
   interceptors: {
     //请求拦截
@@ -74,21 +81,37 @@ const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
     config.params = {}
     config.url = url
   }
-  console.log(config)
+  if (getStorage('token')) {
+    config.headers.token = getStorage('token')
+  }
   return config
 }
 ;(error: AxiosError) => {
-  console.log(error)
   Promise.reject(error)
 }
+let isReLogin = false
 
 const defaultResponseInterceptors = (response: AxiosResponse<any>) => {
-  console.log(response)
   if (response?.config?.responseType === 'blob') {
     // 如果是文件流,直接过
     return response
   } else if (response.data.code === config.code) {
     return response.data
+  } else if (response.data.code === 'A0000A') {
+    if (!isReLogin) {
+      isReLogin = true
+      ElMessageBox.alert(response.data.message, '提示', {
+        showClose: false,
+        callback: () => {
+          clear()
+          tagsViewStore.delAllViews()
+          resetRouter() // 重置静态路由表
+          router.replace('/login')
+          isReLogin = false
+          location.reload()
+        }
+      })
+    }
   } else {
     ElMessage.error(response.data.message)
   }

+ 1 - 1
src/config/axios/service.ts

@@ -27,7 +27,7 @@ axiosInstance.interceptors.response.use(
   (res: AxiosResponse) => {
     const url = res.config.url || ''
     abortControllerMap.delete(url)
-    return res.data
+    return res
   },
   (err: any) => err
 )

+ 1 - 1
src/config/axios/types/index.ts

@@ -22,7 +22,7 @@ interface AxiosConfig<T = AxiosResponse> {
     pro: string
     test: string
   }
-  code: number
+  code: number|string
   defaultHeaders: AxiosHeaders
   timeout: number
   interceptors: RequestInterceptors<T>

+ 0 - 1
src/hooks/web/useTable.ts

@@ -80,7 +80,6 @@ export const useTable = (config: UseTableConfig) => {
       loading.value = true
       try {
         const res = await config?.fetchDataApi()
-        console.log('fetchDataApi res', res)
         if (res) {
           dataList.value = res.list
           total.value = res.total || 0

+ 70 - 48
src/router/index.ts

@@ -1,4 +1,4 @@
-import { createRouter, createWebHistory } from 'vue-router'
+import { createRouter, createWebHashHistory } from 'vue-router'
 import type { RouteRecordRaw } from 'vue-router'
 import type { App } from 'vue'
 import { Layout, getParentLayout } from '@/utils/routerHelper'
@@ -10,7 +10,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
   {
     path: '/',
     component: Layout,
-    redirect: '/dashboard/analysis',
+    redirect: '/dashboard/workplace',
     name: 'Root',
     meta: {
       hidden: true
@@ -59,7 +59,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
   {
     path: '/dashboard',
     component: Layout,
-    redirect: '/dashboard/analysis',
+    redirect: '/dashboard/workplace',
     name: 'Dashboard',
     meta: {
       title: t('router.dashboard'),
@@ -67,16 +67,6 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
       alwaysShow: true
     },
     children: [
-      {
-        path: 'analysis',
-        component: () => import('@/views/Dashboard/Analysis.vue'),
-        name: 'Analysis',
-        meta: {
-          title: t('router.analysis'),
-          noCache: true,
-          affix: true
-        }
-      },
       {
         path: 'workplace',
         component: () => import('@/views/Dashboard/Workplace.vue'),
@@ -105,7 +95,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         component: () => import('@/views/Manage/News/NewsPage.vue'),
         name: 'NewsPage',
         meta: {
-          title: '新闻管理'
+          title: '文案管理'
         }
       },
       {
@@ -113,7 +103,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         component: () => import('@/views/Manage/News/NewsAdd.vue'),
         name: 'NewsAdd',
         meta: {
-          title: '新增新闻',
+          title: '新增文案',
           noTagsView: true,
           noCache: true,
           hidden: true,
@@ -126,7 +116,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         component: () => import('@/views/Manage/News/NewsEdit.vue'),
         name: 'NewsEdit',
         meta: {
-          title: '编辑新闻',
+          title: '编辑文案',
           noTagsView: true,
           noCache: true,
           hidden: true,
@@ -139,7 +129,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         component: () => import('@/views/Manage/News/NewsDetail.vue'),
         name: 'NewsDetail',
         meta: {
-          title: '新闻详情',
+          title: '文案详情',
           noTagsView: true,
           noCache: true,
           hidden: true,
@@ -193,6 +183,30 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/manage/product-page'
         }
+      },
+      {
+        path: 'file-page',
+        component: () => import('@/views/Manage/File/FilePage.vue'),
+        name: 'FilePage',
+        meta: {
+          title: '文件管理'
+        }
+      },
+      {
+        path: 'img-page',
+        component: () => import('@/views/Manage/Img/ImgPage.vue'),
+        name: 'ImgPage',
+        meta: {
+          title: '图片管理'
+        }
+      },
+      {
+        path: 'company-page',
+        component: () => import('@/views/Manage/Company/CompanyPage.vue'),
+        name: 'CompanyPage',
+        meta: {
+          title: '公司信息管理'
+        }
       }
     ]
   },
@@ -202,19 +216,19 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
     redirect: '/authorization/user',
     name: 'Authorization',
     meta: {
-      title: t('router.authorization'),
+      title: '系统管理',
       icon: 'eos-icons:role-binding',
       alwaysShow: true
     },
     children: [
-      {
-        path: 'department',
-        component: () => import('@/views/Authorization/Department/Department.vue'),
-        name: 'Department',
-        meta: {
-          title: t('router.department')
-        }
-      },
+      // {
+      //   path: 'department',
+      //   component: () => import('@/views/Authorization/Department/Department.vue'),
+      //   name: 'Department',
+      //   meta: {
+      //     title: t('router.department')
+      //   }
+      // },
       {
         path: 'user',
         component: () => import('@/views/Authorization/User/User.vue'),
@@ -224,36 +238,44 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'menu',
-        component: () => import('@/views/Authorization/Menu/Menu.vue'),
-        name: 'Menu',
-        meta: {
-          title: t('router.menuManagement')
-        }
-      },
-      {
-        path: 'role',
-        component: () => import('@/views/Authorization/Role/Role.vue'),
-        name: 'Role',
-        meta: {
-          title: t('router.role')
-        }
-      },
-      {
-        path: 'test',
-        component: () => import('@/views/Authorization/Test/Test.vue'),
-        name: 'Test',
+        path: 'dict',
+        component: () => import('@/views/Authorization/Dict/Dict.vue'),
+        name: 'Dict',
         meta: {
-          title: t('router.permission'),
-          permission: ['add', 'edit', 'delete']
+          title: '字典管理'
         }
       }
+      // {
+      //   path: 'menu',
+      //   component: () => import('@/views/Authorization/Menu/Menu.vue'),
+      //   name: 'Menu',
+      //   meta: {
+      //     title: t('router.menuManagement')
+      //   }
+      // },
+      // {
+      //   path: 'role',
+      //   component: () => import('@/views/Authorization/Role/Role.vue'),
+      //   name: 'Role',
+      //   meta: {
+      //     title: t('router.role')
+      //   }
+      // },
+      // {
+      //   path: 'test',
+      //   component: () => import('@/views/Authorization/Test/Test.vue'),
+      //   name: 'Test',
+      //   meta: {
+      //     title: t('router.permission'),
+      //     permission: ['add', 'edit', 'delete']
+      //   }
+      // }
     ]
   }
 ]
 
 const router = createRouter({
-  history: createWebHistory(),
+  history: createWebHashHistory(),
   strict: true,
   routes: constantRouterMap as RouteRecordRaw[],
   scrollBehavior: () => ({ left: 0, top: 0 })

+ 314 - 0
src/views/Authorization/Dict/Dict.vue

@@ -0,0 +1,314 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table } from '@/components/Table'
+import { ref, unref, reactive } from 'vue'
+import { ElButton, ElButtonGroup, ElTooltip } from 'element-plus'
+import { getTree, saveTableApi, delTableListApi, updateTableApi } from '@/api/manage/dict'
+import type { DictData, DictRowData } from '@/api/manage/types'
+import { useTable } from '@/hooks/web/useTable'
+import { Search } from '@/components/Search'
+import Write from './components/Write.vue'
+import Detail from './components/Detail.vue'
+import { Dialog } from '@/components/Dialog'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+import { useIcon } from '@/hooks/web/useIcon'
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const DetailIcon = useIcon({ icon: 'ep:document' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const res = await getTree({
+      pid: '',
+      ...unref(searchParams)
+    })
+    return {
+      list:
+        res.data.map((e: DictRowData) => {
+          return {
+            ...e,
+            hasChildren: true
+          }
+        }) || []
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(id))
+    return !!res
+  }
+})
+const { loading, dataList, pageSize, currentPage } = tableState
+const { getList, delList } = tableMethods
+
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    field: 'name',
+    label: '名称'
+  },
+  {
+    field: 'code',
+    label: '编码',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'showText',
+    label: '显示文字',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'value',
+    label: '值',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'isActive',
+    label: '是否启用',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      component: 'Select',
+      componentProps: {
+        options: [
+          {
+            label: '启用',
+            value: '1'
+          },
+          {
+            label: '禁用',
+            value: '0'
+          }
+        ]
+      }
+    },
+    form: {
+      component: 'Switch',
+      value: '1',
+      componentProps: {
+        activeText: '启用',
+        inactiveText: '禁用',
+        activeValue: '1',
+        inactiveValue: '0'
+      }
+    }
+  },
+  {
+    field: 'description',
+    label: '备注',
+    form: {
+      componentProps: {
+        type: 'textarea'
+      },
+      colProps: {
+        span: 24
+      }
+    },
+    search: {
+      hidden: true
+    }
+  },
+
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    search: {
+      hidden: true
+    },
+    table: {
+      width: 160,
+      fixed: 'right',
+      slots: {
+        default: (data: any) => {
+          const row = data.row as DictRowData
+          return (
+            <>
+              <ElButtonGroup>
+                <ElTooltip content="编辑">
+                  <ElButton text icon={EditIcon} onClick={() => action(row, 'edit')} />
+                </ElTooltip>
+                <ElTooltip content="管理子项">
+                  <ElButton text icon={DetailIcon} onClick={() => action(row, 'detail')} />
+                </ElTooltip>
+                <ElTooltip content="删除">
+                  <ElButton
+                    text
+                    icon={DeleteIcon}
+                    type="danger"
+                    onClick={() => delData(data.row)}
+                  />
+                </ElTooltip>
+              </ElButtonGroup>
+            </>
+          )
+        }
+      }
+    }
+  }
+])
+
+const { allSchemas } = useCrudSchemas(crudSchemas)
+
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  currentPage.value = 1
+  searchParams.value = params
+  getList()
+}
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+
+const currentRow = ref<DictData>()
+const actionType = ref('')
+
+const load = async (
+  row: DictRowData,
+  _treeNode: unknown,
+  resolve: (date: DictRowData[]) => void
+) => {
+  const res = await getTree({
+    pid: row.id,
+    ...unref(searchParams)
+  })
+  resolve(
+    res.data.map((e: DictRowData) => {
+      return {
+        ...e,
+        hasChildren: true
+      }
+    })
+  )
+}
+
+const AddAction = (key: string) => {
+  dialogTitle.value = '新增'
+  currentRow.value = undefined
+  if (key) {
+    currentRow.value = {
+      pid: key
+    }
+  }
+  dialogVisible.value = true
+  actionType.value = 'add'
+}
+
+const delLoading = ref(false)
+const id = ref<string>('')
+
+const delData = async (row: DictRowData) => {
+  id.value = row.id
+  delLoading.value = true
+
+  await delList(unref(id).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const action = (row: DictData, type: string) => {
+  dialogTitle.value = type === 'edit' ? '编辑' : '子项列表'
+  actionType.value = type
+  currentRow.value = row
+  dialogVisible.value = true
+}
+
+const writeRef = ref<ComponentRef<typeof Write>>()
+const tableRef = ref<ComponentRef<typeof Table>>()
+const saveLoading = ref(false)
+const save = async (action) => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    saveLoading.value = true
+    try {
+      if (action == 'add') {
+        const res = await saveTableApi(formData)
+        if (res) {
+          getList()
+        }
+      } else {
+        const res = await updateTableApi(formData)
+        if (res) {
+          getList()
+        }
+      }
+    } catch (error) {
+      console.log(error)
+    } finally {
+      saveLoading.value = false
+      dialogVisible.value = false
+    }
+  }
+}
+</script>
+
+<template>
+  <div>
+    <ContentWrap>
+      <Search
+        :schema="allSchemas.searchSchema"
+        @reset="setSearchParams"
+        @search="setSearchParams"
+      />
+
+      <div class="mb-10px">
+        <ElButton type="primary" @click="AddAction('')">{{ t('exampleDemo.add') }}</ElButton>
+      </div>
+      <Table
+        ref="tableRef"
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :columns="allSchemas.tableColumns"
+        :data="dataList"
+        :loading="loading"
+        row-key="id"
+        :load="load"
+        @register="tableRegister"
+      />
+    </ContentWrap>
+
+    <Dialog v-model="dialogVisible" :title="dialogTitle">
+      <Write
+        v-if="actionType !== 'detail'"
+        ref="writeRef"
+        :form-schema="allSchemas.formSchema"
+        :current-row="currentRow"
+      />
+
+      <Detail
+        v-if="actionType === 'detail'"
+        ref="detailRef"
+        :columns="allSchemas"
+        :row-data="currentRow"
+      />
+
+      <template #footer>
+        <ElButton
+          v-if="actionType !== 'detail'"
+          type="primary"
+          :loading="saveLoading"
+          @click="save(actionType)"
+        >
+          {{ t('exampleDemo.save') }}
+        </ElButton>
+
+        <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+      </template>
+    </Dialog>
+  </div>
+</template>

+ 278 - 0
src/views/Authorization/Dict/components/Detail.vue

@@ -0,0 +1,278 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table } from '@/components/Table'
+import { ref, unref, PropType, reactive } from 'vue'
+import { ElButton, ElButtonGroup, ElTooltip } from 'element-plus'
+import { getTree, saveTableApi, delTableListApi, updateTableApi } from '@/api/manage/dict'
+import type { DictData, DictRowData } from '@/api/manage/types'
+import { useTable } from '@/hooks/web/useTable'
+import { Search } from '@/components/Search'
+import Write from './Write.vue'
+// import Detail from './components/Detail.vue'
+import { Dialog } from '@/components/Dialog'
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+import { useIcon } from '@/hooks/web/useIcon'
+const EditIcon = useIcon({ icon: 'ep:edit' })
+// const DetailIcon = useIcon({ icon: 'ep:document' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+
+const props = defineProps({
+  rowData: {
+    type: Object as PropType<DictData>
+  }
+})
+
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const res = await getTree({
+      pid: props.rowData?.id,
+      ...unref(searchParams)
+    })
+    return {
+      list:
+        res.data.map((e: DictRowData) => {
+          return {
+            ...e,
+            hasChildren: true
+          }
+        }) || []
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(id))
+    return !!res
+  }
+})
+const { loading, dataList, pageSize, currentPage } = tableState
+const { getList, delList } = tableMethods
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    field: 'name',
+    label: '名称'
+  },
+  {
+    field: 'code',
+    label: '编码',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'showText',
+    label: '显示文字',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'value',
+    label: '值',
+    search: {
+      hidden: true
+    }
+  },
+  {
+    field: 'isActive',
+    label: '是否启用',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      component: 'Switch',
+      value: '1',
+      componentProps: {
+        activeText: '启用',
+        inactiveText: '禁用',
+        activeValue: '1',
+        inactiveValue: '0'
+      }
+    }
+  },
+  {
+    field: 'description',
+    label: '备注',
+    form: {
+      componentProps: {
+        type: 'textarea'
+      },
+      colProps: {
+        span: 24
+      }
+    },
+    search: {
+      hidden: true
+    }
+  },
+
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    search: {
+      hidden: true
+    },
+    table: {
+      width: 120,
+      fixed: 'right',
+      slots: {
+        default: (data: any) => {
+          const row = data.row as DictRowData
+          return (
+            <>
+              <ElButtonGroup>
+                <ElTooltip content="编辑">
+                  <ElButton text icon={EditIcon} onClick={() => action(row, 'edit')} />
+                </ElTooltip>
+                <ElTooltip content="删除">
+                  <ElButton
+                    text
+                    icon={DeleteIcon}
+                    type="danger"
+                    onClick={() => delData(data.row)}
+                  />
+                </ElTooltip>
+              </ElButtonGroup>
+            </>
+          )
+        }
+      }
+    }
+  }
+])
+
+const { allSchemas } = useCrudSchemas(crudSchemas)
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  currentPage.value = 1
+  searchParams.value = params
+  getList()
+}
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+
+const currentRow = ref<DictData>()
+const actionType = ref('')
+
+const AddAction = (key: string) => {
+  dialogTitle.value = '新增'
+  currentRow.value = undefined
+  if (key) {
+    currentRow.value = {
+      pid: key
+    }
+  }
+  dialogVisible.value = true
+  actionType.value = 'add'
+}
+const delLoading = ref(false)
+const id = ref<string>('')
+const delData = async (row: DictRowData) => {
+  id.value = row.id
+  delLoading.value = true
+
+  await delList(unref(id).length).finally(() => {
+    delLoading.value = false
+  })
+}
+const action = (row: DictData, type: string) => {
+  dialogTitle.value = type === 'edit' ? '编辑' : '子项列表'
+  actionType.value = type
+  currentRow.value = row
+  dialogVisible.value = true
+}
+const writeRef = ref<ComponentRef<typeof Write>>()
+const tableRef = ref<ComponentRef<typeof Table>>()
+const saveLoading = ref(false)
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    saveLoading.value = true
+    try {
+      if (actionType.value == 'add') {
+        const res = await saveTableApi(formData)
+        if (res) {
+          getList()
+        }
+      } else {
+        const res = await updateTableApi(formData)
+        if (res) {
+          getList()
+        }
+      }
+    } catch (error) {
+      console.log(error)
+    } finally {
+      saveLoading.value = false
+      dialogVisible.value = false
+    }
+  }
+}
+
+defineExpose({
+  getList
+})
+</script>
+
+<template>
+  <div>
+    <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+
+    <div class="mb-10px">
+      <ElButton type="primary" @click="AddAction(props.rowData?.id || '')">{{
+        t('exampleDemo.add')
+      }}</ElButton>
+    </div>
+    <Table
+      ref="tableRef"
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :columns="allSchemas.tableColumns"
+      :data="dataList"
+      :loading="loading"
+      row-key="id"
+      @register="tableRegister"
+    />
+
+    <Dialog v-model="dialogVisible" :title="dialogTitle">
+      <Write
+        v-if="actionType !== 'detail'"
+        ref="writeRef"
+        :form-schema="allSchemas.formSchema"
+        :current-row="currentRow"
+      />
+
+      <!-- <Detail
+        v-if="actionType === 'detail'"
+        :columns="props.allSchemas.tableColumns"
+        :current-row="currentRow"
+      /> -->
+
+      <template #footer>
+        <ElButton
+          v-if="actionType !== 'detail'"
+          type="primary"
+          :loading="saveLoading"
+          @click="save"
+        >
+          {{ t('exampleDemo.save') }}
+        </ElButton>
+
+        <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+      </template>
+    </Dialog>
+  </div>
+</template>

+ 58 - 0
src/views/Authorization/Dict/components/Write.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, reactive, watch } from 'vue'
+import { DictData } from '@/api/manage/types'
+import { useValidator } from '@/hooks/web/useValidator'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<DictData>,
+    default: () => undefined
+  },
+  formSchema: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => []
+  }
+})
+
+const rules = reactive({
+  code: [required()]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose } = formMethods
+
+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)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 120 - 137
src/views/Authorization/User/User.vue

@@ -2,114 +2,119 @@
 import { ContentWrap } from '@/components/ContentWrap'
 import { useI18n } from '@/hooks/web/useI18n'
 import { Table } from '@/components/Table'
-import { ref, unref, nextTick, watch, reactive } from 'vue'
-import { ElButton, ElTree, ElInput, ElDivider } from 'element-plus'
-import { getDepartmentApi, getUserByIdApi, saveUserApi, deleteUserByIdApi } from '@/api/department'
-import type { DepartmentItem, DepartmentUserItem } from '@/api/department/types'
+import { ref, unref, watch, reactive, nextTick } from 'vue'
+import { ElButton, ElTree, ElTooltip, ElButtonGroup } from 'element-plus'
+import { getUserList, addUser, updataUser, deleteUserById, getRoleApi } from '@/api/department'
+import type { DepartmentUserItem } from '@/api/department/types'
 import { useTable } from '@/hooks/web/useTable'
 import { Search } from '@/components/Search'
 import Write from './components/Write.vue'
 import Detail from './components/Detail.vue'
 import { Dialog } from '@/components/Dialog'
 import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+import { useIcon } from '@/hooks/web/useIcon'
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
 
 const { t } = useI18n()
 
 const { tableRegister, tableState, tableMethods } = useTable({
   fetchDataApi: async () => {
     const { pageSize, currentPage } = tableState
-    const res = await getUserByIdApi({
-      id: unref(currentNodeKey),
-      pageIndex: unref(currentPage),
+    const res = await getUserList({
+      pageNum: unref(currentPage),
       pageSize: unref(pageSize),
       ...unref(searchParams)
     })
     return {
       list: res.data.list || [],
-      total: res.data.total || 0
+      total: Number(res.data.totalCount)
     }
   },
   fetchDelApi: async () => {
-    const res = await deleteUserByIdApi(unref(ids))
+    const res = await deleteUserById(unref(id))
     return !!res
   }
 })
 const { total, loading, dataList, pageSize, currentPage } = tableState
-const { getList, getElTableExpose, delList } = tableMethods
+const { getList, delList } = tableMethods
 
 const crudSchemas = reactive<CrudSchema[]>([
   {
-    field: 'selection',
+    field: 'name',
+    label: t('userDemo.username')
+  },
+  {
+    field: 'account',
+    label: t('userDemo.account'),
     search: {
       hidden: true
-    },
-    form: {
-      hidden: true
-    },
-    detail: {
-      hidden: true
-    },
-    table: {
-      type: 'selection'
     }
   },
   {
-    field: 'index',
-    label: t('userDemo.index'),
+    field: 'isActive',
+    label: '是否启用',
     form: {
-      hidden: true
+      component: 'Switch',
+      value: '1',
+      componentProps: {
+        activeText: '启用',
+        inactiveText: '禁用',
+        activeValue: '1',
+        inactiveValue: '0'
+      }
     },
     search: {
-      hidden: true
-    },
-    detail: {
-      hidden: true
-    },
-    table: {
-      type: 'index'
+      component: 'Select',
+      componentProps: {
+        options: [
+          {
+            value: '1',
+            label: '启用'
+          },
+          {
+            value: '0',
+            label: '禁用'
+          }
+        ]
+      }
     }
   },
   {
-    field: 'username',
-    label: t('userDemo.username')
-  },
-  {
-    field: 'account',
-    label: t('userDemo.account')
-  },
-  {
-    field: 'department.id',
-    label: t('userDemo.department'),
-    detail: {
-      slots: {
-        default: (data: DepartmentUserItem) => {
-          return <>{data.department.departmentName}</>
-        }
-      }
-    },
+    field: 'roleId',
+    label: t('userDemo.role'),
     search: {
       hidden: true
     },
     form: {
-      component: 'TreeSelect',
-      componentProps: {
-        nodeKey: 'id',
-        props: {
-          label: 'departmentName'
-        }
-      },
+      component: 'Select',
       optionApi: async () => {
-        const res = await getDepartmentApi()
-        return res.data.list
+        const res = await getRoleApi()
+        return res.data.map((e) => {
+          return {
+            label: e.roleDesc,
+            value: e.id
+          }
+        })
       }
+    }
+  },
+  {
+    field: 'phone',
+    label: '电话',
+    form: {
+      component: 'Input'
     },
-    table: {
-      type: 'index'
+    search: {
+      hidden: true
     }
   },
   {
-    field: 'role',
-    label: t('userDemo.role'),
+    field: 'address',
+    label: '住址',
+    form: {
+      component: 'Input'
+    },
     search: {
       hidden: true
     }
@@ -124,11 +129,27 @@ const crudSchemas = reactive<CrudSchema[]>([
       hidden: true
     }
   },
+  {
+    field: 'password',
+    label: '密码',
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'password'
+      }
+    },
+    table: {
+      hidden: true
+    },
+    search: {
+      hidden: true
+    }
+  },
   {
     field: 'createTime',
     label: t('userDemo.createTime'),
     form: {
-      component: 'Input'
+      hidden: true
     },
     search: {
       hidden: true
@@ -147,21 +168,26 @@ const crudSchemas = reactive<CrudSchema[]>([
       hidden: true
     },
     table: {
-      width: 240,
+      width: 120,
+      fixed: 'right',
       slots: {
         default: (data: any) => {
           const row = data.row as DepartmentUserItem
           return (
             <>
-              <ElButton type="primary" onClick={() => action(row, 'edit')}>
-                {t('exampleDemo.edit')}
-              </ElButton>
-              <ElButton type="success" onClick={() => action(row, 'detail')}>
-                {t('exampleDemo.detail')}
-              </ElButton>
-              <ElButton type="danger" onClick={() => delData(row)}>
-                {t('exampleDemo.del')}
-              </ElButton>
+              <ElButtonGroup>
+                <ElTooltip content="编辑">
+                  <ElButton text icon={EditIcon} onClick={() => action(row, 'edit')} />
+                </ElTooltip>
+                <ElTooltip content="删除">
+                  <ElButton
+                    text
+                    icon={DeleteIcon}
+                    type="danger"
+                    onClick={() => delData(data.row)}
+                  />
+                </ElTooltip>
+              </ElButtonGroup>
             </>
           )
         }
@@ -181,18 +207,6 @@ const setSearchParams = (params: any) => {
 
 const treeEl = ref<typeof ElTree>()
 
-const currentNodeKey = ref('')
-const departmentList = ref<DepartmentItem[]>([])
-const fetchDepartment = async () => {
-  const res = await getDepartmentApi()
-  departmentList.value = res.data.list
-  currentNodeKey.value =
-    (res.data.list[0] && res.data.list[0]?.children && res.data.list[0].children[0].id) || ''
-  await nextTick()
-  unref(treeEl)?.setCurrentKey(currentNodeKey.value)
-}
-fetchDepartment()
-
 const currentDepartment = ref('')
 watch(
   () => currentDepartment.value,
@@ -201,18 +215,6 @@ watch(
   }
 )
 
-const currentChange = (data: DepartmentItem) => {
-  if (data.children) return
-  currentNodeKey.value = data.id
-  currentPage.value = 1
-  getList()
-}
-
-const filterNode = (value: string, data: DepartmentItem) => {
-  if (!value) return true
-  return data.departmentName.includes(value)
-}
-
 const dialogVisible = ref(false)
 const dialogTitle = ref('')
 
@@ -227,29 +229,25 @@ const AddAction = () => {
 }
 
 const delLoading = ref(false)
-const ids = ref<string[]>([])
+const id = ref<number>(0)
 
-const delData = async (row?: DepartmentUserItem) => {
-  const elTableExpose = await getElTableExpose()
-  ids.value = row
-    ? [row.id]
-    : elTableExpose?.getSelectionRows().map((v: DepartmentUserItem) => v.id) || []
+const delData = async (row: DepartmentUserItem) => {
+  id.value = row.id
   delLoading.value = true
 
-  await delList(unref(ids).length).finally(() => {
+  await delList(unref(id)).finally(() => {
     delLoading.value = false
   })
 }
+const writeRef = ref<ComponentRef<typeof Write>>()
 
-const action = (row: DepartmentUserItem, type: string) => {
+const action = async (row: DepartmentUserItem, type: string) => {
   dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
   actionType.value = type
   currentRow.value = { ...row, department: unref(treeEl)?.getCurrentNode() || {} }
   dialogVisible.value = true
 }
 
-const writeRef = ref<ComponentRef<typeof Write>>()
-
 const saveLoading = ref(false)
 
 const save = async () => {
@@ -258,13 +256,17 @@ const save = async () => {
   if (formData) {
     saveLoading.value = true
     try {
-      const res = await saveUserApi(formData)
+      let res: any
+      if (formData.id) {
+        res = await updataUser(formData)
+      } else {
+        res = await addUser(formData)
+      }
       if (res) {
         currentPage.value = 1
         getList()
       }
     } catch (error) {
-      console.log(error)
     } finally {
       saveLoading.value = false
       dialogVisible.value = false
@@ -274,32 +276,8 @@ const save = async () => {
 </script>
 
 <template>
-  <div class="flex w-100% h-100%">
-    <ContentWrap class="flex-1">
-      <div class="flex justify-center items-center">
-        <div class="flex-1">{{ t('userDemo.departmentList') }}</div>
-        <ElInput
-          v-model="currentDepartment"
-          class="flex-[2]"
-          :placeholder="t('userDemo.searchDepartment')"
-          clearable
-        />
-      </div>
-      <ElDivider />
-      <ElTree
-        ref="treeEl"
-        :data="departmentList"
-        default-expand-all
-        node-key="id"
-        :current-node-key="currentNodeKey"
-        :props="{
-          label: 'departmentName'
-        }"
-        :filter-node-method="filterNode"
-        @current-change="currentChange"
-      />
-    </ContentWrap>
-    <ContentWrap class="flex-[3] ml-20px">
+  <div>
+    <ContentWrap>
       <Search
         :schema="allSchemas.searchSchema"
         @reset="setSearchParams"
@@ -308,9 +286,6 @@ const save = async () => {
 
       <div class="mb-10px">
         <ElButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</ElButton>
-        <ElButton :loading="delLoading" type="danger" @click="delData()">
-          {{ t('exampleDemo.del') }}
-        </ElButton>
       </div>
       <Table
         v-model:current-page="currentPage"
@@ -329,7 +304,15 @@ const save = async () => {
       <Write
         v-if="actionType !== 'detail'"
         ref="writeRef"
-        :form-schema="allSchemas.formSchema"
+        :form-schema="
+          allSchemas.formSchema.filter((e) => {
+            if (actionType === 'edit') {
+              return e.field !== 'password'
+            } else {
+              return true
+            }
+          })
+        "
         :current-row="currentRow"
       />
 

+ 5 - 8
src/views/Authorization/User/components/Write.vue

@@ -19,16 +19,12 @@ const props = defineProps({
 })
 
 const rules = reactive({
-  username: [required()],
-  account: [required()],
-  'department.id': [required()],
-  role: [required()],
-  email: [required()],
-  createTime: [required()]
+  name: [required()],
+  account: [required()]
 })
 
 const { formRegister, formMethods } = useForm()
-const { setValues, getFormData, getElFormExpose } = formMethods
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
 
 const submit = async () => {
   const elForm = await getElFormExpose()
@@ -54,7 +50,8 @@ watch(
 )
 
 defineExpose({
-  submit
+  submit,
+  setSchema
 })
 </script>
 

+ 10 - 233
src/views/Dashboard/Workplace.vue

@@ -1,111 +1,9 @@
 <script setup lang="ts">
-import { useTimeAgo } from '@/hooks/web/useTimeAgo'
-import { ElRow, ElCol, ElSkeleton, ElCard, ElDivider, ElLink } from 'element-plus'
+import { ElRow, ElCol, ElSkeleton, ElCard } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
-import { ref, reactive } from 'vue'
-import { CountTo } from '@/components/CountTo'
-import { formatTime } from '@/utils'
-import { Echart } from '@/components/Echart'
-import { EChartsOption } from 'echarts'
-import { radarOption } from './echarts-data'
-import { Highlight } from '@/components/Highlight'
-import {
-  getCountApi,
-  getProjectApi,
-  getDynamicApi,
-  getTeamApi,
-  getRadarApi
-} from '@/api/dashboard/workplace'
-import type { WorkplaceTotal, Project, Dynamic, Team } from '@/api/dashboard/workplace/types'
-import { set } from 'lodash-es'
+import { ref } from 'vue'
 
-const loading = ref(true)
-
-// 获取统计数
-let totalSate = reactive<WorkplaceTotal>({
-  project: 0,
-  access: 0,
-  todo: 0
-})
-
-const getCount = async () => {
-  const res = await getCountApi().catch(() => {})
-  if (res) {
-    totalSate = Object.assign(totalSate, res.data)
-  }
-}
-
-let projects = reactive<Project[]>([])
-
-// 获取项目数
-const getProject = async () => {
-  const res = await getProjectApi().catch(() => {})
-  if (res) {
-    projects = Object.assign(projects, res.data)
-  }
-}
-
-// 获取动态
-let dynamics = reactive<Dynamic[]>([])
-
-const getDynamic = async () => {
-  const res = await getDynamicApi().catch(() => {})
-  if (res) {
-    dynamics = Object.assign(dynamics, res.data)
-  }
-}
-
-// 获取团队
-let team = reactive<Team[]>([])
-
-const getTeam = async () => {
-  const res = await getTeamApi().catch(() => {})
-  if (res) {
-    team = Object.assign(team, res.data)
-  }
-}
-
-// 获取指数
-let radarOptionData = reactive<EChartsOption>(radarOption) as EChartsOption
-
-const getRadar = async () => {
-  const res = await getRadarApi().catch(() => {})
-  if (res) {
-    set(
-      radarOptionData,
-      'radar.indicator',
-      res.data.map((v) => {
-        return {
-          name: t(v.name),
-          max: v.max
-        }
-      })
-    )
-    set(radarOptionData, 'series', [
-      {
-        name: `xxx${t('workplace.index')}`,
-        type: 'radar',
-        data: [
-          {
-            value: res.data.map((v) => v.personal),
-            name: t('workplace.personal')
-          },
-          {
-            value: res.data.map((v) => v.team),
-            name: t('workplace.team')
-          }
-        ]
-      }
-    ])
-  }
-}
-
-const getAllApi = async () => {
-  await Promise.all([getCount(), getProject(), getDynamic(), getTeam(), getRadar()])
-  loading.value = false
-}
-
-getAllApi()
+const loading = ref(false)
 
 const { t } = useI18n()
 </script>
@@ -117,23 +15,23 @@ const { t } = useI18n()
         <ElRow :gutter="20" justify="space-between">
           <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
             <div class="flex items-center">
-              <img
+              <!-- <img
                 src="@/assets/imgs/avatar.jpg"
                 alt=""
                 class="w-70px h-70px rounded-[50%] mr-20px"
-              />
+              /> -->
               <div>
                 <div class="text-20px">
-                  {{ t('workplace.goodMorning') }},Archer,{{ t('workplace.happyDay') }}
+                  {{ t('workplace.happyDay') }}
                 </div>
-                <div class="mt-10px text-14px text-gray-500">
+                <!-- <div class="mt-10px text-14px text-gray-500">
                   {{ t('workplace.toady') }},20℃ - 32℃!
-                </div>
+                </div> -->
               </div>
             </div>
           </ElCol>
           <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
-            <div class="flex h-70px items-center justify-end lt-sm:mt-20px">
+            <!-- <div class="flex h-70px items-center justify-end lt-sm:mt-20px">
               <div class="px-8px text-right">
                 <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.project') }}</div>
                 <CountTo
@@ -163,131 +61,10 @@ const { t } = useI18n()
                   :duration="2600"
                 />
               </div>
-            </div>
+            </div> -->
           </ElCol>
         </ElRow>
       </ElSkeleton>
     </ElCard>
   </div>
-
-  <ElRow class="mt-20px" :gutter="20" justify="space-between">
-    <ElCol :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-20px">
-      <ElCard shadow="never">
-        <template #header>
-          <div class="flex justify-between">
-            <span>{{ t('workplace.project') }}</span>
-            <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
-          </div>
-        </template>
-        <ElSkeleton :loading="loading" animated>
-          <ElRow>
-            <ElCol
-              v-for="(item, index) in projects"
-              :key="`card-${index}`"
-              :xl="8"
-              :lg="8"
-              :md="12"
-              :sm="24"
-              :xs="24"
-            >
-              <ElCard shadow="hover">
-                <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-10px" />
-                  <span class="text-16px">{{ item.name }}</span>
-                </div>
-                <div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div>
-                <div class="mt-20px text-12px text-gray-400 flex justify-between">
-                  <span>{{ item.personal }}</span>
-                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
-                </div>
-              </ElCard>
-            </ElCol>
-          </ElRow>
-        </ElSkeleton>
-      </ElCard>
-
-      <ElCard shadow="never" class="mt-20px">
-        <template #header>
-          <div class="flex justify-between">
-            <span>{{ t('workplace.dynamic') }}</span>
-            <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
-          </div>
-        </template>
-        <ElSkeleton :loading="loading" animated>
-          <div v-for="(item, index) in dynamics" :key="`dynamics-${index}`">
-            <div class="flex items-center">
-              <img
-                src="@/assets/imgs/avatar.jpg"
-                alt=""
-                class="w-35px h-35px rounded-[50%] mr-20px"
-              />
-              <div>
-                <div class="text-14px">
-                  <Highlight :keys="item.keys.map((v) => t(v))">
-                    {{ t('workplace.pushCode') }}
-                  </Highlight>
-                </div>
-                <div class="mt-15px text-12px text-gray-400">
-                  {{ useTimeAgo(item.time) }}
-                </div>
-              </div>
-            </div>
-            <ElDivider />
-          </div>
-        </ElSkeleton>
-      </ElCard>
-    </ElCol>
-    <ElCol :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-20px">
-      <ElCard shadow="never">
-        <template #header>
-          <span>{{ t('workplace.shortcutOperation') }}</span>
-        </template>
-        <ElSkeleton :loading="loading" animated>
-          <ElRow>
-            <ElCol
-              v-for="item in 9"
-              :key="`card-${item}`"
-              :xl="12"
-              :lg="12"
-              :md="12"
-              :sm="24"
-              :xs="24"
-              class="mb-10px"
-            >
-              <ElLink type="default" :underline="false">
-                {{ t('workplace.operation') }}{{ item }}
-              </ElLink>
-            </ElCol>
-          </ElRow>
-        </ElSkeleton>
-      </ElCard>
-
-      <ElCard shadow="never" class="mt-20px">
-        <template #header>
-          <span>xx{{ t('workplace.index') }}</span>
-        </template>
-        <ElSkeleton :loading="loading" animated>
-          <Echart :options="radarOptionData" :height="400" />
-        </ElSkeleton>
-      </ElCard>
-
-      <ElCard shadow="never" class="mt-20px">
-        <template #header>
-          <span>{{ t('workplace.team') }}</span>
-        </template>
-        <ElSkeleton :loading="loading" animated>
-          <ElRow>
-            <ElCol v-for="item in team" :key="`team-${item.name}`" :span="12" class="mb-20px">
-              <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-10px" />
-                <ElLink type="default" :underline="false">
-                  {{ item.name }}
-                </ElLink>
-              </div>
-            </ElCol>
-          </ElRow>
-        </ElSkeleton>
-      </ElCard>
-    </ElCol>
-  </ElRow>
 </template>

+ 4 - 6
src/views/Login/Login.vue

@@ -50,9 +50,7 @@ const toLogin = () => {
             >
               <img src="@/assets/svgs/login-box-bg.svg" key="1" alt="" class="w-350px" />
               <div class="text-3xl text-white" key="2">{{ t('login.welcome') }}</div>
-              <div class="mt-5 font-normal text-white text-14px" key="3">
-                {{ t('login.message') }}
-              </div>
+              <div class="mt-5 font-normal text-white text-14px" key="3"> 管理网站信息 </div>
             </TransitionGroup>
           </div>
         </div>
@@ -76,14 +74,14 @@ const toLogin = () => {
             >
               <LoginForm
                 v-if="isLogin"
-                class="p-20px h-auto m-auto lt-xl:rounded-3xl lt-xl:light:bg-white"
+                class="p-20px h-auto m-auto lt-xl:rounded-3xl bg-white"
                 @to-register="toRegister"
               />
-              <RegisterForm
+              <!-- <RegisterForm
                 v-else
                 class="p-20px h-auto m-auto lt-xl:rounded-3xl lt-xl:light:bg-white"
                 @to-login="toLogin"
-              />
+              /> -->
             </div>
           </Transition>
         </div>

+ 60 - 64
src/views/Login/components/LoginForm.vue

@@ -48,7 +48,7 @@ const schema = reactive<FormSchema[]>([
     }
   },
   {
-    field: 'username',
+    field: 'account',
     label: t('login.username'),
     value: 'admin',
     component: 'Input',
@@ -62,7 +62,7 @@ const schema = reactive<FormSchema[]>([
   {
     field: 'password',
     label: t('login.password'),
-    value: 'admin',
+    value: '123456',
     component: 'InputPassword',
     colProps: {
       span: 24
@@ -111,71 +111,66 @@ const schema = reactive<FormSchema[]>([
                   {t('login.login')}
                 </ElButton>
               </div>
-              <div class="w-[100%] mt-15px">
-                <ElButton class="w-[100%]" onClick={toRegister}>
-                  {t('login.register')}
-                </ElButton>
-              </div>
-            </>
-          )
-        }
-      }
-    }
-  },
-  {
-    field: 'other',
-    component: 'Divider',
-    label: t('login.otherLogin'),
-    componentProps: {
-      contentPosition: 'center'
-    }
-  },
-  {
-    field: 'otherIcon',
-    colProps: {
-      span: 24
-    },
-    formItemProps: {
-      slots: {
-        default: () => {
-          return (
-            <>
-              <div class="flex justify-between w-[100%]">
-                <Icon
-                  icon="ant-design:github-filled"
-                  size={iconSize}
-                  class="cursor-pointer ant-icon"
-                  color={iconColor}
-                  hoverColor={hoverColor}
-                />
-                <Icon
-                  icon="ant-design:wechat-filled"
-                  size={iconSize}
-                  class="cursor-pointer ant-icon"
-                  color={iconColor}
-                  hoverColor={hoverColor}
-                />
-                <Icon
-                  icon="ant-design:alipay-circle-filled"
-                  size={iconSize}
-                  color={iconColor}
-                  hoverColor={hoverColor}
-                  class="cursor-pointer ant-icon"
-                />
-                <Icon
-                  icon="ant-design:weibo-circle-filled"
-                  size={iconSize}
-                  color={iconColor}
-                  hoverColor={hoverColor}
-                  class="cursor-pointer ant-icon"
-                />
-              </div>
             </>
           )
         }
       }
     }
   }
+  // {
+  //   field: 'other',
+  //   component: 'Divider',
+  //   label: t('login.otherLogin'),
+  //   componentProps: {
+  //     contentPosition: 'center'
+  //   }
+  // },
+  // {
+  //   field: 'otherIcon',
+  //   colProps: {
+  //     span: 24
+  //   },
+  //   formItemProps: {
+  //     slots: {
+  //       default: () => {
+  //         return (
+  //           <>
+  //             <div class="flex justify-between w-[100%]">
+  //               <Icon
+  //                 icon="ant-design:github-filled"
+  //                 size={iconSize}
+  //                 class="cursor-pointer ant-icon"
+  //                 color={iconColor}
+  //                 hoverColor={hoverColor}
+  //               />
+  //               <Icon
+  //                 icon="ant-design:wechat-filled"
+  //                 size={iconSize}
+  //                 class="cursor-pointer ant-icon"
+  //                 color={iconColor}
+  //                 hoverColor={hoverColor}
+  //               />
+  //               <Icon
+  //                 icon="ant-design:alipay-circle-filled"
+  //                 size={iconSize}
+  //                 color={iconColor}
+  //                 hoverColor={hoverColor}
+  //                 class="cursor-pointer ant-icon"
+  //               />
+  //               <Icon
+  //                 icon="ant-design:weibo-circle-filled"
+  //                 size={iconSize}
+  //                 color={iconColor}
+  //                 hoverColor={hoverColor}
+  //                 class="cursor-pointer ant-icon"
+  //               />
+  //             </div>
+  //           </>
+  //         )
+  //       }
+  //     }
+  //   }
+  // }
 ])
 
 const iconSize = 30
@@ -223,6 +218,7 @@ const signIn = async () => {
 
         if (res) {
           setStorage(appStore.getUserInfo, res.data)
+          setStorage('token', res.token)
           // 是否使用动态路由
           if (appStore.getDynamicRouter) {
             getRole()
@@ -246,17 +242,17 @@ const signIn = async () => {
 const getRole = async () => {
   const formData = await getFormData<UserType>()
   const params = {
-    roleName: formData.username
+    roleName: formData.account
   }
   // admin - 模拟后端过滤菜单
   // test - 模拟前端过滤菜单
   const res =
-    formData.username === 'admin' ? await getAdminRoleApi(params) : await getTestRoleApi(params)
+    formData.account === 'admin' ? await getAdminRoleApi(params) : await getTestRoleApi(params)
   if (res) {
     const routers = res.data || []
     setStorage('roleRouters', routers)
 
-    formData.username === 'admin'
+    formData.account === 'admin'
       ? await permissionStore.generateRoutes('admin', routers).catch(() => {})
       : await permissionStore.generateRoutes('test', routers).catch(() => {})
 

+ 469 - 0
src/views/Manage/Company/CompanyPage.vue

@@ -0,0 +1,469 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTooltip, ElButtonGroup } from 'element-plus'
+import { Table } from '@/components/Table'
+import {
+  getTableListApi,
+  delTableListApi,
+  saveTableApi,
+  updateTableApi
+} from '@/api/manage/company'
+import { useTable } from '@/hooks/web/useTable'
+import { ref, unref } from 'vue'
+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'
+import { useStorage } from '@/hooks/web/useStorage'
+import { Dialog } from '@/components/Dialog'
+import { CompanyData } from '@/api/manage/types'
+import Write from './components/Write.vue'
+import { useIcon } from '@/hooks/web/useIcon'
+import { Icon } from '@/components/Icon'
+import { uploadFile } from '@/api/common'
+
+defineOptions({
+  name: 'CompanyPage'
+})
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const id = ref<string>('')
+const { getStorage } = useStorage()
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  searchParams.value = params
+  getList()
+}
+const currentRow = ref<CompanyData>({
+  wechatImg: '',
+  companyName: ''
+})
+const wechatImg = ref('')
+const formState = ref<string>('add')
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { currentPage, pageSize } = tableState
+    const res = await getTableListApi({
+      pageNum: unref(currentPage),
+      pageSize: unref(pageSize),
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: Number(res.data.totalCount)
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(id))
+    return !!res
+  }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+const { getList, delList, setColumn } = tableMethods
+
+getList()
+
+useEmitt({
+  name: 'getList',
+  callback: (type: string) => {
+    if (type === 'add') {
+      currentPage.value = 1
+    }
+    getList()
+  }
+})
+
+const { t } = useI18n()
+const appStore = usePageStore()
+const dialogVisible = ref(false)
+// const fileList = ref([])
+const crudSchemas: CrudSchema[] = [
+  {
+    field: 'companyName',
+    label: '公司名称',
+    minWidth: 100,
+    table: {
+      hidden: false
+    }
+  },
+
+  {
+    field: 'companyPhone',
+    label: '公司电话',
+    minWidth: 120,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {}
+  },
+  {
+    field: 'companyEmail',
+    label: '公司邮箱',
+    minWidth: 120,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {}
+  },
+  {
+    field: 'companyAddress',
+    label: '公司地址',
+    minWidth: 120,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: false
+    }
+  },
+  {
+    field: 'companyBrief',
+    label: '公司简介',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      componentProps: {
+        type: 'textarea'
+      },
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    field: 'name',
+    label: '负责人',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    form: {}
+  },
+  {
+    field: 'phone',
+    label: '负责人电话',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {}
+  },
+  {
+    field: 'wechat',
+    label: '微信账号',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {}
+  },
+  {
+    field: 'isActive',
+    label: '是否启用',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      component: 'Switch',
+      value: '1',
+      componentProps: {
+        activeText: '启用',
+        inactiveText: '禁用',
+        activeValue: '1',
+        inactiveValue: '0'
+      }
+    }
+  },
+  {
+    field: 'wechatImg',
+    label: '微信二维码',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      component: 'Upload',
+      componentProps: {
+        httpRequest: (data: any) => {
+          let file = data.file
+          let formData = new FormData()
+          formData.append('file', file)
+          uploadFile(formData).then((response) => {
+            wechatImg.value = response.data.virtualPath
+            currentRow.value.wechatImg = response.data.virtualPath
+          })
+        },
+        limit: 1,
+        showFileList: false,
+        class: 'companyUploader',
+        fileList: wechatImg.value
+          ? [
+              {
+                url: wechatImg.value
+              }
+            ]
+          : [],
+        headers: {
+          token: getStorage('token')
+        },
+        onSuccess: (response) => {
+          wechatImg.value = response.data.virtualPath
+          currentRow.value.wechatImg = response.data.virtualPath
+        },
+        onExceed: (_files, responses) => {
+          wechatImg.value = responses[0].response.data.virtualPath
+          currentRow.value.wechatImg = responses[0].response.data.virtualPath
+        },
+        slots: {
+          default: () => (
+            <>
+              {wechatImg.value ? <img src={wechatImg.value} class="avatar" /> : null}
+              {!wechatImg.value ? (
+                <Icon class="avatar-uploader-icon" icon="ep:plus" size={28}></Icon>
+              ) : null}
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'createTime',
+    label: '创建时间',
+    minWidth: 180,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'action',
+    width: '120px',
+    label: '操作',
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      hidden: false,
+      fixed: 'right',
+      slots: {
+        default: (data: any) => {
+          return (
+            <ElButtonGroup>
+              <ElTooltip content="编辑">
+                <ElButton text icon={EditIcon} onClick={() => handleEdit(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="删除">
+                <ElButton text icon={DeleteIcon} type="danger" onClick={() => delData(data.row)} />
+              </ElTooltip>
+            </ElButtonGroup>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @ts-ignore
+const getSchemas = () => {
+  let localSchemas = appStore.getPageData['CompanyPage']
+  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 writeRef = ref<ComponentRef<typeof Write>>()
+
+const handleEdit = (row: CompanyData) => {
+  currentRow.value = row
+  wechatImg.value = row.wechatImg
+  formState.value = 'edit'
+  dialogVisible.value = true
+}
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  console.log(formData)
+  if (formData) {
+    delLoading.value = true
+    try {
+      if (formData.id) {
+        let res = await updateTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      } else {
+        let res = await saveTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      }
+    } catch (error) {
+    } finally {
+      delLoading.value = false
+      dialogVisible.value = false
+    }
+  }
+}
+
+const delLoading = ref(false)
+
+const delData = async (row: CompanyData) => {
+  if (!row.id) return
+  id.value = row?.id
+  delLoading.value = true
+  await delList(unref(id).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const handleAdd = () => {
+  formState.value = 'add'
+  currentRow.value = {
+    wechatImg: '',
+    companyName: ''
+  }
+  wechatImg.value = ''
+  dialogVisible.value = true
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <el-button type="primary" @click="handleAdd">新增</el-button>
+      <TableSetting page="CompanyPage" :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>
+  <Dialog v-model="dialogVisible" :title="formState == 'add' ? '新增' : '编辑'">
+    <Write ref="writeRef" :form-schema="allSchemas.formSchema" :current-row="currentRow" />
+    <template #footer>
+      <ElButton type="primary" :loading="delLoading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+      <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+@/hooks/event/useEmitt
+
+<style lang="less">
+.uploadBtn {
+  display: inline-block;
+  margin-right: 12px;
+}
+.filePageUploader {
+  width: 100%;
+}
+</style>
+<style scoped>
+.companyUploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+</style>
+
+<style lang="less">
+.companyUploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+  img {
+    width: 178px;
+    height: 178px;
+  }
+  .avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    text-align: center;
+  }
+}
+</style>

+ 66 - 0
src/views/Manage/Company/components/Write.vue

@@ -0,0 +1,66 @@
+<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, reactive, watch } from 'vue'
+import { CompanyData } from '@/api/manage/types'
+import { useValidator } from '@/hooks/web/useValidator'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<CompanyData>,
+    default: () => undefined
+  },
+  formSchema: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => []
+  }
+})
+
+const rules = reactive({
+  companyName: [required()]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
+
+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
+  }
+}
+
+const resetForm = async () => {
+  const elForm = await getElFormExpose()
+  elForm?.resetFields()
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit,
+  setSchema,
+  setValues,
+  resetForm
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 453 - 0
src/views/Manage/File/FilePage.vue

@@ -0,0 +1,453 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTooltip, ElMessage, ElButtonGroup } from 'element-plus'
+import { Table } from '@/components/Table'
+import { getTableListApi, delTableListApi, saveTableApi, updateTableApi } from '@/api/manage/file'
+import { useTable } from '@/hooks/web/useTable'
+import { ref, unref } from 'vue'
+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'
+import { useStorage } from '@/hooks/web/useStorage'
+import { Dialog } from '@/components/Dialog'
+import { FileData } from '@/api/manage/types'
+import Write from './components/Write.vue'
+import { useIcon } from '@/hooks/web/useIcon'
+import { Qrcode } from '@/components/Qrcode'
+import { uploadFile } from '@/api/common'
+defineOptions({
+  name: 'FilePage'
+})
+const QRIcon = useIcon({ icon: 'ic:round-qr-code' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+const DownLoadIcon = useIcon({ icon: 'ep:download' })
+const copyIcon = useIcon({ icon: 'ep:document-copy' })
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const id = ref<string>('')
+const QrVisible = ref<boolean>(false)
+const QrSrc = ref<string>('')
+const { getStorage } = useStorage()
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  searchParams.value = params
+  getList()
+}
+const currentRow = ref<FileData>({
+  virtualPath: ''
+})
+const formState = ref<string>('add')
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { currentPage, pageSize } = tableState
+    const res = await getTableListApi({
+      pageNum: unref(currentPage),
+      pageSize: unref(pageSize),
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: Number(res.data.totalCount)
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(id))
+    return !!res
+  }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+const { getList, delList, setColumn } = tableMethods
+
+getList()
+
+useEmitt({
+  name: 'getList',
+  callback: (type: string) => {
+    if (type === 'add') {
+      currentPage.value = 1
+    }
+    getList()
+  }
+})
+
+const { t } = useI18n()
+const appStore = usePageStore()
+const dialogVisible = ref(false)
+// const fileList = ref([])
+const crudSchemas: CrudSchema[] = [
+  {
+    field: 'name',
+    label: '名称',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    field: 'version',
+    label: '版本号',
+    minWidth: 100,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    }
+  },
+  {
+    field: 'code',
+    label: '版本序列号',
+    minWidth: 100,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      component: 'InputNumber'
+    }
+  },
+  {
+    field: 'fileName',
+    label: '文件名',
+    minWidth: 120,
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'virtualPath',
+    label: '文件链接',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      component: 'Upload',
+      componentProps: {
+        httpRequest: (data: any) => {
+          delLoading.value = true
+          let file = data.file
+          let formData = new FormData()
+          formData.append('file', file)
+          uploadFile(formData).then((response) => {
+            currentRow.value = {
+              ...currentRow.value,
+              ...response.data
+            }
+            delLoading.value = false
+          })
+        },
+        class: 'filePageUploader',
+        fileList: currentRow.value.virtualPath
+          ? [
+              {
+                url: currentRow.value.virtualPath
+              }
+            ]
+          : [],
+        headers: {
+          token: getStorage('token')
+        },
+        onSuccess: (response) => {
+          currentRow.value = {
+            ...currentRow.value,
+            ...response.data
+          }
+          const write = unref(writeRef)
+          write?.setValues({
+            ...response.data
+          })
+        },
+        slots: {
+          default: () => <ElButton type="primary">上传文件</ElButton>
+        }
+      }
+    }
+  },
+  {
+    field: 'filePath',
+    label: '文件路径',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'content',
+    label: '版本更新说明',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      componentProps: {
+        type: 'textarea'
+      }
+    }
+  },
+  {
+    field: 'remark',
+    label: '备注',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      componentProps: {
+        type: 'textarea'
+      }
+    }
+  },
+  {
+    field: 'createTime',
+    label: '创建时间',
+    minWidth: 160,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'action',
+    width: '250px',
+    label: '操作',
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      hidden: false,
+      fixed: 'right',
+      slots: {
+        default: (data: any) => {
+          return (
+            <ElButtonGroup>
+              <ElTooltip content="编辑">
+                <ElButton text icon={EditIcon} onClick={() => handleEdit(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="二维码">
+                <ElButton text icon={QRIcon} onClick={() => showQrCode(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="下载">
+                <ElButton text icon={DownLoadIcon} onClick={() => downloadFile(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="复制链接">
+                <ElButton text icon={copyIcon} onClick={() => onCopy(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="删除">
+                <ElButton text icon={DeleteIcon} type="danger" onClick={() => delData(data.row)} />
+              </ElTooltip>
+            </ElButtonGroup>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @ts-ignore
+const getSchemas = () => {
+  let localSchemas = appStore.getPageData['FilePage']
+  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 writeRef = ref<ComponentRef<typeof Write>>()
+
+const handleEdit = (row: FileData) => {
+  currentRow.value = row
+  formState.value = 'edit'
+  dialogVisible.value = true
+}
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    delLoading.value = true
+    try {
+      if (formData.id) {
+        let res = await updateTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      } else {
+        let res = await saveTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      }
+    } catch (error) {
+    } finally {
+      delLoading.value = false
+      dialogVisible.value = false
+    }
+  }
+}
+
+const delLoading = ref(false)
+
+const delData = async (row: FileData) => {
+  if (!row.id) return
+  id.value = row?.id
+  delLoading.value = true
+  await delList(unref(id).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const showQrCode = (row: FileData) => {
+  QrSrc.value = row.virtualPath
+  QrVisible.value = true
+}
+
+const downloadFile = (row: FileData) => {
+  window.open(row.virtualPath)
+}
+
+const onCopy = (row: FileData) => {
+  navigator.clipboard.writeText(row.virtualPath).then(() => {
+    ElMessage({
+      message: '复制成功!',
+      type: 'success'
+    })
+  })
+}
+
+const handleAdd = () => {
+  formState.value = 'add'
+  currentRow.value = {
+    virtualPath: ''
+  }
+  dialogVisible.value = true
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <el-button type="primary" @click="handleAdd">上传文件</el-button>
+      <TableSetting page="FilePage" :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>
+  <Dialog v-model="dialogVisible" :title="formState == 'add' ? '新增文件' : '编辑文件'">
+    <Write ref="writeRef" :form-schema="allSchemas.formSchema" :current-row="currentRow" />
+    <template #footer>
+      <ElButton type="primary" :loading="delLoading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+      <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+  <Dialog v-model="QrVisible" width="430px" title="二维码">
+    <Qrcode
+      :width="395"
+      :options="{
+        color: {
+          dark: '#55D187',
+          light: '#ffffff'
+        }
+      }"
+      :text="QrSrc"
+    />
+    <template #footer>
+      <ElButton @click="QrVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+@/hooks/event/useEmitt
+
+<style lang="less">
+.uploadBtn {
+  display: inline-block;
+  margin-right: 12px;
+}
+.filePageUploader {
+  width: 100%;
+}
+</style>

+ 66 - 0
src/views/Manage/File/components/Write.vue

@@ -0,0 +1,66 @@
+<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, reactive, watch } from 'vue'
+import { FileData } from '@/api/manage/types'
+import { useValidator } from '@/hooks/web/useValidator'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<FileData>,
+    default: () => undefined
+  },
+  formSchema: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => []
+  }
+})
+
+const rules = reactive({
+  virtualPath: [required()]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
+
+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
+  }
+}
+
+const resetForm = async () => {
+  const elForm = await getElFormExpose()
+  elForm?.resetFields()
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit,
+  setSchema,
+  setValues,
+  resetForm
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 419 - 0
src/views/Manage/Img/ImgPage.vue

@@ -0,0 +1,419 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTooltip, ElMessage, ElButtonGroup } from 'element-plus'
+import { Table } from '@/components/Table'
+import { getTableListApi, delTableListApi, saveTableApi, updateTableApi } from '@/api/manage/img'
+import { useTable } from '@/hooks/web/useTable'
+import { ref, unref } from 'vue'
+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'
+import { useStorage } from '@/hooks/web/useStorage'
+import { Dialog } from '@/components/Dialog'
+import { ImgData } from '@/api/manage/types'
+import Write from './components/Write.vue'
+import { useIcon } from '@/hooks/web/useIcon'
+import { uploadFile } from '@/api/common'
+import { Qrcode } from '@/components/Qrcode'
+defineOptions({
+  name: 'ImgPage'
+})
+const QRIcon = useIcon({ icon: 'ic:round-qr-code' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+const DownLoadIcon = useIcon({ icon: 'ep:download' })
+const copyIcon = useIcon({ icon: 'ep:document-copy' })
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const id = ref<string>('')
+const QrVisible = ref<boolean>(false)
+const QrSrc = ref<string>('')
+const { getStorage } = useStorage()
+const searchParams = ref({})
+const setSearchParams = (params: any) => {
+  searchParams.value = params
+  getList()
+}
+const currentRow = ref<ImgData>({
+  bannerUrl: ''
+})
+const formState = ref<string>('add')
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { currentPage, pageSize } = tableState
+    const res = await getTableListApi({
+      pageNum: unref(currentPage),
+      pageSize: unref(pageSize),
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: Number(res.data.totalCount)
+    }
+  },
+  fetchDelApi: async () => {
+    const res = await delTableListApi(unref(id))
+    return !!res
+  }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+const { getList, delList, setColumn } = tableMethods
+
+getList()
+
+useEmitt({
+  name: 'getList',
+  callback: (type: string) => {
+    if (type === 'add') {
+      currentPage.value = 1
+    }
+    getList()
+  }
+})
+
+const { t } = useI18n()
+const appStore = usePageStore()
+const dialogVisible = ref(false)
+// const fileList = ref([])
+const crudSchemas: CrudSchema[] = [
+  {
+    field: 'bannerType',
+    label: '类型',
+    minWidth: 120,
+    table: {
+      hidden: false,
+      formatter: (_: Recordable, _colomun, cellValue: string) => {
+        return cellValue
+          ? cellValue === '0'
+            ? '首页Banner'
+            : cellValue === '1'
+            ? '公司图片'
+            : cellValue === '2'
+            ? '合作logo'
+            : ''
+          : ''
+      }
+    },
+    form: {
+      component: 'Select',
+      componentProps: {
+        options: [
+          {
+            label: '首页Banner',
+            value: '0'
+          },
+          {
+            label: '公司图片',
+            value: '1'
+          },
+          {
+            label: '合作logo',
+            value: '2'
+          }
+        ]
+      }
+    }
+  },
+  {
+    field: 'bannerUrl',
+    label: '图片地址',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      component: 'Upload',
+      componentProps: {
+        httpRequest: (data: any) => {
+          delLoading.value = true
+          let file = data.file
+          let formData = new FormData()
+          formData.append('file', file)
+          uploadFile(formData).then((response) => {
+            currentRow.value.bannerUrl = response.data.virtualPath
+            const write = unref(writeRef)
+            write?.setValues({
+              bannerUrl: response.data.virtualPath
+            })
+            delLoading.value = false
+          })
+        },
+        class: 'filePageUploader',
+        fileList: currentRow.value.bannerUrl
+          ? [
+              {
+                url: currentRow.value.bannerUrl
+              }
+            ]
+          : [],
+        headers: {
+          token: getStorage('token')
+        },
+        onSuccess: (response) => {
+          currentRow.value.bannerUrl = response.data.virtualPath
+        },
+        slots: {
+          default: () => <ElButton type="primary">上传文件</ElButton>
+        }
+      }
+    }
+  },
+  {
+    field: 'bannerTarget',
+    label: '跳转地址',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    field: 'remark',
+    label: '备注',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      componentProps: {
+        type: 'textarea'
+      }
+    }
+  },
+  {
+    field: 'createTime',
+    label: '创建时间',
+    minWidth: 160,
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'action',
+    width: '250px',
+    label: '操作',
+    search: {
+      hidden: true
+    },
+    form: {
+      hidden: true
+    },
+    detail: {
+      hidden: true
+    },
+    table: {
+      hidden: false,
+      fixed: 'right',
+      slots: {
+        default: (data: any) => {
+          return (
+            <ElButtonGroup>
+              <ElTooltip content="编辑">
+                <ElButton text icon={EditIcon} onClick={() => handleEdit(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="二维码">
+                <ElButton text icon={QRIcon} onClick={() => showQrCode(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="下载">
+                <ElButton text icon={DownLoadIcon} onClick={() => downloadFile(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="复制链接">
+                <ElButton text icon={copyIcon} onClick={() => onCopy(data.row)} />
+              </ElTooltip>
+              <ElTooltip content="删除">
+                <ElButton text icon={DeleteIcon} type="danger" onClick={() => delData(data.row)} />
+              </ElTooltip>
+            </ElButtonGroup>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @ts-ignore
+const getSchemas = () => {
+  let localSchemas = appStore.getPageData['ImgPage']
+  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 writeRef = ref<ComponentRef<typeof Write>>()
+
+const handleEdit = (row: ImgData) => {
+  currentRow.value = row
+  formState.value = 'edit'
+  dialogVisible.value = true
+}
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    delLoading.value = true
+    try {
+      if (formData.id) {
+        let res = await updateTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      } else {
+        let res = await saveTableApi(formData)
+        if (res) {
+          currentPage.value = 1
+          getList()
+        }
+      }
+    } catch (error) {
+    } finally {
+      delLoading.value = false
+      dialogVisible.value = false
+    }
+  }
+}
+
+const delLoading = ref(false)
+
+const delData = async (row: ImgData) => {
+  if (!row.id) return
+  id.value = row?.id
+  delLoading.value = true
+  await delList(unref(id).length).finally(() => {
+    delLoading.value = false
+  })
+}
+
+const showQrCode = (row: ImgData) => {
+  QrSrc.value = row.bannerUrl
+  QrVisible.value = true
+}
+
+const downloadFile = (row: ImgData) => {
+  window.open(row.bannerUrl)
+}
+
+const onCopy = (row: ImgData) => {
+  navigator.clipboard.writeText(row.bannerUrl).then(() => {
+    ElMessage({
+      message: '复制成功!',
+      type: 'success'
+    })
+  })
+}
+
+const handleAdd = () => {
+  formState.value = 'add'
+  currentRow.value = {
+    bannerUrl: ''
+  }
+  dialogVisible.value = true
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <el-button type="primary" @click="handleAdd">上传文件</el-button>
+      <TableSetting page="ImgPage" :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>
+  <Dialog v-model="dialogVisible" :title="formState == 'add' ? '新增文件' : '编辑文件'">
+    <Write ref="writeRef" :form-schema="allSchemas.formSchema" :current-row="currentRow" />
+    <template #footer>
+      <ElButton type="primary" :loading="delLoading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </ElButton>
+      <ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+  <Dialog v-model="QrVisible" width="430px" title="二维码">
+    <Qrcode
+      :width="395"
+      :options="{
+        color: {
+          dark: '#55D187',
+          light: '#ffffff'
+        }
+      }"
+      :text="QrSrc"
+    />
+    <template #footer>
+      <ElButton @click="QrVisible = false">{{ t('dialogDemo.close') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+@/hooks/event/useEmitt
+
+<style lang="less">
+.uploadBtn {
+  display: inline-block;
+  margin-right: 12px;
+}
+.filePageUploader {
+  width: 100%;
+}
+</style>

+ 80 - 0
src/views/Manage/Img/components/Write.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, reactive, watch } from 'vue'
+import { ImgData } from '@/api/manage/types'
+import { useValidator } from '@/hooks/web/useValidator'
+
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<ImgData>,
+    default: () => undefined
+  },
+  formSchema: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => []
+  }
+})
+
+const rules = reactive({
+  bannerUrl: [required()]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
+
+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
+  }
+}
+
+const resetForm = async () => {
+  const elForm = await getElFormExpose()
+  elForm?.resetFields()
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+    setSchema([
+      {
+        field: 'bannerUrl',
+        path: 'componentProps.fileList',
+        value: props.currentRow?.bannerUrl
+          ? [
+              {
+                url: props.currentRow.bannerUrl,
+                name: props.currentRow.bannerUrl
+              }
+            ]
+          : []
+      }
+    ])
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit,
+  setSchema,
+  setValues,
+  resetForm
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 1 - 1
src/views/Manage/News/NewsAdd.vue

@@ -5,7 +5,7 @@ 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 { saveTableApi } from '@/api/manage/news'
 import { useEmitt } from '@/hooks/event/useEmitt'
 
 const { emitter } = useEmitt()

+ 3 - 3
src/views/Manage/News/NewsDetail.vue

@@ -4,8 +4,8 @@ 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 { getTableDetApi } from '@/api/manage/news'
+import { NewsTableData } from '@/api/manage/types'
 import { ElButton } from 'element-plus'
 
 const { push, go } = useRouter()
@@ -14,7 +14,7 @@ const { query } = useRoute()
 
 const { t } = useI18n()
 
-const currentRow = ref<Nullable<TableData>>(null)
+const currentRow = ref<Nullable<NewsTableData>>(null)
 
 const getTableDet = async () => {
   const res = await getTableDetApi(query.id as string)

+ 4 - 4
src/views/Manage/News/NewsEdit.vue

@@ -5,8 +5,8 @@ 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 { updateTableApi, getTableDetApi } from '@/api/manage/news'
+import { NewsTableData } from '@/api/manage/types'
 import { useEmitt } from '@/hooks/event/useEmitt'
 
 const { emitter } = useEmitt()
@@ -17,7 +17,7 @@ const { query } = useRoute()
 
 const { t } = useI18n()
 
-const currentRow = ref<Nullable<TableData>>(null)
+const currentRow = ref<Nullable<NewsTableData>>(null)
 
 const getTableDet = async () => {
   const res = await getTableDetApi(query.id as string)
@@ -37,7 +37,7 @@ const save = async () => {
   const formData = await write?.submit()
   if (formData) {
     loading.value = true
-    const res = await saveTableApi(formData)
+    const res = await updateTableApi(formData)
       .catch(() => {})
       .finally(() => {
         loading.value = false

+ 123 - 124
src/views/Manage/News/NewsPage.vue

@@ -2,11 +2,11 @@
 import { ContentWrap } from '@/components/ContentWrap'
 import { Search } from '@/components/Search'
 import { useI18n } from '@/hooks/web/useI18n'
-import { ElButton, ElTag } from 'element-plus'
+import { ElButton, ElButtonGroup, ElTooltip } from 'element-plus'
 import { Table } from '@/components/Table'
-import { getTableListApi, delTableListApi } from '@/api/table'
+import { getTableListApi, delTableListApi } from '@/api/manage/news'
 import { useTable } from '@/hooks/web/useTable'
-import { TableData } from '@/api/table/types'
+import { NewsTableData } from '@/api/manage/types'
 import { ref, unref } from 'vue'
 import { useRouter } from 'vue-router'
 import { useEmitt } from '@/hooks/event/useEmitt'
@@ -14,14 +14,17 @@ import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
 import { TableSetting } from '@/components/TableSetting'
 import { usePageStore } from '@/store/modules/page'
 import { set } from 'lodash-es'
-
+import { useIcon } from '@/hooks/web/useIcon'
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const DetailIcon = useIcon({ icon: 'ep:document' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
 defineOptions({
   name: 'NewsPage'
 })
 
 const { push } = useRouter()
 
-const ids = ref<string[]>([])
+const id = ref<string>('')
 
 const searchParams = ref({})
 const setSearchParams = (params: any) => {
@@ -33,22 +36,22 @@ const { tableRegister, tableState, tableMethods } = useTable({
   fetchDataApi: async () => {
     const { currentPage, pageSize } = tableState
     const res = await getTableListApi({
-      pageIndex: unref(currentPage),
+      pageNum: unref(currentPage),
       pageSize: unref(pageSize),
       ...unref(searchParams)
     })
     return {
       list: res.data.list,
-      total: res.data.total
+      total: Number(res.data.totalCount)
     }
   },
   fetchDelApi: async () => {
-    const res = await delTableListApi(unref(ids))
+    const res = await delTableListApi(unref(id))
     return !!res
   }
 })
 const { loading, dataList, total, currentPage, pageSize } = tableState
-const { getList, getElTableExpose, delList, setColumn } = tableMethods
+const { getList, delList, setColumn } = tableMethods
 
 getList()
 
@@ -66,41 +69,9 @@ 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'),
+    field: 'articleTitle',
+    label: '文章标题',
+    minWidth: 150,
     search: {
       component: 'Input'
     },
@@ -118,35 +89,88 @@ const crudSchemas: CrudSchema[] = [
     }
   },
   {
-    field: 'author',
-    label: t('tableDemo.author'),
-    search: {
-      hidden: true
-    },
+    field: 'categoryId',
+    label: '文章类目',
+    width: 120,
     table: {
-      hidden: false
+      hidden: false,
+      slots: {
+        default: (data: any) => {
+          let arr = [
+            {
+              label: '新闻资讯',
+              value: '0'
+            },
+            {
+              label: '经典案例',
+              value: '1'
+            },
+            {
+              label: '解决方案',
+              value: '2'
+            },
+            {
+              label: '技术支持',
+              value: '3'
+            }
+          ]
+          let item = arr.find((e) => {
+            return e.value === data.row.categoryId
+          })
+          return item ? item.label : ''
+        }
+      }
+    },
+    search: {
+      component: 'Select',
+      componentProps: {
+        options: [
+          {
+            label: '新闻资讯',
+            value: '0'
+          },
+          {
+            label: '经典案例',
+            value: '1'
+          },
+          {
+            label: '解决方案',
+            value: '2'
+          },
+          {
+            label: '技术支持',
+            value: '3'
+          }
+        ]
+      }
     }
   },
   {
-    field: 'display_time',
-    label: t('tableDemo.displayTime'),
+    field: 'introduce',
+    label: '文章简介',
+    minWidth: 180,
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'textarea'
+      },
+      colProps: {
+        span: 24
+      }
+    },
     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'),
+    field: 'weight',
+    label: '权重',
+    width: 100,
+    headerAlign: 'center',
+    align: 'right',
     search: {
       hidden: true
     },
@@ -154,50 +178,16 @@ const crudSchemas: CrudSchema[] = [
       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>
-          )
-        }
-      }
+      component: 'InputNumber',
+      value: 0
     }
   },
   {
-    field: 'pageviews',
-    label: t('tableDemo.pageviews'),
+    field: 'articleViews',
+    label: '当前浏览量',
+    width: 100,
+    headerAlign: 'center',
+    align: 'right',
     search: {
       hidden: true
     },
@@ -210,8 +200,9 @@ const crudSchemas: CrudSchema[] = [
     }
   },
   {
-    field: 'content',
-    label: t('exampleDemo.content'),
+    field: 'articleContent',
+    label: '文章内容',
+    minWidth: 120,
     search: {
       hidden: true
     },
@@ -233,9 +224,21 @@ const crudSchemas: CrudSchema[] = [
       }
     }
   },
+  {
+    field: 'remark',
+    label: '备注',
+    minWidth: 120,
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    }
+  },
   {
     field: 'action',
-    width: '260px',
+    width: '160px',
+    fixed: 'right',
     label: t('tableDemo.action'),
     search: {
       hidden: true
@@ -251,17 +254,17 @@ const crudSchemas: CrudSchema[] = [
       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>
-            </>
+            <ElButtonGroup>
+              <ElTooltip content="编辑">
+                <ElButton text icon={EditIcon} onClick={() => action(data.row, 'edit')} />
+              </ElTooltip>
+              <ElTooltip content="详情">
+                <ElButton text icon={DetailIcon} onClick={() => action(data.row, 'detail')} />
+              </ElTooltip>
+              <ElTooltip content="删除">
+                <ElButton text icon={DeleteIcon} type="danger" onClick={() => delData(data.row)} />
+              </ElTooltip>
+            </ElButtonGroup>
           )
         }
       }
@@ -305,16 +308,15 @@ const AddAction = () => {
 
 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) || []
+const delData = async (row: NewsTableData) => {
+  id.value = row?.id || ''
   delLoading.value = true
-  await delList(unref(ids).length).finally(() => {
+  await delList(unref(id).length).finally(() => {
     delLoading.value = false
   })
 }
 
-const action = (row: TableData, type: string) => {
+const action = (row: NewsTableData, type: string) => {
   push(`/manage/news-${type}?id=${row.id}`)
 }
 </script>
@@ -325,9 +327,6 @@ const action = (row: TableData, type: string) => {
 
     <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>
 

+ 22 - 26
src/views/Manage/News/components/Detail.vue

@@ -1,6 +1,6 @@
 <script setup lang="tsx">
 import { PropType, reactive } from 'vue'
-import type { TableData } from '@/api/table/types'
+import type { NewsTableData } from '@/api/manage/types'
 import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
 import { useI18n } from '@/hooks/web/useI18n'
 import { ElTag } from 'element-plus'
@@ -9,55 +9,51 @@ const { t } = useI18n()
 
 defineProps({
   currentRow: {
-    type: Object as PropType<Nullable<TableData>>,
+    type: Object as PropType<Nullable<NewsTableData>>,
     default: () => null
   }
 })
 
 const schema = reactive<DescriptionsSchema[]>([
   {
-    field: 'title',
-    label: '新闻标题',
+    field: 'articleTitle',
+    label: '文章标题',
     span: 24
   },
   {
-    field: 'author',
-    label: t('exampleDemo.author')
+    field: 'introduce',
+    label: '文章简介',
+    span: 24
+  },
+  {
+    field: 'weight',
+    label: '权重'
   },
   {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime')
+    field: 'articleViews',
+    label: '当前浏览量'
   },
   {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
+    field: 'articlePic',
+    label: '文章展图',
     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>
-        )
+        return <img style="width:100%" src={data.articlePic}></img>
       }
     }
   },
   {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews')
+    field: 'remark',
+    label: '备注',
+    span: 24
   },
   {
-    field: 'content',
-    label: t('exampleDemo.content'),
+    field: 'articleContent',
+    label: '文章内容',
     span: 24,
     slots: {
       default: (data: any) => {
-        return <div innerHTML={data.content}></div>
+        return <div innerHTML={data.articleContent}></div>
       }
     }
   }

+ 191 - 54
src/views/Manage/News/components/Write.vue

@@ -1,31 +1,31 @@
 <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 { PropType, reactive, watch, ref } from 'vue'
+import { NewsTableData } from '@/api/manage/types'
 import { useValidator } from '@/hooks/web/useValidator'
 import { IDomEditor } from '@wangeditor/editor'
-import { ElButton } from 'element-plus'
-
+import { useStorage } from '@/hooks/web/useStorage'
+import { Icon } from '@/components/Icon'
+import { uploadFile } from '@/api/common'
 const { required } = useValidator()
-
+const { getStorage } = useStorage()
 const props = defineProps({
   currentRow: {
-    type: Object as PropType<Nullable<TableData>>,
+    type: Object as PropType<Nullable<NewsTableData>>,
     default: () => null
   }
 })
 
-const { t } = useI18n()
+const articlePic = ref(props.currentRow?.articlePic)
 
 const { formRegister, formMethods } = useForm()
 const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
-
+type InsertFnType = (url: string, alt: string, href: string) => void
 const schema = reactive<FormSchema[]>([
   {
-    field: 'title',
-    label: t('exampleDemo.title'),
+    field: 'articleTitle',
+    label: '文章标题',
     component: 'Input',
     formItemProps: {
       rules: [required()]
@@ -35,112 +35,214 @@ const schema = reactive<FormSchema[]>([
     }
   },
   {
-    field: 'author',
-    label: t('exampleDemo.author'),
+    field: 'introduce',
+    label: '文章简介',
     component: 'Input',
     formItemProps: {
       rules: [required()]
     },
+    colProps: {
+      span: 24
+    },
     componentProps: {
-      placeholder: '请输入作者'
+      type: 'textarea',
+      placeholder: '请输入'
     }
   },
   {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime'),
+    field: 'date',
+    label: '日期范围',
     component: 'DatePicker',
-    componentProps: {
-      type: 'datetime',
-      valueFormat: 'YYYY-MM-DD HH:mm:ss'
-    },
     formItemProps: {
       rules: [required()]
+    },
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      type: 'daterange',
+      placeholder: '请输入'
     }
   },
   {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
+    field: 'categoryId',
+    label: '文章类目',
     component: 'Select',
-    formItemProps: {
-      rules: [required()]
+    colProps: {
+      span: 8
     },
     componentProps: {
       options: [
         {
-          label: '重要',
-          value: 3
+          label: '新闻资讯',
+          value: '0'
+        },
+        {
+          label: '经典案例',
+          value: '1'
         },
         {
-          label: '良好',
-          value: 2
+          label: '解决方案',
+          value: '2'
         },
         {
-          label: '一般',
-          value: 1
+          label: '技术支持',
+          value: '3'
         }
       ]
     }
   },
   {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews'),
+    field: 'weight',
+    label: '权重',
+    component: 'InputNumber',
+    value: 0,
+    colProps: {
+      span: 8
+    },
+    formItemProps: {
+      rules: [required()]
+    }
+  },
+  {
+    field: 'articleViews',
+    label: '当前浏览量',
     component: 'InputNumber',
     value: 0,
+    colProps: {
+      span: 8
+    },
     formItemProps: {
       rules: [required()]
     }
   },
   {
-    field: 'imageUrl',
+    field: 'articlePic',
     component: 'Upload',
-    label: '图片',
+    label: '文章展图',
     componentProps: {
-      action: 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15',
+      httpRequest: (data: any) => {
+        let file = data.file
+        let formData = new FormData()
+        formData.append('file', file)
+        uploadFile(formData).then((response) => {
+          setValues({
+            articlePic: response.data.virtualPath
+          })
+          articlePic.value = response.data.virtualPath
+        })
+      },
       showFileList: false,
-      onSuccess: (_response, uploadFile) => {
-        console.log(uploadFile)
+      class: 'NewsUploader',
+      headers: {
+        token: getStorage('token')
+      },
+      onSuccess: (response) => {
+        setValues({
+          articlePic: response.data.virtualPath
+        })
+        articlePic.value = response.data.virtualPath
       },
       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>
+        default: () => (
+          <>
+            {articlePic.value ? <img src={articlePic.value} class="avatar" /> : null}
+            {!articlePic.value ? (
+              <Icon class="avatar-uploader-icon" icon="ep:plus" size={28}></Icon>
+            ) : null}
+          </>
+        )
       }
     }
   },
   {
-    field: 'content',
+    field: 'remark',
+    label: '备注',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      type: 'textarea',
+      placeholder: '请输入'
+    }
+  },
+  {
+    field: 'articleContent',
+    label: '文章内容',
     component: 'Editor',
     colProps: {
       span: 24
     },
     componentProps: {
       defaultHtml: '',
+      editorConfig: {
+        MENU_CONF: {
+          uploadImage: {
+            fieldName: 'file',
+            headers: {
+              token: getStorage('token')
+            },
+            async customUpload(file: File, insertFn: InsertFnType) {
+              // 定义表单数据
+              let formData = new FormData()
+              formData.append('file', file)
+              // 上传文件
+              uploadFile(formData).then((res) => {
+                // 调用 insertFn 方法插入图片
+                insertFn(res.data.virtualPath, res.data.fileName, '')
+              })
+            },
+            customInsert(res: any, insertFn: InsertFnType) {
+              // 从 res 中找到 url alt href ,然后插入图片
+              insertFn(res.data.virtualPath, res.fileName, '')
+            }
+          },
+          uploadVideo: {
+            fieldName: 'file',
+            headers: {
+              token: getStorage('token')
+            },
+            async customUpload(file: File, insertFn: InsertFnType) {
+              let formData = new FormData()
+              formData.append('file', file)
+              uploadFile(formData).then((res) => {
+                // 将视频上传成功后的路径和文件名插入到指定位置
+                insertFn(res.data.virtualPath, res.data.fileName, '')
+              })
+            },
+            customInsert(res: any, insertFn: InsertFnType) {
+              // 从 res 中找到 url alt href ,然后插入视频
+              insertFn(res.data.virtualPath, res.fileName, '')
+            }
+          }
+        }
+      },
       // @ts-ignore
       onChange: (edit: IDomEditor) => {
         setValues({
-          content: edit.getHtml()
+          articleContent: edit.getHtml()
         })
       }
-    },
-    label: t('exampleDemo.content')
+    }
   }
 ])
 
-const rules = reactive({
-  title: [required()],
-  author: [required()],
-  importance: [required()],
-  pageviews: [required()],
-  display_time: [required()],
-  content: [required()]
-})
+const rules = reactive({})
 
+// 定义一个submit函数,用于提交表单
 const submit = async () => {
+  // 获取表单元素
   const elForm = await getElFormExpose()
+  // 调用表单元素的validate方法,获取表单验证结果
   const valid = await elForm?.validate().catch((err) => {
+    // 如果表单验证失败,输出错误信息
     console.log(err)
   })
+  // 如果表单验证成功,获取表单数据
   if (valid) {
     const formData = await getFormData()
+    // 返回表单数据
     return formData
   }
 }
@@ -148,22 +250,26 @@ const submit = async () => {
 watch(
   () => props.currentRow,
   (currentRow) => {
+    // 如果当前行为空,则返回
     if (!currentRow) return
+    // 设置值
     setValues(currentRow)
+    // 设置模式
     setSchema([
       {
-        field: 'content',
+        field: 'articleContent',
         path: 'componentProps.defaultHtml',
-        value: currentRow.content
+        value: currentRow.articleContent
       }
     ])
+    // 设置图片
+    articlePic.value = currentRow.articlePic
   },
   {
     deep: true,
     immediate: true
   }
 )
-
 defineExpose({
   submit
 })
@@ -172,3 +278,34 @@ defineExpose({
 <template>
   <Form :rules="rules" @register="formRegister" :schema="schema" />
 </template>
+
+<style scoped>
+.NewsUploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+</style>
+
+<style lang="less">
+/**添加图片上传组件样式 */
+.NewsUploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+  img {
+    width: 178px;
+    height: 178px;
+  }
+  .avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    text-align: center;
+  }
+}
+</style>

+ 1 - 1
src/views/Manage/Product/ProductAdd.vue

@@ -5,7 +5,7 @@ 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 { saveTableApi } from '@/api/manage/product'
 import { useEmitt } from '@/hooks/event/useEmitt'
 
 const { emitter } = useEmitt()

+ 3 - 3
src/views/Manage/Product/ProductDetail.vue

@@ -4,8 +4,8 @@ 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 { getTableDetApi } from '@/api/manage/product'
+import { ProductTableData } from '@/api/manage/types'
 import { ElButton } from 'element-plus'
 
 const { push, go } = useRouter()
@@ -14,7 +14,7 @@ const { query } = useRoute()
 
 const { t } = useI18n()
 
-const currentRow = ref<Nullable<TableData>>(null)
+const currentRow = ref<Nullable<ProductTableData>>(null)
 
 const getTableDet = async () => {
   const res = await getTableDetApi(query.id as string)

+ 4 - 4
src/views/Manage/Product/ProductEdit.vue

@@ -5,8 +5,8 @@ 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 { updateTableApi, getTableDetApi } from '@/api/manage/product'
+import { ProductTableData } from '@/api/manage/types'
 import { useEmitt } from '@/hooks/event/useEmitt'
 
 const { emitter } = useEmitt()
@@ -17,7 +17,7 @@ const { query } = useRoute()
 
 const { t } = useI18n()
 
-const currentRow = ref<Nullable<TableData>>(null)
+const currentRow = ref<Nullable<ProductTableData>>(null)
 
 const getTableDet = async () => {
   const res = await getTableDetApi(query.id as string)
@@ -37,7 +37,7 @@ const save = async () => {
   const formData = await write?.submit()
   if (formData) {
     loading.value = true
-    const res = await saveTableApi(formData)
+    const res = await updateTableApi(formData)
       .catch(() => {})
       .finally(() => {
         loading.value = false

+ 50 - 126
src/views/Manage/Product/ProductPage.vue

@@ -2,11 +2,11 @@
 import { ContentWrap } from '@/components/ContentWrap'
 import { Search } from '@/components/Search'
 import { useI18n } from '@/hooks/web/useI18n'
-import { ElButton, ElTag } from 'element-plus'
+import { ElButton, ElButtonGroup, ElTooltip } from 'element-plus'
 import { Table } from '@/components/Table'
-import { getTableListApi, delTableListApi } from '@/api/table'
+import { getTableListApi, delTableListApi } from '@/api/manage/product'
 import { useTable } from '@/hooks/web/useTable'
-import { TableData } from '@/api/table/types'
+import { ProductTableData } from '@/api/table/types'
 import { ref, unref } from 'vue'
 import { useRouter } from 'vue-router'
 import { useEmitt } from '@/hooks/event/useEmitt'
@@ -14,6 +14,7 @@ import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
 import { TableSetting } from '@/components/TableSetting'
 import { usePageStore } from '@/store/modules/page'
 import { set } from 'lodash-es'
+import { useIcon } from '@/hooks/web/useIcon'
 
 defineOptions({
   name: 'ProductPage'
@@ -21,7 +22,11 @@ defineOptions({
 
 const { push } = useRouter()
 
-const ids = ref<string[]>([])
+const EditIcon = useIcon({ icon: 'ep:edit' })
+const DetailIcon = useIcon({ icon: 'ep:document' })
+const DeleteIcon = useIcon({ icon: 'ep:delete' })
+
+const id = ref<string>('')
 
 const searchParams = ref({})
 const setSearchParams = (params: any) => {
@@ -33,22 +38,22 @@ const { tableRegister, tableState, tableMethods } = useTable({
   fetchDataApi: async () => {
     const { currentPage, pageSize } = tableState
     const res = await getTableListApi({
-      pageIndex: unref(currentPage),
+      pageNum: unref(currentPage),
       pageSize: unref(pageSize),
       ...unref(searchParams)
     })
     return {
       list: res.data.list,
-      total: res.data.total
+      total: Number(res.data.totalCount)
     }
   },
   fetchDelApi: async () => {
-    const res = await delTableListApi(unref(ids))
+    const res = await delTableListApi(unref(id))
     return !!res
   }
 })
 const { loading, dataList, total, currentPage, pageSize } = tableState
-const { getList, getElTableExpose, delList, setColumn } = tableMethods
+const { getList, delList, setColumn } = tableMethods
 
 getList()
 
@@ -66,41 +71,8 @@ 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'),
+    field: 'productName',
+    label: '产品名称',
     search: {
       component: 'Input'
     },
@@ -118,35 +90,27 @@ const crudSchemas: CrudSchema[] = [
     }
   },
   {
-    field: 'author',
-    label: t('tableDemo.author'),
-    search: {
-      hidden: true
+    field: 'productIntroduction',
+    label: '产品简介',
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'textarea'
+      },
+      colProps: {
+        span: 24
+      }
     },
-    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'),
+    field: 'productPrice',
+    label: '产品价格',
     search: {
       hidden: true
     },
@@ -154,50 +118,13 @@ const crudSchemas: CrudSchema[] = [
       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>
-          )
-        }
-      }
+      component: 'InputNumber',
+      value: 0
     }
   },
   {
-    field: 'pageviews',
-    label: t('tableDemo.pageviews'),
+    field: 'productStock',
+    label: '产品库存',
     search: {
       hidden: true
     },
@@ -210,8 +137,8 @@ const crudSchemas: CrudSchema[] = [
     }
   },
   {
-    field: 'content',
-    label: t('exampleDemo.content'),
+    field: 'productDetailed',
+    label: '产品详情',
     search: {
       hidden: true
     },
@@ -235,7 +162,7 @@ const crudSchemas: CrudSchema[] = [
   },
   {
     field: 'action',
-    width: '260px',
+    width: '160px',
     label: t('tableDemo.action'),
     search: {
       hidden: true
@@ -251,17 +178,17 @@ const crudSchemas: CrudSchema[] = [
       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>
-            </>
+            <ElButtonGroup>
+              <ElTooltip content="编辑">
+                <ElButton text icon={EditIcon} onClick={() => action(data.row, 'edit')} />
+              </ElTooltip>
+              <ElTooltip content="详情">
+                <ElButton text icon={DetailIcon} onClick={() => action(data.row, 'detail')} />
+              </ElTooltip>
+              <ElTooltip content="删除">
+                <ElButton text icon={DeleteIcon} type="danger" onClick={() => delData(data.row)} />
+              </ElTooltip>
+            </ElButtonGroup>
           )
         }
       }
@@ -305,16 +232,16 @@ const AddAction = () => {
 
 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) || []
+const delData = async (row: ProductTableData) => {
+  if (!row.id) return
+  id.value = row?.id
   delLoading.value = true
-  await delList(unref(ids).length).finally(() => {
+  await delList(unref(id).length).finally(() => {
     delLoading.value = false
   })
 }
 
-const action = (row: TableData, type: string) => {
+const action = (row: ProductTableData, type: string) => {
   push(`/manage/product-${type}?id=${row.id}`)
 }
 </script>
@@ -325,9 +252,6 @@ const action = (row: TableData, type: string) => {
 
     <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>
 

+ 32 - 30
src/views/Manage/Product/components/Detail.vue

@@ -1,63 +1,65 @@
 <script setup lang="tsx">
 import { PropType, reactive } from 'vue'
-import type { TableData } from '@/api/table/types'
+import type { ProductTableData } from '@/api/manage/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>>,
+    type: Object as PropType<Nullable<ProductTableData>>,
     default: () => null
   }
 })
 
 const schema = reactive<DescriptionsSchema[]>([
   {
-    field: 'title',
-    label: t('exampleDemo.title'),
+    field: 'productName',
+    label: '产品名称',
+    span: 24
+  },
+  {
+    field: 'productIntroduction',
+    label: '产品简介',
+
     span: 24
   },
   {
-    field: 'author',
-    label: t('exampleDemo.author')
+    field: 'productPrice',
+    label: '产品价格'
   },
   {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime')
+    field: 'productStock',
+    label: '库存'
   },
   {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
+    field: 'productSpecifications',
+    label: '产品规格'
+  },
+  {
+    field: 'productCharacteristic',
+    label: '产品特点'
+  },
+  {
+    field: 'productPic',
+    label: '产品图片',
     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>
-        )
+        return <img style="width:100%" src={data.productPic}></img>
       }
     }
   },
   {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews')
+    field: 'remark',
+    label: '备注',
+    span: 24
   },
+
   {
-    field: 'content',
-    label: t('exampleDemo.content'),
+    field: 'productDetailed',
+    label: '产品详情',
     span: 24,
     slots: {
       default: (data: any) => {
-        return <div innerHTML={data.content}></div>
+        return <div innerHTML={data.productDetailed}></div>
       }
     }
   }

+ 160 - 56
src/views/Manage/Product/components/Write.vue

@@ -1,30 +1,32 @@
-<script setup lang="ts">
+<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 { PropType, reactive, watch, ref } from 'vue'
+import { ProductTableData } from '@/api/manage/types'
 import { useValidator } from '@/hooks/web/useValidator'
 import { IDomEditor } from '@wangeditor/editor'
-
+import { useStorage } from '@/hooks/web/useStorage'
+import { Icon } from '@/components/Icon'
+import { uploadFile } from '@/api/common'
 const { required } = useValidator()
-
+const { getStorage } = useStorage()
 const props = defineProps({
   currentRow: {
-    type: Object as PropType<Nullable<TableData>>,
+    type: Object as PropType<Nullable<ProductTableData>>,
     default: () => null
   }
 })
-
-const { t } = useI18n()
+const productPic = ref(props.currentRow?.productPic)
 
 const { formRegister, formMethods } = useForm()
 const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
 
+type InsertFnType = (url: string, alt: string, href: string) => void
+
 const schema = reactive<FormSchema[]>([
   {
-    field: 'title',
-    label: t('exampleDemo.title'),
+    field: 'productName',
+    label: '产品名称',
     component: 'Input',
     formItemProps: {
       rules: [required()]
@@ -34,87 +36,158 @@ const schema = reactive<FormSchema[]>([
     }
   },
   {
-    field: 'author',
-    label: t('exampleDemo.author'),
+    field: 'productIntroduction',
+    label: '产品简介',
     component: 'Input',
     formItemProps: {
       rules: [required()]
     },
+    colProps: {
+      span: 24
+    },
     componentProps: {
-      placeholder: '请输入作者'
+      type: 'textarea',
+      placeholder: '请输入'
     }
   },
   {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime'),
-    component: 'DatePicker',
-    componentProps: {
-      type: 'datetime',
-      valueFormat: 'YYYY-MM-DD HH:mm:ss'
-    },
+    field: 'productPrice',
+    label: '产品价格',
+    component: 'InputNumber',
+    value: 0,
     formItemProps: {
       rules: [required()]
     }
   },
   {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
-    component: 'Select',
+    field: 'productStock',
+    label: '库存',
+    component: 'InputNumber',
+    value: 0,
     formItemProps: {
       rules: [required()]
-    },
+    }
+  },
+  {
+    field: 'productSpecifications',
+    label: '产品规格',
+    component: 'Input'
+  },
+  {
+    field: 'productCharacteristic',
+    label: '产品特点',
+    component: 'Input'
+  },
+  {
+    field: 'productPic',
+    component: 'Upload',
+    label: '产品图片',
     componentProps: {
-      options: [
-        {
-          label: '重要',
-          value: 3
-        },
-        {
-          label: '良好',
-          value: 2
-        },
-        {
-          label: '一般',
-          value: 1
-        }
-      ]
+      httpRequest: (data: any) => {
+        let file = data.file
+        let formData = new FormData()
+        formData.append('file', file)
+        uploadFile(formData).then((response) => {
+          setValues({
+            productPic: response.data.virtualPath
+          })
+          productPic.value = response.data.virtualPath
+        })
+      },
+      showFileList: false,
+      class: 'productUploader',
+      headers: {
+        token: getStorage('token')
+      },
+      onSuccess: (response) => {
+        setValues({
+          productPic: response.data.virtualPath
+        })
+        productPic.value = response.data.virtualPath
+      },
+      slots: {
+        default: () => (
+          <>
+            {productPic.value ? <img src={productPic.value} class="avatar" /> : null}
+            {!productPic.value ? (
+              <Icon class="avatar-uploader-icon" icon="ep:plus" size={28}></Icon>
+            ) : null}
+          </>
+        )
+      }
     }
   },
   {
-    field: 'pageviews',
-    label: t('exampleDemo.pageviews'),
-    component: 'InputNumber',
-    value: 0,
-    formItemProps: {
-      rules: [required()]
+    field: 'remark',
+    label: '备注',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      type: 'textarea',
+      placeholder: '请输入'
     }
   },
   {
-    field: 'content',
+    field: 'productDetailed',
     component: 'Editor',
     colProps: {
       span: 24
     },
     componentProps: {
       defaultHtml: '',
+      editorConfig: {
+        MENU_CONF: {
+          uploadImage: {
+            async customUpload(file: File, insertFn: InsertFnType) {
+              let formData = new FormData()
+              formData.append('file', file)
+              uploadFile(formData).then((res) => {
+                insertFn(res.data.virtualPath, res.data.fileName, '')
+              })
+            },
+            fieldName: 'file',
+            headers: {
+              token: getStorage('token')
+            },
+            customInsert(res: any, insertFn: InsertFnType) {
+              // 从 res 中找到 url alt href ,然后插入图片
+              insertFn(res.data.virtualPath, res.fileName, '')
+            }
+          },
+          uploadVideo: {
+            async customUpload(file: File, insertFn: InsertFnType) {
+              let formData = new FormData()
+              formData.append('file', file)
+              uploadFile(formData).then((res) => {
+                insertFn(res.data.virtualPath, res.data.fileName, '')
+              })
+            },
+            fieldName: 'file',
+            headers: {
+              token: getStorage('token')
+            },
+            customInsert(res: any, insertFn: InsertFnType) {
+              // 从 res 中找到 url alt href ,然后插入视频
+              insertFn(res.data.virtualPath, res.fileName, '')
+            }
+          }
+        }
+      },
       // @ts-ignore
       onChange: (edit: IDomEditor) => {
         setValues({
-          content: edit.getHtml()
+          productDetailed: edit.getHtml()
         })
       }
     },
-    label: t('exampleDemo.content')
+    label: '产品详情'
   }
 ])
 
 const rules = reactive({
-  title: [required()],
-  author: [required()],
-  importance: [required()],
-  pageviews: [required()],
-  display_time: [required()],
-  content: [required()]
+  productName: [required()]
 })
 
 const submit = async () => {
@@ -135,11 +208,12 @@ watch(
     setValues(currentRow)
     setSchema([
       {
-        field: 'content',
+        field: 'productDetailed',
         path: 'componentProps.defaultHtml',
-        value: currentRow.content
+        value: currentRow.productDetailed
       }
     ])
+    productPic.value = currentRow.productPic
   },
   {
     deep: true,
@@ -155,3 +229,33 @@ defineExpose({
 <template>
   <Form :rules="rules" @register="formRegister" :schema="schema" />
 </template>
+
+<style scoped>
+.productUploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+</style>
+
+<style lang="less">
+.productUploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+  img {
+    width: 178px;
+    height: 178px;
+  }
+  .avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    text-align: center;
+  }
+}
+</style>

+ 3 - 3
src/views/hooks/useTagsView.vue

@@ -11,7 +11,7 @@ const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage,
 
 const closeAllTabs = () => {
   closeAll(() => {
-    push('/dashboard/analysis')
+    push('/dashboard/workplace')
   })
 }
 
@@ -33,7 +33,7 @@ const refresh = () => {
 
 const closeCurrentTab = () => {
   closeCurrent(undefined, () => {
-    push('/dashboard/analysis')
+    push('/dashboard/workplace')
   })
 }
 
@@ -42,7 +42,7 @@ const setTabTitle = () => {
 }
 
 const setAnalysisTitle = () => {
-  setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/analysis')
+  setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/workplace')
 }
 </script>
 

+ 2 - 1
types/global.d.ts

@@ -45,7 +45,8 @@ declare global {
 
   declare interface IResponse<T = any> {
     code: string
-    data: T extends any ? T : T & any
+    data: T extends any ? T : T & any,
+    token: string
   }
 
   declare interface ThemeTypes {

+ 14 - 14
vite.config.ts

@@ -6,7 +6,7 @@ import VueJsx from '@vitejs/plugin-vue-jsx'
 import progress from 'vite-plugin-progress'
 import EslintPlugin from 'vite-plugin-eslint'
 import { ViteEjsPlugin } from "vite-plugin-ejs"
-import { viteMockServe } from 'vite-plugin-mock'
+// import { viteMockServe } from 'vite-plugin-mock'
 import PurgeIcons from 'vite-plugin-purge-icons'
 import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite"
 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
@@ -64,17 +64,17 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
         svgoOptions: true
       }),
       PurgeIcons(),
-      viteMockServe({
-        ignore: /^\_/,
-        mockPath: 'mock',
-        localEnabled: !isBuild,
-        prodEnabled: isBuild,
-        injectCode: `
-          import { setupProdMockServer } from '../mock/_createProductionServer'
+      // viteMockServe({
+      //   ignore: /^\_/,
+      //   mockPath: 'mock',
+      //   localEnabled: !isBuild,
+      //   prodEnabled: isBuild,
+      //   injectCode: `
+      //     import { setupProdMockServer } from '../mock/_createProductionServer'
 
-          setupProdMockServer()
-          `
-      }),
+      //     setupProdMockServer()
+      //     `
+      // }),
       DefineOptions(),
       ViteEjsPlugin({
         title: env.VITE_APP_TITLE
@@ -120,10 +120,10 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       port: 4000,
       proxy: {
         // 选项写法
-        '/api': {
-          target: 'http://127.0.0.1:8000',
+        '^/api': {
+          target: 'http://api.dacundianzi.com/',
           changeOrigin: true,
-          rewrite: path => path.replace(/^\/api/, '')
+          rewrite: path => path
         }
       },
       hmr: {