fxs 8 月之前
父节点
当前提交
5eb22d8237
共有 34 个文件被更改,包括 1879 次插入424 次删除
  1. 3 0
      components.d.ts
  2. 17 1
      src/assets/css/common.css
  3. 1 0
      src/assets/icon/file/folder-active.svg
  4. 1 0
      src/assets/icon/file/folder-default.svg
  5. 1 0
      src/assets/icon/file/project-active.svg
  6. 1 0
      src/assets/icon/file/project-default.svg
  7. 二进制
      src/assets/icon/file/video-icon.png
  8. 14 9
      src/components/promotion/MenuTable.vue
  9. 167 95
      src/components/table/PromotionTable.vue
  10. 148 25
      src/components/table/TableQueryForm.vue
  11. 4 0
      src/config/API.ts
  12. 9 33
      src/hooks/File/useFileUpload.ts
  13. 75 0
      src/hooks/Material/useMaterial.ts
  14. 10 10
      src/hooks/useMenuTable.ts
  15. 93 50
      src/hooks/useTable.ts
  16. 4 1
      src/hooks/useTableScroll.ts
  17. 13 0
      src/router/material.ts
  18. 2 2
      src/types/Tables/customIndicatorDialog.ts
  19. 89 17
      src/types/Tables/table.ts
  20. 2 1
      src/types/Tables/tableData/ttAd.ts
  21. 78 0
      src/utils/file/handleImgFile.ts
  22. 224 0
      src/utils/file/handleVideoFile.ts
  23. 0 1
      src/views/Index.vue
  24. 227 30
      src/views/Material/FilmLibrary.vue
  25. 294 90
      src/views/Material/UploadAsset.vue
  26. 278 0
      src/views/Material/config/FimLibraryConfig.ts
  27. 23 0
      src/views/Material/types/filmLibraryType.ts
  28. 42 3
      src/views/Material/types/uploadFileType.ts
  29. 9 9
      src/views/Promotion/accManage/accTencentAd.vue
  30. 14 16
      src/views/Promotion/accManage/accTtAd.vue
  31. 14 16
      src/views/Promotion/adManage/tencentAd.vue
  32. 11 8
      src/views/Promotion/adManage/ttad.vue
  33. 11 7
      src/views/Promotion/promotion.vue
  34. 0 0
      stats.html

+ 3 - 0
components.d.ts

@@ -33,6 +33,8 @@ declare module 'vue' {
     ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
@@ -49,6 +51,7 @@ declare module 'vue' {
     MenuTable: typeof import('./src/components/promotion/MenuTable.vue')['default']
     MyButton: typeof import('./src/components/form/MyButton.vue')['default']
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
+    PromotionTable: typeof import('./src/components/table/PromotionTable.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     StatisticCard: typeof import('./src/components/statisticCard/StatisticCard.vue')['default']

+ 17 - 1
src/assets/css/common.css

@@ -1,3 +1,19 @@
-.w220{
+.w220 {
     width: 220px !important;
+}
+
+.w120 {
+    width: 120px !important;
+}
+
+.w240{
+    width: 240px !important;
+}
+
+.h30{
+    height: 30px !important;
+}
+
+.bottomBorder {
+    border-bottom: 1px solid #e8eaec;
 }

+ 1 - 0
src/assets/icon/file/folder-active.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742787869354" class="icon" viewBox="0 0 1204 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16052" xmlns:xlink="http://www.w3.org/1999/xlink" width="235.15625" height="200"><path d="M561.63235285 164.57352927h335.01838211c27.29779432 0 49.63235285 22.33455854 49.63235366 49.63235284v62.04044147c0 27.29779432-22.33455854 49.63235285-49.63235366 49.63235284H561.63235285c-27.29779432 0-49.63235285-22.33455854-49.63235285-49.63235284v-62.04044147c0-27.29779432 22.33455854-49.63235285 49.63235285-49.63235284z" fill="#AFFCFE" p-id="16053"></path><path d="M983.50735284 933.875H189.38970568c-54.59558862 0-99.26470568-44.66911789-99.26470568-99.26470568V189.38970568c0-54.59558862 44.66911789-99.26470568 99.26470568-99.26470568h285.38602928c68.24448496 18.61213211 54.59558862 53.35477927 99.2647065 99.26470568l31.02021992 49.63235286H983.50735284c54.59558862 0 99.26470568 44.66911789 99.26470569 99.2647065v496.32352928c0 54.59558862-44.66911789 99.26470568-99.26470569 99.26470568z" fill="#2F77F1" p-id="16054"></path></svg>

+ 1 - 0
src/assets/icon/file/folder-default.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742787843454" class="icon" viewBox="0 0 1204 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15793" xmlns:xlink="http://www.w3.org/1999/xlink" width="235.15625" height="200"><path d="M561.63235285 164.57352927h335.01838211c27.29779432 0 49.63235285 22.33455854 49.63235366 49.63235284v62.04044147c0 27.29779432-22.33455854 49.63235285-49.63235366 49.63235284H561.63235285c-27.29779432 0-49.63235285-22.33455854-49.63235285-49.63235284v-62.04044147c0-27.29779432 22.33455854-49.63235285 49.63235285-49.63235284z" fill="#8a8a8a" p-id="15794"></path><path d="M983.50735284 933.875H189.38970568c-54.59558862 0-99.26470568-44.66911789-99.26470568-99.26470568V189.38970568c0-54.59558862 44.66911789-99.26470568 99.26470568-99.26470568h285.38602928c68.24448496 18.61213211 54.59558862 53.35477927 99.2647065 99.26470568l31.02021992 49.63235286H983.50735284c54.59558862 0 99.26470568 44.66911789 99.26470569 99.2647065v496.32352928c0 54.59558862-44.66911789 99.26470568-99.26470569 99.26470568z" fill="#8a8a8a" p-id="15795"></path></svg>

+ 1 - 0
src/assets/icon/file/project-active.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742785436544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12527" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M505.6 477.952c-12.288 0-24.32-2.304-35.84-7.168L67.328 305.92c-11.264-4.608-18.944-15.616-18.944-27.904s6.912-23.552 17.92-28.672l400.128-183.296c26.368-12.032 56.32-12.032 82.688-0.256l398.08 179.2c11.264 5.12 18.176 16.128 18.176 28.416s-7.68 23.296-18.944 27.904l-404.224 169.216c-11.776 4.864-24.064 7.424-36.608 7.424z" fill="#5396FF" p-id="12528"></path><path d="M490.496 523.264c0.256 0.256 0.512 0.256 1.024 0.512 8.96 4.608 19.712 4.608 28.672 0 0.256-0.256 0.512-0.256 1.024-0.512l181.248-72.96 65.536-28.672 73.728-30.976 112.384 65.536c9.984 5.632 15.872 16.384 15.36 27.904-0.512 11.52-7.168 21.504-17.664 26.624L547.84 701.44c-13.056 6.656-27.648 9.984-41.984 9.984-14.336 0-28.672-3.328-41.728-9.728L64.256 514.56c-10.496-4.864-17.152-15.104-17.664-26.624s5.632-22.272 15.616-27.904l113.92-64 78.08 32.512" fill="#5396FF" p-id="12529"></path><path d="M489.984 768.768c11.264 5.632 24.576 5.632 35.584 0l146.944-75.008 77.056-39.68 65.024-33.024h0.256l140.8 80.64c11.264 6.4 17.408 19.456 14.848 32.768-1.792 9.472-8.448 17.408-16.896 21.76l-399.872 205.568c-14.336 7.424-30.208 11.008-46.08 11.008-15.616 0-31.488-3.584-45.568-11.008l-396.8-202.24c-9.984-5.12-16.384-15.36-16.64-26.624-0.256-11.264 5.632-21.76 15.616-27.392l133.888-76.288h0.256l70.144 32.512" fill="#5396FF" p-id="12530"></path></svg>

+ 1 - 0
src/assets/icon/file/project-default.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742785436544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12527" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M505.6 477.952c-12.288 0-24.32-2.304-35.84-7.168L67.328 305.92c-11.264-4.608-18.944-15.616-18.944-27.904s6.912-23.552 17.92-28.672l400.128-183.296c26.368-12.032 56.32-12.032 82.688-0.256l398.08 179.2c11.264 5.12 18.176 16.128 18.176 28.416s-7.68 23.296-18.944 27.904l-404.224 169.216c-11.776 4.864-24.064 7.424-36.608 7.424z" fill="#8a8a8a" p-id="12528"></path><path d="M490.496 523.264c0.256 0.256 0.512 0.256 1.024 0.512 8.96 4.608 19.712 4.608 28.672 0 0.256-0.256 0.512-0.256 1.024-0.512l181.248-72.96 65.536-28.672 73.728-30.976 112.384 65.536c9.984 5.632 15.872 16.384 15.36 27.904-0.512 11.52-7.168 21.504-17.664 26.624L547.84 701.44c-13.056 6.656-27.648 9.984-41.984 9.984-14.336 0-28.672-3.328-41.728-9.728L64.256 514.56c-10.496-4.864-17.152-15.104-17.664-26.624s5.632-22.272 15.616-27.904l113.92-64 78.08 32.512" fill="#8a8a8a" p-id="12529"></path><path d="M489.984 768.768c11.264 5.632 24.576 5.632 35.584 0l146.944-75.008 77.056-39.68 65.024-33.024h0.256l140.8 80.64c11.264 6.4 17.408 19.456 14.848 32.768-1.792 9.472-8.448 17.408-16.896 21.76l-399.872 205.568c-14.336 7.424-30.208 11.008-46.08 11.008-15.616 0-31.488-3.584-45.568-11.008l-396.8-202.24c-9.984-5.12-16.384-15.36-16.64-26.624-0.256-11.264 5.632-21.76 15.616-27.392l133.888-76.288h0.256l70.144 32.512" fill="#8a8a8a" p-id="12530"></path></svg>

二进制
src/assets/icon/file/video-icon.png


+ 14 - 9
src/components/promotion/MenuTable.vue

@@ -13,10 +13,10 @@ import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndi
 import { computed, onMounted, ref } from 'vue'
 
 import Menu from '../navigation/Menu.vue'
-import Table from '../table/Table.vue'
+import PromotionTable from '../table/PromotionTable.vue'
 
 // 自定义表格的类型
-type TableType = InstanceType<typeof Table>
+type TableType = InstanceType<typeof PromotionTable>
 
 interface MenuTableProps {
   saveTableName: string // 保存的表格的名称,后面会拼接上每个菜单的名,这个主要用于保存自定义指标方案
@@ -38,6 +38,8 @@ const props = withDefaults(defineProps<MenuTableProps>(), {
   excludeFields: () => ({}),
 })
 
+const tableContainer = ref<HTMLElement | null>(null)
+
 // 表格ref
 const tableRef = ref<TableType | null>(null)
 
@@ -114,13 +116,14 @@ const {
 
 const emits = defineEmits(['changeDate'])
 
-onMounted(() => {
-  initData()
+onMounted(async () => {
+  await initData()
+  console.log(props.tablePaginationConfig)
 })
 </script>
 
 <template>
-  <div class="menuTableContainer">
+  <div class="menuTableContainer" ref="tableContainer">
     <div class="menuTableHeader">
       <Menu
         :default-active="menuList[0].name"
@@ -148,8 +151,9 @@ onMounted(() => {
       </div>
     </div>
     <div class="menuTableContent">
-      <Table
+      <PromotionTable
         ref="tableRef"
+        :table-container="tableContainer"
         :active-menu="activeMenu"
         :pagination-config="tablePaginationConfig"
         :table-fields="tableInfo[activeMenu].fields"
@@ -159,6 +163,7 @@ onMounted(() => {
         :filters-info="tableInfo[activeMenu].filters"
         :scheme-list="customIndicatorScheme"
         :exclude-export-fields="props.excludeFields[activeMenu]"
+        :remote-pagination="tableInfo[activeMenu].remote"
         @update-custom-indicator="updateCustomIndicator"
         @change-scheme="changeScheme"
         @query-table="queryTable"
@@ -175,7 +180,7 @@ onMounted(() => {
             </el-button>
           </div>
         </template>
-      </Table>
+      </PromotionTable>
     </div>
     <el-dialog
       v-model="operationDialog"
@@ -193,8 +198,8 @@ onMounted(() => {
 <style scoped>
 .menuTableContainer {
   width: 100%;
-  height: 100%;
-  overflow: hidden;
+  /*height: 100%;*/
+  /*overflow: scroll;*/
 }
 
 .menuTableHeader {

+ 167 - 95
src/components/table/Table.vue → src/components/table/PromotionTable.vue

@@ -1,19 +1,20 @@
 <script setup lang="ts">
-import type {
-  BaseFieldItem,
-  TableData,
-  TableFields,
-  TableProps,
-} from '@/types/Tables/table'
-import type { PaginationConfig } from '@/types/Tables/pagination'
-
-import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
-
-import { Operation, Plus } from '@element-plus/icons-vue'
 import { useTable } from '@/hooks/useTable'
 import { useTableScroll } from '@/hooks/useTableScroll'
+import type { PaginationConfig } from '@/types/Tables/pagination'
+import {
+  type BaseFieldItem,
+  type TableData,
+  type TableFields,
+  TableFieldType,
+  type TableProps,
+} from '@/types/Tables/table'
 import { generateUniqueFilename } from '@/utils/common'
 
+import { Operation, Plus } from '@element-plus/icons-vue'
+
+import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
+
 import customIndicatorDialog from '../dialog/customIndicatorDialog.vue'
 import TableQueryForm from './TableQueryForm.vue'
 
@@ -23,6 +24,7 @@ type CustomIndicatorDialog = InstanceType<typeof customIndicatorDialog>
 const props = withDefaults(defineProps<TableProps>(), {
   remotePagination: false,
   excludeExportFields: () => ['action'],
+  needExport: true,
 })
 
 // tableRef
@@ -61,7 +63,6 @@ const emits = defineEmits([
 ])
 
 const {
-  isFixedField,
   initTableFields,
   initIndicatorFields,
   setCacheTableData,
@@ -91,8 +92,8 @@ const cacheTableData = reactive<Array<Array<TableData>>>([])
 // 表格分页信息
 const paginationConfig = reactive<PaginationConfig>({
   curPage: 1,
-  curPageSize: 0,
-  pageSizeList: [],
+  curPageSize: props.paginationConfig.pageSizeList[0],
+  pageSizeList: props.paginationConfig.pageSizeList,
   total: 0,
 })
 
@@ -134,25 +135,33 @@ const defaultActiveNav = ref<string>('')
 // 当前选中的方案名
 const schemeActive = ref<string>('默认')
 
-// 批量操作选中的值
-const batchOper = ref<string>()
+const sortInfo = reactive<{
+  prop: string
+  order: any
+}>({
+  prop: '',
+  order: null,
+})
 
-// 批量操作的选项数组
-const batchOperList = reactive<
-  Array<{
-    label: string
-    value: string
-  }>
->([
-  {
-    label: '删除',
-    value: 'delete',
-  },
-  {
-    label: '新增',
-    value: 'add',
-  },
-])
+// // 批量操作选中的值
+// const batchOper = ref<string>()
+//
+// // 批量操作的选项数组
+// const batchOperList = reactive<
+//   Array<{
+//     label: string
+//     value: string
+//   }>
+// >([
+//   {
+//     label: '删除',
+//     value: 'delete',
+//   },
+//   {
+//     label: '新增',
+//     value: 'add',
+//   },
+// ])
 
 // 导出对话框的可见性
 const exportDialogVisble = ref<boolean>(false)
@@ -170,13 +179,15 @@ const nowScheme = computed(() => {
 const excludeFields = computed<Array<string>>(() => {
   let result: Array<string> = []
   props.tableFields.forEach(item => {
-    item.children.forEach(child => {
-      if (
-        props.excludeExportFields.includes(child.name) &&
-        child.name !== 'action'
-      )
-        result.push(child.label)
-    })
+    if (item.children) {
+      item.children.forEach(child => {
+        if (
+          props.excludeExportFields.includes(child.name) &&
+          child.name !== 'action'
+        )
+          result.push(child.label)
+      })
+    }
   })
   return result
 })
@@ -184,6 +195,7 @@ const excludeFields = computed<Array<string>>(() => {
 // 分页后的表格数据
 const paginationTableData = computed<Array<TableData>>(() => {
   let result: Array<TableData> = []
+
   if (props.remotePagination) {
     result = cacheTableData[paginationConfig.curPage - 1]
   } else {
@@ -191,53 +203,55 @@ const paginationTableData = computed<Array<TableData>>(() => {
     let pageSize = paginationConfig.curPageSize
     result = tableData.slice((curPage - 1) * pageSize, curPage * pageSize)
   }
+
   return result
 })
 
 /**
  * @description: 查询表格
- * @return {*}
  */
-const queryTable = (queryParams: any) => {
-  emits('queryTable', queryParams)
+const queryTable = () => {
+  const nQueryParams = getTableReqParams()
+  emits('queryTable', nQueryParams)
 }
 
 /**
  * @description: 初始化分页配置项
- * @return {*}
  */
 const initPagination = () => {
   Object.assign(paginationConfig, { ...props.paginationConfig })
   paginationConfig.curPageSize = paginationConfig.pageSizeList[0]
+
+  if (!props.remotePagination) {
+    paginationConfig.total = props.tableData.length
+  }
 }
 
 /**
  * @description: 表格分页大小改变
- * @return {*}
  */
-const tableSizeChange = (size: number) => {
+const tableSizeChange = (_size: number) => {
+  paginationConfig.curPage = 1
   // 如果当前是远程分页,则需要通知父组件重新请求数据
+
   if (props.remotePagination) {
-    emits('pageSizeChange', size)
+    emits('queryTable')
   }
-  // setScrollAndHeader()
-  // setScrollPos(true)
-  paginationConfig.curPage = 1
 }
 
 /**
  * @description: 表格页数改变
- * @return {*}
  */
 const tablePageChange = (page: number) => {
   if (props.remotePagination) {
-    emits('curPageChange', page)
+    if (!cacheTableData[page - 1]) {
+      emits('queryTable')
+    }
   }
 }
 
 /**
  * @description: 展示自定义指标弹窗
- * @return {*}
  */
 const showCustomIndicator = () => {
   if (!customIndicatorDialogRef.value) return
@@ -246,8 +260,7 @@ const showCustomIndicator = () => {
 
 /**
  * @description: 应用自定义指标
- * @param {*} newTableFieldsInfo 新的表格字段信息
- * @return {*}
+ * @param newTableFieldsInfo 新的表格字段信息
  */
 const applyCustomIndicator = (
   newTableFieldsInfo: Array<TableFields>,
@@ -262,8 +275,7 @@ const applyCustomIndicator = (
 
 /**
  * @description: 更换方案
- * @param {*} schemeName 方案name
- * @return {*}
+ * @param schemeName 方案name
  */
 const changeScheme = (schemeName: string) => {
   schemeActive.value = schemeName
@@ -272,7 +284,6 @@ const changeScheme = (schemeName: string) => {
 
 /**
  * @description: 更新指标方案
- * @return {*}
  */
 const updateIndicatorScheme = () => {
   nextTick(() => {
@@ -283,7 +294,6 @@ const updateIndicatorScheme = () => {
 
 /**
  * @description: 导出表格数据
- * @return {*}
  */
 const exportData = () => {
   exportDataToExcel(
@@ -297,8 +307,7 @@ const exportData = () => {
 
 /**
  * @description: 更新表格状态
- * @param {*} state
- * @return {*}
+ * @param state
  */
 const changeTableLoading = (state: boolean) => {
   tableLoading.value = state
@@ -306,11 +315,17 @@ const changeTableLoading = (state: boolean) => {
 
 /**
  * @description: 获取当前的查询参数
- * @return {*}
  */
 const getTableReqParams = () => {
   if (tableQueryFormRef.value) {
-    return tableQueryFormRef.value.getParams()
+    const params = tableQueryFormRef.value.getParams()
+    return {
+      ...params,
+      offset: paginationConfig.curPageSize * (paginationConfig.curPage - 1),
+      limit: paginationConfig.curPageSize,
+      prop: sortInfo.prop,
+      order: sortInfo.order,
+    }
   } else {
     return {}
   }
@@ -318,7 +333,6 @@ const getTableReqParams = () => {
 
 /**
  * @description: 初始化查询框
- * @return {*}
  */
 const initQueryForm = () => {
   if (tableQueryFormRef.value) {
@@ -327,11 +341,25 @@ const initQueryForm = () => {
   }
 }
 
+/**
+ * 排序发生变化时触发
+ * @param data 排序信息
+ */
+const sortChange = (data: { column: any; prop: string; order: any }) => {
+  const { prop, order } = data
+  // emits('sortTable', data)
+  sortInfo.prop = prop
+  sortInfo.order = order
+
+  queryTable()
+}
+
 watch(
   () => props.tableData,
   newData => {
+    cacheTableData.splice(0, cacheTableData.length)
     tableData.splice(0, tableData.length, ...newData)
-    initPagination()
+
     setCacheTableData(
       props.remotePagination,
       paginationConfig,
@@ -347,6 +375,26 @@ watch(
 )
 
 watch(
+  () => props.paginationConfig,
+  newConfig => {
+    Object.assign(paginationConfig, { ...newConfig })
+  },
+  {
+    deep: true,
+  },
+)
+
+watch(
+  () => props.tableFields,
+  () => {
+    updateIndicatorScheme()
+  },
+  {
+    deep: true,
+  },
+)
+
+const watchActiveMenu = watch(
   () => props.activeMenu,
   () => {
     nextTick(() => {
@@ -358,11 +406,17 @@ watch(
   { deep: true },
 )
 
+if (!props.activeMenu) {
+  watchActiveMenu()
+}
+
 onMounted(() => {
   initScroll()
-
+  initPagination()
   tableVisOb.observe(tableContent.value as HTMLElement)
+
   tableSizeOb.observe(tableContainer.value as HTMLElement)
+  // setScrollAndHeader()
 })
 
 defineExpose({
@@ -384,36 +438,36 @@ defineExpose({
         <div class="tableOperationLeft">
           <slot name="addItem">
             <el-button color="#197afb" class="addItem w120" :icon="Plus"
-              >添加账户</el-button
-            >
+              >添加账户
+            </el-button>
           </slot>
           <slot name="batchOper">
-            <el-select
-              class="batchOper w120"
-              v-model="batchOper"
-              placeholder="批量操作"
-              :disabled="tableLoading"
-            >
-              <el-option
-                v-for="item in batchOperList"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
+            <!--            <el-select-->
+            <!--              class="batchOper w120"-->
+            <!--              v-model="batchOper"-->
+            <!--              placeholder="批量操作"-->
+            <!--              :disabled="tableLoading"-->
+            <!--            >-->
+            <!--              <el-option-->
+            <!--                v-for="item in batchOperList"-->
+            <!--                :key="item.value"-->
+            <!--                :label="item.label"-->
+            <!--                :value="item.value"-->
+            <!--              />-->
+            <!--            </el-select>-->
           </slot>
         </div>
         <div class="tableOperationRight">
-          <slot name="exportData">
+          <slot name="exportData" v-if="props.needExport">
             <el-button
               class="exportData w120 ml16"
               plain
               @click="exportDialogVisble = true"
               :disabled="tableLoading"
-              >导出数据</el-button
-            >
+              >导出数据
+            </el-button>
           </slot>
-          <slot name="customIndicator">
+          <slot name="customIndicator" v-if="props.schemeList">
             <el-popover
               placement="bottom"
               trigger="hover"
@@ -438,8 +492,8 @@ defineExpose({
                   class="customIndicator w120 ml16"
                   plain
                   :icon="Operation"
-                  >自定义指标</el-button
-                >
+                  >自定义指标
+                </el-button>
               </template>
             </el-popover>
           </slot>
@@ -457,6 +511,7 @@ defineExpose({
           :header-row-style="() => `color:black`"
           ref="tableRef"
           id="table"
+          @sort-change="sortChange"
         >
           <template v-for="item in tableFieldsInfo" :key="item.name">
             <el-table-column
@@ -467,21 +522,37 @@ defineExpose({
               show-overflow-tooltip
             >
               <template #default="scope">
-                <slot name="operations" :row="scope.row"> </slot>
+                <slot name="operations" :row="scope.row"></slot>
               </template>
             </el-table-column>
+            <!--                        :width="-->
+            <!--                        isFixedField(props.fixedFields, item)-->
+            <!--                        ? tableFieldLWidth-->
+            <!--                        : tableFieldSWidth-->
+            <!--                        "-->
             <el-table-column
               show-overflow-tooltip
-              :width="
-                isFixedField(props.fixedFields, item)
-                  ? tableFieldLWidth
-                  : tableFieldSWidth
-              "
-              :fixed="isFixedField(props.fixedFields, item)"
+              :width="item.fixed ? tableFieldLWidth : tableFieldSWidth"
+              :fixed="item.fixed"
               :prop="item.name"
               :label="item.label"
               v-if="item.state && item.name !== 'action'"
+              :sortable="item.sort ? 'custom' : false"
             >
+              <template
+                #default="scope"
+                v-if="item.type === TableFieldType.Tag"
+              >
+                <el-tag
+                  v-for="tag in (scope.row['tag'] ?? []).map(
+                    (v: any) => v.name,
+                  )"
+                  style="margin-right: 5px"
+                  type="primary"
+                >
+                  {{ tag === '' ? '空' : tag }}
+                </el-tag>
+              </template>
             </el-table-column>
           </template>
         </el-table>
@@ -535,7 +606,7 @@ defineExpose({
         <template #footer>
           <div class="dialog-footer">
             <el-button @click="exportDialogVisble = false">取消</el-button>
-            <el-button @click="exportData" color="#197afb"> 确定 </el-button>
+            <el-button @click="exportData" color="#197afb"> 确定</el-button>
           </div>
         </template>
       </el-dialog>
@@ -556,7 +627,7 @@ defineExpose({
   width: 96%;
   height: 92%;
   margin: 0 auto;
-  overflow: auto;
+  //overflow: auto;
 }
 
 .testContainer {
@@ -611,6 +682,7 @@ defineExpose({
   align-items: center;
   justify-content: flex-start;
 }
+
 .batchOper {
   margin: 0 12px;
 }

+ 148 - 25
src/components/table/TableQueryForm.vue

@@ -1,15 +1,15 @@
 <script setup lang="ts">
+import { useMaterial } from '@/hooks/Material/useMaterial.ts'
+import { useDate } from '@/hooks/useDate'
 import type { TableFilterItem } from '@/types/Tables/table'
 import { TableFilterType } from '@/types/Tables/table'
 
-import { onMounted, reactive, ref } from 'vue'
+import { resetReactive, throttleFunc } from '@/utils/common'
+import { type FormInstance } from 'element-plus'
 
-import { resetReactive } from '@/utils/common'
-import { useDate } from '@/hooks/useDate'
-import { Search } from '@element-plus/icons-vue'
-import { throttleFunc } from '@/utils/common'
-import type { FormInstance } from 'element-plus'
+import { onMounted, reactive, ref, watch } from 'vue'
 
+const { createFilter } = useMaterial()
 const { shortcuts, disableDate } = useDate()
 
 interface TableQueryFormProps {
@@ -28,6 +28,11 @@ const throttleIntervalTime = 1000
 // 表单ref
 const filterFormRef = ref<FormInstance>()
 
+// 级联下拉框配置
+const cascadePropsConfig = {
+  expandTrigger: 'hover' as const,
+}
+
 // 表单数据
 const filterFormData = reactive<{
   [key: string]: any
@@ -42,6 +47,14 @@ const filterFieldsStateList = reactive<
   }>
 >([])
 
+const computedFilterFields = reactive<
+  Array<{
+    label: string
+    state: boolean
+    value: any
+  }>
+>([])
+
 // 表单搜索框所对应的字段,因为可能是多选,会在初始化的时候给初值
 const searchSelected = ref('')
 
@@ -136,7 +149,6 @@ const getParams = () => {
 
 /**
  * @description: 查询表格
- * @return {*}
  */
 const queryTable = () => {
   let queryParams = getParams()
@@ -157,6 +169,55 @@ const resetFilterForm = () => {
 
 // 节流版本重置
 const throttleReset = throttleFunc(resetFilterForm, throttleIntervalTime)
+//
+// /**
+//  * 标签搜索
+//  * @param queryString 搜索值
+//  * @param cb 回调
+//  * @param options 标签列表
+//  */
+// const tagSearch = (queryString: string, cb: any, options: TagItem[]) => {
+//   const result = queryString
+//     ? options.filter(createFilter(queryString))
+//     : options
+//
+//   cb(result)
+// }
+
+// const handleTagSelect = (tags: TagItem, name: string) => {
+//   filterFormData[name].push(tags.id)
+// }
+//
+// /**
+//  * 清空标签输入框
+//  */
+// const clearTagInput = (name: string) => {
+//   filterFormData[name] = []
+// }
+
+/**
+ * 新增tag
+ * @param val 传新增值
+ */
+// const tagAdd = async (val: string, name: string, options: TagItem[]) => {
+//   const isExist = options.find(item => item.name === val)
+//   if (!isExist) {
+//     filterFormData[name].splice(filterFormData[name].length - 1, 1)
+//     ElMessage.warning('标签不存在')
+//     return
+//   }
+// }
+
+watch(
+  () => props.filtersInfo,
+  () => {
+    initFilterForm()
+    initFilterFields()
+  },
+  {
+    deep: true,
+  },
+)
 
 onMounted(() => {
   initFilterForm()
@@ -179,7 +240,7 @@ defineExpose({
         ref="filterFormRef"
         :inline="true"
       >
-        <el-form-item style="height: 30px">
+        <el-form-item>
           <el-input
             :readonly="true"
             placeholder="筛选字段"
@@ -208,53 +269,85 @@ defineExpose({
         </el-form-item>
 
         <template v-for="item in filtersInfo" :key="item.name">
-          <el-form-item :prop="item.name" style="height: 30px">
+          <el-form-item :prop="item.name">
             <el-input
               v-model="filterFormData[item.name]"
               style="max-width: 600px"
               placeholder="输入关键字搜索"
-              class="input-with-select"
+              class="filterItem w240"
               v-if="
                 filterFields.includes(item.name) &&
                 item.type === TableFilterType.Search
               "
             >
               <template #prepend>
-                <el-select v-model="searchSelected" style="width: 115px">
+                <el-select
+                  v-model="searchSelected"
+                  v-if="item.needPrefixSelect ?? false"
+                  style="min-width: 80px"
+                >
                   <el-option
                     v-for="option in item.options"
                     :label="option.label"
                     :value="option.value"
                   />
                 </el-select>
-              </template>
-              <template #append>
-                <el-button :icon="Search" />
+                <span v-else style="color: #606266">{{ item.label }}:</span>
               </template>
             </el-input>
 
             <el-select
-              class="filterItem"
+              :empty-values="[null, undefined]"
+              class="filterItem w240"
               v-if="
                 filterFields.includes(item.name) &&
                 item.type === TableFilterType.Select
               "
               v-model="filterFormData[item.name]"
-              placeholder="Select"
-              style="width: 240px"
+              placeholder="请选择"
+              :multiple="item.isMultiple ?? false"
+              :collapse-tags="item.isTags ?? false"
+              :collapse-tags-tooltip="item.isTags ?? false"
+              :max-collapse-tags="item.maxTagCount ?? 1"
+              filterable
             >
-              <template #label="{ label }">
-                <span>{{ item.label }}: </span>
-                <span style="margin-left: 10px">{{ label }}</span>
+              <template #prefix>
+                <span style="color: #606266">{{ item.label }}:</span>
               </template>
               <el-option
                 v-for="option in item.options"
                 :key="option.value"
                 :label="option.label"
                 :value="option.value"
-              />
+              >
+              </el-option>
             </el-select>
 
+            <el-radio-group
+              v-model="filterFormData[item.name]"
+              v-if="
+                filterFields.includes(item.name) &&
+                item.type === TableFilterType.Radio
+              "
+            >
+              <el-radio
+                v-for="option in item.options"
+                :value="option.value"
+                size="large"
+                >{{ option.label }}
+              </el-radio>
+            </el-radio-group>
+
+            <el-cascader
+              v-if="
+                filterFields.includes(item.name) &&
+                item.type === TableFilterType.Cascader
+              "
+              :options="item.options"
+              clearable
+              :props="cascadePropsConfig"
+            />
+
             <div
               class="dateItem"
               v-if="
@@ -277,17 +370,43 @@ defineExpose({
                 :clearable="false"
               />
             </div>
+
+            <!--标签-废弃-->
+            <!--            <el-select-->
+            <!--              v-if="-->
+            <!--                filterFields.includes(item.name) &&-->
+            <!--                item.type === TableFilterType.InputSearchMultiple-->
+            <!--              "-->
+            <!--              v-model="filterFormData[item.name]"-->
+            <!--              multiple-->
+            <!--              collapse-tags-->
+            <!--              collapse-tags-tooltip-->
+            <!--              :max-collapse-tags="2"-->
+            <!--              placeholder="Select"-->
+            <!--              style="width: 240px"-->
+            <!--              filterable-->
+            <!--            >-->
+            <!--                          <template #prefix>-->
+            <!--                            <span style="color: #606266">标签:</span>-->
+            <!--                          </template>-->
+            <!--              <el-option-->
+            <!--                v-for="option in item.options"-->
+            <!--                :key="option.id"-->
+            <!--                :label="option.name"-->
+            <!--                :value="option.id"-->
+            <!--              />-->
+            <!--            </el-select>-->
           </el-form-item>
         </template>
       </el-form>
     </div>
     <div class="filterButtonContainer">
       <el-button class="queryBtn" color="#197afb" @click="throttleQueryTable"
-        >查询</el-button
-      >
+        >查询
+      </el-button>
       <el-button class="queryBtn" color="#626aef" plain @click="throttleReset"
-        >重置</el-button
-      >
+        >重置
+      </el-button>
     </div>
   </div>
 </template>
@@ -332,4 +451,8 @@ defineExpose({
   display: inline-block;
   margin-right: 5px;
 }
+
+.inputTag {
+  margin-top: 10px;
+}
 </style>

+ 4 - 0
src/config/API.ts

@@ -6,4 +6,8 @@ export const MaterialAPI = {
   uploadAsset: `${baseURL}/property/upload`, // 上传文件
   imgSubmit: `${baseURL}/property/imageSet`, // 图片提交
   videoSubmit: `${baseURL}/property/videoSet`, // 视频提交
+  imgList: `${baseURL}/property/imageList`, // 图片列表
+  videoList: `${baseURL}/property/videoList`, // 视频列表
+  imgFilterInfo: `${baseURL}/property/getImageListHead`, // 图片列表筛选信息
+  videoFilterInfo: `${baseURL}/property/getVideoListHead`,
 }

+ 9 - 33
src/hooks/File/useFileUpload.ts

@@ -1,16 +1,6 @@
-import { MaterialAPI } from '@/config/API.ts'
-import axiosInstance from '@/utils/axios/axiosInstance.ts'
-import type {
-  TagsRes,
-  UploadAssetForm,
-} from '@/views/Material/types/uploadFileType.ts'
+import type { UploadAssetForm } from '@/views/Material/types/uploadFileType.ts'
 
-import type {
-  UploadFile,
-  UploadFiles,
-  UploadRawFile,
-  UploadUserFile,
-} from 'element-plus'
+import type { UploadFile, UploadFiles, UploadRawFile } from 'element-plus'
 import { type Reactive, type Ref } from 'vue'
 
 export function useFileUpload() {
@@ -58,21 +48,18 @@ export function useFileUpload() {
   }
 
   const handleFile = (
-    file: UploadFile | UploadRawFile,
-    fileList: UploadUserFile[] | UploadFiles,
+    file: UploadFile,
+    fileList: UploadFiles,
     isVideo: Ref<boolean>,
     formData: Reactive<UploadAssetForm>,
+    previewUrl: Ref<string>,
   ) => {
-    console.log(JSON.parse(JSON.stringify(formData)))
     if (!isValidFile(file)) {
       fileList.splice(1)
       ElMessage.warning('上传文件超出限制或不是规定文件')
       return false
     }
-    const fileType =
-      'raw' in file
-        ? (file as UploadFile).raw?.type
-        : (file as UploadRawFile).type
+    const fileType = file.raw?.type
     if (!fileType) {
       ElMessage.warning('请上传有效文件')
       return false
@@ -85,24 +72,14 @@ export function useFileUpload() {
     isVideo.value = type === 'video'
     // 仅用于过表单检测
     formData.filePath = file.name
-    console.log(JSON.parse(JSON.stringify(formData)))
+
+    previewUrl.value = URL.createObjectURL(file.raw as Blob)
+
     // nextTick(() => {
     //   formRef.value?.validateField('filePath')
     // })
   }
 
-  const getAllTags = async () => {
-    const res = (await axiosInstance.post(
-      MaterialAPI.getAllTags,
-      {},
-    )) as TagsRes
-    if (res.code !== 0) {
-      ElMessage.error('获取标签失败')
-      return []
-    }
-    return res.data
-  }
-
   const validateFile = (rule: any, value: any, callback: any) => {
     console.log(rule)
     console.log(value)
@@ -121,7 +98,6 @@ export function useFileUpload() {
     isValidFile,
     getFileCategory,
     handleFile,
-    getAllTags,
     validateFile,
   }
 }

+ 75 - 0
src/hooks/Material/useMaterial.ts

@@ -0,0 +1,75 @@
+import { MaterialAPI } from '@/config/API.ts'
+import type { ResponseInfo } from '@/types/axios.ts'
+import axiosInstance from '@/utils/axios/axiosInstance.ts'
+import type {
+  GameData,
+  GameSelectItem,
+  TagItem,
+  TagsRes,
+} from '@/views/Material/types/uploadFileType.ts'
+import type { Reactive } from 'vue'
+
+export function useMaterial() {
+  const getGameInfo = async () => {
+    const res = (await axiosInstance.post(
+      MaterialAPI.getAllGamesInfo,
+      {},
+    )) as ResponseInfo
+    if (res.code !== 0) {
+      console.error('获取游戏信息失败')
+      return null
+    }
+    return res.data as GameData
+  }
+
+  const generateGameSelect = (
+    gameInfoList: GameData,
+    gameSelect: Reactive<GameSelectItem[]>,
+  ) => {
+    gameSelect.splice(0, gameSelect.length)
+    for (let [k, v] of Object.entries(gameInfoList)) {
+      const children = v.map(item => {
+        return {
+          value: item.gid,
+          label: item.gameName,
+        }
+      })
+      gameSelect.push({
+        value: k,
+        label: k === '' ? '默认' : k,
+        children,
+      })
+    }
+  }
+
+  const getAllTags = async () => {
+    const res = (await axiosInstance.post(
+      MaterialAPI.getAllTags,
+      {},
+    )) as TagsRes
+    if (res.code !== 0) {
+      ElMessage.error('获取标签失败')
+      return []
+    }
+    return res.data
+  }
+
+  /**
+   * 构建过滤函数
+   * @param queryString 查询值
+   */
+  const createFilter = (queryString: string) => {
+    return (restaurant: TagItem) => {
+      return (
+        restaurant.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0
+      )
+    }
+  }
+
+  return {
+    getGameInfo,
+    generateGameSelect,
+    getAllTags,
+    createFilter,
+  }
+}

+ 10 - 10
src/hooks/useMenuTable.ts

@@ -1,3 +1,7 @@
+import PromotionTable from '@/components/table/PromotionTable.vue'
+import type { BaseMenu } from '@/types/Promotion/Menu'
+import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type {
   BaseFieldItem,
   CustomIndicatorScheme,
@@ -5,16 +9,12 @@ import type {
   TableFields,
 } from '@/types/Tables/table'
 import type { BaseTableInfo } from '@/types/Tables/tablePageData'
+import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndicatorController'
 import type { ComputedRef, Reactive, Ref } from 'vue'
-import type { TablePaginationProps } from '@/types/Tables/pagination'
-import type { BaseMenu } from '@/types/Promotion/Menu'
-import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
 
 import { useTable } from './useTable'
-import Table from '@/components/table/Table.vue'
-import TableCustomIndicatorController from '@/utils/localStorage/tableCustomIndicatorController'
 
-type TableType = InstanceType<typeof Table>
+type TableType = InstanceType<typeof PromotionTable>
 
 export function useMenuTable(
   saveName: string, // 保存的文件名
@@ -35,7 +35,6 @@ export function useMenuTable(
 
   /**
    * @description: 更新表格数据
-   * @return {*}
    */
   const updateTableData = async () => {
     try {
@@ -53,7 +52,9 @@ export function useMenuTable(
             tableInfo[activeMenu.value].data.length,
             ...res,
           )
+          console.log(res)
           tablePaginationConfig.total = res.length
+          console.log(tablePaginationConfig)
         },
       )
     } catch (err) {
@@ -149,11 +150,10 @@ export function useMenuTable(
 
   /**
    * @description: 初始化数据
-   * @return {*}
    */
-  const initData = () => {
+  const initData = async () => {
     initTableController()
-    updateTableData()
+    await updateTableData()
     customIndicatorScheme.value =
       tableControllers[fileName.value].getSchemeList()
   }

+ 93 - 50
src/hooks/useTable.ts

@@ -1,25 +1,28 @@
-import type { TableData, BaseFieldInfo } from '@/types/Tables/table'
-import type { BaseTableInfo } from '@/types/Tables/tablePageData'
 import type { ResponseInfo } from '@/types/axios'
-import type { BaseFieldItem, TableFields } from '@/types/Tables/table'
-import type { Ref } from 'vue'
-import type { PaginationConfig } from '@/types/Tables/pagination'
 import type { TableReq } from '@/types/Tables/MenuTable/menuTableReq'
-
-import { resetReactive } from '@/utils/common'
-import { writeFileXLSX, utils } from 'xlsx'
+import type { PaginationConfig } from '@/types/Tables/pagination'
+import {
+  type BaseFieldInfo,
+  type BaseFieldItem,
+  type TableData,
+  type TableFields,
+  TableFieldType,
+} from '@/types/Tables/table'
+import type { BaseTableInfo } from '@/types/Tables/tablePageData'
 
 import axiosInstance from '@/utils/axios/axiosInstance'
 
+import { resetReactive } from '@/utils/common'
+import type { Ref } from 'vue'
+import { utils, writeFileXLSX } from 'xlsx'
+
 export function useTable() {
   let controller: AbortController | null = null // 请求控制器,用于取消请求
   let tableReqList: Array<string> = [] // 表格的请求列表,暂时只存了url,可以存其他的
 
   /**
    * @description: 获取表格数据
-   * @param {string} url 请求地址
-   * @param {Object} config 配置
-   * @return {*}
+   * @param reqInfo 请求信息
    */
   const getData = async (reqInfo: TableReq): Promise<Array<TableData>> => {
     try {
@@ -44,41 +47,44 @@ export function useTable() {
 
   /**
    * @description: 判断当前字段是否是需要固定的字段
-   * @param {*} info 字段信息
-   * @return {boolean}
+   * @param  fixedFields 固定字段信息
+   * @param info 字段信息
+   * @return 是否需要固定
    */
   const isFixedField = (
-    fixedFields: Array<string>,
+    fixedFields: Array<string> | undefined,
     info: BaseFieldInfo,
   ): boolean => {
-    return fixedFields.includes(info.name)
+    return !!fixedFields && fixedFields.includes(info.name)
   }
 
   /**
    * @description: 更新自定义指标和表格字段
-   * @param {*} newIndicator  新的指标信息
-   * @param {*} newSortedTablefileds 新的表格字段信息
-   * @return {*}
+   * @param  tableInfo 表格信息
+   * @param  activeName 当前激活的导航
+   * @param  newTableFiles  新的表格字段信息
+   * @param  newSortedTableFields 新的表格字段信息
+   * @return 更新是否成功
    */
   const updateTableFields = (
     tableInfo: BaseTableInfo,
     activeName: string,
     newTableFiles: Array<BaseFieldItem<TableFields>>,
-    newSortedTablefileds: Array<TableFields>,
+    newSortedTableFields: Array<TableFields>,
   ): Promise<boolean> => {
-    return new Promise((reslove, reject) => {
+    return new Promise((resolve, reject) => {
       try {
-        let oldTablefields = tableInfo[activeName].fields
+        let oldTableFields = tableInfo[activeName].fields
         let oldSortedFields = tableInfo[activeName].tableSortedFields
 
-        oldTablefields.splice(0, oldTablefields.length, ...newTableFiles)
+        oldTableFields.splice(0, oldTableFields.length, ...newTableFiles)
 
         oldSortedFields.splice(
           0,
           oldSortedFields.length,
-          ...newSortedTablefileds,
+          ...newSortedTableFields,
         )
-        reslove(true)
+        resolve(true)
       } catch (err) {
         console.log('更新失败')
         console.log(err)
@@ -89,11 +95,13 @@ export function useTable() {
 
   /**
    * @description: 初始化自定义指标的字段信息
-   * @return {*}
+   * @param indicatorFields 自定义指标字段信息
+   * @param propsTableFields 表格字段信息
+   * @param defaultActiveNav 默认激活的导航
    */
   const initIndicatorFields = (
     indicatorFields: Array<BaseFieldItem<TableFields>>,
-    propsTableFields: Array<BaseFieldItem<TableFields>>,
+    propsTableFields: Array<BaseFieldItem<BaseFieldInfo>>,
     defaultActiveNav: Ref<string>,
   ) => {
     resetReactive(indicatorFields)
@@ -107,35 +115,68 @@ export function useTable() {
 
   /**
    * @description: 初始化表格字段,把所有字段都放到一个数组中
-   * @return {*}
+   * @param tableFieldsInfo 表格字段信息
+   * @param sortedTableFields 排序过的表格字段信息
+   * @param indicatorFields 自定义指标字段信息
    */
   const initTableFields = (
     tableFieldsInfo: Array<TableFields>,
-    sortedTableFields: Array<TableFields>,
+    sortedTableFields: Array<TableFields> | undefined,
     indicatorFields: Array<BaseFieldItem<TableFields>>,
   ) => {
     resetReactive(tableFieldsInfo)
     // 如果传入了排序字段,则直接使用排序字段,说明已经有缓存了
-    if (sortedTableFields.length > 0) {
+    if (sortedTableFields && sortedTableFields.length > 0) {
       tableFieldsInfo.splice(0, tableFieldsInfo.length, ...sortedTableFields)
     } else {
       // 没传入排序字段,则根据固定字段和自定义指标字段进行排序
       indicatorFields.forEach(item => {
-        item.children.forEach(child => {
-          if (child.fixed || child.state) {
-            item.value.push(child.name)
-            child.state = true
-          }
-          tableFieldsInfo.push(JSON.parse(JSON.stringify(child)))
-        })
+        if (item.value && item.children) {
+          item.children.forEach(child => {
+            // 1. 使用对象合并设置默认值
+            const defaults = {
+              state: true,
+              fixed: false,
+              sort: false,
+              type: TableFieldType.Default,
+            }
+            Object.assign(
+              child,
+              Object.fromEntries(
+                Object.entries(defaults).filter(
+                  ([k]) => (child as any)[k] === undefined,
+                ),
+              ),
+            )
+
+            // 2. 合并条件判断
+            const shouldEnable = child.fixed || child.state
+            if (shouldEnable) {
+              item.value?.push(child.name)
+              child.state = true // 确保状态同步
+            }
+            tableFieldsInfo.push(JSON.parse(JSON.stringify(child)))
+          })
+        } else {
+          console.error('自定义指标字段没有value')
+          // tableFieldsInfo.push({
+          //   label: item.label,
+          //   name: item.name,
+          //   fixed: true,
+          //   state: true,
+          // } as any)
+        }
       })
     }
   }
 
   /**
    * @description: 缓存表格数据。只有是远程查询的时候才会开启缓存
-   * @param {*} newData 新的表格数据
-   * @return {*}
+   *
+   * @param isRemote 是否是远程查询
+   * @param paginationConfig 分页配置
+   * @param cacheTableData 缓存的表格数据
+   * @param newData 新的表格数据
    */
   const setCacheTableData = (
     isRemote: boolean,
@@ -145,23 +186,25 @@ export function useTable() {
   ) => {
     if (isRemote) {
       // 这页没有数据,就缓存一下
-      let curPage = paginationConfig.curPage
-      let curPageSize = paginationConfig.curPageSize
-      if (!cacheTableData[curPage] || cacheTableData[curPage].length === 0) {
-        cacheTableData[curPage] = newData.splice(0, curPageSize) // 缓存数据,后面的splice是防止返回的数据超出当前页长度
+      const { curPage, curPageSize } = paginationConfig
+      const pageIndex = curPage - 1
+      if (
+        !cacheTableData[pageIndex] ||
+        cacheTableData[pageIndex].length === 0
+      ) {
+        cacheTableData[pageIndex] = newData.slice(0, curPageSize) // 缓存数据,后面的slice是防止返回的数据超出当前页长度
       }
     }
   }
   /**
    * @description: 导出表格数据位Excel未见
-   * @param {Array} propTablefields 表格字段信息
-   * @param {Array<any>} tableData 表格数据
-   * @param {string} fileName 保存的文件名
-   * @param {Array<string>} excludeExportFields 被排除的字段
-   * @return {*}
+   * @param  propTableFields 表格字段信息
+   * @param  tableData 表格数据
+   * @param  fileName 保存的文件名
+   * @param excludeExportFields 被排除的字段
    */
   const exportDataToExcel = (
-    propTablefields: Array<BaseFieldItem<TableFields>>,
+    propTableFields: Array<BaseFieldItem<BaseFieldInfo>>,
     tableData: Array<any>,
     fileName: string,
     excludeExportFields: Array<string>,
@@ -175,8 +218,8 @@ export function useTable() {
       let header: {
         [key: string]: string
       } = {}
-      propTablefields.forEach(item => {
-        item.children.forEach(child => {
+      propTableFields.forEach(item => {
+        item.children?.forEach(child => {
           if (!excludeExportFields.includes(child.name))
             header[child.name] = child.label
         })

+ 4 - 1
src/hooks/useTableScroll.ts

@@ -1,4 +1,5 @@
 import type { Ref } from 'vue'
+
 export function useTableScroll(
   elScrollBarH: Ref<HTMLElement | null>,
   tableContent: Ref<HTMLElement | null>,
@@ -46,6 +47,7 @@ export function useTableScroll(
     if (!tableContent.value || !tableContainer.value) return false
     const { scrollTop, offsetHeight, scrollHeight } =
       tableContainer.value as HTMLElement
+    // console.log(scrollTop, offsetHeight, scrollHeight)
     let result = scrollTop + offsetHeight < scrollHeight
     isFixed.value = result
     return result
@@ -83,8 +85,9 @@ export function useTableScroll(
         scrollTop >= contentOffsetTop &&
         scrollTop <= contentOffsetTop + contentScollHeight
       ) {
+        console.log()
         tableHeaderRef.value.style.position = 'fixed'
-        tableHeaderRef.value.style.top = '125px'
+        tableHeaderRef.value.style.top = `${tableContainer.value?.getBoundingClientRect().top}px`
         tableHeaderRef.value.style.zIndex = '666'
       } else {
         // 还原

+ 13 - 0
src/router/material.ts

@@ -1,3 +1,5 @@
+import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
+
 export default [
   {
     path: '/material',
@@ -17,6 +19,17 @@ export default [
         path: 'uploadAsset',
         name: 'UploadAsset',
         component: () => import('@/views/Material/UploadAsset.vue'),
+        beforeEnter(
+          to: RouteLocationNormalized,
+          from: RouteLocationNormalized,
+          next: NavigationGuardNext,
+        ) {
+          if (to.name === 'UploadAsset' && from.name !== 'FilmLibrary') {
+            next({ name: 'FilmLibrary' }) // 直接重定向到父页面
+          } else {
+            next()
+          }
+        },
       },
     ],
   },

+ 2 - 2
src/types/Tables/customIndicatorDialog.ts

@@ -1,4 +1,4 @@
-import type { TableFields, BaseFieldItem } from './table'
+import type { BaseFieldItem, TableFields } from './table'
 
 interface SaveForm {
   schemeName: string
@@ -10,7 +10,7 @@ interface DialogProps {
   indicatorFields: Array<BaseFieldItem<TableFields>>
   defaultActiveNav: string
   tableFieldsInfo: Array<TableFields>
-  fixedFields: Array<string>
+  fixedFields?: Array<string>
   SaveFormData?: SaveForm
 }
 

+ 89 - 17
src/types/Tables/table.ts

@@ -1,4 +1,12 @@
-import type { AllAdmgeTtFields, AllAdMgeTtTablesData } from './tableData/ttAd'
+import type {
+  GameSelectItem,
+  TagItem,
+} from '@/views/Material/types/uploadFileType.ts'
+import type { TablePaginationProps } from './pagination'
+import type {
+  AllTencentAccMgeFields,
+  AllTencentAccMgeTablesData,
+} from './tableData/tencentAcc'
 import type {
   AllAdmgeTencentFields,
   AllAdMgeTencentTablesData,
@@ -7,26 +15,32 @@ import type {
   AllTtAccMgeFields,
   AllTtAccMgeTablesData,
 } from './tableData/ttAcc'
-import type {
-  AllTencentAccMgeFields,
-  AllTencentAccMgeTablesData,
-} from './tableData/tencentAcc'
-
-import type { TablePaginationProps } from './pagination'
+import type { AllAdmgeTtFields, AllAdMgeTtTablesData } from './tableData/ttAd'
+import type { Ref } from 'vue' // 表格查询表单中表单项的类型
 
 // 表格查询表单中表单项的类型
 enum TableFilterType {
   Search,
   Select,
   Date,
+  Radio,
+  Cascader,
+  InputSearchMultiple, // 输入框可多选
+}
+
+export enum TableFieldType {
+  Default,
+  Tag,
 }
 
 // 表格字段的基本信息
 interface BaseFieldInfo {
   label: string // 标签
   name: string // 字段名
-  state: boolean // 是否展示这个字段
-  fixed: boolean // 这个字段是否固定展示
+  state?: boolean // 是否展示这个字段
+  fixed?: boolean // 这个字段是否固定展示
+  sort?: boolean // 是否需要排序
+  type?: TableFieldType // 字段类型,默认为默认类型
 }
 
 // 选择框的选项
@@ -35,6 +49,18 @@ interface SelectFilterOptions {
   value: any
 }
 
+// 单选框的选项
+interface RadioFilterOptions {
+  label: string
+  value: any
+}
+
+interface FilterConditionInfo {
+  label: string
+  state: boolean
+  value: any
+}
+
 // 筛选条件的基础信息
 interface BaseFilterItem {
   type: TableFilterType
@@ -47,6 +73,7 @@ interface BaseFilterItem {
 // 搜索框
 interface SearchFilterItem extends BaseFilterItem {
   type: TableFilterType.Search
+  needPrefixSelect?: boolean // 是否需要前缀
   options: SelectFilterOptions[]
 }
 
@@ -54,6 +81,9 @@ interface SearchFilterItem extends BaseFilterItem {
 interface SelectFilterItem extends BaseFilterItem {
   type: TableFilterType.Select
   options: SelectFilterOptions[]
+  isMultiple?: boolean // 是否多选
+  isTags?: boolean // 是否是标签
+  maxTagCount?: number // 最多显示的标签数量
 }
 
 // 日期选择框
@@ -63,8 +93,32 @@ interface DateFilterItem extends BaseFilterItem {
   endDate: Date
 }
 
+// 单选框
+interface RadioFilterItem extends BaseFilterItem {
+  type: TableFilterType.Radio
+  options: RadioFilterOptions[]
+}
+
+// 级联选择框
+interface CascaderFilterItem extends BaseFilterItem {
+  type: TableFilterType.Cascader
+  options: GameSelectItem[]
+}
+
+interface InputSearchMultipleFilterItem extends BaseFilterItem {
+  type: TableFilterType.InputSearchMultiple
+  tagInput: any // 输入框的值
+  options: TagItem[]
+}
+
 // 所有表格查询表单中表单项的类型
-type TableFilterItem = SearchFilterItem | SelectFilterItem | DateFilterItem
+type TableFilterItem =
+  | SearchFilterItem
+  | SelectFilterItem
+  | DateFilterItem
+  | RadioFilterItem
+  | CascaderFilterItem
+  | InputSearchMultipleFilterItem
 
 // 所有表格数据的数据类型
 type TableData =
@@ -82,13 +136,24 @@ type TableFields =
 
 // 表格每种字段的格式
 // T取值来自所有的表格字段
-interface BaseFieldItem<T extends TableFields> {
+interface BaseFieldItem<T extends BaseFieldInfo> {
   label: string // 用于分段的标签名,即一组数据的标题
   name: string // 用于分段的字段名,即一组数据的字段名
   value: string[] // 被选中的字段的值
   children: T[] // 每一组数据中的字段
+  // sort?: boolean // 是否需要排序,默认为false
+  // type?: TableFieldType // 字段类型,默认为默认类型
 }
 
+// // 表格字段的基本信息
+// interface BaseFieldInfo {
+//   label: string // 标签
+//   name: string // 字段名
+//   state: boolean // 是否展示这个字段
+//   fixed: boolean // 这个字段是否固定展示
+//   sort?: boolean // 是否需要排序
+// }
+
 // 查询字段需要的信息格式
 interface FilterInfo {
   [key: string]: TableFilterItem
@@ -96,19 +161,24 @@ interface FilterInfo {
 
 // 表格的props
 interface TableProps {
+  tableContainer: Ref<HTMLElement | null> // 表格容器
   filtersInfo: {
     [key: string]: TableFilterItem
   }
-  tableData: Array<TableData>
+  // tableData: Array<TableData>
+  tableData: Array<any>
   // tableFields: Array<TableFields>
-  tableFields: Array<BaseFieldItem<TableFields>> // 表格字段信息,分段展示
-  sortedTableFields: Array<TableFields> // 排序过后的表格字段,整体的
-  fixedFields: Array<string> // 需要固定显示的字段
+  tableFields: Array<BaseFieldItem<BaseFieldInfo>> // 表格字段信息,分段展示
+  // tableFields: Array<BaseFieldItem<any>> // 表格字段信息,分段展示
+  sortedTableFields?: Array<TableFields> // 排序过后的表格字段,整体的
+  // sortedTableFields?: Array<any> // 排序过后的表格字段,整体的
+  fixedFields?: Array<string> // 需要固定显示的字段
   remotePagination?: boolean // 是否需要远程分页
   paginationConfig: TablePaginationProps // 分页配置
-  schemeList: Array<CustomIndicatorScheme> // 自定义方案列表
+  schemeList?: Array<CustomIndicatorScheme> // 自定义方案列表
   excludeExportFields?: Array<string> // 需要排除导出的字段
-  activeMenu: string // 当前选中的菜单
+  activeMenu?: string // 当前选中的菜单
+  needExport?: boolean // 是否需要导出
 }
 
 // 保存的表格信息格式
@@ -141,6 +211,7 @@ interface TableInfoItem<
   tableSortedFields: TableFields[] // 排序过后的表格字段,仅用于展示表格字段
   filters: FilterInfo // 过滤信息
   fixedFields: string[] // 固定字段
+  remote: boolean // 是否需要远程分页
 }
 
 export { TableFilterType }
@@ -161,4 +232,5 @@ export type {
   TableInfoItem,
   BaseFieldInfo,
   BaseFieldItem,
+  FilterConditionInfo,
 }

+ 2 - 1
src/types/Tables/tableData/ttAd.ts

@@ -58,8 +58,9 @@ interface AdmgeTTAdData {
 type AdmgeTTAccKeys = keyof AdmgeTTAccData
 
 // 账户表格字段信息
+// 将字段名限制为AdmgeTTAccData中的key
 interface AdmgeTTAccFileds extends BaseFieldInfo {
-  name: AdmgeTTAccKeys // 将字段名限制为AdmgeTTAccData中的key
+  name: AdmgeTTAccKeys
 }
 
 // 项目表格字段Key

+ 78 - 0
src/utils/file/handleImgFile.ts

@@ -0,0 +1,78 @@
+import type { UploadRawFile } from 'element-plus'
+
+interface ImageProcessorConfig {
+  timeout?: number // 超时时间(默认3000ms)
+  crossOrigin?: string // 跨域模式(默认'anonymous')
+}
+
+export class ImageProcessor {
+  private image: HTMLImageElement
+  private config: Required<ImageProcessorConfig>
+  private objectUrl?: string
+
+  constructor(
+    source: string | File | Blob | UploadRawFile,
+    config: ImageProcessorConfig = {},
+  ) {
+    this.config = {
+      timeout: 3000,
+      crossOrigin: 'anonymous',
+      ...config,
+    }
+
+    // 创建图片元素
+    this.image = new Image()
+    this.image.crossOrigin = this.config.crossOrigin
+
+    // 处理不同输入类型
+    if (typeof source === 'string') {
+      this.image.src = source
+    } else {
+      this.objectUrl = URL.createObjectURL(source)
+      this.image.src = this.objectUrl
+    }
+  }
+
+  /**
+   * 获取图片分辨率
+   */
+  public async getResolution(): Promise<{ width: number; height: number }> {
+    return new Promise((resolve, reject) => {
+      // 超时处理
+      const timeoutId = setTimeout(() => {
+        this.cleanup()
+        reject(new Error(`图片加载超时(${this.config.timeout}ms)`))
+      }, this.config.timeout)
+
+      // 成功加载
+      const onLoad = () => {
+        clearTimeout(timeoutId)
+        this.cleanup()
+        resolve({
+          width: this.image.naturalWidth,
+          height: this.image.naturalHeight,
+        })
+      }
+
+      // 加载失败
+      const onError = () => {
+        clearTimeout(timeoutId)
+        this.cleanup()
+        reject(new Error('图片加载失败'))
+      }
+
+      this.image.addEventListener('load', onLoad, { once: true })
+      this.image.addEventListener('error', onError, { once: true })
+    })
+  }
+
+  /**
+   * 清理资源
+   */
+  private cleanup() {
+    if (this.objectUrl) {
+      // URL.revokeObjectURL(this.objectUrl);
+      this.objectUrl = undefined
+    }
+  }
+}

+ 224 - 0
src/utils/file/handleVideoFile.ts

@@ -0,0 +1,224 @@
+export interface VideoProcessorConfig {
+  timeout?: number // 超时时间(默认10秒)
+  crossOrigin?: string // 跨域设置(默认'anonymous')
+  coverQuality?: number // 封面质量 0-1(默认0.8)
+  coverFormat?: 'jpeg' | 'png' // 封面格式(默认'jpeg')
+}
+
+export class VideoProcessor {
+  private video: HTMLVideoElement
+  private url: string
+  private config: Required<VideoProcessorConfig>
+  private cleanupHandlers: (() => void)[] = []
+
+  // 新增初始化状态跟踪
+  private isInitialized = false
+  private initializationPromise: Promise<void> | null = null
+
+  constructor(videoUrl: string, config: VideoProcessorConfig = {}) {
+    this.url = videoUrl
+    this.config = {
+      timeout: 10000,
+      crossOrigin: 'anonymous',
+      coverQuality: 0.8,
+      coverFormat: 'jpeg',
+      ...config,
+    }
+    this.video = document.createElement('video')
+  }
+
+  /**
+   * 初始化视频处理器
+   */
+  private async initialize(): Promise<void> {
+    // 如果已经初始化完成,直接返回
+    if (this.isInitialized) return
+
+    // 如果初始化正在进行中,返回同一个Promise
+    if (this.initializationPromise) {
+      return this.initializationPromise
+    }
+
+    this.initializationPromise = new Promise((resolve, reject) => {
+      // 配置基础属性
+      this.video.crossOrigin = this.config.crossOrigin
+      this.video.src = this.url
+
+      const timeoutId = setTimeout(() => {
+        this.cleanup()
+        reject(new Error(`视频加载超时(${this.config.timeout}ms)`))
+      }, this.config.timeout)
+
+      const cleanup = () => {
+        clearTimeout(timeoutId)
+        this.removeAllEventListeners()
+      }
+      this.registerCleanup(cleanup)
+
+      const onLoaded = () => {
+        this.video.currentTime = Math.min(1, this.video.duration)
+      }
+
+      const onSeeked = () => {
+        cleanup()
+        this.isInitialized = true // 标记为已初始化
+        resolve()
+      }
+
+      const onError = (err: ErrorEvent) => {
+        cleanup()
+        this.isInitialized = false // 初始化失败重置状态
+        reject(err.error || new Error('视频加载失败'))
+      }
+
+      this.video.addEventListener('loadedmetadata', onLoaded)
+      this.video.addEventListener('seeked', onSeeked)
+      this.video.addEventListener('error', onError)
+      this.registerCleanup(() => {
+        this.video.removeEventListener('loadedmetadata', onLoaded)
+        this.video.removeEventListener('seeked', onSeeked)
+        this.video.removeEventListener('error', onError)
+      })
+
+      // 开始加载
+      this.video.load()
+    })
+    return this.initializationPromise
+  }
+
+  /**
+   * 获取视频基础信息
+   */
+  public async getVideoInfo() {
+    await this.initialize()
+    return {
+      width: this.video.videoWidth,
+      height: this.video.videoHeight,
+      duration: this.video.duration,
+      format: this.detectVideoFormat(),
+    }
+  }
+
+  /**
+   * 生成视频封面
+   * @param timePoint 截取时间点(秒)
+   */
+  public async generateCover(timePoint = 1): Promise<string> {
+    await this.initialize()
+
+    return new Promise((resolve, reject) => {
+      const targetTime = Math.min(timePoint, this.video.duration)
+
+      const wrappedHandler = () => {
+        try {
+          this.video.removeEventListener('seeked', wrappedHandler)
+          const canvas = document.createElement('canvas')
+          const ctx = canvas.getContext('2d')
+
+          if (!ctx) {
+            throw new Error('无法获取Canvas上下文')
+          }
+
+          canvas.width = this.video.videoWidth
+          canvas.height = this.video.videoHeight
+          ctx.drawImage(this.video, 0, 0, canvas.width, canvas.height)
+
+          const mimeType = `image/${this.config.coverFormat}`
+          resolve(canvas.toDataURL(mimeType, this.config.coverQuality))
+        } catch (err) {
+          reject(err)
+        }
+      }
+
+      this.video.addEventListener('seeked', wrappedHandler)
+      this.video.currentTime = targetTime
+    })
+  }
+
+  /**
+   * 检测视频格式
+   */
+  private detectVideoFormat(): string {
+    const matches = this.url.match(/\.(\w+)(\?|$)/)
+    return matches ? matches[1].toLowerCase() : 'unknown'
+  }
+
+  /**
+   * 注册清理函数
+   */
+  private registerCleanup(fn: () => void) {
+    // 添加自检防止递归
+    if (this.cleanupHandlers.includes(fn)) {
+      console.warn('重复的清理函数已跳过')
+      return
+    }
+    this.cleanupHandlers.push(fn)
+  }
+  /**
+   * 移除所有事件监听
+   */
+  private removeAllEventListeners() {
+    // 创建副本避免处理过程中数组变化
+    const handlers = [...this.cleanupHandlers]
+    this.cleanupHandlers = [] // 先清空数组
+
+    // 反序执行防止依赖问题
+    handlers.reverse().forEach(cleanup => {
+      try {
+        cleanup()
+      } catch (err) {
+        console.error('清理函数执行失败:', err)
+      }
+    })
+  }
+
+  /**
+   * 资源清理
+   */
+  public cleanup() {
+    this.removeAllEventListeners()
+    this.video.pause()
+    this.video.removeAttribute('src')
+    this.video.load()
+
+    // 安全释放ObjectURL
+    if (this.url.startsWith('blob:')) {
+      // URL.revokeObjectURL(this.url);
+    }
+
+    // 重置初始化状态
+    this.isInitialized = false
+    this.initializationPromise = null
+  }
+}
+
+// // 使用示例
+// const processVideo = async (url: string) => {
+//   const processor = new VideoProcessor(url, {
+//     timeout: 15000,
+//     coverQuality: 0.9
+//   });
+//
+//   try {
+//     // 获取元数据
+//     const info = await processor.getVideoInfo();
+//     console.log('视频信息:', info);
+//
+//     // 生成封面(第5秒)
+//     const cover = await processor.generateCover(5);
+//     console.log('封面Base64:', cover.slice(0, 50) + '...');
+//
+//     return { ...info, cover };
+//   } catch (err) {
+//     console.error('视频处理失败:', err);
+//     throw err;
+//   } finally {
+//     processor.cleanup();
+//   }
+// }
+//
+// // 使用演示
+// const videoUrl = 'https://example.com/sample.mp4';
+// processVideo(videoUrl).then(result => {
+//   console.log('最终处理结果:', result);
+// });

+ 0 - 1
src/views/Index.vue

@@ -423,7 +423,6 @@ onMounted(() => {})
   width: 100%;
   height: calc(100% - 64px);
   /*overflow: hidden*/
-
   background-color: #f2f3f5;
 
   right: 0vw;

+ 227 - 30
src/views/Material/FilmLibrary.vue

@@ -1,43 +1,218 @@
 <script setup lang="ts">
-import { Plus } from '@element-plus/icons-vue'
+import PromotionTable from '@/components/table/PromotionTable.vue'
+import { MaterialAPI } from '@/config/API.ts'
+import { useMaterial } from '@/hooks/Material/useMaterial.ts'
 import router from '@/router'
-import Menu from '@/components/navigation/Menu.vue'
-import { ref } from 'vue'
+import type { ResponseInfo } from '@/types/axios.ts'
 import type { BaseMenu } from '@/types/Promotion/Menu.ts'
+import type { TablePaginationProps } from '@/types/Tables/pagination.ts'
+import { type SelectFilterOptions } from '@/types/Tables/table.ts'
+import axiosInstance from '@/utils/axios/axiosInstance.ts'
+import {
+  imgFields,
+  imgFilterInfo,
+  videoFields,
+  videoFilterInfo,
+} from '@/views/Material/config/FimLibraryConfig.ts'
+import { Plus } from '@element-plus/icons-vue'
+
+import { nextTick, onMounted, reactive, ref } from 'vue'
+
+type PromotionTableType = InstanceType<typeof PromotionTable>
 
+const { getGameInfo, getAllTags } = useMaterial()
+
+// const defaultActive = ref<string>('filmLibrary')
+const menuList = reactive<BaseMenu[]>([])
+const listRef = ref<PromotionTableType>()
+const isVideo = ref<boolean>(false)
+const tableContainer = ref<HTMLElement | null>(null)
+/**
+ * 跳转上传页面
+ */
 const moveToUpload = () => {
   router.push({ name: 'UploadAsset' })
 }
 
-const defaultActive = ref<string>(router.currentRoute.value.path)
-const menu: Array<BaseMenu> = [
-  {
-    name: 'filmLibrary',
-    title: '成片库',
-    path: '/material/filmLibrary',
-  },
-]
+/**
+ * 更新游戏及pid选项
+ */
+const updateGameInfo = async () => {
+  const info = await getGameInfo()
+  if (info === null) {
+    ElMessage.error('获取游戏信息失败')
+    return
+  }
+  const filterInfo = isVideo.value ? videoFilterInfo : imgFilterInfo
+  const pidInfo: SelectFilterOptions[] = []
+  const gidInfo: SelectFilterOptions[] = []
+  const filterInfoPid = (filterInfo as any)['pid']
+  const filterInfoGid = (filterInfo as any)['gid']
+
+  nextTick(() => {
+    for (let [k, v] of Object.entries(info)) {
+      pidInfo.push({
+        value: k,
+        label: k === '' ? '默认' : k,
+      })
+      v.forEach(item => {
+        gidInfo.push({
+          value: item.gid,
+          label: item.gameName,
+        })
+      })
+      filterInfoPid['options'] = pidInfo
+      filterInfoPid['value'] = pidInfo[0].value
+      filterInfoGid['options'] = gidInfo
+    }
+  })
+}
+
+/**
+ * 更新tag标签列表
+ */
+const updateTags = async () => {
+  const tags = await getAllTags()
+  const options = tags.map(item => {
+    return {
+      label: item.name,
+      value: item.id,
+    }
+  })
+  const filterInfo = isVideo.value ? videoFilterInfo : imgFilterInfo
+  const filterTag = (filterInfo as any)['tags']
+  filterTag['options'] = options
+}
+
+const createMenu = async () => {
+  const info = await getGameInfo()
+  if (info === null) {
+    ElMessage.error('游戏信息获取失败')
+    return
+  }
+  menuList.splice(0, menuList.length)
+  for (let [k, v] of Object.entries(info)) {
+    menuList.push({
+      name: k,
+      title: k === '' ? '默认' : k,
+      iconDefault: '/file/project-default.svg',
+      iconActive: '/file/project-active.svg',
+      children: [],
+    })
+    for (let item of v) {
+      menuList[menuList.length - 1].children?.push({
+        name: item.gid,
+        title: item.gameName,
+        iconDefault: '/file/folder-default.svg',
+        iconActive: '/file/folder-active.png',
+      })
+    }
+  }
+}
+
+const tableData = reactive<Array<any>>([])
+
+const paginationConfig = reactive<TablePaginationProps>({
+  total: 0,
+  pageSizeList: [10, 20, 40],
+})
+
+const updateTableData = async () => {
+  const url = isVideo.value ? MaterialAPI.videoList : MaterialAPI.imgList
+  const params = listRef.value?.getTableReqParams() as any
+  if (params['order']) {
+    params['order'] = params['order'] === 'ascending' ? 'asc' : 'desc'
+  }
+  const result = (await axiosInstance.post(url, params)) as ResponseInfo
+  paginationConfig.total = result.data.count
+  if (paginationConfig.total === 0) {
+    tableData.splice(0, tableData.length)
+  } else {
+    tableData.splice(0, tableData.length, ...result.data.list)
+  }
+}
+
+const updateResolution = async () => {
+  // TODO 更新后,区分URL,目前是两个都一样
+  const url = isVideo.value
+    ? MaterialAPI.imgFilterInfo
+    : MaterialAPI.imgFilterInfo
+  const result = (await axiosInstance.post(url)) as ResponseInfo
+  if (result.code !== 0) {
+    ElMessage.error('获取筛选信息失败')
+    return
+  }
+  const resolutionList = result.data['resolution']
+  const filterInfo = isVideo.value ? videoFilterInfo : imgFilterInfo
+  const filterResolution = (filterInfo as any)['resolution']
+  filterResolution['options'] = resolutionList.map((item: any) => {
+    return {
+      label: item,
+      value: item,
+    }
+  })
+}
+
+const updateFilterInfo = async () => {
+  await updateResolution()
+  await updateGameInfo()
+  await updateTags()
+}
+
+const changeAssetType = async () => {
+  await updateFilterInfo()
+  await updateTableData()
+}
+
+onMounted(async () => {
+  await updateFilterInfo()
+  await updateTableData()
+})
 </script>
 
 <template>
-  <div class="filmLibraryContainer">
-    <div class="flSlider">
-      <Menu
-        mode="vertical"
-        :menu-list="menu"
-        :default-active="defaultActive"
-        :router="true"
-      ></Menu>
+  <div class="filmLibraryContainer" ref="tableContainer">
+    <!--    要给v-if,需要等数据加载完再展示 -->
+    <!--    v-if="!isLoading"-->
+    <div class="filmLibraryHeader">
+      <div class="headerContainer bottomBorder">
+        <span>素材类型:</span>
+        <el-radio-group v-model="isVideo" @change="changeAssetType">
+          <el-radio :value="false" border>图片</el-radio>
+          <el-radio :value="true" border>视频</el-radio>
+        </el-radio-group>
+      </div>
     </div>
-
     <div class="flContent">
-      <el-button
-        color="#197AFB"
-        :icon="Plus"
-        class="el-button-mini"
-        @click="moveToUpload"
-        >上传素材
-      </el-button>
+      <PromotionTable
+        :table-container="tableContainer"
+        :filters-info="isVideo ? videoFilterInfo : imgFilterInfo"
+        :table-data="tableData"
+        :table-fields="isVideo ? videoFields : imgFields"
+        :pagination-config="paginationConfig"
+        :need-export="false"
+        :remote-pagination="true"
+        :fixed-fields="['action']"
+        @query-table="updateTableData"
+        ref="listRef"
+        style="height: 100%"
+      >
+        <template #addItem>
+          <el-button
+            color="#197afb"
+            class="el-button-mini"
+            :icon="Plus"
+            @click="moveToUpload"
+            >上传素材
+          </el-button>
+        </template>
+        <template #operations>
+          <div>
+            <el-button text type="primary">预览</el-button>
+            <el-button text type="success">下载</el-button>
+          </div>
+        </template>
+      </PromotionTable>
     </div>
   </div>
 </template>
@@ -46,12 +221,34 @@ const menu: Array<BaseMenu> = [
 .filmLibraryContainer {
   width: 100%;
   height: 100%;
-  display: flex;
+  margin: 0 auto;
+  background-color: white;
+  overflow: scroll;
 }
 
-.flSlider {
+.gameSelectContainer,
+.flQueryForm,
+.flContent {
+  width: 100%;
   height: 100%;
-  position: relative;
+}
+
+.gameSelectContainer,
+.filmLibraryHeader,
+.flContent {
+  padding: 10px 20px;
+}
+
+.filmLibraryHeader {
+  width: 100%;
+  margin: 0 auto;
+  padding: 10px 40px;
+}
+
+.headerContainer {
+  width: 96%;
+  margin: 0 auto;
+  padding-bottom: 10px;
 }
 
 .el-button-mini {

+ 294 - 90
src/views/Material/UploadAsset.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { MaterialAPI } from '@/config/API.ts'
 import { useFileUpload } from '@/hooks/File/useFileUpload.ts'
+import { useMaterial } from '@/hooks/Material/useMaterial.ts'
+import router from '@/router'
 import type { ResponseInfo } from '@/types/axios.ts'
 
 import axiosInstance from '@/utils/axios/axiosInstance.ts'
@@ -8,26 +10,29 @@ import type {
   AddTagRes,
   GameData,
   GameSelectItem,
+  ImgUploadRes,
   ResFileInfo,
   TagItem,
   UploadAssetForm,
 } from '@/views/Material/types/uploadFileType.ts'
-import { Plus } from '@element-plus/icons-vue'
 import type {
   FormInstance,
   FormRules,
   UploadFile,
   UploadFiles,
   UploadInstance,
-  UploadRawFile,
   UploadRequestOptions,
-  UploadUserFile,
 } from 'element-plus'
-import { ElMessageBox, genFileId } from 'element-plus'
-import { nextTick, onMounted, reactive, ref, toRaw } from 'vue'
+import { ElMessageBox } from 'element-plus'
+import { nextTick, onMounted, type Reactive, reactive, ref, toRaw } from 'vue'
+import { VideoProcessor } from '@/utils/file/handleVideoFile.ts'
+import { ImageProcessor } from '@/utils/file/handleImgFile.ts'
 
-const { isValidFile, handleFile, getAllTags } = useFileUpload()
+const { getAllTags } = useMaterial()
+const { isValidFile, handleFile } = useFileUpload()
+const { getGameInfo, createFilter } = useMaterial()
 
+// 表单数据
 const uploadForm = reactive<UploadAssetForm>({
   name: '',
   tags: [],
@@ -35,8 +40,14 @@ const uploadForm = reactive<UploadAssetForm>({
   pid: '',
   filePath: '',
   md5: '',
+  isImg9: 1,
+  resolution: {
+    width: 0,
+    height: 0,
+  },
 })
 
+// 表单规则
 const rules = reactive<FormRules<UploadAssetForm>>({
   name: [
     { required: true, message: '请输入创意名', trigger: 'blur' },
@@ -46,31 +57,52 @@ const rules = reactive<FormRules<UploadAssetForm>>({
     { required: true, message: '请选择标签', trigger: 'change' },
     { required: true, message: '请选择标签', trigger: 'blur' },
   ],
+  isImg9: [
+    { required: true, message: '请选择是否是九宫格', trigger: 'change' },
+  ],
   gid: [{ required: true, message: '请选择游戏', trigger: 'change' }],
   filePath: [{ required: true, message: '请选择素材', trigger: 'change' }],
 })
 
-let gameInfoList: GameData = {}
-const gameSelect = reactive<GameSelectItem[]>([])
-
+const gameSelect = reactive<GameSelectItem[]>([]) // 游戏级联选择框数据
 const uploadRef = ref<UploadInstance>()
 const fileFormRef = ref<FormInstance>()
-const tagInput = ref('')
+const tagInput = ref('') // 标签输入框数据
 const autocompleteRef = ref()
-const isVideo = ref(false)
-const limit = 1
-const allTags: Array<TagItem> = []
+const isVideo = ref(false) // 当前上传的时候是视频
+const limit = 1 // 限制上传文件数量
+const allTags: Array<TagItem> = [] // 所有标签
+const isUploading = ref(false)
+const videoDialogVisible = ref(false)
+const previewUrl = ref('')
+const previewVideoRef = ref<HTMLVideoElement>()
 
+const coverUrl = ref('') // 封面图片地址
+
+// 级联选择框属性配置
 const cascadePropsConfig = {
   expandTrigger: 'hover' as const,
 }
 
+/**
+ * 选择游戏之后的处理
+ * @param value 目前选择的值
+ */
 const gameSelectChange = (value: string[]) => {
   uploadForm.pid = value[0]
   uploadForm.gid = value[1]
 }
 
-const generateGameSelect = () => {
+/**
+ * 生成游戏级联选择框数据
+ *
+ * @param gameInfoList 游戏信息
+ * @param gameSelect 级联选择框的值
+ */
+const generateGameSelect = (
+  gameInfoList: GameData,
+  gameSelect: Reactive<GameSelectItem[]>,
+) => {
   gameSelect.splice(0, gameSelect.length)
   for (let [k, v] of Object.entries(gameInfoList)) {
     const children = v.map(item => {
@@ -87,6 +119,11 @@ const generateGameSelect = () => {
   }
 }
 
+/**
+ * 标签搜索
+ * @param queryString 搜索值
+ * @param cb 回调
+ */
 const tagSearch = async (queryString: string, cb: any) => {
   const result = queryString
     ? allTags.filter(createFilter(queryString))
@@ -95,101 +132,120 @@ const tagSearch = async (queryString: string, cb: any) => {
   cb(result)
 }
 
-const createFilter = (queryString: string) => {
-  return (restaurant: TagItem) => {
-    return (
-      restaurant.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0
-    )
-  }
-}
-
+/**
+ * 处理标签选择后的事件
+ * @param tags 选中的标签
+ */
 const handleTagSelect = (tags: TagItem) => {
   uploadForm.tags.push(tags.id)
+  // 需要在下一帧出发blur,否则无法选中
   nextTick(() => {
     autocompleteRef.value.blur()
   })
 }
 
-const handleExceed = async (
-  files: UploadRawFile[],
-  fileList: UploadUserFile[],
-) => {
-  const confirm = await ElMessageBox.confirm(
-    '上传将覆盖前一个文件,是否继续上传?',
-    {
-      confirmButtonText: '继续上传',
-      cancelButtonText: '取消',
-      type: 'warning',
-    },
-  )
-  const file = files[0]
-  if (!confirm) return false
-  handleFile(file, fileList, isVideo, uploadForm)
-  nextTick(() => {
-    fileFormRef.value?.validateField('filePath')
-  })
-  uploadRef.value!.clearFiles()
-  file.uid = genFileId()
-  uploadRef.value!.handleStart(file)
-}
-
+/**
+ * 当文件列表发生变化时的处理事件
+ *
+ * @param file 选择的文件
+ * @param fileList 文件列表
+ */
 const handleFileChange = (file: UploadFile, fileList: UploadFiles) => {
   if (!file || !file.raw) {
     ElMessage.warning('请传入合法文件')
     return
   }
+  // 不是合法文件直接返回不处理
   if (!isValidFile(file)) {
     fileList.splice(1)
     ElMessage.warning('上传文件超出限制或不是规定文件')
+    return
+  }
+  // 统一处理文件上传
+  const processUpload = async () => {
+    handleFile(file, fileList, isVideo, uploadForm, previewUrl)
+    nextTick(() => {
+      fileFormRef.value?.validateField('filePath')
+    })
+    // 重置文件状态保证可重复上传
+    fileList.forEach(f => (f.status = 'ready'))
+
+    console.log(file)
+    // 如果是视频的话,还需要生成封面
+    if (isVideo.value) {
+      nextTick(async () => {
+        const videoProcessor = new VideoProcessor(previewUrl.value)
+        const { width, height } = await videoProcessor.getVideoInfo()
+        coverUrl.value = await videoProcessor.generateCover()
+        uploadForm.resolution = {
+          width,
+          height,
+        }
+      })
+    } else {
+      const imgProcessor = new ImageProcessor(file.raw!)
+      const { width, height } = await imgProcessor.getResolution()
+      uploadForm.resolution = {
+        width,
+        height,
+      }
+      coverUrl.value = file.url!
+    }
   }
 
-  handleFile(file, fileList, isVideo, uploadForm)
-  nextTick(() => {
-    fileFormRef.value?.validateField('filePath')
-  })
-  // 在第一次上传成功后,文件状态会被设置为success,此时是无法重新提交文件上传的,故需要手动重置
-  fileList.forEach(file => {
-    file.status = 'ready'
-  })
-
-  console.log(uploadForm)
+  if (fileList.length > limit) {
+    // 超限需要用户确认
+    ElMessageBox.confirm('上传将覆盖前一个文件,是否继续上传?', {
+      confirmButtonText: '继续上传',
+      cancelButtonText: '取消',
+      type: 'warning',
+    })
+      .then(() => {
+        // 保留最新文件,删除旧文件(假设旧文件在数组开头)
+        fileList.splice(0, 1)
+        processUpload()
+      })
+      .catch(() => {
+        // 取消时移除当前新增的文件
+        const newFileIndex = fileList.findIndex(f => f.uid === file.uid)
+        if (newFileIndex > -1) fileList.splice(newFileIndex, 1)
+      })
+  } else {
+    // 未超限直接上传
+    processUpload()
+  }
 }
 
+/**
+ * 处理表单提交
+ *
+ * 这个函数不处理表单的提交,只是作为触发器,完成表单验证的功能,然后将后续流程交给文件上传部分
+ * @param formEl 表单对象
+ */
 const submitAsset = async (formEl: FormInstance | undefined) => {
+  if (isUploading.value) return
   if (!formEl) return
+  // 表单验证
   const isValid = await formEl.validate()
   if (!isValid) {
     ElMessage.warning('请填写完整信息')
     return
   }
+  // 交给文件上传的处理流程
   uploadRef.value?.submit()
 }
 
-const submitForm = async () => {
-  const url = isVideo.value ? MaterialAPI.videoSubmit : MaterialAPI.imgSubmit
-  const nFormData = JSON.parse(JSON.stringify(toRaw(uploadForm)))
-  if (isVideo.value) {
-    nFormData.videoPath = nFormData.filePath
-  } else {
-    nFormData.imgPath = nFormData.filePath
-  }
-  delete nFormData.filePath
-
-  // TODO 完善图片和视频的返回值类型
-  const result = await axiosInstance.post(url, nFormData)
-  if (result.code !== 0) {
-    ElMessage.error('上传素材失败')
-    return
-  }
-  ElMessage.success('上传素材成功')
-  uploadRef.value?.clearFiles()
-  tagInput.value = ''
-  fileFormRef.value?.resetFields()
-}
-
+/**
+ * 文件上传
+ *
+ * 表单提交需要先拿到文件上传成功后的回显,所以文件上传成功后才会执行表单的提交
+ * @param options 上传文件信息
+ */
 const uploadFile = async (options: UploadRequestOptions) => {
+  isUploading.value = true
   if (!options || !options.file) {
     ElMessage.warning('请先上传文件')
+    isUploading.value = false
     return
   }
   const file = options.file
@@ -201,20 +257,66 @@ const uploadFile = async (options: UploadRequestOptions) => {
   )) as ResFileInfo
   if (result.code !== 0) {
     ElMessage.error('上传素材失败')
+    isUploading.value = false
     return false
   }
 
   const { md5, path } = result
   uploadForm.md5 = md5
   uploadForm.filePath = path
+
+  // 拿到回显后执行表单的提交流程
   await submitForm()
 }
 
-const goBack = () => {}
+const submitForm = async () => {
+  const url = isVideo.value ? MaterialAPI.videoSubmit : MaterialAPI.imgSubmit
+  const nFormData = JSON.parse(JSON.stringify(toRaw(uploadForm)))
+
+  // 区分视频和图片,他们需要的字段不同
+  if (isVideo.value) {
+    nFormData.videoPath = nFormData.filePath
+    // 对于视频上传需要删除掉九宫格字段信息
+    delete nFormData.isImg9
+  } else {
+    nFormData.imgPath = nFormData.filePath
+  }
+  // 移除不需要的filePath
+  delete nFormData.filePath
+
+  const res = (await axiosInstance.post(url, nFormData)) as ResponseInfo
+  let result = res as ImgUploadRes
+  if (isVideo.value) {
+    result = res as ImgUploadRes
+  }
+  if (result.code !== 0) {
+    ElMessage.error('上传素材失败')
+    isUploading.value = false
+    return
+  }
+  ElMessage.success('上传素材成功')
+  isUploading.value = false
+  // 重置表单
+  uploadRef.value?.clearFiles()
+  tagInput.value = ''
+  fileFormRef.value?.resetFields()
+}
+
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.back()
+}
 
+/**
+ * 新增tag
+ * @param val 传新增值
+ */
 const tagAdd = async (val: string) => {
   const isExist = allTags.find(item => item.name === val)
   const newTagName = val
+  // 首先要判断是否已经存在这个标签,不存在则需要先走新建流程,所以不要直接添加进去,先移除
   uploadForm.tags.splice(uploadForm.tags.length - 1, 1)
   let id = -1
   if (!isExist) {
@@ -241,23 +343,39 @@ const tagAdd = async (val: string) => {
   uploadForm.tags.push(id)
 }
 
+/**
+ * 清空标签输入框
+ */
 const clearTagInput = () => {
   uploadForm.tags = []
 }
 
+/**
+ * 更新tag标签列表
+ */
 const updateTags = async () => {
   const tags = await getAllTags()
   allTags.splice(0, allTags.length, ...tags)
 }
 
+/**
+ * 更新游戏标签,并且重新生成游戏选择列表
+ */
 const updateGameInfo = async () => {
-  const res = (await axiosInstance.post(
-    MaterialAPI.getAllGamesInfo,
-    {},
-  )) as ResponseInfo
-  console.log(res)
-  gameInfoList = res.data as GameData
-  generateGameSelect()
+  const info = await getGameInfo()
+  if (info === null) {
+    ElMessage.error('获取游戏信息失败')
+    return
+  }
+
+  generateGameSelect(info, gameSelect)
+}
+
+/**
+ * 处理预览
+ */
+const handlePreview = () => {
+  videoDialogVisible.value = true
 }
 
 onMounted(async () => {
@@ -291,6 +409,12 @@ onMounted(async () => {
               v-model="uploadForm.name"
             />
           </el-form-item>
+          <el-form-item v-if="!isVideo" label="是否是九宫格图片" prop="isImg9">
+            <el-radio-group v-model="uploadForm.isImg9">
+              <el-radio :value="1">是</el-radio>
+              <el-radio :value="0">否</el-radio>
+            </el-radio-group>
+          </el-form-item>
           <el-form-item label="游戏" prop="gid">
             <el-cascader
               v-model="uploadForm.gid"
@@ -335,6 +459,7 @@ onMounted(async () => {
               </el-input-tag>
             </div>
           </el-form-item>
+          <!--          :limit="limit"-->
           <el-form-item label="文件" prop="filePath">
             <div class="fileContainer">
               <el-upload
@@ -342,12 +467,27 @@ onMounted(async () => {
                 drag
                 :auto-upload="false"
                 list-type="picture"
-                :limit="limit"
                 :on-change="handleFileChange"
-                :on-exceed="handleExceed"
                 ref="uploadRef"
                 :http-request="uploadFile"
               >
+                <template #file="{ file }">
+                  <div class="fileInfo">
+                    <div class="fileIcon" @click="handlePreview">
+                      <el-image
+                        :src="coverUrl"
+                        class="fileListPreviewContent fileListPreviewImg"
+                      />
+                      <div class="fileListPreviewContent fileListPreviewMask">
+                        <el-icon>
+                          <zoom-in />
+                        </el-icon>
+                      </div>
+                    </div>
+
+                    <div>{{ file.name }}</div>
+                  </div>
+                </template>
                 <el-icon class="el-icon--upload">
                   <upload-filled />
                 </el-icon>
@@ -365,12 +505,30 @@ onMounted(async () => {
             </div>
           </el-form-item>
         </el-form>
+        <el-dialog
+          v-model="videoDialogVisible"
+          title="预览"
+          class="previewDialog"
+        >
+          <div class="previewContainer">
+            <video
+              class="previewContent"
+              v-if="isVideo"
+              :src="previewUrl"
+              width="400"
+              height="400"
+              controls
+              ref="previewVideoRef"
+            ></video>
+            <img class="previewContent" v-else :src="previewUrl" alt="" />
+          </div>
+        </el-dialog>
         <div class="fileFooter">
           <el-button
             color="#197AFB"
-            :icon="Plus"
-            class="el-button-mini"
+            class="el-button-mini w120"
             @click="submitAsset(fileFormRef)"
+            :loading="isUploading"
             >确定
           </el-button>
         </div>
@@ -439,7 +597,53 @@ onMounted(async () => {
   color: #b6b7b7;
 }
 
-.fileItem {
+.fileInfo {
   display: flex;
+  align-items: center;
+  user-select: none;
+}
+
+.fileIcon {
+  margin-right: 10px;
+  position: relative;
+  width: 100px;
+  height: 100px;
+  cursor: pointer;
+}
+
+.fileIcon:hover .fileListPreviewMask {
+  visibility: visible;
+}
+
+.fileListPreviewContent {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+}
+
+.fileListPreviewMask {
+  visibility: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: white;
+  font-size: 30px;
+}
+
+.previewDialog {
+  width: 800px;
+  height: 800px;
+}
+
+.previewContainer {
+  width: 80%;
+  height: 100%;
+  margin: 0 auto;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 </style>

+ 278 - 0
src/views/Material/config/FimLibraryConfig.ts

@@ -0,0 +1,278 @@
+import {
+  type BaseFieldInfo,
+  type BaseFieldItem,
+  TableFieldType,
+  type TableFilterItem,
+  TableFilterType,
+} from '@/types/Tables/table.ts'
+import { reactive } from 'vue'
+
+interface FilterInfo {
+  [key: string]: TableFilterItem
+}
+
+interface ImageTag {
+  id: number
+  name: string
+}
+
+interface ImageMetadata {
+  id: number
+  name: string // 图片名称
+  local_path: string // 本地地址
+  gid: string // 游戏ID
+  pid: string // 游戏父级ID
+  wx_id: string // 微信资源ID
+  image_width: number // 图片宽度
+  image_height: number // 图片高度
+  image_file_size: number // 图片大小
+  image_type: string
+  image_signature: string
+  createdAt: string // 创建时间
+  updatedAt: string // 更新时间
+  tags: ImageTag[] // 标签列表
+}
+
+interface VideoMetadata {
+  id: number
+  name: string // 视频名称
+  gid: string // 游戏ID
+  pid: string // 游戏父级ID
+  wx_id: string // 微信资源ID
+  video_signature: string // 视频签名
+  local_path: string // 本地路径
+  createdAt: string // 创建时间
+  updatedAt: string // 更新时间
+}
+
+const videoFilterInfo = reactive<FilterInfo>({
+  gid: {
+    label: '游戏',
+    name: 'gid',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+  pid: {
+    label: '项目',
+    name: 'pid',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+  search: {
+    label: '资源名',
+    name: 'search',
+    type: TableFilterType.Search,
+    value: 'search',
+    needPrefixSelect: false,
+    options: [{ label: '名称', value: 'search' }],
+  },
+  tags: {
+    label: '标签',
+    name: 'tags',
+    type: TableFilterType.Select,
+    isTags: true,
+    isMultiple: true,
+    value: [],
+    options: [],
+  },
+  resolution: {
+    label: '分辨率',
+    name: 'resolution',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+  isLandscape: {
+    label: '横竖屏',
+    name: 'isLandscape',
+    type: TableFilterType.Select,
+    value: '0',
+    options: [
+      { label: '横屏', value: '1' },
+      { label: '竖屏', value: '0' },
+    ],
+  },
+})
+
+const imgFilterInfo = reactive<FilterInfo>({
+  gid: {
+    label: '游戏',
+    name: 'gid',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+  pid: {
+    label: '项目',
+    name: 'pid',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+  search: {
+    label: '资源名',
+    name: 'search',
+    type: TableFilterType.Search,
+    value: 'search',
+    needPrefixSelect: false,
+    options: [{ label: '名称', value: 'search' }],
+  },
+  tags: {
+    label: '标签',
+    name: 'tags',
+    type: TableFilterType.Select,
+    isTags: true,
+    isMultiple: true,
+    value: [],
+    options: [],
+  },
+  isImg9: {
+    label: '是否九宫格',
+    name: 'isImg9',
+    type: TableFilterType.Select,
+    value: '0',
+    options: [
+      { label: '是', value: '1' },
+      { label: '否', value: '0' },
+    ],
+  },
+  resolution: {
+    label: '分辨率',
+    name: 'resolution',
+    type: TableFilterType.Select,
+    value: '',
+    options: [],
+  },
+})
+
+const imgFields: Array<BaseFieldItem<BaseFieldInfo>> = [
+  {
+    label: '图片字段',
+    name: 'img',
+    value: [],
+    children: [
+      {
+        label: '素材名称',
+        name: 'name',
+        state: true,
+        fixed: true,
+      },
+      {
+        label: '游戏ID',
+        name: 'gid',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '项目ID',
+        name: 'pid',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '预览图片',
+        name: 'local_path',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '分辨率',
+        name: 'resolution',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '创建时间',
+        name: 'createdAt',
+        state: true,
+        fixed: true,
+        sort: true,
+      },
+      {
+        label: '更新时间',
+        name: 'updatedAt',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '标签',
+        name: 'tag',
+        type: TableFieldType.Tag,
+        state: true,
+        fixed: true,
+      },
+      {
+        label: '操作',
+        name: 'action',
+        state: true,
+        fixed: true,
+      },
+    ],
+  },
+]
+
+const videoFields: Array<BaseFieldItem<BaseFieldInfo>> = [
+  {
+    label: '视频字段',
+    name: 'video',
+    value: [],
+    children: [
+      {
+        label: '素材名称',
+        name: 'name',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '游戏ID',
+        name: 'gid',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '项目ID',
+        name: 'pid',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '预览视频',
+        name: 'local_path',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '分辨率',
+        name: 'resolution',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '创建时间',
+        name: 'createdAt',
+
+        sort: true,
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '更新时间',
+        name: 'updatedAt',
+        state: true,
+        fixed: false,
+      },
+      {
+        label: '标签',
+        name: 'tag',
+        type: TableFieldType.Tag,
+        state: true,
+        fixed: false,
+      },
+    ],
+  },
+]
+export { videoFilterInfo, imgFields, videoFields, imgFilterInfo }
+
+export type { ImageMetadata, VideoMetadata }

+ 23 - 0
src/views/Material/types/filmLibraryType.ts

@@ -0,0 +1,23 @@
+interface ResTagItem {
+  id: number
+  name: string
+}
+
+interface ResImgItem {
+  id: number
+  name: string
+  local_path: string
+  gid: string
+  pid: string
+  wx_id: string
+  image_width: number
+  image_height: number
+  image_file_size: number
+  image_type: string
+  image_signature: string
+  createdAt: string
+  updatedAt: string
+  tags: ResTagItem[]
+}
+
+export type { ResTagItem, ResImgItem }

+ 42 - 3
src/views/Material/types/uploadFileType.ts

@@ -5,8 +5,15 @@ interface UploadAssetForm {
   pid: string
   filePath: string
   md5: string
-  imgPath?: string
-  videoPath?: string
+  isImg9: number // 是否是九宫格图片
+  imgPath?: string // 图片路径
+  videoPath?: string // 视频路径
+  videoCoverImg?: string // 视频封面图片
+  // 分辨率
+  resolution: {
+    width: number
+    height: number
+  }
 }
 
 type GameItem = {
@@ -34,7 +41,6 @@ interface ResFileInfo {
 interface TagItem {
   id: number
   name: string
-  createdAt: string
 }
 
 interface TagsRes {
@@ -50,6 +56,35 @@ interface AddTagRes {
   }
 }
 
+interface ImageData {
+  image_id: string
+  image_width: number
+  image_height: number
+  image_file_size: number
+  image_type: string
+  image_signature: string
+  outer_image_id: string
+  preview_url: string
+  description: string
+}
+
+interface ImgUploadRes {
+  code: number
+  data: ImageData
+}
+
+interface VideoUploadRes {
+  code: number
+  data: {
+    video_id: string
+  }
+}
+
+interface SelectInfo {
+  value: any
+  Label: string
+}
+
 export type {
   UploadAssetForm,
   GameItem,
@@ -59,4 +94,8 @@ export type {
   TagItem,
   TagsRes,
   AddTagRes,
+  ImageData,
+  ImgUploadRes,
+  VideoUploadRes,
+  SelectInfo,
 }

+ 9 - 9
src/views/Promotion/accManage/accTencentAd.vue

@@ -6,33 +6,30 @@ import type {
   Operations,
 } from '@/types/Tables/Operations/operations'
 import type {
+  BaseFieldItem,
   FilterInfo,
   TableInfoItem,
-  BaseFieldItem,
 } from '@/types/Tables/table'
+import { TableFilterType } from '@/types/Tables/table'
 import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type { BaseMenu } from '@/types/Promotion/Menu'
 
 import type {
   AdvertiserData,
-  BusinessManagerData,
-  AgencyData,
-} from '@/types/Tables/tableData/tencentAcc'
-import type {
   AdvertiserFields,
-  BusinessManagerFields,
+  AgencyData,
   AgencyFields,
+  BusinessManagerData,
+  BusinessManagerFields,
 } from '@/types/Tables/tableData/tencentAcc'
 
 import type {
-  BaseTableInfo,
   BaseAllFieldsInfo,
   BaseAllFilterInfo,
   BaseAllFixedFiledsInfo,
+  BaseTableInfo,
 } from '@/types/Tables/tablePageData'
 import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
-
-import { TableFilterType } from '@/types/Tables/table'
 import { useRequest } from '@/hooks/useRequest'
 import { reactive } from 'vue'
 
@@ -191,6 +188,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['advertiserTable'],
     fixedFields: allFixedFileds['advertiserTable'],
+    remote: true,
   },
   businessManagerTable: {
     data: [],
@@ -205,6 +203,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['businessManagerTable'],
     fixedFields: allFixedFileds['businessManagerTable'],
+    remote: true,
   },
   agencyTable: {
     data: [],
@@ -219,6 +218,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['agencyTable'],
     fixedFields: allFixedFileds['agencyTable'],
+    remote: true,
   },
 })
 

+ 14 - 16
src/views/Promotion/accManage/accTtAd.vue

@@ -1,45 +1,40 @@
 <script setup lang="ts">
+import MenuTable from '@/components/promotion/MenuTable.vue'
+import { useRequest } from '@/hooks/useRequest'
 import { EleBtnType } from '@/types/Button/buttonType'
+import type { BaseMenu } from '@/types/Promotion/Menu'
+import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
 
 import type {
   OperationItem,
   Operations,
 } from '@/types/Tables/Operations/operations'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type {
+  BaseFieldItem,
   FilterInfo,
   TableInfoItem,
-  BaseFieldItem,
 } from '@/types/Tables/table'
-import type { TablePaginationProps } from '@/types/Tables/pagination'
-import type { BaseMenu } from '@/types/Promotion/Menu'
+import { TableFilterType } from '@/types/Tables/table'
 
 import type {
   AdvertiserAccData,
-  ManagerAccData,
-  AgencyAccData,
-} from '@/types/Tables/tableData/ttAcc'
-
-import type {
   AdvertiserAccFileds,
-  ManagerAccFileds,
+  AgencyAccData,
   AgencyAccFileds,
+  ManagerAccData,
+  ManagerAccFileds,
 } from '@/types/Tables/tableData/ttAcc'
 
 import type {
-  BaseTableInfo,
   BaseAllFieldsInfo,
   BaseAllFilterInfo,
   BaseAllFixedFiledsInfo,
+  BaseTableInfo,
 } from '@/types/Tables/tablePageData'
-import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
-
-import { TableFilterType } from '@/types/Tables/table'
-import { useRequest } from '@/hooks/useRequest'
 
 import { reactive } from 'vue'
 
-import MenuTable from '@/components/promotion/MenuTable.vue'
-
 // 表格信息
 interface TableInfo extends BaseTableInfo {
   advertiserAccTable: TableInfoItem<
@@ -193,6 +188,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['advertiserAccTable'],
     fixedFields: allFixedFileds['advertiserAccTable'],
+    remote: true,
   },
   managerAccTable: {
     data: [],
@@ -207,6 +203,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['managerAccTable'],
     fixedFields: allFixedFileds['managerAccTable'],
+    remote: true,
   },
   agencyAccTable: {
     data: [],
@@ -221,6 +218,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['agencyAccTable'],
     fixedFields: allFixedFileds['agencyAccTable'],
+    remote: true,
   },
 })
 

+ 14 - 16
src/views/Promotion/adManage/tencentAd.vue

@@ -1,45 +1,40 @@
 <script setup lang="ts">
+import MenuTable from '@/components/promotion/MenuTable.vue'
+import { useRequest } from '@/hooks/useRequest'
 import { EleBtnType } from '@/types/Button/buttonType'
+import type { BaseMenu } from '@/types/Promotion/Menu'
+import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
 
 import type {
   OperationItem,
   Operations,
 } from '@/types/Tables/Operations/operations'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type {
+  BaseFieldItem,
   FilterInfo,
   TableInfoItem,
-  BaseFieldItem,
 } from '@/types/Tables/table'
-import type { TablePaginationProps } from '@/types/Tables/pagination'
-import type { BaseMenu } from '@/types/Promotion/Menu'
+import { TableFilterType } from '@/types/Tables/table'
 
 import type {
-  MediaTableData,
+  AdFileds,
   AdTableData,
+  CreativityFileds,
   CreativityTableData,
-} from '@/types/Tables/tableData/tencentAd'
-
-import type {
+  MediaTableData,
   MediaTableFileds,
-  AdFileds,
-  CreativityFileds,
 } from '@/types/Tables/tableData/tencentAd'
 
 import type {
-  BaseTableInfo,
   BaseAllFieldsInfo,
   BaseAllFilterInfo,
   BaseAllFixedFiledsInfo,
+  BaseTableInfo,
 } from '@/types/Tables/tablePageData'
-import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
-
-import { TableFilterType } from '@/types/Tables/table'
-import { useRequest } from '@/hooks/useRequest'
 
 import { reactive } from 'vue'
 
-import MenuTable from '@/components/promotion/MenuTable.vue'
-
 // 表格信息
 interface TableInfo extends BaseTableInfo {
   mediaTable: TableInfoItem<MediaTableData, BaseFieldItem<MediaTableFileds>> // 媒体账户信息
@@ -184,6 +179,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['mediaTable'],
     fixedFields: allFixedFileds['mediaTable'],
+    remote: true,
   },
   adTable: {
     data: [],
@@ -198,6 +194,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['adTable'],
     fixedFields: allFixedFileds['adTable'],
+    remote: true,
   },
   creativityTable: {
     data: [],
@@ -212,6 +209,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['creativityTable'],
     fixedFields: allFixedFileds['creativityTable'],
+    remote: true,
   },
 })
 

+ 11 - 8
src/views/Promotion/adManage/ttad.vue

@@ -1,18 +1,22 @@
 <script setup lang="ts">
+import MenuTable from '@/components/promotion/MenuTable.vue'
+import { useDate } from '@/hooks/useDate'
+import { useRequest } from '@/hooks/useRequest'
 import { EleBtnType } from '@/types/Button/buttonType'
+import type { BaseMenu } from '@/types/Promotion/Menu'
+import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
 
 import type {
   OperationItem,
   Operations,
 } from '@/types/Tables/Operations/operations'
+import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type {
   BaseFieldItem,
   FilterInfo,
   TableInfoItem,
 } from '@/types/Tables/table'
 import { TableFilterType } from '@/types/Tables/table'
-import type { TablePaginationProps } from '@/types/Tables/pagination'
-import type { BaseMenu } from '@/types/Promotion/Menu'
 
 import type {
   AdmgeTTAccData,
@@ -29,13 +33,8 @@ import type {
   BaseAllFixedFiledsInfo,
   BaseTableInfo,
 } from '@/types/Tables/tablePageData'
-import type { MenuTableReq } from '@/types/Tables/MenuTable/menuTableReq'
-import { useRequest } from '@/hooks/useRequest'
-import { useDate } from '@/hooks/useDate'
 import { reactive } from 'vue'
 
-import MenuTable from '@/components/promotion/MenuTable.vue'
-
 // 表格信息
 interface TableInfo extends BaseTableInfo {
   account: TableInfoItem<AdmgeTTAccData, BaseFieldItem<AdmgeTTAccFileds>> // 账户信息
@@ -208,6 +207,7 @@ const accFilterInfo: FilterInfo = {
     name: 'search',
     type: TableFilterType.Search,
     value: 'name',
+    needPrefixSelect: true,
     options: [
       { label: '名称', value: 'name' },
       { label: '备注', value: 'backup' },
@@ -544,6 +544,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['account'],
     fixedFields: allFixedFileds['account'],
+    remote: true,
   },
   project: {
     data: [],
@@ -558,6 +559,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['project'],
     fixedFields: allFixedFileds['project'],
+    remote: true,
   },
   advertise: {
     data: [],
@@ -572,6 +574,7 @@ const tableInfo = reactive<TableInfo>({
     tableSortedFields: [],
     filters: allFilters['advertise'],
     fixedFields: allFixedFileds['advertise'],
+    remote: true,
   },
 })
 
@@ -581,7 +584,7 @@ const tablePaginationConfig = reactive<TablePaginationProps>({
   pageSizeList: [10, 20, 40],
 })
 
-// 自定义的操作字段
+// 自定义的操作字段e
 const operations: Operation = {
   account: [
     {

+ 11 - 7
src/views/Promotion/promotion.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import type { BaseMenu } from '@/types/Promotion/Menu'
+import Menu from '@/components/navigation/Menu.vue'
 
 import router from '@/router'
-import Menu from '@/components/navigation/Menu.vue'
+import type { BaseMenu } from '@/types/Promotion/Menu'
 
 import { ref } from 'vue'
 
@@ -11,7 +11,7 @@ const defaultActive = ref<string>(router.currentRoute.value.path)
 
 // 菜单配置
 // 其中父菜单的path需要配置,如果没有子菜单,父菜单的path将作为跳转路由和高亮的依据,参考Meun组件
-const menu: Array<BaseMenu> = [
+const menuList: Array<BaseMenu> = [
   {
     name: 'adManage',
     title: '广告管理',
@@ -62,11 +62,12 @@ const menu: Array<BaseMenu> = [
     <div class="promotionSiderBar">
       <Menu
         mode="vertical"
-        :menu-list="menu"
+        :menu-list="menuList"
         :default-active="defaultActive"
         :router="true"
       ></Menu>
     </div>
+    <!--    TODO 滚动条-->
     <div class="promotionContent">
       <router-view></router-view>
     </div>
@@ -83,7 +84,7 @@ const menu: Array<BaseMenu> = [
 .promotionSiderBar {
   /* width: 180px; */
   height: 100%;
-  position: relative;
+  position: fixed;
 }
 
 .promotionMenu {
@@ -98,9 +99,12 @@ const menu: Array<BaseMenu> = [
 }
 
 .promotionContent {
-  width: 100%;
+  width: 85%;
   height: 100%;
-  overflow: auto;
+  position: relative;
+  left: 15%;
+  overflow: scroll;
+
   /* height: calc(100% - 64px); */
   /* overflow: scroll; */
   /* padding: 20px; */

文件差异内容过多而无法显示
+ 0 - 0
stats.html


部分文件因为文件数量过多而无法显示