[TOC]
# DAY22(2025/07/05)Vue+Highchart
## 進度日誌
- 新增假設資料
- 更改其他檔案
## 筆記區📘 Vue3
### 新增假設資料
由於JSON檔案,結構的問題,無法做圖表聯動,所以需假設聯動資料,模擬所有欄位有交集
src/mockData.js
```js
// src/mockData.js
const genders = ['男', '女', '未填寫']
const ages = ['<=20', '21~30', '31~40', '41~50', '51~60', '>=61', '未填寫']
const cities = ['台北市','新北市','基隆市','宜蘭縣','新竹市','新竹縣','桃園市','苗栗縣','台中市','嘉義市','雲林縣','台南市','高雄市','未填寫']
const tendencies = ['歲末酬賓必來','母親節必來','年中慶必來','周年慶必來','價格敏感客群']
const powers = ['0~1K','1K~2K','2K~5K','5K~10K','10K~50K','50K~100K','100K~1M','1000K~100M+']
// 產生 500 筆假資料
function randomPick(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
export const mockData = Array.from({length: 500}, (_,i) => ({
id: i+1,
gender: randomPick(genders),
age: randomPick(ages),
city: randomPick(cities),
tendency: randomPick(tendencies),
power: randomPick(powers)
}))
```
---
### 更改其他檔案
src/filters.js
```js
import { reactive } from 'vue'
export const pendingFilters = reactive({
gender: [],
age: [],
city: [],
tendency: [],
power: [],
})
export const filters = reactive({
gender: [],
age: [],
city: [],
tendency: [],
power: [],
})
```
App.vue
```html
<script setup>
import { 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'
provide('filters', filters)
provide('pendingFilters', pendingFilters)
const genders = ['男','女','未填寫']
const ages = ['<=20','21~30','31~40','41~50','51~60','>=61','未填寫']
const cities = ['台北市','新北市','基隆市','宜蘭縣','新竹市','新竹縣','桃園市','苗栗縣','台中市','嘉義市','雲林縣','台南市','高雄市','未填寫']
const tendencies = ['歲末酬賓必來','母親節必來','年中慶必來','周年慶必來','價格敏感客群']
const powers = ['0~1K','1K~2K','2K~5K','5K~10K','10K~50K','50K~100K','100K~1M','1000K~100M+']
function toggle(arr, value) {
const idx = arr.indexOf(value)
if (idx >= 0) arr.splice(idx, 1)
else arr.push(value)
}
function applyFilters() {
filters.gender = pendingFilters.gender.slice()
filters.age = pendingFilters.age.slice()
filters.city = pendingFilters.city.slice()
filters.tendency = pendingFilters.tendency.slice()
filters.power = pendingFilters.power.slice()
}
function resetFilters() {
for (let k in pendingFilters) pendingFilters[k] = []
for (let k in filters) filters[k] = []
}
</script>
<template>
<div style="margin-bottom: 24px;">
<div>
<label>性別:</label>
<button v-for="g in genders" :key="g"
:class="pendingFilters.gender.includes(g) ? 'active' : ''"
@click="toggle(pendingFilters.gender, g)">{{ g }}</button>
</div>
<div>
<label>年齡:</label>
<button v-for="a in ages" :key="a"
:class="pendingFilters.age.includes(a) ? 'active' : ''"
@click="toggle(pendingFilters.age, a)">{{ a }}</button>
</div>
<div>
<label>註冊地:</label>
<button v-for="c in cities" :key="c"
:class="pendingFilters.city.includes(c) ? 'active' : ''"
@click="toggle(pendingFilters.city, c)">{{ c }}</button>
</div>
<div>
<label>消費傾向:</label>
<button v-for="t in tendencies" :key="t"
:class="pendingFilters.tendency.includes(t) ? 'active' : ''"
@click="toggle(pendingFilters.tendency, t)">{{ t }}</button>
</div>
<div>
<label>消費力:</label>
<button v-for="p in powers" :key="p"
:class="pendingFilters.power.includes(p) ? 'active' : ''"
@click="toggle(pendingFilters.power, p)">{{ p }}</button>
</div>
<div style="margin-top:8px;">
<button @click="applyFilters">篩選</button>
<button @click="resetFilters">重置</button>
</div>
<div style="margin-top:8px;">
<span v-for="(v,k) in pendingFilters" :key="k">
<span v-for="i in v" :key="i" style="margin-right:4px;">{{ i }} ×</span>
</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;">
<GenderAgeBar />
<CityPieChart />
<TendencyBar />
<PowerHistogram />
</div>
</template>
<style>
button.active {
background: #4fc3f7;
color: #fff;
border-radius: 3px;
}
button {
margin-right: 6px;
margin-bottom: 4px;
padding: 4px 10px;
border: 1px solid #ccc;
background: #f4f4f4;
cursor: pointer;
}
</style>
```
CityPieChart.vue
```html
<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 cities = ['台北市','新北市','基隆市','宜蘭縣','新竹市','新竹縣','桃園市','苗栗縣','台中市','嘉義市','雲林縣','台南市','高雄市','未填寫']
const chartOptions = ref({
chart: { type: 'pie', height: 400 },
title: { text: null },
series: []
})
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>
```
GenderAgeBar.vue
```html
<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 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' } },
series: []
})
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>
```
PowerHistogram.vue
```html
<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 powers = ['0~1K','1K~2K','2K~5K','5K~10K','10K~50K','50K~100K','100K~1M','1000K~100M+']
const chartOptions = ref({
chart: { type: 'column', height: 400 },
title: { text: null },
xAxis: { categories: powers, title: { text: '消費力' } },
series: []
})
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>
```
TendencyBar.vue
```html
<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 tendencies = ['歲末酬賓必來','母親節必來','年中慶必來','周年慶必來','價格敏感客群']
const chartOptions = ref({
chart: { type: 'bar', height: 400 },
title: { text: null },
xAxis: { categories: tendencies, title: { text: '傾向' } },
series: []
})
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>
```
---
目前結果
