Ver código fonte

合并develop分支

fxs 8 meses atrás
pai
commit
7f656a9c35
67 arquivos alterados com 5296 adições e 1057 exclusões
  1. 1 0
      auto-imports.d.ts
  2. 11 0
      components.d.ts
  3. 11 0
      package-lock.json
  4. 2 1
      package.json
  5. 1 0
      public/img/default/defaultGame.svg
  6. 1 0
      public/img/platformIcon/backup/tt - 副本.svg
  7. 1 0
      public/img/platformIcon/backup/tt.svg
  8. 1 0
      public/img/platformIcon/backup/web - 副本.svg
  9. 1 0
      public/img/platformIcon/backup/web.svg
  10. 1 0
      public/img/platformIcon/backup/wx - 副本.svg
  11. 1 0
      public/img/platformIcon/backup/wx.svg
  12. 0 6
      public/img/platformIcon/chrome.svg
  13. 0 3
      public/img/platformIcon/tiktok.svg
  14. 1 0
      public/img/platformIcon/tt.svg
  15. 1 0
      public/img/platformIcon/web.svg
  16. 0 6
      public/img/platformIcon/wechat.svg
  17. 1 0
      public/img/platformIcon/wx.svg
  18. 2 2
      src/App.vue
  19. BIN
      src/assets/default/defaultHead.png
  20. 0 6
      src/assets/platformIcon/chrome.svg
  21. 0 3
      src/assets/platformIcon/tiktok.svg
  22. 0 6
      src/assets/platformIcon/wechat.svg
  23. 385 103
      src/components/Table.vue
  24. 149 0
      src/components/common/Dialog.vue
  25. 188 0
      src/components/common/WithIconSelect.vue
  26. 2 2
      src/components/dataAnalysis/DropDownSelection.vue
  27. 170 40
      src/components/dataAnalysis/HeaderCard.vue
  28. 1 1
      src/components/dataAnalysis/StatisticText.vue
  29. 35 14
      src/components/dataAnalysis/TemporalTrend.vue
  30. 1 2
      src/components/echarts/TimeLineChart.vue
  31. 245 0
      src/components/form/FileUpload.vue
  32. 229 0
      src/components/form/Form.vue
  33. 3 2
      src/hooks/useAnalysis.ts
  34. 13 1
      src/hooks/useDialog.ts
  35. 82 0
      src/hooks/useForm.ts
  36. 52 0
      src/hooks/usePage.ts
  37. 30 5
      src/hooks/useRequest.ts
  38. 73 28
      src/hooks/useTable.ts
  39. 68 0
      src/router/appManage.ts
  40. 99 35
      src/router/home.ts
  41. 9 6
      src/router/index.ts
  42. 4 2
      src/stores/useCommon.ts
  43. 26 14
      src/types/dataAnalysis.ts
  44. 19 0
      src/types/dialog.ts
  45. 23 0
      src/types/form.ts
  46. 17 3
      src/types/table.ts
  47. 2 3
      src/utils/axios/axiosInstance.ts
  48. 79 1
      src/utils/common/index.ts
  49. 13 2
      src/utils/resource/index.ts
  50. 28 1
      src/utils/table/table.ts
  51. 199 0
      src/views/AppManage/BaseInfoView.vue
  52. 572 0
      src/views/AppManage/EventDetailsView.vue
  53. 402 0
      src/views/AppManage/EventManageView.vue
  54. 321 0
      src/views/AppManage/EventMangeTable.vue
  55. 226 0
      src/views/Home/Analysis/EventAnalysisDetail.vue
  56. 194 0
      src/views/Home/Analysis/EventAnalysisTable.vue
  57. 93 0
      src/views/Home/Analysis/EventAnalysisView.vue
  58. 85 40
      src/views/Home/Analysis/KeepView.vue
  59. 193 50
      src/views/Home/Analysis/UserTrendView.vue
  60. 0 348
      src/views/Home/HomeView.vue
  61. 91 88
      src/views/Home/InfoManage/GameManageView.vue
  62. 155 102
      src/views/Home/InfoManage/PlayerManageView.vue
  63. 166 67
      src/views/Home/Overview/OverView.vue
  64. 469 0
      src/views/Index.vue
  65. 1 0
      src/views/Login/LoginView.vue
  66. 1 11
      tsconfig.app.json
  67. 46 53
      vite.config.ts

+ 1 - 0
auto-imports.d.ts

@@ -6,4 +6,5 @@
 export {}
 declare global {
   const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElNotification: typeof import('element-plus/es')['ElNotification']
 }

+ 11 - 0
components.d.ts

@@ -7,6 +7,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    Dialog: typeof import('./src/components/common/Dialog.vue')['default']
     DropDownSelection: typeof import('./src/components/dataAnalysis/DropDownSelection.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -14,6 +15,10 @@ declare module 'vue' {
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
+    ElDrawer: typeof import('element-plus/es')['ElDrawer']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
@@ -26,6 +31,7 @@ declare module 'vue' {
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
@@ -33,13 +39,17 @@ declare module 'vue' {
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
+    FileUpload: typeof import('./src/components/form/FileUpload.vue')['default']
     FilterPopover: typeof import('./src/components/toolsBtn/FilterPopover.vue')['default']
+    Form: typeof import('./src/components/form/Form.vue')['default']
     HeaderCard: typeof import('./src/components/dataAnalysis/HeaderCard.vue')['default']
     IconIcBaselineVisibility: typeof import('~icons/ic/baseline-visibility')['default']
     IconIcBaselineVisibilityOff: typeof import('~icons/ic/baseline-visibility-off')['default']
     IconIconParkGameThree: typeof import('~icons/icon-park/game-three')['default']
     IconMaterialSymbolsLightLogout: typeof import('~icons/material-symbols-light/logout')['default']
     IconMdiPassword: typeof import('~icons/mdi/password')['default']
+    IconTablerPointFilled: typeof import('~icons/tabler/point-filled')['default']
     MyButton: typeof import('./src/components/form/MyButton.vue')['default']
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
     RegreshBtn: typeof import('./src/components/toolsBtn/RegreshBtn.vue')['default']
@@ -49,6 +59,7 @@ declare module 'vue' {
     Table: typeof import('./src/components/Table.vue')['default']
     TemporalTrend: typeof import('./src/components/dataAnalysis/TemporalTrend.vue')['default']
     TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']
+    WithIconSelect: typeof import('./src/components/common/WithIconSelect.vue')['default']
   }
   export interface ComponentCustomProperties {
     vLoading: typeof import('element-plus/es')['ElLoadingDirective']

+ 11 - 0
package-lock.json

@@ -28,6 +28,7 @@
         "@iconify-json/logos": "^1.1.44",
         "@iconify-json/material-symbols-light": "^1.1.28",
         "@iconify-json/mdi": "^1.1.68",
+        "@iconify-json/tabler": "^1.2.0",
         "@rushstack/eslint-patch": "^1.8.0",
         "@tsconfig/node20": "^20.1.4",
         "@types/node": "^20.14.5",
@@ -751,6 +752,16 @@
         "@iconify/types": "*"
       }
     },
+    "node_modules/@iconify-json/tabler": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/@iconify-json/tabler/-/tabler-1.2.0.tgz",
+      "integrity": "sha512-ke7ESt/wurkWr/VrMrAguN8jOmf8tD9LOOqhn3OwAx8kg0lYwMkOahV6jTGXh2zU90z1drsD34cKV7RoNe0PuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@iconify/types": "*"
+      }
+    },
     "node_modules/@iconify/types": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz",

+ 2 - 1
package.json

@@ -4,7 +4,7 @@
   "private": true,
   "type": "module",
   "scripts": {
-    "dev": "vite",
+    "dev": "vite --host 0.0.0.0",
     "build": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
@@ -33,6 +33,7 @@
     "@iconify-json/logos": "^1.1.44",
     "@iconify-json/material-symbols-light": "^1.1.28",
     "@iconify-json/mdi": "^1.1.68",
+    "@iconify-json/tabler": "^1.2.0",
     "@rushstack/eslint-patch": "^1.8.0",
     "@tsconfig/node20": "^20.1.4",
     "@types/node": "^20.14.5",

+ 1 - 0
public/img/default/defaultGame.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="1725591308908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4433" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M704.32 974.4H321.81333333c-77.54666667 0-150.61333333-30.4-205.76-85.54666667S30.50666667 760.64 30.50666667 683.2v-0.42666667c0-77.54666667 30.4-150.61333333 85.54666666-205.76C171.2 421.86666667 244.26666667 391.46666667 321.81333333 391.46666667h382.61333334c77.54666667 0 150.61333333 30.4 205.76 85.54666666 55.14666667 55.14666667 85.54666667 128.21333333 85.54666666 205.76v0.42666667c0 77.54666667-30.4 150.61333333-85.54666666 205.76s-128.32 85.44-205.86666667 85.44zM321.81333333 466.24c-119.36 0-216.53333333 97.17333333-216.53333333 216.53333333v0.42666667c0 119.36 97.17333333 216.53333333 216.53333333 216.53333333h382.61333334c119.36 0 216.53333333-97.17333333 216.53333333-216.53333333v-0.42666667c0-119.36-97.17333333-216.53333333-216.53333333-216.53333333H321.81333333z" fill="#ac4cfd" p-id="4434"></path><path d="M263.89333333 655.68H412.8v57.38666667H263.89333333z" fill="#ac4cfd" p-id="4435"></path><path d="M311.11466667 756.24106667l0.35093333-148.90666667 57.38666667 0.1344-0.352 148.90666667zM682.77333333 764.69333333c-45.65333333 0-82.88-37.12-82.88-82.88s37.12-82.88 82.88-82.88 82.88 37.12 82.88 82.88-37.12 82.88-82.88 82.88z m0-117.65333333c-19.2 0-34.88 15.68-34.88 34.88s15.68 34.88 34.88 34.88 34.88-15.68 34.88-34.88-15.57333333-34.88-34.88-34.88zM545.70666667 463.78666667L488.53333333 432.74666667l100.05333334-184.42666667H385.81333333l-27.84-49.38666667 86.4-143.36 55.78666667 33.49333334-56.74666667 94.18666666h199.89333334l28.58666666 48z" fill="#ac4cfd" p-id="4436"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/tt - 副本.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="1726209175050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1296" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#111111" p-id="1297"></path><path d="M204.27776 670.59712a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V134.35904h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z" fill="#FF4040" p-id="1298"></path><path d="M164.92544 631.23456a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V94.99648h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z" fill="#00F5FF" p-id="1299"></path><path d="M410.91072 427.58144c-158.8224 20.15232-284.44672 222.72-154.112 405.00224 120.40192 98.47808 373.68832 41.20576 380.70272-171.85792l-0.17408-324.1472a280.7296 280.7296 0 0 0 142.88896 38.62528V261.2224a144.98816 144.98816 0 0 1-72.8064-54.82496 135.23968 135.23968 0 0 1-54.70208-72.45824h-123.66848l-0.08192 561.41824c-0.11264 78.46912-130.9696 106.41408-164.18816 30.2592-83.18976-39.77216-64.37888-190.9248 46.31552-192.57344z" fill="#FFFFFF" p-id="1300"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/tt.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="1725874777803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1034" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M866.304 0H157.696C71.168 0 0 69.632 0 156.16v711.168C0 954.368 71.168 1024 157.696 1024h709.12c86.528 0 157.184-69.632 157.696-156.16V156.16C1023.488 69.632 952.832 0 866.304 0zM842.24 444.416c-63.488 0.512-126.976-19.456-179.2-56.32 0 80.384 0 160.768 1.024 241.152 0 10.752 0.512 21.504-0.512 32.256-2.56 39.424-15.36 77.312-34.816 111.616-16.896 29.184-39.936 55.808-67.584 75.776-35.328 26.112-79.36 40.96-123.392 41.984-22.528 0.512-45.056-0.512-66.56-5.632-30.72-6.656-60.416-19.456-86.016-37.376-0.512-0.512-1.536-1.024-1.536-1.536-27.648-19.456-50.688-44.544-67.584-73.728-23.04-38.4-34.816-83.968-33.28-129.024 1.024-41.472 13.312-82.432 34.816-117.76 19.456-32.256 47.104-59.904 79.36-79.36 46.592-28.672 103.424-40.96 157.696-33.28v30.72c0.512 12.8 0 25.6 0.512 37.888v65.024c-16.384-5.632-34.304-5.632-51.2-1.536-19.968 4.608-39.424 13.312-55.296 26.624-9.728 8.192-17.92 17.92-23.552 29.184-9.728 18.944-13.312 40.96-11.264 62.464 2.56 20.48 11.264 40.448 24.576 56.32 9.216 10.752 20.48 19.968 32.768 27.136 33.28 18.432 76.288 16.896 108.544-3.584 31.232-19.456 51.2-54.784 51.2-91.136V322.56c0.512-62.976 0-125.952 0.512-188.928h130.048c-0.512 11.264 1.024 22.016 3.072 33.28h0.512c3.584 18.432 10.24 35.84 18.944 52.736 13.312 25.6 32.768 48.64 57.344 63.488 1.536 1.024 2.56 2.56 4.096 3.584 18.432 11.264 38.4 19.968 59.392 24.064 12.288 2.56 25.088 3.584 37.376 4.096v129.536z" fill="#000000" p-id="1035"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/web - 副本.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="1726209354219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1023" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M-78.43050413-31.57210636m209.68898924 0l745.56084983 0q209.68898924 0 209.68898803 209.68898925l0 745.56084982q0 209.68898924-209.68898803 209.68898803l-745.56084983 0q-209.68898924 0-209.68898924-209.68898803l0-745.56084982q0-209.68898924 209.68898924-209.68898925Z" fill="#FFFFFF" p-id="1024"></path><path d="M705.22384595 648.51918119L503.11860861 1032.59951255a478.30058416 478.30058416 0 0 1-401.90389514-737.40627815l209.3045587 370.660237z" fill="#539E55" p-id="1025"></path><path d="M513.773568 331.93543453L940.19798691 355.68517484C1047.62320729 596.96617031 934.67103141 878.34461738 693.39003593 985.76983776A548.53474592 548.53474592 0 0 1 497.32519344 1032.72945103l207.15433954-383.90395942z" fill="#F1BF45" p-id="1026"></path><path d="M313.30347569 671.66651582l-212.19360768-376.40338485c143.84664607-221.51311815 441.86129749-288.35730773 663.36276602-144.51066167a478.78985774 478.78985774 0 0 1 175.84751562 205.14572788l-426.01812848-23.84629791z" fill="#D44F3E" p-id="1027"></path><path d="M504.03891002 550.8973078m-221.33837733 0a221.33837733 221.33837733 0 1 0 442.67675466 0 221.33837733 221.33837733 0 1 0-442.67675466 0Z" fill="#FFFFFF" p-id="1028"></path><path d="M340.94747436 556.72200185a168.91612972 163.09143567 90 1 0 326.18287134 0 168.91612972 163.09143567 90 1 0-326.18287134 0Z" fill="#467EE6" p-id="1029"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/web.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="1725845503147" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1030" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M34.13333334-8.53333334l948.08746667 0-77.55093333 900.02773334-401.95413334 123.42613333-382.29333333-123.42613333zM793.25866668 263.44106666l22.9376-136.53333333-610.57706667-1.09226667 39.3216 409.6 397.58506667 0 0 107.04213334-133.25653334 46.96746666-138.71786666-49.152-8.73813334-57.89013333-109.22666666 1.09226667 16.384 144.1792 233.74506666 81.92 245.76-77.55093334 31.67573334-329.86453333-432.5376 0-10.92266667-138.71786667 456.56746667 0z" p-id="1031"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/wx - 副本.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="1726209181365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1467" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#65DB79" p-id="1468"></path><path d="M663.21408 407.02976c-128.75776 0-233.13408 87.296-233.13408 194.97984s104.37632 194.97984 233.13408 194.97984a273.85856 273.85856 0 0 0 88.79104-14.76608l66.74432 35.55328-8.98048-64.12288a183.78752 183.78752 0 0 0 86.5792-151.64416c-0.01024-107.68384-104.38656-194.97984-233.13408-194.97984z" fill="#FFFFFF" p-id="1469"></path><path d="M404.48 194.56c137.0112 0 250.28608 83.968 276.16256 195.2768-48.82432-4.01408-302.08 23.27552-261.85728 271.36-36.4032 0.1024-86.016-1.49504-121.5488-13.4656l-80.62976 42.97728 10.8544-77.45536C164.7104 571.7504 122.88 505.00608 122.88 430.08c0-130.048 126.07488-235.52 281.6-235.52z" fill="#FFFFFF" p-id="1470"></path><path d="M313.344 352.256m-36.864 0a36.864 36.864 0 1 0 73.728 0 36.864 36.864 0 1 0-73.728 0Z" fill="#65DB79" p-id="1471"></path><path d="M497.664 352.256m-36.864 0a36.864 36.864 0 1 0 73.728 0 36.864 36.864 0 1 0-73.728 0Z" fill="#65DB79" p-id="1472"></path><path d="M585.728 544.768m-32.768 0a32.768 32.768 0 1 0 65.536 0 32.768 32.768 0 1 0-65.536 0Z" fill="#65DB79" p-id="1473"></path><path d="M741.376 544.768m-32.768 0a32.768 32.768 0 1 0 65.536 0 32.768 32.768 0 1 0-65.536 0Z" fill="#65DB79" p-id="1474"></path></svg>

+ 1 - 0
public/img/platformIcon/backup/wx.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="1725845505819" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1183" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M236.24 323.769c0 24.005 19.32 43.325 43.325 43.325s43.326-19.32 43.326-43.325c0-24.005-19.321-43.325-43.326-43.325-24.004 0-43.325 19.32-43.325 43.325z m336.064 223.067c0 18.735 15.223 33.958 33.958 33.958s33.958-15.223 33.958-33.958-15.223-33.958-33.958-33.958-33.958 15.223-33.958 33.958zM456.38 323.769c0 24.005 19.32 43.325 43.325 43.325 24.005 0 43.325-19.32 43.325-43.325 0-24.005-19.32-43.325-43.325-43.325-24.005 0-43.325 19.32-43.325 43.325z" p-id="1184"></path><path d="M858.017 0H165.983C74.648 0 0.293 74.356 0.293 166.276v690.863c0 91.92 74.355 166.276 165.69 166.276h692.034c91.335 0 165.69-74.356 165.69-166.276V166.276C1023.707 74.94 949.352 0 858.017 0zM384.366 686.18c-38.642 0-69.672-8.197-108.899-15.808L167.154 724.82l31.03-93.09C120.316 577.28 74.063 507.022 74.063 422.128c0-147.54 139.929-264.05 310.303-264.05 152.81 0 286.298 93.09 313.23 217.798a255.341 255.341 0 0 0-29.859-1.757c-147.54 0-264.05 110.07-264.05 245.315 0 22.834 3.513 44.496 9.367 64.988-9.367 1.171-18.735 1.757-28.688 1.757z m457.843 108.313l23.42 77.283-84.895-46.838c-31.03 7.61-62.06 15.808-93.09 15.808-147.541 0-264.051-100.703-264.051-225.41 0-124.12 116.51-225.408 264.05-225.408 139.344 0 263.465 101.288 263.465 225.409-0.585 70.843-46.838 132.903-108.899 179.156z" p-id="1185"></path><path d="M741.507 546.836c0 18.735 15.223 33.958 33.958 33.958s33.958-15.223 33.958-33.958-15.223-33.958-33.958-33.958c-19.32 0-33.958 15.223-33.958 33.958z" p-id="1186"></path></svg>

+ 0 - 6
public/img/platformIcon/chrome.svg

@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 15 15">
-	<path fill="currentColor" d="M2.503 1.907A7.47 7.47 0 0 1 7.5 0a7.5 7.5 0 0 1 6.635 4H7.5a3.5 3.5 0 0 0-3.23 2.149z" />
-	<path fill="currentColor" d="M1.745 2.69a7.503 7.503 0 0 0 3.41 11.937l2.812-3.658Q7.737 11 7.5 11a3.5 3.5 0 0 1-3.412-2.716a.5.5 0 0 1-.05-.092z" />
-	<path fill="currentColor" d="M6.215 14.89Q6.842 15 7.5 15a7.5 7.5 0 0 0 7.072-10.005L14.5 5H9.95A3.5 3.5 0 0 1 11 7.5a3.5 3.5 0 0 1-.953 2.405z" />
-	<path fill="currentColor" d="M5 7.5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0" />
-</svg>

+ 0 - 3
public/img/platformIcon/tiktok.svg

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-	<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2m5.939 7.713v.646a.37.37 0 0 1-.38.37a5.36 5.36 0 0 1-2.903-1.108v4.728a3.94 3.94 0 0 1-1.18 2.81a4 4 0 0 1-2.87 1.17a4.1 4.1 0 0 1-2.862-1.17a3.98 3.98 0 0 1-1.026-3.805c.159-.642.48-1.232.933-1.713a3.58 3.58 0 0 1 2.79-1.313h.82v1.703a.348.348 0 0 1-.39.348a1.918 1.918 0 0 0-1.23 3.631c.27.155.572.246.882.267c.24.01.48-.02.708-.092a1.93 1.93 0 0 0 1.313-1.816V5.754a.36.36 0 0 1 .359-.36h1.415a.36.36 0 0 1 .359.34a3.3 3.3 0 0 0 1.282 2.245a3.25 3.25 0 0 0 1.641.636a.37.37 0 0 1 .338.35z" />
-</svg>

+ 1 - 0
public/img/platformIcon/tt.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="1726209175050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1296" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#111111" p-id="1297"></path><path d="M204.27776 670.59712a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V134.35904h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z" fill="#FF4040" p-id="1298"></path><path d="M164.92544 631.23456a246.25152 246.25152 0 0 1 245.97504-245.97504v147.57888a98.49856 98.49856 0 0 0-98.38592 98.38592c0 48.34304 26.14272 100.352 83.54816 100.352 3.81952 0 93.55264-0.88064 93.55264-77.19936V94.99648h157.26592a133.31456 133.31456 0 0 0 133.12 132.99712l-0.13312 147.31264a273.152 273.152 0 0 1-142.62272-38.912l-0.06144 317.98272c0 146.00192-124.24192 224.77824-241.14176 224.77824-131.74784 0.03072-231.1168-106.56768-231.1168-247.92064z" fill="#00F5FF" p-id="1299"></path><path d="M410.91072 427.58144c-158.8224 20.15232-284.44672 222.72-154.112 405.00224 120.40192 98.47808 373.68832 41.20576 380.70272-171.85792l-0.17408-324.1472a280.7296 280.7296 0 0 0 142.88896 38.62528V261.2224a144.98816 144.98816 0 0 1-72.8064-54.82496 135.23968 135.23968 0 0 1-54.70208-72.45824h-123.66848l-0.08192 561.41824c-0.11264 78.46912-130.9696 106.41408-164.18816 30.2592-83.18976-39.77216-64.37888-190.9248 46.31552-192.57344z" fill="#FFFFFF" p-id="1300"></path></svg>

+ 1 - 0
public/img/platformIcon/web.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="1726209354219" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1023" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M-78.43050413-31.57210636m209.68898924 0l745.56084983 0q209.68898924 0 209.68898803 209.68898925l0 745.56084982q0 209.68898924-209.68898803 209.68898803l-745.56084983 0q-209.68898924 0-209.68898924-209.68898803l0-745.56084982q0-209.68898924 209.68898924-209.68898925Z" fill="#FFFFFF" p-id="1024"></path><path d="M705.22384595 648.51918119L503.11860861 1032.59951255a478.30058416 478.30058416 0 0 1-401.90389514-737.40627815l209.3045587 370.660237z" fill="#539E55" p-id="1025"></path><path d="M513.773568 331.93543453L940.19798691 355.68517484C1047.62320729 596.96617031 934.67103141 878.34461738 693.39003593 985.76983776A548.53474592 548.53474592 0 0 1 497.32519344 1032.72945103l207.15433954-383.90395942z" fill="#F1BF45" p-id="1026"></path><path d="M313.30347569 671.66651582l-212.19360768-376.40338485c143.84664607-221.51311815 441.86129749-288.35730773 663.36276602-144.51066167a478.78985774 478.78985774 0 0 1 175.84751562 205.14572788l-426.01812848-23.84629791z" fill="#D44F3E" p-id="1027"></path><path d="M504.03891002 550.8973078m-221.33837733 0a221.33837733 221.33837733 0 1 0 442.67675466 0 221.33837733 221.33837733 0 1 0-442.67675466 0Z" fill="#FFFFFF" p-id="1028"></path><path d="M340.94747436 556.72200185a168.91612972 163.09143567 90 1 0 326.18287134 0 168.91612972 163.09143567 90 1 0-326.18287134 0Z" fill="#467EE6" p-id="1029"></path></svg>

+ 0 - 6
public/img/platformIcon/wechat.svg

@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-	<g fill="none" fill-rule="evenodd">
-		<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
-		<path fill="currentColor" d="M16 10c3.154 0 6 2.186 6 5.213c0 1.696-.92 3.153-2.26 4.08c-.044.265-.008.532-.008.799a.903.903 0 0 1-.897.908c-.68 0-1.31-.224-1.843-.645a7 7 0 0 1-.992.071c-3.154 0-6-2.186-6-5.213S12.846 10 16 10M9.5 3c3.777 0 7.149 2.535 7.474 6.062A8 8 0 0 0 16 9c-3.533 0-7 2.473-7 6.209q0 .531.09 1.027a9 9 0 0 1-.926-.094c-.67.549-1.443.858-2.31.858c-.518 0-.937-.434-.937-.97l.004-.197l.017-.397a2.1 2.1 0 0 0-.055-.59C3.17 13.667 2 11.794 2 9.624C2 5.807 5.525 3 9.5 3M14 13a1 1 0 1 0 0 2a1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2M7 6a1 1 0 1 0 0 2a1 1 0 0 0 0-2m5 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2" />
-	</g>
-</svg>

+ 1 - 0
public/img/platformIcon/wx.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="1726209181365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1467" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#65DB79" p-id="1468"></path><path d="M663.21408 407.02976c-128.75776 0-233.13408 87.296-233.13408 194.97984s104.37632 194.97984 233.13408 194.97984a273.85856 273.85856 0 0 0 88.79104-14.76608l66.74432 35.55328-8.98048-64.12288a183.78752 183.78752 0 0 0 86.5792-151.64416c-0.01024-107.68384-104.38656-194.97984-233.13408-194.97984z" fill="#FFFFFF" p-id="1469"></path><path d="M404.48 194.56c137.0112 0 250.28608 83.968 276.16256 195.2768-48.82432-4.01408-302.08 23.27552-261.85728 271.36-36.4032 0.1024-86.016-1.49504-121.5488-13.4656l-80.62976 42.97728 10.8544-77.45536C164.7104 571.7504 122.88 505.00608 122.88 430.08c0-130.048 126.07488-235.52 281.6-235.52z" fill="#FFFFFF" p-id="1470"></path><path d="M313.344 352.256m-36.864 0a36.864 36.864 0 1 0 73.728 0 36.864 36.864 0 1 0-73.728 0Z" fill="#65DB79" p-id="1471"></path><path d="M497.664 352.256m-36.864 0a36.864 36.864 0 1 0 73.728 0 36.864 36.864 0 1 0-73.728 0Z" fill="#65DB79" p-id="1472"></path><path d="M585.728 544.768m-32.768 0a32.768 32.768 0 1 0 65.536 0 32.768 32.768 0 1 0-65.536 0Z" fill="#65DB79" p-id="1473"></path><path d="M741.376 544.768m-32.768 0a32.768 32.768 0 1 0 65.536 0 32.768 32.768 0 1 0-65.536 0Z" fill="#65DB79" p-id="1474"></path></svg>

+ 2 - 2
src/App.vue

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:06:49
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-29 09:43:18
+ * @LastEditTime: 2024-09-05 17:30:14
  * @FilePath: \Game-Backstage-Management-System\src\App.vue
  * @Description: 
  * 
@@ -13,7 +13,7 @@ import { zhCn } from 'element-plus/es/locales.mjs'
 
 <template>
   <el-config-provider :locale="zhCn">
-    <RouterView />
+    <router-view></router-view>
   </el-config-provider>
 </template>
 

BIN
src/assets/default/defaultHead.png


+ 0 - 6
src/assets/platformIcon/chrome.svg

@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 15 15">
-	<path fill="currentColor" d="M2.503 1.907A7.47 7.47 0 0 1 7.5 0a7.5 7.5 0 0 1 6.635 4H7.5a3.5 3.5 0 0 0-3.23 2.149z" />
-	<path fill="currentColor" d="M1.745 2.69a7.503 7.503 0 0 0 3.41 11.937l2.812-3.658Q7.737 11 7.5 11a3.5 3.5 0 0 1-3.412-2.716a.5.5 0 0 1-.05-.092z" />
-	<path fill="currentColor" d="M6.215 14.89Q6.842 15 7.5 15a7.5 7.5 0 0 0 7.072-10.005L14.5 5H9.95A3.5 3.5 0 0 1 11 7.5a3.5 3.5 0 0 1-.953 2.405z" />
-	<path fill="currentColor" d="M5 7.5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0" />
-</svg>

+ 0 - 3
src/assets/platformIcon/tiktok.svg

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-	<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2m5.939 7.713v.646a.37.37 0 0 1-.38.37a5.36 5.36 0 0 1-2.903-1.108v4.728a3.94 3.94 0 0 1-1.18 2.81a4 4 0 0 1-2.87 1.17a4.1 4.1 0 0 1-2.862-1.17a3.98 3.98 0 0 1-1.026-3.805c.159-.642.48-1.232.933-1.713a3.58 3.58 0 0 1 2.79-1.313h.82v1.703a.348.348 0 0 1-.39.348a1.918 1.918 0 0 0-1.23 3.631c.27.155.572.246.882.267c.24.01.48-.02.708-.092a1.93 1.93 0 0 0 1.313-1.816V5.754a.36.36 0 0 1 .359-.36h1.415a.36.36 0 0 1 .359.34a3.3 3.3 0 0 0 1.282 2.245a3.25 3.25 0 0 0 1.641.636a.37.37 0 0 1 .338.35z" />
-</svg>

+ 0 - 6
src/assets/platformIcon/wechat.svg

@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-	<g fill="none" fill-rule="evenodd">
-		<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
-		<path fill="currentColor" d="M16 10c3.154 0 6 2.186 6 5.213c0 1.696-.92 3.153-2.26 4.08c-.044.265-.008.532-.008.799a.903.903 0 0 1-.897.908c-.68 0-1.31-.224-1.843-.645a7 7 0 0 1-.992.071c-3.154 0-6-2.186-6-5.213S12.846 10 16 10M9.5 3c3.777 0 7.149 2.535 7.474 6.062A8 8 0 0 0 16 9c-3.533 0-7 2.473-7 6.209q0 .531.09 1.027a9 9 0 0 1-.926-.094c-.67.549-1.443.858-2.31.858c-.518 0-.937-.434-.937-.97l.004-.197l.017-.397a2.1 2.1 0 0 0-.055-.59C3.17 13.667 2 11.794 2 9.624C2 5.807 5.525 3 9.5 3M14 13a1 1 0 1 0 0 2a1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2M7 6a1 1 0 1 0 0 2a1 1 0 0 0 0-2m5 0a1 1 0 1 0 0 2a1 1 0 0 0 0-2" />
-	</g>
-</svg>

+ 385 - 103
src/components/Table.vue

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 18:16:18
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-02 15:57:42
+ * @LastEditTime: 2024-09-18 12:00:45
  * @FilePath: \Game-Backstage-Management-System\src\components\Table.vue
  * @Description: 
  * 
@@ -11,14 +11,20 @@
 import type { PropsParams, TablePaginationSetting } from '@/types/table'
 import type { ReqConfig } from '@/types/dataAnalysis'
 import { FilterType, FieldSpecialEffectType } from '@/types/table'
+import { initLoadResouce } from '@/utils/resource'
+import { fuzzySearch } from '@/utils/common'
 
-import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { computed, onMounted, reactive, ref, toRaw, watch } from 'vue'
 import { useTable } from '@/hooks/useTable'
 
 import FilterPopover from './toolsBtn/FilterPopover.vue'
 import RegreshBtn from './toolsBtn/RegreshBtn.vue'
+import { useRequest } from '@/hooks/useRequest'
 
 import type { FormInstance } from 'element-plus'
+import axiosInstance from '@/utils/axios/axiosInstance'
+
+const { analysisResCode } = useRequest()
 
 // 表格工具图标大小
 const toolsIconSize = ref(25)
@@ -33,11 +39,14 @@ const props = withDefaults(defineProps<PropsParams>(), {
   needLeftTools: false,
   needRightTools: false,
   openFilterQuery: false,
-  openPageQuery: false
+  openPageQuery: false,
+  needUpload: false,
+  needDownLoad: false
 })
 
 // 父组件触发的方法
-const emits = defineEmits(['addNewItem'])
+// 删除了一个事件触发,loadSuccess
+const emits = defineEmits(['addNewItem', 'upload', 'downLoad'])
 
 // 加载动画
 const loading = ref(false)
@@ -45,16 +54,20 @@ const loading = ref(false)
 // 表格数据
 const tableData: Array<any> = reactive([])
 
+// 备份表格数据,用于在不分页查询的时候,恢复数据
+const backupTableData: Array<any> = []
+
 // 查询表单的数据
-const queryFormData = reactive<any>({})
+const queryFormData = reactive<{ [key: string]: any }>({})
+
+const backupQueryFormData = reactive<{ [key: string]: any }>({})
 
 // 分页数据
 const paginationConfig2 = reactive<TablePaginationSetting>({
   currentPage: 0,
   limit: 0,
   total: 0,
-  pagesizeList: [],
-  hasLodingData: 0
+  pagesizeList: []
 })
 
 // 请求配置
@@ -63,6 +76,14 @@ const reqconfig = reactive<ReqConfig>({
   otherOptions: {}
 })
 
+// 资源的加载路径
+const resourceInfo: Record<string, string> = {
+  defaultHead: `/img/default/defaultHead.png`
+}
+
+// 使用blob的资源路径信息
+const blobUrlInfo = reactive<Record<string, string>>({})
+
 // 一些公用方法
 const { getTableData } = useTable(tableData, paginationConfig2)
 
@@ -84,9 +105,6 @@ const inputFieldsList = computed(() => {
 // 所有类型为select的表单控件信息
 const selectFieldsList = computed(() => {
   return props.queryInfo?.filter((item) => {
-    if (item.default) {
-      queryFormData[item.name] = item.default
-    }
     return item.type === FilterType.SELECT
   })
 })
@@ -116,91 +134,139 @@ const handleSizeChange = (val: number) => {
  * @return {*}
  */
 const getData = () => {
-  return new Promise((resolve, reject) => {
-    if (props.dataList) {
-      tableData.splice(0, tableData.length, ...props.dataList)
-      paginationConfig2.total = props.paginationConfig.total
-      loading.value = false
-      resolve(true)
-    } else {
-      loading.value = true
-      if (props.requestConfig) {
-        if (props.openPageQuery) {
-          // 如果开启了分页查询,那么要计算出需要展示的页码位置所对应的偏移量
-          // 同时要将查询的条数改为对应的用户选择的展示条数
-          reqconfig.otherOptions.offset =
-            (paginationConfig2.currentPage - 1) * paginationConfig2.limit
-          reqconfig.otherOptions.limit = paginationConfig2.limit
-        }
-
-        // 查询时要根据是否开启分页查询传入对应参数
-        getTableData(reqconfig.url, reqconfig.otherOptions, props.openPageQuery)
-          .then(() => {
-            resolve(true)
-          })
-          .catch((err) => {
-            console.log(err)
-            reject(err)
-          })
-          .finally(() => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      Object.assign(queryFormData, JSON.parse(JSON.stringify(backupQueryFormData)))
+      const loadTableData = async () => {
+        return new Promise((resolve, reject) => {
+          if (props.dataList) {
+            tableData.splice(0, tableData.length, ...props.dataList)
+
+            paginationConfig2.total = props.paginationConfig.total
             loading.value = false
-          })
-      } else {
-        loading.value = false
-        throw new Error('no match requestConfig')
-      }
-    }
+            // emits('loadSuccess', tableData)
 
-    if (tableData.length) {
-      if (props.needAverage) {
-        let rowData: any = {}
-        let oldList: Array<any> = JSON.parse(JSON.stringify(tableData))
-        Object.values(props.tableFieldsInfo).map((item, index) => {
-          let sum = oldList
-            .map((item) => item.count)
-            .reduce((accumulator, currentValue) => accumulator + currentValue, 0)
-          let averageList = oldList
-            .map((val) => val[item.name])
-            .filter((item) => item !== undefined)
-          if (index === 0) rowData[item.name] = '均值'
-          else if (item.name === 'count') rowData[item.name] = sum
-          else {
-            let num =
-              averageList.reduce((accumulator, currentValue) => accumulator + currentValue, 0) /
-              averageList.length
-
-            rowData[item.name] = isNaN(num) ? 0 : num.toFixed(2)
+            resolve(true)
+          } else {
+            loading.value = true
+            if (props.requestConfig) {
+              if (props.openPageQuery) {
+                // 如果开启了分页查询,那么要计算出需要展示的页码位置所对应的偏移量
+                // 同时要将查询的条数改为对应的用户选择的展示条数
+                reqconfig.otherOptions.offset =
+                  (paginationConfig2.currentPage - 1) * paginationConfig2.limit
+                reqconfig.otherOptions.limit = paginationConfig2.limit
+              }
+
+              // 查询时要根据是否开启分页查询传入对应参数
+              getTableData(reqconfig.url, reqconfig.otherOptions, props.openPageQuery)
+                .then(() => {
+                  // emits('loadSuccess', tableData)
+                  backupTableData.splice(0, backupTableData.length, ...tableData)
+
+                  resolve(true)
+                })
+                .catch((err) => {
+                  console.log(err)
+
+                  reject(err)
+                })
+                .finally(() => {
+                  loading.value = false
+                })
+            } else {
+              loading.value = false
+
+              throw new Error('no match requestConfig')
+            }
           }
         })
-        insertRow(0, rowData)
       }
+      // 等待数据加载完成
+      await loadTableData()
+        .then(async () => {
+          if (props.needAverage) {
+            let rowData: any = {}
+            let oldList: Array<any> = JSON.parse(JSON.stringify(tableData))
+            Object.values(props.tableFieldsInfo).map((item, index) => {
+              let sum = oldList
+                .map((item) => item.count)
+                .reduce((accumulator, currentValue) => accumulator + currentValue, 0)
+              let averageList = oldList
+                .map((val) => val[item.name])
+                .filter((item) => item !== undefined)
+              if (index === 0) rowData[item.name] = '均值'
+              else if (item.name === 'count') rowData[item.name] = sum
+              else {
+                let num =
+                  averageList.reduce((accumulator, currentValue) => accumulator + currentValue, 0) /
+                  averageList.length
+
+                rowData[item.name] = isNaN(num) ? 0 : num.toFixed(2)
+              }
+            })
+            insertRow(0, rowData)
+          }
+          resolve(true)
+        })
+        .catch((err) => {
+          console.log(err)
+          reject(err)
+        })
+    } catch (err) {
+      console.log(err)
+      reject(err)
     }
   })
 }
 
-// 清空表格数据
+/**
+ * @description: 清空表格数据
+ * @return {*}
+ */
 const resetTableData = () => {
   tableData.splice(0, tableData.length)
 }
 
-// 按条件查询
+/**
+ * @description: 按条件查询,如果开启了分页查询,那么会直接重新查询数据,否则,会根据现有数据进行查询
+ * @return {*}
+ */
 const queryTableData = () => {
   if (props.requestConfig) {
-    reqconfig.otherOptions = { ...reqconfig.otherOptions, ...queryFormData }
-    getData()
-  } else {
-    throw new Error('no match requestConfig')
+    reqconfig.otherOptions = { ...props.requestConfig.otherOptions, ...queryFormData }
+  }
+  if (props.openPageQuery) getData()
+  else {
+    let filteredTable = []
+    // 过滤出来所有符合formData数据的条件
+    filteredTable = backupTableData.filter((item) => {
+      let state = true
+      for (let [k, v] of Object.entries(queryFormData)) {
+        // 模糊查询,看值是否跟表格中的数据匹配
+        if (!fuzzySearch(v, item[k])) {
+          state = false
+          break
+        }
+      }
+      return state
+    })
+
+    tableData.splice(0, tableData.length, ...filteredTable)
   }
 }
 
 /**
- * @description: 重置整个查询表单(无效目前)
- * @param {*} formEl
+ * @description: 重置整个查询表单,重置后,再请求一次全部表格
+ * @param {*} formEl  表单对象
  * @return {*}
  */
 const resetQueryForm = (formEl: FormInstance | undefined) => {
   if (!formEl) return
-  formEl?.resetFields()
+  // clearReactiveData(queryFormData)
+  // queryFormData
+  Object.assign(queryFormData, JSON.parse(JSON.stringify(backupQueryFormData)))
+  queryTableData()
 }
 
 /**
@@ -248,37 +314,49 @@ const tableCellStyle = (info: any) => {
   } else return {}
 }
 
-// 根据分页大小的切换来更新数据
-// 这里将他赋值,用于根据传入的配置来选择是否开启该监听
 /**
- * @description: 监听litmit,currentpage,gid的变化,改变后去重新请求数据
+ * @description: 监听litmit,currentpage的变化,改变后去重新请求数据
  * 如果是limit的变化,则需要把当前页置为1
  *
  *  对于Gid需要去监听props的,而不是本地的,因为是外部的改变
  * @return {*}
  */
 const changePageLimit = watch(
-  () => [
-    paginationConfig2.limit,
-    props.requestConfig?.otherOptions.gid,
-    paginationConfig2.currentPage
-  ],
-  ([newLimit, newGid], [oldLimit, oldGid]) => {
+  () => [paginationConfig2.limit, paginationConfig2.currentPage],
+  ([newLimit, newCurPage], [oldLimit, oldCruPage]) => {
     if (newLimit != oldLimit) {
       // 需要给分页按钮加上:current-page.sync="current_page" 配置,不然不生效
       // 当改变每页大小时把之前的缓存全部清除,重新开始
       paginationConfig2.currentPage = 1
       // resetTableData()
     }
-    if (newGid != oldGid) reqconfig.otherOptions.gid = newGid
 
-    if (newLimit != oldLimit || !tableData[paginationConfig2.currentPage] || newGid != oldGid) {
+    if (newCurPage != oldCruPage) paginationConfig2.currentPage = newCurPage
+
+    // if (newGid != oldGid) reqconfig.otherOptions.gid = newGid
+    // || newGid != oldGid
+
+    if (newLimit != oldLimit || !tableData[paginationConfig2.currentPage]) {
       getData()
     }
   },
   { deep: true }
 )
 
+/**
+ * @description: 监听gid的变化,重新请求数据,这里很奇怪,跟上面的limit和page放到一起不起作用
+ * @return {*}
+ */
+watch(
+  () => props.requestConfig?.otherOptions.gid,
+  (newGid, oldGid) => {
+    if (newGid != oldGid) {
+      reqconfig.otherOptions.gid = newGid
+      getData()
+    }
+  }
+)
+
 // 监听传入的datalist的变化,然后去更新数据
 const changeDataList = watch(
   () => [props.dataList],
@@ -290,6 +368,15 @@ const changeDataList = watch(
   }
 )
 
+// 监听日期的变化,
+const watchDateChange = watch(
+  () => [props.requestConfig?.otherOptions.startTime, props.requestConfig?.otherOptions.endTime],
+  () => {
+    getData()
+  },
+  { deep: true }
+)
+
 /**
  * @description: 创建row-key优化表格性能
  * @return {*}
@@ -298,6 +385,11 @@ const createRowKey = () => {
   return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
 }
 
+//如果没有日期就取消掉
+if (!props.requestConfig?.otherOptions.startTime && !props.requestConfig?.otherOptions.endTime) {
+  watchDateChange()
+}
+
 // 没传入datalist则取消该监听
 if (!props.dataList) {
   changeDataList()
@@ -308,29 +400,106 @@ if (!props.openPageQuery) {
   changePageLimit()
 }
 
+/**
+ * @description: 拷贝一份配置文件
+ * @return {*}
+ */
 const initpageConfig = () => {
   Object.assign(paginationConfig2, props.paginationConfig)
 }
 
+/**
+ * @description: 初始化请求配置,用于把拷贝一份新的数据
+ * @return {*}
+ */
 const initReqConfig = () => {
   Object.assign(reqconfig, props.requestConfig)
 }
 
+/**
+ * @description: 初始化查询框的数据
+ * @return {*}
+ */
+const initFormData = () => {
+  props.queryInfo?.map((item: any) => {
+    queryFormData[item.name] = item.default
+  })
+  // backupQueryFormData = JSON.parse(JSON.stringify(queryFormData))
+  Object.assign(backupQueryFormData, JSON.parse(JSON.stringify(queryFormData)))
+}
+
+/**
+ * @description: 表格排序
+ * @param {*} data 获取到的数据
+ * @return {*}
+ */
+const tableSortChange = (data: { column: any; prop: string; order: any }) => {
+  let { order } = { ...data }
+  if (order === 'ascending') order = 'asc'
+  else if (order === 'descending') order = 'desc'
+  else order = ''
+  reqconfig.otherOptions.order = order
+  getData()
+}
+
+/**
+ * @description: 删除行
+ * @param {*} url 请求地址
+ * @param {*} row 行数据
+ * @return {*}
+ */
+const deleteRow = (url: string, filedsInfo: any) => {
+  axiosInstance
+    .post(url, { ...filedsInfo })
+    .then((data) => {
+      analysisResCode(data).then(() => {
+        getData()
+      })
+    })
+    .catch((err) => {
+      console.log(err)
+    })
+}
+
+/**
+ * @description: 下载表格数据
+ * @return {*}
+ */
+const downLoadTable = () => {
+  emits('downLoad', JSON.parse(JSON.stringify(tableData)))
+}
+
+/**
+ * @description: 外部获取数据
+ * @return {*}
+ */
+const outGetTableData = () => {
+  return toRaw(tableData).flat()
+}
+
 // 定义暴露出去的方法
 defineExpose({
   getData,
-  resetTableData
+  resetTableData,
+  deleteRow,
+  downLoadTable,
+  outGetTableData
 })
 
 onMounted(() => {
   initpageConfig()
   initReqConfig()
+  initFormData()
   if (props.loadingState !== undefined) {
     loading.value = props.loadingState
   }
-  if (!props.openPageQuery) {
-    getData()
-  }
+  // if (!props.openPageQuery) {
+  //   getData()
+  // }
+  // 去加载所有需要的资源
+  initLoadResouce(resourceInfo).then((data) => {
+    Object.assign(blobUrlInfo, data)
+  })
 })
 </script>
 
@@ -359,11 +528,18 @@ onMounted(() => {
           </el-form-item>
 
           <!-- 所有选择框 -->
+          <!-- <el-config-provider :value-on-clear="null" :empty-values="[undefined, null]"> -->
           <el-form-item :label="item.label" v-for="item in selectFieldsList" class="filterItem">
-            <el-select v-model="queryFormData[item.name]" :placeholder="item.placeholder">
+            <el-select
+              :empty-values="[undefined, null]"
+              v-model="queryFormData[item.name]"
+              :placeholder="item.placeholder"
+              :value-key="item.name"
+            >
               <el-option v-for="val in item.otherOption" :label="val.cnName" :value="val.value" />
             </el-select>
           </el-form-item>
+          <!-- </el-config-provider> -->
 
           <!-- 所有日期选择框 -->
           <el-form-item :label="item.label" v-for="item in dateFieldsList" class="filterItem">
@@ -391,15 +567,31 @@ onMounted(() => {
       </div>
     </div>
     <!-- 分割线 -->
-    <el-divider class="partition" content-position="center" />
+    <!-- <el-divider class="partition" content-position="center" /> -->
     <div class="tableTools">
       <div class="leftTools">
-        <el-button v-if="needLeftTools" type="primary" color="#165dff" @click="emits('addNewItem')">
-          <el-icon><Plus /></el-icon>新增
-        </el-button>
+        <div class="leftToolsGroup" v-if="needLeftTools" style="display: flex">
+          <el-button class="leftToolBtn" color="#165dff" @click="emits('addNewItem')">
+            <el-icon><Plus /></el-icon>新增
+          </el-button>
+          <el-button
+            class="leftToolBtn"
+            color="#626aef"
+            @click="emits('upload', outGetTableData())"
+            v-if="needUpload"
+          >
+            <el-icon><Upload /></el-icon>上传
+          </el-button>
+        </div>
       </div>
       <div class="rightTools" v-if="needRightTools">
-        <el-button color="#f0f1f3" size="default" class="rightToolsItem">
+        <el-button
+          v-if="needDownload"
+          color="#f0f1f3"
+          size="default"
+          class="rightToolsItem"
+          @click="downLoadTable"
+        >
           <el-icon><Download /></el-icon>下载
         </el-button>
 
@@ -421,6 +613,7 @@ onMounted(() => {
         :cell-style="tableCellStyle"
         v-loading="loading"
         :row-key="createRowKey()"
+        @sort-change="tableSortChange"
       >
         <el-table-column
           v-if="props.needRowindex"
@@ -433,10 +626,11 @@ onMounted(() => {
           <el-table-column
             :prop="item.name"
             :label="item.cnName"
-            width="auto"
+            :min-width="item.specialEffect?.type === FieldSpecialEffectType.DROPDOWN ? '170px' : ''"
             align="center"
             show-overflow-tooltip
             v-if="item.isShow"
+            :sortable="item.needSort === true ? 'custorm' : false"
           >
             <template v-slot="scope">
               <!-- tag类 -->
@@ -450,7 +644,8 @@ onMounted(() => {
                     : item.specialEffect.othnerInfo.text[1]
                 }}
               </el-tag>
-
+              <!-- :src="loadResource(scope.row[item.name])" -->
+              <!-- :src="scope.row[item.name]" -->
               <!-- 头像类 -->
               <el-image
                 v-else-if="item.specialEffect?.type === FieldSpecialEffectType.IMG"
@@ -463,7 +658,7 @@ onMounted(() => {
               >
                 <template #error>
                   <!--  -->
-                  <img style="width: 35px; height: 35px" src="../assets/default/defaultHead.png" />
+                  <img style="width: 35px; height: 35px" :src="blobUrlInfo.defaultHead" />
                 </template>
               </el-image>
 
@@ -481,9 +676,77 @@ onMounted(() => {
 
               <!-- 翻译类 -->
               <el-text v-else-if="item.specialEffect?.type === FieldSpecialEffectType.TRANSLATE">
-                {{ item.specialEffect.othnerInfo.translateText[scope.row[item.name]] }}
+                <el-icon
+                  v-if="item.specialEffect.othnerInfo.icon"
+                  style="padding-right: 8px"
+                  :color="scope.row[item.name] ? '#409EFF' : '#F56C6C'"
+                  ><icon-tabler-point-filled></icon-tabler-point-filled
+                ></el-icon>
+                {{
+                  item.specialEffect.othnerInfo.translateText[scope.row[item.name]]
+                    ? item.specialEffect.othnerInfo.translateText[scope.row[item.name]]
+                    : '未知'
+                }}
+              </el-text>
+
+              <!-- 状态类 -->
+              <el-text v-else-if="item.specialEffect?.type === FieldSpecialEffectType.STATE">
+                <span>
+                  <el-icon
+                    style="padding-right: 8px"
+                    :color="scope.row[item.name] ? '#409EFF' : '#F56C6C'"
+                    ><icon-tabler-point-filled></icon-tabler-point-filled
+                  ></el-icon>
+                  {{
+                    scope.row[item.name]
+                      ? item.specialEffect.othnerInfo.text[0]
+                      : item.specialEffect.othnerInfo.text[1]
+                  }}</span
+                >
               </el-text>
 
+              <!-- 开关类 -->
+
+              <el-switch
+                :active-value="1"
+                :inactive-value="0"
+                v-else-if="item.specialEffect?.type === FieldSpecialEffectType.SWITCH"
+                v-model="scope.row[item.name]"
+                :data="scope.row[item.name]"
+                size="default"
+              >
+              </el-switch>
+
+              <!-- 下拉菜单类 -->
+              <el-dropdown
+                trigger="click"
+                v-else-if="item.specialEffect?.type === FieldSpecialEffectType.DROPDOWN"
+              >
+                <span
+                  class="el-dropdown-link"
+                  style="display: flex; align-items: center; cursor: pointer"
+                >
+                  <el-icon
+                    style="padding-right: 8px"
+                    :color="scope.row[item.name] ? '#409EFF' : '#F56C6C'"
+                    ><icon-tabler-point-filled></icon-tabler-point-filled
+                  ></el-icon>
+                  {{
+                    scope.row[item.name]
+                      ? item.specialEffect.othnerInfo.text[0]
+                      : item.specialEffect.othnerInfo.text[1]
+                  }}
+
+                  <el-icon class="el-icon--right"><arrow-down /></el-icon>
+                </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item :command="{ value: true }">使用中</el-dropdown-item>
+                    <el-dropdown-item :command="{ value: false }">已弃用</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+
               <el-text v-else>
                 <!-- 其他列按默认方式显示 -->
 
@@ -522,10 +785,13 @@ onMounted(() => {
 
 <style scoped>
 .tableContent {
-  margin: 0 auto;
-  width: 98%;
+  margin: 10px auto 20px;
+  width: 100%;
 
   /* height: 100%; */
+  box-shadow:
+    0 4px 8px 0 rgba(0, 0, 0, 0.02),
+    0 1px 3px 0 rgba(0, 0, 0, 0.02);
 }
 .filterBox,
 .tableBox {
@@ -549,7 +815,7 @@ onMounted(() => {
   display: flex;
   align-items: center;
   color: black;
-  font-size: 20px;
+  font-size: 16px;
   font-weight: bold;
   /* background-color: lightblue; */
   /* margin-bottom: 1%; */
@@ -617,20 +883,32 @@ onMounted(() => {
 }
 
 .rightTools {
-  width: 10%;
+  width: 5%;
 }
 
 .tableBox {
   width: 98%;
   /* height: 98%; */
-  margin: 0 auto;
+  margin: 5px auto;
+  box-shadow:
+    0 4px 8px 0 rgba(0, 0, 0, 0.02),
+    0 1px 3px 0 rgba(0, 0, 0, 0.02);
   /* margin-top: 0.5%;
   margin-bottom: 2%; */
 }
 
+.tableBody {
+  box-shadow: 0 0 3px 0px rgba(0, 0, 0, 0.1);
+  /* box-shadow:
+    -4px -4px 8px 1px rgba(0, 0, 0, 0.02),
+    -4px -4px 3px 1px rgba(0, 0, 0, 0.02); */
+}
+
 .userTablePaginationBox {
+  box-sizing: border-box;
   width: 98%;
-  margin: 0.5% auto;
+  margin: 0% auto;
+  padding: 1% 0;
   display: flex;
   justify-content: center;
 }
@@ -645,4 +923,8 @@ onMounted(() => {
   color: #515b6f;
   font-weight: 400;
 }
+
+.leftToolBtn {
+  margin-right: 5px;
+}
 </style>

+ 149 - 0
src/components/common/Dialog.vue

@@ -0,0 +1,149 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-09-04 11:21:05
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-09 18:02:59
+ * @FilePath: \Game-Backstage-Management-System\src\components\common\Dialog.vue
+ * @Description: 
+ * 
+-->
+
+<script setup lang="ts">
+import Form from '../form/Form.vue'
+import { useDialog } from '@/hooks/useDialog'
+
+import { nextTick, onMounted, reactive, ref } from 'vue'
+
+import type { DialogConfig } from '@/types/dialog'
+
+interface DialogProps {
+  config: DialogConfig
+}
+
+// 临时用的close
+const { dialogClose2 } = useDialog()
+
+const emits = defineEmits(['formSubmit'])
+
+// 新增的URL与更新URL,因为有时候更新和新增在一起,有时候是分开的,所以在这里去区分
+const addUrl = ref<string>('')
+const updateUrl = ref<string>('')
+
+// 对话框ref
+const dialogFormRef = ref<InstanceType<typeof Form>>()
+
+// props
+const props = withDefaults(defineProps<DialogProps>(), {})
+
+// props中的关于配置的信息
+const dialogConfigInfo = props.config
+
+// 游戏配置对话框设置
+const dialogConfig = reactive({
+  dialogVisible: false,
+  title: '',
+  formLabelWidth: '150px',
+  type: 0 // 0 是新增 1是修改
+})
+
+// 游戏配置提交
+const submiteGameChange = () => {
+  dialogFormRef.value?.submitFormData().then(() => {
+    dialogConfig.dialogVisible = false
+  })
+}
+
+// 表单关闭
+const closeDialog = () => {
+  dialogClose2(dialogFormRef.value, dialogConfig)
+}
+
+// 表单添加
+const addForm = () => {
+  dialogConfigInfo.reqConfig.url = addUrl.value
+  dialogConfig.dialogVisible = true
+}
+
+// 表单修改
+const editForm = (row: any, updateURL?: string) => {
+  dialogConfig.dialogVisible = true
+  if (updateURL) {
+    updateUrl.value = updateURL
+    dialogConfigInfo.reqConfig.url = updateUrl.value
+  }
+
+  nextTick(() => {
+    dialogFormRef.value?.fillForm(row)
+  })
+}
+
+onMounted(() => {
+  dialogConfig.title = dialogConfigInfo.title
+  addUrl.value = props.config.reqConfig.url // 保存一下新增的URL
+})
+
+/**
+ * @description: 对字段进行加密
+ * @param {*} fields  字段名
+ * @param {*} useFormField  是否对表单的字段加密
+ * @param {*} encryptMsg  加密的消息
+ * @return {*}
+ */
+const encrypt = (fields: string, useFormField: boolean, encryptMsg: Array<string>) => {
+  dialogFormRef.value?.encryptData(fields, useFormField, encryptMsg).finally(() => {
+    dialogConfig.dialogVisible = false
+  })
+}
+
+const subForm = () => {
+  emits('formSubmit')
+}
+
+defineExpose({
+  addForm,
+  editForm,
+  encrypt
+})
+</script>
+
+<template>
+  <div class="dialog">
+    <el-dialog
+      @close="closeDialog"
+      v-model="dialogConfig.dialogVisible"
+      :title="dialogConfig.title"
+      :close-on-click-modal="false"
+      style="width: 40%"
+      append-to-body
+    >
+      <Form
+        :disabled="true"
+        @sub-form="subForm"
+        ref="dialogFormRef"
+        :config="{
+          fieldsInfo: dialogConfigInfo.fieldsInfo,
+          reqConfig: dialogConfigInfo.reqConfig,
+          rules: dialogConfigInfo.rules
+        }"
+      ></Form>
+      <template #footer>
+        <div class="dialog-footer">
+          <slot name="otherBtn"></slot>
+          <slot name="btnGroup">
+            <el-button class="dialogBtn" type="primary" @click="submiteGameChange()">
+              确认
+            </el-button>
+            <el-button class="dialogBtn" @click="closeDialog">取消</el-button>
+          </slot>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.dialogBtn {
+  box-sizing: border-box;
+  margin-right: 10px;
+}
+</style>

+ 188 - 0
src/components/common/WithIconSelect.vue

@@ -0,0 +1,188 @@
+<script setup lang="ts">
+import { onMounted, ref, reactive, computed } from 'vue'
+import { initLoadResouce } from '@/utils/resource'
+import type { DropdownInstance } from 'element-plus'
+import type { IconDropdownItem } from '@/types/dataAnalysis'
+
+interface DropdownInfo {
+  isRadio?: boolean
+  slectInfo: Array<IconDropdownItem>
+}
+
+/**
+ * @description: 最大选择数
+ * @return {*}
+ */
+const maxSelect = 1
+
+// props
+const props = withDefaults(defineProps<DropdownInfo>(), {
+  isRadio: true
+})
+
+// emits
+const emits = defineEmits(['changePf'])
+
+// 下拉框
+const dropDownRef = ref<DropdownInstance>()
+
+// 资源的加载路径
+const resourceInfo: Record<string, string> = props.slectInfo.reduce(
+  (acc, item) => {
+    acc[item.value] = item.icon
+    return acc
+  },
+  {} as Record<string, string>
+)
+
+// 使用blob的资源路径信息
+const blobUrlInfo = reactive<Record<string, string>>({})
+
+// 备份信息
+const backupInfo = reactive<Array<IconDropdownItem>>([])
+
+/**
+ * @description: 确认选择
+ * @return {*}
+ */
+const confirmSelect = () => {
+  Object.assign(backupInfo, JSON.parse(JSON.stringify(props.slectInfo)))
+  dropDownRef.value?.handleClose()
+  emits(
+    'changePf',
+    props.slectInfo.filter((item) => item.isSelected).map((item) => item.value)
+  )
+}
+
+/**
+ * @description: 取消选择,当取消选择后,需要恢复到原来的状态
+ * @return {*}
+ */
+const cancleSelect = () => {
+  dropDownRef.value?.handleClose()
+  Object.assign(props.slectInfo, backupInfo)
+}
+
+/**
+ * @description: 下拉框出现与消失的处理
+ * @param {*} state 展示状态,值为true或者false,代表打开或者关闭
+ * @return {*}
+ */
+const dropdownVis = (state: boolean) => {
+  if (state) {
+    Object.assign(backupInfo, JSON.parse(JSON.stringify(props.slectInfo)))
+  } else {
+    Object.assign(props.slectInfo, backupInfo)
+  }
+}
+
+/**
+ * @description: 用于限制最多的选择个数
+ * @return {*}
+ */
+const canSelect = computed(() => {
+  return props.slectInfo.filter((item) => item.isSelected).length < maxSelect
+})
+
+/**
+ * @description: 选择事件,当目前可以选择或者是这个选项已经被选中的时候,就允许改变他的状态
+ * @param {*} item  当前的选项信息
+ * @return {*}
+ */
+const selectPf = (item: any) => {
+  if (canSelect.value || item.isSelected) {
+    item.isSelected = !item.isSelected
+  }
+}
+
+onMounted(() => {
+  // 去加载所有需要的资源
+
+  initLoadResouce(resourceInfo).then((data) => {
+    Object.assign(blobUrlInfo, data)
+  })
+})
+</script>
+
+<template>
+  <div>
+    <el-dropdown
+      trigger="click"
+      @visible-change="dropdownVis"
+      :hide-on-click="false"
+      ref="dropDownRef"
+    >
+      <span class="displayBox">
+        <div class="displayIcon">
+          <span class="iconItem" v-for="item in props.slectInfo">
+            <el-image
+              v-if="item.isSelected"
+              style="width: 20px; height: 20px; margin-right: 5px"
+              :src="blobUrlInfo[item.value]"
+              :fit="'cover'"
+            />
+          </span>
+        </div>
+
+        <el-icon class="el-icon--right">
+          <arrow-down />
+        </el-icon>
+      </span>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item
+            v-for="item in props.slectInfo"
+            :key="item.value"
+            :value="item.value"
+            @click="selectPf(item)"
+            :disabled="!canSelect && !item.isSelected"
+          >
+            <el-image
+              style="width: 20px; height: 20px; margin-right: 5px"
+              :src="blobUrlInfo[item.value]"
+              :fit="'cover'"
+            />
+            <!-- 禁用掉原生的点击事件,自己实现点击 -->
+            <el-checkbox
+              :disabled="!canSelect && !item.isSelected"
+              v-model="item.isSelected"
+              size="small"
+              @click.native.prevent="return"
+            />
+          </el-dropdown-item>
+        </el-dropdown-menu>
+        <span class="btnGroup">
+          <el-button class="btnItem" size="small" type="primary" @click="confirmSelect"
+            >确认</el-button
+          >
+          <el-button class="btnItem" size="small" @click="cancleSelect">取消</el-button>
+        </span>
+      </template>
+    </el-dropdown>
+  </div>
+</template>
+
+<style scoped>
+.displayBox {
+  display: flex;
+  align-items: center;
+}
+
+.displayIcon {
+  display: flex;
+  align-items: center;
+}
+
+.btnGroup {
+  display: flex;
+  justify-content: center;
+}
+.btnItem {
+  margin-right: 4px;
+  margin-left: 4px;
+}
+
+.disabledSelect {
+  cursor: not-allowed !important;
+}
+</style>

+ 2 - 2
src/components/dataAnalysis/DropDownSelection.vue

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-23 14:42:47
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-28 12:25:54
+ * @LastEditTime: 2024-09-09 09:58:18
  * @FilePath: \Game-Backstage-Management-System\src\components\dataAnalysis\DropDownSelection.vue
  * @Description: 下拉选择框,可用于分类字段或者切换平台等
  * 
@@ -26,7 +26,7 @@ const changeSelect = (val: any) => {
 </script>
 
 <template>
-  <div style="width: 150px; margin-right: 30px">
+  <div style="width: 150px">
     <el-select
       style="width: 100%"
       @change="changeSelect"

+ 170 - 40
src/components/dataAnalysis/HeaderCard.vue

@@ -7,33 +7,21 @@
 -->
 
 <script setup lang="ts">
-import DropDownSelection from './DropDownSelection.vue'
-import type { DropDownInfo, HeaderCardProps } from '@/types/dataAnalysis'
-import { onMounted, ref } from 'vue'
+import router from '@/router'
+import type { HeaderCardProps, IconDropdownItem } from '@/types/dataAnalysis'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+import WithIconSelect from '@/components/common/WithIconSelect.vue'
+import { useCommonStore } from '@/stores/useCommon'
 
-const props = defineProps<HeaderCardProps>()
+const { selectInfo } = useCommonStore()
 
-const emits = defineEmits(['changePf', 'changeDate'])
+const props = withDefaults(defineProps<HeaderCardProps>(), {
+  openDateSelect: false,
+  needPfSelect: true,
+  needBreadcrumb: false
+})
 
-// 平台下拉框信息
-const platFormOptionInfo: DropDownInfo = {
-  defaultSelect: props.defaultPf,
-  title: '请选择平台',
-  optionsList: [
-    {
-      value: 'web',
-      label: 'Web'
-    },
-    {
-      value: 'wx',
-      label: '微信'
-    },
-    {
-      value: 'tt',
-      label: '抖音'
-    }
-  ]
-}
+const emits = defineEmits(['changePf', 'changeDate'])
 
 // 快速选择日期
 const shortcuts = [
@@ -74,11 +62,6 @@ const dateChange = (val: any) => {
   emits('changeDate', val)
 }
 
-// 平台变化
-const changePf = (val: any) => {
-  emits('changePf', val)
-}
-
 // 控制日期范围
 /**
  * @description: 禁止选取今天之后的日期
@@ -89,23 +72,153 @@ const disableDate = (time: Date) => {
   return time.getTime() > Date.now()
 }
 
+const breadcrumbList = reactive<
+  Array<{
+    title: string
+    pathName: string
+  }>
+>([])
+
+// 是否可点击
+const breadcrumbCanClick = computed(() => breadcrumbList.length > 1)
+
+// 返回总览
+const goBack = (index: number) => {
+  if (breadcrumbCanClick) {
+    router.push(breadcrumbList[index].pathName).then(() => {
+      breadcrumbList.splice(index + 1, breadcrumbList.length - 1)
+    })
+  }
+}
+
+/**
+ * @description: 添加导航栏的信息
+ * @param {*} title 标题
+ * @param {*} pathName 对应的路由name
+ * @return {*}
+ */
+const addPath = (title: string, pathName: string) => {
+  breadcrumbList.push({ title, pathName })
+}
+
+/**
+ * @description: 当跳转到其他页面的时候,就需要清除掉这里的面包屑导航
+ * @return {*}
+ */
+const clearBreadcrumb = () => {
+  let nowName = router.currentRoute.value.name
+  // 如果现在是第一个导航,那么清除掉,这个是为了当直接点击左侧导航栏进入table页的时候可以清空
+  // 第二个条件是当切换到其他导航的时候,也给他清空掉
+  if (
+    nowName === breadcrumbList[0].pathName ||
+    breadcrumbList.every((item) => item.pathName !== nowName)
+  ) {
+    breadcrumbList.splice(1, breadcrumbList.length - 1)
+  }
+}
+
+// 下拉框的平台信息
+const pfSelectInfo = reactive<Array<IconDropdownItem>>([
+  {
+    value: 'web',
+    icon: '/img/platformIcon/web.svg',
+    label: '网页',
+    isSelected: false
+  },
+  {
+    value: 'wx',
+    icon: '/img/platformIcon/wx.svg',
+    label: '微信',
+    isSelected: true
+  },
+  {
+    value: 'tt',
+    icon: '/img/platformIcon/tt.svg',
+    label: '抖音',
+    isSelected: false
+  }
+])
+
+const changePlatForm = (val: Array<any>) => {
+  selectInfo.pf = [val[0]]
+}
+
+/**
+ * @description: 监控当前路由,每次路由改变都要去执行clearBreadcrumb,来判断是否需要清除当前面包屑
+ * @param {*} watch
+ * @return {*}
+ */
+const watchRoute = watch(
+  () => [router.currentRoute.value],
+  () => {
+    clearBreadcrumb()
+  },
+  { deep: true }
+)
+
+/**
+ * @description: 这里是为了去监控store中的pf,pf变化的时候,emits最新的pf
+ *   整体的流程是:当下拉框改变,他会emtis一个事件出来,这个时候,headercard组件接收新的值,去改变store中的pf
+ *   headercard组件中还有一个watch,用来监听store中的pf,一旦改变那么就去重新给下拉选择框赋值,并且etmiss一次
+ *   这个watch会在开始的时候执行一次,用来在页面加载的时候,给下拉框赋值,并且请求
+ * @return {*}
+ */
+watch(
+  () => selectInfo.pf,
+  (newPf) => {
+    emits('changePf', [newPf[0]])
+    pfSelectInfo.forEach((item) => {
+      if (item.value === newPf[0]) {
+        item.isSelected = true
+      } else {
+        item.isSelected = false
+      }
+    })
+  },
+  { immediate: true, deep: true }
+)
+
+if (!props.needBreadcrumb) watchRoute()
+
+defineExpose({
+  addPath,
+  clearBreadcrumb
+})
+
 onMounted(() => {
+  breadcrumbList.push({
+    title: props.title,
+    pathName: router.currentRoute.value.name as string
+  })
   dateChange(selectDate.value)
 })
 </script>
 
 <template>
   <div class="headerCard">
-    <span class="title">{{ title }}</span>
-    <el-divider direction="vertical" />
-    <div class="selectBox">
+    <p class="titleBox">
+      <span
+        @click="breadcrumbCanClick ? goBack(index) : ''"
+        v-for="(item, index) in breadcrumbList"
+        :class="[
+          index === breadcrumbList.length - 1 ? 'activeCrumbs' : 'noActive',
+          breadcrumbCanClick ? 'canClick' : 'unAble'
+        ]"
+      >
+        <span v-if="index === 0" class="titleContent">
+          {{ item.title }}
+        </span>
+        <span v-else>
+          <span class="divLine">/</span>
+          <span class="titleContent">{{ item.title }}</span>
+        </span>
+      </span>
+    </p>
+
+    <div class="selectBox" v-if="props.needPfSelect">
+      <el-divider direction="vertical" />
       <div class="selectItem">
-        <DropDownSelection
-          @changeSelect="changePf"
-          :defaultSelect="platFormOptionInfo.defaultSelect"
-          :title="platFormOptionInfo.title"
-          :optionsList="platFormOptionInfo.optionsList"
-        ></DropDownSelection>
+        <WithIconSelect @change-pf="changePlatForm" :slect-info="pfSelectInfo"></WithIconSelect>
       </div>
     </div>
     <div v-if="props.openDateSelect" class="datePicker">
@@ -137,12 +250,25 @@ onMounted(() => {
   background-color: white;
 }
 
-.title {
-  font-size: 16px;
+.canClick {
+  cursor: pointer;
+}
+
+.unAble {
+  cursor: default;
+}
+
+.activeCrumbs {
+  font-size: 18px;
   color: #17233d;
   font-weight: 600;
 }
 
+.noActive {
+  font-size: 16px;
+  color: rgba(23, 35, 61, 0.55);
+}
+
 .selectBox {
   display: inline-flex;
 }
@@ -182,4 +308,8 @@ onMounted(() => {
 .disableSelect:hover {
   cursor: no-drop;
 }
+
+.divLine {
+  margin: 0 5px;
+}
 </style>

+ 1 - 1
src/components/dataAnalysis/StatisticText.vue

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-26 13:57:37
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-02 15:50:11
+ * @LastEditTime: 2024-09-11 15:00:44
  * @FilePath: \Game-Backstage-Management-System\src\components\dataAnalysis\StatisticText.vue
  * @Description: 用于展示统计数据,如总览页面上方的总览数据
  * 

+ 35 - 14
src/components/dataAnalysis/TemporalTrend.vue

@@ -7,7 +7,7 @@
 -->
 
 <script setup lang="ts">
-import { onMounted, reactive, ref, watch } from 'vue'
+import { nextTick, onMounted, reactive, ref, watch } from 'vue'
 import type { TemporalTrendProps, OptionsProps, StaticField, ReqConfig } from '@/types/dataAnalysis'
 import type { TablePaginationSetting, TableFieldInfo } from '@/types/table'
 import Table from '../Table.vue'
@@ -16,10 +16,6 @@ import StatisticText from './StatisticText.vue'
 
 import axiosInstance from '@/utils/axios/axiosInstance'
 
-const props = defineProps<TemporalTrendProps>()
-const activeTab = ref<string>('') // 激活的Tab
-const iconSize = ref(20) // 图标的尺寸
-
 interface CacheData {
   paginationConfig: TablePaginationSetting
   tableDataList: Array<any>
@@ -28,6 +24,18 @@ interface CacheData {
   statisticFieldsInfo: Array<StaticField>
 }
 
+const props = withDefaults(defineProps<TemporalTrendProps>(), {
+  type: 1,
+  needChangeExpress: true,
+  selectExpressForm: 1,
+  needTable: true,
+  needCharts: true,
+  needDownload: false
+})
+const activeTab = ref<string>('') // 激活的Tab
+const iconSize = ref(20) // 图标的尺寸
+const chartTable = ref<InstanceType<typeof Table>>() // 图表的表格
+
 // 缓存数据
 const cacheData: Record<string, CacheData> = {}
 
@@ -81,27 +89,37 @@ let chartInfo = reactive<OptionsProps>({
 })
 
 /**
- * @description: 改变图表形式的时候
+ * @description: 改变图表形式,当是表格的时候,去执行表格的获取数据方法,拿到最新的数据
  * @param {*} name 图表的展现形式,可以使table或者trend
  * @return {*}
  */
 const changeSelectShape = (name: number) => {
+  if (selectShape.value === name) return
   selectShape.value = name
+  if (name === 2) {
+    nextTick(() => {
+      chartTable.value?.getData()
+    })
+  }
 }
 
 /**
  * @description: 生成表格字段信息,必须要调用,因为legend的字段也依赖这里
  * @return {*}
  */
+
 const createTableField = () => {
   //  生成表格的字段信息
+
   tableFieldsInfo.splice(0, tableFieldsInfo.length)
-  for (const [key, value] of Object.entries(props.tableFieldsInfo)) {
+
+  for (const [key, value] of Object.entries(props.tableFieldsInfo[activeTab.value])) {
     // 根据传入的信息,生成table需要的字段信息
     tableFieldsInfo.push({
       name: key,
       cnName: value as string,
-      isShow: true
+      isShow: true,
+      needSort: false
     })
   }
 }
@@ -115,8 +133,9 @@ const createTableData = (data: any) => {
   // 图表的x轴信息
 
   const xInfo = Object.keys(data[props.resDataFieldsInfo['xAxis']])
+
   const valInfo: any = {}
-  // 把所有的表格需要的数据假如valInfo,其中index字段需要单独拿出来用x轴的信息填充
+  // 把所有的表格需要的数据加入valInfo,其中index字段需要单独拿出来用x轴的信息填充
   props.resDataFieldsInfo['values'].map((key: string) => {
     valInfo[key] = Object.values(data[key])
   })
@@ -137,6 +156,7 @@ const createTableData = (data: any) => {
     newList.push(newItem)
   }
   tableDataList.splice(0, tableDataList.length, ...newList)
+
   paginationConfig.total = xInfo.length
 }
 
@@ -181,7 +201,6 @@ const setCacheData = (isUse: boolean = false) => {
     if (cachedData) {
       ;({ tableFieldsInfo, tableDataList, paginationConfig, statisticFieldsInfo, chartInfo } =
         JSON.parse(JSON.stringify(cachedData)))
-      console.log(statisticFieldsInfo)
     } else {
       console.warn('No cached data found for the current tab.')
     }
@@ -224,12 +243,14 @@ const isLegalData = (data: any): boolean => {
 const getData = async (type: number) => {
   loadDataState.state = false
   loadDataState.loading = true
+
   axiosInstance
     .post(requestInfo.url, { ...requestInfo.otherOptions, type })
     .then((info) => {
       let data = info.data
       loadDataState.state = true
 
+      // 判断返回的数据是否合法,不合法直接全置为空
       if (!isLegalData(data)) {
         paginationConfig = reactive({ ...initPageConfig })
 
@@ -250,7 +271,7 @@ const getData = async (type: number) => {
         // 如果有统计数据的配置,则生成
         if (props.staticFields) {
           statisticFieldsInfo.splice(0, statisticFieldsInfo.length)
-          props.staticFields.map((item) => {
+          props.staticFields[activeTab.value].map((item) => {
             statisticFieldsInfo.push({
               name: item.name,
               cnName: item.cnName,
@@ -300,6 +321,7 @@ Object.assign(requestInfo, props.requestConfig)
  * @description: 监听请求参数的变化,有变化了之后重新请求数据,并且把tab置为第一个
  * @return {*}
  */
+
 watch(
   () => [requestInfo.otherOptions],
   () => {
@@ -313,8 +335,7 @@ watch(
 )
 
 onMounted(() => {
-  // if (!props.waitTimeSelect) getData(1)
-  getData(1)
+  // getData(1)
 
   if (props.tabInfo) activeTab.value = props.tabInfo[0].name
 })
@@ -393,7 +414,7 @@ onMounted(() => {
             /></el-icon>
           </span>
         </span>
-        <span class="toolItem">
+        <span class="toolItem" v-if="needDownload">
           <el-icon :size="iconSize"><Download /></el-icon>
         </span>
       </div>

+ 1 - 2
src/components/echarts/TimeLineChart.vue

@@ -164,7 +164,6 @@ const changeLoading = (state: boolean) => {
 watch(
   () => [props.legendData, props.seriesData, props.xAxisData],
   () => {
-    console.log(props.seriesData)
     initOptions()
   },
   { deep: true }
@@ -191,7 +190,7 @@ onMounted(() => {
      * @return {*}
      */
     // window.addEventListener('resize', debounceFunc(changeSize, 500))
-    const debounceChangeSize = debounceFunc(changeSize, 500)
+    const debounceChangeSize = debounceFunc(changeSize, 200)
     const ro = new ResizeObserver(debounceChangeSize)
     ro.observe(chart.value)
   })

+ 245 - 0
src/components/form/FileUpload.vue

@@ -0,0 +1,245 @@
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { ElMessage, genFileId } from 'element-plus'
+import type { UploadInstance, UploadRawFile } from 'element-plus'
+
+interface UploadProps {
+  title: string
+}
+
+// 上传动画
+const loading = ref(false)
+
+// 上传组价
+const uploadRef = ref<UploadInstance>()
+
+// 侧边弹出框组件
+const tipVisisble = ref(false)
+
+// 上传成功的对外回调
+const emits = defineEmits(['uploadSuccess'])
+
+// props
+defineProps<UploadProps>()
+
+/**
+ * @description: 上传文件相关的所有信息
+ * @return {*}
+ */
+const uploadInfo = reactive({
+  uploadVisible: false
+})
+
+/**
+ * @description: 上传前对文件的处理
+ * @return {*}
+ */
+const beforeUpload = (file: UploadRawFile) => {
+  if (!file) return false // 取消上传
+  loading.value = true // 开始上传
+  let state = false
+  let reader = new FileReader()
+  reader.readAsText(file)
+  reader.onload = function () {
+    try {
+      const finnalResult = reader.result as string
+      //   console.log(JSON.parse(finnalResult))
+      emits('uploadSuccess', JSON.parse(finnalResult))
+      state = true
+    } catch {
+      ElMessage.error('文件内容不是JSON格式,上传失败')
+    }
+  }
+  return state // 继续上传
+}
+
+/**
+ * @description: 准备开始上传
+ * @return {*}
+ */
+const startUpload = () => {
+  uploadInfo.uploadVisible = true
+}
+
+/**
+ * @description: 取消上传
+ * @return {*}
+ */
+const cancelUpload = () => {
+  uploadInfo.uploadVisible = false
+}
+
+/**
+ * @description: 确认上传
+ * @return {*}
+ */
+const confirmUpload = () => {
+  uploadRef.value?.submit()
+}
+
+/**
+ * @description:处理超出文件数量限制后的方法,这里是直接覆盖掉
+ * @param {*} files 上传的文件
+ * @return {*}
+ */
+const handleExceed = (files: UploadRawFile[]) => {
+  uploadRef.value!.clearFiles()
+  const file = files[0] as UploadRawFile
+  file.uid = genFileId()
+  uploadRef.value!.handleStart(file)
+}
+
+/**
+ * @description: 打开提示
+ * @return {*}
+ */
+const openTip = () => {
+  tipVisisble.value = true
+}
+
+/**
+ * @description: 关闭掉上传组件
+ * @return {*}
+ */
+const closeUpload = () => {
+  uploadRef.value?.clearFiles()
+  uploadInfo.uploadVisible = false
+  loading.value = false // 上传结束
+}
+
+/**
+ * @description: 上传结束的回调
+ * @return {*}
+ */
+const uploadCallback = () => {
+  uploadInfo.uploadVisible = false // 关闭上传框
+  loading.value = false // 上传结束
+}
+
+defineExpose({
+  startUpload,
+  closeUpload,
+  uploadCallback
+})
+</script>
+
+<template>
+  <div class="uploadFile">
+    <el-dialog
+      align-center
+      v-model="uploadInfo.uploadVisible"
+      title="文件上传"
+      width="500"
+      @close="closeUpload"
+    >
+      <div class="uploadBox" v-loading="loading" element-loading-text="正在上传...">
+        <el-upload
+          :on-exceed="handleExceed"
+          :before-upload="beforeUpload"
+          accept=".json"
+          class="upload-demo"
+          drag
+          action="#"
+          :auto-upload="false"
+          :limit="1"
+          ref="uploadRef"
+        >
+          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+          <div class="el-upload__text">拖拽上传或者<em>点击文件上传</em></div>
+          <template #tip>
+            <div class="el-upload__tip">
+              请上传一个JSON格式文件,上传格式请点击<span @click="openTip" class="openTip"
+                >这里</span
+              >查看。<br />
+            </div>
+          </template>
+        </el-upload>
+      </div>
+      <template #footer>
+        <div class="dialog-footer" v-if="!loading">
+          <el-button type="primary" @click="confirmUpload"> 确定 </el-button>
+          <el-button @click="cancelUpload">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <el-drawer v-model="tipVisisble" :direction="'rtl'">
+      <template #header>
+        <h4>上传文件格式</h4>
+      </template>
+      <template #default>
+        <div>
+          <p>请提供以下格式的json文件</p>
+          <br />
+          <p>
+            <b>allEventTable</b>用于存放所有需要更新或者需要新增的事件,<b>以数组形式传入.</b>
+
+            如果是需要更新的事件,需要提供<b>id</b>字段,否则不需要。如果不传入id字段则<b
+              >默认为新增</b
+            >
+          </p>
+          <br />
+          <p>
+            <b>allOptionsInfo</b
+            >用于存放所有的事件选项,以<b>对象</b>形式传入,<b>对象的键为该选项所对应的事件的actionId</b>,
+            同样,新增的选项不需要传入id字段,如果不传入id字段则<b>默认为新增</b>
+          </p>
+          <pre>
+                    <code>
+{
+    "allEventTable": [
+      // 更新
+        {
+            "id": 1,
+            "gid": "1200",
+            "actionId": "123",
+            "actionName": "第二关过关",
+            "status": 0,
+            "remark": "t"
+        },
+      // 新增
+        {
+            "gid": "1200",
+            "actionId": "123",
+            "actionName": "第二关过关",
+            "status": 0,
+            "remark": "t"
+        }
+    ],
+    // 选项的键需要对应对应事件的actionId
+    "allOptionsInfo": {
+      // 更新  
+        "button": [
+            {
+                "id": 1,
+                "optionId": "test",
+                "optionName": "test",
+                "optionType": "string",
+                "status": 0,
+            }
+        ],
+        // 新增
+        "123":[
+            {
+                "optionId": "ba",
+                "optionName": "bgbb",
+                "optionType": "int",
+                "status": 0
+            }
+        ]
+    }
+}
+
+                    </code>
+                </pre>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<style scoped>
+.openTip {
+  color: #409eff;
+  cursor: pointer;
+}
+</style>

+ 229 - 0
src/components/form/Form.vue

@@ -0,0 +1,229 @@
+<script setup lang="ts">
+import type { FormInstance } from 'element-plus'
+import type { FormConfig } from '@/types/form'
+
+import { FormFieldType } from '@/types/form'
+import { reactive, ref } from 'vue'
+import { useForm } from '@/hooks/useForm'
+import CryptoJS from 'crypto-js'
+
+const { submitForm } = useForm()
+interface FormProp {
+  inline?: boolean
+  disabled?: boolean
+  config: FormConfig
+}
+
+// 表单ref
+const formRef = ref<FormInstance>()
+
+// props
+const props = withDefaults(defineProps<FormProp>(), {
+  inline: false,
+  disabled: false
+})
+
+// 表单的数据
+const formData = reactive<Record<string, any>>({})
+
+// 备份的表单数据
+const backupData = reactive<Record<string, any>>({})
+
+/**
+ * @description: 表单数据提交
+ * @return {*}
+ */
+const submitFormData = (otherOption?: any) => {
+  return new Promise((reslove, reject) => {
+    Object.assign(formData, props.config.reqConfig.otherOptions.formData)
+    formRef.value?.validate(async (vaild: boolean) => {
+      if (vaild) {
+        submitForm(formRef.value, props.config.reqConfig.url, { ...formData, ...otherOption })
+          .then(() => {
+            emits('subForm')
+            reslove(true)
+          })
+          .catch((err) => {
+            reject(err)
+          })
+      } else {
+        console.log('验证不通过')
+      }
+    })
+  })
+}
+
+/**
+ * @description: 重置表单为空
+ * @return {*}
+ */
+const resetForm = () => {
+  formRef.value?.resetFields()
+}
+
+/**
+ * @description: 填充表单
+ * @param {*} row 传过来的数据
+ * @return {*}
+ */
+const fillForm = (row: any) => {
+  Object.assign(formData, row)
+}
+
+/**
+ * @description: 开启编辑的时候备份一下data
+ * @return {*}
+ */
+const backupFormData = () => {
+  Object.assign(backupData, formData)
+}
+
+/**
+ * @description: 恢复表单数据
+ * @return {*}
+ */
+const resumeFormData = () => {
+  Object.assign(formData, backupData)
+}
+
+/**
+ * @description: 清除表单的验证信息
+ * @return {*}
+ */
+const clearValid = () => {
+  formRef.value?.clearValidate()
+}
+
+/**
+ * @description: 用于加密数据
+ * 当使用表单的数据加密的时候,encryptMsg是表单的字段,会根据表单对应字段的数据进行加密
+ * 反之,则直接使用传入的信息进行加密
+ * @param {*} fields  加密的字段
+ * @param {*} useFormField  是否使用表单的数据生成加密消息
+ * @param {*} encryptMsg  加密的消息或者是需要加密的表单的字段名
+ * @return {*}
+ */
+const encryptData = (fields: string, useFormField: boolean, encryptMsg: Array<string>) => {
+  return new Promise((reslove, reject) => {
+    let message = ``
+    let fieldData = ``
+    encryptMsg.map((item) => {
+      message += useFormField ? formData[item] : item
+    })
+    fieldData = CryptoJS.HmacMD5(message, formData[fields]).toString()
+    submitFormData({ [fields]: fieldData })
+      .then(() => {
+        reslove(true)
+      })
+      .catch((err) => {
+        reject(err)
+      })
+  })
+}
+
+/**
+ * @description: 获取表单的数据
+ * @return {*}
+ */
+const getFormData = () => {
+  return JSON.parse(JSON.stringify(formData))
+}
+
+const emits = defineEmits(['subForm'])
+
+defineExpose({
+  submitFormData,
+  resetForm,
+  fillForm,
+  encryptData,
+  backupFormData,
+  resumeFormData,
+  clearValid,
+  getFormData
+})
+</script>
+
+<template>
+  <div class="formBox">
+    <!-- :inline="props.inline" -->
+    <el-form
+      :class="props.inline ? 'formInline' : 'form'"
+      :label-position="props.inline ? 'top' : 'left'"
+      :rules="props.config.rules"
+      :model="formData"
+      ref="formRef"
+      :label-width="'auto'"
+      :disabled="!props.disabled"
+    >
+      <template v-for="item in props.config.fieldsInfo">
+        <el-form-item
+          :prop="item.name"
+          :label="item.cnName"
+          :class="['formItem', item.type === FormFieldType.RICHTEXT ? 'richtextItem' : '']"
+        >
+          <el-input
+            style="width: 300px"
+            v-if="item.type === FormFieldType.INPUT"
+            v-model="formData[item.name]"
+            autocomplete="off"
+            :id="item.name"
+          />
+          <el-select
+            style="width: 300px"
+            v-if="item.type === FormFieldType.SELECT && item.otherOptions?.options"
+            v-model="formData[item.name]"
+            :placeholder="item.otherOptions.placeholder"
+            size="default"
+            :id="item.name"
+          >
+            <el-option
+              v-for="val in item.otherOptions.options"
+              :key="val.name"
+              :label="val.label"
+              :value="val.value"
+            />
+          </el-select>
+          <el-input
+            :id="item.name"
+            v-if="item.type === FormFieldType.RICHTEXT"
+            v-model="formData[item.name]"
+            style="width: 300px; margin-bottom: 20px"
+            :autosize="{ minRows: 4 }"
+            type="textarea"
+            :placeholder="item.otherOptions?.placeholder"
+          />
+        </el-form-item>
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<style scoped>
+.formBox {
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.formInline {
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+
+.formItem {
+  /* width: 50%; */
+  /* display: flex; */
+  justify-content: center;
+}
+
+.richtextItem {
+  flex-basis: 100%;
+}
+</style>

+ 3 - 2
src/hooks/useAnalysis.ts

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:15:49
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-28 10:55:54
- * @FilePath: \Game-Backstage-Management-System\src\hooks\useTable.ts
+ * @LastEditTime: 2024-09-11 15:27:44
+ * @FilePath: \Game-Backstage-Management-System\src\hooks\useAnalysis.ts
  * @Description:
  *
  */
@@ -19,6 +19,7 @@ export function useAnalysis() {
       config.otherOptions[k] = newVal[k]
     })
   }
+
   return {
     updateReqConfig
   }

+ 13 - 1
src/hooks/useDialog.ts

@@ -2,11 +2,12 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-21 17:23:32
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-02 16:22:53
+ * @LastEditTime: 2024-09-04 15:46:03
  * @FilePath: \Game-Backstage-Management-System\src\hooks\useDialog.ts
  * @Description:
  *
  */
+import Form from '@/components/form/Form.vue'
 import type { DialogSetting } from '@/types/table'
 import type { FormInstance } from 'element-plus'
 import { useRequest } from './useRequest'
@@ -27,6 +28,16 @@ export function useDialog() {
     formEl.resetFields()
   }
 
+  const dialogClose2 = (
+    formEl: InstanceType<typeof Form> | undefined,
+    dialogConfig: DialogSetting
+  ) => {
+    if (!formEl) return
+
+    dialogConfig.dialogVisible = false
+    formEl.resetForm()
+  }
+
   // 对话框提交
   const submitDialog = (
     formEl: FormInstance | undefined,
@@ -107,6 +118,7 @@ export function useDialog() {
 
   return {
     dialogClose,
+    dialogClose2,
     submitDialog,
     handleEdit,
     addNeweItem

+ 82 - 0
src/hooks/useForm.ts

@@ -0,0 +1,82 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-09-04 15:07:56
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-09 12:10:43
+ * @FilePath: \Game-Backstage-Management-System\src\hooks\useForm.ts
+ * @Description:
+ *
+ */
+import type { FormInstance } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { nextTick } from 'vue'
+import axiosInstance from '@/utils/axios/axiosInstance'
+import { useRequest } from './useRequest'
+
+const { analysisResCode } = useRequest()
+export function useForm() {
+  // 对话框提交
+  const submitForm = (formEl: FormInstance | undefined, url: string, formData: any) => {
+    return new Promise((reslove, reject) => {
+      ElMessageBox.confirm('确认提交吗?', '警告', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        type: 'warning',
+        appendTo: 'body'
+      })
+        .then(() => {
+          try {
+            if (!formEl) {
+              reject(new Error('no formEl'))
+              return
+            }
+            formEl.validate(async (valid, field) => {
+              if (valid) {
+                let result = await axiosInstance.post(url, formData)
+                let info = JSON.parse(JSON.stringify(result))
+
+                analysisResCode(info)
+                  .then(() => {
+                    reslove(true)
+                  })
+                  .catch((err) => {
+                    reject(err)
+                    console.log(err)
+                  })
+              } else {
+                console.log(field)
+                console.log('表单校验不通过')
+              }
+            })
+          } catch (err) {
+            console.log(err)
+            ElMessage({
+              type: 'error',
+              message: '未知错误'
+            })
+            throw new Error('other err')
+          }
+        })
+        .catch(() => {
+          reject(false)
+        })
+    })
+  }
+
+  // 修改按钮
+  const editForm = (row: any, formData: any) => {
+    // 这里放到nextTick,因为resetFields原理是将表单重置到dom刚渲染时的数据
+    // 而这个表单,如果第一次使用就点击编辑的话,会将初始值直接设置为这行的数据,导致无法重置
+    nextTick(() => {
+      for (const key in row) {
+        if (key in formData) {
+          formData[key] = row[key]
+        } else {
+          console.log(key)
+        }
+      }
+    })
+  }
+
+  return { submitForm, editForm }
+}

+ 52 - 0
src/hooks/usePage.ts

@@ -0,0 +1,52 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-09-10 10:31:42
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-11 16:28:26
+ * @FilePath: \Game-Backstage-Management-System\src\hooks\usePage.ts
+ * @Description:
+ *
+ */
+import { onActivated, onDeactivated, watch } from 'vue'
+import { compareWatchData, saveWatchData } from '@/utils/common'
+
+export function usePage() {
+  const watchPageChange = (
+    watchData: () => Array<any>,
+    bakcupData: any,
+    cb: (...argus: any[]) => void
+  ) => {
+    let watchStop: (() => void) | null = null
+
+    onActivated(() => {
+      const currentData = watchData()
+
+      if (!compareWatchData(bakcupData, currentData)) {
+        cb(...currentData)
+      }
+
+      if (!watchStop) {
+        watchStop = watch(
+          () => watchData(),
+          (newData) => {
+            cb(...newData)
+          },
+          { deep: true }
+        )
+      }
+    })
+
+    onDeactivated(() => {
+      if (watchStop) {
+        const currentData = watchData()
+        saveWatchData(currentData, bakcupData)
+        watchStop()
+        watchStop = null
+      }
+    })
+  }
+
+  return {
+    watchPageChange
+  }
+}

+ 30 - 5
src/hooks/useRequest.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:24:06
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-03 15:24:24
+ * @LastEditTime: 2024-09-18 12:26:12
  * @FilePath: \Game-Backstage-Management-System\src\hooks\useRequest.ts
  * @Description:
  *
@@ -16,13 +16,20 @@ import type { AxiosResponse } from 'axios'
 import type { ResponseInfo } from '@/types/res'
 
 export function useRequest() {
-  // const baseIp = 'http://server.ichunhao.cn' // 线上
+  let baseIp = ''
+  // 根据环境不同,切换不同的IP
+  if (import.meta.env.MODE === 'development') {
+    // baseIp = 'http://service.ichunhao.cn' // 正式库
+    baseIp = 'http://server.ichunhao.cn' // 测试服
+  } else {
+    baseIp = 'http://service.ichunhao.cn' // 正式库
+  }
   // const baseIp = 'http://192.168.1.139:8000' // 本地
-  const baseIp = 'http://service.ichunhao.cn' // 正式服
 
   const AllApi = {
     // mock: `http://127.0.0.1:8003/mock`,
-    // mockKeep: `http://127.0.0.1:8003/mockKeep`,
+    mockEvent: `http://127.0.0.1:8003/mockEvent`,
+    mockDate: `http://127.0.0.1:8003/mockDate`,
 
     getGameTable: `${baseIp}/user/getGidConfig`, // 获取游戏列表
     getUserTable: `${baseIp}/user/userList`, // 获取用户列表
@@ -43,7 +50,25 @@ export function useRequest() {
     userTrendsOverview: `${baseIp}/user/userTrendsOverview`, //用户趋势 -总览
     userDataTrades: `${baseIp}/user/dataTrades`, //用户趋势 -数据趋势
     userDataTradesDetail: `${baseIp}/user/dataTradesDetail`, //用户趋势 -数据趋势详情
-    userRemainDataBydDay: `${baseIp}/user/remainDataBydDay` //用户留存数据
+    userRemainDataBydDay: `${baseIp}/user/remainDataBydDay`, //用户留存数据
+
+    // 事件相关
+    // 事件
+    gameActionList: `${baseIp}/user/gameActionList`, // 游戏事件列表
+    gameActionDetail: `${baseIp}/user/gameActionDetail`, // 事件详情
+    updateGameAction: `${baseIp}/user/updateGameAction`, // 更新游戏事件
+    setGameAction: `${baseIp}/user/setGameAction`, // 新增事件
+
+    // 事件参数
+    gameActionOptionList: `${baseIp}/user/gameActionOptionList`, // 获取事件参数列表
+    addGameActionOption: `${baseIp}/user/addGameActionOption`, // 新增事件参数
+    updateGameActionOption: `${baseIp}/user/updateGameActionOption`, // 更新事件参数
+    deleteGameActionOption: `${baseIp}/user/deleteGameActionOption`, // 删除事件参数
+
+    // 事件分析
+    userActionDetailDistribution: `${baseIp}/user/userActionDetailDistribution`, // 事件统计趋势图
+    userActionDetail: `${baseIp}/user/userActionDetail`, // 事件统计详情
+    userActionList: `${baseIp}/user/userActionList` // 游戏事件统计列表
   }
 
   const analysisResCode = (data: AxiosResponse, kind?: string): Promise<ResponseInfo> => {

+ 73 - 28
src/hooks/useTable.ts

@@ -2,53 +2,98 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:15:49
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-31 17:35:14
+ * @LastEditTime: 2024-09-18 10:09:12
  * @FilePath: \Game-Backstage-Management-System\src\hooks\useTable.ts
  * @Description:
  *
  */
+import type { ResponseInfo } from '@/types/res'
 import axiosInstance from '../utils/axios/axiosInstance'
-import { useRequest } from './useRequest'
+
+import { loadResource } from '@/utils/resource'
 
 import type { TablePaginationSetting, DialogSetting } from '@/types/table'
 import { type FormInstance } from 'element-plus'
 
 export function useTable(tableData: Array<any>, paginationSetting: TablePaginationSetting) {
-  const { analysisResCode } = useRequest()
+  // const { analysisResCode } = useRequest()
 
   const getTableData = (url: string, option: any, isPagination: boolean = false) => {
-    return new Promise(async (reslove) => {
+    return new Promise(async (reslove, reject) => {
       try {
-        await axiosInstance.post(url, option).then((data) => {
-          analysisResCode(data)
-            .then((info) => {
-              let data = info.data
-
-              // 如果开启了分页,那么默认这个tabledata是一个二维数组,每个位置对应当页的一个数据数组
-              // 没开启则是一个一维数组,直接赋值
-              if (isPagination) {
-                tableData[paginationSetting.currentPage] = data
-              } else {
-                tableData.splice(0, tableData.length, ...data)
-              }
+        await axiosInstance.post(url, option).then(async (result) => {
+          let info = JSON.parse(JSON.stringify(result)) as ResponseInfo
+          let data = info.data
 
-              // 如果有的接口没有返回count属性,就需要自己写
-              // 这个length,如果数组长为0,则需要自己赋值,不然会报错
-              if (info.count) paginationSetting.total = info.count
-              else if (info.data) {
-                paginationSetting.total = info.data.length
-              } else {
-                paginationSetting.total = 0
+          // 加载图片资源
+          // 如果有头像字段,就去加载这个头像字段,缓存起来
+          const loadImg = async () => {
+            return new Promise((resolve) => {
+              let resList: Array<Promise<any>> = []
+              if (data && data.length > 0) {
+                data.slice(0, data.length).map(async (item: any) => {
+                  if (item.head) {
+                    const loadHead = loadResource(item.head).then((res) => {
+                      item.head = res
+                    })
+                    resList.push(loadHead)
+                  }
+                })
               }
-              reslove(true)
-            })
-            .catch((err) => {
-              console.log(err)
-              throw new Error('请求失败')
+              Promise.allSettled(resList).then(() => {
+                resolve(true)
+              })
             })
+          }
+          await loadImg()
+
+          // 如果开启了分页,那么默认这个tabledata是一个二维数组,每个位置对应当页的一个数据数组
+          // 没开启则是一个一维数组,直接赋值
+          if (isPagination) {
+            tableData[paginationSetting.currentPage] = data
+          } else {
+            if (data && data.length > 0) tableData.splice(0, tableData.length, ...data)
+          }
+
+          // 如果有的接口没有返回count属性,就需要自己写
+          // 这个length,如果数组长为0,则需要自己赋值,不然会报错
+          if (info.count) paginationSetting.total = info.count
+          else if (info.data) {
+            paginationSetting.total = info.data.length
+          } else {
+            paginationSetting.total = 0
+          }
+          reslove(true)
+          // analysisResCode(data)
+          //   .then((info) => {
+          //     let data = info.data
+
+          //     // 如果开启了分页,那么默认这个tabledata是一个二维数组,每个位置对应当页的一个数据数组
+          //     // 没开启则是一个一维数组,直接赋值
+          //     if (isPagination) {
+          //       tableData[paginationSetting.currentPage] = data
+          //     } else {
+          //       tableData.splice(0, tableData.length, ...data)
+          //     }
+
+          //     // 如果有的接口没有返回count属性,就需要自己写
+          //     // 这个length,如果数组长为0,则需要自己赋值,不然会报错
+          //     if (info.count) paginationSetting.total = info.count
+          //     else if (info.data) {
+          //       paginationSetting.total = info.data.length
+          //     } else {
+          //       paginationSetting.total = 0
+          //     }
+          //     reslove(true)
+          //   })
+          //   .catch((err) => {
+          //     console.log(err)
+          //     throw new Error('请求失败')
+          //   })
         })
       } catch (err) {
         console.log(err)
+        reject(err)
         throw new Error('网络请求错误')
       }
     })

+ 68 - 0
src/router/appManage.ts

@@ -0,0 +1,68 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-20 14:24:58
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-12 17:45:31
+ * @FilePath: \Game-Backstage-Management-System\src\router\appManage.ts
+ * @Description:
+ *
+ */
+import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
+export default [
+  {
+    path: '/appManage',
+    redirect: '/appManage/gameBaseInfo',
+    name: 'AppManage',
+    children: [
+      {
+        path: 'gameBaseInfo',
+        name: 'GameBaseInfo',
+        icon: 'Memo',
+        cnName: '基本信息',
+        component: () => import('@/views/AppManage/BaseInfoView.vue'),
+        meta: {
+          activeMenu: 'gameBaseInfo',
+          needKeepAlive: true
+        }
+      },
+      {
+        path: 'eventManage',
+        name: 'EventManage',
+        icon: 'Management',
+        cnName: '事件管理',
+        showChild: false,
+        component: () => import('@/views/AppManage/EventManageView.vue'),
+        redirect: '/appManage/eventManage/eventTable',
+        meta: {
+          needKeepAlive: false,
+          activeMenu: 'eventManage'
+        },
+        children: [
+          {
+            path: 'eventDetail', // 注意:这里没有使用动态参数,而是使用查询参数
+            name: 'EventDetail',
+            component: () => import('@/views/AppManage/EventDetailsView.vue'),
+            beforeEnter: (
+              to: RouteLocationNormalized,
+              from: RouteLocationNormalized,
+              next: NavigationGuardNext
+            ) => {
+              // next()
+              if (!from.name || to.fullPath === '/appManage/eventManage/eventDetail') {
+                // 当页面刷新(即没有来源路由)时,重定向到 /eventManage/eventTable
+                next({ path: '/appManage/eventManage/eventTable' })
+              } else {
+                next()
+              }
+            }
+          },
+          {
+            path: 'eventTable',
+            name: 'EventTable',
+            component: () => import('@/views/AppManage/EventMangeTable.vue')
+          }
+        ]
+      }
+    ]
+  }
+]

+ 99 - 35
src/router/home.ts

@@ -2,60 +2,124 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:24:58
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-31 09:49:26
+ * @LastEditTime: 2024-09-12 17:47:02
  * @FilePath: \Game-Backstage-Management-System\src\router\home.ts
  * @Description:
  *
  */
-
+import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
 export default [
   {
     path: '/home',
-    component: () => import('@/views/Home/HomeView.vue'),
+    redirect: '/home/overView',
+    name: 'Home',
     children: [
       {
-        path: '',
-        redirect: 'overView'
-      },
-      {
         path: 'overView',
         name: 'OverView',
+        icon: 'PieChart',
+        cnName: '数据总览',
         component: () => import('@/views/Home/Overview/OverView.vue'),
         meta: {
+          activeMenu: 'overView',
           needKeepAlive: true
         }
       },
       {
-        path: 'playerManageView',
-        name: 'PlayerManageView',
-        component: () => import('@/views/Home/InfoManage/PlayerManageView.vue'),
-        meta: {
-          needKeepAlive: true
-        }
-      },
-      {
-        path: 'gameManageView',
-        name: 'GameManageView',
-        component: () => import('@/views/Home/InfoManage/GameManageView.vue'),
-        meta: {
-          needKeepAlive: true
-        }
-      },
-      {
-        path: 'keepView',
-        name: 'KeepView',
-        component: () => import('@/views/Home/Analysis/KeepView.vue'),
-        meta: {
-          needKeepAlive: true
-        }
+        path: 'infoManage',
+        cnName: '信息管理',
+        icon: 'Histogram',
+        showChild: true,
+        children: [
+          {
+            path: 'gameManageView',
+            name: 'GameManageView',
+            cnName: '游戏管理',
+            component: () => import('@/views/Home/InfoManage/GameManageView.vue'),
+            meta: {
+              activeMenu: 'gameManageView',
+              needKeepAlive: true
+            }
+          },
+          {
+            path: 'playerManageView',
+            name: 'PlayerManageView',
+            cnName: '玩家管理',
+            component: () => import('@/views/Home/InfoManage/PlayerManageView.vue'),
+            meta: {
+              activeMenu: 'playerManageView',
+              needKeepAlive: true
+            }
+          }
+        ]
       },
       {
-        path: 'userTrendView',
-        name: 'UserTrendView',
-        component: () => import('@/views/Home/Analysis/UserTrendView.vue'),
-        meta: {
-          needKeepAlive: true
-        }
+        path: 'dataAnalysis',
+        cnName: '数据分析',
+        icon: 'DataAnalysis',
+        showChild: true,
+        children: [
+          {
+            path: 'keepView',
+            name: 'KeepView',
+            cnName: '留存分析',
+            component: () => import('@/views/Home/Analysis/KeepView.vue'),
+            meta: {
+              activeMenu: 'keepView',
+              needKeepAlive: true
+            }
+          },
+          {
+            path: 'userTrendView',
+            name: 'UserTrendView',
+            cnName: '用户趋势',
+            component: () => import('@/views/Home/Analysis/UserTrendView.vue'),
+            meta: {
+              activeMenu: 'userTrendView',
+              needKeepAlive: true
+            }
+          },
+          {
+            path: 'eventAnalysisView',
+            name: 'EventAnalysisView',
+            cnName: '事件分析',
+            showChild: false,
+            redirect: '/home/dataAnalysis/eventAnalysisView/eventAnalysisTable',
+            component: () => import('@/views/Home/Analysis/EventAnalysisView.vue'),
+            meta: {
+              activeMenu: 'eventAnalysisView',
+              needKeepAlive: true
+            },
+            children: [
+              {
+                path: 'eventAnalysisTable',
+                name: 'EventAnalysisTable',
+                component: () => import('@/views/Home/Analysis/EventAnalysisTable.vue')
+              },
+              {
+                path: 'eventAnalysisDetail',
+                name: 'EventAnalysisDetail',
+                component: () => import('@/views/Home/Analysis/EventAnalysisDetail.vue'),
+                beforeEnter: (
+                  to: RouteLocationNormalized,
+                  from: RouteLocationNormalized,
+                  next: NavigationGuardNext
+                ) => {
+                  // next()
+                  if (
+                    !from.name ||
+                    to.fullPath === '/home/dataAnalysis/eventAnalysisView/eventAnalysisDetail'
+                  ) {
+                    // 当页面刷新(即没有来源路由)时,重定向到 /eventManage/eventTable
+                    next({ path: '/home/dataAnalysis/eventAnalysisView/eventAnalysisTable' })
+                  } else {
+                    next()
+                  }
+                }
+              }
+            ]
+          }
+        ]
       }
     ]
   }

+ 9 - 6
src/router/index.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:06:49
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-31 15:55:29
+ * @LastEditTime: 2024-09-06 16:59:23
  * @FilePath: \Game-Backstage-Management-System\src\router\index.ts
  * @Description:
  *
@@ -13,17 +13,20 @@ import { authToken } from '@/utils/axios/auth'
 
 import HomeRoutes from './home'
 import LoginRoutes from './login'
+import AppManage from './appManage'
 
 const routes = [
-  ...HomeRoutes,
   ...LoginRoutes,
   {
-    path: '/',
-    redirect: '/home/overView'
+    path: '/index',
+    name: 'Index',
+    redirect: '/index/home/overView',
+    component: () => import('@/views/Index.vue'),
+    children: [...HomeRoutes, ...AppManage]
   },
   {
-    path: '/:pathMach(.*)',
-    redirect: '/'
+    path: '/',
+    redirect: '/home/overView'
   }
 ]
 

+ 4 - 2
src/stores/useCommon.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-28 11:46:10
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-28 12:08:24
+ * @LastEditTime: 2024-09-13 15:45:29
  * @FilePath: \Game-Backstage-Management-System\src\stores\useCommon.ts
  * @Description:通用的store,在多个页面均会使用
  *
@@ -12,11 +12,13 @@ import { defineStore } from 'pinia'
 
 interface SelectInfo {
   gid: string
+  pf: Array<string>
 }
 
 export const useCommonStore = defineStore('commonStore', () => {
   const selectInfo = reactive<SelectInfo>({
-    gid: '1001'
+    gid: '1001',
+    pf: ['wx']
   })
   return { selectInfo }
 })

+ 26 - 14
src/types/dataAnalysis.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-23 14:58:29
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-02 15:23:17
+ * @LastEditTime: 2024-09-13 16:29:32
  * @FilePath: \Game-Backstage-Management-System\src\types\dataAnalysis.ts
  * @Description:用于dataAnalysis相关组件的type
  *
@@ -54,7 +54,7 @@ export interface StaticDataInfo {
 export interface TabInfo {
   name: string // tab的字段
   tabTitle: string // tab的标题
-  type: number // 到时候需要获取数据的url
+  type: number // 请求参数中,需要的字段,用来区分不同的请求
 }
 
 /**
@@ -77,36 +77,40 @@ export interface ResDataFieldInfo {
 // 表格字段
 // key需要与上方的resDataField的values字段对应
 export interface TrendTableField {
-  [key: string]: string
+  [key: string]: {
+    [key: string]: any
+  }
 }
 
 // 时间趋势组件所需要的props
 export interface TemporalTrendProps {
-  waitTimeSelect: boolean // 是否需要等待时间组件选择完成后请求
+  needDownload?: boolean // 是否需要下载
 
   tabInfo?: Array<TabInfo> // 用于切换的tab的信息
 
-  type: number // 趋势组件的类型,1为小时段,2为日期统计
-  needChangeExpress: boolean // 是否需要切换表格/图表
-  selectExpressForm: number // 默认的表现形式,1为图表,2为表格
+  type?: number // 趋势组件的类型,1为小时段,2为日期统计
+  needChangeExpress?: boolean // 是否需要切换表格/图表
+  selectExpressForm?: number // 默认的表现形式,1为图表,2为表格
 
-  needTable: boolean // 是否需要表格
-  tableFieldsInfo: TrendTableField // 表格的字段信息
+  needTable?: boolean // 是否需要表格
+  tableFieldsInfo: TrendTableField // 表格的字段信息,必填,因为legend的字段也依赖这里
 
-  needCharts: boolean // 是否需要图表
+  needCharts?: boolean // 是否需要图表
 
   resDataFieldsInfo: ResDataFieldInfo // 返回的数据中每个字段的分类信息
   requestConfig: ReqConfig // 请求参数配置,需要监听,一旦变化,需要重新获取数据
 
-  staticFields?: Array<StaticField> // 图表中统计组件的字段信息
+  staticFields?: { [key: string]: Array<StaticField> } // 图表中统计组件的字段信息
+  // staticFields?: Array<StaticField> // 图表中统计组件的字段信息
   title: string // 上方显示的title
 }
 
 // 头部切换组件所需要的props
 export interface HeaderCardProps {
   title: string // title信息
-  defaultPf?: string // 默认选择的pf
-  openDateSelect: boolean // 是否开启时间选择
+  openDateSelect?: boolean // 是否开启时间选择
+  needPfSelect?: boolean // 是否需要平台选择
+  needBreadcrumb?: boolean // 是否需要面包屑导航
 }
 
 // 趋势图组件需要的信息
@@ -115,5 +119,13 @@ export interface TemporalTrendInfo {
   resDataField: ResDataFieldInfo // 返回数据中的字段信息
   tabList?: Array<TabInfo> //表格数据
   trendTableFields: TrendTableField // 表格的字段,同时也作为legend的一部分
-  chartsStaticField?: Array<StaticField> // 统计字段的信息
+  // chartsStaticField?: Array<StaticField> // 统计字段的信息
+  chartsStaticField?: { [key: string]: Array<StaticField> } // 统计字段的信息
+}
+
+export interface IconDropdownItem {
+  value: string
+  icon: string
+  label: string
+  isSelected: boolean
 }

+ 19 - 0
src/types/dialog.ts

@@ -0,0 +1,19 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-09-04 14:02:44
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-04 14:12:51
+ * @FilePath: \Game-Backstage-Management-System\src\types\dialog.ts
+ * @Description:
+ *
+ */
+import type { FormRules } from 'element-plus'
+import type { ReqConfig } from './dataAnalysis'
+import type { FormField } from './form'
+
+export interface DialogConfig {
+  title: string
+  rules: FormRules
+  reqConfig: ReqConfig
+  fieldsInfo: Array<FormField>
+}

+ 23 - 0
src/types/form.ts

@@ -0,0 +1,23 @@
+import type { ReqConfig } from '@/types/dataAnalysis'
+import type { FormRules } from 'element-plus'
+export enum FormFieldType {
+  SELECT = 'select',
+  INPUT = 'input',
+  RICHTEXT = 'richText'
+}
+
+export interface FormField {
+  name: string
+  cnName: string
+  type: FormFieldType
+  otherOptions?: {
+    placeholder?: string
+    options?: Array<{ name: string; label: string; value: any }>
+  }
+}
+
+export interface FormConfig {
+  reqConfig: ReqConfig
+  fieldsInfo: Array<FormField>
+  rules: FormRules
+}

+ 17 - 3
src/types/table.ts

@@ -1,3 +1,12 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-20 17:56:13
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-11 17:02:07
+ * @FilePath: \Game-Backstage-Management-System\src\types\table.ts
+ * @Description:
+ *
+ */
 import type { ReqConfig } from './dataAnalysis'
 // 颜色类型
 export enum ColorType {
@@ -23,7 +32,6 @@ export interface TablePaginationSetting {
   currentPage: number // 当前页码
   total: number // 数据总数
   pagesizeList: Array<number> // 页数大小列表
-  hasLodingData: number // 已经加载的数据
 }
 
 // 表格信息过滤时,需要用到的过滤类型
@@ -48,7 +56,10 @@ export enum FieldSpecialEffectType {
   IMG = 'img',
   TAG = 'tag',
   TEXT = 'text',
-  TRANSLATE = 'translate'
+  TRANSLATE = 'translate',
+  SWITCH = 'switch',
+  DROPDOWN = 'dropdown',
+  STATE = 'state'
 }
 
 // 表格字段信息格式
@@ -56,9 +67,10 @@ export interface TableFieldInfo {
   name: string
   cnName: string
   isShow: boolean
+  needSort: boolean
   specialEffect?: {
     type: FieldSpecialEffectType
-    othnerInfo: any
+    othnerInfo?: any
   }
 }
 
@@ -68,6 +80,8 @@ export interface PropsParams {
   needRowindex?: boolean // 是否需要行号
   needAverage?: boolean // 是否需要均值功能
   needLeftTools?: boolean // 是否需要左侧的工具栏
+  needUpload?: boolean // 是否需要上传功能
+  needDownload?: boolean // 是否需要下载功能
   needRightTools?: boolean // 是否需要右侧工具栏
   openFilterQuery?: boolean // 是否开启上方查询功能
   openPageQuery?: boolean // 是否开启分页查询

+ 2 - 3
src/utils/axios/axiosInstance.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:18:52
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-31 12:55:24
+ * @LastEditTime: 2024-09-09 10:58:26
  * @FilePath: \Game-Backstage-Management-System\src\utils\axios\axiosInstance.ts
  * @Description:
  *
@@ -15,7 +15,6 @@ import { useRequest } from '@/hooks/useRequest'
 import { MessageType } from '@/types/res'
 
 const { AllApi } = useRequest()
-// import qs from "qs";
 // 创建axios实例
 const axiosInstance = axios.create()
 // 请求拦截器
@@ -68,7 +67,7 @@ axiosInstance.interceptors.response.use(
       message: '服务器错误,请稍后再试',
       duration: 1500
     })
-    router.push('/login')
+    // router.push('/login')
     return Promise.reject(error)
   }
 )

+ 79 - 1
src/utils/common/index.ts

@@ -2,11 +2,14 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-26 15:46:42
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-08-30 15:45:41
+ * @LastEditTime: 2024-09-13 17:35:33
  * @FilePath: \Game-Backstage-Management-System\src\utils\common\index.ts
  * @Description:
  *
  */
+
+import { ElMessage } from 'element-plus'
+
 // 防抖
 export function debounceFunc<T extends (...args: any[]) => any>(
   func: T,
@@ -81,3 +84,78 @@ export function resetTimeToMidnight(dateTime: Date): string {
 export const getAssetsImageUrl = (url: string) => {
   return new URL(`../../assets/${url}`, import.meta.url).href
 }
+
+/**
+ * @description: 复制文字到剪贴板
+ * @param {string} text
+ * @return {*}
+ */
+export const copyText = (text: string) => {
+  return new Promise((reslove, reject) => {
+    navigator.clipboard
+      .writeText(text)
+      .then(() => {
+        ElMessage({
+          type: 'success',
+          message: '复制成功'
+        })
+        reslove(true)
+      })
+      .catch((err) => {
+        ElMessage({
+          type: 'error',
+          message: '复制失败'
+        })
+        reject(err)
+        throw new Error(err)
+      })
+  })
+}
+
+/**
+ * @description: 用于清除reactive对象的所有属性并保持响应式
+ * @param {Record} data 传入的对象
+ * @return {*}
+ */
+export const clearReactiveData = (data: Record<string, any>) => {
+  Object.keys(data).forEach((key) => {
+    delete data[key]
+  })
+}
+
+/**
+ * @description: 用于保存watch切换之前的数据
+ * @param {any} data  需要保存的数据
+ * @param {any} store 需要保存到的对象
+ * @return {*}
+ */
+export const saveWatchData = (data: any, store: any) => {
+  if (Array.isArray(data)) {
+    // 这里需要深拷贝,否则会导致引用问题
+    store.splice(0, store.length, ...JSON.parse(JSON.stringify(data)))
+  } else {
+    Object.assign(store, JSON.parse(JSON.stringify(data)))
+  }
+}
+
+/**
+ * @description: 用于比较watch切换之前的数据
+ * @param {any} data 需要比较的数据
+ * @param {any} store 需要比较到的对象
+ * @return {*}  boolean
+ */
+export const compareWatchData = (data: any, store: any): boolean => {
+  return JSON.stringify(data) === JSON.stringify(store)
+}
+
+/**
+ * @description:   模糊查询
+ * @param {string} pattern 需要匹配的模式,即正则表达式
+ * @param {string} text   需要被搜索的目标字符串
+ * @param {boolean} matchCase 是否区分大小写,默认为true,即不区分大小写
+ * @return {boolean} 返回匹配结果
+ */
+export const fuzzySearch = (pattern: string, text: string, matchCase: boolean = true): boolean => {
+  const regex = new RegExp(pattern, matchCase ? 'i' : '') // 'i' 标志表示忽略大小写
+  return regex.test(text)
+}

+ 13 - 2
src/utils/resource/index.ts

@@ -1,3 +1,12 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-31 14:51:20
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-18 09:24:17
+ * @FilePath: \Game-Backstage-Management-System\src\utils\resource\index.ts
+ * @Description:
+ *
+ */
 // 缓存对象
 const resourceCache: Record<string, string> = {}
 
@@ -9,8 +18,11 @@ const resourceCache: Record<string, string> = {}
  * @returns 返回blob对象的url
  */
 export const loadResource = (url: string): Promise<string> => {
+  // console.log(resourceCache, url)
+
   // 检查是否已经存储了这个资源,已经缓存了就直接返回
   if (resourceCache[url]) {
+    // alert(JSON.stringify(resourceCache[url]))
     return Promise.resolve(resourceCache[url])
   }
 
@@ -36,7 +48,7 @@ export const loadResource = (url: string): Promise<string> => {
 
 /**
  * @description: 初始化加载所有的资源
- * @param {Record} resourceObj  请求的url对象,键值都为string
+ * @param {Record} resourceObj  请求的url对象,键值都为string,key为资源的名称,值为资源的请求地址
  * @return {*} 返回一个包含了key和blobURL的对象,用来表示所有资源的请求情况
  */
 export const initLoadResouce = (
@@ -61,7 +73,6 @@ export const initLoadResouce = (
         resultObj[key] = value // 如果成功,存入结果对象中
       }
     })
-
     return resultObj // 返回所有成功的资源
   })
 }

+ 28 - 1
src/utils/table/table.ts

@@ -1,7 +1,6 @@
 import { useTableStore } from '@/stores/useTable'
 import { useRequest } from '@/hooks/useRequest'
 import axiosInstance from '../axios/axiosInstance'
-
 const { AllApi } = useRequest()
 
 // 拿到所有游戏的信息
@@ -35,3 +34,31 @@ export const getAllGameInfo = async () => {
     throw new Error('获取游戏列表失败')
   }
 }
+
+/**
+ * @description: 根据路由的name判断这个组件是否需要监听事件
+ * @param {string} routeName 路由name,类型是来自RouteRecordNameGeneric类型
+ * @return {*}
+ */
+export const shouldListenToEvent = (routeName: string | symbol | undefined, mathName: string) => {
+  return routeName === mathName
+}
+
+/**
+ * @description: 下载文件信息
+ * @param {string} fileName 指定文件名
+ * @param {any} info  需要下载的信息
+ * @return {*}
+ */
+export const downLoadData = (fileName: string, info: any) => {
+  let result = {
+    ...info
+  }
+  let blob = new Blob([JSON.stringify(result)], { type: 'application/json' })
+  let url = URL.createObjectURL(blob)
+  let a = document.createElement('a')
+  a.href = url
+  a.download = fileName
+  a.click()
+  URL.revokeObjectURL(url)
+}

+ 199 - 0
src/views/AppManage/BaseInfoView.vue

@@ -0,0 +1,199 @@
+<script setup lang="ts">
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+
+import { reactive, onMounted, ref, watch } from 'vue'
+import { initLoadResouce } from '@/utils/resource'
+import { copyText } from '@/utils/common'
+import axiosInstance from '@/utils/axios/axiosInstance'
+import { useRequest } from '@/hooks/useRequest'
+import { useCommonStore } from '@/stores/useCommon'
+
+const { AllApi } = useRequest()
+const { selectInfo } = useCommonStore()
+
+interface AppInfo {
+  gameName: string
+  gid: string
+  ttAppid: string // 抖音App ID
+  ttSecret: string // 抖音App Secret
+  wxAppid: string // 微信App ID
+  wxSecret: string // 微信App Secret
+  img: string
+}
+
+// 返回的APP信息
+const appInfo = reactive<AppInfo>({
+  gameName: '',
+  gid: '',
+  ttAppid: '',
+  ttSecret: '',
+  wxAppid: '',
+  wxSecret: '',
+  img: '/img/default/defaultGame.svg'
+})
+
+const fieldsInfo = {
+  gameName: '游戏名称',
+
+  ttAppid: '抖音App ID',
+  ttSecret: '抖音App Secret',
+  wxAppid: '微信App ID',
+  wxSecret: '微信App Secret',
+  gid: '游戏ID'
+}
+
+// 资源的加载路径
+const resourceInfo: Record<string, string> = {
+  defaultHead: `/img/default/defaultGame.svg`
+}
+
+// 返回的所有数据
+const allGameInfo = reactive<Array<any>>([])
+
+// 使用blob的资源路径信息
+const blobUrlInfo = reactive<Record<string, string>>({})
+
+const Gid = ref<Array<HTMLSpanElement> | null>(null)
+
+const copyGid = () => {
+  copyText(appInfo.gid).then(() => {
+    if (Gid.value) {
+      const range = document.createRange()
+      range.selectNodeContents(Gid.value[0])
+
+      const selection = window.getSelection()
+      if (selection) {
+        selection.removeAllRanges()
+        selection.addRange(range)
+      }
+    }
+  })
+}
+
+const changeGameInfo = () => {
+  let nowGame = allGameInfo.find((item: any) => {
+    return item.gid === selectInfo.gid
+  })
+  Object.assign(appInfo, nowGame)
+}
+
+const getGameInfo = () => {
+  axiosInstance
+    .post(AllApi.getGameTable, {
+      appSecret: '6YJSuc50uJ18zj45'
+    })
+    .then((data) => {
+      let info = data.data
+      Object.assign(allGameInfo, info)
+      changeGameInfo()
+    })
+}
+
+watch(
+  () => selectInfo.gid,
+  () => {
+    getGameInfo()
+  }
+)
+
+onMounted(() => {
+  // 去加载所有需要的资源
+  initLoadResouce(resourceInfo).then((data) => {
+    Object.assign(blobUrlInfo, data)
+  })
+
+  getGameInfo()
+})
+</script>
+
+<template>
+  <div class="baseInfo">
+    <HeaderCard :title="'基本信息'" :need-pf-select="false"></HeaderCard>
+    <div class="body">
+      <div class="info">
+        <div class="infoItem" v-for="[key, val] in Object.entries(fieldsInfo)">
+          <span class="itmeTitle">{{ val }}</span>
+          <span class="text" :ref="key">{{ appInfo[key as keyof AppInfo] }}</span>
+          <span class="copy" v-if="key === 'gid'" @click="copyGid">复制</span>
+        </div>
+      </div>
+      <div class="photo">
+        <el-image :src="appInfo.img">
+          <template #error>
+            <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>
+          </template>
+        </el-image>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.baseInfo {
+  width: 98%;
+  margin: 1% auto;
+
+  box-sizing: border-box;
+}
+
+.body {
+  box-sizing: border-box;
+  margin-top: 15px;
+  /* padding: 0 24px; */
+  display: flex;
+  align-items: center;
+  width: 100%;
+  position: relative;
+}
+
+.info {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 40px 40px 40px 144px;
+  background: #fff;
+  box-shadow:
+    0 4px 8px 0 rgba(0, 0, 0, 0.02),
+    0 1px 3px 0 rgba(0, 0, 0, 0.02);
+  border-radius: 4px;
+  position: relative;
+}
+
+.photo {
+  position: absolute;
+  left: 40px;
+  top: 40px;
+  border-radius: 16px;
+  width: 64px;
+  height: 64px;
+}
+
+.infoItem {
+  margin-bottom: 15px;
+}
+
+.itmeTitle {
+  display: inline-block;
+  width: 110px;
+  text-align: left;
+  padding-right: 0;
+  font-size: 14px;
+  color: #808695;
+  line-height: 2px;
+}
+
+.text {
+  font-weight: 600;
+  color: #1c2438;
+  font-size: 14px;
+  margin-left: 30px;
+  line-height: 20px;
+}
+
+.copy {
+  margin-left: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #2285f0;
+  cursor: pointer;
+}
+</style>

+ 572 - 0
src/views/AppManage/EventDetailsView.vue

@@ -0,0 +1,572 @@
+<script setup lang="ts">
+import Form from '@/components/form/Form.vue'
+import Table from '@/components/Table.vue'
+import Dialog from '@/components/common/Dialog.vue'
+import FileUpload from '@/components/form/FileUpload.vue'
+
+import type { ReqConfig } from '@/types/dataAnalysis'
+import { type FormField, FormFieldType } from '@/types/form'
+import { ElMessageBox, type FormRules } from 'element-plus'
+import type { FormConfig } from '@/types/form'
+import {
+  type TablePaginationSetting,
+  type TableFieldInfo,
+  FieldSpecialEffectType
+} from '@/types/table'
+import type { DialogConfig } from '@/types/dialog'
+import { watch, onUnmounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { useRequest } from '@/hooks/useRequest'
+import { useCommonStore } from '@/stores/useCommon'
+import axiosInstance from '@/utils/axios/axiosInstance'
+import router from '@/router'
+
+const { selectInfo } = useCommonStore()
+const { AllApi, analysisResCode } = useRequest()
+
+const emits = defineEmits(['tableDataLoaded', 'upload'])
+
+// 事件表单对象
+const eventFormRef = ref()
+
+// 参数表格ref对象
+const optionTableRef = ref()
+
+// 是否是新增触发
+const isAdd = ref(true)
+
+// 事件ID,这里的actionID,在获取事件详情的时候传入的字段是id字段,但是在获取
+// 参数列表的时候,传入的是actionid字段。。。。。。
+const actionId = ref()
+
+// 编辑状态
+const eventEditState = ref(false)
+
+// 属性对话框
+const attrDialog = ref()
+
+// 上传ref
+const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
+
+// 上传的信息
+const uploadInfo = reactive({
+  dialogTitle: '上传事件信息'
+})
+
+// 表单规则字段
+const ruleForm = reactive({
+  // actionId:"",
+  actionName: '',
+  status: 1,
+  remark: ''
+})
+
+// 表单规则
+const rules = reactive<FormRules<typeof ruleForm>>({
+  // actionId: [
+  //   { required: true, message: '事件ID是必填项', trigger: 'blur' },
+  //   { min: 1, max: 10, message: '事件ID长度必须在1到10之间', trigger: 'blur' }
+  // ],
+  actionName: [
+    { required: true, message: '事件名称是必填项', trigger: 'blur' },
+    { min: 5, max: 20, message: '事件名称长度必须在5到20之间', trigger: 'blur' }
+  ],
+  remark: [
+    { required: false, message: '备注是可选项', trigger: 'blur' },
+    { max: 100, message: '备注长度不能超过100个字符', trigger: 'blur' }
+  ],
+  status: [
+    { required: true, message: '是否启用是必选项', trigger: 'change' },
+    { type: 'number', message: '启用状态必须是数字', trigger: 'change' }
+  ]
+})
+
+// 表单字段信息
+const FormFields: Array<FormField> = [
+  // {
+  //   name: 'actionId',
+  //   cnName: '事件ID',
+  //   type: FormFieldType.INPUT
+  // },
+  {
+    name: 'actionName',
+    cnName: '事件名',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'status',
+    cnName: '使用状态',
+    type: FormFieldType.SELECT,
+    otherOptions: {
+      placeholder: '请选择状态',
+      options: [
+        {
+          name: 'disabled',
+          label: '禁用',
+          value: 0
+        },
+        {
+          name: 'abled',
+          label: '启用',
+          value: 1
+        }
+      ]
+    }
+  },
+  {
+    name: 'remark',
+    cnName: '备注',
+    type: FormFieldType.RICHTEXT,
+    otherOptions: {
+      placeholder: '请输入备注'
+    }
+  }
+]
+
+// 表单请求参数
+const formReq = reactive<ReqConfig>({
+  url: AllApi.updateGameAction,
+  otherOptions: {}
+})
+
+// 表单设置
+const formConfig = reactive<FormConfig>({
+  reqConfig: formReq,
+  fieldsInfo: FormFields,
+  rules
+})
+
+// 表格分页设置
+const pageConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldConfig = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'id',
+    cnName: 'ID',
+    isShow: true,
+    needSort: true
+  },
+  {
+    name: 'actionId',
+    cnName: '事件ID',
+    isShow: false,
+    needSort: false
+  },
+
+  {
+    name: 'optionId',
+    cnName: '选项ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'optionName',
+    cnName: '选项名称',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'optionType',
+    cnName: '选项类型',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.TRANSLATE,
+      othnerInfo: {
+        translateText: {
+          string: '字符串',
+          int: '数字',
+          array: '数组'
+        }
+      }
+    }
+  },
+  {
+    name: 'status',
+    cnName: '使用状态状态',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      othnerInfo: {
+        text: ['启用', '禁用']
+      },
+      type: FieldSpecialEffectType.STATE
+    }
+  },
+  {
+    name: 'createdAt',
+    cnName: '创建时间',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'updatedAt',
+    cnName: '更新时间',
+    isShow: true,
+    needSort: false
+  }
+])
+
+// 表格请求配置
+const tableReqConfig = reactive<ReqConfig>({
+  url: AllApi.gameActionOptionList,
+  otherOptions: {}
+})
+
+// 对话框请求参数
+const dialogReqConfig = reactive<ReqConfig>({
+  url: AllApi.addGameActionOption,
+  otherOptions: {
+    formData: {}
+  }
+})
+
+// 对话框表单规则字段
+const dilogRuleForm = reactive({
+  optionName: '',
+  optionId: '',
+  status: 1,
+  optionType: ''
+})
+
+// 表单规则
+const dialogRules = reactive<FormRules<typeof dilogRuleForm>>({
+  optionName: [{ required: true, message: '选项名称是必填项', trigger: 'change', type: 'string' }],
+  optionId: [{ required: true, message: '选项ID是必填项', trigger: 'change', type: 'string' }],
+  status: [{ required: true, message: '选项状态是必填项', trigger: 'change', type: 'number' }],
+  optionType: [
+    {
+      required: true,
+      message: '选项类型是必填项',
+      trigger: 'change'
+    }
+  ]
+})
+
+// 表单字段信息
+const dialogFormFields: Array<FormField> = [
+  {
+    name: 'optionName',
+    cnName: '选项名称',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'optionId',
+    cnName: '选项ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'status',
+    cnName: '选项状态',
+    type: FormFieldType.SELECT,
+    otherOptions: {
+      placeholder: '请选择状态',
+      options: [
+        {
+          name: 'disabled',
+          label: '禁用',
+          value: 0 // 对应int型状态
+        },
+        {
+          name: 'abled',
+          label: '启用',
+          value: 1 // 对应int型状态
+        }
+      ]
+    }
+  },
+  {
+    name: 'optionType',
+    cnName: '选项类型',
+    type: FormFieldType.SELECT,
+    otherOptions: {
+      placeholder: '请选择类型',
+      options: [
+        {
+          name: 'string',
+          label: '字符串',
+          value: 'string'
+        },
+        {
+          name: 'int',
+          label: '整数',
+          value: 'int'
+        },
+        {
+          name: 'array',
+          label: '数组',
+          value: 'array'
+        }
+      ]
+    }
+  }
+]
+
+// 对话框设置
+const dialogConfig = reactive<DialogConfig>({
+  title: '自定义属性',
+  reqConfig: dialogReqConfig,
+  rules: dialogRules,
+  fieldsInfo: dialogFormFields
+})
+
+/**
+ * @description: 获取事件详情
+ * @param {*} id 这个id还是路由参数中query传过来的id
+ * @return {*}
+ */
+const getEventInfo = (id: number) => {
+  axiosInstance
+    .post(AllApi.gameActionDetail, { id })
+    .then((data) => {
+      analysisResCode(data).then((info) => {
+        let result = info.data
+        let updateEventField = {
+          gid: '',
+          actionId: '',
+          actionName: '',
+          remark: '',
+          status: 1
+        }
+        eventFormRef.value.fillForm(Object.assign(updateEventField, { ...result }))
+      })
+    })
+    .catch((err) => {
+      console.log(err)
+    })
+}
+
+// 新增属性
+const addNewAttr = () => {
+  attrDialog.value.addForm()
+}
+
+// 改变编辑状态
+const changeEditState = (state: boolean) => {
+  eventEditState.value = state
+}
+
+/**
+ * @description: 取消编辑,同时把数据恢复
+ * @return {*}
+ */
+const cancelEdit = () => {
+  changeEditState(false)
+
+  eventFormRef.value.clearValid()
+  eventFormRef.value.resumeFormData()
+}
+
+/**
+ * @description: 保存编辑,提交表单,只有表单验证通过了才能更改编辑状态
+ * @return {*}
+ */
+const saveEdit = () => {
+  eventFormRef.value.submitFormData().then(() => {
+    changeEditState(false)
+  })
+}
+
+/**
+ * @description: 开始编辑,备份一份现有的表单数据
+ * @return {*}
+ */
+const startEdit = () => {
+  changeEditState(true)
+  eventFormRef.value.backupFormData()
+}
+
+/**
+ * @description: 接受到表单提交事件后,需要去重新请求一下表格的数据
+ * @return {*}
+ */
+const formSub = () => {
+  optionTableRef.value.getData()
+}
+
+/**
+ * @description: 修改选项
+ * @return {*}
+ */
+const editOption = (row: any) => {
+  attrDialog.value.editForm(row, AllApi.updateGameActionOption)
+}
+
+/**
+ * @description: 删除选项
+ * @return {*}
+ */
+const delOption = (row: any) => {
+  ElMessageBox.confirm('确认删除该配置吗', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(() => {
+      optionTableRef.value.deleteRow(AllApi.deleteGameActionOption, { id: row.id })
+    })
+    .catch(() => {})
+}
+
+/**
+ * @description: 初始化一些参数
+ *  加载时如果有actionid,则代表是修改页面,需要去请求数据,没有则是新增页面
+ * 这里的actionid,是返回参数中id、actionID中的前者,在这个页面会被用于请求事件详情和相关的参数列表
+ * 在请求的时候,他们的名字不同,但传入的都是这个id
+ * @return {*}
+ */
+const initParams = () => {
+  const routes = useRoute()
+  let query_actionId = routes.query.id as string
+  if (query_actionId) {
+    isAdd.value = false
+    actionId.value = parseInt(query_actionId)
+    getEventInfo(parseInt(actionId.value))
+    tableReqConfig.otherOptions.actionId = actionId.value
+    dialogReqConfig.otherOptions.formData.actionId = actionId.value
+  }
+}
+
+// 初始化参数
+initParams()
+
+const watchGid = watch(
+  () => selectInfo.gid,
+  () => {
+    router.replace({ name: 'EventManage' })
+  }
+)
+
+onUnmounted(() => {
+  watchGid()
+})
+</script>
+
+<template>
+  <div class="eventDetail">
+    <div class="header">
+      <span>基本信息</span>
+      <div class="headerBtn">
+        <span v-if="eventEditState">
+          <el-button class="handleBtn" @click="cancelEdit" color="#626aef" plain>取消</el-button>
+          <el-button class="handleBtn" @click="saveEdit" color="#626aef">保存</el-button>
+        </span>
+        <span v-else>
+          <el-button class="handleBtn" @click="startEdit" color="#626aef">编辑</el-button>
+        </span>
+      </div>
+    </div>
+  </div>
+  <div class="eventForm">
+    <div class="formBody">
+      <Form
+        :disabled="eventEditState"
+        ref="eventFormRef"
+        :inline="true"
+        :config="formConfig"
+      ></Form>
+    </div>
+  </div>
+  <div class="attrList">
+    <div class="header">
+      <span>属性列表</span>
+    </div>
+    <div class="list">
+      <Table
+        :need-right-tools="true"
+        :need-left-tools="true"
+        :open-page-query="true"
+        :pagination-config="pageConfig"
+        :table-fields-info="tableFieldConfig"
+        :request-config="tableReqConfig"
+        :need-download="true"
+        @add-new-item="addNewAttr"
+        ref="optionTableRef"
+      >
+        <template #tableOperation>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-button
+                size="small"
+                color="#626aef"
+                @click="editOption(scope.row)"
+                class="operationBtn"
+              >
+                修改
+              </el-button>
+
+              <el-button
+                size="small"
+                color="#EF5A6F"
+                @click="delOption(scope.row)"
+                class="operationBtn"
+              >
+                删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </template>
+      </Table>
+    </div>
+  </div>
+  <div class="addAttrDialo">
+    <Dialog ref="attrDialog" :config="dialogConfig" @form-submit="formSub"></Dialog>
+  </div>
+  <div class="uploadFileBox">
+    <FileUpload ref="uploadRef" :title="uploadInfo.dialogTitle"></FileUpload>
+  </div>
+</template>
+
+<style scoped>
+.eventDetail,
+.eventForm,
+.attrList {
+  width: 98%;
+  margin: 0 auto;
+  background-color: white;
+  box-sizing: border-box;
+}
+
+.header {
+  width: 100%;
+  box-sizing: border-box;
+  background-color: white;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 56px;
+  box-shadow: inset 0 -1px 0 0 rgba(23, 35, 61, 0.1);
+  font-weight: 600;
+  font-size: 14px;
+  color: #17233d;
+  padding: 12px 24px;
+}
+
+.headerBtn {
+  box-sizing: border-box;
+}
+
+.handleBtn {
+  margin-right: 15px;
+}
+
+.eventForm {
+  padding: 24px;
+}
+
+.formBody {
+  width: 45%;
+}
+
+.operationBtn {
+  cursor: pointer;
+  margin-right: 5px;
+  color: white;
+}
+</style>

+ 402 - 0
src/views/AppManage/EventManageView.vue

@@ -0,0 +1,402 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-09-02 17:57:15
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-18 12:00:30
+ * @FilePath: \Game-Backstage-Management-System\src\views\AppManage\EventManageView.vue
+ * @Description: 
+ * 
+-->
+<script setup lang="ts">
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+import { shouldListenToEvent } from '@/utils/table/table'
+import { ref, reactive, computed } from 'vue'
+
+// import { ElMessage, ElNotification } from 'element-plus'
+
+import FileUpload from '@/components/form/FileUpload.vue'
+import axiosInstance from '@/utils/axios/axiosInstance'
+import { useRequest } from '@/hooks/useRequest'
+import { useCommonStore } from '@/stores/useCommon'
+import { downLoadData } from '@/utils/table/table'
+
+import { resetTimeToMidnight } from '@/utils/common'
+import router from '@/router'
+
+const { selectInfo } = useCommonStore()
+
+const { AllApi } = useRequest()
+
+// 事件表格ref
+const eventTableRef = ref()
+
+// 头部ref
+const headerCard = ref<typeof HeaderCard>()
+
+// 上传ref
+const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
+
+// 上传的信息
+const uploadInfo = reactive({
+  dialogTitle: '上传事件信息'
+})
+
+// 现在的路由路径名称
+const nowRouteName = computed(() => router.currentRoute.value.name)
+
+/**
+ * @description: 进入详情页,触发headercard的添加事件,增加一个面包屑导航
+ * @param {*} info 传入的信息
+ * @return {*}
+ */
+const headerAddPath = (info: any) => {
+  const { name, pathName } = info
+  headerCard.value?.addPath(name, pathName)
+}
+
+/**
+ * @description:  提交所有新上传的事件及选项请求
+ * @param {*} reqList 请求列表
+ * @param {*} msg 提示信息,用于展示上传之后返回的消息
+ * @return {*}
+ */
+const submitUpload = async (reqList: Array<Promise<boolean>>, msg?: string) => {
+  await Promise.allSettled(reqList).then((res) => {
+    if (
+      res.every((item) => {
+        return item.status === 'fulfilled' && item.value === true
+      })
+    ) {
+      ElNotification({
+        type: 'success',
+        title: '上传完成',
+        message: `${msg}上传成功`,
+        position: 'top-left'
+      })
+    } else {
+      ElNotification({
+        type: 'error',
+        title: '上传完成',
+        message: `${msg}部分上传失败,请检查参数`,
+        duration: 3000,
+        position: 'top-left'
+      })
+      // ElMessage.error()
+    }
+  })
+}
+
+/**
+ * @description: 请求表格数据,如果一次请求没有拿到所有的数据,则再请求一次
+ * @param {*} url 请求地址
+ * @param {*} otherInfo 请求参数
+ * @param {*} offset 偏移量
+ * @param {*} limit 请求数量
+ * @return {*}
+ */
+const getTableData = async (
+  url: string,
+  otherInfo: any,
+  offset = 0,
+  limit = 10000
+): Promise<Array<any>> => {
+  let total = limit + offset // 目前请求到的总数
+  let finalResult = []
+  finalResult = await axiosInstance
+    .post(url, {
+      offset,
+      limit,
+      ...otherInfo
+    })
+    .then(async (res) => {
+      let resData = JSON.parse(JSON.stringify(res))
+      let result = []
+      if (!resData.data) return []
+      // nowOptionList.push(...resData.data)
+      if (resData.count > total) {
+        result = await getTableData(url, otherInfo, total, limit)
+      }
+      return [...resData.data, ...result]
+    })
+    .catch((err) => {
+      console.log(err)
+      return []
+    })
+  return finalResult
+}
+
+/**
+ * @description:  批量请求选项数据
+ * @param {*} url 请求的地址
+ * @param {*} reqParams 请求的参数列表,主要是actionid
+ * @param {*} eventTable 事件列表
+ * @return {Promise<Object>}  返回一个最终的结果,其中key是每个事件的actionid,值是选项列表
+ */
+const batReqOptionsData = async (
+  url: string,
+  reqParams: Array<any>,
+  eventTable: Array<any>
+): Promise<Object> => {
+  let reqList: Array<Promise<any>> = []
+  let finalResult: {
+    [key: string]: Array<any>
+  } = {}
+
+  reqParams.map((item) => {
+    reqList.push(
+      getTableData(url, item)
+        .then((res) => {
+          let actionId = eventTable.find((i) => i.id === item.actionId).actionId
+          finalResult[actionId] = res
+        })
+        .catch((err) => {
+          console.log(err)
+        })
+    )
+  })
+  await Promise.all(reqList)
+  return finalResult
+}
+
+type GetTableReturn<T> = T extends 'all'
+  ? { allEventTable: Array<any>; allOptionsInfo: any }
+  : T extends 'event'
+    ? { allEventTable: Array<any> }
+    : T extends 'option'
+      ? { allOptionsInfo: any }
+      : never
+
+/**
+ * @description: 拿到事件数据和选项数据,首先需要拿到所有的事件列表,然后将他们的actionId抽出来形成一个列表,这个列表去作为optin的查询参数批量查询
+ * @return {*} 返回事件数据和选项数据
+ */
+const getAllTable = async <T extends 'all' | 'event' | 'option' = 'all'>(
+  table?: T
+): Promise<GetTableReturn<T>> => {
+  let allEventTable: Array<any> = [],
+    allOptionsInfo: any = null
+  if (table === 'event') {
+    allEventTable = await getTableData(AllApi.gameActionList, { gid: selectInfo.gid })
+    return { allEventTable } as GetTableReturn<T>
+  }
+
+  if (table === 'option') {
+    let optionReqList = allEventTable.map((item) => {
+      return { actionId: parseInt(item.id) }
+    })
+    allOptionsInfo = await batReqOptionsData(
+      AllApi.gameActionOptionList,
+      optionReqList,
+      allEventTable
+    )
+    return { allOptionsInfo } as GetTableReturn<T>
+  }
+
+  return { allEventTable, allOptionsInfo } as GetTableReturn<T>
+}
+
+/**
+ * @description: 开始上传
+ * @return {*}
+ */
+const startUpload = async () => {
+  if (uploadRef.value) {
+    uploadRef.value.startUpload()
+  }
+}
+
+/**
+ * @description: 获取事件列表和选项列表的数据,开始下载
+ * @return {*}
+ */
+const startDownload = async () => {
+  let { allEventTable, allOptionsInfo } = await getAllTable()
+  downLoadData(`allevents_${resetTimeToMidnight(new Date())}`, {
+    allEventTable,
+    allOptionsInfo
+  })
+}
+
+// 上传选项的时候,选项的键要设置为actionid
+// 需要先上传事件,然后上传完了,用上传的选项中的第一个(如果有)的actionid去找到对应的事件的ID(不是actionid),然后作为选项上传的id
+/**
+ * @description: 当文件添加后,开始进行上传前的处理
+ *  首先需要上传事件,如果现有上传的列表中的actionid已经包含在了现有列表中,那么判定为更新,否则为新增
+ *  上传完成后,需要首先获取一次新的数据,然后再开始上传选项。
+ *  分别在上传和已有的选项列表中,找到对应的acionID的那一组选项,对这一组数据,如果已经存在对应的id,那么就是更新,否则为新增
+ *  然后上传,上传完毕后,整体列表刷新,关闭弹框
+ * @param {*} data 上传的文件数据,里面包含allEventTable:所有的事件列表,allOptionsInfo:所有选项列表
+ * @return {*}
+ */
+const uploadSuccess = async (data: any) => {
+  let uploadEventTable = data.allEventTable
+  let uploadOptionsInfo = data.allOptionsInfo
+  let allEventTable: Array<any> = [],
+    allOptionsInfo: { [key: string]: any } = {}
+  // 上传的事件列表有值,则开始上传
+  ;({ allEventTable } = await getAllTable('event')) // 获取所有事件列表和选项列表
+
+  // 开始上传事件
+  let eventReqList: Array<Promise<boolean>> = []
+  let eventReqUrl = AllApi.setGameAction
+  if (Array.isArray(uploadEventTable) && uploadEventTable.length) {
+    // 将新事件和旧事件区分,对于新事件走新增,对于旧事件走更新
+    uploadEventTable.map((item) => {
+      let { id, createdAt, updatedAt, ...otherInfo } = item
+      if (allEventTable.some((i) => i.actionId === item.actionId)) {
+        eventReqUrl = AllApi.updateGameAction
+      } else {
+        eventReqUrl = AllApi.setGameAction
+      }
+      let eventReq = axiosInstance
+        .post(eventReqUrl, otherInfo)
+        .then((res: any) => {
+          if (res.code === 0) return true
+          return false
+        })
+        .catch((err) => {
+          console.log(err)
+          return false
+        })
+      eventReqList.push(eventReq) // 统一放入请求列表中
+    })
+    await submitUpload(eventReqList, '事件') // 等待所有的事件请求完成
+  }
+  ;({ allEventTable, allOptionsInfo } = await getAllTable()) // 重新获取所有事件列表和选项列表
+  let optionsReqList: Array<Promise<boolean>> = []
+
+  allEventTable.map((item) => {
+    // 在上传的事件列表中,找到有对应的事件的actionid的那一组数据
+    let uploadOptionItem = uploadOptionsInfo[item.actionId] as Array<any>
+    // 在现有的事件列表中,找到对应事件的actionid的那一组数据
+    let nowOptionItem = allOptionsInfo[item.actionId] as Array<any> // 找到所有在已有事件列表中的选项列表
+
+    // 如果有已存在的事件,并且上传的选项列表中也有对应的actionid,则开始上传
+    if (uploadOptionItem) {
+      // 对找到的那一组数据进行循环,区分出来哪些是已有的,哪些是新上传的
+      // 新上传的需要给他加上actionid,然后上传,这个actionid其实是事件列表的id字段
+      uploadOptionItem.map((i) => {
+        let optionReqUrl = AllApi.addGameActionOption // 选项上传的url
+        let { id, actionId, createdAt, updatedAt, ...otherInfo } = i // 上传参数拆分出来
+        let reqParams = {}
+        // 分出来哪些是新增加的选项,哪些是需要更新的选项
+        // 新增的选项需要加入的是事件列表的主键,而更新中需要加入的是option的id
+        if (nowOptionItem.some((k) => k.id === i.id)) {
+          reqParams = { id, ...otherInfo }
+
+          optionReqUrl = AllApi.updateGameActionOption
+        } else {
+          actionId = item.id
+          reqParams = { actionId, ...otherInfo }
+          optionReqUrl = AllApi.addGameActionOption
+        }
+        let optionReq = axiosInstance
+          .post(optionReqUrl, reqParams)
+          .then((res: any) => {
+            if (res.code === 0) return true
+            return false
+          })
+          .catch((err) => {
+            console.log(err)
+            return false
+          })
+        optionsReqList.push(optionReq)
+      })
+    }
+  })
+  await submitUpload(optionsReqList, '选项') // 等待所有选项上传完成
+  uploadRef.value?.uploadCallback()
+  eventTableRef.value?.updateData()
+}
+</script>
+
+<template>
+  <div class="enventManage">
+    <div class="breadcrumbBox">
+      <HeaderCard
+        ref="headerCard"
+        :need-breadcrumb="true"
+        :title="'事件管理'"
+        :need-pf-select="false"
+      ></HeaderCard>
+    </div>
+
+    <div class="handleEvent" v-if="nowRouteName === 'EventTable'">
+      <div class="fileGroup">
+        <el-button class="fileBtn" color="#626aef" @click="startUpload">上传</el-button>
+        <el-button class="fileBtn" @click="startDownload">下载</el-button>
+      </div>
+    </div>
+    <div class="eventTableBox">
+      <!-- 监听表格的跳转事件 -->
+      <router-view v-slot="{ Component, route }">
+        <!-- 是eventtable组件就去监听enterdetail事件 -->
+        <!-- 注释也不要写到keep-alive里面,会报错 -->
+
+        <component
+          :is="Component"
+          ref="eventTableRef"
+          v-if="shouldListenToEvent(route.name, 'EventTable')"
+          @enterDetail="headerAddPath"
+        />
+
+        <!-- 如果不是正常渲染其他组件 -->
+        <component v-if="route.name !== 'EventTable'" :is="Component" />
+      </router-view>
+    </div>
+    <div class="uploadFileBox">
+      <FileUpload
+        @upload-success="uploadSuccess"
+        ref="uploadRef"
+        :title="uploadInfo.dialogTitle"
+      ></FileUpload>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.enventManage {
+  width: 98%;
+  margin: 1% auto;
+  background-color: white;
+  box-sizing: border-box;
+  position: relative;
+}
+
+.breadcrumbBox {
+  background-color: white;
+  box-sizing: border-box;
+  height: 64px;
+  font-size: 16px;
+  color: #17233d;
+  font-weight: 600;
+  /* padding: 0 24px; */
+  line-height: 64px;
+}
+
+.eventTableBox {
+  box-sizing: border-box;
+  padding: 0px 24px;
+}
+
+.handleEvent {
+  position: absolute;
+  /* width: 12%; */
+  background-color: white;
+  box-sizing: border-box;
+  /* height: 48px; */
+  font-size: 16px;
+  font-weight: 600;
+  top: 20px;
+  right: 24px;
+  /* position: relative; */
+  /* justify-content: flex-end; */
+}
+
+.fileGroup {
+  width: 100%;
+}
+.fileBtn {
+  margin-right: 10px;
+}
+</style>

+ 321 - 0
src/views/AppManage/EventMangeTable.vue

@@ -0,0 +1,321 @@
+<script setup lang="ts">
+import Table from '@/components/Table.vue'
+import Dialog from '@/components/common/Dialog.vue'
+import { useRequest } from '@/hooks/useRequest'
+import { useCommonStore } from '@/stores/useCommon'
+
+import type { TablePaginationSetting, TableFieldInfo, QueryInfo, SelectInfo } from '@/types/table'
+import { FieldSpecialEffectType, FilterType } from '@/types/table'
+import type { ReqConfig } from '@/types/dataAnalysis'
+import type { FormRules } from 'element-plus'
+import { type DialogConfig } from '@/types/dialog'
+import { FormFieldType } from '@/types/form'
+import type { FormField } from '@/types/form'
+
+import { reactive, ref, watch } from 'vue'
+
+import router from '@/router'
+
+const { selectInfo } = useCommonStore()
+
+const { AllApi } = useRequest()
+
+const eventDialog = ref()
+
+const eventTable = ref<InstanceType<typeof Table> | null>(null)
+
+// 主要为了给面包屑导航提供信息
+const emits = defineEmits(['enterDetail', 'upload'])
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'id',
+    cnName: 'ID',
+    isShow: true,
+    needSort: true
+  },
+  {
+    name: 'gid',
+    cnName: '游戏ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'actionId',
+    cnName: '事件ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'actionName',
+    cnName: '事件名称',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'status',
+    cnName: '事件状态',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.STATE,
+      othnerInfo: {
+        text: ['已使用', '已停用']
+      }
+    }
+  },
+  {
+    name: 'remark',
+    cnName: '备注',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'createdAt',
+    cnName: '创建时间',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'updatedAt',
+    cnName: '更新时间',
+    isShow: true,
+    needSort: false
+  }
+])
+
+// 表格请求配置
+const requestConfig = reactive<ReqConfig>({
+  url: AllApi.gameActionList,
+  otherOptions: {
+    gid: selectInfo.gid
+  }
+})
+
+// 事件类型查询信息
+const eventStatus: Array<SelectInfo> = [
+  {
+    name: 'all',
+    cnName: '全部',
+    value: ''
+  },
+  {
+    name: 'use',
+    cnName: '启用',
+    value: '1'
+  },
+  {
+    name: 'nouse',
+    cnName: '禁用',
+    value: '0'
+  }
+]
+
+// 查询字段设置
+const queryInfo: Array<QueryInfo> = [
+  {
+    name: 'search',
+    label: '',
+    type: FilterType.INPUT,
+    placeholder: '请输入事件名搜索',
+    default: ''
+  },
+  {
+    name: 'status',
+    label: '事件状态',
+    type: FilterType.SELECT,
+    placeholder: '请选择事件类型',
+    otherOption: eventStatus,
+    default: ''
+  }
+]
+
+// 对话框表单规则字段
+const dilogRuleForm = reactive({
+  actionId: '',
+  actionName: '',
+  remark: '',
+  status: ''
+})
+
+// 对话表单规则
+const dialogRules = reactive<FormRules<typeof dilogRuleForm>>({
+  actionId: [
+    { required: true, message: '事件ID是必填项', trigger: 'blur' },
+    { min: 1, max: 10, message: '事件ID长度必须在1到10之间', trigger: 'blur' }
+  ],
+  actionName: [
+    { required: true, message: '事件名称是必填项', trigger: 'blur' },
+    { min: 5, max: 20, message: '事件名称长度必须在5到20之间', trigger: 'blur' }
+  ],
+  remark: [
+    { required: false, message: '备注是可选项', trigger: 'blur' },
+    { max: 100, message: '备注长度不能超过100个字符', trigger: 'blur' }
+  ],
+  status: [
+    { required: true, message: '是否启用是必选项', trigger: 'change' },
+    { type: 'number', message: '启用状态必须是数字', trigger: 'change' }
+  ]
+})
+
+// 对话框请求参数
+const dialogReq = reactive<ReqConfig>({
+  url: AllApi.setGameAction,
+  otherOptions: {
+    formData: {
+      gid: selectInfo.gid
+    }
+  }
+})
+
+// 对话框表单字段信息
+const FormFields: Array<FormField> = [
+  {
+    name: 'actionId',
+    cnName: '事件ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'actionName',
+    cnName: '事件名称',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'remark',
+    cnName: '备注',
+    type: FormFieldType.RICHTEXT
+  },
+  {
+    name: 'status',
+    cnName: '是否启用',
+    type: FormFieldType.SELECT,
+    otherOptions: {
+      placeholder: '请选择启用状态',
+      options: [
+        {
+          name: 'use',
+          label: '启用',
+          value: 1
+        },
+        {
+          name: 'nouse',
+          label: '禁用',
+          value: 0
+        }
+      ]
+    }
+  }
+]
+
+// 对话框需要的props
+const dialogInfo = reactive<DialogConfig>({
+  title: '新增事件',
+  rules: dialogRules,
+  reqConfig: dialogReq,
+  fieldsInfo: FormFields
+})
+
+/**
+ * @description: 查看详情,同时触发事件给面包屑导航
+ * @param {*} row 行信息
+ * @return {*}
+ */
+const viewDetails = (row: any) => {
+  if (row.id) {
+    emits('enterDetail', {
+      name: row.actionName,
+      pathName: 'EventDetail'
+    })
+
+    router.push({
+      name: 'EventDetail',
+      query: { id: row.id }
+    })
+  } else {
+    throw new Error('no id')
+  }
+}
+
+/**
+ * @description: 触发新增
+ * @return {*}
+ */
+const addNewEvent = () => {
+  eventDialog.value.addForm()
+}
+
+/**
+ * @description: 表单提交
+ * @return {*}
+ */
+const subForm = () => {
+  if (eventTable.value) {
+    eventTable.value.getData()
+  }
+}
+
+/**
+ * @description: 更新表格数据
+ * @return {*}
+ */
+const updateData = () => {
+  eventTable.value?.getData()
+}
+
+watch(
+  () => selectInfo.gid,
+  (val: string) => {
+    requestConfig.otherOptions.gid = val
+  }
+)
+
+defineExpose({
+  updateData
+})
+</script>
+
+<template>
+  <div>
+    <Table
+      ref="eventTable"
+      :need-rowindex="false"
+      :request-config="requestConfig"
+      :open-page-query="true"
+      :pagination-config="pagingConfig"
+      :table-fields-info="tableFieldsInfo"
+      :query-info="queryInfo"
+      :need-left-tools="true"
+      :open-filter-query="true"
+      :need-right-tools="true"
+      @add-new-item="addNewEvent"
+    >
+      <template #tableOperation>
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-text class="operationBtn" type="primary" @click="viewDetails(scope.row)"
+              >详情</el-text
+            >
+          </template>
+        </el-table-column>
+      </template>
+    </Table>
+    <div class="eventDialog">
+      <Dialog ref="eventDialog" @form-submit="subForm" :config="dialogInfo"></Dialog>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.operationBtn {
+  cursor: pointer;
+}
+</style>

+ 226 - 0
src/views/Home/Analysis/EventAnalysisDetail.vue

@@ -0,0 +1,226 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-27 17:11:23
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-13 14:41:37
+ * @FilePath: \Game-Backstage-Management-System\src\views\Home\Analysis\EventAnalysisDetail.vue
+ * @Description: 
+ * 
+-->
+<script setup lang="ts">
+import Table from '@/components/Table.vue'
+import TemporalTrend from '@/components/dataAnalysis/TemporalTrend.vue'
+import { resetTimeToMidnight } from '@/utils/common'
+
+import { onMounted, reactive, ref, watch } from 'vue'
+import { useRequest } from '@/hooks/useRequest'
+import { useRoute } from 'vue-router'
+import { useAnalysis } from '@/hooks/useAnalysis'
+
+import type {
+  ReqConfig,
+  ResDataFieldInfo,
+  TabInfo,
+  TemporalTrendProps,
+  TrendTableField
+} from '@/types/dataAnalysis'
+import { type TablePaginationSetting, type TableFieldInfo } from '@/types/table'
+
+const { updateReqConfig } = useAnalysis()
+
+interface eventDetailProps {
+  startTime: string
+  endTime: string
+}
+
+const props = withDefaults(defineProps<eventDetailProps>(), {
+  startTime: resetTimeToMidnight(new Date()),
+  endTime: resetTimeToMidnight(new Date())
+})
+
+const { AllApi } = useRequest()
+
+const eventId = ref(-1)
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'date',
+    cnName: '日期',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeCount',
+    cnName: '触发次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeUserCount',
+    cnName: '活跃用户数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeCountRate',
+    cnName: '活跃用户发生率',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeCountUser',
+    cnName: '每次启动发生数',
+    isShow: true,
+    needSort: false
+  }
+])
+
+// 表格请求配置
+const tableRequestConfig = reactive<ReqConfig>({
+  url: AllApi.userActionDetail,
+  otherOptions: {
+    id: eventId,
+    startTime: props.startTime,
+    endTime: props.endTime
+  }
+})
+
+// 图表信息
+// 返回数据中字段分类
+const resDataField: ResDataFieldInfo = {
+  xAxis: 'list', // X轴字段为日期
+  values: ['list'] // Y轴字段为事件数
+}
+
+// 图表中表格的字段信息
+const chartTableField: TrendTableField = {
+  eventCount: {
+    index: '日期',
+    list: '事件数'
+  },
+  activeEventCount: {
+    index: '日期',
+    list: '事件达成设备数'
+  }
+}
+
+// 请求参数配置
+const chartReqConfig = reactive<ReqConfig>({
+  url: AllApi.userActionDetailDistribution,
+  otherOptions: {
+    id: eventId,
+    startTime: resetTimeToMidnight(new Date()),
+    endTime: resetTimeToMidnight(new Date())
+  }
+})
+
+// 图表用于切换的tab
+const tabInfo: Array<TabInfo> = [
+  {
+    name: 'eventCount',
+    tabTitle: '事件数',
+    type: 1
+  },
+  {
+    name: 'activeEventCount',
+    tabTitle: '事件达成设备数',
+    type: 2
+  }
+]
+
+const chartProps = reactive<TemporalTrendProps>({
+  title: '事件趋势',
+  type: 2,
+  tableFieldsInfo: chartTableField,
+  requestConfig: chartReqConfig,
+  resDataFieldsInfo: resDataField,
+  tabInfo
+})
+
+const initParams = () => {
+  const routes = useRoute()
+  let query_actionId = routes.query.id as string
+
+  if (query_actionId) {
+    eventId.value = parseInt(query_actionId)
+  }
+}
+
+/**
+ * @description: 监听时间变化,去重新请求数据
+ * @return {*}
+ */
+watch(
+  () => [props.startTime, props.endTime, eventId],
+  ([newStart, newEnd, newId]) => {
+    updateReqConfig(chartReqConfig, {
+      id: newId,
+      startTime: newStart,
+      endTime: newEnd
+    })
+    updateReqConfig(tableRequestConfig, {
+      id: newId,
+      startTime: newStart,
+      endTime: newEnd
+    })
+    // eventTable.value.getData()
+  },
+  {
+    deep: true
+  }
+)
+initParams()
+onMounted(() => {})
+</script>
+<template>
+  <div class="eventDetail">
+    <div class="chart">
+      <TemporalTrend
+        :title="chartProps.title"
+        :type="chartProps.type"
+        :table-fields-info="chartProps.tableFieldsInfo"
+        :request-config="chartProps.requestConfig"
+        :res-data-fields-info="chartProps.resDataFieldsInfo"
+        :tab-info="chartProps.tabInfo"
+        :need-change-express="false"
+      ></TemporalTrend>
+    </div>
+    <div class="table">
+      <div class="boxHeader">
+        <span class="headerTitle">事件详情</span>
+      </div>
+      <el-divider />
+      <Table
+        :pagination-config="pagingConfig"
+        :table-fields-info="tableFieldsInfo"
+        :request-config="tableRequestConfig"
+      ></Table>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.eventDetail {
+  width: 100%;
+  background-color: white;
+}
+.table {
+  padding: 0 24px;
+  box-sizing: border-box;
+}
+
+.headerTitle {
+  font-size: 14px;
+  color: #1c2438;
+  font-weight: 600;
+}
+</style>

+ 194 - 0
src/views/Home/Analysis/EventAnalysisTable.vue

@@ -0,0 +1,194 @@
+<script setup lang="ts">
+import Table from '@/components/Table.vue'
+
+import { reactive, ref } from 'vue'
+import { useRequest } from '@/hooks/useRequest'
+import { useCommonStore } from '@/stores/useCommon'
+import { resetTimeToMidnight } from '@/utils/common'
+
+import type { ReqConfig } from '@/types/dataAnalysis'
+import {
+  type TablePaginationSetting,
+  type TableFieldInfo,
+  type QueryInfo,
+  FilterType
+} from '@/types/table'
+import router from '@/router'
+
+import { usePage } from '@/hooks/usePage'
+const { watchPageChange } = usePage()
+
+const { AllApi } = useRequest()
+const { selectInfo } = useCommonStore()
+
+const eventTable = ref<InstanceType<typeof Table>>()
+
+// 主要为了给面包屑导航提供信息
+const emits = defineEmits(['enterDetail'])
+
+interface eventTableProps {
+  startTime: string
+  endTime: string
+}
+
+const props = withDefaults(defineProps<eventTableProps>(), {
+  startTime: resetTimeToMidnight(new Date()),
+  endTime: resetTimeToMidnight(new Date())
+})
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'actionId',
+    cnName: '事件ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'actionName',
+    cnName: '事件名称',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'actionCount',
+    cnName: '操作次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'actionUserCount',
+    cnName: '操作用户数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeUserRate',
+    cnName: '活跃用户率',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'loginActiveRate',
+    cnName: '登录活跃率',
+    isShow: true,
+    needSort: false
+  }
+])
+
+// 表格请求配置
+const requestConfig = reactive<ReqConfig>({
+  url: AllApi.userActionList,
+  otherOptions: {
+    gid: selectInfo.gid,
+    startTime: props.startTime,
+    endTime: props.endTime
+  }
+})
+
+// 事件表格的上方查询字段信息
+const eventTableFilterInfo: Array<QueryInfo> = [
+  {
+    name: 'actionName',
+    label: '',
+    type: FilterType.INPUT,
+    placeholder: '输入事件名查询'
+  }
+]
+/**
+ * @description: 查看详情
+ * @param {*} row 行信息
+ * @return {*}
+ */
+const viewDetails = (row: any) => {
+  emits('enterDetail', {
+    name: row.actionName,
+    pathName: 'EventAnalysisDetail'
+  })
+  router.push({
+    name: 'EventAnalysisDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/**
+ * @description: 更新时间
+ * @param {*} startTime 开始时间
+ * @param {*} endTime 结束时间
+ * @return {*}
+ */
+const updateDate = (startTime: string, endTime: string) => {
+  requestConfig.otherOptions.startTime = startTime
+  requestConfig.otherOptions.endTime = endTime
+}
+
+/**
+ * @description: 更新Gid
+ * @param {*} newGid 新的gid
+ * @return {*}
+ */
+const updateGid = (newGid: string) => {
+  requestConfig.otherOptions.gid = newGid
+}
+
+const backupDate = reactive([])
+const backupSelect = reactive([])
+
+watchPageChange(() => [selectInfo.gid], backupSelect, updateGid)
+
+watchPageChange(() => [props.startTime, props.endTime], backupDate, updateDate)
+</script>
+<template>
+  <div class="eventTable">
+    <div class="content">
+      <Table
+        ref="eventTable"
+        :need-rowindex="false"
+        :request-config="requestConfig"
+        :open-page-query="false"
+        :open-filter-query="true"
+        :query-info="eventTableFilterInfo"
+        :pagination-config="pagingConfig"
+        :table-fields-info="tableFieldsInfo"
+        :need-left-tools="false"
+      >
+        <template #tableOperation>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-text class="operationBtn" type="primary" @click="viewDetails(scope.row)"
+                >详情</el-text
+              >
+            </template>
+          </el-table-column>
+        </template>
+      </Table>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.eventTable {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 0 24px;
+  background-color: white;
+}
+
+.content {
+  width: 100%;
+}
+
+.operationBtn {
+  cursor: pointer;
+}
+</style>

+ 93 - 0
src/views/Home/Analysis/EventAnalysisView.vue

@@ -0,0 +1,93 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-27 17:11:23
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-13 16:05:45
+ * @FilePath: \Game-Backstage-Management-System\src\views\Home\Analysis\EventAnalysisView.vue
+ * @Description: 
+ * 
+-->
+<script setup lang="ts">
+import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
+import { resetTimeToMidnight } from '@/utils/common'
+import { shouldListenToEvent } from '@/utils/table/table'
+import { reactive, ref } from 'vue'
+
+import type { HeaderCardProps } from '@/types/dataAnalysis'
+
+// 顶部ref
+const headerCard = ref()
+
+// 头部组件需要的 props
+const headerProps = reactive<HeaderCardProps>({
+  title: '事件分析',
+  openDateSelect: true
+})
+
+// 开始时间
+const startTime = ref()
+// 结束时间
+const endTime = ref()
+
+/**
+ * @description: 进入详情页,触发headercard的添加事件,增加一个面包屑导航
+ * @param {*} info 传入的信息
+ * @return {*}
+ */
+const headerAddPath = (info: any) => {
+  const { name, pathName } = info
+
+  headerCard.value?.addPath(name, pathName)
+}
+
+/**
+ * @description: 时间改变
+ * @param {*} newDate
+ * @return {*}
+ */
+const dateChange = (newDate: Array<Date>) => {
+  startTime.value = resetTimeToMidnight(newDate[0])
+  endTime.value = resetTimeToMidnight(newDate[1])
+}
+</script>
+<template>
+  <div class="eventAnalysis">
+    <div class="header">
+      <HeaderCard
+        ref="headerCard"
+        :title="headerProps.title"
+        :open-date-select="headerProps.openDateSelect"
+        :need-breadcrumb="true"
+        :need-pf-select="false"
+        @change-date="dateChange"
+      ></HeaderCard>
+    </div>
+
+    <div class="content">
+      <!-- 监听表格的跳转事件 -->
+      <router-view v-slot="{ Component, route }" :startTime="startTime" :endTime="endTime">
+        <!-- 是eventtable组件就去监听enterdetail事件 -->
+        <keep-alive>
+          <component
+            :is="Component"
+            v-if="shouldListenToEvent(route.name, 'EventAnalysisTable')"
+            @enterDetail="headerAddPath"
+          />
+        </keep-alive>
+        <!-- 如果不是正常渲染其他组件 -->
+        <component v-if="route.name !== 'EventAnalysisTable'" :is="Component" />
+      </router-view>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.eventAnalysis {
+  width: 98%;
+  margin: 1% auto;
+  box-sizing: border-box;
+
+  background-color: white;
+  /* border: 1px solid #e5e6eb; */
+}
+</style>

+ 85 - 40
src/views/Home/Analysis/KeepView.vue

@@ -2,13 +2,13 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-27 17:11:23
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-03 15:26:47
+ * @LastEditTime: 2024-09-18 12:25:50
  * @FilePath: \Game-Backstage-Management-System\src\views\Home\Analysis\KeepView.vue
  * @Description: 
  * 
 -->
 <script setup lang="ts">
-import { watch, reactive, ref, toRaw } from 'vue'
+import { reactive, ref, toRaw } from 'vue'
 import Table from '@/components/Table.vue'
 import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
 import axiosInstance from '@/utils/axios/axiosInstance'
@@ -20,6 +20,9 @@ import { useRequest } from '@/hooks/useRequest'
 import { useCommonStore } from '@/stores/useCommon'
 import { useAnalysis } from '@/hooks/useAnalysis'
 
+import { usePage } from '@/hooks/usePage'
+const { watchPageChange } = usePage()
+
 const { updateReqConfig } = useAnalysis()
 const { selectInfo } = useCommonStore()
 
@@ -27,7 +30,7 @@ const { AllApi, analysisResCode } = useRequest()
 
 // 选择的信息
 const keepViewSelect = reactive({
-  pf: 'web'
+  pf: selectInfo.pf[0]
 })
 
 const loading = ref(true) // 加载状态
@@ -50,66 +53,77 @@ const keepDataTableInfo = reactive<{
     otherOptions: {
       pf: keepViewSelect.pf,
       gid: selectInfo.gid,
-      type: 1,
       startTime: resetTimeToMidnight(new Date()),
-      endTime: resetTimeToMidnight(new Date())
+      endTime: resetTimeToMidnight(new Date()),
+      type: 1
     }
   },
   tableFieldsInfo: toRaw([
     {
       name: 'date',
       cnName: '日期',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'count',
       cnName: '用户数',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+1day',
       cnName: '+1日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+2day',
       cnName: '+2日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+3day',
       cnName: '+3日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+4day',
       cnName: '+4日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+5day',
       cnName: '+5日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+6day',
       cnName: '+6日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+7day',
       cnName: '+7日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+14day',
       cnName: '+14日',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: '+30day',
       cnName: '+30日',
-      isShow: true
+      isShow: true,
+      needSort: false
     }
   ])
 })
@@ -118,11 +132,11 @@ const keepTableData = reactive<Array<any>>([])
 
 /**
  * @description: 选择的平台改变
- * @param {*} pf  平台名
+ * @param {*} pf  选择的平台数组,暂时只用第一个
  * @return {*}
  */
-const changePf = (pf: string) => {
-  keepViewSelect.pf = pf
+const changePf = (pf: Array<string>) => {
+  keepViewSelect.pf = pf[0]
 }
 
 /**
@@ -172,31 +186,63 @@ const getTableData = () => {
           loading.value = false
         })
     })
+    .catch((err) => {
+      loading.value = false
+      throw new Error(err)
+    })
 }
 
 /**
- * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
- * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+ * @description: 更新所有监听req的参数
+ * @param {*} pf  新pf
+ * @param {*} gid 新gid
  * @return {*}
  */
-watch(
-  () => [keepViewSelect.pf, selectInfo.gid],
-  ([newPf, newGid]) => {
-    updateReqConfig(keepDataTableInfo.requestConfig, { pf: newPf, gid: newGid })
-  }
-)
+const updateAllReq = (pf: string, gid: string) => {
+  updateReqConfig(keepDataTableInfo.requestConfig, { pf, gid })
+}
 
-/**
- * @description: 单独监听一下他的变化,去手动更新数据
- * @return {*}
- */
-watch(
-  () => keepDataTableInfo.requestConfig,
-  () => {
-    getTableData()
-  },
-  { deep: true }
-)
+const backupReq = reactive([])
+const backupSelect = reactive([])
+
+watchPageChange(() => [keepViewSelect.pf, selectInfo.gid], backupSelect, updateAllReq)
+watchPageChange(() => [keepDataTableInfo.requestConfig], backupReq, getTableData)
+
+// onActivated(() => {
+//   /**
+//    * @description: 单独监听一下他的变化,去手动更新数据
+//    * @return {*}
+//    */
+//   watchReq = watch(
+//     () => keepDataTableInfo.requestConfig,
+//     () => {
+//       getTableData()
+//     },
+//     { deep: true, immediate: true }
+//   )
+
+//   /**
+//    * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
+//    * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+//    * @return {*}
+//    */
+
+//   watchSelectInfo = watch(
+//     () => [keepViewSelect.pf, selectInfo.gid],
+//     ([newPf, newGid]) => {
+//       updateReqConfig(keepDataTableInfo.requestConfig, { pf: newPf, gid: newGid })
+//     },
+//     {
+//       deep: true,
+//       immediate: true
+//     }
+//   )
+// })
+
+// onDeactivated(() => {
+//   watchReq()
+//   watchSelectInfo()
+// })
 </script>
 <template>
   <div class="KeepViewBox">
@@ -204,7 +250,6 @@ watch(
       <HeaderCard
         :title="headerCardInfo.title"
         :open-date-select="headerCardInfo.openDateSelect"
-        :default-pf="keepViewSelect.pf"
         @change-pf="changePf"
         @change-date="changeDate"
       ></HeaderCard>
@@ -225,7 +270,7 @@ watch(
 .KeepViewBox {
   width: 98%;
   margin: 1% auto;
-  /* background-color: white; */
+  background-color: white;
   border: 1px solid #e5e6eb;
 }
 

+ 193 - 50
src/views/Home/Analysis/UserTrendView.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
 import Table from '@/components/Table.vue'
-import { onMounted, reactive, watch, ref, toRaw } from 'vue'
+import { reactive, ref, toRaw } from 'vue'
 import type { StaticField, ReqConfig, TemporalTrendInfo } from '@/types/dataAnalysis'
 import type { TablePaginationSetting, TableFieldInfo } from '@/types/table'
 
@@ -12,6 +12,8 @@ import { useRequest } from '@/hooks/useRequest'
 import { useAnalysis } from '@/hooks/useAnalysis'
 import { resetTimeToMidnight } from '@/utils/common'
 import axiosInstance from '@/utils/axios/axiosInstance'
+import { usePage } from '@/hooks/usePage'
+const { watchPageChange } = usePage()
 
 const { updateReqConfig } = useAnalysis()
 const { AllApi, analysisResCode } = useRequest()
@@ -23,7 +25,7 @@ const userTrendStaticRef = ref()
 // 目前选择的信息
 // 这里不太合理,应该根据返回的数据中的pf和game赋值,因为这个可能会变
 const userTrendSelectInfo = reactive({
-  pf: 'web'
+  pf: 'wx'
 })
 
 // 总览数据的字段对应的信息
@@ -109,24 +111,89 @@ const dataTrendInfo = reactive<TemporalTrendInfo>({
       type: 6
     }
   ]),
-  chartsStaticField: toRaw([
-    {
-      name: 'avg',
-      cnName: '均值',
-      value: ''
-    },
-    {
-      name: 'count',
-      cnName: '总数',
-      value: ''
-    }
-  ]),
+  chartsStaticField: toRaw({
+    newUser: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ],
+    dailyActiveUser: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ],
+    weeklyActiveUser: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      }
+    ],
+    detailDatalyActiveUser: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      }
+    ],
+    launchCount: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      }
+    ],
+    dailyUsageTime: [
+      {
+        name: 'avg',
+        cnName: '均值',
+        value: ''
+      }
+    ]
+  }),
   resDataField: toRaw({
     xAxis: 'imeDistribution', // x轴的刻度信息所在的字段
     values: ['imeDistribution'] // 值所在的字段
   }),
   trendTableFields: toRaw({
-    imeDistribution: '新增'
+    newUser: {
+      index: '日期',
+      imeDistribution: '新增用户'
+    },
+    dailyActiveUser: {
+      index: '日期',
+      imeDistribution: '日活跃用户'
+    },
+    weeklyActiveUser: {
+      index: '日期',
+      imeDistribution: '周活跃用户'
+    },
+    detailDatalyActiveUser: {
+      index: '日期',
+      imeDistribution: '月活跃用户'
+    },
+    launchCount: {
+      index: '日期',
+      imeDistribution: '启动次数'
+    },
+    dailyUsageTime: {
+      index: '日期',
+      imeDistribution: '单日使用时长'
+    }
   })
 })
 
@@ -156,37 +223,44 @@ const detailDataTableInfo = reactive<{
     {
       name: 'date',
       cnName: '日期',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'newUser',
       cnName: '新增用户',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'activeUser',
       cnName: '日活跃用户',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'activeUserWeek',
       cnName: '周活跃用户',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'activeUserMouth',
       cnName: '月活跃用户',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'activeStart',
       cnName: '启动次数',
-      isShow: true
+      isShow: true,
+      needSort: false
     },
     {
       name: 'avgTime',
       cnName: '平均使用时长',
-      isShow: true
+      isShow: true,
+      needSort: false
     }
   ])
 })
@@ -195,26 +269,12 @@ const detailDataTableInfo = reactive<{
 const detailDataTableData = reactive<Array<any>>([])
 
 /**
- * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
- * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
- * @return {*}
- */
-watch(
-  () => [userTrendSelectInfo.pf, selectInfo.gid],
-  ([newPf, newGid]) => {
-    updateReqConfig(userTrendDataReqConfig, { pf: newPf, gid: newGid })
-    updateReqConfig(dataTrendInfo.dataReqConfig, { pf: newPf, gid: newGid })
-    updateReqConfig(detailDataTableInfo.requestConfig, { pf: newPf, gid: newGid })
-  }
-)
-
-/**
  * @description: 选择的平台改变
- * @param {*} pf  平台名
+ * @param {*} pf  选择的平台数组,暂时只用第一个
  * @return {*}
  */
-const changePf = (pf: string) => {
-  userTrendSelectInfo.pf = pf
+const changePf = (pf: Array<string>) => {
+  userTrendSelectInfo.pf = pf[0]
 }
 
 /**
@@ -260,25 +320,109 @@ const getDetailData = () => {
           })
         }
         detailDataTableData.splice(0, detailDataTableData.length, ...newList)
-        console.log(newList.length)
         detailDataTableInfo.paginationConfig.total = newList.length
       })
     })
 }
 
 /**
- * @description: 单独监听一下他的变化,去手动更新数据
+ * @description: 更新所有监听req的参数
+ * @param {*} pf  新pf
+ * @param {*} gid 新gid
  * @return {*}
  */
-watch(
-  () => detailDataTableInfo.requestConfig,
-  () => {
-    getDetailData()
-  },
-  { deep: true }
-)
+const updateAllReq = (pf: string, gid: string) => {
+  updateReqConfig(userTrendDataReqConfig, { pf, gid })
+  updateReqConfig(dataTrendInfo.dataReqConfig, { pf, gid })
+  updateReqConfig(detailDataTableInfo.requestConfig, { pf, gid })
+}
+
+/**
+ * @description: 监听变化,去手动更新数据
+ * @return {*}
+ */
+
+const backupReq = reactive([]) // 保存请求参数
+
+const backupSelect = reactive([]) // 保存选择数据
+
+watchPageChange(() => [userTrendSelectInfo.pf, selectInfo.gid], backupSelect, updateAllReq)
+watchPageChange(() => [detailDataTableInfo.requestConfig], backupReq, getDetailData)
+
+// onActivated(() => {
+//   /**
+//    * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
+//    * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+//    * @return {*}
+//    */
+
+//   console.log('执行')
+//   if (!compareWatchData(backupSelect, { pf: userTrendSelectInfo.pf, gid: selectInfo.gid })) {
+//     console.log('compare')
+
+//     updateAllReq(userTrendSelectInfo.pf, selectInfo.gid)
+//   }
+
+//   if (!watchSelect) {
+//     watchSelect = watch(
+//       () => [userTrendSelectInfo.pf, selectInfo.gid],
+//       ([newPf, newGid]) => {
+//         console.log('change')
+//         updateAllReq(newPf, newGid)
+//       },
+//       { deep: true }
+//     )
+//   }
+
+//   if (!compareWatchData(backupReq, detailDataTableInfo.requestConfig)) {
+//     console.log('compare1')
+//     getDetailData()
+//   }
+
+//   if (!watchReq) {
+//     watchReq = watch(
+//       () => detailDataTableInfo.requestConfig,
+//       () => {
+//         if (compareWatchData(backupReq, detailDataTableInfo.requestConfig)) {
+//           console.log('change1')
+//           getDetailData()
+//         }
+//       },
+//       { deep: true }
+//     )
+//   }
+// })
+
+// onDeactivated(() => {
+//   if (watchReq) {
+//     saveWatchData(detailDataTableInfo.requestConfig, backupReq)
+//     watchReq()
+//     watchReq = null
+//   }
+
+//   if (watchSelect) {
+//     saveWatchData(
+//       {
+//         pf: userTrendSelectInfo.pf,
+//         gid: selectInfo.gid
+//       },
+//       backupSelect
+//     )
+//     watchSelect()
+//     watchSelect = null
+//   }
+// })
 
-onMounted(() => {})
+// watch(
+//   () => test.name,
+//   (newdata) => {
+//     console.log('本地')
+//     console.log(newdata)
+//   },
+//   {
+//     deep: true
+//   }
+// )
 </script>
 <template>
   <div class="userTrendBox">
@@ -300,7 +444,6 @@ onMounted(() => {})
     </div>
     <div class="dataTrendBox">
       <TemporalTrend
-        :wait-time-select="true"
         :need-charts="true"
         :table-fields-info="dataTrendInfo.trendTableFields"
         :res-data-fields-info="dataTrendInfo.resDataField"

+ 0 - 348
src/views/Home/HomeView.vue

@@ -1,348 +0,0 @@
-<script setup lang="ts">
-import { RouterView } from 'vue-router'
-import { onMounted, reactive, ref } from 'vue'
-import { ElMessage } from 'element-plus'
-import { getAllGameInfo } from '@/utils/table/table'
-import router from '@/router'
-import type { DropDownInfo } from '@/types/dataAnalysis'
-import DropDownSelection from '@/components/dataAnalysis/DropDownSelection.vue'
-import { useCommonStore } from '@/stores/useCommon'
-import { initLoadResouce } from '@/utils/resource'
-
-const { selectInfo } = useCommonStore()
-const isCollapse = ref(false)
-const menuList = [
-  {
-    title: '数据总览',
-    icon: 'PieChart',
-    children: [
-      {
-        pathName: 'OverView',
-        title: '工作台'
-      }
-    ]
-  },
-  {
-    title: '信息管理',
-    icon: 'Histogram',
-    children: [
-      {
-        pathName: 'GameManageView',
-        title: '游戏管理'
-      },
-      {
-        pathName: 'PlayerManageView',
-        title: '玩家管理'
-      }
-    ]
-  },
-  {
-    title: '数据分析',
-    icon: 'DataAnalysis',
-    children: [
-      {
-        pathName: 'KeepView',
-        title: '留存分析'
-      },
-      {
-        pathName: 'UserTrendView',
-        title: '用户趋势'
-      }
-    ]
-  }
-]
-
-const changeCollapse = () => {
-  isCollapse.value = !isCollapse.value
-}
-
-// 登出
-const logOut = () => {
-  ElMessage({
-    type: 'success',
-    message: '退出成功',
-    duration: 1000
-  })
-  localStorage.removeItem('token')
-  localStorage.removeItem('refreshToken')
-  router.push('/login')
-}
-
-// 游戏下拉选择框需要的数据
-const gameSelectInfo = reactive<DropDownInfo>({
-  defaultSelect: '1001',
-  title: '请选择游戏',
-  optionsList: []
-})
-
-const gameinfoLoad = ref(false)
-
-/**
- * @description: 更新整个页面的游戏选择
- * @param {*} gid 游戏id
- * @return {*}
- */
-const changeGame = (gid: any) => {
-  selectInfo.gid = gid
-}
-
-getAllGameInfo().then((data) => {
-  if (data) {
-    data.map((item) => {
-      gameSelectInfo.optionsList.push({
-        value: item.gid,
-        label: item.gameName
-      })
-    })
-  }
-  gameinfoLoad.value = true
-})
-
-// 资源的加载路径
-const resourceInfo: Record<string, string> = {
-  logo: `/img/logo.svg`,
-  defaultHead: `/img/default/defaultHead.png`
-}
-
-// 使用blob的资源路径信息
-const blobUrlInfo = reactive<Record<string, string>>({})
-
-onMounted(() => {
-  // 去加载所有需要的资源
-  initLoadResouce(resourceInfo).then((data) => {
-    Object.assign(blobUrlInfo, data)
-  })
-})
-</script>
-
-<template>
-  <div class="body">
-    <div class="sideBarBox">
-      <el-menu :router="true" :default-active="$route.name" class="sideBar" :collapse="isCollapse">
-        <el-menu-item index="/" class="logoBox">
-          <el-image :fit="'fill'" class="logoImg" :src="blobUrlInfo.logo"></el-image>
-          <template #title><span class="logoText">淳皓科技</span></template>
-        </el-menu-item>
-        <el-sub-menu v-for="item in menuList" :index="item.title">
-          <template #title>
-            <el-icon><component :is="item.icon" /></el-icon>
-            <span class="menuTitle">{{ item.title }}</span>
-          </template>
-          <el-menu-item v-for="v in item.children" :index="v.pathName">{{ v.title }}</el-menu-item>
-        </el-sub-menu>
-        <div class="sideBarFold" @click="changeCollapse">
-          <el-icon :size="25"><Fold /></el-icon>
-        </div>
-      </el-menu>
-    </div>
-    <div class="navBarBox">
-      <div class="gameSelect">
-        <el-icon class="gameIcon" :size="20">
-          <icon-icon-park-game-three></icon-icon-park-game-three>
-        </el-icon>
-        <DropDownSelection
-          :default-select="gameSelectInfo.defaultSelect"
-          :title="gameSelectInfo.title"
-          :options-list="gameSelectInfo.optionsList"
-          :size="'default'"
-          @change-select="changeGame"
-        ></DropDownSelection>
-      </div>
-      <div class="headPortraitBox">
-        <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">
-          <template #reference>
-            <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>
-          </template>
-          <div class="userTools">
-            <span class="userToolsItem" @click="logOut">
-              <icon-material-symbols-light-logout></icon-material-symbols-light-logout>
-              <span> 退出登录</span>
-            </span>
-          </div>
-        </el-popover>
-      </div>
-    </div>
-    <!-- <div class="content">
-      <RouterView v-if="gameinfoLoad" />
-    </div> -->
-    <div class="content">
-      <router-view v-slot="{ Component, route }">
-        <keep-alive>
-          <component
-            :is="Component"
-            :key="route.path"
-            v-if="route.meta.needKeepAlive == true"
-          ></component>
-        </keep-alive>
-        <component
-          :is="Component"
-          :key="route.path"
-          v-if="route.meta.needKeepAlive == false"
-        ></component>
-      </router-view>
-    </div>
-  </div>
-</template>
-
-<style scoped>
-.body {
-  width: 100%;
-  display: flex;
-  height: 100vh;
-}
-
-.gameSelect {
-  position: absolute;
-  top: 50%;
-  transform: translateY(-50%);
-  /* width: 10%; */
-  height: 80%;
-  display: flex;
-  align-items: center;
-  right: 10%;
-  display: flex;
-  align-items: center;
-
-  /* background-color: lightblue; */
-}
-.gameIcon {
-  /* box-sizing: border-box; */
-  /* padding-right: 12px; */
-  margin-right: 12px;
-}
-
-/* 设置宽度后,content无法适应宽度,只能去间接的调整内部元素的宽度 */
-.sideBarBox {
-  position: relative;
-  /* width: 12%; */
-  z-index: 2;
-  height: 100vh;
-  top: 0;
-}
-
-.sideBar {
-  /* width: 12vw; */
-  height: 100vh;
-  position: relative;
-  overflow: scroll;
-}
-
-/* 设置弹出层的样式 */
-.el-popper > .logoText {
-  width: 100px;
-  font-size: 16px;
-  /* color: red; */
-}
-
-/* 调整LOGO */
-.logoBox {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.logoImg {
-  display: flex;
-  align-items: center;
-  width: 33px;
-  /* margin-right: 20px; */
-  /* height: 50px; */
-}
-
-.logoText {
-  width: 80%;
-  height: 100%;
-  margin-left: 15%;
-  display: flex;
-  font-size: 18px;
-  align-items: center;
-  /* background-color: lightcoral; */
-}
-
-/* 主要用来调整整个menu的宽度 */
-.menuTitle {
-  margin-right: 40px;
-}
-
-.sideBarFold {
-  width: 5%;
-  height: 3%;
-  position: absolute;
-  right: 40px;
-  bottom: 20px;
-}
-
-.navBarBox {
-  position: fixed;
-  width: 100vw;
-  z-index: 1;
-  height: 7vh;
-  top: 0;
-  background-color: white;
-  right: 0;
-  border-bottom: 1px solid gainsboro;
-}
-
-.headPortraitBox {
-  position: absolute;
-  right: 3%;
-  top: 50%;
-  transform: translateY(-50%);
-}
-
-.userTools {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-around;
-  align-items: center;
-}
-
-.userToolsItem {
-  cursor: pointer;
-  width: 100%;
-  height: 4vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  /* padding: 10px; */
-  margin: 2%;
-}
-
-.userToolsItem > span {
-  margin-left: 10%;
-}
-
-.userToolsItem:hover {
-  background-color: #f2f3f5;
-}
-
-.headPortrait {
-  cursor: pointer;
-  width: 50px;
-}
-
-.content {
-  /* flex-grow: 1; */
-  /* position: absolute; */
-
-  width: 100%;
-  /* height: 93%; */
-  margin-top: 7vh;
-  overflow: scroll;
-  background-color: #f2f3f5;
-  right: 0vw;
-  top: 0vh;
-}
-</style>
-
-<!-- 为了让popper-class生效,需要的单独写一份 -->
-<style>
-.headPopper {
-  padding: 0px !important;
-  border: 1px solid #e5e6eb;
-
-  background-color: white;
-}
-</style>

+ 91 - 88
src/views/Home/InfoManage/GameManageView.vue

@@ -1,31 +1,20 @@
 <script setup lang="ts">
-import { type TablePaginationSetting, type TableFieldInfo } from '@/types/table'
-import type { FormRules, FormInstance } from 'element-plus'
-
+import Dialog from '@/components/common/Dialog.vue'
 import Table from '@/components/Table.vue'
 
-import { reactive, ref } from 'vue'
+import { type TablePaginationSetting, type TableFieldInfo } from '@/types/table'
+import type { FormRules } from 'element-plus'
+import type { DialogConfig } from '@/types/dialog'
+import type { FormField } from '@/types/form'
+import { FormFieldType } from '@/types/form'
 
+import { onMounted, reactive, ref } from 'vue'
 import { useRequest } from '@/hooks/useRequest'
-import { useDialog } from '@/hooks/useDialog'
 
 const { AllApi } = useRequest()
-const { dialogClose, submitDialog, handleEdit, addNeweItem } = useDialog()
 
-interface GameDialogFormData {
-  gid: string
-  wxAppid: string
-  wxSecret: string
-  ttAppid: string
-  ttSecret: string
-  gameName: string
-  appSecret: string
-}
-
-const gameTableRef = ref()
-
-// 游戏配置对话框对象
-const gameDialogFormRef = ref<FormInstance>()
+const gameTableRef = ref<InstanceType<typeof Table>>()
+const gameDialogRef = ref()
 
 // 配置请求参数
 const requestConfig = reactive({
@@ -40,9 +29,7 @@ const paginationConfig: TablePaginationSetting = {
   limit: 15, // 每页展示个数
   currentPage: 1, // 当前页码
   total: 0, // 数据总数
-  pagesizeList: [15, 30], // 页数大小列表
-
-  hasLodingData: 0 // 已经加载的数据
+  pagesizeList: [15, 30] // 页数大小列表
 }
 
 // 字段信息
@@ -50,43 +37,41 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
   {
     name: 'gameName',
     cnName: '游戏名',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'gid',
     cnName: '游戏ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'ttAppid',
     cnName: '抖音App ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'ttSecret',
     cnName: '抖音App Secret',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'wxAppid',
     cnName: '微信App ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'wxSecret',
     cnName: '微信App Secret',
-    isShow: true
+    isShow: true,
+    needSort: false
   }
 ])
 
-// 游戏配置对话框设置
-const dialogConfig = reactive({
-  dialogVisible: false,
-  title: '游戏配置',
-  formLabelWidth: '150px',
-  type: 0 // 0 是新增 1是修改
-})
-
 // 表单校验规则
 const gameFormRule = reactive({
   gameName: '',
@@ -179,24 +164,75 @@ const gameRules = reactive<FormRules<typeof gameFormRule>>({
   ]
 })
 
-// 对话框表单数据
-const gameFormData = reactive<GameDialogFormData>({
-  gid: '',
-  wxAppid: '',
-  wxSecret: '',
-  ttAppid: '',
-  ttSecret: '',
-  gameName: '',
-  appSecret: ''
+// 表单字段信息
+const dialogFormFields: Array<FormField> = [
+  {
+    name: 'gameName',
+    cnName: '游戏名',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'gid',
+    cnName: '游戏ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'ttAppid',
+    cnName: '抖音App ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'ttSecret',
+    cnName: '抖音App Secret',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'wxAppid',
+    cnName: '微信App ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'wxSecret',
+    cnName: '微信App Secret',
+    type: FormFieldType.INPUT
+  }
+]
+
+/**
+ * @description: 游戏对话框提交
+ * @return {*}
+ */
+const gameDialogConfig = reactive<DialogConfig>({
+  title: '游戏配置',
+  rules: gameRules,
+  reqConfig: {
+    url: AllApi.addGame,
+    otherOptions: {
+      formData: {
+        appSecret: '6YJSuc50uJ18zj45'
+      }
+    }
+  },
+  fieldsInfo: dialogFormFields
 })
 
-// 游戏配置提交
-const submiteGameChange = () => {
-  gameFormData.appSecret = '6YJSuc50uJ18zj45'
-  submitDialog(gameDialogFormRef.value, dialogConfig, AllApi.addGame, gameFormData).then(() => {
-    gameTableRef.value.getData()
-  })
+const addNewItem = () => {
+  gameDialogRef.value.addForm()
+}
+
+const handleEdit = (row: any) => {
+  gameDialogRef.value.editForm(row)
 }
+
+const formSub = () => {
+  gameTableRef.value?.getData()
+}
+
+onMounted(() => {
+  gameTableRef.value?.getData()
+})
+
+// gameTableRef.value?.getData()
 </script>
 <template>
   <div class="gameMangeBox">
@@ -211,7 +247,7 @@ const submiteGameChange = () => {
       :table-fields-info="filedsInfo"
       :request-config="requestConfig"
       :pagination-config="paginationConfig"
-      @addNewItem="addNeweItem(dialogConfig)"
+      @addNewItem="addNewItem"
     >
       <template #tableOperation>
         <el-table-column label="操作" align="center">
@@ -219,50 +255,17 @@ const submiteGameChange = () => {
             <el-button
               size="small"
               type="primary"
-              @click="handleEdit(scope.row, gameFormData, dialogConfig)"
+              @click="handleEdit(scope.row)"
               class="operationBtn"
             >
               修改
             </el-button>
-            <!-- <el-button
-              class="operationBtn"
-              size="small"
-              type="success"
-              @click="enterUserPage(scope.row)"
-            >
-              进入
-            </el-button> -->
           </template>
         </el-table-column>
       </template>
     </Table>
     <div class="optionDialog">
-      <el-dialog
-        @close="dialogClose(gameDialogFormRef, dialogConfig)"
-        v-model="dialogConfig.dialogVisible"
-        :title="dialogConfig.title"
-      >
-        <el-form :rules="gameRules" :model="gameFormData" ref="gameDialogFormRef">
-          <template v-for="item in filedsInfo">
-            <el-form-item
-              :prop="item.name"
-              :label="item.cnName"
-              :label-width="dialogConfig.formLabelWidth"
-            >
-              <el-input
-                v-model="gameFormData[item.name as keyof GameDialogFormData]"
-                autocomplete="off"
-              />
-            </el-form-item>
-          </template>
-        </el-form>
-        <template #footer>
-          <div class="dialog-footer">
-            <el-button type="primary" @click="submiteGameChange()"> 确认 </el-button>
-            <el-button @click="dialogClose(gameDialogFormRef, dialogConfig)">取消</el-button>
-          </div>
-        </template>
-      </el-dialog>
+      <Dialog @form-submit="formSub" ref="gameDialogRef" :config="gameDialogConfig"></Dialog>
     </div>
   </div>
 </template>

+ 155 - 102
src/views/Home/InfoManage/PlayerManageView.vue

@@ -9,48 +9,40 @@ import {
   ColorType
 } from '@/types/table'
 
+import Dialog from '@/components/common/Dialog.vue'
 import Table from '@/components/Table.vue'
-import CryptoJS from 'crypto-js'
+
 import axiosInstance from '@/utils/axios/axiosInstance'
 
-import { onMounted, reactive, ref, watch } from 'vue'
+import { onMounted, reactive, ref } from 'vue'
 import { ElMessageBox } from 'element-plus'
-import type { FormRules, FormInstance } from 'element-plus'
+
+import type { FormRules } from 'element-plus'
+import type { FormField } from '@/types/form'
+import type { DialogConfig } from '@/types/dialog'
+import { FormFieldType } from '@/types/form'
 
 import { useTableStore } from '@/stores/useTable'
 import { useRequest } from '@/hooks/useRequest'
-import { useDialog } from '@/hooks/useDialog'
 import { useCommonStore } from '@/stores/useCommon'
 
 const { AllApi, analysisResCode } = useRequest()
 
 const tableStore = useTableStore()
 const commonStore = useCommonStore()
-const { dialogClose, submitDialog, handleEdit } = useDialog()
-
-// 对话框表单数据格式
-interface PlayerDialogFormData {
-  gid: string
-  openId: string
-  pf: string
-  option: string
-  userId: string
-}
 
 // 表格对象
 const playerTableRef = ref()
 
 // 游戏配置对话框对象
-const playerDialogFormRef = ref<FormInstance>()
+const playerDialogFormRef = ref()
 
 // 配置分页数据
 const paginationConfig: TablePaginationSetting = {
   limit: 15, // 每页展示个数
   currentPage: 1, // 当前页码
   total: 0, // 数据总数
-  pagesizeList: [15, 30], // 页数大小列表
-
-  hasLodingData: 0 // 已经加载的数据
+  pagesizeList: [15, 30] // 页数大小列表
 }
 
 // 配置请求参数
@@ -103,12 +95,14 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
   {
     name: 'gid',
     cnName: '游戏ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'head',
     cnName: '头像',
     isShow: true,
+    needSort: false,
     specialEffect: {
       type: FieldSpecialEffectType.IMG,
       othnerInfo: {}
@@ -118,6 +112,7 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
     name: 'inBlack',
     cnName: '是否在黑名单',
     isShow: true,
+    needSort: false,
     specialEffect: {
       type: FieldSpecialEffectType.TAG,
       othnerInfo: {
@@ -129,17 +124,20 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
   {
     name: 'nickName',
     cnName: '昵称',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'openId',
     cnName: 'Open ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   },
   {
     name: 'option',
     cnName: '权限',
     isShow: true,
+    needSort: false,
     specialEffect: {
       type: FieldSpecialEffectType.TEXT,
       othnerInfo: {
@@ -152,6 +150,7 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
     name: 'pf',
     cnName: '平台',
     isShow: true,
+    needSort: false,
     specialEffect: {
       type: FieldSpecialEffectType.TRANSLATE,
       othnerInfo: {
@@ -166,17 +165,55 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
   {
     name: 'userId',
     cnName: '玩家ID',
-    isShow: true
+    isShow: true,
+    needSort: false
   }
 ])
 
-// 游戏配置对话框设置
-const dialogConfig = reactive({
-  dialogVisible: false,
-  title: '用户权限配置',
-  formLabelWidth: '150px',
-  type: 0 // 0 是新增 1是修改
-})
+// // 游戏配置对话框设置
+// const dialogConfig = reactive({
+//   dialogVisible: false,
+//   title: '用户权限配置',
+//   formLabelWidth: '150px',
+//   type: 0 // 0 是新增 1是修改
+// })
+
+// // 表单校验规则
+// const optionFormRule = reactive({
+//   option: ''
+// })
+
+// // 表单规则
+// const gameRules = reactive<FormRules<typeof optionFormRule>>({
+//   option: [
+//     { required: true, message: '请输入权限', trigger: 'blur' },
+//     { min: 1, max: 255, message: '最短1位,最长255位', trigger: 'blur' }
+//   ]
+// })
+
+// // 对话框表单数据
+// const optionFormData = reactive<PlayerDialogFormData>({
+//   gid: '',
+//   openId: '',
+//   pf: '',
+//   option: '',
+//   userId: ''
+// })
+
+// // 游戏配置提交
+// const submiteOptionChange = (isEncrypt: boolean = false) => {
+//   let option = optionFormData.option
+//   if (isEncrypt) {
+//     let message = `${optionFormData.gid}${optionFormData.userId}${optionFormData.pf}`
+//     option = CryptoJS.HmacMD5(message, optionFormData.option).toString()
+//   }
+//   submitDialog(playerDialogFormRef.value, dialogConfig, AllApi.addOption, {
+//     ...optionFormData,
+//     option
+//   }).then(() => {
+//     playerTableRef.value.getData()
+//   })
+// }
 
 // 表单校验规则
 const optionFormRule = reactive({
@@ -184,22 +221,44 @@ const optionFormRule = reactive({
 })
 
 // 表单规则
-const gameRules = reactive<FormRules<typeof optionFormRule>>({
+const optionRules = reactive<FormRules<typeof optionFormRule>>({
   option: [
     { required: true, message: '请输入权限', trigger: 'blur' },
     { min: 1, max: 255, message: '最短1位,最长255位', trigger: 'blur' }
   ]
 })
 
-// 对话框表单数据
-const optionFormData = reactive<PlayerDialogFormData>({
-  gid: '',
-  openId: '',
-  pf: '',
-  option: '',
-  userId: ''
+// 表单字段信息
+const dialogFormFields: Array<FormField> = [
+  {
+    name: 'option',
+    cnName: '权限',
+    type: FormFieldType.INPUT
+  }
+]
+
+/**
+ * @description: 游戏对话框提交
+ * @return {*}
+ */
+const optionDialogConfig = reactive<DialogConfig>({
+  title: '权限配置',
+  rules: optionRules,
+  reqConfig: {
+    url: AllApi.addOption,
+    otherOptions: {}
+  },
+  fieldsInfo: dialogFormFields
 })
 
+const handleEdit = (row: any) => {
+  playerDialogFormRef.value.editForm(row)
+}
+
+const formSub = () => {
+  playerTableRef.value.getData()
+}
+
 // 封禁/解封用户
 const blockedPlayer = (row: any) => {
   let url = row.inBlack ? AllApi.deleteUserToBlack : AllApi.addUserToBlack
@@ -214,41 +273,64 @@ const blockedPlayer = (row: any) => {
     confirmButtonText: '确认',
     cancelButtonText: '取消',
     type: 'warning'
-  }).then(() => {
-    axiosInstance.post(url, playerInfo).then((data) => {
-      analysisResCode(data)
-        .then((info) => {
-          console.log(info)
-          playerTableRef.value.getData()
-        })
-        .catch((err) => {
-          console.log(err)
-        })
-    })
   })
+    .then(() => {
+      axiosInstance.post(url, playerInfo).then((data) => {
+        analysisResCode(data)
+          .then((info) => {
+            console.log(info)
+            playerTableRef.value.getData()
+          })
+          .catch((err) => {
+            console.log(err)
+          })
+      })
+    })
+    .catch(() => {
+      // 在点击取消时他会抛出错误,这里需要去捕获一下,不然会在控制台出现
+    })
 }
 
-// 游戏配置提交
-const submiteOptionChange = (isEncrypt: boolean = false) => {
-  let option = optionFormData.option
-  if (isEncrypt) {
-    let message = `${optionFormData.gid}${optionFormData.userId}${optionFormData.pf}`
-    option = CryptoJS.HmacMD5(message, optionFormData.option).toString()
-  }
-  submitDialog(playerDialogFormRef.value, dialogConfig, AllApi.addOption, {
-    ...optionFormData,
-    option
-  }).then(() => {
-    playerTableRef.value.getData()
-  })
+/**
+ * @description: 加密字段
+ * @return {*}
+ */
+const encrypt = () => {
+  playerDialogFormRef.value.encrypt('option', true, ['gid', 'userId', 'pf'])
 }
 
-watch(
-  () => commonStore.selectInfo.gid,
-  (val) => {
-    requestConfig.otherOptions.gid = val
-  }
-)
+// let watchGid: any = null
+
+// onActivated(() => {
+//   /**
+//    * @description: 监听gid的变化
+//    * @return {*}
+//    */
+//   watchGid = watch(
+//     () => commonStore.selectInfo.gid,
+//     (val) => {
+//       requestConfig.otherOptions.gid = val
+//     },
+//     {
+//       immediate: true
+//     }
+//   )
+// })
+
+// onDeactivated(() => {
+//   watchGid()
+// })
+
+const updateGid = (gid: any) => {
+  requestConfig.otherOptions.gid = gid
+}
+
+const backupSelect = reactive([])
+
+import { usePage } from '@/hooks/usePage'
+const { watchPageChange } = usePage()
+
+watchPageChange(() => [commonStore.selectInfo.gid], backupSelect, updateGid)
 
 onMounted(() => {
   tableStore.allGameInfo.map((item) => {
@@ -281,7 +363,7 @@ onMounted(() => {
             <el-button
               size="small"
               type="warning"
-              @click="handleEdit(scope.row, optionFormData, dialogConfig)"
+              @click="handleEdit(scope.row)"
               class="operationBtn"
             >
               修改权限
@@ -299,40 +381,11 @@ onMounted(() => {
       </template>
     </Table>
     <div class="optionDialog">
-      <el-dialog
-        @close="dialogClose(playerDialogFormRef, dialogConfig)"
-        v-model="dialogConfig.dialogVisible"
-        :title="dialogConfig.title"
-      >
-        <el-form :rules="gameRules" :model="optionFormData" ref="playerDialogFormRef">
-          <template v-for="item in filedsInfo">
-            <el-form-item
-              :prop="item.name"
-              :label="item.cnName"
-              :label-width="dialogConfig.formLabelWidth"
-              v-if="item.name === 'option'"
-            >
-              <el-input
-                v-model="optionFormData[item.name as keyof PlayerDialogFormData]"
-                autocomplete="off"
-              />
-            </el-form-item>
-          </template>
-        </el-form>
-        <template #footer>
-          <div class="dialog-footer">
-            <el-button class="subBtnItem" type="warning" @click="submiteOptionChange(true)"
-              >加密上传</el-button
-            >
-            <el-button class="subBtnItem" type="primary" @click="submiteOptionChange()">
-              普通上传
-            </el-button>
-            <el-button class="subBtnItem" @click="dialogClose(playerDialogFormRef, dialogConfig)"
-              >取消</el-button
-            >
-          </div>
+      <Dialog @form-submit="formSub" ref="playerDialogFormRef" :config="optionDialogConfig">
+        <template #otherBtn>
+          <el-button class="operationBtn" type="warning" @click="encrypt"> 加密上传 </el-button>
         </template>
-      </el-dialog>
+      </Dialog>
     </div>
   </div>
 </template>
@@ -350,6 +403,6 @@ onMounted(() => {
 }
 
 .operationBtn {
-  margin-right: 5%;
+  margin-right: 10px;
 }
 </style>

+ 166 - 67
src/views/Home/Overview/OverView.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
-import { onMounted, reactive, watch, ref, toRaw } from 'vue'
+import { reactive, ref, toRaw } from 'vue'
 import type { StaticField, ReqConfig, TemporalTrendInfo } from '@/types/dataAnalysis'
 
 import StatisticText from '@/components/dataAnalysis/StatisticText.vue'
@@ -9,6 +9,9 @@ import { useCommonStore } from '@/stores/useCommon'
 import { useRequest } from '@/hooks/useRequest'
 import { useAnalysis } from '@/hooks/useAnalysis'
 
+import { usePage } from '@/hooks/usePage'
+const { watchPageChange } = usePage()
+
 const { updateReqConfig } = useAnalysis()
 const { AllApi } = useRequest()
 const { selectInfo } = useCommonStore()
@@ -19,7 +22,7 @@ const overviewStaticRef = ref()
 // 目前选择的信息
 // 这里不太合理,应该根据返回的数据中的pf和game赋值,因为这个可能会变
 const overViewSelectInfo = reactive({
-  pf: 'web'
+  pf: selectInfo.pf[0]
 })
 
 // 总览数据的字段对应的信息
@@ -55,13 +58,14 @@ const overViewDataReqConfig = reactive<ReqConfig>({
   }
 })
 
+// 这里不要给pf和gid赋值,不然趋势图组件监听不到gid和pf的变化,不会请求数据
 // 分时段组件所需要的信息
 const periodInfo = reactive<TemporalTrendInfo>({
   dataReqConfig: {
     url: AllApi.timeDistributionData,
     otherOptions: {
-      pf: overViewSelectInfo.pf,
-      gid: selectInfo.gid
+      pf: '',
+      gid: ''
     }
   },
   tabList: toRaw([
@@ -81,31 +85,79 @@ const periodInfo = reactive<TemporalTrendInfo>({
       type: 3
     }
   ]),
-  chartsStaticField: toRaw([
-    {
-      name: 'yesterdayCount',
-      cnName: '昨日',
-      value: ''
-    },
-    {
-      name: 'todayCount',
-      cnName: '今日',
-      value: ''
-    },
-    {
-      name: 'yesterdayThisTimeCount',
-      cnName: '昨日此时',
-      value: ''
-    }
-  ]),
+  chartsStaticField: toRaw({
+    newUser: [
+      {
+        name: 'yesterdayCount',
+        cnName: '昨日',
+        value: ''
+      },
+      {
+        name: 'todayCount',
+        cnName: '今日',
+        value: ''
+      },
+      {
+        name: 'yesterdayThisTimeCount',
+        cnName: '昨日此时',
+        value: ''
+      }
+    ],
+    activeUser: [
+      {
+        name: 'yesterdayCount',
+        cnName: '昨日',
+        value: ''
+      },
+      {
+        name: 'todayCount',
+        cnName: '今日',
+        value: ''
+      },
+      {
+        name: 'yesterdayThisTimeCount',
+        cnName: '昨日此时',
+        value: ''
+      }
+    ],
+    launchCount: [
+      {
+        name: 'yesterdayCount',
+        cnName: '昨日',
+        value: ''
+      },
+      {
+        name: 'todayCount',
+        cnName: '今日',
+        value: ''
+      },
+      {
+        name: 'yesterdayThisTimeCount',
+        cnName: '昨日此时',
+        value: ''
+      }
+    ]
+  }),
   resDataField: toRaw({
     xAxis: 'today', // x轴的刻度信息所在的字段
     values: ['today', 'yesterday'] // 值所在的字段
   }),
   trendTableFields: toRaw({
-    index: '时间段',
-    today: '今日',
-    yesterday: '昨日'
+    newUser: {
+      index: '时间段',
+      today: '今日新增',
+      yesterday: '昨日新增'
+    },
+    activeUser: {
+      index: '时间段',
+      today: '今日活跃',
+      yesterday: '昨日活跃'
+    },
+    launchCount: {
+      index: '时间段',
+      today: '今日启动',
+      yesterday: '昨日启动'
+    }
   })
 })
 
@@ -114,8 +166,8 @@ const monthInfo = reactive<TemporalTrendInfo>({
   dataReqConfig: {
     url: AllApi.userMouthDistributionData,
     otherOptions: {
-      pf: overViewSelectInfo.pf,
-      gid: selectInfo.gid
+      pf: '',
+      gid: ''
     }
   },
   tabList: toRaw([
@@ -138,69 +190,117 @@ const monthInfo = reactive<TemporalTrendInfo>({
       name: 'deviceDuration',
       tabTitle: '单设备时长',
       type: 4
-    },
-    {
-      name: 'retentionRate',
-      tabTitle: '留存率',
-      type: 5
-    }
-  ]),
-  chartsStaticField: toRaw([
-    {
-      name: 'avg',
-      cnName: '平均值',
-      value: ''
-    },
-    {
-      name: 'count',
-      cnName: '总数',
-      value: ''
     }
   ]),
+  chartsStaticField: toRaw({
+    newUser: [
+      {
+        name: 'avg',
+        cnName: '平均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ],
+    activeUser: [
+      {
+        name: 'avg',
+        cnName: '平均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ],
+    launchCount: [
+      {
+        name: 'avg',
+        cnName: '平均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ],
+    deviceDuration: [
+      {
+        name: 'avg',
+        cnName: '平均值',
+        value: ''
+      },
+      {
+        name: 'count',
+        cnName: '总数',
+        value: ''
+      }
+    ]
+  }),
   resDataField: toRaw({
     xAxis: 'timeDistribution', // x轴的刻度信息所在的字段
     values: ['timeDistribution'] // 值所在的字段
   }),
   trendTableFields: toRaw({
-    index: '日期',
-    timeDistribution: '新增'
+    newUser: {
+      index: '日期',
+      timeDistribution: '新增用户'
+    },
+    activeUser: {
+      index: '日期',
+      timeDistribution: '活跃用户'
+    },
+    launchCount: {
+      index: '日期',
+      timeDistribution: '启动次数'
+    },
+    deviceDuration: {
+      index: '日期',
+      timeDistribution: '单设备时长'
+    }
   })
 })
 
 /**
- * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
- * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+ * @description: 选择的平台改变
+ * @param {*} pf  选择的平台数组,暂时只用第一个
  * @return {*}
  */
-watch(
-  () => [overViewSelectInfo.pf, selectInfo.gid],
-  ([newPf, newGid]) => {
-    updateReqConfig(overViewDataReqConfig, { pf: newPf, gid: newGid })
-    updateReqConfig(periodInfo.dataReqConfig, { pf: newPf, gid: newGid })
-    updateReqConfig(monthInfo.dataReqConfig, { pf: newPf, gid: newGid })
-  }
-)
+const changePf = (pf: Array<string>) => {
+  overViewSelectInfo.pf = pf[0]
+}
 
 /**
- * @description: 选择的平台改变
- * @param {*} pf  平台名
+ * @description: 更新所有的请求接口
+ * @param {*} pf 平台
+ * @param {*} gid 游戏id
  * @return {*}
  */
-const changePf = (pf: string) => {
-  overViewSelectInfo.pf = pf
+const updateAllReq = (pf: string, gid: string) => {
+  updateReqConfig(overViewDataReqConfig, { pf, gid })
+  updateReqConfig(periodInfo.dataReqConfig, { pf, gid })
+  updateReqConfig(monthInfo.dataReqConfig, { pf, gid })
 }
 
-onMounted(() => {})
+/**
+ * @description: 监听pf和gid的变化,数据变化后立即重新请求所有相关数据
+ *    当没有激活时,则取消监听
+ * @tip watch监听reactive的数据时,必须以getter形式,不然会警告
+ * @return {*}
+ */
+
+const backupSelect = reactive([])
+watchPageChange(() => [overViewSelectInfo.pf, selectInfo.gid], backupSelect, updateAllReq)
 </script>
 <template>
   <div class="overViewBox">
     <div class="header">
-      <HeaderCard
-        :open-date-select="false"
-        :default-pf="overViewSelectInfo.pf"
-        @change-pf="changePf"
-        :title="'数据总览'"
-      ></HeaderCard>
+      <HeaderCard :open-date-select="false" @change-pf="changePf" :title="'数据总览'"></HeaderCard>
     </div>
     <div class="staticBox">
       <StatisticText
@@ -211,7 +311,6 @@ onMounted(() => {})
     </div>
     <div class="periodTrendBox">
       <TemporalTrend
-        :wait-time-select="false"
         :need-charts="true"
         :table-fields-info="periodInfo.trendTableFields"
         :res-data-fields-info="periodInfo.resDataField"

+ 469 - 0
src/views/Index.vue

@@ -0,0 +1,469 @@
+<!--
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-08-20 14:06:49
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-09-18 11:56:31
+ * @FilePath: \Game-Backstage-Management-System\src\views\Index.vue
+ * @Description: 
+ * 
+-->
+<script setup lang="ts">
+import { zhCn } from 'element-plus/es/locales.mjs'
+import { RouterView } from 'vue-router'
+import { onMounted, reactive, ref, computed, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { getAllGameInfo } from '@/utils/table/table'
+import router from '@/router'
+import type { DropDownInfo } from '@/types/dataAnalysis'
+import DropDownSelection from '@/components/dataAnalysis/DropDownSelection.vue'
+import { useCommonStore } from '@/stores/useCommon'
+import { initLoadResouce } from '@/utils/resource'
+
+const route = useRoute()
+const { selectInfo } = useCommonStore()
+const isCollapse = ref(false)
+const navBarSelect = ref<string>('Home')
+const siderBarOpened = ref<Array<string>>(['数据总览'])
+const siderBar = ref()
+const loadingState = ref(false) // 用来标记必要信息的加载状态
+
+// 路由信息,同时也是侧边栏生成的依据信息
+const menuList = reactive<Array<any>>([])
+
+// 默认选中
+const defaultActive = computed(() => {
+  return route.meta.activeMenu
+})
+
+// 顶部导航栏信息
+const navBarMenuList = [
+  {
+    name: 'Home',
+    title: '应用分析'
+  },
+
+  {
+    name: 'AppManage',
+    title: '应用管理'
+  }
+]
+
+/**
+ * @description: 侧边栏折叠改变
+ * @return {*}
+ */
+const changeCollapse = () => {
+  isCollapse.value = !isCollapse.value
+}
+
+// 登出
+const logOut = () => {
+  ElMessage({
+    type: 'success',
+    message: '退出成功',
+    duration: 1000
+  })
+  localStorage.removeItem('token')
+  localStorage.removeItem('refreshToken')
+  router.push('/login')
+}
+
+// 游戏下拉选择框需要的数据
+const gameSelectInfo = reactive<DropDownInfo>({
+  defaultSelect: '1001',
+  title: '请选择游戏',
+  optionsList: []
+})
+
+/**
+ * @description: 更新整个页面的游戏选择
+ * @param {*} gid 游戏id
+ * @return {*}
+ */
+const changeGame = (gid: any) => {
+  selectInfo.gid = gid
+}
+
+/**
+ * @description: 头部导航栏改变
+ * @param {*} val 对应的name
+ * @return {*}
+ */
+const changeNavBar = (val: string) => {
+  navBarSelect.value = val
+
+  router.push(`/${val}`)
+  createdMenuList()
+  let title = navBarMenuList.find((item) => item.name === val)?.title
+  if (title) {
+    siderBarOpened.value.splice(0, 1, title)
+  }
+}
+
+// 资源的加载路径
+const resourceInfo: Record<string, string> = {
+  logo: `/img/logo.svg`,
+  defaultHead: `/img/default/defaultHead.png`
+}
+
+// 使用blob的资源路径信息
+const blobUrlInfo = reactive<Record<string, string>>({})
+
+// 侧边栏跳转路由的基本路由
+const basePath = ref<string | undefined>()
+
+/**
+ * @description: 创建侧边栏menu
+ * @return {*}
+ */
+const createdMenuList = () => {
+  let routes = router.options.routes // 获取路由信息
+  let indexRoutesChild = routes.find((item) => item.name === 'Index')?.children
+  let activeMenu = indexRoutesChild?.find((item) => {
+    return item.name === navBarSelect.value // 根据顶部导航栏的选中情况来选择选中哪个具体的路由信息,可以打印自己看一下
+  })
+  basePath.value = activeMenu?.path // 找到需要激活的菜单的路由,后续用来拼接需要跳转的路由
+  menuList.splice(0, menuList.length, ...(activeMenu?.children as Array<any>)) // 清空原来的路由信息,并且加入新选中的
+}
+
+/**
+ * @description: 当路由地址改变的时候,去获取最新的导航栏位置,并且重新生成侧边栏,不然刷新后,侧边栏会无法选中
+ * @param {*} router
+ * @return {*}
+ */
+watch(
+  () => [router.currentRoute.value.fullPath],
+  ([newFullPath]) => {
+    let routes = router.options.routes // 获取路由信息
+    let indexRoutesChild = routes.find((item) => item.name === 'Index')?.children
+    let activeMenu = indexRoutesChild?.find((item) => {
+      return newFullPath.includes(item.path) // 根据顶部导航栏的选中情况来选择选中哪个具体的路由信息,可以打印自己看一下
+    })
+    navBarSelect.value = activeMenu?.name as string
+    createdMenuList()
+  },
+  {
+    immediate: true
+  }
+)
+
+/**
+ * @description: 获取所有游戏列表
+ * @return {*}
+ */
+getAllGameInfo().then((data) => {
+  if (data) {
+    data.map((item) => {
+      gameSelectInfo.optionsList.push({
+        value: item.gid,
+        label: item.gameName
+      })
+    })
+    gameSelectInfo.defaultSelect = data[0].gid
+    changeGame(data[0].gid)
+    loadingState.value = true
+  } else {
+    throw new Error('游戏信息获取失败')
+  }
+})
+
+onMounted(() => {
+  // 去加载所有需要的资源
+  initLoadResouce(resourceInfo).then((data) => {
+    Object.assign(blobUrlInfo, data)
+  })
+})
+</script>
+
+<template>
+  <el-config-provider :locale="zhCn">
+    <div class="body" v-if="loadingState">
+      <div class="navBarBox">
+        <div class="logoBox">
+          <el-image :fit="'fill'" class="logoImg" :src="blobUrlInfo.logo"></el-image>
+          <span>淳皓科技</span>
+        </div>
+
+        <div class="gameSelect">
+          <el-icon class="gameIcon" :size="20">
+            <icon-icon-park-game-three></icon-icon-park-game-three>
+          </el-icon>
+          <DropDownSelection
+            :default-select="gameSelectInfo.defaultSelect"
+            :title="gameSelectInfo.title"
+            :options-list="gameSelectInfo.optionsList"
+            :size="'default'"
+            @change-select="changeGame"
+          ></DropDownSelection>
+        </div>
+        <!-- 顶部导航栏 -->
+
+        <div class="navBarMenu">
+          <el-menu
+            :default-active="navBarSelect"
+            class="el-menu-demo"
+            mode="horizontal"
+            @select="changeNavBar"
+          >
+            <el-menu-item
+              v-for="item in navBarMenuList"
+              class="navBarMenuItem"
+              :index="item.name"
+              >{{ item.title }}</el-menu-item
+            >
+          </el-menu>
+        </div>
+        <div class="headPortraitBox">
+          <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">
+            <template #reference>
+              <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>
+            </template>
+            <div class="userTools">
+              <span class="userToolsItem" @click="logOut">
+                <icon-material-symbols-light-logout></icon-material-symbols-light-logout>
+                <span> 退出登录</span>
+              </span>
+            </div>
+          </el-popover>
+        </div>
+      </div>
+      <!-- 侧边栏 -->
+      <div class="sideBarBox">
+        <el-menu
+          :default-active="defaultActive"
+          class="sideBar"
+          :collapse="isCollapse"
+          ref="siderBar"
+        >
+          <template v-for="(item, index) in menuList">
+            <el-sub-menu :index="`${index}`" v-if="item.children && item.showChild">
+              <template #title>
+                <el-icon><component :is="item.icon"></component></el-icon>
+                <span>{{ item.cnName }}</span>
+              </template>
+              <router-link
+                style="text-decoration: none"
+                v-for="val in item.children"
+                :to="{ path: basePath + '/' + item.path + '/' + val.path }"
+                :key="index"
+              >
+                <el-menu-item :index="val.path">{{ val.cnName }}</el-menu-item>
+              </router-link>
+            </el-sub-menu>
+
+            <router-link
+              style="text-decoration: none"
+              v-else
+              :to="{ path: basePath + '/' + item.path }"
+              :key="index"
+            >
+              <el-menu-item :index="item.path">
+                <el-icon><component :is="item.icon" /></el-icon>
+                <template #title>
+                  <span class="menuTitle">{{ item.cnName }}</span>
+                </template>
+              </el-menu-item>
+            </router-link>
+          </template>
+          <div class="sideBarFold" @click="changeCollapse">
+            <el-icon :size="25"><Fold /></el-icon>
+          </div>
+        </el-menu>
+      </div>
+
+      <div class="content">
+        <router-view v-slot="{ Component, route }">
+          <keep-alive>
+            <component
+              :is="Component"
+              :key="route.meta.activeMenu"
+              v-if="route.meta.needKeepAlive == true"
+            ></component>
+          </keep-alive>
+          <component
+            :is="Component"
+            :key="route.meta.activeMenu"
+            v-if="route.meta.needKeepAlive == false"
+          ></component>
+        </router-view>
+      </div>
+    </div>
+  </el-config-provider>
+</template>
+
+<style scoped>
+.body {
+  width: 100%;
+  display: flex;
+  height: 100vh;
+}
+
+/* 设置宽度后,content无法适应宽度,只能去间接的调整内部元素的宽度 */
+.sideBarBox {
+  position: relative;
+  /* width: 12%; */
+  z-index: 1;
+  height: 93vh;
+  margin-top: 7vh;
+  top: 0;
+}
+
+.sideBar {
+  /* width: 12vw; */
+  height: 93vh;
+  position: relative;
+  overflow: scroll;
+}
+
+/* 设置弹出层的样式 */
+.el-popper > .logoText {
+  width: 100px;
+  font-size: 16px;
+  /* color: red; */
+}
+
+.logoImg {
+  display: flex;
+  align-items: center;
+  width: 33px;
+  /* margin-right: 20px; */
+  /* height: 50px; */
+}
+
+.logoText {
+  width: 80%;
+  height: 100%;
+  margin-left: 15%;
+  display: flex;
+  font-size: 18px;
+  align-items: center;
+  /* background-color: lightcoral; */
+}
+
+/* 主要用来调整整个menu的宽度 */
+.menuTitle {
+  margin-right: 40px;
+}
+
+.sideBarFold {
+  width: 5%;
+  height: 3%;
+  position: absolute;
+  right: 40px;
+  bottom: 20px;
+}
+
+.navBarBox {
+  position: fixed;
+  display: flex;
+  align-items: center;
+  width: 100vw;
+  z-index: 2;
+  height: 7vh;
+  top: 0;
+  background-color: white;
+  right: 0;
+  border-bottom: 1px solid gainsboro;
+}
+
+/* 调整LOGO */
+.logoBox {
+  box-sizing: border-box;
+  left: 30px;
+  position: relative;
+
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.gameSelect {
+  position: relative;
+
+  height: 80%;
+  display: flex;
+  align-items: center;
+  left: 5%;
+  display: flex;
+  align-items: center;
+}
+
+.gameIcon {
+  /* box-sizing: border-box; */
+  /* padding-right: 12px; */
+  margin-right: 12px;
+}
+
+.navBarMenu {
+  width: 60%;
+  position: relative;
+  left: 6%;
+}
+
+.headPortraitBox {
+  position: absolute;
+  right: 3%;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.userTools {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  align-items: center;
+}
+
+.userToolsItem {
+  cursor: pointer;
+  width: 100%;
+  height: 4vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  /* padding: 10px; */
+  margin: 2%;
+}
+
+.userToolsItem > span {
+  margin-left: 10%;
+}
+
+.userToolsItem:hover {
+  background-color: #f2f3f5;
+}
+
+.headPortrait {
+  cursor: pointer;
+  width: 50px;
+}
+
+.content {
+  /* flex-grow: 1; */
+  /* position: absolute; */
+
+  width: 100%;
+  /* height: 93%; */
+  margin-top: 7vh;
+  overflow: scroll;
+  background-color: #f2f3f5;
+  right: 0vw;
+  top: 0vh;
+}
+</style>
+
+<!-- 为了让popper-class生效,需要的单独写一份 -->
+<style>
+.headPopper {
+  padding: 0px !important;
+  border: 1px solid #e5e6eb;
+
+  background-color: white;
+}
+.el-menu--horizontal.el-menu {
+  border-bottom: none;
+}
+</style>

+ 1 - 0
src/views/Login/LoginView.vue

@@ -97,6 +97,7 @@ const resourceInfo: Record<string, string> = {
 const blobUrlInfo = reactive<Record<string, string>>({})
 
 onMounted(() => {
+  console.log('tt')
   // 去加载所有需要的资源
   initLoadResouce(resourceInfo).then((data) => {
     Object.assign(blobUrlInfo, data)

+ 1 - 11
tsconfig.app.json

@@ -1,16 +1,6 @@
 {
   "extends": "@vue/tsconfig/tsconfig.dom.json",
-  "include": [
-    "src/**/*.ts",
-    "src/**/*.d.ts",
-    "src/**/*.tsx",
-    "src/**/*.vue",
-    "src/components/toolsPopover/FilterPopover.vue",
-    "src/components/input/TextInput.vue",
-    "src/components/input/TextInput.VUE",
-    "src/components/svgIcon/SvgIcon.vue",
-    "src/components/svgIcon/SvgIcon.vue"
-  ],
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"],
   "exclude": ["src/**/__tests__/*"],
   "compilerOptions": {
     "composite": true,

+ 46 - 53
vite.config.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:06:49
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-09-02 16:27:01
+ * @LastEditTime: 2024-09-13 16:43:31
  * @FilePath: \Game-Backstage-Management-System\vite.config.ts
  * @Description:
  *
@@ -26,8 +26,6 @@ import viteCompression from 'vite-plugin-compression'
 import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
 const DEFAULT_OPTIONS = {
   test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i,
-  exclude: undefined,
-  include: undefined,
   includePublic: true,
   logStats: true,
   ansiColors: true,
@@ -88,59 +86,54 @@ const DEFAULT_OPTIONS = {
   cacheLocation: undefined
 }
 
-export default defineConfig({
-  build: {
-    rollupOptions: {
-      output: {
-        manualChunks: {
-          echarts: ['echarts']
-        },
-        chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
-        entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
-        assetFileNames: '[ext]/[name]-[hash].[ext]' // 资源文件像 字体,图片等
+export default defineConfig(({ mode }) => {
+  return {
+    build: {
+      rollupOptions: {
+        output: {
+          manualChunks: {
+            echarts: ['echarts']
+          },
+          chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
+          entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
+          assetFileNames: '[ext]/[name]-[hash].[ext]' // 资源文件像 字体,图片等
+        }
       }
     },
-    terserOptions: {
-      compress: {
-        drop_console: true,
-        drop_debugger: true
+    esbuild: {
+      drop: mode === 'production' ? ['console', 'debugger'] : []
+    },
+    plugins: [
+      vue(),
+      ViteImageOptimizer(DEFAULT_OPTIONS),
+      viteCompression({
+        verbose: true, // 默认即可
+        disable: false, // 开启压缩(不禁用),默认即可
+        deleteOriginFile: false, // 删除源文件
+        threshold: 5120, // 压缩前最小文件大小
+        algorithm: 'gzip', // 压缩算法
+        ext: '.gz' // 文件类型
+      }),
+      visualizer({ open: true }),
+      AutoImport({
+        resolvers: [ElementPlusResolver()]
+      }),
+      Components({
+        resolvers: [
+          ElementPlusResolver(),
+          IconsResolver({
+            prefix: 'icon'
+          })
+        ]
+      }),
+      Icons({
+        autoInstall: true
+      })
+    ],
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url))
       }
     }
-  },
-
-  esbuild: {
-    drop: ['console', 'debugger']
-  },
-  plugins: [
-    vue(),
-    ViteImageOptimizer(DEFAULT_OPTIONS),
-    viteCompression({
-      verbose: true, // 默认即可
-      disable: false, // 开启压缩(不禁用),默认即可
-      deleteOriginFile: false, // 删除源文件
-      threshold: 5120, // 压缩前最小文件大小
-      algorithm: 'gzip', // 压缩算法
-      ext: '.gz' // 文件类型
-    }),
-    visualizer({ open: true }),
-    AutoImport({
-      resolvers: [ElementPlusResolver()]
-    }),
-    Components({
-      resolvers: [
-        ElementPlusResolver(),
-        IconsResolver({
-          prefix: 'icon'
-        })
-      ]
-    }),
-    Icons({
-      autoInstall: true
-    })
-  ],
-  resolve: {
-    alias: {
-      '@': fileURLToPath(new URL('./src', import.meta.url))
-    }
   }
 })