handy 1 mês atrás
pai
commit
fa60782ec3

+ 8 - 8
package.json

@@ -11,18 +11,18 @@
     "format": "prettier --write src/"
   },
   "dependencies": {
-    "vue": "^3.4.21",
-    "axios": "^1.6.7"
+    "axios": "^1.6.7",
+    "vue": "^3.4.21"
   },
   "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^8.56.1",
+    "@typescript-eslint/parser": "^8.56.1",
     "@vitejs/plugin-vue": "^5.0.4",
-    "typescript": "^5.2.2",
-    "vite": "^5.1.6",
-    "vue-tsc": "^2.0.6",
-    "@typescript-eslint/eslint-plugin": "^7.1.1",
-    "@typescript-eslint/parser": "^7.1.1",
     "eslint": "^8.57.0",
     "eslint-plugin-vue": "^9.22.0",
-    "prettier": "^3.2.5"
+    "prettier": "^3.2.5",
+    "typescript": "^5.2.2",
+    "vite": "^5.1.6",
+    "vue-tsc": "^2.0.6"
   }
 }

+ 160 - 197
pnpm-lock.yaml

@@ -16,11 +16,11 @@ importers:
         version: 3.5.29(typescript@5.9.3)
     devDependencies:
       '@typescript-eslint/eslint-plugin':
-        specifier: ^7.1.1
-        version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
+        specifier: ^8.56.1
+        version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
       '@typescript-eslint/parser':
-        specifier: ^7.1.1
-        version: 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+        specifier: ^8.56.1
+        version: 8.56.1(eslint@8.57.1)(typescript@5.9.3)
       '@vitejs/plugin-vue':
         specifier: ^5.0.4
         version: 5.2.4(vite@5.4.21)(vue@3.5.29(typescript@5.9.3))
@@ -387,63 +387,64 @@ packages:
   '@types/estree@1.0.8':
     resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
 
-  '@typescript-eslint/eslint-plugin@7.18.0':
-    resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/eslint-plugin@8.56.1':
+    resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      '@typescript-eslint/parser': ^7.0.0
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+      '@typescript-eslint/parser': ^8.56.1
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.0.0'
 
-  '@typescript-eslint/parser@7.18.0':
-    resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/parser@8.56.1':
+    resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/project-service@8.56.1':
+    resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
 
-  '@typescript-eslint/scope-manager@7.18.0':
-    resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/scope-manager@8.56.1':
+    resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/type-utils@7.18.0':
-    resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/tsconfig-utils@8.56.1':
+    resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/type-utils@8.56.1':
+    resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.0.0'
 
-  '@typescript-eslint/types@7.18.0':
-    resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/types@8.56.1':
+    resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/typescript-estree@7.18.0':
-    resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/typescript-estree@8.56.1':
+    resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+      typescript: '>=4.8.4 <6.0.0'
 
-  '@typescript-eslint/utils@7.18.0':
-    resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/utils@8.56.1':
+    resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.56.0
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.0.0'
 
-  '@typescript-eslint/visitor-keys@7.18.0':
-    resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
+  '@typescript-eslint/visitor-keys@8.56.1':
+    resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@ungap/structured-clone@1.3.0':
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -531,10 +532,6 @@ packages:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
-  array-union@2.1.0:
-    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
-    engines: {node: '>=8'}
-
   asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
@@ -544,6 +541,10 @@ packages:
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  balanced-match@4.0.4:
+    resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+    engines: {node: 18 || 20 || >=22}
+
   boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
 
@@ -553,9 +554,9 @@ packages:
   brace-expansion@2.0.2:
     resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
 
-  braces@3.0.3:
-    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
-    engines: {node: '>=8'}
+  brace-expansion@5.0.4:
+    resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
+    engines: {node: 18 || 20 || >=22}
 
   call-bind-apply-helpers@1.0.2:
     resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
@@ -614,10 +615,6 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
-  dir-glob@3.0.1:
-    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
-    engines: {node: '>=8'}
-
   doctrine@3.0.0:
     resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
@@ -669,6 +666,10 @@ packages:
     resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
+  eslint-visitor-keys@5.0.1:
+    resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+    engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
   eslint@8.57.1:
     resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -701,10 +702,6 @@ packages:
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
-  fast-glob@3.3.3:
-    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
-    engines: {node: '>=8.6.0'}
-
   fast-json-stable-stringify@2.1.0:
     resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
 
@@ -714,14 +711,19 @@ packages:
   fastq@1.20.1:
     resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
 
+  fdir@6.5.0:
+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+    engines: {node: '>=12.0.0'}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
   file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
 
-  fill-range@7.1.1:
-    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
-    engines: {node: '>=8'}
-
   find-up@5.0.0:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
@@ -765,10 +767,6 @@ packages:
     resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
     engines: {node: '>= 0.4'}
 
-  glob-parent@5.1.2:
-    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
-    engines: {node: '>= 6'}
-
   glob-parent@6.0.2:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
@@ -781,10 +779,6 @@ packages:
     resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
     engines: {node: '>=8'}
 
-  globby@11.1.0:
-    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
-    engines: {node: '>=10'}
-
   gopd@1.2.0:
     resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
     engines: {node: '>= 0.4'}
@@ -816,6 +810,10 @@ packages:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
 
+  ignore@7.0.5:
+    resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+    engines: {node: '>= 4'}
+
   import-fresh@3.3.1:
     resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
     engines: {node: '>=6'}
@@ -839,10 +837,6 @@ packages:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
 
-  is-number@7.0.0:
-    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
-    engines: {node: '>=0.12.0'}
-
   is-path-inside@3.0.3:
     resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
     engines: {node: '>=8'}
@@ -887,14 +881,6 @@ packages:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
 
-  merge2@1.4.1:
-    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
-    engines: {node: '>= 8'}
-
-  micromatch@4.0.8:
-    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
-    engines: {node: '>=8.6'}
-
   mime-db@1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
@@ -903,6 +889,10 @@ packages:
     resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
     engines: {node: '>= 0.6'}
 
+  minimatch@10.2.4:
+    resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+    engines: {node: 18 || 20 || >=22}
+
   minimatch@3.1.5:
     resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
 
@@ -961,16 +951,12 @@ packages:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
 
-  path-type@4.0.0:
-    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
-    engines: {node: '>=8'}
-
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
-  picomatch@2.3.1:
-    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
-    engines: {node: '>=8.6'}
+  picomatch@4.0.3:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
 
   postcss-selector-parser@6.1.2:
     resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
@@ -1033,10 +1019,6 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  slash@3.0.0:
-    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
-    engines: {node: '>=8'}
-
   source-map-js@1.2.1:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
@@ -1056,15 +1038,15 @@ packages:
   text-table@0.2.0:
     resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
 
-  to-regex-range@5.0.1:
-    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
-    engines: {node: '>=8.0'}
+  tinyglobby@0.2.15:
+    resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+    engines: {node: '>=12.0.0'}
 
-  ts-api-utils@1.4.3:
-    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
-    engines: {node: '>=16'}
+  ts-api-utils@2.4.0:
+    resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
+    engines: {node: '>=18.12'}
     peerDependencies:
-      typescript: '>=4.2.0'
+      typescript: '>=4.8.4'
 
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
@@ -1369,86 +1351,96 @@ snapshots:
 
   '@types/estree@1.0.8': {}
 
-  '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
+  '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
-      '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
-      '@typescript-eslint/scope-manager': 7.18.0
-      '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
-      '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
-      '@typescript-eslint/visitor-keys': 7.18.0
+      '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 8.56.1
+      '@typescript-eslint/type-utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.56.1
       eslint: 8.57.1
-      graphemer: 1.4.0
-      ignore: 5.3.2
+      ignore: 7.0.5
       natural-compare: 1.4.0
-      ts-api-utils: 1.4.3(typescript@5.9.3)
-    optionalDependencies:
+      ts-api-utils: 2.4.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+  '@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/scope-manager': 7.18.0
-      '@typescript-eslint/types': 7.18.0
-      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
-      '@typescript-eslint/visitor-keys': 7.18.0
+      '@typescript-eslint/scope-manager': 8.56.1
+      '@typescript-eslint/types': 8.56.1
+      '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.56.1
       debug: 4.4.3
       eslint: 8.57.1
-    optionalDependencies:
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@7.18.0':
+  '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+      '@typescript-eslint/types': 8.56.1
+      debug: 4.4.3
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.56.1':
+    dependencies:
+      '@typescript-eslint/types': 8.56.1
+      '@typescript-eslint/visitor-keys': 8.56.1
+
+  '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/types': 7.18.0
-      '@typescript-eslint/visitor-keys': 7.18.0
+      typescript: 5.9.3
 
-  '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+  '@typescript-eslint/type-utils@8.56.1(eslint@8.57.1)(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
-      '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/types': 8.56.1
+      '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3)
       debug: 4.4.3
       eslint: 8.57.1
-      ts-api-utils: 1.4.3(typescript@5.9.3)
-    optionalDependencies:
+      ts-api-utils: 2.4.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@7.18.0': {}
+  '@typescript-eslint/types@8.56.1': {}
 
-  '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)':
+  '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/types': 7.18.0
-      '@typescript-eslint/visitor-keys': 7.18.0
+      '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
+      '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+      '@typescript-eslint/types': 8.56.1
+      '@typescript-eslint/visitor-keys': 8.56.1
       debug: 4.4.3
-      globby: 11.1.0
-      is-glob: 4.0.3
-      minimatch: 9.0.9
+      minimatch: 10.2.4
       semver: 7.7.4
-      ts-api-utils: 1.4.3(typescript@5.9.3)
-    optionalDependencies:
+      tinyglobby: 0.2.15
+      ts-api-utils: 2.4.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+  '@typescript-eslint/utils@8.56.1(eslint@8.57.1)(typescript@5.9.3)':
     dependencies:
       '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
-      '@typescript-eslint/scope-manager': 7.18.0
-      '@typescript-eslint/types': 7.18.0
-      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 8.56.1
+      '@typescript-eslint/types': 8.56.1
+      '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
       eslint: 8.57.1
+      typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
-      - typescript
 
-  '@typescript-eslint/visitor-keys@7.18.0':
+  '@typescript-eslint/visitor-keys@8.56.1':
     dependencies:
-      '@typescript-eslint/types': 7.18.0
-      eslint-visitor-keys: 3.4.3
+      '@typescript-eslint/types': 8.56.1
+      eslint-visitor-keys: 5.0.1
 
   '@ungap/structured-clone@1.3.0': {}
 
@@ -1564,8 +1556,6 @@ snapshots:
 
   argparse@2.0.1: {}
 
-  array-union@2.1.0: {}
-
   asynckit@0.4.0: {}
 
   axios@1.13.6:
@@ -1578,6 +1568,8 @@ snapshots:
 
   balanced-match@1.0.2: {}
 
+  balanced-match@4.0.4: {}
+
   boolbase@1.0.0: {}
 
   brace-expansion@1.1.12:
@@ -1589,9 +1581,9 @@ snapshots:
     dependencies:
       balanced-match: 1.0.2
 
-  braces@3.0.3:
+  brace-expansion@5.0.4:
     dependencies:
-      fill-range: 7.1.1
+      balanced-match: 4.0.4
 
   call-bind-apply-helpers@1.0.2:
     dependencies:
@@ -1637,10 +1629,6 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
-  dir-glob@3.0.1:
-    dependencies:
-      path-type: 4.0.0
-
   doctrine@3.0.0:
     dependencies:
       esutils: 2.0.3
@@ -1717,6 +1705,8 @@ snapshots:
 
   eslint-visitor-keys@3.4.3: {}
 
+  eslint-visitor-keys@5.0.1: {}
+
   eslint@8.57.1:
     dependencies:
       '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
@@ -1782,14 +1772,6 @@ snapshots:
 
   fast-deep-equal@3.1.3: {}
 
-  fast-glob@3.3.3:
-    dependencies:
-      '@nodelib/fs.stat': 2.0.5
-      '@nodelib/fs.walk': 1.2.8
-      glob-parent: 5.1.2
-      merge2: 1.4.1
-      micromatch: 4.0.8
-
   fast-json-stable-stringify@2.1.0: {}
 
   fast-levenshtein@2.0.6: {}
@@ -1798,14 +1780,14 @@ snapshots:
     dependencies:
       reusify: 1.1.0
 
+  fdir@6.5.0(picomatch@4.0.3):
+    optionalDependencies:
+      picomatch: 4.0.3
+
   file-entry-cache@6.0.1:
     dependencies:
       flat-cache: 3.2.0
 
-  fill-range@7.1.1:
-    dependencies:
-      to-regex-range: 5.0.1
-
   find-up@5.0.0:
     dependencies:
       locate-path: 6.0.0
@@ -1854,10 +1836,6 @@ snapshots:
       dunder-proto: 1.0.1
       es-object-atoms: 1.1.1
 
-  glob-parent@5.1.2:
-    dependencies:
-      is-glob: 4.0.3
-
   glob-parent@6.0.2:
     dependencies:
       is-glob: 4.0.3
@@ -1875,15 +1853,6 @@ snapshots:
     dependencies:
       type-fest: 0.20.2
 
-  globby@11.1.0:
-    dependencies:
-      array-union: 2.1.0
-      dir-glob: 3.0.1
-      fast-glob: 3.3.3
-      ignore: 5.3.2
-      merge2: 1.4.1
-      slash: 3.0.0
-
   gopd@1.2.0: {}
 
   graphemer@1.4.0: {}
@@ -1904,6 +1873,8 @@ snapshots:
 
   ignore@5.3.2: {}
 
+  ignore@7.0.5: {}
+
   import-fresh@3.3.1:
     dependencies:
       parent-module: 1.0.1
@@ -1924,8 +1895,6 @@ snapshots:
     dependencies:
       is-extglob: 2.1.1
 
-  is-number@7.0.0: {}
-
   is-path-inside@3.0.3: {}
 
   isexe@2.0.0: {}
@@ -1963,19 +1932,16 @@ snapshots:
 
   math-intrinsics@1.1.0: {}
 
-  merge2@1.4.1: {}
-
-  micromatch@4.0.8:
-    dependencies:
-      braces: 3.0.3
-      picomatch: 2.3.1
-
   mime-db@1.52.0: {}
 
   mime-types@2.1.35:
     dependencies:
       mime-db: 1.52.0
 
+  minimatch@10.2.4:
+    dependencies:
+      brace-expansion: 5.0.4
+
   minimatch@3.1.5:
     dependencies:
       brace-expansion: 1.1.12
@@ -2029,11 +1995,9 @@ snapshots:
 
   path-key@3.1.1: {}
 
-  path-type@4.0.0: {}
-
   picocolors@1.1.1: {}
 
-  picomatch@2.3.1: {}
+  picomatch@4.0.3: {}
 
   postcss-selector-parser@6.1.2:
     dependencies:
@@ -2107,8 +2071,6 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  slash@3.0.0: {}
-
   source-map-js@1.2.1: {}
 
   strip-ansi@6.0.1:
@@ -2123,11 +2085,12 @@ snapshots:
 
   text-table@0.2.0: {}
 
-  to-regex-range@5.0.1:
+  tinyglobby@0.2.15:
     dependencies:
-      is-number: 7.0.0
+      fdir: 6.5.0(picomatch@4.0.3)
+      picomatch: 4.0.3
 
-  ts-api-utils@1.4.3(typescript@5.9.3):
+  ts-api-utils@2.4.0(typescript@5.9.3):
     dependencies:
       typescript: 5.9.3
 

+ 240 - 108
src/App.vue

@@ -12,24 +12,64 @@
         <span class="title-meta">· 总库位 {{ locations.length }}</span>
         <span class="title-meta">· 可用库位 {{ availableLocationsCount }}</span>
         <span class="title-legend">
-          <span class="legend-chip legend-chip-a">A</span>
-          <span class="legend-chip legend-chip-b">B</span>
-          <span class="legend-chip legend-chip-c">C</span>
+          <button
+            type="button"
+            :class="[
+              'legend-chip',
+              'legend-chip-a',
+              { 'legend-chip-inactive': !categoryColorVisibility.A }
+            ]"
+            @click="toggleCategoryColorVisibility('A')"
+          >
+            A
+          </button>
+          <button
+            type="button"
+            :class="[
+              'legend-chip',
+              'legend-chip-b',
+              { 'legend-chip-inactive': !categoryColorVisibility.B }
+            ]"
+            @click="toggleCategoryColorVisibility('B')"
+          >
+            B
+          </button>
+          <button
+            type="button"
+            :class="[
+              'legend-chip',
+              'legend-chip-c',
+              { 'legend-chip-inactive': !categoryColorVisibility.C }
+            ]"
+            @click="toggleCategoryColorVisibility('C')"
+          >
+            C
+          </button>
         </span>
       </h1>
       <div class="controls">
         <label class="filter-item">
           <span class="selector-label">库位类型</span>
-          <select v-model="selectedCategory" class="level-select">
+          <select
+            v-model="selectedCategory"
+            class="level-select"
+          >
             <option value="">全部</option>
-            <option v-for="category in categoryOptions" :key="category" :value="category">
+            <option
+              v-for="category in categoryOptions"
+              :key="category"
+              :value="category"
+            >
               {{ category }}
             </option>
           </select>
         </label>
         <label class="filter-item">
           <span class="selector-label">库位属性</span>
-          <select v-model="selectedLocationAttribute" class="level-select">
+          <select
+            v-model="selectedLocationAttribute"
+            class="level-select"
+          >
             <option value="">全部</option>
             <option
               v-for="attribute in locationAttributeOptions"
@@ -42,7 +82,10 @@
         </label>
         <label class="filter-item">
           <span class="selector-label">容器</span>
-          <select v-model="selectedHasContainer" class="level-select">
+          <select
+            v-model="selectedHasContainer"
+            class="level-select"
+          >
             <option value="">全部</option>
             <option value="Y">有容器</option>
             <option value="N">无容器</option>
@@ -57,22 +100,34 @@
               type="text"
               placeholder="输入库位组"
               @keydown.enter="applyLocGroupFilter"
-            />
+            >
             <button
               v-if="locGroupKeywordInput"
               class="filter-clear-btn"
               type="button"
               @click="clearLocGroupFilter"
             >
-              <svg viewBox="0 0 16 16" aria-hidden="true" class="filter-action-icon">
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="filter-action-icon"
+              >
                 <path
                   d="M4.22 4.22a.75.75 0 0 1 1.06 0L8 6.94l2.72-2.72a.75.75 0 1 1 1.06 1.06L9.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L8 9.06l-2.72 2.72a.75.75 0 1 1-1.06-1.06L6.94 8 4.22 5.28a.75.75 0 0 1 0-1.06z"
                   fill="currentColor"
                 />
               </svg>
             </button>
-            <button class="filter-confirm-btn" type="button" @click="applyLocGroupFilter">
-              <svg viewBox="0 0 16 16" aria-hidden="true" class="filter-action-icon">
+            <button
+              class="filter-confirm-btn"
+              type="button"
+              @click="applyLocGroupFilter"
+            >
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="filter-action-icon"
+              >
                 <path
                   d="M6.5 2.5a4 4 0 1 0 2.47 7.15l2.69 2.68 1.06-1.06-2.68-2.69A4 4 0 0 0 6.5 2.5zm0 1.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5z"
                   fill="currentColor"
@@ -90,22 +145,34 @@
               type="text"
               placeholder="输入库位号"
               @keydown.enter="applyLocationIdFilter"
-            />
+            >
             <button
               v-if="locationIdKeywordInput"
               class="filter-clear-btn"
               type="button"
               @click="clearLocationIdFilter"
             >
-              <svg viewBox="0 0 16 16" aria-hidden="true" class="filter-action-icon">
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="filter-action-icon"
+              >
                 <path
                   d="M4.22 4.22a.75.75 0 0 1 1.06 0L8 6.94l2.72-2.72a.75.75 0 1 1 1.06 1.06L9.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L8 9.06l-2.72 2.72a.75.75 0 1 1-1.06-1.06L6.94 8 4.22 5.28a.75.75 0 0 1 0-1.06z"
                   fill="currentColor"
                 />
               </svg>
             </button>
-            <button class="filter-confirm-btn" type="button" @click="applyLocationIdFilter">
-              <svg viewBox="0 0 16 16" aria-hidden="true" class="filter-action-icon">
+            <button
+              class="filter-confirm-btn"
+              type="button"
+              @click="applyLocationIdFilter"
+            >
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="filter-action-icon"
+              >
                 <path
                   d="M6.5 2.5a4 4 0 1 0 2.47 7.15l2.69 2.68 1.06-1.06-2.68-2.69A4 4 0 0 0 6.5 2.5zm0 1.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5z"
                   fill="currentColor"
@@ -121,28 +188,36 @@
             class="toggle-input"
             type="checkbox"
             @change="handleGroupBorderToggle"
-          />
+          >
           <span class="toggle-track">
-            <span class="toggle-thumb"></span>
+            <span class="toggle-thumb" />
           </span>
         </label>
+        <label class="filter-item">
+          <span class="selector-label">库区</span>
+          <select
+            v-model="selectedZoneId"
+            class="level-select"
+          >
+            <option value="">全部</option>
+            <option
+              v-for="zoneId in zoneOptions"
+              :key="zoneId"
+              :value="zoneId"
+            >
+              {{ zoneId }}
+            </option>
+          </select>
+        </label>
         <label class="toggle-item">
-          <span class="selector-label">库区边框</span>
+          <span class="selector-label">卡片</span>
           <input
-            :checked="showZoneBorder"
+            v-model="showTooltip"
             class="toggle-input"
             type="checkbox"
-            @change="handleZoneBorderToggle"
-          />
-          <span class="toggle-track">
-            <span class="toggle-thumb"></span>
-          </span>
-        </label>
-        <label class="toggle-item">
-          <span class="selector-label">卡片</span>
-          <input v-model="showTooltip" class="toggle-input" type="checkbox" />
+          >
           <span class="toggle-track">
-            <span class="toggle-thumb"></span>
+            <span class="toggle-thumb" />
           </span>
         </label>
         <select
@@ -150,11 +225,18 @@
           class="level-select level-select-floor"
           @change="handleLevelChange"
         >
-          <option v-for="level in levelRange" :key="level" :value="level">
+          <option
+            v-for="level in levelRange"
+            :key="level"
+            :value="level"
+          >
             {{ level }}层
           </option>
         </select>
-        <div ref="refreshControlRef" class="refresh-control">
+        <div
+          ref="refreshControlRef"
+          class="refresh-control"
+        >
           <button
             class="refresh-btn"
             :disabled="loading || refreshing"
@@ -175,13 +257,17 @@
               type="text"
               inputmode="numeric"
               @keydown.enter="applyRefreshInterval"
-            />
+            >
             <button
               class="refresh-popover-confirm"
               type="button"
               @click="applyRefreshInterval"
             >
-              <svg viewBox="0 0 16 16" aria-hidden="true" class="filter-action-icon">
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="filter-action-icon"
+              >
                 <path
                   d="M6.46 11.03 3.43 8a.75.75 0 1 1 1.06-1.06l1.97 1.96 5.05-5.04A.75.75 0 0 1 12.57 4.9l-5.58 5.59a.75.75 0 0 1-1.06 0z"
                   fill="currentColor"
@@ -190,27 +276,44 @@
             </button>
           </div>
         </div>
-        <button class="logout-btn" @click="handleLogout">退出</button>
+        <button
+          class="logout-btn"
+          @click="handleLogout"
+        >
+          退出
+        </button>
       </div>
     </header>
 
     <main class="main-content">
-      <div v-if="loading" class="loading">加载中...</div>
-      <div v-else-if="error" class="error">
+      <div
+        v-if="loading"
+        class="loading"
+      >
+        加载中...
+      </div>
+      <div
+        v-else-if="error"
+        class="error"
+      >
         {{ error }}
       </div>
-      <div v-else class="map-container">
+      <div
+        v-else
+        class="map-container"
+      >
         <WarehouseMap
           :locations="locations"
           :current-level="currentLevel"
           :selected-category="selectedCategory"
           :selected-location-attribute="selectedLocationAttribute"
           :selected-has-container="selectedHasContainer"
+          :selected-zone-id="selectedZoneId"
           :loc-group-keyword="appliedLocGroupKeyword"
           :location-id-keyword="appliedLocationIdKeyword"
           :show-group-border="showGroupBorder"
-          :show-zone-border="showZoneBorder"
           :show-tooltip="showTooltip"
+          :category-color-visibility="categoryColorVisibility"
           @select-loc-group="handleSelectLocGroup"
           @select-location-id="handleSelectLocationId"
         />
@@ -233,7 +336,11 @@ const REFRESH_INTERVAL_STORAGE_KEY = 'warehouse-map.refresh-interval-ms'
 const getInitialLevel = () => {
   const savedLevel = window.localStorage.getItem(LEVEL_STORAGE_KEY)
   const parsedLevel = savedLevel ? Number(savedLevel) : NaN
-  if (Number.isInteger(parsedLevel) && parsedLevel >= config.minLevel && parsedLevel <= config.maxLevel) {
+  if (
+    Number.isInteger(parsedLevel) &&
+    parsedLevel >= config.minLevel &&
+    parsedLevel <= config.maxLevel
+  ) {
     return parsedLevel
   }
   return config.minLevel
@@ -258,12 +365,17 @@ const showLoginModal = ref(false)
 const selectedCategory = ref('')
 const selectedLocationAttribute = ref<LocationAttributeCode | ''>('')
 const selectedHasContainer = ref<'Y' | 'N' | ''>('')
+const selectedZoneId = ref('')
+const categoryColorVisibility = ref<Record<'A' | 'B' | 'C', boolean>>({
+  A: true,
+  B: true,
+  C: true
+})
 const locGroupKeywordInput = ref('')
 const appliedLocGroupKeyword = ref('')
 const locationIdKeywordInput = ref('')
 const appliedLocationIdKeyword = ref('')
 const showGroupBorder = ref(false)
-const showZoneBorder = ref(false)
 const showTooltip = ref(true)
 const refreshIntervalMs = ref(getInitialRefreshInterval())
 const showRefreshPopover = ref(false)
@@ -299,13 +411,26 @@ const availableLocationsCount = computed(() => {
 })
 
 const locationAttributeOptions = computed<LocationAttributeCode[]>(() => {
-  return [...new Set(locations.value.map((loc) => loc.locationAttribute).filter(Boolean))] as LocationAttributeCode[]
+  return [
+    ...new Set(locations.value.map((loc) => loc.locationAttribute).filter(Boolean))
+  ] as LocationAttributeCode[]
+})
+
+const zoneOptions = computed(() => {
+  return [...new Set(locations.value.map((loc) => loc.zoneId).filter(Boolean))].sort()
 })
 
 const getLocationAttributeLabel = (attribute: LocationAttributeCode) => {
   return LOCATION_ATTRIBUTE_LABEL_MAP[attribute] || attribute
 }
 
+const toggleCategoryColorVisibility = (category: 'A' | 'B' | 'C') => {
+  categoryColorVisibility.value = {
+    ...categoryColorVisibility.value,
+    [category]: !categoryColorVisibility.value[category]
+  }
+}
+
 const copyText = async (text: string) => {
   if (!text) return
 
@@ -346,19 +471,7 @@ const clearLocationIdFilter = () => {
 }
 
 const handleGroupBorderToggle = (event: Event) => {
-  const enabled = (event.target as HTMLInputElement).checked
-  showGroupBorder.value = enabled
-  if (enabled) {
-    showZoneBorder.value = false
-  }
-}
-
-const handleZoneBorderToggle = (event: Event) => {
-  const enabled = (event.target as HTMLInputElement).checked
-  showZoneBorder.value = enabled
-  if (enabled) {
-    showGroupBorder.value = false
-  }
+  showGroupBorder.value = (event.target as HTMLInputElement).checked
 }
 
 const refreshCountdownText = computed(() => {
@@ -391,9 +504,9 @@ const loadLocationData = async (options: { silent?: boolean } = {}) => {
     })
     locations.value = data
     hasLoadedOnce.value = true
-  } catch (err: any) {
+  } catch (err: unknown) {
     if (!shouldUseSilentRefresh) {
-      error.value = err.message || '加载数据失败,请检查接口连接'
+      error.value = err instanceof Error ? err.message : '加载数据失败,请检查接口连接'
     }
     console.error('Failed to load location data:', err)
   } finally {
@@ -543,13 +656,13 @@ onBeforeUnmount(() => {
   height: 100vh;
   display: flex;
   flex-direction: column;
-  background: #0f1419;
+  background: #000000;
 }
 
 .header {
   padding: 10px 18px;
-  background: linear-gradient(180deg, #24313b 0%, #161d24 100%);
-  border-bottom: 1px solid #6f8ca7;
+  background: linear-gradient(180deg, #050505 0%, #000000 100%);
+  border-bottom: 1px solid #1c1c1c;
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -562,14 +675,14 @@ onBeforeUnmount(() => {
   font-size: 18px;
   font-weight: bold;
   line-height: 1.1;
-  color: #d5e4f2;
-  text-shadow: 0 0 8px rgba(111, 140, 167, 0.18);
+  color: #f2f2f2;
+  text-shadow: 0 0 8px rgba(255, 255, 255, 0.08);
 }
 
 .title-meta {
   font-size: 11px;
   font-weight: normal;
-  color: rgba(230, 237, 243, 0.75);
+  color: rgba(255, 255, 255, 0.6);
 }
 
 .title-legend {
@@ -591,6 +704,22 @@ onBeforeUnmount(() => {
   font-weight: 600;
   line-height: 1;
   color: #f4f7fa;
+  border: none;
+  cursor: pointer;
+  transition:
+    transform 0.2s ease,
+    opacity 0.2s ease,
+    box-shadow 0.2s ease;
+}
+
+.legend-chip:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14);
+}
+
+.legend-chip-inactive {
+  opacity: 0.42;
+  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
 }
 
 .legend-chip-a {
@@ -617,14 +746,14 @@ onBeforeUnmount(() => {
   display: flex;
   align-items: center;
   gap: 6px;
-  color: #d4e2f2;
+  color: #d8d8d8;
 }
 
 .filter-item {
   display: flex;
   align-items: center;
   gap: 4px;
-  color: #d4e2f2;
+  color: #d8d8d8;
 }
 
 .filter-input-item {
@@ -635,20 +764,20 @@ onBeforeUnmount(() => {
   display: flex;
   align-items: center;
   gap: 4px;
-  color: #d4e2f2;
+  color: #d8d8d8;
 }
 
 .selector-label {
   font-size: 11px;
-  color: #b9cbdb;
+  color: #8e8e8e;
 }
 
 .level-select {
   min-width: 76px;
   padding: 6px 24px 6px 8px;
-  background: rgba(255, 255, 255, 0.04);
-  border: 1px solid rgba(111, 140, 167, 0.4);
-  color: #eef4f8;
+  background: rgba(255, 255, 255, 0.03);
+  border: 1px solid #252525;
+  color: #f2f2f2;
   cursor: pointer;
   font-size: 12px;
   transition: all 0.3s;
@@ -656,12 +785,14 @@ onBeforeUnmount(() => {
   outline: none;
   appearance: none;
   background-image:
-    linear-gradient(45deg, transparent 50%, #7a96af 50%),
-    linear-gradient(135deg, #7a96af 50%, transparent 50%);
+    linear-gradient(45deg, transparent 50%, #6d6d6d 50%),
+    linear-gradient(135deg, #6d6d6d 50%, transparent 50%);
   background-position:
     calc(100% - 13px) calc(50% - 2px),
     calc(100% - 8px) calc(50% - 2px);
-  background-size: 5px 5px, 5px 5px;
+  background-size:
+    5px 5px,
+    5px 5px;
   background-repeat: no-repeat;
   line-height: 1;
 }
@@ -680,9 +811,9 @@ onBeforeUnmount(() => {
   width: 100%;
   padding: 6px 8px;
   padding-right: 50px;
-  background: rgba(255, 255, 255, 0.04);
-  border: 1px solid rgba(111, 140, 167, 0.4);
-  color: #eef4f8;
+  background: rgba(255, 255, 255, 0.03);
+  border: 1px solid #252525;
+  color: #f2f2f2;
   font-size: 12px;
   border-radius: 4px;
   outline: none;
@@ -690,13 +821,13 @@ onBeforeUnmount(() => {
 }
 
 .filter-input::placeholder {
-  color: rgba(185, 203, 219, 0.58);
+  color: rgba(255, 255, 255, 0.28);
 }
 
 .filter-input:hover,
 .filter-input:focus {
-  background: rgba(111, 140, 167, 0.12);
-  border-color: #7a96af;
+  background: rgba(255, 255, 255, 0.05);
+  border-color: #3a3a3a;
 }
 
 .filter-confirm-btn {
@@ -709,9 +840,9 @@ onBeforeUnmount(() => {
   align-items: center;
   justify-content: center;
   border: none;
-  border-left: 1px solid rgba(111, 140, 167, 0.3);
+  border-left: 1px solid #202020;
   background: transparent;
-  color: #dce8f3;
+  color: #cfcfcf;
   cursor: pointer;
   border-radius: 0 3px 3px 0;
 }
@@ -726,15 +857,15 @@ onBeforeUnmount(() => {
   align-items: center;
   justify-content: center;
   border: none;
-  border-left: 1px solid rgba(111, 140, 167, 0.3);
+  border-left: 1px solid #202020;
   background: transparent;
-  color: #dce8f3;
+  color: #cfcfcf;
   cursor: pointer;
 }
 
 .filter-clear-btn:hover,
 .filter-confirm-btn:hover {
-  background: rgba(111, 140, 167, 0.14);
+  background: rgba(255, 255, 255, 0.06);
 }
 
 .filter-action-icon {
@@ -753,8 +884,8 @@ onBeforeUnmount(() => {
   width: 34px;
   height: 18px;
   border-radius: 999px;
-  background: rgba(111, 140, 167, 0.24);
-  border: 1px solid rgba(111, 140, 167, 0.5);
+  background: #111111;
+  border: 1px solid #2c2c2c;
   transition: all 0.2s;
   cursor: pointer;
 }
@@ -766,13 +897,13 @@ onBeforeUnmount(() => {
   width: 14px;
   height: 14px;
   border-radius: 50%;
-  background: #eef4f8;
+  background: #f2f2f2;
   transition: transform 0.2s;
 }
 
 .toggle-input:checked + .toggle-track {
-  background: rgba(111, 140, 167, 0.45);
-  border-color: #d7e2ea;
+  background: #2b2b2b;
+  border-color: #5a5a5a;
 }
 
 .toggle-input:checked + .toggle-track .toggle-thumb {
@@ -781,16 +912,16 @@ onBeforeUnmount(() => {
 
 .level-select:hover,
 .level-select:focus {
-  background: rgba(111, 140, 167, 0.12);
-  border-color: #7a96af;
+  background: rgba(255, 255, 255, 0.05);
+  border-color: #3a3a3a;
 }
 
 .refresh-btn {
   min-width: 58px;
   padding: 6px 8px;
-  background: rgba(111, 140, 167, 0.12);
-  border: 1px solid rgba(111, 140, 167, 0.5);
-  color: #e6edf3;
+  background: #101010;
+  border: 1px solid #282828;
+  color: #e8e8e8;
   cursor: pointer;
   font-size: 11px;
   transition: all 0.3s;
@@ -810,8 +941,8 @@ onBeforeUnmount(() => {
   align-items: center;
   gap: 4px;
   padding: 4px;
-  background: rgba(20, 29, 37, 0.96);
-  border: 1px solid rgba(111, 140, 167, 0.4);
+  background: rgba(4, 4, 4, 0.98);
+  border: 1px solid #242424;
   border-radius: 4px;
   box-shadow: 0 8px 18px rgba(0, 0, 0, 0.24);
   z-index: 20;
@@ -820,9 +951,9 @@ onBeforeUnmount(() => {
 .refresh-popover-input {
   width: 56px;
   padding: 6px 8px;
-  background: rgba(255, 255, 255, 0.04);
-  border: 1px solid rgba(111, 140, 167, 0.4);
-  color: #eef4f8;
+  background: rgba(255, 255, 255, 0.03);
+  border: 1px solid #252525;
+  color: #f2f2f2;
   font-size: 12px;
   border-radius: 4px;
   outline: none;
@@ -830,8 +961,8 @@ onBeforeUnmount(() => {
 }
 
 .refresh-popover-input:focus {
-  background: rgba(111, 140, 167, 0.12);
-  border-color: #7a96af;
+  background: rgba(255, 255, 255, 0.05);
+  border-color: #3a3a3a;
 }
 
 .refresh-popover-confirm {
@@ -840,21 +971,21 @@ onBeforeUnmount(() => {
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  border: 1px solid rgba(111, 140, 167, 0.3);
-  background: rgba(255, 255, 255, 0.04);
-  color: #dce8f3;
+  border: 1px solid #252525;
+  background: rgba(255, 255, 255, 0.03);
+  color: #cfcfcf;
   border-radius: 4px;
   cursor: pointer;
 }
 
 .refresh-popover-confirm:hover {
-  background: rgba(111, 140, 167, 0.14);
-  border-color: #7a96af;
+  background: rgba(255, 255, 255, 0.06);
+  border-color: #3a3a3a;
 }
 
 .refresh-btn:hover:not(:disabled) {
-  background: rgba(111, 140, 167, 0.2);
-  border-color: #7a96af;
+  background: #181818;
+  border-color: #3a3a3a;
 }
 
 .refresh-btn:disabled {
@@ -884,6 +1015,7 @@ onBeforeUnmount(() => {
   flex: 1;
   padding: 0 12px 12px;
   overflow: auto;
+  background: #000000;
 }
 
 .loading,
@@ -893,7 +1025,7 @@ onBeforeUnmount(() => {
   align-items: center;
   height: 100%;
   font-size: 18px;
-  color: #d5e4f2;
+  color: #f2f2f2;
 }
 
 .error {

+ 34 - 12
src/components/LoginModal.vue

@@ -1,11 +1,20 @@
 <template>
-  <div v-if="visible" class="modal-overlay" @click.self="handleCancel">
+  <div
+    v-if="visible"
+    class="modal-overlay"
+    @click.self="handleCancel"
+  >
     <div class="modal-content">
       <div class="modal-header">
         <h2>登录</h2>
       </div>
       <div class="modal-body">
-        <div v-if="error" class="error-message">{{ error }}</div>
+        <div
+          v-if="error"
+          class="error-message"
+        >
+          {{ error }}
+        </div>
         <form @submit.prevent="handleSubmit">
           <div class="form-group">
             <label for="username">账号</label>
@@ -16,7 +25,7 @@
               placeholder="请输入账号"
               required
               autocomplete="username"
-            />
+            >
           </div>
           <div class="form-group">
             <label for="password">密码</label>
@@ -28,12 +37,12 @@
                 placeholder="请输入密码"
                 required
                 autocomplete="current-password"
-              />
+              >
               <button
                 type="button"
                 class="password-toggle"
-                @click="togglePasswordVisibility"
                 :title="showPassword ? '隐藏密码' : '显示密码'"
+                @click="togglePasswordVisibility"
               >
                 <svg
                   v-if="!showPassword"
@@ -47,8 +56,12 @@
                   stroke-linecap="round"
                   stroke-linejoin="round"
                 >
-                  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                  <circle cx="12" cy="12" r="3"></circle>
+                  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
+                  <circle
+                    cx="12"
+                    cy="12"
+                    r="3"
+                  />
                 </svg>
                 <svg
                   v-else
@@ -64,14 +77,23 @@
                 >
                   <path
                     d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
-                  ></path>
-                  <line x1="1" y1="1" x2="23" y2="23"></line>
+                  />
+                  <line
+                    x1="1"
+                    y1="1"
+                    x2="23"
+                    y2="23"
+                  />
                 </svg>
               </button>
             </div>
           </div>
           <div class="form-actions">
-            <button type="submit" class="btn-primary" :disabled="loading">
+            <button
+              type="submit"
+              class="btn-primary"
+              :disabled="loading"
+            >
               {{ loading ? '登录中...' : '登录' }}
             </button>
           </div>
@@ -128,8 +150,8 @@ const handleSubmit = async () => {
 
     setToken(response.token)
     emit('success')
-  } catch (err: any) {
-    error.value = err.message || '登录失败,请稍后重试'
+  } catch (err: unknown) {
+    error.value = err instanceof Error ? err.message : '登录失败,请稍后重试'
     console.error('Login failed:', err)
   } finally {
     loading.value = false

+ 175 - 98
src/components/WarehouseMap.vue

@@ -1,8 +1,20 @@
 <template>
   <div class="warehouse-map">
-    <div v-if="gridData.length === 0" class="no-data">暂无数据</div>
-    <div v-else ref="mapWrapperRef" class="map-wrapper">
-      <div class="map-grid" :style="gridStyle">
+    <div
+      v-if="gridData.length === 0"
+      class="no-data"
+    >
+      暂无数据
+    </div>
+    <div
+      v-else
+      ref="mapWrapperRef"
+      class="map-wrapper"
+    >
+      <div
+        class="map-grid"
+        :style="gridStyle"
+      >
         <div
           v-for="(cell, index) in gridData"
           :key="index"
@@ -15,27 +27,47 @@
           @click="handleCellClick(cell)"
           @contextmenu.prevent="handleCellContextMenu(cell)"
         >
-          <div v-if="isLocationCell(cell) && isCellVisible(cell)" class="cell-content">
-            <div class="category-badge" :style="getCategoryStyle(cell)">
+          <div
+            v-if="isLocationCell(cell) && isCellVisible(cell)"
+            class="cell-content"
+          >
+            <div
+              class="category-badge"
+              :style="getCategoryStyle(cell)"
+            >
               {{ getHeatLabel(cell) }}
             </div>
-            <div class="loc-group">{{ cell.locGroup1 }}</div>
+            <div class="loc-group">
+              {{ cell.locGroup1 }}
+            </div>
             <div
               :class="[
                 'location-id',
                 {
-                  'location-id-mismatch': cell.categoryMismatch && !hasAbnormalLocationAttribute(cell),
+                  'location-id-mismatch':
+                    cell.categoryMismatch && !hasAbnormalLocationAttribute(cell),
                   'location-id-abnormal-attribute': hasAbnormalLocationAttribute(cell)
                 }
               ]"
             >
               {{ cell.locationId }}
             </div>
-            <div v-if="cell.categoryMismatch" class="location-attribute-tag">热度编号异常</div>
-            <div v-if="hasAbnormalLocationAttribute(cell)" class="location-attribute-tag">
+            <div
+              v-if="cell.categoryMismatch"
+              class="location-attribute-tag"
+            >
+              热度编号异常
+            </div>
+            <div
+              v-if="hasAbnormalLocationAttribute(cell)"
+              class="location-attribute-tag"
+            >
               {{ getLocationAttributeLabel(cell) }}
             </div>
-            <div v-if="cell.containerCode" class="container-code">
+            <div
+              v-if="cell.containerCode"
+              class="container-code"
+            >
               {{ cell.containerCode }}
             </div>
           </div>
@@ -52,7 +84,11 @@
         class="cell-tooltip"
         :style="tooltipStyle"
       >
-        <div v-for="(line, index) in tooltipLines" :key="index" class="cell-tooltip-line">
+        <div
+          v-for="(line, index) in tooltipLines"
+          :key="index"
+          class="cell-tooltip-line"
+        >
           {{ line }}
         </div>
       </div>
@@ -73,12 +109,13 @@ interface Props {
   locations: LocationResourceDataVO[]
   currentLevel: number
   selectedCategory: string
+  categoryColorVisibility: Record<string, boolean>
   selectedLocationAttribute: string
   selectedHasContainer: string
+  selectedZoneId: string
   locGroupKeyword: string
   locationIdKeyword: string
   showGroupBorder: boolean
-  showZoneBorder: boolean
   showTooltip: boolean
 }
 
@@ -97,7 +134,13 @@ interface GridCell extends LocationResourceDataVO {
   categoryMismatch: boolean
 }
 
-type MapCell = GridCell | WarehouseLayoutSpecialCell | null
+interface AisleCell {
+  type: 'aisle'
+  gridRow: number
+  gridCol: number
+}
+
+type MapCell = GridCell | WarehouseLayoutSpecialCell | AisleCell
 
 const props = defineProps<Props>()
 const emit = defineEmits<{
@@ -152,6 +195,12 @@ const CATEGORY_THEME_MAP: Record<string, { solid: string; soft: string; text: st
   }
 }
 
+const INACTIVE_CATEGORY_THEME = {
+  solid: '#5f6b7a',
+  soft: 'rgba(95, 107, 122, 0.16)',
+  text: '#eef4f8'
+}
+
 const LOCATION_ATTRIBUTE_LABEL_MAP: Record<string, string> = {
   OK: '正常',
   FI: '禁入',
@@ -188,19 +237,17 @@ const parsedLocations = computed<GridCell[]>(() => {
       ...loc,
       parsed: parseLocationId(loc.locationId)
     }))
-    .filter((loc): loc is LocationResourceDataVO & { parsed: ParsedLocation } => loc.parsed !== null)
+    .filter(
+      (loc): loc is LocationResourceDataVO & { parsed: ParsedLocation } => loc.parsed !== null
+    )
     .map((loc) => {
       const normalizedCategory = normalizeCategory(loc.category)
       const expectedCategory = DEPTH_CATEGORY_MAP[loc.parsed.depth] || null
-      const enhancedPosition = applyWarehouseLayoutEnhancement(
-        props.currentLevel,
-        loc.parsed,
-        {
-          // 原始布局规则:第一个数字是列,第二个数字是行。
-          gridRow: loc.parsed.y,
-          gridCol: loc.parsed.x
-        }
-      )
+      const enhancedPosition = applyWarehouseLayoutEnhancement(props.currentLevel, loc.parsed, {
+        // 原始布局规则:第一个数字是列,第二个数字是行。
+        gridRow: loc.parsed.y,
+        gridCol: loc.parsed.x
+      })
 
       return {
         ...loc,
@@ -218,12 +265,7 @@ const parsedLocations = computed<GridCell[]>(() => {
 const specialCells = computed(() => getWarehouseLayoutSpecialCells(props.currentLevel))
 
 const specialCellMap = computed(() => {
-  return new Map(
-    specialCells.value.map((cell) => [
-      `${cell.gridRow}-${cell.gridCol}`,
-      cell
-    ])
-  )
+  return new Map(specialCells.value.map((cell) => [`${cell.gridRow}-${cell.gridCol}`, cell]))
 })
 
 const locationMap = computed(() => {
@@ -283,8 +325,11 @@ const gridData = computed(() => {
         grid.push(specialCell)
         continue
       }
-
-      grid.push(null)
+      grid.push({
+        type: 'aisle',
+        gridRow: x,
+        gridCol: y
+      })
     }
   }
 
@@ -300,12 +345,17 @@ const gridStyle = computed(() => {
   const availableWidth = Math.max(wrapperSize.value.width - horizontalPadding, 0)
   const availableHeight = Math.max(wrapperSize.value.height - verticalPadding, 0)
 
-  const cellWidth = availableWidth > 0
-    ? Math.max((availableWidth - (gridMetrics.value.cols - 1) * gap) / gridMetrics.value.cols, 28)
-    : 72
-  const cellHeight = availableHeight > 0
-    ? Math.max((availableHeight - (gridMetrics.value.rows - 1) * gap) / gridMetrics.value.rows, 24)
-    : 60
+  const cellWidth =
+    availableWidth > 0
+      ? Math.max((availableWidth - (gridMetrics.value.cols - 1) * gap) / gridMetrics.value.cols, 28)
+      : 72
+  const cellHeight =
+    availableHeight > 0
+      ? Math.max(
+          (availableHeight - (gridMetrics.value.rows - 1) * gap) / gridMetrics.value.rows,
+          24
+        )
+      : 60
   const compactSize = Math.min(cellWidth, cellHeight)
 
   const showBadge = compactSize > 38 ? 'inline-flex' : 'none'
@@ -333,24 +383,30 @@ const gridStyle = computed(() => {
 
 const isCellMatched = (cell: GridCell) => {
   const matchedCategory = !props.selectedCategory || cell.category === props.selectedCategory
-  const matchedLocationAttribute = !props.selectedLocationAttribute
-    || cell.locationAttribute === props.selectedLocationAttribute
+  const matchedLocationAttribute =
+    !props.selectedLocationAttribute || cell.locationAttribute === props.selectedLocationAttribute
   const hasContainer = Boolean(cell.containerCode && cell.containerCode.trim())
-  const matchedHasContainer = !props.selectedHasContainer
-    || (props.selectedHasContainer === 'Y' && hasContainer)
-    || (props.selectedHasContainer === 'N' && !hasContainer)
+  const matchedHasContainer =
+    !props.selectedHasContainer ||
+    (props.selectedHasContainer === 'Y' && hasContainer) ||
+    (props.selectedHasContainer === 'N' && !hasContainer)
+  const matchedZoneId = !props.selectedZoneId || String(cell.zoneId || '') === props.selectedZoneId
   const normalizedKeyword = props.locGroupKeyword.trim().toUpperCase()
-  const matchedLocGroup = !normalizedKeyword
-    || cell.locGroup1.toUpperCase().includes(normalizedKeyword)
+  const matchedLocGroup =
+    !normalizedKeyword || cell.locGroup1.toUpperCase().includes(normalizedKeyword)
   const normalizedLocationIdKeyword = props.locationIdKeyword.trim().toUpperCase()
-  const matchedLocationId = !normalizedLocationIdKeyword
-    || cell.locationId.toUpperCase().includes(normalizedLocationIdKeyword)
-
-  return matchedCategory
-    && matchedLocationAttribute
-    && matchedHasContainer
-    && matchedLocGroup
-    && matchedLocationId
+  const matchedLocationId =
+    !normalizedLocationIdKeyword ||
+    cell.locationId.toUpperCase().includes(normalizedLocationIdKeyword)
+
+  return (
+    matchedCategory &&
+    matchedLocationAttribute &&
+    matchedHasContainer &&
+    matchedZoneId &&
+    matchedLocGroup &&
+    matchedLocationId
+  )
 }
 
 const getGridPointByIndex = (index: number) => {
@@ -382,10 +438,12 @@ const isCellInSelectedRange = (cell: GridCell) => {
     return true
   }
 
-  return cell.parsed.gridRow >= selectedRange.value.rowStart
-    && cell.parsed.gridRow <= selectedRange.value.rowEnd
-    && cell.parsed.gridCol >= selectedRange.value.colStart
-    && cell.parsed.gridCol <= selectedRange.value.colEnd
+  return (
+    cell.parsed.gridRow >= selectedRange.value.rowStart &&
+    cell.parsed.gridRow <= selectedRange.value.rowEnd &&
+    cell.parsed.gridCol >= selectedRange.value.colStart &&
+    cell.parsed.gridCol <= selectedRange.value.colEnd
+  )
 }
 
 const isCellVisible = (cell: GridCell) => {
@@ -393,7 +451,7 @@ const isCellVisible = (cell: GridCell) => {
 }
 
 const isSpecialCell = (cell: MapCell): cell is WarehouseLayoutSpecialCell => {
-  return Boolean(cell && 'type' in cell)
+  return Boolean(cell && 'type' in cell && cell.type !== 'aisle')
 }
 
 const isLocationCell = (cell: MapCell): cell is GridCell => {
@@ -401,12 +459,25 @@ const isLocationCell = (cell: MapCell): cell is GridCell => {
 }
 
 const getCellClass = (cell: MapCell) => {
-  if (isSpecialCell(cell)) return [cell.type]
-  if (!cell) return ['aisle']
-  if (!isCellVisible(cell)) return ['aisle']
+  if (isSpecialCell(cell)) {
+    return [cell.type]
+  }
 
-  const classNames = [`category-${cell.category.toLowerCase()}`]
-  if (hasActiveBorder() && hasSameBorderNeighbor(cell)) {
+  if (!isLocationCell(cell)) {
+    return ['aisle']
+  }
+
+  if (!isCellVisible(cell)) {
+    return ['aisle']
+  }
+
+  const classNames = ['location-cell']
+  if (props.categoryColorVisibility[cell.category] !== false) {
+    classNames.push(`category-${cell.category.toLowerCase()}`)
+  } else {
+    classNames.push('category-muted')
+  }
+  if (props.showGroupBorder && hasSameBorderNeighbor(cell)) {
     classNames.push('grouped')
   }
   return classNames
@@ -431,11 +502,12 @@ const getCellTitle = (cell: GridCell | null) => {
   if (!cell) return '过道'
   if (!isCellMatched(cell)) return '未命中筛选条件'
   if (!isCellInSelectedRange(cell)) return '未命中当前选区'
-  const mismatchText = cell.categoryMismatch && cell.expectedCategory
-    ? `\n异常: 深度${cell.parsed.depth} 期望热度${cell.expectedCategory}`
-    : cell.categoryMismatch
-      ? `\n异常: 深度${cell.parsed.depth} 未配置对应热度`
-      : ''
+  const mismatchText =
+    cell.categoryMismatch && cell.expectedCategory
+      ? `\n异常: 深度${cell.parsed.depth} 期望热度${cell.expectedCategory}`
+      : cell.categoryMismatch
+        ? `\n异常: 深度${cell.parsed.depth} 未配置对应热度`
+        : ''
   const locationAttributeLabel = cell.locationAttribute
     ? LOCATION_ATTRIBUTE_LABEL_MAP[cell.locationAttribute] || cell.locationAttribute
     : '-'
@@ -456,7 +528,10 @@ const getCellTitle = (cell: GridCell | null) => {
 }
 
 const getCategoryStyle = (cell: GridCell) => {
-  const theme = CATEGORY_THEME_MAP[cell.category]
+  const theme =
+    props.categoryColorVisibility[cell.category] === false
+      ? INACTIVE_CATEGORY_THEME
+      : CATEGORY_THEME_MAP[cell.category]
   return {
     background: theme?.solid || '#5f6b7a',
     color: theme?.text || '#fff'
@@ -475,17 +550,10 @@ const tooltipStyle = computed<CSSProperties>(() => ({
   top: `${tooltipPosition.value.y}px`
 }))
 
-const hasActiveBorder = () => {
-  return props.showGroupBorder || props.showZoneBorder
-}
-
 const getBorderGroupValue = (cell: GridCell) => {
   if (props.showGroupBorder) {
     return cell.locGroup1
   }
-  if (props.showZoneBorder) {
-    return cell.zoneId || ''
-  }
   return ''
 }
 
@@ -502,19 +570,25 @@ const isSameBorderNeighbor = (cell: GridCell, xOffset: number, yOffset: number)
 }
 
 const hasSameBorderNeighbor = (cell: GridCell) => {
-  return isSameBorderNeighbor(cell, -1, 0)
-    || isSameBorderNeighbor(cell, 1, 0)
-    || isSameBorderNeighbor(cell, 0, -1)
-    || isSameBorderNeighbor(cell, 0, 1)
+  return (
+    isSameBorderNeighbor(cell, -1, 0) ||
+    isSameBorderNeighbor(cell, 1, 0) ||
+    isSameBorderNeighbor(cell, 0, -1) ||
+    isSameBorderNeighbor(cell, 0, 1)
+  )
 }
 
 const getCellStyle = (cell: MapCell): CSSProperties => {
-  if (!isLocationCell(cell) || !isCellVisible(cell) || !hasActiveBorder() || !hasSameBorderNeighbor(cell)) {
+  if (
+    !isLocationCell(cell) ||
+    !isCellVisible(cell) ||
+    !props.showGroupBorder ||
+    !hasSameBorderNeighbor(cell)
+  ) {
     return {}
   }
 
   const borderWidth = 'var(--group-outline-width, 2px)'
-
   return {
     '--group-border-color': '#ffffff',
     '--group-border-top': isSameBorderNeighbor(cell, -1, 0) ? '0px' : borderWidth,
@@ -605,8 +679,8 @@ const handleCellMouseEnter = (cell: MapCell, index: number, event: MouseEvent) =
     const point = getGridPointByIndex(index)
     if (point) {
       selectionEnd.value = point
-      didSelectionMove.value = point.row !== selectionStart.value.row
-        || point.col !== selectionStart.value.col
+      didSelectionMove.value =
+        point.row !== selectionStart.value.row || point.col !== selectionStart.value.col
     }
     tooltipVisible.value = false
     hoveredCell.value = null
@@ -706,7 +780,7 @@ onBeforeUnmount(() => {
   justify-content: center;
   align-items: center;
   height: 100%;
-  color: #8ea0b1;
+  color: #8b8b8b;
   font-size: 16px;
 }
 
@@ -714,7 +788,7 @@ onBeforeUnmount(() => {
   flex: 1;
   position: relative;
   overflow: auto;
-  background: #121820;
+  background: #000000;
 }
 
 .map-grid {
@@ -754,16 +828,16 @@ onBeforeUnmount(() => {
 }
 
 .grid-cell.aisle {
-  background: #18202a;
+  background: #050505;
 }
 
 .grid-cell.wall {
-  background: #323841;
+  background: #151515;
   cursor: default;
 }
 
 .grid-cell.elevator {
-  background: #20303b;
+  background: #101010;
   cursor: default;
 }
 
@@ -779,9 +853,13 @@ onBeforeUnmount(() => {
   background: rgba(255, 255, 0, 0.16);
 }
 
+.grid-cell.category-muted {
+  background: rgba(95, 107, 122, 0.16);
+}
+
 .grid-cell:not(.aisle):not(.wall):not(.elevator):hover {
   transform: scale(1.03);
-  box-shadow: 0 0 12px rgba(223, 231, 239, 0.12);
+  box-shadow: 0 0 12px rgba(255, 255, 255, 0.08);
   z-index: 10;
 }
 
@@ -812,7 +890,7 @@ onBeforeUnmount(() => {
 .loc-group {
   display: var(--cell-group-display, -webkit-box);
   font-size: var(--cell-group-font-size, 10px);
-  color: #b0becb;
+  color: #a0a0a0;
   text-align: center;
   line-height: 1.2;
   word-break: break-all;
@@ -825,7 +903,7 @@ onBeforeUnmount(() => {
   display: block;
   font-size: var(--cell-id-font-size, 11px);
   font-weight: bold;
-  color: #eef4f8;
+  color: #f2f2f2;
   text-align: center;
   line-height: 1.1;
   white-space: nowrap;
@@ -855,7 +933,7 @@ onBeforeUnmount(() => {
 .container-code {
   max-width: 100%;
   font-size: calc(var(--cell-id-font-size, 11px) - 2px);
-  color: #b9c7d4;
+  color: #9a9a9a;
   text-align: center;
   line-height: 1.1;
   white-space: nowrap;
@@ -866,13 +944,13 @@ onBeforeUnmount(() => {
 .special-cell-label {
   font-size: calc(var(--cell-id-font-size, 11px) - 1px);
   font-weight: 700;
-  color: rgba(237, 243, 247, 0.82);
+  color: rgba(255, 255, 255, 0.76);
   letter-spacing: 1px;
   user-select: none;
 }
 
 .special-cell-label-elevator {
-  color: rgba(221, 234, 244, 0.92);
+  color: rgba(255, 255, 255, 0.86);
 }
 
 .cell-tooltip {
@@ -880,11 +958,11 @@ onBeforeUnmount(() => {
   z-index: 1000;
   max-width: 300px;
   padding: 10px 12px;
-  border: 1px solid rgba(255, 255, 255, 0.18);
+  border: 1px solid #1f1f1f;
   border-radius: 8px;
-  background: rgba(11, 17, 24, 0.96);
+  background: rgba(0, 0, 0, 0.96);
   box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
-  color: #edf3f7;
+  color: #f2f2f2;
   font-size: 12px;
   line-height: 1.45;
   pointer-events: none;
@@ -894,5 +972,4 @@ onBeforeUnmount(() => {
 .cell-tooltip-line + .cell-tooltip-line {
   margin-top: 3px;
 }
-
 </style>

+ 7 - 10
src/components/warehouse-layout-enhancers.ts

@@ -54,7 +54,10 @@ interface WarehouseLayoutRuleSet {
   columnCompactions: readonly ColumnCompactionRule[]
 }
 
-const scopedRows = <T extends object>(rows: readonly number[], rule: T): T & Pick<RowScopedRule, 'rows'> => ({
+const scopedRows = <T extends object>(
+  rows: readonly number[],
+  rule: T
+): T & Pick<RowScopedRule, 'rows'> => ({
   ...rule,
   rows
 })
@@ -229,14 +232,12 @@ const sumLocalColumnAisleOffsets = (
 const sumLocalColumnShiftOffsets = (
   parsedLocation: WarehouseLayoutParsedLocation,
   rules: readonly LocalColumnShiftRule[]
-) =>
-  sumColumnScopedRuleValues(parsedLocation, rules, (rule) => rule.shiftCols)
+) => sumColumnScopedRuleValues(parsedLocation, rules, (rule) => rule.shiftCols)
 
 const sumManualColumnOffsets = (
   parsedLocation: WarehouseLayoutParsedLocation,
   rules: readonly ManualColumnOffsetRule[]
-) =>
-  sumColumnScopedRuleValues(parsedLocation, rules, (rule) => rule.offsetCols)
+) => sumColumnScopedRuleValues(parsedLocation, rules, (rule) => rule.offsetCols)
 
 const matchesColumnScope = (column: number, rule: ColumnScopedRule) => {
   return column >= rule.startColumn && (rule.endColumn === undefined || column <= rule.endColumn)
@@ -264,11 +265,7 @@ const getBaseGridColOffset = (
 
   return (
     columnShiftCount +
-    sumGlobalColumnAisleOffsets(
-      parsedLocation.y,
-      effectiveColumn,
-      ruleSet.globalColumnAisles
-    ) +
+    sumGlobalColumnAisleOffsets(parsedLocation.y, effectiveColumn, ruleSet.globalColumnAisles) +
     sumLocalColumnAisleOffsets(parsedLocation.y, effectiveColumn, ruleSet.localColumnAisles)
   )
 }