[TOC]
# DAY24(2025/07/07)Vue+Highchart
## 進度日誌
- 結合UI庫(element)
## 筆記區📘 Vue3
### 結合UI庫(element)
App.vue
```vue
<template>
<el-button @click="showFilters = !showFilters" type="primary" plain size="small" class="mb-2"
style="border-radius: 14px;">
<span v-if="showFilters">收合篩選</span>
<span v-else>展開篩選</span>
</el-button>
<transition name="fade">
<div v-if="showFilters" class="filters-box">
<div v-for="grp in filterGroups" :key="grp.label" class="filter-group">
<label>{{ grp.label }}</label>
<el-checkbox-group v-model="pendingFilters[grp.key]" size="large">
<el-checkbox v-for="opt in grp.options" :value="opt" :key="opt" class="custom-checkbox">{{ opt
}}</el-checkbox>
</el-checkbox-group>
</div>
<div class="filter-btn-row">
<el-button type="primary" plain @click="applyFilters" size="large">篩選</el-button>
<el-button type="info" plain @click="resetFilters" size="large">重置</el-button>
</div>
<div class="filter-tags">
<template v-for="(arr, type) in pendingFilters" :key="type">
<el-tag v-for="val in arr" :key="type + '-' + val" closable size="large" @close="removeTag(type, val)"
:type="filters[type].includes(val) ? 'success' : 'info'" effect="light" class="filter-tag"
@click="() => handleTagClick(type, val)">{{ val }}</el-tag>
</template>
</div>
</div>
</transition>
<!-- ====== 這裡是四格圖表 ====== -->
<div class="dash-grid">
<GenderAgeBar />
<CityPieChart />
<TendencyBar />
<PowerHistogram />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import { filters, pendingFilters } from './filters'
import GenderAgeBar from './components/GenderAgeBar.vue'
import CityPieChart from './components/CityPieChart.vue'
import TendencyBar from './components/TendencyBar.vue'
import PowerHistogram from './components/PowerHistogram.vue'
const showFilters = ref(true)
provide('filters', filters)
provide('pendingFilters', pendingFilters)
const filterGroups = [
{ key: 'gender', label: '性別:', options: ['男', '女', '未填寫'] },
{ key: 'age', label: '年齡:', options: ['<=20', '21~30', '31~40', '41~50', '51~60', '>=61', '未填寫'] },
{ key: 'city', label: '註冊地:', options: ['台北市', '新北市', '基隆市', '宜蘭縣', '新竹市', '新竹縣', '桃園市', '苗栗縣', '台中市', '嘉義市', '雲林縣', '台南市', '高雄市', '未填寫'] },
{ key: 'tendency', label: '消費傾向:', options: ['歲末酬賓必來', '母親節必來', '年中慶必來', '周年慶必來', '價格敏感客群'] },
{ key: 'power', label: '消費力:', options: ['0~1K', '1K~2K', '2K~5K', '5K~10K', '10K~50K', '50K~100K', '100K~1M', '1000K~100M+'] },
]
// === 必須加這些 function ===
function applyFilters() {
Object.keys(filters).forEach(k => {
filters[k].splice(0)
filters[k].push(...pendingFilters[k])
})
}
function resetFilters() {
Object.keys(filters).forEach(k => filters[k].splice(0))
Object.keys(pendingFilters).forEach(k => pendingFilters[k].splice(0))
}
function removeTag(type, val) {
const arr = pendingFilters[type]
const idx = arr.indexOf(val)
if (idx >= 0) arr.splice(idx, 1)
}
function handleTagClick(type, val) {
const arr = pendingFilters[type]
const idx = arr.indexOf(val)
if (idx >= 0) arr.splice(idx, 1)
else arr.push(val)
}
</script>
<style>
/* ...保持你之前那段 style 即可 ... */
.dash-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 36px;
}
@media (max-width: 900px) {
.dash-grid {
grid-template-columns: 1fr;
gap: 12px;
}
}
</style>
```
main.js
```js
import { createApp } from 'vue'
import App from './App.vue'
import HighchartsVue from 'highcharts-vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(HighchartsVue)
app.use(ElementPlus)
app.mount('#app')
```
GenderAgeBar.vue
```vue
<template>
<div>
<h2>性別與年齡分布</h2>
<highcharts :options="chartOptions" />
</div>
</template>
<script setup>
import { ref, watch, inject, onMounted } from 'vue'
import { mockData } from '../mockData'
const filters = inject('filters')
const pendingFilters = inject('pendingFilters')
const categories = ['<=20','21~30','31~40','41~50','51~60','>=61','未填寫']
const genders = ['男','女','未填寫']
const chartOptions = ref({
chart: { type: 'bar', height: 400 },
title: { text: null },
xAxis: { categories, title: { text: '年齡' } },
yAxis: { min: 0, title: { text: '人數', align: 'high' } },
legend: { reversed: true },
plotOptions: {
series: {
stacking: 'normal',
cursor: 'pointer',
allowPointSelect: true,
point: {
events: {
click: function () {
// 健壯性保護
if (!pendingFilters || !pendingFilters.gender || !pendingFilters.age) return
const gender = this.series.name
const age = this.category
// 性別多選
const gidx = pendingFilters.gender.indexOf(gender)
if (gidx === -1) pendingFilters.gender.push(gender)
else pendingFilters.gender.splice(gidx, 1)
// 年齡多選
const aidx = pendingFilters.age.indexOf(age)
if (aidx === -1) pendingFilters.age.push(age)
else pendingFilters.age.splice(aidx, 1)
// (進階可加 applyFilters() 立即套用)
}
}
}
}
},
series: [],
credits: { enabled: false }
})
function updateChart() {
const filtered = mockData.filter(item =>
(filters.gender.length === 0 || filters.gender.includes(item.gender)) &&
(filters.age.length === 0 || filters.age.includes(item.age)) &&
(filters.city.length === 0 || filters.city.includes(item.city)) &&
(filters.tendency.length === 0 || filters.tendency.includes(item.tendency)) &&
(filters.power.length === 0 || filters.power.includes(item.power))
)
chartOptions.value.series = genders
.filter(g => filters.gender.length === 0 || filters.gender.includes(g))
.map(gender => ({
name: gender,
data: categories
.filter(a => filters.age.length === 0 || filters.age.includes(a))
.map(age =>
filtered.filter(d => d.gender === gender && d.age === age).length
)
}))
chartOptions.value.xAxis.categories =
categories.filter(a => filters.age.length === 0 || filters.age.includes(a))
}
onMounted(updateChart)
watch(
() => [
filters.gender.slice(),
filters.age.slice(),
filters.city.slice(),
filters.tendency.slice(),
filters.power.slice()
],
updateChart,
{ deep: true }
)
</script>
```
CityPieChart.vue
```vue
<template>
<div>
<h2>註冊地分布</h2>
<highcharts :options="chartOptions" />
</div>
</template>
<script setup>
import { ref, watch, inject, onMounted } from 'vue'
import { mockData } from '../mockData'
const filters = inject('filters')
const pendingFilters = inject('pendingFilters')
const cities = [
'台北市','新北市','基隆市','宜蘭縣','新竹市','新竹縣','桃園市','苗栗縣','台中市',
'嘉義市','雲林縣','台南市','高雄市','未填寫'
]
const chartOptions = ref({
chart: { type: 'pie', height: 320 },
title: { text: null },
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: { enabled: true, format: '{point.name}: {point.y}' },
point: {
events: {
click: function () {
if (!pendingFilters || !pendingFilters.city) return
const city = this.name
const idx = pendingFilters.city.indexOf(city)
if (idx === -1) pendingFilters.city.push(city)
else pendingFilters.city.splice(idx, 1)
}
}
}
}
},
series: [],
credits: { enabled: false }
})
function updateChart() {
const filtered = mockData.filter(item =>
(filters.gender.length === 0 || filters.gender.includes(item.gender)) &&
(filters.age.length === 0 || filters.age.includes(item.age)) &&
(filters.city.length === 0 || filters.city.includes(item.city)) &&
(filters.tendency.length === 0 || filters.tendency.includes(item.tendency)) &&
(filters.power.length === 0 || filters.power.includes(item.power))
)
chartOptions.value.series = [{
name: '人數',
data: cities
.filter(c => filters.city.length === 0 || filters.city.includes(c))
.map(city => ({
name: city,
y: filtered.filter(d => d.city === city).length
}))
}]
}
onMounted(updateChart)
watch(
() => [
filters.gender.slice(),
filters.age.slice(),
filters.city.slice(),
filters.tendency.slice(),
filters.power.slice()
],
updateChart,
{ deep: true }
)
</script>
```
TendencyBar.vue
```vue
<template>
<div>
<h2>消費傾向分布</h2>
<highcharts :options="chartOptions" />
</div>
</template>
<script setup>
import { ref, watch, inject, onMounted } from 'vue'
import { mockData } from '../mockData'
const filters = inject('filters')
const pendingFilters = inject('pendingFilters')
const tendencies = [
'歲末酬賓必來','母親節必來','年中慶必來','周年慶必來','價格敏感客群'
]
const chartOptions = ref({
chart: { type: 'column', height: 320 },
title: { text: null },
xAxis: { categories: tendencies, title: { text: '消費傾向' } },
yAxis: { min: 0, title: { text: '人數', align: 'high' } },
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
click: function () {
if (!pendingFilters || !pendingFilters.tendency) return
const tendency = this.category
const idx = pendingFilters.tendency.indexOf(tendency)
if (idx === -1) pendingFilters.tendency.push(tendency)
else pendingFilters.tendency.splice(idx, 1)
}
}
}
}
},
series: [],
credits: { enabled: false }
})
function updateChart() {
const filtered = mockData.filter(item =>
(filters.gender.length === 0 || filters.gender.includes(item.gender)) &&
(filters.age.length === 0 || filters.age.includes(item.age)) &&
(filters.city.length === 0 || filters.city.includes(item.city)) &&
(filters.tendency.length === 0 || filters.tendency.includes(item.tendency)) &&
(filters.power.length === 0 || filters.power.includes(item.power))
)
chartOptions.value.series = [{
name: '人數',
data: tendencies
.filter(t => filters.tendency.length === 0 || filters.tendency.includes(t))
.map(tendency =>
filtered.filter(d => d.tendency === tendency).length
)
}]
chartOptions.value.xAxis.categories =
tendencies.filter(t => filters.tendency.length === 0 || filters.tendency.includes(t))
}
onMounted(updateChart)
watch(
() => [
filters.gender.slice(),
filters.age.slice(),
filters.city.slice(),
filters.tendency.slice(),
filters.power.slice()
],
updateChart,
{ deep: true }
)
</script>
```
PowerHistogram.vue
```vue
<template>
<div>
<h2>消費力分布</h2>
<highcharts :options="chartOptions" />
</div>
</template>
<script setup>
import { ref, watch, inject, onMounted } from 'vue'
import { mockData } from '../mockData'
const filters = inject('filters')
const pendingFilters = inject('pendingFilters')
const powers = [
'0~1K','1K~2K','2K~5K','5K~10K','10K~50K','50K~100K','100K~1M','1000K~100M+'
]
const chartOptions = ref({
chart: { type: 'column', height: 320 },
title: { text: null },
xAxis: { categories: powers, title: { text: '消費力' } },
yAxis: { min: 0, title: { text: '人數', align: 'high' } },
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
click: function () {
if (!pendingFilters || !pendingFilters.power) return
const power = this.category
const idx = pendingFilters.power.indexOf(power)
if (idx === -1) pendingFilters.power.push(power)
else pendingFilters.power.splice(idx, 1)
}
}
}
}
},
series: [],
credits: { enabled: false }
})
function updateChart() {
const filtered = mockData.filter(item =>
(filters.gender.length === 0 || filters.gender.includes(item.gender)) &&
(filters.age.length === 0 || filters.age.includes(item.age)) &&
(filters.city.length === 0 || filters.city.includes(item.city)) &&
(filters.tendency.length === 0 || filters.tendency.includes(item.tendency)) &&
(filters.power.length === 0 || filters.power.includes(item.power))
)
chartOptions.value.series = [{
name: '人數',
data: powers
.filter(p => filters.power.length === 0 || filters.power.includes(p))
.map(power =>
filtered.filter(d => d.power === power).length
)
}]
chartOptions.value.xAxis.categories =
powers.filter(p => filters.power.length === 0 || filters.power.includes(p))
}
onMounted(updateChart)
watch(
() => [
filters.gender.slice(),
filters.age.slice(),
filters.city.slice(),
filters.tendency.slice(),
filters.power.slice()
],
updateChart,
{ deep: true }
)
</script>
```
---
結果呈現
