Browse Source

feat: 完成首页页面的功能编写

曾坤森 2 weeks ago
parent
commit
3967a2955c

+ 1 - 0
.stylelintrc.cjs

@@ -14,5 +14,6 @@ module.exports = {
       },
     ],
     'no-descending-specificity': null,
+    'declaration-block-no-redundant-longhand-properties': null,
   },
 };

+ 1 - 0
src/api/dashboard.ts

@@ -45,6 +45,7 @@ export interface DashboardParams {
   startTime: string | null;
   endTime: string | null;
   entityType: number | null;
+  station?: string | null;
 }
 export interface LoginRes {
   success: boolean;

+ 1 - 0
src/api/home.ts

@@ -19,6 +19,7 @@ export interface Tree {
   medium: number;
   low: number;
   child: any[];
+  expanded?: boolean;
 }
 export interface AlarmTotalRes {
   success: boolean;

+ 1 - 0
src/assets/icon/folder.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764038898911" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5097" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M576 268.8h313.6c12.8 0 19.2-12.8 19.2-25.6v-76.8c0-12.8-6.4-25.6-19.2-25.6H518.4c-19.2 0-25.6 25.6-12.8 38.4l57.6 83.2c0 6.4 6.4 6.4 12.8 6.4z" p-id="5098"></path><path d="M902.4 320H576c-12.8 0-32-6.4-38.4-25.6L409.6 128c-12.8-12.8-25.6-19.2-44.8-19.2H128c-32 0-57.6 32-57.6 64V800c0 38.4 25.6 64 57.6 64h774.4c32 0 57.6-32 57.6-64V384c0-38.4-25.6-64-57.6-64zM633.6 672c0 19.2-12.8 32-32 32H204.8c-19.2 0-32-12.8-32-32s12.8-32 32-32H608c12.8 0 25.6 12.8 25.6 32z m166.4 0c0 19.2-12.8 32-32 32h-25.6c-19.2 0-32-12.8-32-32s12.8-32 32-32H768c19.2 0 32 12.8 32 32z" p-id="5099"></path></svg>

+ 21 - 0
src/assets/style/global.less

@@ -134,4 +134,25 @@ body {
 
 .arco-modal {
   background-color: var(--color-theme-2);
+
+  .arco-modal-header {
+    border-bottom-color: var(--color-border);
+
+    .arco-modal-title {
+      color: var(--color-white);
+      font-weight: 600;
+    }
+  }
+
+  .arco-modal-close-btn {
+    color: var(--color-white);
+  }
+
+  .arco-modal-footer {
+    border-top-color: var(--color-border);
+  }
+
+  .arco-modal-body {
+    color: var(--color-white);
+  }
 }

+ 2 - 4
src/components/menu/nav-second-menu.vue

@@ -15,8 +15,7 @@
   </ul>
 </template>
 <script lang="tsx" setup name="NavSecondMenu">
-import useMenuTree from './use-menu-tree';
-import { defineComponent, ref, h, compile, computed } from 'vue';
+import { ref, h, compile, computed } from 'vue';
 import {
   useRoute,
   useRouter,
@@ -59,7 +58,6 @@ const displayedMenuList = computed(() => {
   // 最后使用默认菜单
   return menuList.value || [];
 });
-console.log('displayedMenuList', displayedMenuList.value);
 // 处理自身鼠标进入事件
 const handleSelfMouseEnter = () => {
   mouseInSelf = true;
@@ -87,7 +85,7 @@ const handleSelfMouseLeave = () => {
 const handleJumpRoute = (item: RouteRecordRaw) => {
   console.log('item', item.name);
   // 使用 name 跳转避免路径解析问题
-  router.push({ name: item.name });
+  router.push({ name: item.name, params: { station: 'all' } });
 };
 </script>
 <style lang="less" scoped>

+ 6 - 3
src/components/navbar/index.vue

@@ -25,7 +25,7 @@
       <GlobalBreadcrumb v-show="!topMenu" />
       <div class="center-side">
         <NavMenu
-          v-if="topMenu"
+          v-if="topMenu && !showTopNavbar"
           :internalHoveredItem="internalHoveredItem"
           @menuHover="handleMenuHover"
         ></NavMenu>
@@ -209,17 +209,19 @@ import useLocale from '@/hooks/locale';
 import useUser from '@/hooks/user';
 import NavMenu from '@/components/menu/nav-menu.vue';
 import NavSecondMenu from '@/components/menu/nav-second-menu.vue';
-import MessageBox from '../message-box/index.vue';
 import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
 import { useRoute, useRouter, RouteRecordNormalized } from 'vue-router';
 
 const router = useRouter();
+const route = useRoute();
+
 const appStore = useAppStore();
 const userStore = useUserStore();
 const { logout } = useUser();
 const { changeLocale, currentLocale } = useLocale();
 const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
 const locales = [...LOCALE_OPTIONS];
+const showTopNavbar = computed(() => route.name === 'HomePage');
 const avatar = computed(() => {
   return userStore.avatar;
 });
@@ -362,7 +364,8 @@ const internalHoveredItemFun = (item: RouteRecordNormalized) => {
 
     .nav-btn {
       color: rgb(var(--gray-8));
-      font-size: 16px;
+      font-weight: 600;
+      font-size: 20px;
       border-color: rgb(var(--gray-2));
     }
 

+ 5 - 2
src/layout/default-layout.vue

@@ -78,8 +78,11 @@ const router = useRouter();
 const route = useRoute();
 const permission = usePermission();
 useResponsive(true);
+
 const navbarHeight = `40px`;
-const topNavbarHeight = `76px`;
+const topNavbarHeight = computed(() =>
+  route.name === 'HomePage' ? '40px' : '76px'
+);
 const navbar = computed(() => appStore.navbar);
 const topMenu = computed(() => appStore.topMenu);
 const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
@@ -97,7 +100,7 @@ const paddingStyle = computed(() => {
       ? { paddingLeft: `${menuWidth.value}px` }
       : {};
   const paddingTop = navbar.value
-    ? { paddingTop: topMenu.value ? topNavbarHeight : navbarHeight }
+    ? { paddingTop: topMenu.value ? topNavbarHeight.value : navbarHeight }
     : {};
   return { ...paddingLeft, ...paddingTop };
 });

+ 4 - 1
src/main.ts

@@ -8,6 +8,8 @@ import i18n from './locale';
 import directive from './directive';
 import './mock';
 import App from './App.vue';
+import ECharts from 'vue-echarts';
+
 // Styles are imported via arco-plugin. See config/plugin/arcoStyleImport.ts in the directory for details
 // 样式通过 arco-plugin 插件导入。详见目录文件 config/plugin/arcoStyleImport.ts
 // https://arco.design/docs/designlab/use-theme-package
@@ -17,7 +19,8 @@ const app = createApp(App);
 
 app.use(ArcoVue, {});
 app.use(ArcoVueIcon);
-
+// 全局注册 v-chart 组件
+app.component('VChart', ECharts);
 app.use(router);
 app.use(store);
 app.use(i18n);

+ 1 - 1
src/router/routes/modules/dashboard.ts

@@ -13,7 +13,7 @@ const DASHBOARD: AppRouteRecordRaw = {
   },
   children: [
     {
-      path: 'workplace',
+      path: 'workplace/:station',
       name: 'Workplace',
       component: () => import('@/views/dashboard/workplace/index.vue'),
       meta: {

+ 7 - 1
src/views/dashboard/workplace/index.vue

@@ -171,6 +171,7 @@ import {
   getCurrentInstance,
   computed,
 } from 'vue';
+import { useRoute } from 'vue-router';
 import { queryDashboardList, exportDashboardList } from '@/api/dashboard';
 import type { DashboardParams, DataList } from '@/api/dashboard';
 import { SizeProps, Pagination } from '@/types/global';
@@ -187,7 +188,9 @@ import DeviceInfoDialog from './device-info/index.vue';
 import useDictList from '@/hooks/dict-list';
 
 const { t } = useI18n();
-
+const {
+  params: { station },
+} = useRoute();
 const { loading, setLoading } = useLoading(true);
 const cloneColumns = computed(() => [
   {
@@ -246,6 +249,7 @@ const generateFormModel = () => {
     endTime: null,
     time: ['', ''],
     entityType: null,
+    station: null,
   } as DashboardParams;
 };
 const renderData = ref<DataList[]>([] as DataList[]);
@@ -269,6 +273,8 @@ function searchTable() {
     : ['', ''];
   formModel.value.startTime = startTime ? startTime : null;
   formModel.value.endTime = endTime ? endTime : null;
+  formModel.value.station =
+    station && station.toString() !== 'all' ? (station as string) : null;
   queryDashboardList(formModel.value)
     .then(res => {
       pagination.current = formModel.value.pageIndex;

+ 224 - 0
src/views/home/count/index.vue

@@ -0,0 +1,224 @@
+<template>
+  <v-chart ref="chartRef" :option="chartOptions" autoresize class="chart" />
+  <div class="box">
+    <div>
+      <span class="count">{{ data.high }}</span>
+      <div>
+        <div class="hight"></div>
+        <span class="label">High</span>
+      </div>
+    </div>
+    <div>
+      <span class="count">{{ data.medium }}</span>
+      <div>
+        <div class="mediun"></div>
+        <span class="label">Mediun</span>
+      </div>
+    </div>
+    <div>
+      <span class="count">{{ data.low }}</span>
+      <div>
+        <div class="low"></div>
+        <span class="label">Low</span>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup name="CountPage">
+import { ref, computed } from 'vue';
+import { use } from 'echarts/core';
+import { BarChart } from 'echarts/charts';
+import {
+  TooltipComponent,
+  LegendComponent,
+  GridComponent,
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+import type { AlarmTotalRes } from '@/api/home';
+import type { ComposeOption } from 'echarts/core';
+import type { BarSeriesOption } from 'echarts/charts';
+import type {
+  TooltipComponentOption,
+  LegendComponentOption,
+  GridComponentOption,
+} from 'echarts/components';
+
+interface ListPageProps {
+  data: AlarmTotalRes;
+}
+const props = withDefaults(defineProps<ListPageProps>(), {
+  data: () => ({}) as AlarmTotalRes,
+});
+// 使用 computed 来处理数据,确保响应性
+const chartData = computed(() => props.data);
+use([
+  TooltipComponent,
+  LegendComponent,
+  GridComponent,
+  BarChart,
+  CanvasRenderer,
+]);
+
+type EChartsOption = ComposeOption<
+  | TooltipComponentOption
+  | LegendComponentOption
+  | GridComponentOption
+  | BarSeriesOption
+>;
+
+const chartOptions = computed<EChartsOption>(() => {
+  return {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+      formatter: '{b}: {c}',
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: 0,
+      top: 0,
+      containLabel: true,
+    },
+    xAxis: {
+      show: false,
+      type: 'value',
+      axisLine: {
+        show: false,
+      },
+      axisTick: {
+        show: false,
+      },
+      axisLabel: {
+        color: '#fff',
+        fontSize: 12,
+      },
+      splitLine: {
+        lineStyle: {
+          color: 'rgba(255, 255, 255, 0.1)',
+        },
+      },
+    },
+    yAxis: {
+      show: false,
+      type: 'category',
+      data: ['High', 'Medium', 'Low'],
+      axisLine: {
+        show: false,
+      },
+      axisTick: {
+        show: false,
+      },
+      axisLabel: {
+        color: '#fff',
+        fontSize: 14,
+        margin: 10,
+      },
+    },
+    series: [
+      {
+        name: '数值',
+        type: 'bar',
+        data: [
+          {
+            value: props.data?.high || 0,
+            label: {
+              show: false,
+            },
+            itemStyle: {
+              color: '#ff4d4f',
+            },
+          },
+          {
+            value: props.data?.medium || 0,
+            label: {
+              show: false,
+            },
+            itemStyle: {
+              color: '#ffa940',
+            },
+          },
+          {
+            value: props.data?.low || 0,
+            label: {
+              show: false,
+            },
+            itemStyle: {
+              color: '#52c41a',
+            },
+          },
+        ],
+        barWidth: '40%',
+        label: {
+          show: true,
+          position: 'right',
+          color: '#fff',
+          fontSize: 14,
+        },
+      },
+    ],
+  };
+});
+</script>
+<style lang="less" scoped>
+.chart {
+  width: 100%;
+  height: 220px;
+}
+
+.box {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  width: 100%;
+
+  div {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 33.33%;
+
+    .count {
+      margin-bottom: 5px;
+      color: var(--color-white);
+      font-weight: 700;
+      font-size: 24px;
+    }
+
+    div {
+      display: flex;
+      flex-direction: row;
+      width: 200px;
+
+      .label {
+        margin-left: 8px;
+        color: var(--color-white);
+        font-weight: 600;
+        font-size: 14px;
+      }
+    }
+  }
+
+  .hight {
+    width: 15px;
+    height: 15px;
+    background-color: #ff4d4f;
+  }
+
+  .mediun {
+    width: 15px;
+    height: 15px;
+    background-color: #ffa940;
+  }
+
+  .low {
+    width: 15px;
+    height: 15px;
+    background-color: #52c41a;
+  }
+}
+</style>

+ 29 - 17
src/views/home/index.vue

@@ -2,35 +2,43 @@
   <div class="container">
     <div class="card-box">
       <div class="left-box">
-        <a-card class="count-card"> </a-card>
-        <a-card class="list-card">
+        <a-card class="count-card no-border">
+          <CountPage :data></CountPage>
+        </a-card>
+        <a-card class="list-card no-border">
           <HomePage :data></HomePage>
         </a-card>
       </div>
-      <a-card class="right-card"> </a-card>
+      <a-card class="right-card">
+        <TreePage :data></TreePage>
+      </a-card>
     </div>
   </div>
 </template>
 <script lang="ts" setup name="HomePage">
-import {
-  ref,
-  reactive,
-  shallowRef,
-  h,
-  getCurrentInstance,
-  computed,
-  onMounted,
-} from 'vue';
+import { ref } from 'vue';
+import { useIntervalFn } from '@vueuse/core';
 import type { AlarmTotalRes } from '@/api/home';
 import { fetchHomeAlarmTotal } from '@/api/home';
 
 import HomePage from './list/index.vue';
-
+import CountPage from './count/index.vue';
+import TreePage from './tree/index.vue';
 const data = ref<AlarmTotalRes>({} as AlarmTotalRes);
-fetchHomeAlarmTotal().then(res => {
-  data.value = res;
-  console.log('res', res);
-});
+const getData = () => {
+  fetchHomeAlarmTotal().then(res => {
+    data.value = res;
+    console.log('res', res);
+  });
+};
+const { pause, resume, isActive } = useIntervalFn(
+  () => {
+    /* your function */
+    getData();
+  },
+  1 * 60 * 1000
+);
+getData();
 </script>
 <style lang="less" scoped>
 .container {
@@ -55,5 +63,9 @@ fetchHomeAlarmTotal().then(res => {
   .count-card {
     margin-bottom: 10px;
   }
+
+  :deep(.arco-card-bordered) {
+    border: none;
+  }
 }
 </style>

+ 21 - 12
src/views/home/list/index.vue

@@ -35,6 +35,19 @@
         record.time && dayjs(record.time).format('YYYY-MM-DD HH:mm:ss')
       }}</span>
     </template>
+    <template #name="{ record }">
+      <div
+        class="hover-link"
+        @click="
+          router.push({
+            name: 'Workplace',
+            params: { station: record.station },
+          })
+        "
+      >
+        {{ record.name }}
+      </div>
+    </template>
   </a-table>
 </template>
 
@@ -49,23 +62,17 @@ import {
   onMounted,
   watch,
 } from 'vue';
-import {
-  queryDashboardList,
-  exportDashboardList,
-  deleteDeviceDetails,
-} from '@/api/dashboard';
 import type { DashboardParams, DataList } from '@/api/dashboard';
 import type { AlarmTotalRes } from '@/api/home';
 import { SizeProps, Pagination } from '@/types/global';
 import BTag from '@/components/business/b-tag/index.vue';
-import type { TableColumnData } from '@arco-design/web-vue';
 import useLoading from '@/hooks/loading';
 import { useI18n } from 'vue-i18n';
 import dayjs from 'dayjs';
 import { DeviceInfo } from '@/utils/const';
-import { Modal } from '@arco-design/web-vue';
 import type { AdditionalProp } from '@/api/dict';
 import useDictList from '@/hooks/dict-list';
+import router from '@/router';
 
 interface ListPageProps {
   data: AlarmTotalRes;
@@ -90,6 +97,7 @@ const cloneColumns = computed(() => [
     dataIndex: 'time',
     slotName: 'time',
     ellipsis: true,
+    tooltip: true,
     width: 120,
   },
   {
@@ -191,11 +199,12 @@ const handleClick = (value: DataList) => {
 </script>
 
 <style lang="less" scoped>
-.container {
-  padding: 10px 10px 20px;
+.table-list {
+  margin-top: 0;
+}
 
-  .table-list {
-    margin-top: 0;
-  }
+.hover-link {
+  color: var(--primary-4);
+  cursor: pointer;
 }
 </style>

+ 148 - 0
src/views/home/tree/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="folder-structure">
+    <div v-for="(folder, key) in treeData" :key="key" class="folder-item">
+      <div class="folder-main" @click="toggleFolder(key)">
+        <span class="folder-toggle">
+          <icon-down v-if="folder.expanded" />
+          <icon-right v-else />
+        </span>
+        <span class="folder-name">{{ folder.abbreviation }}</span>
+      </div>
+
+      <div
+        v-if="folder.expanded && folder.child.length > 0"
+        class="folder-children"
+      >
+        <div
+          v-for="child in folder.child"
+          :key="child.name"
+          class="child-folder"
+          @click="handleChild(child)"
+        >
+          <FolderIcon
+            class="folder-icon"
+            :style="{
+              fill: child.high
+                ? '#ff4d4f'
+                : child.medium
+                  ? '#ffa940'
+                  : child.low
+                    ? '#52c41a'
+                    : '#ffd700',
+            }"
+          />
+          <span class="child-name">{{ child.abbreviation }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup name="TreePage">
+import type { AlarmTotalRes, Tree } from '@/api/home';
+import { watch, ref } from 'vue';
+import useLoading from '@/hooks/loading';
+// 导入 SVG 作为 Vue 组件
+import FolderIcon from '@/assets/icon/folder.svg';
+import router from '@/router';
+const { loading, setLoading } = useLoading(true);
+interface ListPageProps {
+  data: AlarmTotalRes;
+}
+const props = withDefaults(defineProps<ListPageProps>(), {
+  data: () => ({}) as AlarmTotalRes,
+});
+const treeData = ref<Tree[]>(props.data.tree);
+watch(
+  () => props.data,
+  () => {
+    treeData.value = props.data.tree;
+    treeData.value.forEach(item => {
+      item.expanded = true;
+    });
+    setLoading(false);
+  }
+);
+// 切换文件夹展开/折叠状态
+const toggleFolder = (folderName: number) => {
+  if (treeData.value[folderName]) {
+    treeData.value[folderName].expanded = !treeData.value[folderName].expanded;
+  }
+};
+
+const handleChild = (folder: Tree) => {
+  router.push({ name: 'Workplace', params: { station: folder.name } });
+};
+</script>
+<style lang="less" scoped>
+.folder-structure {
+  margin: 0;
+}
+
+.folder-item {
+  margin: 2px 0;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.folder-item:hover {
+  background-color: var(--color-theme-2);
+  border-radius: 5px;
+}
+
+.folder-main {
+  display: flex;
+  align-items: center;
+  padding: 8px 0;
+}
+
+.folder-name {
+  margin-top: 3px;
+  color: var(--color-white);
+  font-weight: 500;
+  font-size: 16px;
+}
+
+.child-name {
+  margin-top: 5px;
+  color: var(--color-white);
+  font-weight: 500;
+  font-size: 14px;
+}
+
+.folder-toggle {
+  width: 20px;
+  margin-right: 8px;
+  color: #4facfe;
+  font-size: 14px;
+  text-align: center;
+}
+
+.folder-children {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  margin-left: 10px;
+  padding-left: 15px;
+  border-left: 1px solid var(--color-border);
+}
+
+.child-folder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin-left: 10px;
+  padding: 5px 0;
+  text-align: center;
+}
+
+.folder-icon {
+  width: 40px;
+  height: 25px;
+}
+
+.child-folder:hover {
+  background: var(--color-border);
+  border-radius: 5px;
+}
+</style>

+ 2 - 2
src/views/login/components/login-form.vue

@@ -151,14 +151,14 @@ const setRememberPassword = (value: boolean) => {
   }
 
   &-title {
-    color: var(--color-text-1);
+    color: var(--color-white);
     font-weight: 500;
     font-size: 24px;
     line-height: 32px;
   }
 
   &-sub-title {
-    color: var(--color-text-3);
+    color: var(--color-white);
     font-size: 16px;
     line-height: 24px;
   }

+ 1 - 1
src/views/system/dict/index.vue

@@ -267,7 +267,7 @@ const handleDeleteFun = (id: number) => {
 
   .table-list {
     .hover-link {
-      color: rgb(var(--primary-6));
+      color: rgb(var(--primary-4));
       cursor: pointer;
     }
   }