168 lines
3.9 KiB
Vue
168 lines
3.9 KiB
Vue
<template>
|
||
<!-- 左中:圖表 -->
|
||
<section
|
||
class="row-span-4 bg-white/30 backdrop-blur-sm rounded-md shadow min-h-0 flex items-stretch"
|
||
>
|
||
<div
|
||
class="w-full h-full min-h-[200px] p-8 sm:min-h-[240px] md:min-h-[300px]"
|
||
>
|
||
<div ref="chartEl" class="w-full h-full"></div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||
import * as echarts from "echarts";
|
||
import { brand } from "@/styles/palette";
|
||
|
||
const props = defineProps({
|
||
activeKey: { type: String, default: "residents" },
|
||
// 結構同 currentFacility.charts:
|
||
// { [key]: { legends: [string, string], a: number[], b: number[] }, ... }
|
||
charts: { type: Object, required: true },
|
||
});
|
||
|
||
const chartEl = ref(null);
|
||
let chart = null;
|
||
let ro = null; // ResizeObserver
|
||
|
||
// 工具:近 7 天標籤
|
||
function last7DaysLabels() {
|
||
const labels = [];
|
||
const now = new Date();
|
||
for (let i = 6; i >= 0; i--) {
|
||
const d = new Date(now);
|
||
d.setDate(now.getDate() - i);
|
||
labels.push(`${d.getMonth() + 1}/${d.getDate()}`);
|
||
}
|
||
return labels;
|
||
}
|
||
|
||
// 標題
|
||
function mkTitle(text, subtext = "", opts = {}) {
|
||
return {
|
||
text,
|
||
subtext,
|
||
left: "center",
|
||
top: 12,
|
||
itemGap: 8,
|
||
textStyle: {
|
||
color: brand.black,
|
||
fontSize: 20,
|
||
fontWeight: 600,
|
||
fontFamily: '"Noto Sans TC"',
|
||
},
|
||
subtextStyle: {
|
||
color: brand.gray,
|
||
fontSize: 14,
|
||
fontWeight: 400,
|
||
fontFamily: '"Noto Sans TC"',
|
||
lineHeight: 20,
|
||
},
|
||
...opts,
|
||
};
|
||
}
|
||
|
||
function applyChartOption() {
|
||
if (!chart) return;
|
||
const conf = props.charts?.[props.activeKey];
|
||
if (!conf) return;
|
||
|
||
const title = `${conf.legends?.[0] ?? ""} / ${conf.legends?.[1] ?? ""}`;
|
||
const subTitle = "(近 7 天)";
|
||
|
||
chart.setOption({
|
||
color: [brand.green, brand.purple],
|
||
title: mkTitle(title, subTitle),
|
||
grid: { top: 84, left: 36, right: 16, bottom: 56, containLabel: true },
|
||
tooltip: { trigger: "axis", axisPointer: { type: "line" }, confine: true },
|
||
xAxis: {
|
||
type: "category",
|
||
boundaryGap: false,
|
||
data: last7DaysLabels(),
|
||
axisLine: { lineStyle: { color: brand.gray } },
|
||
axisTick: { show: false },
|
||
axisLabel: { color: brand.gray },
|
||
splitLine: { show: false },
|
||
},
|
||
legend: {
|
||
data: conf.legends || [],
|
||
bottom: 8,
|
||
icon: "circle",
|
||
itemWidth: 10,
|
||
itemHeight: 10,
|
||
itemGap: 24,
|
||
textStyle: { color: brand.gray },
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
name: "數量",
|
||
nameLocation: "middle",
|
||
nameGap: 40,
|
||
nameTextStyle: { padding: [0, 8, 0, 8] },
|
||
axisLine: { show: true },
|
||
axisTick: { show: true },
|
||
axisLabel: { color: brand.gray },
|
||
splitLine: { show: true, lineStyle: { color: brand.grayLight } },
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
name: conf.legends?.[0] ?? "",
|
||
type: "line",
|
||
data: conf.a || [],
|
||
symbol: "circle",
|
||
symbolSize: 6,
|
||
lineStyle: { width: 2 },
|
||
},
|
||
{
|
||
name: conf.legends?.[1] ?? "",
|
||
type: "line",
|
||
data: conf.b || [],
|
||
symbol: "circle",
|
||
symbolSize: 6,
|
||
lineStyle: { width: 2 },
|
||
},
|
||
],
|
||
});
|
||
}
|
||
|
||
function initChart() {
|
||
if (!chartEl.value) return;
|
||
chart = echarts.init(chartEl.value);
|
||
applyChartOption();
|
||
}
|
||
|
||
function disposeChart() {
|
||
if (chart) {
|
||
chart.dispose();
|
||
chart = null;
|
||
}
|
||
}
|
||
|
||
function onWindowResize() {
|
||
chart?.resize?.();
|
||
}
|
||
|
||
onMounted(() => {
|
||
initChart();
|
||
window.addEventListener("resize", onWindowResize);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener("resize", onWindowResize);
|
||
disposeChart();
|
||
});
|
||
|
||
// 只要 activeKey 或 charts(內容/參照)變化,就重繪
|
||
watch(
|
||
() => [props.activeKey, props.charts],
|
||
() => applyChartOption(),
|
||
{ deep: true }
|
||
);
|
||
</script>
|