Initial commit
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
46
README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 地熱
|
||||
|
||||
此專案是使用 Vite + Vue 3 建立的前端管理介面。
|
||||
|
||||
## 主要功能
|
||||
- Vue 3 + Vite
|
||||
- Element Plus UI
|
||||
- Pinia 狀態管理
|
||||
- Vue Router
|
||||
- ECharts 圖表
|
||||
- Leaflet 地圖
|
||||
|
||||
## 環境需求
|
||||
- Node.js(建議 v18+)
|
||||
- npm 或 yarn
|
||||
|
||||
## 安裝
|
||||
|
||||
在專案根目錄執行:
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
# 或使用 yarn
|
||||
# yarn
|
||||
```
|
||||
|
||||
## 常用指令
|
||||
|
||||
請使用 PowerShell(或相同 shell)在專案根目錄執行:
|
||||
|
||||
```powershell
|
||||
npm run dev # 本機開發 (Vite)
|
||||
npm run build # 產生 production bundle
|
||||
npm run preview # 本地預覽 build 後的結果
|
||||
```
|
||||
|
||||
這些 scripts 自 `package.json` 取得:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
```
|
||||
|
||||
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>結元能源</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2634
package-lock.json
generated
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "fabulous_fe",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.4",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-tilelayer-mbtiles": "^1.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
75
src/App.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import Navbar from "./components/Navbar.vue";
|
||||
import Sidebar from "./components/Sidebar.vue";
|
||||
import Breadcrumb from "./components/Breadcrumb.vue";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const size = "default";
|
||||
const zIndex = 2000;
|
||||
const isCollapse = ref(false);
|
||||
|
||||
const isMobile = ref(false);
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkIsMobile();
|
||||
window.addEventListener("resize", checkIsMobile);
|
||||
// 首次進入自動跳轉到 /plantsMap (hash 模式)
|
||||
if (window.location.hash === '' || window.location.hash === '#/' || window.location.hash === '#') {
|
||||
router.replace('/plantsMap');
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", checkIsMobile);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :size="size" :z-index="zIndex">
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<template v-if="!isMobile">
|
||||
<el-aside :width="isCollapse ? '0px' : '280px'">
|
||||
<Sidebar :isCollapse="isCollapse" />
|
||||
</el-aside>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-drawer
|
||||
v-if="isMobile"
|
||||
v-model="isCollapse"
|
||||
direction="ltr"
|
||||
:with-header="false"
|
||||
size="70%"
|
||||
body-class="mobile-sidebar-drawer"
|
||||
>
|
||||
<Sidebar :isCollapse="isCollapse" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<Navbar v-model:isCollapse="isCollapse" />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<!-- <Breadcrumb /> -->
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-header {
|
||||
padding: 0;
|
||||
}
|
||||
:deep(.mobile-sidebar-drawer) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
3
src/assets/bars-3.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
4
src/assets/logo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="537" height="530" viewBox="0 0 537 530" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="355.171" y="82.4884" width="256.214" height="256.214" transform="rotate(45 355.171 82.4884)" fill="#80B559"/>
|
||||
<path d="M0 265L265 0L333.5 68.5L137 265L333.5 461.5L265 530L0 265Z" fill="#80B559"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 312 B |
BIN
src/assets/marker-icon-blue.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/marker-icon-red.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/marker-shadow.png
Normal file
|
After Width: | Height: | Size: 618 B |
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
3
src/assets/x-mark.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
78
src/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isShallow: typeof import('vue')['isShallow']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
49
src/components.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Breadcrumb: typeof import('./components/Breadcrumb.vue')['default']
|
||||
ComboBarLineChart: typeof import('./components/Chart/ComboBarLineChart.vue')['default']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElList: typeof import('element-plus/es')['ElList']
|
||||
ElListItem: typeof import('element-plus/es')['ElListItem']
|
||||
ElListItemContent: typeof import('element-plus/es')['ElListItemContent']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
InfoList: typeof import('./components/InfoList.vue')['default']
|
||||
LineChart: typeof import('./components/Chart/LineChart.vue')['default']
|
||||
Navbar: typeof import('./components/Navbar.vue')['default']
|
||||
PieChart: typeof import('./components/Chart/PieChart.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Sidebar: typeof import('./components/Sidebar.vue')['default']
|
||||
}
|
||||
}
|
||||
19
src/components/Breadcrumb.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(item, idx) in breadcrumbs" :key="item.path || idx" :to="item.path && idx !== breadcrumbs.length - 1 ? { path: item.path } : undefined">
|
||||
<span v-if="!item.path || idx === breadcrumbs.length - 1">{{ item.meta && item.meta.title ? item.meta.title : item.name }}</span>
|
||||
<span v-else>{{ item.meta && item.meta.title ? item.meta.title : item.name }}</span>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const breadcrumbs = computed(() => {
|
||||
// route.matched 會依序包含所有父路由
|
||||
return route.matched.filter(r => r.meta && r.meta.title);
|
||||
});
|
||||
</script>
|
||||
103
src/components/Chart/ComboBarLineChart.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div :id="chartId" style="width: 100%; min-height: 250px"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, nextTick, computed } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const chartId = `comboChart-${Math.random().toString(36).slice(2)}`
|
||||
let chartInstance = null
|
||||
|
||||
const years = computed(() => props.data.map(item => item.year))
|
||||
|
||||
// 取得所有電廠名稱
|
||||
const plantNames = computed(() => {
|
||||
const set = new Set();
|
||||
props.data.forEach(item => {
|
||||
item.data.forEach(d => set.add(d.name));
|
||||
});
|
||||
return Array.from(set);
|
||||
});
|
||||
|
||||
// 產生 series
|
||||
function getSeries() {
|
||||
const barSeries = plantNames.value.map(name => ({
|
||||
name,
|
||||
type: 'bar',
|
||||
data: props.data.map(item => {
|
||||
const found = item.data.find(d => d.name === name);
|
||||
return found ? found.value : 0;
|
||||
}),
|
||||
barMaxWidth: 32
|
||||
}));
|
||||
// 總發電量
|
||||
const totalSeries = {
|
||||
name: '總發電量',
|
||||
type: 'line',
|
||||
data: props.data.map(item => item.data.reduce((sum, d) => sum + d.value, 0)),
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
lineStyle: { width: 3, color: '#409EFF' },
|
||||
itemStyle: { color: '#409EFF', borderColor: '#fff', borderWidth: 2 }
|
||||
};
|
||||
return [...barSeries, totalSeries];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const chartDom = document.getElementById(chartId)
|
||||
if (chartDom) {
|
||||
chartInstance = echarts.init(chartDom)
|
||||
chartInstance.setOption({
|
||||
title: props.title ? {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, color: '#333' }
|
||||
} : undefined,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
legend: {
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: years.value
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
name: 'MW',
|
||||
},
|
||||
series: getSeries()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
95
src/components/Chart/LineChart.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div :id="chartId" style="width: 100%; min-height: 250px"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, nextTick, computed } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const chartId = `lineChart-${Math.random().toString(36).slice(2)}`;
|
||||
let chartInstance = null;
|
||||
|
||||
const xData = computed(() => props.data.map((item) => item.time));
|
||||
const yData = computed(() => props.data.map((item) => item.value));
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const chartDom = document.getElementById(chartId);
|
||||
if (chartDom) {
|
||||
chartInstance = echarts.init(chartDom);
|
||||
chartInstance.setOption({
|
||||
title: props.title
|
||||
? {
|
||||
text: props.title,
|
||||
left: "center",
|
||||
textStyle: { fontSize: 16, color: "#333" },
|
||||
}
|
||||
: undefined,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
data: ["發電量"],
|
||||
bottom: 0,
|
||||
},
|
||||
grid: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: xData.value,
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
minInterval: 1,
|
||||
name: "MW",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "發電量",
|
||||
type: "line",
|
||||
data: yData.value,
|
||||
symbol: "circle",
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: "#409EFF",
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#409EFF",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "rgba(64,158,255,0.08)",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
90
src/components/Chart/PieChart.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div :id="chartId" style="width: 100%; min-height: 200px"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const chartId = `pieChart-${Math.random().toString(36).slice(2)}`
|
||||
let chartInstance = null
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const chartDom = document.getElementById(chartId)
|
||||
if (chartDom) {
|
||||
chartInstance = echarts.init(chartDom)
|
||||
chartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b} : {d}%'
|
||||
},
|
||||
title: props.title
|
||||
? {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, color: '#333' }
|
||||
}
|
||||
: undefined,
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '90%',
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: true,
|
||||
alignTo: 'edge',
|
||||
position: 'outer',
|
||||
formatter: '{b} : {d}%',
|
||||
edgeDistance: 10,
|
||||
minMargin: 0,
|
||||
lineHeight: 10
|
||||
},
|
||||
labelLine: {
|
||||
length: 10,
|
||||
length2: 0,
|
||||
maxSurfaceAngle: 80
|
||||
},
|
||||
labelLayout: function (params) {
|
||||
const isLeft = params.labelRect.x < chartInstance.getWidth() / 2
|
||||
const points = params.labelLinePoints
|
||||
points[2][0] = isLeft
|
||||
? params.labelRect.x
|
||||
: params.labelRect.x + params.labelRect.width
|
||||
return {
|
||||
labelLinePoints: points,
|
||||
y: params.labelRect.y - 4
|
||||
}
|
||||
},
|
||||
data: props.data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
60
src/components/InfoList.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<ul class="info-list">
|
||||
<li v-for="item in data" :key="item.label">
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
<span class="unit">{{ item.unit }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 175px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.info-list li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.info-list li:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
.info-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.info-list .label {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
width: 30%;
|
||||
}
|
||||
.info-list .value {
|
||||
color: #409eff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin: 0 8px;
|
||||
width: 60%;
|
||||
text-align: center;
|
||||
}
|
||||
.info-list .unit {
|
||||
color: #888;
|
||||
width: 30%;
|
||||
font-size: 0.85rem;
|
||||
text-align: end;
|
||||
}
|
||||
</style>
|
||||
77
src/components/Navbar.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<nav class="nav">
|
||||
<div class="title-area">
|
||||
<el-button class="collapse-btn" @click="toggleCollapse">
|
||||
<img
|
||||
v-if="collapseState"
|
||||
src="../assets/bars-3.svg"
|
||||
alt="展開"
|
||||
style="width: 24px; height: 24px"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="../assets/x-mark.svg"
|
||||
alt="收合"
|
||||
style="width: 24px; height: 24px"
|
||||
/>
|
||||
</el-button>
|
||||
<span class="title">工控運維管理平台</span>
|
||||
</div>
|
||||
<el-dropdown size="large">
|
||||
<el-button type="text" circle>
|
||||
<el-avatar :icon="UserFilled" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu style="font-size: large;">
|
||||
<el-dropdown-item>個人資料</el-dropdown-item>
|
||||
<el-dropdown-item>帳號登出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, defineEmits } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
const props = defineProps({
|
||||
isCollapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["update:isCollapse"]);
|
||||
const collapseState = computed({
|
||||
get: () => props.isCollapse,
|
||||
set: (val: boolean) => emit("update:isCollapse", val),
|
||||
});
|
||||
const toggleCollapse = () => {
|
||||
collapseState.value = !collapseState.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0px 0px 28px 0px rgba(86, 61, 124, 0.13);
|
||||
}
|
||||
.title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.collapse-btn {
|
||||
vertical-align: middle;
|
||||
padding: 20px 15px;
|
||||
}
|
||||
</style>
|
||||
192
src/components/Sidebar.vue
Normal file
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="$route.name"
|
||||
class="el-menu-vertical-demo"
|
||||
background-color="#00152a"
|
||||
active-text-color="#fff"
|
||||
text-color="#95a2b5"
|
||||
unique-opened
|
||||
>
|
||||
<div class="title-area">
|
||||
<img
|
||||
src="../assets/logo.svg"
|
||||
alt="logo"
|
||||
style="width: 40px; height: 40px"
|
||||
/>
|
||||
<span>結元能源</span>
|
||||
</div>
|
||||
<el-sub-menu index="overview">
|
||||
<template #title>
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>總覽</span>
|
||||
</template>
|
||||
<el-menu-item index="PlantsMap" @click="$router.push('/plantsMap')">地圖總覽</el-menu-item>
|
||||
<el-menu-item index="PlantsOverview" @click="$router.push('/plants')">電廠總覽</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="factory-info">
|
||||
<template #title>
|
||||
<el-icon><Postcard /></el-icon>
|
||||
<span>電廠資訊</span>
|
||||
</template>
|
||||
<el-menu-item index="factory-1">四磺子坪</el-menu-item>
|
||||
<el-menu-item index="factory-2">宜蘭大清水</el-menu-item>
|
||||
<el-menu-item index="factory-3">宜蘭小清水</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="inspection">
|
||||
<template #title>
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
<span>巡檢系統</span>
|
||||
</template>
|
||||
<el-menu-item index="inspection-task">巡檢任務</el-menu-item>
|
||||
<el-menu-item index="inspection-setting">巡檢設定</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="report">
|
||||
<template #title>
|
||||
<el-icon><DataLine /></el-icon>
|
||||
<span>報表查詢</span>
|
||||
</template>
|
||||
<el-menu-item index="report-factory">電廠報表</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="alert">
|
||||
<template #title>
|
||||
<el-icon><Bell /></el-icon>
|
||||
<span>即時告警</span>
|
||||
</template>
|
||||
<el-menu-item index="alert-event">異常事件查詢</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="material">
|
||||
<template #title>
|
||||
<el-icon><MessageBox /></el-icon>
|
||||
<span>備料管理</span>
|
||||
</template>
|
||||
<el-menu-item index="material-item">備品料件管理</el-menu-item>
|
||||
<el-menu-item index="material-location">倉庫櫃位管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="security">
|
||||
<template #title>
|
||||
<el-icon><VideoCamera /></el-icon>
|
||||
<span>智慧安防</span>
|
||||
</template>
|
||||
<el-menu-item index="security-system">安防系統</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="system">
|
||||
<template #title>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系統設定</span>
|
||||
</template>
|
||||
<el-menu-item index="system-factory">電廠設定</el-menu-item>
|
||||
<el-menu-item index="system-account">帳號設定</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, toRefs, defineProps } from "vue";
|
||||
import {
|
||||
Document,
|
||||
Menu as IconMenu,
|
||||
Location,
|
||||
Setting,
|
||||
Postcard,
|
||||
DocumentChecked,
|
||||
DataLine,
|
||||
Bell,
|
||||
MessageBox,
|
||||
VideoCamera
|
||||
} from "@element-plus/icons-vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-menu-vertical-demo {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
270deg,
|
||||
rgba(51, 148, 225, 0.18),
|
||||
transparent
|
||||
);
|
||||
background-color: #00152a;
|
||||
}
|
||||
|
||||
.title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
font-size: 1.8rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover) {
|
||||
color: #fff !important;
|
||||
background-color: #102e48 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
font-size: 1.1rem;
|
||||
padding-top: 30px !important;
|
||||
padding-bottom: 30px !important;
|
||||
padding-left: 60px !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item:hover) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* 為 menu-item 添加圓點 */
|
||||
:deep(.el-menu-item::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left:30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #95a2b5;
|
||||
}
|
||||
|
||||
/* 選中狀態的圓點為藍色 */
|
||||
:deep(.el-menu-item.is-active::after) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right:25px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #24b3a4;
|
||||
}
|
||||
|
||||
/* 展開狀態的子選單標題 */
|
||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
|
||||
background-color: #102e48 !important;
|
||||
color: #fff !important;
|
||||
box-shadow: inset 3px 0 0 #627ca0;
|
||||
}
|
||||
|
||||
/* 子選單內容背景 */
|
||||
:deep(.el-menu.el-menu--inline) {
|
||||
background: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-menu.el-menu--inline::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left:32.5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: .8px;
|
||||
height: 110%;
|
||||
background-color: #95a2b5;
|
||||
z-index: 80;
|
||||
}
|
||||
</style>
|
||||
10
src/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
26
src/router/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Home from "../views/Home.vue";
|
||||
import PlantsOverview from "../views/PlantsOverview.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/plantsMap",
|
||||
name: "PlantsMap",
|
||||
component: Home,
|
||||
meta: { title: "地圖總覽", icon: "Location", menuGroup: "overview" },
|
||||
},
|
||||
{
|
||||
path: "/plants",
|
||||
name: "PlantsOverview",
|
||||
component: PlantsOverview,
|
||||
meta: { title: "電廠總覽", icon: "Postcard", menuGroup: "overview" },
|
||||
},
|
||||
// 你可以依需求繼續擴充其他路由
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
0
src/store/index.js
Normal file
39
src/style.css
Normal file
@ -0,0 +1,39 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 統一 el-card 樣式 */
|
||||
.el-card.custom-card {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08);
|
||||
background: #fff;
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-card.custom-card .el-card__header{
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.8px;
|
||||
color: #213547;
|
||||
}
|
||||
273
src/views/Home.vue
Normal file
@ -0,0 +1,273 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from "vue";
|
||||
import PieChart from "../components/Chart/PieChart.vue";
|
||||
import LineChart from "../components/Chart/LineChart.vue";
|
||||
import ComboBarLineChart from "../components/Chart/ComboBarLineChart.vue";
|
||||
import InfoList from "../components/InfoList.vue";
|
||||
import markerIconBlue from "../assets/marker-icon-blue.png";
|
||||
import markerShadow from "../assets/marker-shadow.png";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
const avgData = [
|
||||
{
|
||||
year: 2021,
|
||||
data: [
|
||||
{ name: "大清水", value: 1120 },
|
||||
{ name: "小清水", value: 890 },
|
||||
{ name: "四磺子坪", value: 870 },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: 2022,
|
||||
data: [
|
||||
{ name: "大清水", value: 1134 },
|
||||
{ name: "小清水", value: 1120 },
|
||||
{ name: "四磺子坪", value: 780 },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: 2023,
|
||||
data: [
|
||||
{ name: "大清水", value: 1145 },
|
||||
{ name: "小清水", value: 1115 },
|
||||
{ name: "四磺子坪", value: 975 },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: 2024,
|
||||
data: [
|
||||
{ name: "大清水", value: 1172 },
|
||||
{ name: "小清水", value: 1135 },
|
||||
{ name: "四磺子坪", value: 1115 },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: 2025,
|
||||
data: [
|
||||
{ name: "大清水", value: 1185 },
|
||||
{ name: "小清水", value: 1143 },
|
||||
{ name: "四磺子坪", value: 1121 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const weekTrend = [
|
||||
{ time: "周一", value: 120 },
|
||||
{ time: "周二", value: 132 },
|
||||
{ time: "周三", value: 101 },
|
||||
{ time: "周四", value: 134 },
|
||||
{ time: "周五", value: 90 },
|
||||
{ time: "周六", value: 230 },
|
||||
{ time: "周日", value: 210 },
|
||||
];
|
||||
|
||||
const unconfirmed = ref(2014);
|
||||
const elecData = ref([
|
||||
{ value: 42, name: "大清水" },
|
||||
{ value: 26, name: "小清水" },
|
||||
{ value: 32, name: "四磺子坪" },
|
||||
]);
|
||||
|
||||
const plantOverview = reactive([
|
||||
{ label: "電廠數量", value: 9, unit: "座" },
|
||||
{ label: "總裝置容量", value: 189, unit: "MW" },
|
||||
{ label: "總發電量", value: 290, unit: "MW" },
|
||||
{ label: "總收益", value: 54123, unit: "萬元" },
|
||||
]);
|
||||
|
||||
const plantAccumulate = reactive([
|
||||
{ label: "大清水", value: 234, unit: "MW" },
|
||||
{ label: "小清水", value: 189, unit: "MW" },
|
||||
{ label: "四磺子坪", value: 100, unit: "MW" },
|
||||
]);
|
||||
|
||||
// 自訂地熱 icon
|
||||
const geothermalIcon = L.icon({
|
||||
iconUrl: markerIconBlue,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowUrl: markerShadow,
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
|
||||
// 地熱廠 marker 資料
|
||||
const geothermalMarkers = [
|
||||
{
|
||||
position: [25.19544, 121.60236],
|
||||
popup: {
|
||||
title: "四磺子坪",
|
||||
img: "https://picsum.photos/200/100?random=1",
|
||||
deviceNumber: 12,
|
||||
online: 11,
|
||||
desc: "11 MW",
|
||||
},
|
||||
},
|
||||
{
|
||||
position: [24.61231, 121.63697],
|
||||
popup: {
|
||||
title: "清水發電廠",
|
||||
img: "https://picsum.photos/200/100?random=2",
|
||||
deviceNumber: 8,
|
||||
online: 8,
|
||||
desc: "24 MW",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let timer = null;
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化 OpenStreetMap 地圖
|
||||
const map = L.map("leaflet-map").setView([25.05, 121.6], 9);
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 18,
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
geothermalMarkers.forEach(({ position, popup }) => {
|
||||
L.marker(position, { icon: geothermalIcon })
|
||||
.addTo(map)
|
||||
.bindTooltip(
|
||||
`<h4>${popup.title}</h4>
|
||||
<div class="info">
|
||||
<div>發電量 : ${popup.desc}</div>
|
||||
</div>
|
||||
`,
|
||||
{
|
||||
direction: "top",
|
||||
permanent: true,
|
||||
className: "custom-tooltip",
|
||||
offset: [0, -50], // Y 軸向上偏移 30px
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
timer = setInterval(() => {
|
||||
unconfirmed.value++;
|
||||
|
||||
const income = plantOverview.find((item) => item.label === "總收益");
|
||||
if (income) income.value += 10;
|
||||
|
||||
const acccome = plantAccumulate.find((item) => item.label === "四磺子坪");
|
||||
if (acccome) acccome.value += 1;
|
||||
}, 5 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :xs="24" :lg="7">
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>異常狀態</span>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12" style="margin-bottom: 10px">
|
||||
<el-statistic
|
||||
title="已復歸"
|
||||
:value="1234"
|
||||
value-style="color: #67C23A; font-size: 2rem;"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-statistic
|
||||
title="未復歸"
|
||||
:value="1345"
|
||||
value-style="color: #F56C6C; font-size: 2rem;"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-statistic
|
||||
title="已確認"
|
||||
:value="1120"
|
||||
value-style="color: #409EFF; font-size: 2rem;"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-statistic
|
||||
title="未確認"
|
||||
:value="unconfirmed"
|
||||
value-style="color: #E6A23C; font-size: 2rem;"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>發電分布</span>
|
||||
</template>
|
||||
<PieChart :data="elecData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="10">
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>今日發電量</span>
|
||||
</template>
|
||||
<div id="leaflet-map" style="height: 470px; width: 100%"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="7">
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>電廠概覽</span>
|
||||
</template>
|
||||
<InfoList :data="plantOverview" />
|
||||
</el-card>
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>累積發電</span>
|
||||
</template>
|
||||
<InfoList :data="plantAccumulate" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>發電趨勢</span>
|
||||
</template>
|
||||
<LineChart :data="weekTrend" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card shadow class="custom-card">
|
||||
<template #header>
|
||||
<span>年平均發電比較</span>
|
||||
</template>
|
||||
<ComboBarLineChart :data="avgData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-statistic__head) {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
:deep(.custom-tooltip) {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 4px 8px rgba(44, 62, 80, 0.24);
|
||||
padding: 10px;
|
||||
}
|
||||
:deep(.custom-tooltip) h4 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 1rem;
|
||||
color: #343a40;
|
||||
margin-top: 0;
|
||||
}
|
||||
:deep(.custom-tooltip) img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
:deep(.custom-tooltip) .info {
|
||||
font-size: .9rem;
|
||||
}
|
||||
</style>
|
||||
14
src/views/PlantsOverview.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<h1>電廠總覽</h1>
|
||||
<p>這裡是電廠總覽頁面,請依需求補充內容。</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 可在此引入資料與元件
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plants-overview {
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
21
vite.config.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ["vue", "vue-router"],
|
||||
dts: "src/auto-imports.d.ts",
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: "src/components.d.ts",
|
||||
}),
|
||||
],
|
||||
});
|
||||