1.main.ts 引入 ElementPlus 暗黑主题样式
import 'element-plus/theme-chalk/dark/css-vars.css'
2.绑定主题切换按钮
<el-switch v-model="isDark" :active-icon="Moon" :inactive-icon="Sunny" inline-prompt @change="toggleDark" />

3.主题切换
3.1 亮色主题

3.2 暗黑主题

4.解决暗色主题图表中的字体没有跟随主题切换
4.1 定义全局样式文件 style.less
// 定义全局样式
:root {// 暗黑主题--text-color-dark: white; /* 字体颜色 */// --bg-color-dark: rgb(224, 18, 18); /* 背景颜色 */// 亮色主题--text-color-light: black; /* 字体颜色 */// --bg-color-light: rgb(231, 31, 31); /* 背景颜色 */
}body {margin: 0;padding: 0;transition: background-color 0.3s ease; /* 平滑过渡效果 */
}router-link,a {text-decoration: none; /* 无下划线 */
}/* 暗黑主题,设置样式 */
body.dark {color: var(--text-color-dark); /* 字体颜色 */// background-color: var(--bg-color-dark); /* 背景颜色 */
}/* 亮色主题,设置样式 */
body.light {color: var(--text-color-light); /* 字体颜色 */// background-color: var(--bg-color-light); /* 背景颜色 */
}
4.2 main.ts 引入 style.less
// 导入公共样式
import '@/common/style.less'
4.3 定义全局主题样式设置的 global.ts
import { useDark } from '@vueuse/core';// 使用 useDark 钩子来检测当前是否处于暗黑模式
let isDark = useDark();// 全局 ts
export default{IS_DARK:isDark,// 获取全局变量颜色值:isDarkFlag 默认是暗黑主题,dark 暗色主题样式(默认设置暗色主题字体颜色),light 亮色主题样式(默认设置亮色主题字体颜色)setThemeStyle(isDarkFlag:boolean=isDark.value, dark:string='--text-color-dark', light:string='--text-color-light'){return isDarkFlag? getComputedStyle(document.documentElement).getPropertyValue(dark): getComputedStyle(document.documentElement).getPropertyValue(light);}
}
4.4 使用 watch 监听主题变化,从而改变图表中的字体样式(以月销售额图表为例)
4.4.1 导入 global.ts

4.4.2 设置图表配置项,使用全局样式的字体颜色

4.4.3 使用 watch 监听 global.IS_DARK.value 变化,更新 ECharts 的配置

4.5 主题切换,图表字体样式也切换
4.5.1 亮色主题

4.5.2 暗黑主题

4.6 前端代码(以上代码截图中的行号对应这里的行号)
<template><el-card class="container"><template #header><div class="header"><el-breadcrumb :separator-icon="ArrowRight"><el-breadcrumb-item :to="{ path: '/home/index' }" class="title">首页</el-breadcrumb-item><el-breadcrumb-item class="title">产品管理</el-breadcrumb-item><el-breadcrumb-item class="title">产品统计</el-breadcrumb-item></el-breadcrumb></div></template><div class="top"><div class="date-picker"><el-date-pickerv-model="date"type="year"format="YYYY"value-format="YYYY-MM-DD"@change="draw"/></div><div class="statistics"><el-form inline><el-form-item label="年销售额(+)"><el-input v-model="sale" :type="saleVisible ? 'text' : 'password'" disabled ><template #append><el-button :icon="saleVisible ? Hide : View" @click="showSale" /></template></el-input></el-form-item><el-form-item label="年成本(-)"><el-input v-model="cost" :type="costVisible ? 'text' : 'password'" disabled><template #append><el-button :icon="costVisible ? Hide : View" @click="showCost" /></template></el-input></el-form-item><el-form-item label="年销售退货额(-)"><el-input v-model="saleReturns" :type="saleReturnsVisible ? 'text' : 'password'" disabled><template #append><el-button :icon="saleReturnsVisible ? Hide : View" @click="showSaleReturns" /></template></el-input></el-form-item><el-form-item label="年采购退货额(+)"><el-input v-model="purchaseReturns" :type="purchaseReturnsVisible ? 'text' : 'password'" disabled><template #append><el-button :icon="purchaseReturnsVisible ? Hide : View" @click="showPurchaseReturns" /></template></el-input></el-form-item><el-form-item label="年利润"><el-input v-model="profit" :type="profitVisible ? 'text' : 'password'" disabled><template #append><el-button :icon="profitVisible ? Hide : View" @click="showProfit" /></template></el-input></el-form-item></el-form></div></div><el-scrollbar height="670px"><div class="mycharts"><!-- 1、各产品月销量 --><div id="saleVolume" class="mychart"></div><!-- 2、月销售总额 --><div id="saleRevenue" class="mychart"></div></div><div class="mycharts"><!-- 3、库存 --><div id="stock" class="mychart"></div><!-- 4、月销售退货量 --><div id="returns" class="mychart"></div></div></el-scrollbar></el-card>
</template><script setup lang="ts">import ordersApi from '@/api/product/orders';import productApi from '@/api/product/product';import { onMounted, reactive, ref,watch } from 'vue'import { ArrowRight,View,Hide } from '@element-plus/icons-vue'import * as echarts from 'echarts';import returnsApi from '@/api/product/returns';import global from '@/common/global'const now = new Date(); // 当前日期const year = now.getFullYear(); // 获取当前年份let date=ref(`${year}-01-01`)// 1:年成本(采购总额+销售邮费+退货邮费),2:年销售额,3:年利润let maps=reactive(new Map()) as any;// 1:年采购退货总额,2:年销售退货总额,3:年退货邮费let returnsMap=reactive(new Map()) as any;// 年成本(-):采购总额+销售邮费+退货邮费const cost=ref(0.00)// 年销售额(+)const sale=ref(0.00)// 年利润const profit=ref(0.0)// 年销售退货额(-)const saleReturns=ref(0.00)// 年采购退货额(+)const purchaseReturns=ref(0.00)const costVisible=ref(false)const saleVisible=ref(false)const profitVisible=ref(false)const saleReturnsVisible=ref(false)const purchaseReturnsVisible=ref(false)const showCost= ()=>{costVisible.value = !costVisible.value;}const showSale= ()=>{saleVisible.value = !saleVisible.value;}const showProfit= ()=>{profitVisible.value = !profitVisible.value;}const showSaleReturns= ()=>{saleReturnsVisible.value = !saleReturnsVisible.value;}const showPurchaseReturns= ()=>{purchaseReturnsVisible.value = !purchaseReturnsVisible.value;}const init= ()=>{cost.value=maps.get("1");sale.value=maps.get("2");saleReturns.value=returnsMap.get("2");purchaseReturns.value=returnsMap.get("1");profit.value=(sale.value+purchaseReturns.value)-(cost.value+saleReturns.value);}// 1:年成本(采购总额+销售邮费+退货邮费),2:年销售额,3:年利润const getData= async()=>{const response = await ordersApi.yearStatistics(date.value);Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {// 将字符串键转换回数字// const Key = Number(key);maps.set(key, value);}// 成本增加退货邮费maps.set("1",maps.get("1")+returnsMap.get("3"));init();}// 1:年采购退货总额,2:年销售退货总额,3:年退货邮费const getReturnsData= async()=>{const response = await returnsApi.yearStatistics(date.value);Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {// 将字符串键转换回数字// const Key = Number(key);returnsMap.set(key, value);}getData();}let saleVolumeOption=reactive({}) as any;// 1、各产品月销量 折线图const drawSaleVolume= async()=>{// 配置项saleVolumeOption=reactive({title: {text: '月销量',top: 5,// 设置标题文字样式textStyle: {color: global.setThemeStyle()}},// 设置图例legend:{data: [],top: 10,// 设置图例文字样式textStyle: {color: global.setThemeStyle()}},tooltip: {trigger:"axis", // 坐标轴触发},xAxis: {type: 'category',data: [],axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},yAxis: {// name: '各产品月销量',type: 'value',axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},// 选中高亮emphasis:{focus:"series"},series: []});// xAxisData 与后端返回的数据适配const xAxisData=reactive(['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']) as any// xData 用于前端展示const xData=reactive(['1月', '2月', '3月', '4月','5月', '6月', '7月', '8月','9月', '10月', '11月', '12月']) as any// series数据const seriesData = reactive([]) as any;// 图例let legendData = reactive([]) as any;// 后端返回的map数据let map=reactive(new Map()) as any;// 初始化 Echarts 实例const myEchart=echarts.init(document.getElementById("saleVolume"));// 折线图 填充x轴数据saleVolumeOption.xAxis.data=xData;// 将后端数据转换为y轴适配的数据(map->list),定义了一个匿名的异步函数,并立即执行(async() => {const response = await ordersApi.saleStatistics(date.value);Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {map.set(key, value);}return map;})().then(async()=>{// map.forEach(async(value:any, key:any) => {// seriesData.push({// name: key,// type: 'line',// // 然后,为这个series填充data,data应该是对应月份的数值// data: xAxisData.map((month:any) => value[month] || 0)// });// keys.push(key);// })// // 填充 折线图 y轴数据// option.series=seriesData;// // option.legend.data=legendData;// 创建一个Promise数组,用于等待所有产品名称的获取const promises = Array.from(map.entries()).map(async ([key, value] : any) => {const res = await productApi.getNameById(key);seriesData.push({name: res.data,type: 'line',smooth: true,data: xAxisData.map((month:any) => value[month]),});// 同时添加到图例数据legendData.push(res.data);});// 等待所有Promise完成await Promise.all(promises);// 更新图表配置saleVolumeOption.series = seriesData;saleVolumeOption.legend.data = legendData;// 绘制图表myEchart.setOption(saleVolumeOption);});}let saleRevenueOption=reactive({}) as any// 2、月销售总额 折线图const drawSaleRevenue= async()=>{// 配置项saleRevenueOption=reactive({title: {text: '月销售额',top: 5,// 设置文字样式textStyle: {color: global.setThemeStyle()}},// 设置图例legend:{data: [],top: 10,// 设置文字样式textStyle: {color: global.setThemeStyle()}},tooltip: {trigger:"axis", // 坐标轴触发},xAxis: {type: 'category',data: [],axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},yAxis: {// name: '月销售总额',type: 'value',axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},// 选中高亮emphasis:{focus:"series"},series: []});// xAxisData 与后端返回的数据适配const xAxisData=reactive(['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']) as any// xData 用于前端展示const xData=reactive(['1月', '2月', '3月', '4月','5月', '6月', '7月', '8月','9月', '10月', '11月', '12月']) as any// series数据let seriesData = reactive([]) as any;// 图例let legendData = reactive([]) as any;// 后端返回的map数据let map=reactive(new Map()) as any;// 初始化 Echarts 实例const myEchart=echarts.init(document.getElementById("saleRevenue"));// 折线图 填充x轴数据saleRevenueOption.xAxis.data=xData;// 将后端数据转换为y轴适配的数据(map->list),定义了一个匿名的异步函数,并立即执行(async() => {const response = await ordersApi.totalStatistics(date.value);Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {map.set(key, value);}return map;})().then(()=>{seriesData.push({name: '月销售额',type: 'line',smooth: true,data: xAxisData.map((month:any) => map.get(month)),label:{show:true,position:'top',formatter:function(data:any){return data.value === 0 ? '' : data.value},// 设置图例文字样式textStyle: {color: global.setThemeStyle()}}});// 设置图例数据legendData.push('月销售额');// 更新图表配置saleRevenueOption.series = seriesData;saleRevenueOption.legend.data = legendData;// 绘制图表myEchart.setOption(saleRevenueOption);});}let stockOption=reactive({}) as any// 3、库存 饼图const drawStock= async()=>{// 配置项stockOption=reactive({title: {text: '库存',top: 5,// 设置文字样式textStyle: {color: global.setThemeStyle()}},// 设置图例legend:{data: [],top: 40,left:"left",orient:"vertical", // 竖直排列// 设置文字样式textStyle: {color: global.setThemeStyle()}},tooltip: {},// 选中高亮emphasis:{focus:"series"},series: []});// series数据let seriesData = reactive([]) as any;// 图例let legendData = reactive([]) as any;// 后端返回的map数据let map=reactive(new Map()) as any;// 初始化 Echarts 实例const myEchart=echarts.init(document.getElementById("stock"));// 将后端数据转换为y轴适配的数据(map->list),定义了一个匿名的异步函数,并立即执行(async() => {const response = await productApi.stockStatistics();Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {map.set(key, value);}return map;})().then(()=>{seriesData.push({// name: '库存',type: 'pie',data: [],radius: '70%',label:{show:true,formatter: `{b}:{c}`,position: "outside", //outside 外部显示 inside 内部显示// 设置文字样式textStyle: {color: global.setThemeStyle()}}});map.forEach((value:any, key:any) => {seriesData[0].data.push({ name: key, value: value });// 设置图例legendData.push(key);});// 更新图表配置stockOption.series = seriesData;stockOption.legend.data = legendData;// 绘制图表myEchart.setOption(stockOption);});}let returnsOption=reactive({}) as any// 4、各产品月销售退货量 折线图const drawReturns= async()=>{// 配置项returnsOption=reactive({title: {text: '月销售退货量',top: 5,// 设置文字样式textStyle: {color: global.setThemeStyle()}},// 设置图例legend:{data: [],top: 10,// 设置文字样式textStyle: {color: global.setThemeStyle()}},tooltip: {trigger:"axis", // 坐标轴触发},xAxis: {type: 'category',data: [],axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},yAxis: {// name: '各产品月销量',type: 'value',axisLabel: {// 设置坐标轴字体颜色color: global.setThemeStyle()}},// 选中高亮emphasis:{focus:"series"},series: []});// xAxisData 与后端返回的数据适配const xAxisData=reactive(['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']) as any// xData 用于前端展示const xData=reactive(['1月', '2月', '3月', '4月','5月', '6月', '7月', '8月','9月', '10月', '11月', '12月']) as any// series数据const seriesData = reactive([]) as any;// 图例let legendData = reactive([]) as any;// 后端返回的map数据let map=reactive(new Map()) as any;// 初始化 Echarts 实例const myEchart=echarts.init(document.getElementById("returns"));// 折线图 填充x轴数据returnsOption.xAxis.data=xData;// 将后端数据转换为y轴适配的数据(map->list),定义了一个匿名的异步函数,并立即执行(async() => {const response = await returnsApi.statistics(date.value);Object.entries()函数时,它会将对象的键转换为字符串类型for (const [key, value] of Object.entries(response.data)) {map.set(key, value);}return map;})().then(async()=>{// 创建一个Promise数组,用于等待所有产品名称的获取const promises = Array.from(map.entries()).map(async ([key, value] : any) => {const res = await productApi.getNameById(key);seriesData.push({name: res.data,type: 'line',smooth: true,data: xAxisData.map((month:any) => value[month]),});// 同时添加到图例数据legendData.push(res.data);});// 等待所有Promise完成await Promise.all(promises);// 更新图表配置returnsOption.series = seriesData;returnsOption.legend.data = legendData;// 绘制图表myEchart.setOption(returnsOption);});}// 监听 global.IS_DARK.value 变化,更新 ECharts 的配置watch(()=>global.IS_DARK.value, (newValue:boolean) => {draw();});const draw= ()=>{drawSaleVolume();drawSaleRevenue();drawStock();drawReturns();getReturnsData();}onMounted(()=>{draw();})</script><style scoped lang="less">.container{height: 100%;box-sizing: border-box; }.header{display: flex;align-items: center;justify-content: space-between;}.title{font-size: large;font-weight: 600;}.mycharts{display: flex;}/* 要设置宽高,否则无法显示 */.mychart{width: 100%;height: 320px;border: 1px solid pink;margin: 5px;}.date-picker{margin-left: 5px;margin-bottom: 5px;margin-right: 20px;}.statistics .el-form .el-form-item .el-input{width: 150px;}.top{display: flex;justify-content: space-between;height: 40px;}</style>