Bladeren bron

feat: 前端项目初始化,Vue3 + Vite + Element Plus,移除登录认证功能

Hugvvn 1 maand geleden
commit
0615fbedec

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+node_modules/
+dist/
+.env.local
+.env.*.local

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM nginx:alpine
+
+# 复制 nginx 配置
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# 复制前端构建产物
+COPY dist/ /usr/share/nginx/html/
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]

+ 7 - 0
env.d.ts

@@ -0,0 +1,7 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<object, object, any>
+  export default component
+}

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>多维度检索</title>
+</head>
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+</html>

+ 21 - 0
nginx.conf

@@ -0,0 +1,21 @@
+server {
+    listen 80;
+    server_name localhost;
+
+    # 前端静态文件
+    location / {
+        root /usr/share/nginx/html;
+        try_files $uri $uri/ /index.html;
+    }
+
+    # API 代理到后端
+    location /api/ {
+        proxy_pass http://backend:8000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_read_timeout 300s;
+    }
+}

+ 1721 - 0
package-lock.json

@@ -0,0 +1,1721 @@
+{
+  "name": "lqmd-retrieval-web",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "lqmd-retrieval-web",
+      "version": "1.0.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.3.1",
+        "element-plus": "^2.4.4",
+        "katex": "^0.17.0",
+        "markdown-it": "^14.2.0",
+        "vue": "^3.4.0",
+        "vue-router": "^4.2.5"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^4.5.2",
+        "typescript": "^5.3.3",
+        "vite": "^5.0.8",
+        "vue-tsc": "^1.8.27"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.3",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz",
+      "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+      "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz",
+      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz",
+      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.5",
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz",
+      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+      "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+      "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+      "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+      "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+      "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+      "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+      "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+      "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+      "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+      "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+      "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+      "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+      "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+      "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+      "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+      "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+      "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+      "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+      "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+      "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+      "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+      "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+      "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.24",
+      "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
+      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+      "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.0.0 || ^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz",
+      "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "1.11.1"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz",
+      "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "muggle-string": "^0.3.1"
+      }
+    },
+    "node_modules/@volar/typescript": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz",
+      "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "1.11.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
+      "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/shared": "3.5.34",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz",
+      "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz",
+      "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/compiler-core": "3.5.34",
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.14",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz",
+      "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
+      "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "~1.11.1",
+        "@volar/source-map": "~1.11.1",
+        "@vue/compiler-dom": "^3.3.0",
+        "@vue/shared": "^3.3.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.3.1",
+        "path-browserify": "^1.0.1",
+        "vue-template-compiler": "^2.7.14"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz",
+      "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz",
+      "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz",
+      "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/runtime-core": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz",
+      "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "vue": "3.5.34"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz",
+      "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz",
+      "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "14.3.0",
+        "@vueuse/shared": "14.3.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz",
+      "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz",
+      "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz",
+      "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/element-plus": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.0.tgz",
+      "integrity": "sha512-POgH+TtoreaEKWqYYAVQyE6i8rQMEFqAEublyF29dBA5yASWPLKY6EzfeqBTr2Uv26mPss4vSrMrNPyaK7LX5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+        "@types/lodash": "^4.17.24",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "14.3.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.20",
+        "lodash": "^4.18.1",
+        "lodash-es": "^4.18.1",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.2.8"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.7"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/katex": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmmirror.com/katex/-/katex-0.17.0.tgz",
+      "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
+      "funding": [
+        "https://opencollective.com/katex",
+        "https://github.com/sponsors/katex"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^8.3.0"
+      },
+      "bin": {
+        "katex": "cli.js"
+      }
+    },
+    "node_modules/linkify-it": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.1.tgz",
+      "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/markdown-it"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/markdown-it": {
+      "version": "14.2.0",
+      "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.2.0.tgz",
+      "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/markdown-it"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.1",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
+    "node_modules/markdown-it/node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "license": "MIT"
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/muggle-string": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz",
+      "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.12",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz",
+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.5.15",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
+      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.12",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz",
+      "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.4",
+        "@rollup/rollup-android-arm64": "4.60.4",
+        "@rollup/rollup-darwin-arm64": "4.60.4",
+        "@rollup/rollup-darwin-x64": "4.60.4",
+        "@rollup/rollup-freebsd-arm64": "4.60.4",
+        "@rollup/rollup-freebsd-x64": "4.60.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+        "@rollup/rollup-linux-arm64-musl": "4.60.4",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+        "@rollup/rollup-linux-loong64-musl": "4.60.4",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-musl": "4.60.4",
+        "@rollup/rollup-openbsd-x64": "4.60.4",
+        "@rollup/rollup-openharmony-arm64": "4.60.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+        "@rollup/rollup-win32-x64-gnu": "4.60.4",
+        "@rollup/rollup-win32-x64-msvc": "4.60.4",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz",
+      "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz",
+      "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-sfc": "3.5.34",
+        "@vue/runtime-dom": "3.5.34",
+        "@vue/server-renderer": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-component-type-helpers": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.1.tgz",
+      "integrity": "sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==",
+      "license": "MIT"
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-template-compiler": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
+      "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz",
+      "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~1.11.1",
+        "@vue/language-core": "1.8.27",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      }
+    }
+  }
+}

+ 25 - 0
package.json

@@ -0,0 +1,25 @@
+{
+  "name": "lqmd-retrieval-web",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "element-plus": "^2.4.4",
+    "katex": "^0.17.0",
+    "markdown-it": "^14.2.0",
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.5.2",
+    "typescript": "^5.3.3",
+    "vite": "^5.0.8",
+    "vue-tsc": "^1.8.27"
+  }
+}

+ 3 - 0
src/App.vue

@@ -0,0 +1,3 @@
+<template>
+  <router-view />
+</template>

+ 71 - 0
src/api/retrieval.ts

@@ -0,0 +1,71 @@
+import { api } from '@/utils/request'
+
+export interface SearchParams {
+  user_id: string
+  query: string
+  file_name?: string
+  standard_number?: string
+  issuing_authority?: string
+  document_type?: string
+  professional_field?: string
+  start_time?: string
+  end_time?: string
+  page?: number
+  page_size?: number
+}
+
+export interface SearchResult {
+  pk: number
+  distance: number
+  text: string
+  highlighted_text: string
+  parent_id: number
+  metadata: {
+    file_name: string
+    chinese_name: string
+    standard_number: string
+    issuing_authority: string | null
+    document_type: string
+    professional_field: string
+    hierarchy: string
+    file_url: string
+    plan_type_list: Record<string, any>
+  }
+  created_time: string
+  rank: number
+}
+
+export interface SearchResponse {
+  objs: SearchResult[]
+  page: number
+  page_size: number
+}
+
+export function multidimensionalSearch(data: SearchParams) {
+  return api.post('/retrieval/multidimensional_search', data)
+}
+
+export function getIssuingAuthorities(keyword?: string) {
+  const url = keyword
+    ? `/retrieval/authorities?keyword=${encodeURIComponent(keyword)}`
+    : '/retrieval/authorities'
+  return api.get(url)
+}
+
+export function getSearchLogs(params: {
+  user_id: string
+  status?: string
+  query_keyword?: string
+  start_time?: string
+  end_time?: string
+  page?: number
+  page_size?: number
+  sort_by?: string
+  sort_order?: string
+}) {
+  const query = new URLSearchParams()
+  Object.entries(params).forEach(([k, v]) => {
+    if (v !== undefined && v !== null && v !== '') query.set(k, String(v))
+  })
+  return api.get(`/retrieval/search_logs?${query.toString()}`)
+}

+ 427 - 0
src/components/FilterPanel.vue

@@ -0,0 +1,427 @@
+<template>
+  <div class="filter-panel">
+    <div class="filter-row">
+      <span class="filter-label">文档类型</span>
+      <div class="filter-tags">
+        <span
+          v-for="t in docTypes"
+          :key="t"
+          class="filter-tag"
+          :class="{ active: docType === t }"
+          @click="selectDocType(t)"
+        >{{ t }}</span>
+      </div>
+    </div>
+    <div class="filter-row">
+      <span class="filter-label">发布日期</span>
+      <div class="filter-content">
+        <el-date-picker
+          :model-value="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="年 /月 /日"
+          end-placeholder="年 /月 /日"
+          size="default"
+          value-format="YYYY-MM-DD"
+          @update:model-value="onDateRangeChange"
+          class="date-picker"
+        />
+      </div>
+    </div>
+    <div class="filter-row">
+      <span class="filter-label">发布单位</span>
+      <!-- 按钮模式 -->
+      <div v-if="!showSearch" class="filter-tags">
+        <span
+          class="filter-tag"
+          :class="{ active: issuingAuthority === '全部' }"
+          @click="selectAuthority({ shortName: '全部', fullName: '全部', count: 0 })"
+        >全部</span>
+        <span
+          v-for="d in topAuthorities"
+          :key="d.fullName"
+          class="filter-tag"
+          :class="{ active: issuingAuthority === d.fullName }"
+          :title="d.fullName"
+          @click="selectAuthority(d)"
+        >{{ d.shortName }}</span>
+        <span
+          v-if="!loadingAuthorities"
+          class="filter-tag more-tag"
+          @click="toggleSearch"
+        >更多</span>
+        <span v-else class="filter-tag more-tag" style="cursor: default; opacity: 0.5;">加载中...</span>
+      </div>
+      <!-- 搜索模式 -->
+      <div v-else class="filter-content">
+        <div class="search-inline">
+          <el-input
+            v-model="searchInput"
+            placeholder="输入发布单位名称,如发改委"
+            size="default"
+            clearable
+            prefix-icon="Search"
+            class="authority-search"
+            @input="onSearchInput"
+          />
+          <span class="collapse-btn" @click="toggleSearch">收起</span>
+        </div>
+        <div v-if="searchResults.length > 0" class="search-results">
+          <div
+            v-for="item in searchResults"
+            :key="item.fullName"
+            class="search-result-item"
+            :class="{ active: issuingAuthority === item.fullName }"
+            @click="selectAuthority(item)"
+          >
+            <span class="result-short">{{ item.shortName }}</span>
+            <span class="result-full">{{ item.fullName }}</span>
+          </div>
+        </div>
+        <div v-if="hasSearched && searchInput && searchResults.length === 0" class="search-empty">
+          未找到匹配的发布单位
+        </div>
+      </div>
+    </div>
+    <div class="filter-row">
+      <span class="filter-label">关键词</span>
+      <div class="filter-content">
+        <el-input
+          :model-value="keyword"
+          placeholder="输入精准关键词进行检索"
+          size="default"
+          clearable
+          class="keyword-input"
+          @update:model-value="$emit('update:keyword', $event)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, computed } from 'vue'
+import { getIssuingAuthorities } from '@/api/retrieval'
+import { ElMessage } from 'element-plus'
+
+interface Authority {
+  shortName: string
+  fullName: string
+  count: number
+}
+
+const docTypes = ['全部', '法规制度', '技术标准', '操作指南', '通知公告', '工作报告']
+
+// 从数据库动态加载的发布单位列表(不含前5固定按钮)
+const allAuthorities = ref<Authority[]>([])
+const loadingAuthorities = ref(false)
+
+/** 生成简称:从全称中精简 */
+function makeShortName(fullName: string): string {
+  // 去掉"中华人民共和国"前缀
+  let name = fullName.replace(/^中华人民共和国/, '')
+  // 常见精简规则
+  name = name
+    .replace('国家', '')
+    .replace('委员会', '委')
+    .replace('管理', '')
+  return name
+}
+
+// 前5发布单位(用于按钮),取数量最多的
+const topAuthorities = computed(() => allAuthorities.value.slice(0, 5))
+
+async function loadAuthorities(keyword?: string) {
+  if (loadingAuthorities.value) return
+  loadingAuthorities.value = true
+  try {
+    const res: any = await getIssuingAuthorities(keyword)
+    if (res.code === '1' && res.data?.authorities) {
+      // 后端返回: { full_name, count }
+      allAuthorities.value = res.data.authorities.map((a: any) => ({
+        shortName: makeShortName(a.full_name),
+        fullName: a.full_name,
+        count: a.count,
+      }))
+    }
+  } catch (e: any) {
+    ElMessage.error('加载发布单位失败: ' + (e.message || '未知错误'))
+  } finally {
+    loadingAuthorities.value = false
+  }
+}
+
+// 页面加载时预加载发布单位
+onMounted(() => {
+  loadAuthorities()
+})
+
+const props = defineProps<{
+  docType: string
+  department: string
+  dateRange: [string, string] | null
+  keyword: string
+}>()
+
+const emit = defineEmits<{
+  'update:docType': [value: string]
+  'update:department': [value: string]
+  'update:dateRange': [value: [string, string] | null]
+  'update:keyword': [value: string]
+  change: [params: {
+    docType: string
+    department: string
+    publishDateFrom: string
+    publishDateTo: string
+    keyword: string
+  }]
+}>()
+
+// 发布单位状态
+const issuingAuthority = ref('全部')
+const showSearch = ref(false)
+const searchInput = ref('')
+const searchResults = ref<Authority[]>([])
+const hasSearched = ref(false)
+let debounceTimer: ReturnType<typeof setTimeout> | null = null
+let isSelectingFromList = false
+
+function selectDocType(type: string) {
+  emit('update:docType', type)
+  emitChange(type)
+}
+
+function selectAuthority(item: Authority) {
+  isSelectingFromList = true
+  hasSearched.value = false
+  if (debounceTimer) clearTimeout(debounceTimer)
+  issuingAuthority.value = item.fullName
+  searchInput.value = item.fullName
+  searchResults.value = []
+  emit('update:department', item.fullName)
+  emitChange(undefined, item.fullName)
+  setTimeout(() => {
+    isSelectingFromList = false
+  }, 50)
+}
+
+function toggleSearch() {
+  showSearch.value = !showSearch.value
+  if (!showSearch.value) {
+    searchInput.value = ''
+    searchResults.value = []
+    hasSearched.value = false
+  } else {
+    // 展开时如果没有数据,加载一次
+    if (allAuthorities.value.length === 0) {
+      loadAuthorities()
+    }
+  }
+}
+
+function onSearchInput(val: string) {
+  if (isSelectingFromList) return
+  hasSearched.value = val.trim() !== ''
+  if (debounceTimer) clearTimeout(debounceTimer)
+  debounceTimer = setTimeout(() => {
+    if (!val.trim()) {
+      searchResults.value = []
+      return
+    }
+    // 优先从已加载的列表中搜索
+    let results = allAuthorities.value.filter(a =>
+      a.fullName.includes(val.trim()) || a.shortName.includes(val.trim())
+    )
+    // 如果本地没搜到,调 API 搜索
+    if (results.length === 0 && allAuthorities.value.length > 0) {
+      loadAuthorities(val.trim()).then(() => {
+        results = allAuthorities.value.filter(a =>
+          a.fullName.includes(val.trim()) || a.shortName.includes(val.trim())
+        )
+        searchResults.value = results
+      })
+    } else {
+      searchResults.value = results
+    }
+  }, 300)
+}
+
+function onDateRangeChange(val: [string, string] | null) {
+  emit('update:dateRange', val)
+  emitChange(undefined, undefined, val)
+}
+
+function emitChange(
+  docType?: string,
+  department?: string,
+  dateRange?: [string, string] | null
+) {
+  emit('change', {
+    docType: docType ?? props.docType,
+    department: department ?? props.department,
+    publishDateFrom: (dateRange ?? props.dateRange)?.[0] || '',
+    publishDateTo: (dateRange ?? props.dateRange)?.[1] || '',
+    keyword: props.keyword,
+  })
+}
+</script>
+
+<style scoped>
+.filter-panel {
+  background: #fff;
+  border-radius: 12px;
+  padding: 24px 32px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+  margin-bottom: 8px;
+}
+.filter-row {
+  display: flex;
+  align-items: flex-start;
+  padding: 10px 0;
+  border-bottom: 1px dashed #f0f0f0;
+}
+.filter-row:last-child {
+  border-bottom: none;
+}
+.filter-label {
+  width: 80px;
+  flex-shrink: 0;
+  font-size: 14px;
+  color: #909399;
+  font-weight: 400;
+  margin-top: 6px;
+}
+.filter-content {
+  flex: 1;
+}
+.filter-tags {
+  flex: 1;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.filter-tag {
+  padding: 6px 18px;
+  border-radius: 4px;
+  font-size: 13px;
+  color: #606266;
+  background: #f5f7fa;
+  cursor: pointer;
+  transition: all 0.2s;
+  user-select: none;
+  border: 1px solid transparent;
+  min-width: 60px;
+  text-align: center;
+}
+.filter-tag:hover {
+  color: #f5a623;
+  background: #fff8f0;
+  border-color: #f5d9b0;
+}
+.filter-tag.active {
+  color: #fff;
+  background: #f5a623;
+  border-color: #f5a623;
+  box-shadow: 0 2px 8px rgba(245, 166, 35, 0.3);
+  font-weight: 500;
+}
+.more-tag {
+  color: #f5a623;
+  background: #fff8f0;
+  border-color: #f5d9b0;
+}
+.more-tag:hover {
+  color: #fff;
+  background: #f5a623;
+  border-color: #f5a623;
+}
+
+/* 搜索模式 */
+.search-inline {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.authority-search {
+  flex: 1;
+  max-width: 360px;
+}
+.collapse-btn {
+  font-size: 13px;
+  color: #f5a623;
+  cursor: pointer;
+  white-space: nowrap;
+  padding: 6px 14px;
+  border-radius: 4px;
+  border: 1px solid #f5d9b0;
+  background: #fff8f0;
+  transition: all 0.2s;
+}
+.collapse-btn:hover {
+  color: #fff;
+  background: #f5a623;
+  border-color: #f5a623;
+}
+.search-results {
+  margin-top: 4px;
+  max-height: 220px;
+  overflow-y: auto;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+.search-result-item {
+  padding: 8px 16px;
+  cursor: pointer;
+  transition: background 0.15s;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+.search-result-item:hover {
+  background: #f5f7fa;
+}
+.search-result-item.active {
+  background: #fff8f0;
+}
+.result-short {
+  font-size: 13px;
+  font-weight: 500;
+  min-width: 100px;
+  color: #303133;
+}
+.search-result-item.active .result-short {
+  color: #f5a623;
+}
+.result-full {
+  font-size: 12px;
+  color: #909399;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.search-empty {
+  padding: 16px;
+  text-align: center;
+  color: #c0c4cc;
+  font-size: 13px;
+}
+
+.date-picker {
+  width: 100%;
+  max-width: 460px;
+}
+.date-picker :deep(.el-input__wrapper) {
+  border-radius: 4px;
+  box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+.keyword-input {
+  width: 320px;
+  max-width: 100%;
+}
+.keyword-input :deep(.el-input__wrapper) {
+  border-radius: 4px;
+  box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+</style>

+ 39 - 0
src/components/Pagination.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="pagination-wrapper">
+    <el-pagination
+      v-model:current-page="page"
+      v-model:page-size="pageSize"
+      :page-sizes="[10, 20, 50, 100]"
+      :total="total"
+      layout="total, sizes, prev, pager, next"
+      @current-change="emit('change', page, pageSize)"
+      @size-change="emit('change', 1, pageSize)"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+const props = defineProps<{
+  total: number
+  currentPage: number
+  pageSize: number
+}>()
+
+const emit = defineEmits<{ change: [page: number, pageSize: number] }>()
+
+const page = ref(props.currentPage)
+const pageSize = ref(props.pageSize)
+
+watch(() => props.currentPage, (v) => { page.value = v })
+watch(() => props.pageSize, (v) => { pageSize.value = v })
+</script>
+
+<style scoped>
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+}
+</style>

+ 235 - 0
src/components/ResultCard.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="result-card">
+    <h3 class="result-title" @click.stop="$emit('fragment', item)">
+      <span class="title-text">{{ item.title }}</span>
+    </h3>
+    <div class="result-meta">
+      <el-tag v-if="item.doc_type && item.doc_type !== '-'" size="small" class="meta-tag">
+        {{ item.doc_type }}
+      </el-tag>
+      <el-tag v-if="item.department && item.department !== '-'" size="small" class="meta-tag dept-tag">
+        {{ item.department }}
+      </el-tag>
+      <span v-if="item.publish_date" class="meta-info">
+        <el-icon><Calendar /></el-icon>
+        {{ formatDate(item.publish_date) }}
+      </span>
+      <span v-if="item.view_count !== undefined" class="meta-info">
+        <el-icon><View /></el-icon>
+        {{ item.view_count }} 次浏览
+      </span>
+    </div>
+    <p
+      class="result-abstract"
+      v-html="renderMath(truncateText(item.highlighted_text || item.abstract || '', 300))"
+      @click.stop="$emit('fragment', item)"
+    ></p>
+    <div class="result-tags" v-if="item.tags && item.tags.length">
+      <el-tag v-for="tag in item.tags" :key="tag" size="small" class="custom-tag">
+        {{ tag }}
+      </el-tag>
+    </div>
+    <div class="result-actions">
+      <el-button size="small" class="action-btn" @click.stop="$emit('preview', item)">
+        <el-icon><View /></el-icon> 文档预览
+      </el-button>
+      <el-button size="small" class="action-btn download-btn" @click.stop="$emit('download', item)">
+        <el-icon><Download /></el-icon> 下载
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { View, Download, Calendar } from '@element-plus/icons-vue'
+import katex from 'katex'
+
+defineProps<{ item: any; keyword: string }>()
+defineEmits<{ preview: [item: any]; download: [item: any]; fragment: [item: any] }>()
+
+function formatDate(dateStr: string): string {
+  if (!dateStr) return '-'
+  return dateStr.split(' ')[0]
+}
+
+function renderMath(text: string): string {
+  if (!text) return ''
+  let result = text.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
+    try {
+      return katex.renderToString(math, { throwOnError: false, displayMode: true })
+    } catch {
+      return `<span class="katex-error">${_}</span>`
+    }
+  })
+  result = result.replace(/\$([\s\S]*?)\$/g, (_, math) => {
+    try {
+      return katex.renderToString(math, { throwOnError: false, displayMode: false })
+    } catch {
+      return `<span class="katex-error">${_}</span>`
+    }
+  })
+  return result
+}
+
+function truncateText(html: string, maxLength: number): string {
+  if (html.length <= maxLength) return html
+
+  let count = 0
+  let cutIndex = 0
+  let inTag = false
+
+  for (let i = 0; i < html.length; i++) {
+    const char = html[i]
+    if (char === '<') {
+      inTag = true
+    } else if (char === '>') {
+      inTag = false
+    } else if (!inTag) {
+      count++
+      if (count >= maxLength) {
+        cutIndex = i + 1
+        break
+      }
+    }
+  }
+
+  let result = html.substring(0, cutIndex)
+
+  const openTags: string[] = []
+  const tagRegex = /<\/?([a-zA-Z]+)(?:\s[^>]*)?>/g
+  let match
+  const truncated = result
+  while ((match = tagRegex.exec(truncated)) !== null) {
+    const tag = match[1].toLowerCase()
+    if (match[0].startsWith('</')) {
+      if (openTags.length > 0 && openTags[openTags.length - 1] === tag) {
+        openTags.pop()
+      }
+    } else {
+      if (!['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tag)) {
+        openTags.push(tag)
+      }
+    }
+  }
+
+  while (openTags.length > 0) {
+    result += `</${openTags.pop()}>`
+  }
+
+  return result + '...'
+}
+</script>
+
+<style scoped>
+.result-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px 24px;
+  margin-bottom: 16px;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid #f0f0f0;
+}
+.result-card:hover {
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+}
+.result-title {
+  font-size: 16px;
+  color: #333;
+  margin: 0 0 10px;
+  font-weight: 600;
+  line-height: 1.5;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+.result-title:hover {
+  color: #f5a623;
+}
+.title-text {
+  flex: 1;
+}
+.result-meta {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+.meta-tag {
+  border: 1px solid #e8e8e8;
+  background: #fafafa;
+  color: #909399;
+  font-size: 12px;
+  padding: 1px 8px;
+  border-radius: 2px;
+  height: auto;
+}
+.dept-tag {
+  border-color: #d9ecff;
+  background: #ecf5ff;
+  color: #409eff;
+}
+.meta-info {
+  font-size: 12px;
+  color: #b0b0b0;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.meta-info :deep(.el-icon) {
+  font-size: 14px;
+}
+.result-abstract {
+  font-size: 13px;
+  color: #666;
+  line-height: 1.8;
+  margin: 0 0 12px;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+.result-abstract:hover {
+  color: #333;
+}
+.result-abstract :deep(em) {
+  color: #f5a623;
+  font-style: normal;
+  font-weight: 600;
+}
+.result-tags {
+  display: flex;
+  gap: 6px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+.custom-tag {
+  border: 1px solid #d9ecff;
+  background: #f5f9ff;
+  color: #409eff;
+  padding: 2px 8px;
+  font-size: 12px;
+  border-radius: 2px;
+  height: auto;
+}
+.result-actions {
+  display: flex;
+  gap: 12px;
+  padding-top: 4px;
+}
+.action-btn {
+  border-radius: 4px;
+  padding: 6px 16px;
+  font-size: 13px;
+}
+.action-btn :deep(.el-icon) {
+  margin-right: 4px;
+}
+.download-btn {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.download-btn:hover {
+  background: #66b1ff;
+  border-color: #66b1ff;
+}
+</style>

+ 72 - 0
src/components/SearchBar.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="search-bar">
+    <div class="search-container">
+      <input
+        :value="modelValue"
+        type="text"
+        placeholder="请输入关键词进行搜索"
+        class="search-input"
+        @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
+        @keyup.enter="handleSearch"
+      />
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{ modelValue: string }>()
+const emit = defineEmits<{
+  'update:modelValue': [value: string]
+  search: [keyword: string]
+}>()
+
+function handleSearch() {
+  emit('search', props.modelValue)
+}
+</script>
+
+<style scoped>
+.search-bar {
+  max-width: 700px;
+  margin: 24px auto 20px;
+}
+.search-container {
+  display: flex;
+  align-items: center;
+  border: 2px solid #f5a623;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: border-color 0.3s;
+}
+.search-container:focus-within {
+  border-color: #e8960f;
+}
+.search-input {
+  flex: 1;
+  height: 46px;
+  padding: 0 16px;
+  border: none;
+  outline: none;
+  font-size: 15px;
+  color: #303133;
+}
+.search-input::placeholder {
+  color: #c0c4cc;
+}
+.search-btn {
+  height: 46px;
+  padding: 0 32px;
+  background: #f5a623;
+  border: none;
+  color: #fff;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: background 0.3s;
+  white-space: nowrap;
+}
+.search-btn:hover {
+  background: #e8960f;
+}
+</style>

+ 39 - 0
src/components/SortDropdown.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="sort-dropdown">
+    <span class="sort-label">排序方式</span>
+    <el-select v-model="sortBy" size="default" @change="emit('change', sortBy)" class="sort-select" :teleported="false">
+      <el-option label="相关性排序" value="relevance" />
+      <el-option label="最新发布" value="date_desc" />
+    </el-select>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const emit = defineEmits<{ change: [value: string] }>()
+const sortBy = ref('relevance')
+</script>
+
+<style scoped>
+.sort-dropdown {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.sort-label {
+  font-size: 14px;
+  color: #909399;
+  white-space: nowrap;
+}
+.sort-select {
+  width: 130px;
+}
+.sort-select :deep(.el-input__wrapper) {
+  border-radius: 4px;
+  box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+.sort-select :deep(.el-input__inner) {
+  font-size: 13px;
+}
+</style>

+ 20 - 0
src/components/StatsOverview.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="stats-overview">
+    <span class="stats-text">找到 <strong>{{ total }}</strong> 个相关文档</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ total: number }>()
+</script>
+
+<style scoped>
+.stats-overview {
+  font-size: 14px;
+  color: #999;
+}
+.stats-text strong {
+  color: #f5a623;
+  font-weight: 600;
+}
+</style>

+ 46 - 0
src/layouts/DefaultLayout.vue

@@ -0,0 +1,46 @@
+<template>
+  <header class="app-header">
+    <div class="header-inner">
+      <div class="logo">
+        <span class="logo-icon">🔍</span>
+        <span class="logo-text">多维度检索</span>
+      </div>
+    </div>
+  </header>
+  <main class="app-main">
+    <keep-alive>
+      <router-view />
+    </keep-alive>
+  </main>
+</template>
+
+<style scoped>
+.app-header {
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+  padding: 0 24px;
+  height: 56px;
+  display: flex;
+  align-items: center;
+}
+.header-inner {
+  max-width: 1200px;
+  width: 100%;
+  margin: 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.logo {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #e6a23c;
+}
+.app-main {
+  min-height: calc(100vh - 56px);
+  background: #f5f7fa;
+}
+</style>

+ 21 - 0
src/main.ts

@@ -0,0 +1,21 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import 'katex/dist/katex.min.css'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+
+import App from './App.vue'
+import router from './router'
+import './styles/index.css'
+
+const app = createApp(App)
+
+app.use(router)
+app.use(ElementPlus, { locale: zhCn })
+
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.mount('#app')

+ 27 - 0
src/router/index.ts

@@ -0,0 +1,27 @@
+import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/',
+    component: () => import('@/layouts/DefaultLayout.vue'),
+    children: [
+      {
+        path: '',
+        name: 'Search',
+        component: () => import('@/views/SearchPage.vue'),
+      },
+      {
+        path: 'fragment',
+        name: 'FragmentDetail',
+        component: () => import('@/views/FragmentDetail.vue'),
+      },
+    ],
+  },
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes,
+})
+
+export default router

+ 53 - 0
src/styles/index.css

@@ -0,0 +1,53 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
+    'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  background: #f5f7fa;
+  color: #303133;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+#app {
+  min-height: 100vh;
+}
+
+a {
+  color: #409eff;
+  text-decoration: none;
+}
+
+a:hover {
+  color: #f5a623;
+}
+
+/* Element Plus overrides */
+:root {
+  --el-color-primary: #409eff;
+}
+
+.el-empty__description {
+  color: #c0c4cc;
+  font-size: 14px;
+}
+
+/* Pagination */
+.el-pagination {
+  --el-pagination-button-color: #606266;
+}
+
+/* KaTeX overrides */
+.result-abstract :deep(.katex) {
+  font-size: 1em;
+}
+
+.result-abstract :deep(.katex-display) {
+  margin: 8px 0;
+  overflow-x: auto;
+  overflow-y: hidden;
+}

+ 26 - 0
src/utils/request.ts

@@ -0,0 +1,26 @@
+const BASE_URL = '/api/v1'
+
+async function request<T = any>(url: string, options: RequestInit = {}): Promise<T> {
+  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
+  if (options.headers) {
+    Object.entries(options.headers as Record<string, string>).forEach(([k, v]) => {
+      headers[k] = v
+    })
+  }
+
+  const response = await fetch(BASE_URL + url, { ...options, headers })
+
+  if (!response.ok) {
+    const err = await response.json().catch(() => ({}))
+    throw new Error(err.message || `请求失败 (${response.status})`)
+  }
+
+  return response.json()
+}
+
+export const api = {
+  get: <T = any>(url: string) => request<T>(url, { method: 'GET' }),
+  post: <T = any>(url: string, body?: any) => request<T>(url, { method: 'POST', body: JSON.stringify(body) }),
+}
+
+export default api

+ 198 - 0
src/views/FragmentDetail.vue

@@ -0,0 +1,198 @@
+<template>
+  <div class="fragment-detail">
+    <div class="detail-header">
+      <el-button class="back-btn" @click="goBack">
+        <el-icon><ArrowLeft /></el-icon> 返回搜索
+      </el-button>
+    </div>
+
+    <div class="detail-content">
+      <h2 class="detail-title">{{ item.title }}</h2>
+
+      <div class="detail-meta">
+        <el-tag v-if="item.doc_type && item.doc_type !== '-'" size="default" class="meta-tag">
+          {{ item.doc_type }}
+        </el-tag>
+        <el-tag v-if="item.department && item.department !== '-'" size="default" class="meta-tag dept-tag">
+          {{ item.department }}
+        </el-tag>
+        <span v-if="item.publish_date" class="meta-info">
+          <el-icon><Calendar /></el-icon>
+          {{ formatDate(item.publish_date) }}
+        </span>
+        <span v-if="item.standard_number" class="meta-info">
+          <el-icon><Document /></el-icon>
+          {{ item.standard_number }}
+        </span>
+      </div>
+
+      <div class="detail-tags" v-if="item.tags && item.tags.length">
+        <el-tag v-for="tag in item.tags" :key="tag" size="default" class="custom-tag">
+          {{ tag }}
+        </el-tag>
+      </div>
+
+      <div class="detail-divider"></div>
+
+      <div class="detail-text" v-html="renderMath(item.highlighted_text || item.text || '')"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ArrowLeft, Calendar, Document } from '@element-plus/icons-vue'
+import katex from 'katex'
+
+const router = useRouter()
+const route = useRoute()
+
+// 从 sessionStorage 恢复片段数据
+const stored = sessionStorage.getItem('fragment_detail')
+const item = ref<any>(stored ? JSON.parse(stored) : {})
+
+function goBack() {
+  sessionStorage.removeItem('fragment_detail')
+  router.back()
+}
+
+function formatDate(dateStr: string): string {
+  if (!dateStr) return ''
+  return dateStr.split(' ')[0]
+}
+
+function renderMath(text: string): string {
+  if (!text) return ''
+  let result = text.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
+    try {
+      return katex.renderToString(math, { throwOnError: false, displayMode: true })
+    } catch {
+      return `<span class="katex-error">${_}</span>`
+    }
+  })
+  result = result.replace(/\$([\s\S]*?)\$/g, (_, math) => {
+    try {
+      return katex.renderToString(math, { throwOnError: false, displayMode: false })
+    } catch {
+      return `<span class="katex-error">${_}</span>`
+    }
+  })
+  return result
+}
+</script>
+
+<style scoped>
+.fragment-detail {
+  max-width: 900px;
+  margin: 0 auto;
+  padding: 24px 24px 60px;
+}
+
+.detail-header {
+  margin-bottom: 24px;
+}
+
+.back-btn {
+  border-radius: 4px;
+  font-size: 14px;
+}
+
+.back-btn :deep(.el-icon) {
+  margin-right: 4px;
+}
+
+.detail-content {
+  background: #fff;
+  border-radius: 8px;
+  padding: 32px;
+  border: 1px solid #f0f0f0;
+}
+
+.detail-title {
+  font-size: 20px;
+  color: #333;
+  margin: 0 0 16px;
+  font-weight: 600;
+  line-height: 1.5;
+}
+
+.detail-meta {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+
+.meta-tag {
+  border: 1px solid #e8e8e8;
+  background: #fafafa;
+  color: #909399;
+  font-size: 13px;
+  padding: 2px 12px;
+  border-radius: 2px;
+  height: auto;
+}
+
+.dept-tag {
+  border-color: #d9ecff;
+  background: #ecf5ff;
+  color: #409eff;
+}
+
+.meta-info {
+  font-size: 13px;
+  color: #b0b0b0;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.meta-info :deep(.el-icon) {
+  font-size: 16px;
+}
+
+.detail-tags {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 16px;
+  flex-wrap: wrap;
+}
+
+.custom-tag {
+  border: 1px solid #d9ecff;
+  background: #f5f9ff;
+  color: #409eff;
+  padding: 2px 12px;
+  font-size: 13px;
+  border-radius: 2px;
+  height: auto;
+}
+
+.detail-divider {
+  height: 1px;
+  background: #f0f0f0;
+  margin: 20px 0;
+}
+
+.detail-text {
+  font-size: 14px;
+  color: #444;
+  line-height: 2;
+}
+
+.detail-text :deep(em) {
+  color: #f5a623;
+  font-style: normal;
+  font-weight: 600;
+}
+
+.detail-text :deep(.katex) {
+  font-size: 1em;
+}
+
+.detail-text :deep(.katex-display) {
+  margin: 12px 0;
+}
+</style>

+ 527 - 0
src/views/SearchPage.vue

@@ -0,0 +1,527 @@
+<template>
+  <div class="search-page">
+    <SearchBar v-model="searchParams.query" @search="handleSearch" />
+    <FilterPanel
+      v-model:docType="docType"
+      v-model:department="department"
+      v-model:dateRange="dateRange"
+      v-model:keyword="filterKeyword"
+      @change="handleFilterChange"
+    />
+    <div class="result-area">
+      <div class="result-header">
+        <StatsOverview :total="total" />
+        <SortDropdown @change="handleSort" />
+      </div>
+      <div class="result-list" v-loading="loading">
+        <ResultCard
+          v-for="item in results"
+          :key="item.pk"
+          :item="mapResultToItem(item)"
+          :keyword="searchParams.query"
+          @preview="handlePreview"
+          @download="handleDownload"
+          @fragment="handleFragment"
+        />
+        <div v-if="!loading && results.length === 0 && searched" class="empty-result">
+          <el-empty description="暂无搜索结果" />
+        </div>
+      </div>
+      <div v-if="total > 0" class="pagination-wrapper">
+        <Pagination
+          :total="total"
+          :current-page="page"
+          :page-size="pageSize"
+          @change="handlePageChange"
+        />
+      </div>
+    </div>
+
+    <!-- 文档预览弹窗 -->
+    <el-dialog
+      v-model="previewVisible"
+      :title="previewTitle"
+      width="80%"
+      top="5vh"
+      :close-on-click-modal="false"
+      @close="closePreview"
+    >
+      <div v-loading="previewLoading" class="preview-container">
+        <iframe
+          v-if="previewIframeUrl && !previewLoading"
+          :src="previewIframeUrl"
+          class="preview-iframe"
+          frameborder="0"
+        ></iframe>
+        <el-empty v-if="!previewIframeUrl && !previewLoading" description="无法预览此文档" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, onActivated, onDeactivated } from 'vue'
+import { useRouter, useRoute, onBeforeRouteLeave } from 'vue-router'
+import { multidimensionalSearch } from '@/api/retrieval'
+import { ElMessage } from 'element-plus'
+import SearchBar from '@/components/SearchBar.vue'
+import FilterPanel from '@/components/FilterPanel.vue'
+import ResultCard from '@/components/ResultCard.vue'
+import Pagination from '@/components/Pagination.vue'
+import StatsOverview from '@/components/StatsOverview.vue'
+import SortDropdown from '@/components/SortDropdown.vue'
+
+// keep-alive 缓存标识
+defineOptions({ name: 'SearchPage' })
+
+const router = useRouter()
+const route = useRoute()
+
+// ============================================================
+// 搜索参数
+// ============================================================
+const searchParams = reactive({
+  query: '',
+  file_name: '',
+  standard_number: '',
+  issuing_authority: '',
+  document_type: '',
+  professional_field: '',
+  start_time: '',
+  end_time: '',
+  sort_mode: 'relevance',
+})
+
+// ============================================================
+// 筛选面板状态(父组件统一管理,通过 props 传给 FilterPanel)
+// ============================================================
+const docType = ref('全部')
+const department = ref('全部')
+const dateRange = ref<[string, string] | null>(null)
+const filterKeyword = ref('')
+
+// ============================================================
+// 搜索结果状态
+// ============================================================
+const loading = ref(false)
+const results = ref<any[]>([])
+const total = ref(0)
+const page = ref(1)
+const pageSize = ref(10)
+const searched = ref(false)
+const hasRestored = ref(false) // 防止重复恢复
+
+// 本地浏览量增量(解决 mapResultToItem 每次创建新对象导致更新丢失的问题)
+const browseCountDelta = reactive<Record<string, number>>({})
+
+// 预览相关
+const previewVisible = ref(false)
+const previewIframeUrl = ref('')
+const previewTitle = ref('')
+const previewLoading = ref(false)
+
+// ============================================================
+// sessionStorage key
+// ============================================================
+const STORAGE_KEY = 'search_page_state'
+
+interface StoredState {
+  searchParams: Record<string, string>
+  docType: string
+  department: string
+  dateRange: [string, string] | null
+  filterKeyword: string
+  results: any[]
+  total: number
+  page: number
+  pageSize: number
+  searched: boolean
+  browseCountDelta: Record<string, number>
+}
+
+/** 保存当前搜索状态到 sessionStorage */
+function saveState() {
+  if (!searched.value) return
+  const state: StoredState = {
+    searchParams: { ...searchParams },
+    docType: docType.value,
+    department: department.value,
+    dateRange: dateRange.value,
+    filterKeyword: filterKeyword.value,
+    results: results.value,
+    total: total.value,
+    page: page.value,
+    pageSize: pageSize.value,
+    searched: searched.value,
+    browseCountDelta: { ...browseCountDelta },
+  }
+  try {
+    sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))
+    console.log('[SearchPage] ✅ state saved, query:', searchParams.query, 'results:', results.value.length)
+  } catch (e) {
+    console.warn('[SearchPage] failed to save state:', e)
+  }
+}
+
+/** 从 sessionStorage 恢复搜索状态 */
+function restoreState(): boolean {
+  try {
+    const raw = sessionStorage.getItem(STORAGE_KEY)
+    if (!raw) {
+      console.log('[SearchPage] no saved state')
+      return false
+    }
+    const state: StoredState = JSON.parse(raw)
+    if (!state || !state.searched) {
+      console.log('[SearchPage] saved state invalid')
+      return false
+    }
+
+    Object.assign(searchParams, state.searchParams)
+    docType.value = state.docType
+    department.value = state.department
+    dateRange.value = state.dateRange
+    filterKeyword.value = state.filterKeyword
+    results.value = state.results || []
+    total.value = state.total ?? 0
+    page.value = state.page ?? 1
+    pageSize.value = state.pageSize ?? 10
+    searched.value = true
+    hasRestored.value = true
+
+    Object.assign(browseCountDelta, state.browseCountDelta || {})
+    console.log('[SearchPage] ✅ state restored, query:', searchParams.query, 'results:', results.value.length)
+    return true
+  } catch (e) {
+    console.warn('[SearchPage] failed to restore state:', e)
+    return false
+  }
+}
+
+/** 清除保存的搜索状态 */
+function clearState() {
+  sessionStorage.removeItem(STORAGE_KEY)
+}
+
+// ============================================================
+// 路由守卫 — 离开时保存状态(比 onDeactivated 更可靠)
+// ============================================================
+onBeforeRouteLeave((to, from, next) => {
+  if (searched.value) {
+    console.log('[SearchPage] onBeforeRouteLeave: saving state before navigating to', to.path)
+    saveState()
+  }
+  next()
+})
+
+// ============================================================
+// 生命周期
+// ============================================================
+const RETURNING_FLAG = 'returning_from_fragment'
+
+onMounted(() => {
+  // 首次登录不恢复状态,不执行搜索,显示空搜索框
+  // 只有从详情页返回时才恢复
+  const isReturning = sessionStorage.getItem(RETURNING_FLAG) === 'true'
+  sessionStorage.removeItem(RETURNING_FLAG)
+
+  if (isReturning) {
+    console.log('[SearchPage] onMounted: returning from fragment, restoring state')
+    if (restoreState()) return
+  }
+
+  if (!searched.value) {
+    console.log('[SearchPage] onMounted: fresh start, no search (wait for user input)')
+    // 不执行 doSearch(),让用户自己输入关键词搜索
+  }
+})
+
+onActivated(() => {
+  // keep-alive 缓存激活时,内存中已有状态则保持不动
+  if (searched.value) {
+    console.log('[SearchPage] onActivated: state preserved in memory')
+    return
+  }
+  // 如果没有状态,且是从详情页返回,才恢复
+  const isReturning = sessionStorage.getItem(RETURNING_FLAG) === 'true'
+  if (isReturning) {
+    sessionStorage.removeItem(RETURNING_FLAG)
+    if (restoreState()) {
+      console.log('[SearchPage] onActivated: restored from sessionStorage')
+      return
+    }
+  }
+  // 否则首次登录,不执行搜索,等待用户输入
+  console.log('[SearchPage] onActivated: fresh start, no search (wait for user input)')
+})
+
+onDeactivated(() => {
+  if (searched.value) {
+    console.log('[SearchPage] onDeactivated: saving state')
+    saveState()
+  }
+})
+
+// ============================================================
+// 搜索逻辑
+// ============================================================
+function handleSearch(kw: string) {
+  searchParams.query = kw
+  page.value = 1
+  doSearch()
+}
+
+function handleFilterChange(params: any) {
+  if (params.docType) docType.value = params.docType
+  if (params.department) department.value = params.department
+  // 始终更新 searchParams 中的值,"全部"对应空字符串
+  if (params.docType !== undefined) searchParams.document_type = params.docType === '全部' ? '' : params.docType
+  if (params.department !== undefined) {
+    searchParams.issuing_authority = params.department === '全部' ? '' : params.department
+  }
+  // keyword 仅在明确传入时更新,避免空字符串覆盖已有搜索词
+  if (params.keyword && params.keyword !== '') {
+    filterKeyword.value = params.keyword
+    searchParams.query = params.keyword
+  }
+  if (params.publishDateFrom) searchParams.start_time = params.publishDateFrom
+  if (params.publishDateTo) searchParams.end_time = params.publishDateTo
+  page.value = 1
+  doSearch()
+}
+
+function handleSort(sortBy: string) {
+  searchParams.sort_mode = sortBy
+  page.value = 1
+  doSearch()
+}
+
+function handlePageChange(p: number, ps: number) {
+  page.value = p
+  pageSize.value = ps
+  doSearch()
+}
+
+// ============================================================
+// 下载
+// ============================================================
+async function handleDownload(item: any) {
+  if (!item.file_url) {
+    ElMessage.warning('该文档暂无下载链接')
+    return
+  }
+  if (item.document_id) {
+    incrementBrowseCount(item.document_id)
+  }
+  const url = `/api/v1/retrieval/files/download?file_url=${encodeURIComponent(item.file_url)}`
+
+  try {
+    const resp = await fetch(url)
+    if (!resp.ok) {
+      const err = await resp.json().catch(() => ({}))
+      throw new Error(err.message || `下载失败 (${resp.status})`)
+    }
+    const cd = resp.headers.get('content-disposition') || ''
+    let filename = item.file_name || 'document'
+    const m = cd.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/)
+    if (m) {
+      try {
+        filename = decodeURIComponent(m[1])
+      } catch {
+        filename = m[1]
+      }
+    }
+    const blob = await resp.blob()
+    const a = document.createElement('a')
+    a.href = URL.createObjectURL(blob)
+    a.download = filename
+    document.body.appendChild(a)
+    a.click()
+    a.remove()
+    URL.revokeObjectURL(a.href)
+    ElMessage.success('下载成功')
+  } catch (e: any) {
+    ElMessage.error(e.message || '下载失败')
+  }
+}
+
+// ============================================================
+// 预览
+// ============================================================
+async function handlePreview(item: any) {
+  if (!item.file_url) {
+    ElMessage.warning('该文档暂无预览链接')
+    return
+  }
+  if (item.document_id) {
+    incrementBrowseCount(item.document_id)
+  }
+  previewTitle.value = item.title || '文档预览'
+  previewLoading.value = true
+  previewVisible.value = true
+
+  const url = `/api/v1/retrieval/files/preview?file_url=${encodeURIComponent(item.file_url)}`
+
+  try {
+    const resp = await fetch(url)
+    if (!resp.ok) {
+      const err = await resp.json().catch(() => ({}))
+      throw new Error(err.message || `预览失败 (${resp.status})`)
+    }
+
+    const contentType = resp.headers.get('content-type') || 'application/pdf'
+    const arrayBuffer = await resp.arrayBuffer()
+    const blob = new Blob([arrayBuffer], { type: contentType })
+    const blobUrl = URL.createObjectURL(blob)
+    previewIframeUrl.value = blobUrl
+  } catch (e: any) {
+    ElMessage.error(e.message || '预览失败')
+    previewVisible.value = false
+  } finally {
+    previewLoading.value = false
+  }
+}
+
+function closePreview() {
+  previewVisible.value = false
+  if (previewIframeUrl.value) {
+    URL.revokeObjectURL(previewIframeUrl.value)
+  }
+  previewIframeUrl.value = ''
+}
+
+// ============================================================
+// 片段详情 — 点击摘要文字跳转到片段详情页
+// ============================================================
+function handleFragment(item: any) {
+  // 跳转前先保存搜索状态
+  saveState()
+
+  // 设置返回标志,告诉 SearchPage 回来时要恢复状态
+  sessionStorage.setItem(RETURNING_FLAG, 'true')
+
+  sessionStorage.setItem('fragment_detail', JSON.stringify(item))
+  router.push({ name: 'FragmentDetail' })
+}
+
+/** 增加浏览量 */
+async function incrementBrowseCount(documentId: string) {
+  try {
+    const resp = await fetch(`/api/v1/retrieval/documents/${documentId}/browse`, {
+      method: 'POST',
+    })
+    const json = await resp.json()
+    browseCountDelta[documentId] = (browseCountDelta[documentId] || 0) + 1
+  } catch (e: any) {
+    console.warn('增加浏览量失败:', e.message)
+  }
+}
+
+function mapResultToItem(result: any): any {
+  const meta = result.metadata || {}
+  const docId = meta.document_id || ''
+  return {
+    id: result.pk,
+    pk: result.pk,
+    distance: result.distance,
+    title: meta.chinese_name || meta.file_name || '',
+    doc_type: meta.document_type || '-',
+    department: meta.issuing_authority || '-',
+    publish_date: result.created_time,
+    view_count: (meta.browse_count || 0) + (browseCountDelta[docId] || 0),
+    document_id: docId,
+    abstract: result.highlighted_text || result.text || '',
+    tags: [meta.standard_number, meta.hierarchy].filter(Boolean),
+    hierarchy: meta.hierarchy,
+    file_url: meta.file_url,
+    file_name: meta.file_name,
+    standard_number: meta.standard_number,
+    text: result.text,
+    highlighted_text: result.highlighted_text,
+  }
+}
+
+async function doSearch() {
+  loading.value = true
+  searched.value = true
+  try {
+    const body: Record<string, any> = {
+      query: searchParams.query || ' ',
+      page: page.value,
+      page_size: pageSize.value,
+      sort_mode: searchParams.sort_mode,
+    }
+    if (searchParams.file_name) body.file_name = searchParams.file_name
+    if (searchParams.standard_number) body.standard_number = searchParams.standard_number
+    if (searchParams.issuing_authority) body.issuing_authority = searchParams.issuing_authority
+    if (searchParams.document_type) body.document_type = searchParams.document_type
+    if (searchParams.professional_field) body.professional_field = searchParams.professional_field
+    if (searchParams.start_time) body.start_time = searchParams.start_time
+    if (searchParams.end_time) body.end_time = searchParams.end_time
+
+    const res: any = await multidimensionalSearch(body)
+    if (res.code !== undefined && String(res.code) !== '1' && String(res.code) !== '000000') {
+      ElMessage.error(res.message || '检索失败')
+      results.value = []
+      total.value = 0
+      return
+    }
+    const data = res.data
+    if (data) {
+      results.value = data.objs || []
+      total.value = data.total_count ?? data.objs?.length ?? 0
+    } else {
+      results.value = []
+      total.value = 0
+    }
+  } catch (e: any) {
+    ElMessage.error(e.message || '检索失败')
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.search-page {
+  max-width: 1000px;
+  margin: 0 auto;
+  padding: 24px 24px 60px;
+}
+.result-area {
+  margin-top: 20px;
+}
+.result-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 0 4px;
+}
+.result-list {
+  min-height: 200px;
+}
+.empty-result {
+  padding: 40px 0;
+}
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  margin-top: 24px;
+}
+
+.preview-container {
+  height: 75vh;
+  min-height: 500px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.preview-iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+  border-radius: 4px;
+  background: #f5f5f5;
+}
+</style>

+ 25 - 0
tsconfig.json

@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+    "strict": true,
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noFallthroughCasesInSwitch": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.vue", "env.d.ts"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 21 - 0
vite.config.ts

@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    port: 9007,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8007',
+        changeOrigin: true,
+      },
+    },
+  },
+})