[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> ``` --- 目前結果 ![image](https://hackmd.io/_uploads/SyWtZFFHeg.png)