Browse Source

feat: admin-web 站点切换入口移至顶栏左侧并统一各页面站点筛选联动

- 站点切换器独立组件置于顶栏左侧面包屑前,改用可搜索下拉并支持会话记忆
- 新增 useStationScope 组合式函数,统计/财务/站点等 11 个页面默认跟随当前站点并响应全局切换
- 修复 ExtQueryForm 不同步外部查询值变化的问题
- 首页仪表盘站点未变化时不再重复加载
skyline 3 days ago
parent
commit
c3a3b5f0c4

+ 5 - 0
.gitignore

@@ -43,3 +43,8 @@ admin-h5/dist/
 .impeccable/
 .trae/
 skills-lock.json
+admin-web-new/.claude/settings.local.json
+admin-mp/node_modules/
+admin-app/node_modules/
+admin-app/dist/
+.claude/

+ 6 - 12
admin-web/src/components/form/ExtQueryForm.vue

@@ -228,19 +228,13 @@ const state = ref({
 });
 
 watch(()=>props.modelValue,(nv,ov)=>{
-  if(nv!=ov){
-    console.log(nv)
-    // nextTick(()=>{
-    //   let val = {...props.modelValue}
-    //   state.value.cols.forEach((col: any) => {
-    //     val[col.prop] = null;
-    //   })
-    //   //console.log(val)
-    //   state.value.form = val;
-    // })
+  if(nv && nv!=ov){
+    //外部变更(如全局站点切换联动)同步进查询表单
+    Object.keys(nv).forEach(key => {
+      if(state.value.form[key] !== nv[key]) state.value.form[key] = nv[key];
+    })
   }
-
-})
+},{deep: true})
 
 onMounted(() => {
   if (props.importConfig && props.importConfig.url) {

+ 43 - 0
admin-web/src/hooks/useStationScope.ts

@@ -0,0 +1,43 @@
+import { onMounted, onUnmounted } from 'vue';
+import { Session } from '/@/utils/storage';
+import mittBus from '/@/utils/mitt';
+
+/**
+ * 站点范围组合式函数:页面站点筛选与全局"当前站点"联动
+ * 1. 页面初始化时,若站点筛选为空则默认选中全局当前站点(onBeforeMount 中手动赋值的优先级更高)
+ * 2. 全局站点切换时(顶栏站点选择器),联动更新页面筛选并刷新列表
+ *
+ * @param formQuery 页面查询条件对象(需包含 stationId 字段)
+ * @param loadData 页面数据加载方法,切换站点时以 refresh=true 重新加载
+ * @param options.follow 是否响应全局站点切换,默认 true
+ */
+export const useStationScope = (
+    formQuery: any,
+    loadData?: (refresh?: boolean) => void,
+    options?: { follow?: boolean }
+) => {
+  const follow = options?.follow !== false;
+
+  const onStationChange = (stationId: any) => {
+    if (!follow || !stationId) return;
+    //站点未变化时不重复加载
+    if (formQuery.stationId === stationId) return;
+    formQuery.stationId = stationId;
+    if (loadData) loadData(true);
+  };
+
+  onMounted(() => {
+    //初始化:站点筛选为空时默认选中全局当前站点
+    if (!formQuery.stationId) {
+      let currentStationId = Session.get('currentStationId');
+      if (currentStationId) {
+        formQuery.stationId = currentStationId;
+      }
+    }
+    mittBus.on('stationChangeRefresh', onStationChange);
+  });
+
+  onUnmounted(() => {
+    mittBus.off('stationChangeRefresh', onStationChange);
+  });
+};

+ 3 - 0
admin-web/src/layout/navBars/breadcrumb/index.vue

@@ -1,6 +1,8 @@
 <template>
 	<div class="layout-navbars-breadcrumb-index">
 		<Logo v-if="setIsShowLogo" />
+		<!-- 全局站点切换(当前数据范围) -->
+		<StationSwitcher />
 		<Breadcrumb />
 <!--		<Horizontal :menuList="state.menuList"  />-->
 <!--		<Horizontal :menuList="state.menuList" v-if="isLayoutTransverse" />-->
@@ -20,6 +22,7 @@ import mittBus from '/@/utils/mitt';
 const Breadcrumb = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/breadcrumb.vue'));
 const Toolbar = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/toolbar.vue'));
 const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
+const StationSwitcher = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/stationSwitcher.vue'));
 const Horizontal = defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue'));
 
 // 定义变量内容

+ 90 - 0
admin-web/src/layout/navBars/breadcrumb/stationSwitcher.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="layout-navbars-breadcrumb-station">
+    <el-select-v2
+        v-model="state.currentStationId"
+        :options="state.options"
+        filterable
+        placeholder="选择站点"
+        class="station-select"
+        @change="handleStationChange">
+      <template #prefix>
+        <SvgIcon name="ele-Location"></SvgIcon>
+      </template>
+    </el-select-v2>
+  </div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbStationSwitcher">
+import {reactive, onMounted} from 'vue';
+import mittBus from '/@/utils/mitt';
+import {Session} from '/@/utils/storage';
+import {$body} from '/@/utils/request';
+
+const state = reactive({
+  stationList: [] as Array<any>,
+  currentStationId: null as any,
+  options: [] as Array<any>
+});
+
+//站点切换
+const handleStationChange = (stationId: string) => {
+  let station = state.stationList.find((k: any) => k.stationId === stationId);
+  if (station) {
+    Session.set("currentStationId", stationId);
+    mittBus.emit('stationChangeRefresh', stationId);
+  }
+}
+
+const loadStationList = () => {
+  $body(`/washStation/list`, {pageNum: 1, pageSize: 1024}).then((res: any) => {
+    let {list} = res;
+    state.stationList = list || [];
+    state.options = state.stationList.map((k: any) => {
+      return {value: k.stationId, label: k.stationName}
+    });
+    if (state.stationList.length > 0) {
+      //优先恢复上次选择的站点,其次默认第一个
+      let savedId = Session.get('currentStationId');
+      let current = state.stationList.find((k: any) => k.stationId === savedId) || state.stationList[0];
+      state.currentStationId = current.stationId;
+      setTimeout(() => {
+        Session.set("currentStationId", current.stationId);
+        mittBus.emit('stationChangeRefresh', current.stationId);
+      }, 200)
+    }
+  }).catch(e => {
+    //console.error(e)
+  })
+}
+
+// 页面加载时
+onMounted(() => {
+  loadStationList();
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-station {
+  display: flex;
+  align-items: center;
+  height: 50px;
+  padding: 0 8px;
+
+  .station-select {
+    width: 230px;
+
+    :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;
+    }
+
+    :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;
+    }
+  }
+}
+</style>

+ 1 - 51
admin-web/src/layout/navBars/breadcrumb/toolbar.vue

@@ -1,22 +1,6 @@
 <template>
   <div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
 
-    <!--    站点切换-->
-    <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="handleStationChange">
-      <div class="layout-navbars-breadcrumb-user-icon">
-        <!--        <i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>-->
-        <SvgIcon name="ele-Location"></SvgIcon>
-        <span style="color:var(--el-text-color-primary)">{{ state.currentStation.stationName }} </span>
-        <!--        <el-icon class="el-icon&#45;&#45;right"><ele-Location />    </el-icon>-->
-      </div>
-      <template #dropdown>
-        <el-dropdown-menu>
-          <el-dropdown-item v-for="station in state.stationList" :key="station.stationId" :command="station.stationId" :disabled="state.currentStation.stationId == station.stationId">{{ station.stationName }}</el-dropdown-item>
-        </el-dropdown-menu>
-      </template>
-    </el-dropdown>
-
-
     <!--    字体大小-->
     <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
       <div class="layout-navbars-breadcrumb-user-icon">
@@ -127,7 +111,6 @@ import other from '/@/utils/other';
 import mittBus from '/@/utils/mitt';
 import {Session, Local} from '/@/utils/storage';
 import u from "/@/utils/u";
-import {$body} from "/@/utils/request";
 
 // 引入组件
 const Notification = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/notification.vue'));
@@ -153,9 +136,7 @@ const props = defineProps({
 const state = reactive({
   isScreenfull: false,
   disabledI18n: 'zh-cn',
-  disabledSize: 'large',
-  stationList: [],
-  currentStation: {}
+  disabledSize: 'large'
 });
 
 const onSwitchTheme = () => {
@@ -248,18 +229,6 @@ const onSearchClick = () => {
   searchRef.value.openSearch();
 };
 
-//站点切换
-const handleStationChange = (stationId: string) => {
-  //console.log(stationId)
-
-  let station = state.stationList.find((k: any) => k.stationId === stationId)
-  if (station) {
-    state.currentStation = station;
-    Session.set("currentStationId",stationId)
-    mittBus.emit('stationChangeRefresh', stationId);
-  }
-}
-
 
 // 组件大小改变
 const onComponentSizeChange = (size: string) => {
@@ -288,26 +257,7 @@ onMounted(() => {
     initI18nOrSize('globalComponentSize', 'disabledSize');
     initI18nOrSize('globalI18n', 'disabledI18n');
   }
-
-  loadStationList();
 });
-
-const loadStationList = () => {
-  $body(`/washStation/list`, {pageNum: 1,pageSize:1024}).then((res: any) => {
-    let {list} = res;
-    state.stationList = list;
-    if (list && list.length > 0) {
-      state.currentStation = list[0];
-      setTimeout(()=>{
-        mittBus.emit('stationChangeRefresh', list[0].stationId);
-        Session.set("currentStationId",list[0].stationId)
-      },200)
-
-    }
-  }).catch(e => {
-    //console.error(e)
-  })
-}
 </script>
 
 <style scoped lang="scss">

+ 4 - 0
admin-web/src/views/admin/account/index.vue

@@ -248,6 +248,7 @@ import ExtPage from '/@/components/form/ExtPage.vue'
 import mittBus from '/@/utils/mitt';
 import ExtDLabel from "/@/components/form/ExtDLabel.vue";
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const AdminUserDialog = defineAsyncComponent(() => import("/@/views/admin/account/detail.vue"));
 
@@ -325,6 +326,9 @@ const state = reactive({
 onBeforeMount(() => {
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   loadData();
 

+ 4 - 0
admin-web/src/views/admin/finance/settlement.vue

@@ -156,6 +156,7 @@ import {Msg} from "/@/utils/message";
 
 import ExtPage from '/@/components/form/ExtPage.vue'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const queryRef = ref();
 
@@ -195,6 +196,9 @@ const state = reactive({
   ],
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   loadData();
 

+ 4 - 0
admin-web/src/views/admin/finance/splitRecord.vue

@@ -171,6 +171,7 @@ import {ElButton} from 'element-plus'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
 import ExtDSelect from "/@/components/form/ExtDSelect.vue";
 import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 
 //定义引用
@@ -217,6 +218,9 @@ const state = reactive({
 
 //生命周期钩子
 //生命周期钩子
+//站点筛选默认全局当前站点;本页面常由站点账户跳转携带指定站点,不跟随全局切换
+useStationScope(state.formQuery, loadData, {follow: false});
+
 onBeforeMount(() => {
 
   let query = route.query;

+ 4 - 0
admin-web/src/views/admin/finance/withdraw.vue

@@ -137,6 +137,7 @@ import mittBus from '/@/utils/mitt';
 
 import {ElButton} from 'element-plus'
 import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 
 //定义引用
@@ -190,6 +191,9 @@ onBeforeMount(() => {
 
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   loadData();
 

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

@@ -486,7 +486,8 @@ onMounted(() => {
   loadCurrentEquipmentStatus();
 
   mittBus.on('stationChangeRefresh', (stationId) => {
-    if (stationId) {
+    //站点未变化时不重复加载
+    if (stationId && stationId !== state.currentStationId) {
       state.currentStationId = stationId;
       loadStationStat()
       loadStationStatToday()

+ 4 - 0
admin-web/src/views/admin/investor/index.vue

@@ -148,6 +148,7 @@ import ExtPage from '/@/components/form/ExtPage.vue'
 import mittBus from '/@/utils/mitt';
 import ExtDSelect from "/@/components/form/ExtDSelect.vue";
 import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const InvestorInfoDialog = defineAsyncComponent(() => import("/@/views/admin/investor/dialog.vue"));
 
@@ -201,6 +202,9 @@ const state = reactive({
 onBeforeMount(() => {
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   loadData();
 

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

@@ -99,6 +99,7 @@ import u from '/@/utils/u'
 
 import ExtPage from '/@/components/form/ExtPage.vue'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const queryRef = ref();
 
@@ -129,6 +130,9 @@ const state = reactive({
   ],
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   loadData();
 

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

@@ -132,6 +132,7 @@ import mittBus from '/@/utils/mitt';
 
 import {ElButton} from 'element-plus'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 
 //定义引用
@@ -176,6 +177,9 @@ const state = reactive({
 //         }
 // );
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 //生命周期钩子
 onBeforeMount(() => {
 

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

@@ -92,6 +92,7 @@ import {ElButton, ElMessageBox} from 'element-plus'
 import DeviceDialog from '/@/views/admin/station/device/dialog.vue'
 import DeviceConfigDialog from '/@/views/admin/station/device/config.vue'
 import DeviceRemoteDialog from '/@/views/admin/station/device/remote.vue'
+import {useStationScope} from '/@/hooks/useStationScope'
 
 
 //定义引用
@@ -233,6 +234,9 @@ const state = reactive({
 //         }
 // );
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 //生命周期钩子
 onBeforeMount(() => {
   let token = Session.get("token")

+ 17 - 0
admin-web/src/views/admin/station/fault/index.vue

@@ -198,6 +198,7 @@ import {Msg} from "/@/utils/message";
 import ExtPage from '/@/components/form/ExtPage.vue'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
 import mittBus from '/@/utils/mitt';
+import {useStationScope} from '/@/hooks/useStationScope';
 
 
 const state = reactive({
@@ -319,10 +320,26 @@ const loadFaultRecords = (refresh: boolean = false) => {
 
 // ============ 生命周期 ============
 
+//站点筛选与全局当前站点联动:切换站点时同步订阅与故障记录两个页签的筛选
+const reloadFaultPage = (refresh?: boolean) => {
+  state.faultQuery.stationId = state.formQuery.stationId;
+  if (state.activeTab === 'faultRecord') {
+    loadFaultRecords(!!refresh);
+  }
+  loadSubscribers(!!refresh);
+};
+useStationScope(state.formQuery, reloadFaultPage);
+
 onBeforeMount(() => {
 });
 
 onMounted(() => {
+  //默认选中全局当前站点时,自动加载订阅列表
+  if (state.formQuery.stationId) {
+    state.faultQuery.stationId = state.formQuery.stationId;
+    loadSubscribers(true);
+  }
+
   nextTick(() => {
     let bodyHeight = document.body.clientHeight;
     state.tableHeight = bodyHeight - 280;

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

@@ -119,6 +119,7 @@ import {Msg} from "/@/utils/message";
 
 import ExtPage from '/@/components/form/ExtPage.vue'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const queryRef = ref();
 
@@ -157,6 +158,9 @@ const state = reactive({
   ],
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   // 同步日期默认昨天
   const yesterday = new Date();

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

@@ -106,6 +106,7 @@ import {Msg} from "/@/utils/message";
 
 import ExtPage from '/@/components/form/ExtPage.vue'
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useStationScope} from '/@/hooks/useStationScope';
 
 const queryRef = ref();
 
@@ -143,6 +144,9 @@ const state = reactive({
   ],
 })
 
+//站点筛选与全局当前站点联动
+useStationScope(state.formQuery, loadData);
+
 onMounted(() => {
   // 同步月份默认上月
   const now = new Date();