Browse Source

处理 产品管理页面

王飞 1 year ago
parent
commit
256f762954

+ 1 - 1
src/api/department/types.ts

@@ -28,7 +28,7 @@ export interface DepartmentUserItem {
 
 export interface DepartmentUserResponse {
   list: DepartmentUserItem[]
-  total: number
+  totalCount: number
 }
 
 export interface UserParams {

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

@@ -6,7 +6,7 @@ interface RoleParams {
 }
 
 export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
-  return request.post({ url: '/api/login/login', data })
+  return request.post({ url: '/api/login/login', data, headersType: 'application/json' })
 }
 
 export const loginOutApi = (userId: string): Promise<IResponse> => {

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

@@ -0,0 +1,14 @@
+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: `/sysApk/deleteApkById/${id}` })
+}
+
+export const saveTableApi = (data: any) => {
+  return request.post({ url: '/api/sysApk/addSysApk', data })
+}

+ 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}` })
+}

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

@@ -0,0 +1,21 @@
+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
+}
+

+ 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>

+ 16 - 5
src/config/axios/config.ts

@@ -5,11 +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 } = useStorage()
+const { getStorage, clear } = useStorage()
+const tagsViewStore = useTagsViewStore()
 
 const config: AxiosConfig = {
   /**
@@ -80,21 +84,28 @@ const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
   if (getStorage('token')) {
     config.headers.token = getStorage('token')
   }
-  console.log(config)
   return config
 }
 ;(error: AxiosError) => {
-  console.log(error)
   Promise.reject(error)
 }
 
 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') {
+    ElMessageBox.alert(response.data.message, '提示', {
+      showClose: false,
+      callback: () => {
+        clear()
+        tagsViewStore.delAllViews()
+        resetRouter() // 重置静态路由表
+        router.replace('/login')
+      }
+    })
   } else {
     ElMessage.error(response.data.message)
   }

+ 10 - 2
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'
@@ -183,6 +183,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/manage/product-page'
         }
+      },
+      {
+        path: 'file-page',
+        component: () => import('@/views/Manage/File/FilePage.vue'),
+        name: 'FilePage',
+        meta: {
+          title: '文件管理'
+        }
       }
     ]
   },
@@ -243,7 +251,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
 ]
 
 const router = createRouter({
-  history: createWebHistory(),
+  history: createWebHashHistory(),
   strict: true,
   routes: constantRouterMap as RouteRecordRaw[],
   scrollBehavior: () => ({ left: 0, top: 0 })

+ 15 - 12
src/views/Authorization/User/User.vue

@@ -2,7 +2,7 @@
 import { ContentWrap } from '@/components/ContentWrap'
 import { useI18n } from '@/hooks/web/useI18n'
 import { Table } from '@/components/Table'
-import { ref, unref, watch, reactive } from 'vue'
+import { ref, unref, watch, reactive, nextTick } from 'vue'
 import { ElButton, ElTree } from 'element-plus'
 import { getUserList, addUser, updataUser, deleteUserById, getRoleApi } from '@/api/department'
 import type { DepartmentUserItem } from '@/api/department/types'
@@ -25,7 +25,7 @@ const { tableRegister, tableState, tableMethods } = useTable({
     })
     return {
       list: res.data.list || [],
-      total: res.data.total || 0
+      total: res.data.totalCount || 0
     }
   },
   fetchDelApi: async () => {
@@ -85,7 +85,6 @@ const crudSchemas = reactive<CrudSchema[]>([
       component: 'Select',
       optionApi: async () => {
         const res = await getRoleApi()
-        console.log(res)
         return res.data.map((e) => {
           return {
             label: e.roleDesc,
@@ -164,7 +163,8 @@ const crudSchemas = reactive<CrudSchema[]>([
       hidden: true
     },
     table: {
-      width: 240,
+      width: 160,
+      fixed: 'right',
       slots: {
         default: (data: any) => {
           const row = data[0].row as DepartmentUserItem
@@ -173,9 +173,6 @@ const crudSchemas = reactive<CrudSchema[]>([
               <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>
@@ -230,16 +227,15 @@ const delData = async (row: DepartmentUserItem) => {
     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 () => {
@@ -259,7 +255,6 @@ const save = async () => {
         getList()
       }
     } catch (error) {
-      console.log(error)
     } finally {
       saveLoading.value = false
       dialogVisible.value = false
@@ -297,7 +292,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"
       />
 

+ 3 - 2
src/views/Authorization/User/components/Write.vue

@@ -24,7 +24,7 @@ const rules = reactive({
 })
 
 const { formRegister, formMethods } = useForm()
-const { setValues, getFormData, getElFormExpose } = formMethods
+const { setValues, getFormData, getElFormExpose, setSchema } = formMethods
 
 const submit = async () => {
   const elForm = await getElFormExpose()
@@ -50,7 +50,8 @@ watch(
 )
 
 defineExpose({
-  submit
+  submit,
+  setSchema
 })
 </script>
 

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

@@ -0,0 +1,391 @@
+<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { Search } from '@/components/Search'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElButton, ElTooltip } from 'element-plus'
+import { Table } from '@/components/Table'
+import { getTableListApi, delTableListApi, saveTableApi } 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'
+
+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 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>()
+
+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: res.data.total
+    }
+  },
+  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 crudSchemas: CrudSchema[] = [
+  {
+    field: 'name',
+    label: '名称',
+
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    field: 'version',
+    label: '版本号',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    }
+  },
+  {
+    field: 'code',
+    label: '版本序列号',
+    table: {
+      hidden: false
+    },
+    search: {
+      hidden: true
+    },
+    form: {
+      component: 'InputNumber'
+    }
+  },
+  {
+    field: 'fileName',
+    label: '文件名',
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'virtualPath',
+    label: '文件链接',
+
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      component: 'Upload',
+      componentProps: {
+        action: '/api/sysApk/uploadFile',
+        limit: 1,
+        class: 'productUploader',
+        headers: {
+          token: getStorage('token')
+        },
+        onSuccess: (response) => {
+          currentRow.value = {
+            ...currentRow.value,
+            ...response.data
+          }
+        },
+        onExceed: (_files, responses) => {
+          currentRow.value = {
+            ...currentRow.value,
+            ...responses[0].response.data
+          }
+        },
+        slots: {
+          default: () => <ElButton type="primary">上传文件</ElButton>
+        }
+      }
+    }
+  },
+  {
+    field: 'filePath',
+    label: '文件路径',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      hidden: true
+    }
+  },
+  {
+    field: 'content',
+    label: '版本更新说明',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      componentProps: {
+        type: 'textarea'
+      }
+    }
+  },
+  {
+    field: 'remark',
+    label: '备注',
+    search: {
+      hidden: true
+    },
+    table: {
+      hidden: false
+    },
+    form: {
+      colProps: {
+        span: 24
+      },
+      componentProps: {
+        type: 'textarea'
+      }
+    }
+  },
+  {
+    field: 'createTime',
+    label: '创建时间',
+    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,
+      slots: {
+        default: (data: any) => {
+          return (
+            <>
+              <ElTooltip content="二维码">
+                <ElButton text icon={QRIcon} onClick={() => showQrCode(data)}></ElButton>
+              </ElTooltip>
+              <ElTooltip content="下载">
+                <ElButton text icon={DownLoadIcon} onClick={() => downloadFile(data)}></ElButton>
+              </ElTooltip>
+              <ElTooltip content="复制链接">
+                <ElButton text icon={copyIcon}></ElButton>
+              </ElTooltip>
+              <ElButton
+                icon={DeleteIcon}
+                text
+                type="danger"
+                onClick={() => delData(data[0].row)}
+              ></ElButton>
+            </>
+          )
+        }
+      }
+    }
+  }
+]
+
+// @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 save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    delLoading.value = true
+    try {
+      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) => {
+  console.log(row)
+  window.open(row.virtualPath)
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
+
+    <div class="mb-10px">
+      <el-button type="primary" @click="dialogVisible = true">上传文件</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="新增文件">
+    <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;
+}
+</style>

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

@@ -0,0 +1,60 @@
+<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
+  }
+}
+
+watch(
+  () => props.currentRow,
+  (currentRow) => {
+    if (!currentRow) return
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit,
+  setSchema,
+  setValues
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 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

+ 32 - 113
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 } 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'
@@ -21,7 +21,7 @@ defineOptions({
 
 const { push } = useRouter()
 
-const ids = ref<string[]>([])
+const id = ref<string>('')
 
 const searchParams = ref({})
 const setSearchParams = (params: any) => {
@@ -33,7 +33,7 @@ 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)
     })
@@ -43,12 +43,12 @@ const { tableRegister, tableState, tableMethods } = useTable({
     }
   },
   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 +66,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 +85,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 +113,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 +132,8 @@ const crudSchemas: CrudSchema[] = [
     }
   },
   {
-    field: 'content',
-    label: t('exampleDemo.content'),
+    field: 'productDetailed',
+    label: '产品详情',
     search: {
       hidden: true
     },
@@ -305,16 +227,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 +247,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>
 

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

@@ -1,63 +1,66 @@
 <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: 'productStock',
+    label: '库存'
+  },
+  {
+    field: 'productSpecifications',
+    label: '产品规格'
   },
   {
-    field: 'display_time',
-    label: t('exampleDemo.displayTime')
+    field: 'productCharacteristic',
+    label: '产品特点'
   },
   {
-    field: 'importance',
-    label: t('exampleDemo.importance'),
+    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>
       }
     }
   }

+ 137 - 53
src/views/Manage/Product/components/Write.vue

@@ -1,30 +1,31 @@
-<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'
 
 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 +35,139 @@ 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
-        }
-      ]
+      action: '/api/sysApk/uploadFile',
+      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,
+    field: 'remark',
+    label: '备注',
+    component: 'Input',
     formItemProps: {
       rules: [required()]
+    },
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      type: 'textarea',
+      placeholder: '请输入'
     }
   },
   {
-    field: 'content',
+    field: 'productDetailed',
     component: 'Editor',
     colProps: {
       span: 24
     },
     componentProps: {
       defaultHtml: '',
+      editorConfig: {
+        MENU_CONF: {
+          uploadImage: {
+            server: '/api/sysApk/uploadFile',
+            fieldName: 'file',
+            headers: {
+              token: getStorage('token')
+            },
+            customInsert(res: any, insertFn: InsertFnType) {
+              // 从 res 中找到 url alt href ,然后插入图片
+              insertFn(res.data.virtualPath, res.fileName, '')
+            }
+          },
+          uploadVideo: {
+            server: '/api/sysApk/uploadFile',
+            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 +188,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 +209,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>

+ 2 - 2
vite.config.ts

@@ -120,10 +120,10 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       port: 4000,
       proxy: {
         // 选项写法
-        '/api': {
+        '^/api': {
           target: 'http://192.168.0.131:8081',
           changeOrigin: true,
-          rewrite: path => path.replace(/^\/api/, '')
+          rewrite: path => path
         }
       },
       hmr: {