2 커밋 c3a3b5f0c4 ... 1d9e2548cc

작성자 SHA1 메시지 날짜
  skyline 1d9e2548cc feat: 站点切换器融入顶栏视觉语言,重做下拉面板与交互反馈 3 일 전
  skyline 335e6662df fix: 修复 useStationScope 回调前置引用引发的页面 TDZ 崩溃 3 일 전

+ 172 - 16
admin-web/src/layout/navBars/breadcrumb/stationSwitcher.vue

@@ -1,21 +1,33 @@
 <template>
-  <div class="layout-navbars-breadcrumb-station">
+  <div class="layout-navbars-breadcrumb-station"
+       :class="{'is-open': state.open, 'is-empty': !state.loading && state.stationList.length === 0}"
+       title="切换当前查看的站点,页面数据范围会跟随变化">
     <el-select-v2
         v-model="state.currentStationId"
         :options="state.options"
+        :loading="state.loading"
+        :loading-text="'正在加载站点…'"
+        :no-data-text="'没有匹配的站点'"
+        :disabled="!state.loading && state.stationList.length === 0"
+        :placeholder="placeholder"
         filterable
-        placeholder="选择站点"
+        popper-class="station-select-popper"
         class="station-select"
-        @change="handleStationChange">
+        @change="handleStationChange"
+        @visible-change="state.open = $event">
       <template #prefix>
-        <SvgIcon name="ele-Location"></SvgIcon>
+        <SvgIcon name="ele-Location" :size="15"/>
+      </template>
+      <template #header>
+        <span>站点范围</span>
+        <span>共 {{ state.stationList.length }} 个站点</span>
       </template>
     </el-select-v2>
   </div>
 </template>
 
 <script setup lang="ts" name="layoutBreadcrumbStationSwitcher">
-import {reactive, onMounted} from 'vue';
+import {reactive, computed, onMounted} from 'vue';
 import mittBus from '/@/utils/mitt';
 import {Session} from '/@/utils/storage';
 import {$body} from '/@/utils/request';
@@ -23,7 +35,15 @@ import {$body} from '/@/utils/request';
 const state = reactive({
   stationList: [] as Array<any>,
   currentStationId: null as any,
-  options: [] as Array<any>
+  options: [] as Array<any>,
+  loading: true,
+  open: false
+});
+
+const placeholder = computed(() => {
+  if (state.loading) return '加载站点中';
+  if (state.stationList.length === 0) return '暂无站点';
+  return '选择站点';
 });
 
 //站点切换
@@ -54,6 +74,8 @@ const loadStationList = () => {
     }
   }).catch(e => {
     //console.error(e)
+  }).finally(() => {
+    state.loading = false;
   })
 }
 
@@ -68,23 +90,157 @@ onMounted(() => {
   display: flex;
   align-items: center;
   height: 50px;
-  padding: 0 8px;
+  padding: 0 12px 0 4px;
+  position: relative;
+
+  //与面包屑之间的分隔:范围(哪个站)与位置(哪个页)是两个语境
+  &::after {
+    content: '';
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 1px;
+    height: 20px;
+    background: var(--el-border-color-lighter);
+  }
 
   .station-select {
-    width: 230px;
+    width: 180px;
 
-    :deep(.el-input__wrapper),
+    //去框融入顶栏:静态时无边框无底色,悬停/聚焦时才浮起一层浅底
     :deep(.el-select__wrapper) {
-      background: var(--el-fill-color-light);
-      border-radius: 16px;
-      box-shadow: 0 0 0 1px var(--el-border-color-lighter) inset;
+      height: 34px;
+      min-height: 34px;
+      padding: 0 8px;
+      border-radius: 8px;
+      background: transparent;
+      box-shadow: none;
+      transition: background-color .2s cubic-bezier(.4, 0, .2, 1);
+
+      &.is-hovering {
+        background: var(--next-color-user-hover);
+      }
+
+      &.is-focused {
+        background: var(--next-color-user-hover);
+      }
+
+      &.is-disabled {
+        background: transparent;
+      }
+    }
+
+    :deep(.el-select__prefix) {
+      display: flex;
+      align-items: center;
+      margin-right: 6px;
+      color: var(--next-bg-topBarColor);
+      transition: color .2s cubic-bezier(.4, 0, .2, 1);
+    }
+
+    :deep(.el-select__placeholder),
+    :deep(.el-select__input) {
+      color: var(--next-bg-topBarColor);
+      font-size: 14px;
+      font-weight: 500;
+      transition: color .2s cubic-bezier(.4, 0, .2, 1);
+    }
+
+    //未选中任何站点时的占位:退一档
+    :deep(.el-select__placeholder.is-transparent) {
+      color: var(--el-text-color-placeholder);
+      font-weight: 400;
+    }
+
+    :deep(.el-select__caret) {
+      color: var(--next-bg-topBarColor);
+      opacity: 0.6;
+      transform-origin: center;
+      transition: transform .2s cubic-bezier(.4, 0, .2, 1), opacity .2s cubic-bezier(.4, 0, .2, 1), color .2s;
+    }
+  }
+
+  //悬停时文字与图标转为品牌色,呼应 Ghost 按钮规则(禁用态除外)
+  &:not(.is-empty) .station-select:hover {
+    :deep(.el-select__wrapper .el-select__placeholder),
+    :deep(.el-select__wrapper .el-select__input),
+    :deep(.el-select__wrapper .el-select__prefix),
+    :deep(.el-select__wrapper .el-select__caret) {
+      color: var(--el-color-primary);
+      opacity: 1;
+    }
+  }
+
+  //展开时箭头翻转
+  &.is-open .station-select {
+    :deep(.el-select__caret) {
+      transform: rotate(180deg);
+      opacity: 1;
+      color: var(--el-color-primary);
     }
+  }
+
+  //禁用态(无站点可用)
+  &:deep(.el-select__wrapper.is-disabled .el-select__placeholder),
+  &:deep(.el-select__wrapper.is-disabled .el-select__prefix),
+  &:deep(.el-select__wrapper.is-disabled .el-select__caret) {
+    color: var(--el-text-color-disabled);
+    opacity: 1;
+  }
+}
+</style>
+
+<style lang="scss">
+// 下拉面板(teleported,需全局样式)
+.station-select-popper {
+  .el-select-dropdown {
+    padding: 6px;
+    border: none;
+    border-radius: 12px;
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.06);
+  }
+
+  .el-select-dropdown__header {
+    display: flex;
+    justify-content: space-between;
+    padding: 4px 10px 6px;
+    font-size: 12px;
+    font-weight: 500;
+    color: var(--el-text-color-secondary);
 
-    :deep(.el-input__wrapper.is-focus),
-    :deep(.el-select__wrapper.is-focused) {
-      background: var(--el-bg-color);
-      box-shadow: 0 0 0 1px var(--el-color-primary) inset;
+    span:last-child {
+      font-weight: 400;
+      color: var(--el-text-color-placeholder);
     }
   }
+
+  .el-select-dropdown__list {
+    padding: 2px;
+  }
+
+  .el-select-dropdown__item {
+    height: 34px;
+    margin: 2px 0;
+    padding: 0 10px;
+    border-radius: 8px;
+    font-size: 14px;
+  }
+
+  .el-select-dropdown__item.is-hovering {
+    background: var(--el-fill-color-light);
+  }
+
+  //当前站点:品牌色 + 加粗标记「你在这里」
+  .el-select-dropdown__item.is-selected {
+    color: var(--el-color-primary);
+    font-weight: 600;
+  }
+
+  .el-select-dropdown__empty {
+    padding: 14px 10px;
+    font-size: 13px;
+    color: var(--el-text-color-placeholder);
+  }
 }
 </style>

+ 2 - 2
admin-web/src/views/admin/account/index.vue

@@ -326,8 +326,8 @@ const state = reactive({
 onBeforeMount(() => {
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   loadData();

+ 2 - 2
admin-web/src/views/admin/finance/settlement.vue

@@ -196,8 +196,8 @@ const state = reactive({
   ],
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   loadData();

+ 2 - 2
admin-web/src/views/admin/finance/splitRecord.vue

@@ -218,8 +218,8 @@ const state = reactive({
 
 //生命周期钩子
 //生命周期钩子
-//站点筛选默认全局当前站点;本页面常由站点账户跳转携带指定站点,不跟随全局切换
-useStationScope(state.formQuery, loadData, {follow: false});
+//站点筛选默认全局当前站点;本页面常由站点账户跳转携带指定站点,不跟随全局切换(回调懒引用,规避 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh), {follow: false});
 
 onBeforeMount(() => {
 

+ 2 - 2
admin-web/src/views/admin/finance/withdraw.vue

@@ -191,8 +191,8 @@ onBeforeMount(() => {
 
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   loadData();

+ 2 - 2
admin-web/src/views/admin/investor/index.vue

@@ -202,8 +202,8 @@ const state = reactive({
 onBeforeMount(() => {
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   loadData();

+ 2 - 2
admin-web/src/views/admin/parking-coupon-record/index.vue

@@ -130,8 +130,8 @@ const state = reactive({
   ],
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   loadData();

+ 2 - 2
admin-web/src/views/admin/station/account/index.vue

@@ -177,8 +177,8 @@ const state = reactive({
 //         }
 // );
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 //生命周期钩子
 onBeforeMount(() => {

+ 2 - 2
admin-web/src/views/admin/station/device/index.vue

@@ -234,8 +234,8 @@ const state = reactive({
 //         }
 // );
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 //生命周期钩子
 onBeforeMount(() => {

+ 2 - 2
admin-web/src/views/admin/statistics/daily-stat.vue

@@ -158,8 +158,8 @@ const state = reactive({
   ],
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   // 同步日期默认昨天

+ 2 - 2
admin-web/src/views/admin/statistics/month-stat.vue

@@ -144,8 +144,8 @@ const state = reactive({
   ],
 })
 
-//站点筛选与全局当前站点联动
-useStationScope(state.formQuery, loadData);
+//站点筛选与全局当前站点联动(回调懒引用,规避 loadData 声明顺序的 TDZ 错误)
+useStationScope(state.formQuery, (refresh?: boolean) => loadData(refresh));
 
 onMounted(() => {
   // 同步月份默认上月