فهرست منبع

添加聊天记录展示功能,新增聊天记录视图和相关路由,支持ZIP文件的拖拽上传和下载功能,同时更新记录视图以支持文件下载。

wui 7 ماه پیش
والد
کامیت
08cb469e5e
8فایلهای تغییر یافته به همراه1415 افزوده شده و 422 حذف شده
  1. 457 9
      package-lock.json
  2. 1 0
      package.json
  3. 5 0
      src/router/index.js
  4. 8 0
      src/services/api.js
  5. 662 0
      src/views/ChatRecordsView.vue
  6. 5 0
      src/views/MainView.vue
  7. 105 34
      src/views/RecordsView.vue
  8. 172 379
      yarn.lock

+ 457 - 9
package-lock.json

@@ -9,17 +9,21 @@
       "version": "0.0.0",
       "dependencies": {
         "@primeuix/themes": "^1.0.0",
+        "@primevue/forms": "^4.3.3",
         "@tailwindcss/vite": "^4.0.17",
+        "@vueuse/core": "^13.0.0",
+        "axios": "^1.8.4",
         "chart.js": "^4.4.8",
+        "jszip": "^3.10.1",
         "less": "^4.2.2",
-        "particles.js": "^2.0.0",
         "pinia": "^3.0.1",
         "primeflex": "^4.0.0",
         "primeicons": "^7.0.0",
         "primevue": "^4.3.3",
         "tailwindcss-primeui": "^0.6.1",
         "vue": "^3.5.13",
-        "vue-router": "^4.5.0"
+        "vue-router": "^4.5.0",
+        "zod": "^3.24.2"
       },
       "devDependencies": {
         "@eslint/js": "^9.21.0",
@@ -825,6 +829,25 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@primeuix/forms": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmmirror.com/@primeuix/forms/-/forms-0.0.4.tgz",
+      "integrity": "sha512-WKrxZPM9fPAEsM0xcTrOOJn86MbfOEzPwSwpO94Y7RtguWw+1nrvqYNzCcmVqO6zBi0BVMihoWxMKFIRzTOuZg==",
+      "dependencies": {
+        "@primeuix/utils": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primeuix/forms/node_modules/@primeuix/utils": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/@primeuix/utils/-/utils-0.4.1.tgz",
+      "integrity": "sha512-5+1NLfyna+gLRPeFTo+xlR0tfPVLuVdidbeahAMLkQga5Rw0LxyUBCyD2/Zv2JkV69o2T+hpEDyddl3VdnYoBw==",
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
     "node_modules/@primeuix/styled": {
       "version": "0.5.0",
       "resolved": "https://registry.npmmirror.com/@primeuix/styled/-/styled-0.5.0.tgz",
@@ -880,6 +903,53 @@
         "vue": "^3.5.0"
       }
     },
+    "node_modules/@primevue/forms": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmmirror.com/@primevue/forms/-/forms-4.3.5.tgz",
+      "integrity": "sha512-szMwme/1nCLnIdJDykkxFXtp4hVCxGiX8+EHZ18a0FAEQC6JzyhJ+mHh+S34nqMjtUXJntkAAFW4Jk3uxOqIBg==",
+      "dependencies": {
+        "@primeuix/forms": "^0.0.4",
+        "@primeuix/utils": "^0.5.3",
+        "@primevue/core": "4.3.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primevue/forms/node_modules/@primeuix/styled": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmmirror.com/@primeuix/styled/-/styled-0.6.4.tgz",
+      "integrity": "sha512-7ePLwqazLV0x269YlPMeE4wtQKT0NScY2/gEin0/96krTiGiElmlzKMMbH69bVApm/sfen5DZGuCEEwPiBJJ5g==",
+      "dependencies": {
+        "@primeuix/utils": "^0.5.3"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primevue/forms/node_modules/@primeuix/utils": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmmirror.com/@primeuix/utils/-/utils-0.5.4.tgz",
+      "integrity": "sha512-8LggV3Jz59pymHQD10e/u63z/GemQ22RBeu2Gb1eJgBYVwn1iOb82LR+daeAc/LxrXCC5pHnftnCmnZO6vInLA==",
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primevue/forms/node_modules/@primevue/core": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmmirror.com/@primevue/core/-/core-4.3.5.tgz",
+      "integrity": "sha512-YBlSr/EbXsnsTOyfgqmbrJQ7AI5EThaeGZvfDFjPIIEpokEK+Q32++9xPn3MH8rcM8zPsfMeBOWi4/OJkOqG4w==",
+      "dependencies": {
+        "@primeuix/styled": "^0.6.4",
+        "@primeuix/utils": "^0.5.3"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
     "node_modules/@primevue/icons": {
       "version": "4.3.3",
       "resolved": "https://registry.npmmirror.com/@primevue/icons/-/icons-4.3.3.tgz",
@@ -1026,6 +1096,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.21",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+      "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "5.2.3",
       "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
@@ -1294,6 +1369,41 @@
       "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
       "license": "MIT"
     },
+    "node_modules/@vueuse/core": {
+      "version": "13.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.3.0.tgz",
+      "integrity": "sha512-uYRz5oEfebHCoRhK4moXFM3NSCd5vu2XMLOq/Riz5FdqZMy2RvBtazdtL3gEcmDyqkztDe9ZP/zymObMIbiYSg==",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "13.3.0",
+        "@vueuse/shared": "13.3.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "13.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.3.0.tgz",
+      "integrity": "sha512-42IzJIOYCKIb0Yjv1JfaKpx8JlCiTmtCWrPxt7Ja6Wzoq0h79+YVXmBV03N966KEmDEESTbp5R/qO3AB5BDnGw==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "13.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.3.0.tgz",
+      "integrity": "sha512-L1QKsF0Eg9tiZSFXTgodYnu0Rsa2P0En2LuLrIs/jgrkyiDuJSsPZK+tx+wU0mMsYHUYEjNsuE41uqqkuR8VhA==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.14.1",
       "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
@@ -1357,6 +1467,21 @@
       "dev": true,
       "license": "Python-2.0"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
+      "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1440,6 +1565,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
@@ -1520,6 +1657,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@@ -1546,6 +1694,11 @@
         "url": "https://github.com/sponsors/mesqueeb"
       }
     },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1648,6 +1801,14 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/detect-libc": {
       "version": "2.0.3",
       "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -1657,6 +1818,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.5.128",
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
@@ -1712,6 +1886,47 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/esbuild": {
       "version": "0.25.2",
       "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.2.tgz",
@@ -2137,6 +2352,40 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz",
+      "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fs-extra": {
       "version": "11.3.0",
       "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz",
@@ -2165,6 +2414,14 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2175,6 +2432,41 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/get-stream": {
       "version": "9.0.1",
       "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz",
@@ -2218,6 +2510,17 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/graceful-fs": {
       "version": "4.2.11",
       "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2234,6 +2537,42 @@
         "node": ">=8"
       }
     },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/hookable": {
       "version": "5.5.3",
       "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -2286,6 +2625,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+    },
     "node_modules/import-fresh": {
       "version": "3.3.1",
       "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2313,6 +2657,11 @@
         "node": ">=0.8.19"
       }
     },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
     "node_modules/is-docker": {
       "version": "3.0.0",
       "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
@@ -2432,6 +2781,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
@@ -2528,6 +2882,17 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
     "node_modules/keyv": {
       "version": "4.5.4",
       "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@@ -2585,6 +2950,14 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
     "node_modules/lightningcss": {
       "version": "1.29.2",
       "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.29.2.tgz",
@@ -2707,6 +3080,14 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/mime": {
       "version": "1.6.0",
       "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
@@ -2720,6 +3101,25 @@
         "node": ">=4"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
@@ -2917,6 +3317,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
@@ -2952,12 +3357,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/particles.js": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmmirror.com/particles.js/-/particles.js-2.0.0.tgz",
-      "integrity": "sha512-8e0JIqkRbMMPlFBnF9f+92hX1s07jdkd3tqB8uHE9L+cwGGjIYjQM7QLgt0FQ5MZp6SFFYYDm/Y48pqK3ZvJOQ==",
-      "license": "MIT"
-    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
@@ -3175,6 +3574,16 @@
         "node": ">=12.11.0"
       }
     },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "node_modules/prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",
@@ -3192,6 +3601,20 @@
         "node": ">=6"
       }
     },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
     "node_modules/resolve-from": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3260,6 +3683,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3284,6 +3712,11 @@
         "semver": "bin/semver.js"
       }
     },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3363,6 +3796,14 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
     "node_modules/strip-final-newline": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
@@ -3579,7 +4020,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/vite": {
@@ -3885,6 +4325,14 @@
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
+    },
+    "node_modules/zod": {
+      "version": "3.25.64",
+      "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.64.tgz",
+      "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
     }
   }
 }

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "@vueuse/core": "^13.0.0",
     "axios": "^1.8.4",
     "chart.js": "^4.4.8",
+    "jszip": "^3.10.1",
     "less": "^4.2.2",
     "pinia": "^3.0.1",
     "primeflex": "^4.0.0",

+ 5 - 0
src/router/index.js

@@ -34,6 +34,11 @@ const router = createRouter({
           path: 'records',
           name: 'records',
           component: () => import('@/views/RecordsView.vue')
+        },
+        {
+          path: 'chat-records',
+          name: 'chat-records',
+          component: () => import('@/views/ChatRecordsView.vue')
         }
       ]
     },

+ 8 - 0
src/services/api.js

@@ -166,3 +166,11 @@ export const uploadFile = async (file) => {
   })
   return response.data
 }
+
+// 文件下载API
+export const downloadFile = async (key) => {
+  const response = await api.post('/files/download', { key }, {
+    responseType: 'blob'
+  })
+  return response.data
+}

+ 662 - 0
src/views/ChatRecordsView.vue

@@ -0,0 +1,662 @@
+<script setup>
+import { ref, nextTick } from 'vue'
+import { useToast } from 'primevue/usetoast'
+import JSZip from 'jszip'
+
+const toast = useToast()
+
+// 响应式数据
+const exportData = ref(null)
+const exportInfo = ref(null)
+const selectedChatId = ref(null)
+const exportSummaryData = ref(null)
+const isLoading = ref(false)
+const isDragOver = ref(false)
+const zipData = ref(null)
+
+// 拖拽区域引用
+const dropZoneRef = ref()
+
+// 解析日期字符串为Date对象
+const parseDate = (dateStr) => {
+  try {
+    // 处理形如 "2025/6/10 17:37:21" 的日期格式
+    const parts = dateStr.split(' ')
+    if (parts.length !== 2) return new Date(0)
+    
+    const dateParts = parts[0].split('/')
+    const timeParts = parts[1].split(':')
+    
+    if (dateParts.length !== 3 || timeParts.length !== 3) return new Date(0)
+    
+    const year = parseInt(dateParts[0])
+    const month = parseInt(dateParts[1]) - 1 // 月份从0开始
+    const day = parseInt(dateParts[2])
+    const hour = parseInt(timeParts[0])
+    const minute = parseInt(timeParts[1])
+    const second = parseInt(timeParts[2])
+    
+    // 检查是否为有效日期值
+    if (isNaN(year) || isNaN(month) || isNaN(day) || 
+        isNaN(hour) || isNaN(minute) || isNaN(second)) {
+      return new Date(0)
+    }
+    
+    return new Date(year, month, day, hour, minute, second)
+  } catch (e) {
+    console.error('日期解析错误:', e, dateStr)
+    return new Date(0)
+  }
+}
+
+// 创建导出摘要数据
+const createExportSummary = () => {
+  if (!exportInfo.value) return
+  
+  const { user, stats, date } = exportInfo.value
+  
+  // 创建导出摘要数据对象
+  exportSummaryData.value = {
+    name: "导出摘要",
+    messages: [
+      {
+        id: 1,
+        title: "Telegram 聊天记录导出",
+        exportTime: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
+        userName: user.firstName + ' ' + user.lastName,
+        userHandle: user.username,
+        userId: user.id,
+        dialogCount: stats.dialogs,
+        imageCount: stats.images,
+        mediaDownload: stats.skipMediaDownload ? "未下载" : "已下载",
+        text: "",
+        mediaType: "None",
+        date: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
+        imagePath: null
+      }
+    ]
+  }
+}
+
+// 处理文件拖拽
+const handleDragOver = (e) => {
+  e.preventDefault()
+  isDragOver.value = true
+}
+
+const handleDragLeave = (e) => {
+  e.preventDefault()
+  isDragOver.value = false
+}
+
+const handleDrop = async (e) => {
+  e.preventDefault()
+  isDragOver.value = false
+  
+  const files = e.dataTransfer?.files
+  if (!files || files.length === 0) return
+  
+  const file = files[0]
+  if (!file.name.endsWith('.zip')) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '请拖拽ZIP格式的文件',
+      life: 3000
+    })
+    return
+  }
+  
+  await processZipFile(file)
+}
+
+// 处理ZIP文件
+const processZipFile = async (file) => {
+  isLoading.value = true
+  
+  try {
+    const zip = new JSZip()
+    const zipDataResult = await zip.loadAsync(file)
+    zipData.value = zipDataResult // 保存ZIP数据引用
+    
+    // 查找并解析export_info.json
+    const exportInfoFile = zipDataResult.file('export_info.json')
+    if (!exportInfoFile) {
+      throw new Error('未找到export_info.json文件')
+    }
+    
+    const exportInfoContent = await exportInfoFile.async('string')
+    exportInfo.value = JSON.parse(exportInfoContent)
+    
+    // 查找并解析telegram_export.json
+    const exportDataFile = zipDataResult.file('telegram_export.json')
+    if (!exportDataFile) {
+      throw new Error('未找到telegram_export.json文件')
+    }
+    
+    const exportDataContent = await exportDataFile.async('string')
+    exportData.value = JSON.parse(exportDataContent)
+    
+    // 创建导出摘要
+    createExportSummary()
+    
+    // 默认选择导出摘要会话
+    selectChat('export_summary')
+    
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '文件解析成功',
+      life: 3000
+    })
+    
+  } catch (error) {
+    console.error('解析文件失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error instanceof Error ? error.message : '文件解析失败',
+      life: 3000
+    })
+  } finally {
+    isLoading.value = false
+  }
+}
+
+// 选择聊天
+const selectChat = async (chatId) => {
+  selectedChatId.value = chatId
+  // 等待DOM更新后处理图片
+  await handleImagesAfterUpdate()
+}
+
+// 获取聊天列表
+const getChatList = () => {
+  if (!exportData.value || !exportSummaryData.value) return []
+  
+  const chats = []
+  
+  // 添加导出摘要作为第一个会话
+  chats.push({
+    chatId: 'export_summary',
+    name: exportSummaryData.value.name,
+    messageCount: exportSummaryData.value.messages.length,
+    isSummary: true
+  })
+  
+  // 按最新消息时间排序聊天,过滤掉export_summary避免重复
+  const sortedChats = Object.entries(exportData.value)
+    .filter(([chatId]) => chatId !== 'export_summary') // 过滤掉export_summary
+    .map(([chatId, chat]) => {
+      const latestMessage = chat.messages.length > 0 ? chat.messages[0] : null
+      const latestDate = latestMessage ? parseDate(latestMessage.date) : new Date(0)
+      return { chatId, chat, latestDate }
+    })
+    .sort((a, b) => b.latestDate.getTime() - a.latestDate.getTime())
+  
+  sortedChats.forEach(({ chatId, chat }) => {
+    chats.push({
+      chatId,
+      name: chat.name,
+      messageCount: chat.messages.length,
+      isSummary: false
+    })
+  })
+  
+  return chats
+}
+
+// 获取当前选中的聊天
+const getCurrentChat = () => {
+  if (selectedChatId.value === 'export_summary') {
+    return exportSummaryData.value
+  }
+  return exportData.value && selectedChatId.value ? exportData.value[selectedChatId.value] : null
+}
+
+// 获取当前聊天标题
+const getCurrentChatTitle = () => {
+  const chat = getCurrentChat()
+  return chat?.name || '聊天记录查看器'
+}
+
+// 获取当前聊天的消息
+const getCurrentMessages = () => {
+  const chat = getCurrentChat()
+  if (!chat) return []
+  
+  if (selectedChatId.value === 'export_summary') {
+    return chat.messages
+  }
+  
+  // 确保所有会话消息按日期排序(从旧到新)
+  return [...chat.messages].sort((a, b) => {
+    const dateA = parseDate(a.date)
+    const dateB = parseDate(b.date)
+    return dateA.getTime() - dateB.getTime()
+  })
+}
+
+// HTML转义函数,防止XSS
+const escapeHtml = (html) => {
+  const div = document.createElement('div')
+  div.textContent = html
+  return div.innerHTML
+}
+
+// 获取图片的base64数据
+const getImageBase64 = async (imagePath) => {
+  if (!zipData.value || !imagePath) return null
+  
+  try {
+    const imageFile = zipData.value.file(imagePath)
+    if (!imageFile) {
+      console.warn('图片文件不存在:', imagePath)
+      return null
+    }
+    
+    const imageData = await imageFile.async('base64')
+    return imageData
+  } catch (error) {
+    console.error('获取图片失败:', error, imagePath)
+    return null
+  }
+}
+
+// 处理消息内容(同步版本,用于模板)
+const processMessageContent = (message) => {
+  let content = ''
+  
+  // 确保有时间显示,如果没有date字段则显示当前时间或默认值
+  const messageTime = message.date || '未知时间'
+  
+  if (message.mediaType === 'Photo' && message.imagePath) {
+    content += `
+      <div class="message-text">${escapeHtml(message.text)}</div>
+      <div class="message-image-container">
+        <img class="message-image" data-image-path="${message.imagePath}" alt="图片" loading="lazy" @error="handleImageError">
+      </div>
+      <div class="message-time">${messageTime}</div>
+    `
+  } else if (message.mediaType === 'WebPage' && message.text) {
+    // 尝试提取URL
+    const urlMatch = message.text.match(/https?:\/\/[^\s]+/)
+    const url = urlMatch ? urlMatch[0] : message.text
+    
+    content += `
+      <div class="message-text">${escapeHtml(message.text)}</div>
+      <div class="message-web-page">
+        <a href="${url}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>
+      </div>
+      <div class="message-time">${messageTime}</div>
+    `
+  } else {
+    content += `
+      <div class="message-text">${escapeHtml(message.text)}</div>
+      <div class="message-time">${messageTime}</div>
+    `
+  }
+  
+  return content
+}
+
+// 处理图片错误
+const handleImageError = (event) => {
+  const img = event.target
+  img.style.display = 'none'
+  const errorDiv = document.createElement('div')
+  errorDiv.className = 'message-image-error'
+  errorDiv.textContent = '图片加载失败'
+  img.parentNode.appendChild(errorDiv)
+}
+
+// 在DOM更新后处理图片加载
+const handleImagesAfterUpdate = async () => {
+  await nextTick()
+  const images = document.querySelectorAll('.message-image[data-image-path]')
+  
+  for (const img of images) {
+    const imagePath = img.dataset.imagePath
+    if (imagePath && !img.src.includes('data:image')) {
+      try {
+        const imageBase64 = await getImageBase64(imagePath)
+        if (imageBase64) {
+          img.src = `data:image/jpeg;base64,${imageBase64}`
+        } else {
+          img.style.display = 'none'
+          const errorDiv = document.createElement('div')
+          errorDiv.className = 'message-image-error'
+          errorDiv.textContent = '图片加载失败'
+          img.parentNode.appendChild(errorDiv)
+        }
+      } catch (error) {
+        console.error('图片加载失败:', error)
+        img.style.display = 'none'
+        const errorDiv = document.createElement('div')
+        errorDiv.className = 'message-image-error'
+        errorDiv.textContent = '图片加载失败'
+        img.parentNode.appendChild(errorDiv)
+      }
+    }
+  }
+}
+</script>
+
+<template>
+  <div class="chat-records-container">
+    <!-- 拖拽区域 -->
+    <div 
+      v-if="!exportData"
+      ref="dropZoneRef"
+      class="drop-zone"
+      :class="{ 'drag-over': isDragOver }"
+      @dragover="handleDragOver"
+      @dragleave="handleDragLeave"
+      @drop="handleDrop"
+    >
+      <div class="drop-zone-content">
+        <i class="pi pi-upload text-6xl mb-4 text-gray-400"></i>
+        <h2 class="text-2xl font-bold mb-2">拖拽ZIP文件到此处</h2>
+        <p class="text-gray-500">自动解析并展示</p>
+        <div v-if="isLoading" class="mt-4">
+          <i class="pi pi-spin pi-spinner text-2xl"></i>
+          <span class="ml-2">正在解析文件...</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 聊天记录查看器 -->
+    <div v-else class="chat-viewer">
+      <div class="chat-viewer-header">
+        <h1 class="text-2xl font-bold">{{ getCurrentChatTitle() }}</h1>
+      </div>
+      
+      <div class="chat-viewer-content">
+        <!-- 侧边栏 - 聊天列表 -->
+        <div class="chat-sidebar">
+          <div class="chat-list">
+            <div
+              v-for="chat in getChatList()"
+              :key="chat.chatId"
+              class="chat-item"
+              :class="{ 'active': selectedChatId === chat.chatId }"
+              @click="selectChat(chat.chatId)"
+            >
+              <div class="chat-item-name">{{ chat.name }}</div>
+              <div class="chat-item-info">
+                <span class="chat-item-message-count">{{ chat.messageCount }}条消息</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 主内容区 - 消息列表 -->
+        <div class="chat-main">
+          <div class="messages-container">
+            <!-- 导出摘要消息 -->
+            <div v-if="selectedChatId === 'export_summary'" class="export-summary">
+              <div 
+                v-for="message in getCurrentMessages()" 
+                :key="message.id"
+                class="message message-incoming export-summary-message"
+              >
+                <div class="summary-item">
+                  <span class="summary-item-icon">📁</span>
+                  <span>{{ message.title }}</span>
+                </div>
+                <div class="summary-item">
+                  <span class="summary-item-icon">👤</span>
+                  <span>{{ message.userName }} (@{{ message.userHandle }})</span>
+                </div>
+                <div class="summary-item">
+                  <span class="summary-item-icon">🕒</span>
+                  <span>导出时间: {{ message.exportTime }}</span>
+                </div>
+                <div class="summary-item">
+                  <span class="summary-item-icon">💬</span>
+                  <span>会话数: {{ message.dialogCount }}</span>
+                </div>
+                <div class="summary-item">
+                  <span class="summary-item-icon">🖼️</span>
+                  <span>图片数: {{ message.imageCount }}</span>
+                </div>
+                <div class="summary-item">
+                  <span class="summary-item-icon">💾</span>
+                  <span>媒体文件: {{ message.mediaDownload }}</span>
+                </div>
+                <div class="message-time">{{ message.date }}</div>
+              </div>
+            </div>
+
+            <!-- 普通聊天消息 -->
+            <div v-else class="messages">
+              <div 
+                v-for="message in getCurrentMessages()" 
+                :key="message.id"
+                class="message message-incoming"
+                v-html="processMessageContent(message)"
+              >
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.chat-records-container {
+  width: 100%;
+  height: calc(100vh - 120px);
+  max-width: 100%;
+}
+
+/* 拖拽区域样式 */
+.drop-zone {
+  width: 100%;
+  height: 100%;
+  border: 2px dashed #d1d5db;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+  background-color: #f9fafb;
+}
+
+.drop-zone.drag-over {
+  border-color: #3b82f6;
+  background-color: #eff6ff;
+}
+
+.drop-zone-content {
+  text-align: center;
+  padding: 2rem;
+}
+
+/* 聊天查看器样式 */
+.chat-viewer {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-viewer-header {
+  padding: 1rem;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #f9fafb;
+}
+
+.chat-viewer-content {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+
+/* 侧边栏样式 */
+.chat-sidebar {
+  width: 300px;
+  border-right: 1px solid #e5e7eb;
+  background-color: #f9fafb;
+  overflow-y: auto;
+}
+
+.chat-list {
+  padding: 0.5rem;
+}
+
+.chat-item {
+  padding: 1rem;
+  margin-bottom: 0.5rem;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+  background-color: white;
+  border: 1px solid #e5e7eb;
+}
+
+.chat-item:hover {
+  background-color: #f3f4f6;
+}
+
+.chat-item.active {
+  background-color: #3b82f6;
+  color: white;
+}
+
+.chat-item-name {
+  font-weight: 600;
+  margin-bottom: 0.25rem;
+}
+
+.chat-item-info {
+  font-size: 0.875rem;
+  opacity: 0.8;
+}
+
+/* 主内容区样式 */
+.chat-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.messages-container {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1rem;
+}
+
+/* 消息样式 */
+.message {
+  margin-bottom: 1rem;
+  padding: 1rem;
+  border-radius: 8px;
+  background-color: #f3f4f6;
+  max-width: 80%;
+}
+
+.message-incoming {
+  margin-right: auto;
+}
+
+.message-text {
+  margin-bottom: 0.5rem;
+  line-height: 1.5;
+  word-wrap: break-word;
+}
+
+.message-time {
+  font-size: 0.65rem !important;
+  color: #dc2626 !important;
+  margin-top: 1rem !important;
+  text-align: right !important;
+  font-style: italic !important;
+  opacity: 0.9 !important;
+}
+
+/* 导出摘要样式 */
+.export-summary-message {
+  background-color: #dbeafe;
+  border: 1px solid #93c5fd;
+}
+
+.summary-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 0.5rem;
+}
+
+.summary-item-icon {
+  margin-right: 0.5rem;
+  font-size: 1.2em;
+}
+
+/* 图片消息样式 */
+.message-image-container {
+  margin-top: 0.5rem;
+}
+
+.message-image {
+  max-width: 100%;
+  max-height: 300px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: transform 0.2s ease;
+}
+
+.message-image:hover {
+  transform: scale(1.02);
+}
+
+/* 图片错误样式 */
+.message-image-error {
+  margin-top: 0.5rem;
+  padding: 0.5rem;
+  background-color: #fef2f2;
+  border: 1px solid #fecaca;
+  border-radius: 4px;
+  color: #dc2626;
+  font-size: 0.875rem;
+  text-align: center;
+}
+
+/* 网页链接样式 */
+.message-web-page {
+  margin-top: 0.5rem;
+  padding: 0.5rem;
+  background-color: #f0f9ff;
+  border-radius: 4px;
+  border: 1px solid #bae6fd;
+}
+
+.message-web-page a {
+  color: #0369a1;
+  text-decoration: none;
+  word-break: break-all;
+}
+
+.message-web-page a:hover {
+  text-decoration: underline;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .chat-viewer-content {
+    flex-direction: column;
+  }
+  
+  .chat-sidebar {
+    width: 100%;
+    height: 200px;
+    border-right: none;
+    border-bottom: 1px solid #e5e7eb;
+  }
+  
+  .message {
+    max-width: 95%;
+  }
+}
+</style> 

+ 5 - 0
src/views/MainView.vue

@@ -37,6 +37,11 @@ const navItems = [
     icon: 'pi pi-fw pi-list',
     name: 'records'
   },
+  {
+    label: '聊天展示',
+    icon: 'pi pi-fw pi-comments',
+    name: 'chat-records'
+  },
   {
     label: '用户管理',
     icon: 'pi pi-fw pi-user',

+ 105 - 34
src/views/RecordsView.vue

@@ -1,7 +1,8 @@
 <script setup>
-import { createRecord, deleteRecord, getRecordById, listRecords, updateRecord, uploadFile } from '@/services/api'
+import { createRecord, deleteRecord, getRecordById, listRecords, updateRecord, uploadFile, downloadFile } from '@/services/api'
 import { Form } from '@primevue/forms'
 import { zodResolver } from '@primevue/forms/resolvers/zod'
+import { useUserStore } from '@/stores/user'
 import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
@@ -280,6 +281,91 @@ const copyUrl = (url) => {
   })
 }
 
+// 从URL中提取OSS key
+const extractKeyFromUrl = (url) => {
+  try {
+    // 假设URL格式为: https://bucket.oss-region.aliyuncs.com/path/to/file
+    const urlObj = new URL(url)
+    // 移除开头的斜杠
+    return urlObj.pathname.substring(1)
+  } catch (e) {
+    console.error('无法解析URL:', url)
+    return null
+  }
+}
+
+// 从描述中提取文件名
+const extractFileNameFromDescription = (description) => {
+  if (!description) return 'download'
+  
+  try {
+    // 尝试解析JSON
+    const parsed = JSON.parse(description)
+    if (typeof parsed === 'object' && parsed !== null) {
+      // 提取指定字段
+      const userName = parsed.userName || ''
+      const userId = parsed.userId || ''
+      
+      // 如果两个字段都有值,使用 userId_@userName 格式
+      if (userId && userName) {
+        return `${userId}_@${userName}`
+      }
+      // 如果只有userId,使用userId
+      else if (userId) {
+        return userId
+      }
+      // 如果只有userName,使用userName
+      else if (userName) {
+        return userName
+      }
+    }
+  } catch (e) {
+    // 如果不是JSON,返回原描述(去除特殊字符)
+    return description.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50)
+  }
+  
+  return 'download'
+}
+
+// 下载文件
+const handleFileDownload = async (url, description) => {
+  try {
+    const key = extractKeyFromUrl(url)
+    if (!key) {
+      throw new Error('无法解析文件路径')
+    }
+    
+    // 从描述中提取文件名
+    const fileName = extractFileNameFromDescription(description)
+    
+    // 通过API服务下载文件
+    const blob = await downloadFile(key)
+    
+    const downloadUrl = window.URL.createObjectURL(blob)
+    const a = document.createElement('a')
+    a.href = downloadUrl
+    a.download = fileName
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+    window.URL.revokeObjectURL(downloadUrl)
+    
+    toast.add({
+      severity: 'success',
+      summary: '下载成功',
+      detail: '文件下载完成',
+      life: 3000
+    })
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '下载失败',
+      detail: error.message || '文件下载失败',
+      life: 3000
+    })
+  }
+}
+
 // 文件上传处理函数
 const handleFileUpload = async (event) => {
   const file = event.target.files[0]
@@ -365,39 +451,16 @@ onMounted(() => {
         </div>
       </template>
 
-      <Column field="id" header="ID" style="width: 80px">
+      <Column field="id" header="ID" >
         <template #body="slotProps">
           <span class="font-mono text-sm">{{ slotProps.data.id }}</span>
         </template>
       </Column>
 
-      <Column field="url" header="URL" style="min-width: 500px">
-        <template #body="slotProps">
-          <div class="flex items-center gap-2">
-            <a
-              :href="slotProps.data.url"
-              target="_blank"
-              class="text-blue-600 hover:text-blue-800 underline truncate max-w-xs"
-              :title="slotProps.data.url"
-            >
-              {{ formatUrl(slotProps.data.url) }}
-            </a>
-            <Button
-              icon="pi pi-copy"
-              size="small"
-              text
-              rounded
-              @click="copyUrl(slotProps.data.url)"
-              :title="'复制URL'"
-            />
-          </div>
-        </template>
-      </Column>
-
-      <Column field="description" header="描述" style="min-width: 200px">
+      <Column field="description" header="描述" style="min-width: 300px; max-width: 400px">
         <template #body="slotProps">
           <div 
-            class="max-w-xs whitespace-pre-line text-sm" 
+            class="whitespace-pre-line text-sm" 
             :title="formatDescription(slotProps.data.description)"
             style="max-height: 100px; overflow-y: auto;"
           >
@@ -406,15 +469,17 @@ onMounted(() => {
         </template>
       </Column>
 
-      <Column field="createdAt" header="创建时间" style="min-width: 200px">
-        <template #body="slotProps">
-          {{ formatDate(slotProps.data.createdAt) }}
-        </template>
-      </Column>
-
-      <Column header="操作" style="min-width: 150px">
+      <Column header="操作" style="width: 400px">
         <template #body="slotProps">
           <div class="flex gap-1">
+            <Button
+              icon="pi pi-download"
+              size="small"
+              text
+              rounded
+              @click="handleFileDownload(slotProps.data.url, slotProps.data.description)"
+              :title="'下载文件'"
+            />
             <Button
               icon="pi pi-pencil"
               severity="info"
@@ -436,6 +501,12 @@ onMounted(() => {
           </div>
         </template>
       </Column>
+
+      <Column field="createdAt" header="创建时间" style="width: 160px">
+        <template #body="slotProps">
+          <span class="text-sm">{{ formatDate(slotProps.data.createdAt) }}</span>
+        </template>
+      </Column>
     </DataTable>
 
     <!-- 记录表单对话框 -->

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 172 - 379
yarn.lock


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است