handy 1 місяць тому
батько
коміт
76d97968d4
5 змінених файлів з 320 додано та 119 видалено
  1. 201 81
      src/App.vue
  2. 3 1
      src/components/LoginModal.vue
  3. 111 29
      src/components/WarehouseMap.vue
  4. 0 1
      src/config/index.ts
  5. 5 7
      src/utils/auth.ts

+ 201 - 81
src/App.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="dashboard">
+  <div :class="['dashboard', themeClass]">
     <LoginModal
       :visible="showLoginModal"
       @success="handleLoginSuccess"
@@ -9,8 +9,8 @@
     <header class="header">
       <h1 class="title">
         {{ systemTitle }}
-        <span class="title-meta">· 总库位 {{ locations.length }}</span>
-        <span class="title-meta">· 可用库位 {{ availableLocationsCount }}</span>
+        <span class="title-meta">· 总库位 <span class="title-meta-value">{{ locations.length }}</span></span>
+        <span class="title-meta">· 可用库位 <span class="title-meta-value">{{ availableLocationsCount }}</span></span>
         <span class="title-fill-rate">
           <span
             class="title-meta title-fill-rate-item"
@@ -18,7 +18,7 @@
             @mousemove="handleFillRateMouseMove"
             @mouseleave="handleFillRateMouseLeave"
           >
-            · 总库满度 {{ overallFillRate }}
+            · 总库满度 <span class="title-meta-value">{{ overallFillRate }}</span>
           </span>
           <span
             class="title-meta title-meta-a title-fill-rate-item"
@@ -26,7 +26,7 @@
             @mousemove="handleFillRateMouseMove"
             @mouseleave="handleFillRateMouseLeave"
           >
-            A {{ categoryFillRateMap.A }}
+            A <span class="title-meta-value">{{ categoryFillRateMap.A }}</span>
           </span>
           <span
             class="title-meta title-meta-b title-fill-rate-item"
@@ -34,7 +34,7 @@
             @mousemove="handleFillRateMouseMove"
             @mouseleave="handleFillRateMouseLeave"
           >
-            B {{ categoryFillRateMap.B }}
+            B <span class="title-meta-value">{{ categoryFillRateMap.B }}</span>
           </span>
           <span
             class="title-meta title-meta-c title-fill-rate-item"
@@ -42,7 +42,7 @@
             @mousemove="handleFillRateMouseMove"
             @mouseleave="handleFillRateMouseLeave"
           >
-            C {{ categoryFillRateMap.C }}
+            C <span class="title-meta-value">{{ categoryFillRateMap.C }}</span>
           </span>
         </span>
         <span class="title-legend">
@@ -310,6 +310,17 @@
             </button>
           </div>
         </div>
+        <label class="toggle-item theme-toggle">
+          <span class="selector-label">主题</span>
+          <input
+            v-model="isLightTheme"
+            class="toggle-input"
+            type="checkbox"
+          >
+          <span class="toggle-track">
+            <span class="toggle-thumb" />
+          </span>
+        </label>
         <button
           class="logout-btn"
           @click="handleLogout"
@@ -358,6 +369,7 @@
           :show-group-border="showGroupBorder"
           :show-tooltip="showTooltip"
           :category-color-visibility="categoryColorVisibility"
+          :theme-mode="isLightTheme ? 'light' : 'dark'"
           @select-loc-group="handleSelectLocGroup"
           @select-location-id="handleSelectLocationId"
         />
@@ -374,6 +386,7 @@ import {
   onBeforeUnmount,
   nextTick,
   watchEffect,
+  watch,
   type CSSProperties
 } from 'vue'
 import { fetchLocationData } from './api/location'
@@ -414,6 +427,17 @@ const refreshing = ref(false)
 const hasLoadedOnce = ref(false)
 const error = ref('')
 const showLoginModal = ref(false)
+const THEME_STORAGE_KEY = 'warehouse-map.theme'
+
+const getInitialTheme = () => {
+  const saved = window.localStorage.getItem(THEME_STORAGE_KEY)
+  if (saved === 'dark' || saved === 'light') {
+    return saved
+  }
+  return 'light'
+}
+
+const isLightTheme = ref(getInitialTheme() === 'light')
 const selectedCategory = ref('')
 const selectedLocationAttribute = ref<LocationAttributeCode | ''>('')
 const selectedHasContainer = ref<'Y' | 'N' | ''>('')
@@ -452,6 +476,8 @@ const LOCATION_ATTRIBUTE_LABEL_MAP: Record<LocationAttributeCode, string> = {
   SC: '管控'
 }
 
+const themeClass = computed(() => (isLightTheme.value ? 'theme-light' : 'theme-dark'))
+
 const levelRange = computed(() => {
   const levels = []
   for (let i = config.minLevel; i <= config.maxLevel; i++) {
@@ -504,7 +530,9 @@ const categoryFillRateMap = computed<Record<'A' | 'B' | 'C', string>>(() => {
   return categories.reduce(
     (result, category) => {
       const categoryLocations = locations.value.filter((loc) => loc.category === category)
-      const occupiedCount = categoryLocations.filter((loc) => hasContainer(loc.containerCode)).length
+      const occupiedCount = categoryLocations.filter((loc) =>
+        hasContainer(loc.containerCode)
+      ).length
       result[category] = formatFillRate(categoryLocations.length, occupiedCount)
       return result
     },
@@ -521,7 +549,9 @@ const categoryFillRateDetailMap = computed<Record<'A' | 'B' | 'C', string>>(() =
   return categories.reduce(
     (result, category) => {
       const categoryLocations = locations.value.filter((loc) => loc.category === category)
-      const occupiedCount = categoryLocations.filter((loc) => hasContainer(loc.containerCode)).length
+      const occupiedCount = categoryLocations.filter((loc) =>
+        hasContainer(loc.containerCode)
+      ).length
       result[category] = formatFillRateDetail(occupiedCount, categoryLocations.length)
       return result
     },
@@ -803,6 +833,10 @@ watchEffect(() => {
   document.title = systemTitle.value
 })
 
+watch(isLightTheme, (isLight) => {
+  window.localStorage.setItem(THEME_STORAGE_KEY, isLight ? 'light' : 'dark')
+})
+
 onBeforeUnmount(() => {
   if (refreshTimer !== null) {
     window.clearTimeout(refreshTimer)
@@ -820,13 +854,101 @@ onBeforeUnmount(() => {
   height: 100vh;
   display: flex;
   flex-direction: column;
-  background: #000000;
+  background: var(--bg);
+  color: var(--text);
+  --bg: #000000;
+  --header-bg: linear-gradient(180deg, #050505 0%, #000000 100%);
+  --header-border: #1c1c1c;
+  --text: #f2f2f2;
+  --text-muted: rgba(255, 255, 255, 0.6);
+  --text-dim: #d8d8d8;
+  --label-text: #8e8e8e;
+  --panel-bg: rgba(0, 0, 0, 0.96);
+  --panel-border: #1f1f1f;
+  --panel-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
+  --input-bg: rgba(255, 255, 255, 0.03);
+  --input-border: #252525;
+  --input-text: #f2f2f2;
+  --input-placeholder: rgba(255, 255, 255, 0.28);
+  --input-hover-bg: rgba(255, 255, 255, 0.05);
+  --input-hover-border: #3a3a3a;
+  --icon-border: #202020;
+  --toggle-track-bg: #111111;
+  --toggle-track-border: #2c2c2c;
+  --toggle-thumb-bg: #f2f2f2;
+  --toggle-track-checked-bg: #2b2b2b;
+  --toggle-track-checked-border: #5a5a5a;
+  --refresh-bg: #101010;
+  --refresh-border: #282828;
+  --refresh-text: #e8e8e8;
+  --refresh-hover-bg: #181818;
+  --refresh-hover-border: #3a3a3a;
+  --popover-bg: rgba(4, 4, 4, 0.98);
+  --popover-border: #242424;
+  --btn-neutral-bg: rgba(255, 255, 255, 0.03);
+  --btn-neutral-border: #252525;
+  --btn-neutral-text: #cfcfcf;
+  --btn-neutral-hover-bg: rgba(255, 255, 255, 0.06);
+  --btn-neutral-hover-border: #3a3a3a;
+  --danger-bg: rgba(210, 92, 92, 0.08);
+  --danger-border: rgba(210, 92, 92, 0.45);
+  --danger-text: #f2bcbc;
+  --danger-hover-bg: rgba(210, 92, 92, 0.16);
+  --danger-hover-border: rgba(210, 92, 92, 0.7);
+  --danger-hover-text: #ffd4d4;
+  --error-text: #ff4444;
+  --select-arrow: #6d6d6d;
+}
+
+.dashboard.theme-light {
+  --bg: #f6f7f9;
+  --header-bg: linear-gradient(180deg, #ffffff 0%, #f0f2f5 100%);
+  --header-border: #d9dee6;
+  --text: #1f1f1f;
+  --text-muted: rgba(0, 0, 0, 0.55);
+  --text-dim: #3f3f3f;
+  --label-text: #5f6368;
+  --panel-bg: rgba(255, 255, 255, 0.96);
+  --panel-border: #d7dce3;
+  --panel-shadow: 0 12px 24px rgba(31, 35, 40, 0.12);
+  --input-bg: #ffffff;
+  --input-border: #cfd6df;
+  --input-text: #1f1f1f;
+  --input-placeholder: rgba(0, 0, 0, 0.35);
+  --input-hover-bg: #f7f9fb;
+  --input-hover-border: #aeb7c2;
+  --icon-border: #cfd6df;
+  --toggle-track-bg: #e4e7ec;
+  --toggle-track-border: #c6ccd5;
+  --toggle-thumb-bg: #ffffff;
+  --toggle-track-checked-bg: #cfd6df;
+  --toggle-track-checked-border: #aeb7c2;
+  --refresh-bg: #ffffff;
+  --refresh-border: #cfd6df;
+  --refresh-text: #2a2f36;
+  --refresh-hover-bg: #f1f4f8;
+  --refresh-hover-border: #aeb7c2;
+  --popover-bg: #ffffff;
+  --popover-border: #d7dce3;
+  --btn-neutral-bg: #ffffff;
+  --btn-neutral-border: #cfd6df;
+  --btn-neutral-text: #2a2f36;
+  --btn-neutral-hover-bg: #f1f4f8;
+  --btn-neutral-hover-border: #aeb7c2;
+  --danger-bg: rgba(210, 92, 92, 0.1);
+  --danger-border: rgba(210, 92, 92, 0.45);
+  --danger-text: #a83b3b;
+  --danger-hover-bg: rgba(210, 92, 92, 0.16);
+  --danger-hover-border: rgba(210, 92, 92, 0.7);
+  --danger-hover-text: #8f2f2f;
+  --error-text: #c73939;
+  --select-arrow: #6b7280;
 }
 
 .header {
   padding: 10px 18px;
-  background: linear-gradient(180deg, #050505 0%, #000000 100%);
-  border-bottom: 1px solid #1c1c1c;
+  background: var(--header-bg);
+  border-bottom: 1px solid var(--header-border);
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -839,14 +961,18 @@ onBeforeUnmount(() => {
   font-size: 18px;
   font-weight: bold;
   line-height: 1.1;
-  color: #f2f2f2;
+  color: var(--text);
   text-shadow: 0 0 8px rgba(255, 255, 255, 0.08);
 }
 
 .title-meta {
   font-size: 11px;
   font-weight: normal;
-  color: rgba(255, 255, 255, 0.6);
+  color: var(--text-muted);
+}
+
+.title-meta-value {
+  font-weight: 700;
 }
 
 .title-legend {
@@ -872,11 +998,11 @@ onBeforeUnmount(() => {
   z-index: 1000;
   max-width: 160px;
   padding: 10px 12px;
-  border: 1px solid #1f1f1f;
+  border: 1px solid var(--panel-border);
   border-radius: 8px;
-  background: rgba(0, 0, 0, 0.96);
-  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
-  color: #f2f2f2;
+  background: var(--panel-bg);
+  box-shadow: var(--panel-shadow);
+  color: var(--text);
   font-size: 12px;
   line-height: 1.45;
   pointer-events: none;
@@ -887,16 +1013,10 @@ onBeforeUnmount(() => {
   margin-top: 3px;
 }
 
-.title-meta-a {
-  color: rgba(201, 242, 215, 0.78);
-}
-
-.title-meta-b {
-  color: rgba(203, 220, 255, 0.8);
-}
-
+.title-meta-a,
+.title-meta-b,
 .title-meta-c {
-  color: rgba(240, 220, 156, 0.82);
+  color: var(--text-muted);
 }
 
 .legend-chip {
@@ -954,14 +1074,14 @@ onBeforeUnmount(() => {
   display: flex;
   align-items: center;
   gap: 6px;
-  color: #d8d8d8;
+  color: var(--text-dim);
 }
 
 .filter-item {
   display: flex;
   align-items: center;
   gap: 4px;
-  color: #d8d8d8;
+  color: var(--text-dim);
 }
 
 .filter-input-item {
@@ -972,20 +1092,20 @@ onBeforeUnmount(() => {
   display: flex;
   align-items: center;
   gap: 4px;
-  color: #d8d8d8;
+  color: var(--text-dim);
 }
 
 .selector-label {
   font-size: 11px;
-  color: #8e8e8e;
+  color: var(--label-text);
 }
 
 .level-select {
   min-width: 76px;
   padding: 6px 24px 6px 8px;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid #252525;
-  color: #f2f2f2;
+  background: var(--input-bg);
+  border: 1px solid var(--input-border);
+  color: var(--input-text);
   cursor: pointer;
   font-size: 12px;
   transition: all 0.3s;
@@ -993,8 +1113,8 @@ onBeforeUnmount(() => {
   outline: none;
   appearance: none;
   background-image:
-    linear-gradient(45deg, transparent 50%, #6d6d6d 50%),
-    linear-gradient(135deg, #6d6d6d 50%, transparent 50%);
+    linear-gradient(45deg, transparent 50%, var(--select-arrow) 50%),
+    linear-gradient(135deg, var(--select-arrow) 50%, transparent 50%);
   background-position:
     calc(100% - 13px) calc(50% - 2px),
     calc(100% - 8px) calc(50% - 2px);
@@ -1019,9 +1139,9 @@ onBeforeUnmount(() => {
   width: 100%;
   padding: 6px 8px;
   padding-right: 50px;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid #252525;
-  color: #f2f2f2;
+  background: var(--input-bg);
+  border: 1px solid var(--input-border);
+  color: var(--input-text);
   font-size: 12px;
   border-radius: 4px;
   outline: none;
@@ -1029,13 +1149,13 @@ onBeforeUnmount(() => {
 }
 
 .filter-input::placeholder {
-  color: rgba(255, 255, 255, 0.28);
+  color: var(--input-placeholder);
 }
 
 .filter-input:hover,
 .filter-input:focus {
-  background: rgba(255, 255, 255, 0.05);
-  border-color: #3a3a3a;
+  background: var(--input-hover-bg);
+  border-color: var(--input-hover-border);
 }
 
 .filter-confirm-btn {
@@ -1048,9 +1168,9 @@ onBeforeUnmount(() => {
   align-items: center;
   justify-content: center;
   border: none;
-  border-left: 1px solid #202020;
+  border-left: 1px solid var(--icon-border);
   background: transparent;
-  color: #cfcfcf;
+  color: var(--btn-neutral-text);
   cursor: pointer;
   border-radius: 0 3px 3px 0;
 }
@@ -1065,15 +1185,15 @@ onBeforeUnmount(() => {
   align-items: center;
   justify-content: center;
   border: none;
-  border-left: 1px solid #202020;
+  border-left: 1px solid var(--icon-border);
   background: transparent;
-  color: #cfcfcf;
+  color: var(--btn-neutral-text);
   cursor: pointer;
 }
 
 .filter-clear-btn:hover,
 .filter-confirm-btn:hover {
-  background: rgba(255, 255, 255, 0.06);
+  background: var(--btn-neutral-hover-bg);
 }
 
 .filter-action-icon {
@@ -1092,8 +1212,8 @@ onBeforeUnmount(() => {
   width: 34px;
   height: 18px;
   border-radius: 999px;
-  background: #111111;
-  border: 1px solid #2c2c2c;
+  background: var(--toggle-track-bg);
+  border: 1px solid var(--toggle-track-border);
   transition: all 0.2s;
   cursor: pointer;
 }
@@ -1105,13 +1225,13 @@ onBeforeUnmount(() => {
   width: 14px;
   height: 14px;
   border-radius: 50%;
-  background: #f2f2f2;
+  background: var(--toggle-thumb-bg);
   transition: transform 0.2s;
 }
 
 .toggle-input:checked + .toggle-track {
-  background: #2b2b2b;
-  border-color: #5a5a5a;
+  background: var(--toggle-track-checked-bg);
+  border-color: var(--toggle-track-checked-border);
 }
 
 .toggle-input:checked + .toggle-track .toggle-thumb {
@@ -1120,16 +1240,16 @@ onBeforeUnmount(() => {
 
 .level-select:hover,
 .level-select:focus {
-  background: rgba(255, 255, 255, 0.05);
-  border-color: #3a3a3a;
+  background: var(--input-hover-bg);
+  border-color: var(--input-hover-border);
 }
 
 .refresh-btn {
   min-width: 58px;
   padding: 6px 8px;
-  background: #101010;
-  border: 1px solid #282828;
-  color: #e8e8e8;
+  background: var(--refresh-bg);
+  border: 1px solid var(--refresh-border);
+  color: var(--refresh-text);
   cursor: pointer;
   font-size: 11px;
   transition: all 0.3s;
@@ -1149,19 +1269,19 @@ onBeforeUnmount(() => {
   align-items: center;
   gap: 4px;
   padding: 4px;
-  background: rgba(4, 4, 4, 0.98);
-  border: 1px solid #242424;
+  background: var(--popover-bg);
+  border: 1px solid var(--popover-border);
   border-radius: 4px;
-  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.24);
+  box-shadow: var(--panel-shadow);
   z-index: 20;
 }
 
 .refresh-popover-input {
   width: 56px;
   padding: 6px 8px;
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid #252525;
-  color: #f2f2f2;
+  background: var(--input-bg);
+  border: 1px solid var(--input-border);
+  color: var(--input-text);
   font-size: 12px;
   border-radius: 4px;
   outline: none;
@@ -1169,8 +1289,8 @@ onBeforeUnmount(() => {
 }
 
 .refresh-popover-input:focus {
-  background: rgba(255, 255, 255, 0.05);
-  border-color: #3a3a3a;
+  background: var(--input-hover-bg);
+  border-color: var(--input-hover-border);
 }
 
 .refresh-popover-confirm {
@@ -1179,21 +1299,21 @@ onBeforeUnmount(() => {
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  border: 1px solid #252525;
-  background: rgba(255, 255, 255, 0.03);
-  color: #cfcfcf;
+  border: 1px solid var(--btn-neutral-border);
+  background: var(--btn-neutral-bg);
+  color: var(--btn-neutral-text);
   border-radius: 4px;
   cursor: pointer;
 }
 
 .refresh-popover-confirm:hover {
-  background: rgba(255, 255, 255, 0.06);
-  border-color: #3a3a3a;
+  background: var(--btn-neutral-hover-bg);
+  border-color: var(--btn-neutral-hover-border);
 }
 
 .refresh-btn:hover:not(:disabled) {
-  background: #181818;
-  border-color: #3a3a3a;
+  background: var(--refresh-hover-bg);
+  border-color: var(--refresh-hover-border);
 }
 
 .refresh-btn:disabled {
@@ -1203,9 +1323,9 @@ onBeforeUnmount(() => {
 
 .logout-btn {
   padding: 6px 10px;
-  background: rgba(210, 92, 92, 0.08);
-  border: 1px solid rgba(210, 92, 92, 0.45);
-  color: #f2bcbc;
+  background: var(--danger-bg);
+  border: 1px solid var(--danger-border);
+  color: var(--danger-text);
   cursor: pointer;
   font-size: 11px;
   transition: all 0.3s;
@@ -1214,16 +1334,16 @@ onBeforeUnmount(() => {
 }
 
 .logout-btn:hover {
-  background: rgba(210, 92, 92, 0.16);
-  border-color: rgba(210, 92, 92, 0.7);
-  color: #ffd4d4;
+  background: var(--danger-hover-bg);
+  border-color: var(--danger-hover-border);
+  color: var(--danger-hover-text);
 }
 
 .main-content {
   flex: 1;
   padding: 0 12px 12px;
   overflow: auto;
-  background: #000000;
+  background: var(--bg);
 }
 
 .loading,
@@ -1233,11 +1353,11 @@ onBeforeUnmount(() => {
   align-items: center;
   height: 100%;
   font-size: 18px;
-  color: #f2f2f2;
+  color: var(--text);
 }
 
 .error {
-  color: #ff4444;
+  color: var(--error-text);
 }
 
 .map-container {

+ 3 - 1
src/components/LoginModal.vue

@@ -329,7 +329,9 @@ const handleCancel = () => {
   background-position:
     calc(100% - 16px) calc(50% - 2px),
     calc(100% - 11px) calc(50% - 2px);
-  background-size: 5px 5px, 5px 5px;
+  background-size:
+    5px 5px,
+    5px 5px;
   background-repeat: no-repeat;
   cursor: pointer;
 }

+ 111 - 29
src/components/WarehouseMap.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="warehouse-map">
+  <div :class="['warehouse-map', themeClass]">
     <div
       v-if="gridData.length === 0"
       class="no-data"
@@ -117,6 +117,7 @@ interface Props {
   locationIdKeyword: string
   showGroupBorder: boolean
   showTooltip: boolean
+  themeMode?: 'dark' | 'light'
 }
 
 interface ParsedLocation {
@@ -142,7 +143,9 @@ interface AisleCell {
 
 type MapCell = GridCell | WarehouseLayoutSpecialCell | AisleCell
 
-const props = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+  themeMode: 'dark'
+})
 const emit = defineEmits<{
   (event: 'select-loc-group', locGroup1: string): void
   (event: 'select-location-id', locationId: string): void
@@ -177,7 +180,7 @@ const DEPTH_CATEGORY_MAP: Record<number, string> = {
   3: 'C'
 }
 
-const CATEGORY_THEME_MAP: Record<string, { solid: string; soft: string; text: string }> = {
+const CATEGORY_THEME_MAP_DARK: Record<string, { solid: string; soft: string; text: string }> = {
   A: {
     solid: '#1f7a3f',
     soft: 'rgba(31, 122, 63, 0.18)',
@@ -195,12 +198,48 @@ const CATEGORY_THEME_MAP: Record<string, { solid: string; soft: string; text: st
   }
 }
 
-const INACTIVE_CATEGORY_THEME = {
+const CATEGORY_THEME_MAP_LIGHT: Record<string, { solid: string; soft: string; text: string }> = {
+  A: {
+    solid: '#1f7a3f',
+    soft: 'rgba(31, 122, 63, 0.12)',
+    text: '#ffffff'
+  },
+  B: {
+    solid: '#2f5fd7',
+    soft: 'rgba(47, 95, 215, 0.12)',
+    text: '#ffffff'
+  },
+  C: {
+    solid: '#b8921f',
+    soft: 'rgba(184, 146, 31, 0.18)',
+    text: '#1f1f1f'
+  }
+}
+
+const INACTIVE_CATEGORY_THEME_DARK = {
   solid: '#5f6b7a',
   soft: 'rgba(95, 107, 122, 0.16)',
   text: '#eef4f8'
 }
 
+const INACTIVE_CATEGORY_THEME_LIGHT = {
+  solid: '#8a97a5',
+  soft: 'rgba(138, 151, 165, 0.18)',
+  text: '#ffffff'
+}
+
+const themeClass = computed(() => (props.themeMode === 'light' ? 'theme-light' : 'theme-dark'))
+
+const categoryThemeMap = computed(() =>
+  props.themeMode === 'light' ? CATEGORY_THEME_MAP_LIGHT : CATEGORY_THEME_MAP_DARK
+)
+
+const inactiveCategoryTheme = computed(() =>
+  props.themeMode === 'light' ? INACTIVE_CATEGORY_THEME_LIGHT : INACTIVE_CATEGORY_THEME_DARK
+)
+
+const groupBorderColor = computed(() => (props.themeMode === 'light' ? '#6f7a86' : '#ffffff'))
+
 const LOCATION_ATTRIBUTE_LABEL_MAP: Record<string, string> = {
   OK: '正常',
   FI: '禁入',
@@ -542,8 +581,8 @@ const getCellTitle = (cell: GridCell | null) => {
 const getCategoryStyle = (cell: GridCell) => {
   const theme =
     props.categoryColorVisibility[cell.category] === false
-      ? INACTIVE_CATEGORY_THEME
-      : CATEGORY_THEME_MAP[cell.category]
+      ? inactiveCategoryTheme.value
+      : categoryThemeMap.value[cell.category]
   return {
     background: theme?.solid || '#5f6b7a',
     color: theme?.text || '#fff'
@@ -602,7 +641,7 @@ const getCellStyle = (cell: MapCell): CSSProperties => {
 
   const borderWidth = 'var(--group-outline-width, 2px)'
   return {
-    '--group-border-color': '#ffffff',
+    '--group-border-color': groupBorderColor.value,
     '--group-border-top': isSameBorderNeighbor(cell, -1, 0) ? '0px' : borderWidth,
     '--group-border-right': isSameBorderNeighbor(cell, 0, 1) ? '0px' : borderWidth,
     '--group-border-bottom': isSameBorderNeighbor(cell, 1, 0) ? '0px' : borderWidth,
@@ -785,6 +824,49 @@ onBeforeUnmount(() => {
   display: flex;
   flex-direction: column;
   position: relative;
+  --map-bg: #000000;
+  --map-text-muted: #8b8b8b;
+  --aisle-bg: #050505;
+  --wall-bg: #151515;
+  --elevator-bg: #101010;
+  --cell-category-a: rgba(31, 122, 63, 0.18);
+  --cell-category-b: rgba(47, 95, 215, 0.18);
+  --cell-category-c: rgba(184, 146, 31, 0.2);
+  --cell-category-muted: rgba(95, 107, 122, 0.16);
+  --cell-id: #f2f2f2;
+  --cell-group: #a0a0a0;
+  --cell-container: #9a9a9a;
+  --cell-alert: #ff4d4f;
+  --special-label: rgba(255, 255, 255, 0.76);
+  --special-label-strong: rgba(255, 255, 255, 0.86);
+  --tooltip-border: #1f1f1f;
+  --tooltip-bg: rgba(0, 0, 0, 0.96);
+  --tooltip-text: #f2f2f2;
+  --tooltip-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
+  --hover-shadow: 0 0 12px rgba(255, 255, 255, 0.08);
+}
+
+.warehouse-map.theme-light {
+  --map-bg: #f6f7f9;
+  --map-text-muted: #5f6368;
+  --aisle-bg: #e9edf2;
+  --wall-bg: #d9dee6;
+  --elevator-bg: #e2e6ec;
+  --cell-category-a: rgba(31, 122, 63, 0.12);
+  --cell-category-b: rgba(47, 95, 215, 0.12);
+  --cell-category-c: rgba(184, 146, 31, 0.18);
+  --cell-category-muted: rgba(138, 151, 165, 0.18);
+  --cell-id: #2a2f36;
+  --cell-group: #5f6368;
+  --cell-container: #6f7a86;
+  --cell-alert: #c73939;
+  --special-label: rgba(31, 35, 40, 0.7);
+  --special-label-strong: rgba(31, 35, 40, 0.82);
+  --tooltip-border: #d7dce3;
+  --tooltip-bg: #ffffff;
+  --tooltip-text: #2a2f36;
+  --tooltip-shadow: 0 12px 24px rgba(31, 35, 40, 0.12);
+  --hover-shadow: 0 0 12px rgba(31, 35, 40, 0.12);
 }
 
 .no-data {
@@ -792,7 +874,7 @@ onBeforeUnmount(() => {
   justify-content: center;
   align-items: center;
   height: 100%;
-  color: #8b8b8b;
+  color: var(--map-text-muted);
   font-size: 16px;
 }
 
@@ -800,7 +882,7 @@ onBeforeUnmount(() => {
   flex: 1;
   position: relative;
   overflow: auto;
-  background: #000000;
+  background: var(--map-bg);
 }
 
 .map-grid {
@@ -840,16 +922,16 @@ onBeforeUnmount(() => {
 }
 
 .grid-cell.aisle {
-  background: #050505;
+  background: var(--aisle-bg);
 }
 
 .grid-cell.wall {
-  background: #151515;
+  background: var(--wall-bg);
   cursor: default;
 }
 
 .grid-cell.elevator {
-  background: #101010;
+  background: var(--elevator-bg);
   cursor: default;
 }
 
@@ -860,24 +942,24 @@ onBeforeUnmount(() => {
 }
 
 .grid-cell.category-a {
-  background: rgba(31, 122, 63, 0.18);
+  background: var(--cell-category-a);
 }
 
 .grid-cell.category-b {
-  background: rgba(47, 95, 215, 0.18);
+  background: var(--cell-category-b);
 }
 
 .grid-cell.category-c {
-  background: rgba(184, 146, 31, 0.2);
+  background: var(--cell-category-c);
 }
 
 .grid-cell.category-muted {
-  background: rgba(95, 107, 122, 0.16);
+  background: var(--cell-category-muted);
 }
 
 .grid-cell:not(.aisle):not(.wall):not(.elevator):hover {
   transform: scale(1.03);
-  box-shadow: 0 0 12px rgba(255, 255, 255, 0.08);
+  box-shadow: var(--hover-shadow);
   z-index: 10;
 }
 
@@ -908,7 +990,7 @@ onBeforeUnmount(() => {
 .loc-group {
   display: var(--cell-group-display, -webkit-box);
   font-size: var(--cell-group-font-size, 10px);
-  color: #a0a0a0;
+  color: var(--cell-group);
   text-align: center;
   line-height: 1.2;
   word-break: break-all;
@@ -921,7 +1003,7 @@ onBeforeUnmount(() => {
   display: block;
   font-size: var(--cell-id-font-size, 11px);
   font-weight: bold;
-  color: #f2f2f2;
+  color: var(--cell-id);
   text-align: center;
   line-height: 1.1;
   white-space: nowrap;
@@ -930,17 +1012,17 @@ onBeforeUnmount(() => {
 }
 
 .location-id.location-id-mismatch {
-  color: #ff4d4f;
+  color: var(--cell-alert);
 }
 
 .location-id.location-id-abnormal-attribute {
-  color: #ff4d4f;
+  color: var(--cell-alert);
 }
 
 .location-attribute-tag {
   max-width: 100%;
   font-size: calc(var(--cell-id-font-size, 11px) - 2px);
-  color: #ff4d4f;
+  color: var(--cell-alert);
   text-align: center;
   line-height: 1.1;
   white-space: nowrap;
@@ -951,7 +1033,7 @@ onBeforeUnmount(() => {
 .container-code {
   max-width: 100%;
   font-size: calc(var(--cell-id-font-size, 11px) - 2px);
-  color: #9a9a9a;
+  color: var(--cell-container);
   text-align: center;
   line-height: 1.1;
   white-space: nowrap;
@@ -962,13 +1044,13 @@ onBeforeUnmount(() => {
 .special-cell-label {
   font-size: calc(var(--cell-id-font-size, 11px) - 1px);
   font-weight: 700;
-  color: rgba(255, 255, 255, 0.76);
+  color: var(--special-label);
   letter-spacing: 1px;
   user-select: none;
 }
 
 .special-cell-label-elevator {
-  color: rgba(255, 255, 255, 0.86);
+  color: var(--special-label-strong);
 }
 
 .cell-tooltip {
@@ -976,11 +1058,11 @@ onBeforeUnmount(() => {
   z-index: 1000;
   max-width: 300px;
   padding: 10px 12px;
-  border: 1px solid #1f1f1f;
+  border: 1px solid var(--tooltip-border);
   border-radius: 8px;
-  background: rgba(0, 0, 0, 0.96);
-  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
-  color: #f2f2f2;
+  background: var(--tooltip-bg);
+  box-shadow: var(--tooltip-shadow);
+  color: var(--tooltip-text);
   font-size: 12px;
   line-height: 1.45;
   pointer-events: none;

+ 0 - 1
src/config/index.ts

@@ -23,7 +23,6 @@ export const DEFAULT_API_ENVIRONMENT: ApiEnvironment = 'sit'
 
 // 系统配置文件
 export const config = {
-
   // 登录接口
   loginApi: '/api/user/login',
 

+ 5 - 7
src/utils/auth.ts

@@ -1,8 +1,4 @@
-import {
-  API_ENVIRONMENT_OPTIONS,
-  DEFAULT_API_ENVIRONMENT,
-  type ApiEnvironment
-} from '../config'
+import { API_ENVIRONMENT_OPTIONS, DEFAULT_API_ENVIRONMENT, type ApiEnvironment } from '../config'
 
 const TOKEN_KEY = 'auth_token'
 const API_ENVIRONMENT_KEY = 'api_environment'
@@ -40,8 +36,10 @@ export const setApiEnvironment = (environment: ApiEnvironment): void => {
 
 export const getApiBaseUrl = (): string => {
   const environment = getApiEnvironment()
-  return API_ENVIRONMENT_OPTIONS.find((option) => option.key === environment)?.baseUrl
-    || API_ENVIRONMENT_OPTIONS[0].baseUrl
+  return (
+    API_ENVIRONMENT_OPTIONS.find((option) => option.key === environment)?.baseUrl ||
+    API_ENVIRONMENT_OPTIONS[0].baseUrl
+  )
 }
 
 export const isAuthenticated = (): boolean => {