123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233 |
- <script setup lang="ts">
- import chroma from 'chroma-js'
- import * as echarts from 'echarts'
- import { NNumberAnimation } from 'naive-ui'
- import { onMounted, watch, ref, computed, onUnmounted, nextTick } from 'vue'
- import { ContentWrapper } from '@/components'
- import { usePersonalization } from '@/composables'
- import { usePreferencesStore } from '@/stores'
- import twc from '@/utils/tailwindColor'
- import type { ECharts } from 'echarts'
- defineOptions({
- name: 'Dashboard',
- })
- const { isDark, color } = usePersonalization()
- const preferencesStore = usePreferencesStore()
- const cardList = ref(generateCardData())
- const revenueChart = ref<HTMLDivElement | null>(null)
- let revenueChartInstance: ECharts | null = null
- let revenueChartResizeHandler: (() => void) | null = null
- const revenueBarChart = ref<HTMLDivElement | null>(null)
- let revenueBarChartInstance: ECharts | null = null
- let revenueBarChartResizeHandler: (() => void) | null = null
- const revenueBarChart2 = ref<HTMLDivElement | null>(null)
- let revenueBarChart2Instance: ECharts | null = null
- let revenueBarChart2ResizeHandler: (() => void) | null = null
- const monthlyRadarChart = ref<HTMLDivElement | null>(null)
- let monthlyRadarChartInstance: ECharts | null = null
- let monthlyRadarChartResizeHandler: (() => void) | null = null
- const highestRevenueChart = ref<HTMLDivElement | null>(null)
- let highestRevenueChartInstance: ECharts | null = null
- let highestRevenueChartResizeHandler: (() => void) | null = null
- let collapseResizeTimeout: ReturnType<typeof setTimeout> | null = null
- const CHART_CONFIG = {
- MONTHS: Array.from({ length: 12 }, (_, i) => `${i + 1}月`),
- }
- const getBusinessLinesConfig = () => [
- {
- name: '主要收入',
- color: color.value,
- dataRange: { min: 30000, max: 85000 },
- },
- {
- name: '核心业务',
- color: twc.cyan[500],
- dataRange: { min: 25000, max: 75000 },
- },
- {
- name: '辅助收入',
- color: twc.lime[500],
- dataRange: { min: 15000, max: 50000 },
- },
- {
- name: '订阅收入',
- color: twc.orange[500],
- dataRange: { min: 10000, max: 35000 },
- },
- {
- name: '广告收入',
- color: twc.pink[500],
- dataRange: { min: 8000, max: 25000 },
- },
- ]
- const barChartSelectedLegend = ref(getBusinessLinesConfig()[0].name)
- const revenueChartSelected = ref<Record<string, boolean>>(
- Object.fromEntries(getBusinessLinesConfig().map((line) => [line.name, true])),
- )
- const highestChartSelected = ref<'max' | 'min'>('max')
- function generateCardData() {
- const now = new Date()
- const currentMonth = now.getMonth() + 1
- const baseUserCount = 10000 + Math.floor(Math.random() * 5000)
- const todayVisits = Math.floor(
- 5000 + Math.random() * 8000 + Math.sin((now.getHours() / 24) * Math.PI) * 2000,
- )
- const monthlySales = Math.floor(1500000 + Math.random() * 1000000 + (currentMonth / 12) * 500000)
- const pendingOrders = Math.floor(150 + Math.random() * 200)
- return [
- {
- title: '用户总数',
- value: baseUserCount,
- percentage: parseFloat((3.2 + Math.random() * 4).toFixed(2)),
- iconClass: 'iconify ph--users-bold text-indigo-50 dark:text-indigo-150',
- iconBgClass:
- 'text-indigo-500/5 bg-indigo-400 ring-4 ring-indigo-200 dark:bg-indigo-650 dark:ring-indigo-500/30 transition-all',
- precision: 0,
- description: `${currentMonth}月新增 ${Math.floor(100 + Math.random() * 200)} 人`,
- },
- {
- title: '今日访问',
- value: todayVisits,
- percentage: parseFloat((-2 + Math.random() * 20).toFixed(2)),
- iconClass: 'iconify ph--eye-bold text-blue-50 dark:text-blue-150',
- iconBgClass:
- 'text-blue-500/5 bg-blue-400 ring-4 ring-blue-200 dark:bg-blue-650 dark:ring-blue-500/30 transition-all',
- precision: 0,
- description: '较昨日变化',
- },
- {
- title: `${currentMonth}月销售额`,
- value: monthlySales,
- percentage: parseFloat((5 + Math.random() * 10).toFixed(2)),
- iconClass: 'iconify ph--currency-dollar-bold text-emerald-50 dark:text-emerald-150',
- iconBgClass:
- 'text-emerald-500/5 bg-emerald-400 ring-4 ring-emerald-200 dark:bg-emerald-650 dark:ring-emerald-500/30 transition-all',
- precision: 2,
- description: '本月累计收入',
- },
- {
- title: '待处理订单',
- value: pendingOrders,
- percentage: parseFloat((-8 + Math.random() * 6).toFixed(2)),
- iconClass: 'iconify ph--shopping-cart-bold text-orange-50 dark:text-orange-150',
- iconBgClass:
- 'text-orange-500/5 bg-orange-400 ring-4 ring-orange-200 dark:bg-orange-650 dark:ring-orange-500/30 transition-all',
- precision: 0,
- description: '需要及时处理',
- },
- ]
- }
- const generateRandomData = (
- baseMin: number,
- baseMax: number,
- trend: 'up' | 'down' | 'stable' = 'stable',
- ) => {
- const data = []
- let currentBase = (baseMin + baseMax) / 2
- for (let i = 0; i < 12; i++) {
- if (trend === 'up') {
- currentBase += (baseMax - baseMin) * 0.03
- } else if (trend === 'down') {
- currentBase -= (baseMax - baseMin) * 0.02
- }
- const baseVariation = (baseMax - baseMin) * 0.6 * (Math.random() - 0.5)
- const seasonalFactor = Math.sin((i * Math.PI) / 6) * (baseMax - baseMin) * 0.15
- const randomSpike = Math.random() > 0.8 ? (baseMax - baseMin) * 0.2 * (Math.random() - 0.5) : 0
- const variation = baseVariation + seasonalFactor + randomSpike
- const value = Math.max(baseMin * 0.3, Math.min(90000, currentBase + variation))
- data.push(Math.round(value))
- }
- return data
- }
- const businessLinesWithData = computed(() =>
- getBusinessLinesConfig().map((line) => ({
- ...line,
- data: generateRandomData(line.dataRange.min, line.dataRange.max, 'up'),
- })),
- )
- const createTooltipConfig = (formatter?: any) => ({
- trigger: 'axis',
- backgroundColor: isDark.value ? twc.neutral[750] : '#fff',
- borderWidth: 0,
- padding: 8,
- extraCssText: 'box-shadow: none;',
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- axisPointer: {
- type: 'none',
- },
- ...(formatter && { formatter }),
- })
- const chartDataManager = {
- getMonths: () => CHART_CONFIG.MONTHS,
- getAllLines: () => businessLinesWithData.value,
- getLineByName: (name: string) => {
- return businessLinesWithData.value.find((line) => line.name === name)
- },
- getAllNames: () => {
- return businessLinesWithData.value.map((line) => line.name)
- },
- getAllColors: () => {
- return businessLinesWithData.value.map((line) => line.color)
- },
- getHighestRevenueLine: () => {
- return businessLinesWithData.value.reduce((prev, current) => {
- const prevTotal = prev.data.reduce((sum, value) => sum + value, 0)
- const currentTotal = current.data.reduce((sum, value) => sum + value, 0)
- return prevTotal > currentTotal ? prev : current
- })
- },
- getLowestRevenueLine: () => {
- return businessLinesWithData.value.reduce((prev, current) => {
- const prevTotal = prev.data.reduce((sum, value) => sum + value, 0)
- const currentTotal = current.data.reduce((sum, value) => sum + value, 0)
- return prevTotal < currentTotal ? prev : current
- })
- },
- getCurrentMonthData: (month: number) => {
- return businessLinesWithData.value.map((line) => ({
- name: line.name,
- value: line.data[month],
- color: line.color,
- }))
- },
- }
- function initRevenueChart() {
- if (!revenueChart.value) return
- const chart = echarts.init(revenueChart.value)
- const option = {
- title: [
- {
- text: '收入概览',
- left: 0,
- top: 0,
- textStyle: {
- fontSize: 18,
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontWeight: 'normal',
- },
- },
- {
- text: '总收入趋势与增长分析',
- left: 0,
- top: 28,
- textStyle: {
- fontSize: 14,
- color: isDark.value ? twc.neutral[500] : twc.neutral[450],
- fontWeight: 'normal',
- },
- },
- ],
- tooltip: createTooltipConfig((params: any) => {
- const date = params[0].axisValue
- let result = `<div>${date}数据</div>`
- params.forEach((item: any) => {
- const value = item.value.toLocaleString()
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:4px; width:10px; height:10px; border-radius:50%; background-color:${item.color};"></span>
- <span style="margin-right: 10px">${item.seriesName}</span>
- <span>${value}</span>
- </div>
- `
- })
- return result
- }),
- legend: {
- top: 6,
- right: 22,
- itemGap: 16,
- icon: 'circle',
- itemWidth: 12,
- itemHeight: 12,
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 14,
- },
- data: chartDataManager.getAllNames(),
- selected: revenueChartSelected.value,
- },
- grid: {
- left: 20,
- right: 20,
- top: 72,
- bottom: 0,
- containLabel: true,
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: chartDataManager.getMonths(),
- axisLine: { show: false },
- axisTick: { show: false },
- axisLabel: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- splitLine: { show: false },
- },
- yAxis: {
- type: 'value',
- min: 0,
- max: 100000,
- interval: 10000,
- axisLine: { show: false },
- axisTick: { show: false },
- axisLabel: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 11,
- formatter(value: number) {
- if (value === 0) return '0'
- return `${(value / 1000).toFixed(0)},000`
- },
- },
- splitLine: {
- show: true,
- lineStyle: {
- color: isDark.value ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)',
- width: 1,
- },
- },
- },
- series: businessLinesWithData.value.map((line, idx) => ({
- name: line.name,
- type: 'line',
- data: line.data,
- symbol: 'circle',
- showSymbol: false,
- smooth: true,
- lineStyle: {
- color: line.color,
- width: idx < 2 ? 2 : 1,
- type: idx < 2 ? 'solid' : 'dashed',
- },
- itemStyle: {
- color: line.color,
- },
- emphasis: {
- focus: 'series',
- symbolSize: 6,
- itemStyle: {
- color: line.color,
- borderColor: line.color,
- borderWidth: 2,
- },
- },
- areaStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: chroma(line.color).alpha(0.12).hex() },
- { offset: 1, color: chroma(line.color).alpha(0.02).hex() },
- ],
- },
- },
- })),
- animationDuration: 1000,
- animationEasing: 'cubicOut' as const,
- animationDelay(idx: number) {
- return idx * 100
- },
- }
- chart.setOption(option)
- chart.on('legendselectchanged', (params: any) => {
- if (params?.selected) {
- revenueChartSelected.value = params.selected
- }
- })
- revenueChartInstance = chart
- revenueChartResizeHandler = () => chart.resize()
- window.addEventListener('resize', revenueChartResizeHandler, { passive: true })
- }
- function initRevenueBarChart() {
- if (!revenueBarChart.value) return
- const chart = echarts.init(revenueBarChart.value)
- const option = {
- color: chartDataManager.getAllColors(),
- grid: { left: 5, right: 5, top: 60, bottom: 0, containLabel: false },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: chartDataManager.getMonths(),
- show: false,
- },
- yAxis: {
- type: 'value',
- min: 0,
- show: false,
- },
- legend: {
- top: 6,
- itemWidth: 10,
- itemHeight: 10,
- itemGap: 16,
- inactiveColor: isDark.value ? twc.neutral[400] : twc.neutral[350],
- inactiveBorderColor: isDark.value ? twc.neutral[400] : twc.neutral[350],
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 13,
- },
- selectedMode: 'single',
- selected: Object.fromEntries(
- businessLinesWithData.value.map((line) => [
- line.name,
- line.name === barChartSelectedLegend.value,
- ]),
- ),
- data: businessLinesWithData.value.map((line) => ({
- name: line.name,
- icon: 'circle',
- itemStyle: {
- borderColor: line.color,
- },
- })),
- },
- series: businessLinesWithData.value.map((line) => ({
- name: line.name,
- type: 'line',
- stack: 'Total',
- data: line.data,
- symbol: 'circle',
- symbolSize: 6,
- showSymbol: true,
- lineStyle: {
- width: 2,
- },
- itemStyle: {
- borderColor: isDark.value ? twc.neutral[800] : twc.neutral[50],
- borderWidth: 2,
- },
- emphasis: {
- focus: 'series',
- symbolSize: 6,
- itemStyle: {
- color: line.color,
- borderColor: line.color,
- borderWidth: 2,
- },
- },
- areaStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: chroma(line.color).alpha(0.12).hex() },
- { offset: 1, color: chroma(line.color).alpha(0.02).hex() },
- ],
- },
- },
- })),
- tooltip: createTooltipConfig((params: any) => {
- const color = params[0].color
- const date = params[0].axisValue
- const value = params[0].value.toLocaleString()
- let result = `<div>${date}数据</div>`
- params.forEach((item: any) => {
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:4px; width:10px; height:10px; border-radius:50%; background-color:${color};"></span>
- <span style="margin-right: 10px">${item.seriesName}</span>
- <span>${value}</span>
- </div>
- `
- })
- return result
- }),
- animationDuration: 1000,
- animationEasing: 'cubicOut' as const,
- animationDelay(idx: number) {
- return idx * 100
- },
- }
- chart.setOption(option)
- revenueBarChartInstance = chart
- revenueBarChartResizeHandler = () => chart.resize()
- window.addEventListener('resize', revenueBarChartResizeHandler, { passive: true })
- chart.on('legendselectchanged', (params: any) => {
- barChartSelectedLegend.value = params.name
- if (revenueBarChart2Instance) {
- const selectedLine = chartDataManager.getLineByName(params.name)
- if (!selectedLine) return
- revenueBarChart2Instance.setOption({
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'none',
- },
- backgroundColor: isDark.value ? twc.neutral[750] : '#fff',
- borderWidth: 0,
- padding: 8,
- extraCssText: 'box-shadow: none;',
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- formatter(params: any) {
- const date = params[0].axisValue
- const seriesName = params[0].seriesName
- const value = params[0].value.toLocaleString()
- let result = `<div>${date}数据</div>`
- params.forEach(() => {
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:4px; width:10px; height:10px; border-radius:50%; background-color:${selectedLine.color};"></span>
- <span style="margin-right: 10px">${seriesName}</span>
- <span>${value}</span>
- </div>
- `
- })
- return result
- },
- },
- series: [
- {
- name: params.name,
- type: 'bar',
- barWidth: '60%',
- data: selectedLine.data,
- itemStyle: {
- color: chroma(selectedLine.color).alpha(0.15).hex(),
- borderWidth: 0,
- borderRadius: [3, 3, 0, 0],
- },
- emphasis: {
- itemStyle: {
- color: chroma(selectedLine.color).alpha(0.3).hex(),
- borderWidth: 0,
- },
- },
- },
- ],
- })
- }
- })
- }
- function initRevenueBarChart2() {
- if (!revenueBarChart2.value) return
- const chart = echarts.init(revenueBarChart2.value)
- const selectedLine = chartDataManager.getLineByName(barChartSelectedLegend.value)
- if (!selectedLine) return
- const option = {
- color: chartDataManager.getAllColors(),
- grid: { left: 0, right: 0, top: 0, bottom: 0, containLabel: false },
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'none',
- },
- backgroundColor: isDark.value ? twc.neutral[750] : '#fff',
- borderWidth: 0,
- padding: 8,
- extraCssText: 'box-shadow: none;',
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- formatter(params: any) {
- const date = params[0].axisValue
- const value = params[0].value.toLocaleString()
- const seriesName = params[0].seriesName
- let result = `<div>${date}数据</div>`
- params.forEach(() => {
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:4px; width:10px; height:10px; border-radius:50%; background-color:${selectedLine.color};"></span>
- <span style="margin-right: 10px">${seriesName}</span>
- <span>${value}</span>
- </div>
- `
- })
- return result
- },
- },
- xAxis: {
- type: 'category',
- data: chartDataManager.getMonths(),
- show: false,
- },
- yAxis: {
- type: 'value',
- show: false,
- },
- series: [
- {
- name: barChartSelectedLegend.value,
- type: 'bar',
- barWidth: '60%',
- data: selectedLine.data,
- itemStyle: {
- color: chroma(selectedLine.color).alpha(0.15).hex(),
- borderWidth: 0,
- borderRadius: [3, 3, 0, 0],
- },
- emphasis: {
- itemStyle: {
- color: chroma(selectedLine.color).alpha(0.3).hex(),
- borderWidth: 0,
- },
- },
- },
- ],
- animationDuration: 1000,
- animationEasing: 'cubicOut' as const,
- animationDelay(idx: number) {
- return idx * 50
- },
- }
- chart.setOption(option)
- revenueBarChart2Instance = chart
- revenueBarChart2ResizeHandler = () => chart.resize()
- window.addEventListener('resize', revenueBarChart2ResizeHandler, { passive: true })
- }
- function initMonthlyRadarChart() {
- if (!monthlyRadarChart.value) return
- const now = new Date()
- const currentMonth = now.getMonth()
- const currentMonthData = chartDataManager.getCurrentMonthData(currentMonth)
- const chart = echarts.init(monthlyRadarChart.value)
- const option = {
- title: [
- {
- text: '当月各业务收入',
- left: 0,
- top: 0,
- textStyle: {
- fontSize: 15,
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontWeight: 'normal',
- },
- },
- {
- text: `${currentMonth + 1}月数据对比`,
- left: 0,
- top: 28,
- textStyle: {
- fontSize: 14,
- color: isDark.value ? twc.neutral[500] : twc.neutral[450],
- fontWeight: 'normal',
- },
- },
- ],
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'none',
- },
- backgroundColor: isDark.value ? twc.neutral[750] : '#fff',
- borderWidth: 0,
- padding: 8,
- extraCssText: 'box-shadow: none;',
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- formatter(params: any) {
- const color = params[0].color.slice(0, -2)
- const date = params[0].axisValue
- const value = params[0].data.value
- let result = `<div>${date}</div>`
- params.forEach(() => {
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:8px; width:10px; height:10px; border-radius:50%; background-color:${color};"></span>
- <span>${value}</span>
- </div>
- `
- })
- return result
- },
- },
- grid: {
- left: 10,
- right: 5,
- top: 100,
- bottom: -10,
- containLabel: true,
- },
- xAxis: {
- type: 'category',
- show: false,
- data: chartDataManager.getAllNames(),
- },
- yAxis: {
- type: 'value',
- axisLine: {
- show: false,
- },
- axisTick: {
- show: false,
- },
- splitLine: {
- lineStyle: {
- color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
- type: 'dashed',
- },
- },
- axisLabel: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- formatter(value: number) {
- if (value >= 1000) {
- return `${(value / 1000).toFixed(0)}k`
- }
- return value
- },
- },
- },
- series: [
- {
- type: 'bar',
- barWidth: '50%',
- data: currentMonthData.map((item) => ({
- value: item.value,
- itemStyle: {
- color: chroma(item.color).alpha(0.35).hex(),
- borderRadius: [3, 3, 0, 0],
- },
- })),
- label: {
- show: true,
- position: 'top',
- distance: 10,
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- formatter(params: any) {
- const index = params.dataIndex
- return `${chartDataManager.getAllNames()[index]}`
- },
- },
- },
- ],
- animationDuration: 1000,
- animationEasing: 'cubicOut' as const,
- }
- chart.setOption(option)
- monthlyRadarChartInstance = chart
- monthlyRadarChartResizeHandler = () => chart.resize()
- window.addEventListener('resize', monthlyRadarChartResizeHandler, { passive: true })
- }
- function initHighestRevenueChart() {
- if (!highestRevenueChart.value) return
- const highestLine = chartDataManager.getHighestRevenueLine()
- const lowestLine = chartDataManager.getLowestRevenueLine()
- const chartData = [
- {
- legendName: '最高',
- legendValue: 'max',
- businessName: highestLine.name,
- color: highestLine.color,
- data: highestLine.data,
- },
- {
- legendName: '最低',
- legendValue: 'min',
- businessName: lowestLine.name,
- color: lowestLine.color,
- data: lowestLine.data,
- },
- ]
- const chart = echarts.init(highestRevenueChart.value)
- const legendSelected: Record<string, boolean> = {
- max: highestChartSelected.value === 'max',
- min: highestChartSelected.value === 'min',
- }
- const option = {
- title: [
- {
- text: '年度最高业务收入',
- left: 0,
- top: 0,
- textStyle: {
- fontSize: 15,
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontWeight: 'normal',
- },
- },
- {
- text: `{a|${legendSelected.max ? chartData[0].businessName : chartData[1].businessName}}`,
- left: 0,
- top: 24,
- textStyle: {
- rich: {
- a: {
- fontSize: 14,
- color: legendSelected.max ? chartData[0].color : chartData[1].color,
- backgroundColor: chroma(legendSelected.max ? chartData[0].color : chartData[1].color)
- .alpha(0.1)
- .hex(),
- padding: [4, 6],
- borderRadius: 2,
- lineHeight: 22,
- },
- },
- },
- },
- ],
- color: [chartData[0].color, chartData[1].color],
- grid: { left: -30, right: -30, top: 70, bottom: 0, containLabel: false },
- xAxis: {
- type: 'category',
- data: chartDataManager.getMonths(),
- show: false,
- },
- yAxis: {
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: {
- lineStyle: {
- color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
- type: 'dashed',
- },
- },
- },
- legend: {
- top: 6,
- right: 22,
- orient: 'horizontal',
- itemGap: 16,
- icon: 'circle',
- itemWidth: 12,
- itemHeight: 12,
- inactiveBorderWidth: 0,
- inactiveColor: isDark.value ? twc.neutral[400] : twc.neutral[350],
- inactiveBorderColor: isDark.value ? twc.neutral[400] : twc.neutral[350],
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 14,
- },
- data: chartData.map((item) => ({
- name: item.legendName,
- itemStyle: {
- borderColor: item.color,
- borderWidth: 0,
- },
- })),
- selectedMode: 'single',
- selected: Object.fromEntries(
- chartData.map((item) => [item.legendName, legendSelected[item.legendValue] ?? false]),
- ),
- },
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'none' },
- backgroundColor: isDark.value ? twc.neutral[750] : '#fff',
- borderWidth: 0,
- padding: 8,
- extraCssText: 'box-shadow: none;',
- textStyle: {
- color: isDark.value ? twc.neutral[400] : twc.neutral[600],
- fontSize: 12,
- },
- formatter(params: any) {
- const color = params[0].color
- const date = params[0].axisValue
- const value = params[0].value.toLocaleString()
- const chartItem = chartData.find((item) => item.legendName === params[0].seriesName)
- const realName = chartItem ? chartItem.businessName : params[0].seriesName
- let result = `<div>${date}数据</div>`
- result += `
- <div style="display: flex; align-items: center; margin-top: 4px;">
- <span style="display:inline-block; margin-right:4px; width:10px; height:10px; border-radius:50%; background-color:${color};"></span>
- <span style="margin-right: 10px">${realName}</span>
- <span>${value}</span>
- </div>
- `
- return result
- },
- },
- series: chartData.map((item) => ({
- name: item.legendName,
- type: 'line',
- step: 'middle',
- data: item.data,
- left: 0,
- symbol: 'circle',
- symbolSize: 6,
- showSymbol: true,
- lineStyle: {
- width: 2,
- color: item.color,
- },
- itemStyle: {
- color: item.color,
- borderColor: isDark.value ? twc.neutral[800] : twc.neutral[50],
- borderWidth: 2,
- },
- areaStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: chroma(item.color).alpha(0.12).hex() },
- { offset: 1, color: chroma(item.color).alpha(0.02).hex() },
- ],
- },
- },
- })),
- animationDuration: 1000,
- animationEasing: 'cubicOut' as const,
- animationDelay(idx: number) {
- return idx * 100
- },
- }
- chart.setOption(option)
- highestRevenueChartInstance = chart
- highestRevenueChartResizeHandler = () => chart.resize()
- window.addEventListener('resize', highestRevenueChartResizeHandler, { passive: true })
- chart.on('legendselectchanged', (params: any) => {
- const chartItem = chartData.find((item) => item.legendName === params.name)
- if (!chartItem) return
- const isHighest = chartItem.legendValue === 'max'
- highestChartSelected.value = isHighest ? 'max' : 'min'
- chart.setOption({
- title: [
- {
- text: isHighest ? '年度最高收入业务' : '年度最低收入业务',
- },
- {
- text: `{a|${chartItem.businessName}}`,
- textStyle: {
- rich: {
- a: {
- color: chartItem.color,
- backgroundColor: chroma(chartItem.color).alpha(0.1).hex(),
- },
- },
- },
- },
- ],
- })
- })
- }
- onMounted(() => {
- initRevenueChart()
- initRevenueBarChart()
- initRevenueBarChart2()
- initMonthlyRadarChart()
- initHighestRevenueChart()
- })
- onUnmounted(() => {
- if (revenueChartInstance) {
- if (revenueChartResizeHandler) {
- window.removeEventListener('resize', revenueChartResizeHandler)
- revenueChartResizeHandler = null
- }
- revenueChartInstance.dispose()
- revenueChartInstance = null
- }
- if (revenueBarChartInstance) {
- if (revenueBarChartResizeHandler) {
- window.removeEventListener('resize', revenueBarChartResizeHandler)
- revenueBarChartResizeHandler = null
- }
- revenueBarChartInstance.dispose()
- revenueBarChartInstance = null
- }
- if (revenueBarChart2Instance) {
- if (revenueBarChart2ResizeHandler) {
- window.removeEventListener('resize', revenueBarChart2ResizeHandler)
- revenueBarChart2ResizeHandler = null
- }
- revenueBarChart2Instance.dispose()
- revenueBarChart2Instance = null
- }
- if (monthlyRadarChartInstance) {
- if (monthlyRadarChartResizeHandler) {
- window.removeEventListener('resize', monthlyRadarChartResizeHandler)
- monthlyRadarChartResizeHandler = null
- }
- monthlyRadarChartInstance.dispose()
- monthlyRadarChartInstance = null
- }
- if (highestRevenueChartInstance) {
- if (highestRevenueChartResizeHandler) {
- window.removeEventListener('resize', highestRevenueChartResizeHandler)
- highestRevenueChartResizeHandler = null
- }
- highestRevenueChartInstance.dispose()
- highestRevenueChartInstance = null
- }
- if (collapseResizeTimeout !== null) {
- clearTimeout(collapseResizeTimeout)
- collapseResizeTimeout = null
- }
- })
- function resizeAllCharts() {
- if (revenueChartInstance) revenueChartInstance.resize()
- if (revenueBarChartInstance) revenueBarChartInstance.resize()
- if (revenueBarChart2Instance) revenueBarChart2Instance.resize()
- if (monthlyRadarChartInstance) monthlyRadarChartInstance.resize()
- if (highestRevenueChartInstance) highestRevenueChartInstance.resize()
- }
- watch(
- [
- () => preferencesStore.preferences.sidebarMenu.collapsed,
- () => preferencesStore.preferences.navigationMode,
- ],
- () => {
- if (collapseResizeTimeout !== null) {
- clearTimeout(collapseResizeTimeout)
- collapseResizeTimeout = null
- }
- nextTick(() => {
- collapseResizeTimeout = setTimeout(() => {
- resizeAllCharts()
- }, 350)
- })
- },
- )
- watch([isDark, color], () => {
- if (revenueChartInstance) {
- if (revenueChartResizeHandler) {
- window.removeEventListener('resize', revenueChartResizeHandler)
- revenueChartResizeHandler = null
- }
- revenueChartInstance.dispose()
- revenueChartInstance = null
- }
- if (revenueBarChartInstance) {
- if (revenueBarChartResizeHandler) {
- window.removeEventListener('resize', revenueBarChartResizeHandler)
- revenueBarChartResizeHandler = null
- }
- revenueBarChartInstance.dispose()
- revenueBarChartInstance = null
- }
- if (revenueBarChart2Instance) {
- if (revenueBarChart2ResizeHandler) {
- window.removeEventListener('resize', revenueBarChart2ResizeHandler)
- revenueBarChart2ResizeHandler = null
- }
- revenueBarChart2Instance.dispose()
- revenueBarChart2Instance = null
- }
- if (monthlyRadarChartInstance) {
- if (monthlyRadarChartResizeHandler) {
- window.removeEventListener('resize', monthlyRadarChartResizeHandler)
- monthlyRadarChartResizeHandler = null
- }
- monthlyRadarChartInstance.dispose()
- monthlyRadarChartInstance = null
- }
- if (highestRevenueChartInstance) {
- if (highestRevenueChartResizeHandler) {
- window.removeEventListener('resize', highestRevenueChartResizeHandler)
- highestRevenueChartResizeHandler = null
- }
- highestRevenueChartInstance.dispose()
- highestRevenueChartInstance = null
- }
- initRevenueChart()
- initRevenueBarChart()
- initRevenueBarChart2()
- initMonthlyRadarChart()
- initHighestRevenueChart()
- })
- </script>
- <template>
- <ContentWrapper content-class="flex flex-col gap-y-4 max-sm:gap-y-2">
- <div class="grid grid-cols-1 gap-4 max-sm:gap-2 md:grid-cols-2 lg:grid-cols-4">
- <div
- v-for="{
- title,
- value,
- precision,
- percentage,
- description,
- iconBgClass,
- iconClass,
- } in cardList"
- :key="title"
- class="flex items-center justify-between gap-x-4 overflow-hidden rounded bg-naive-card p-6 shadow-xs transition-[background-color]"
- >
- <div class="flex-1">
- <span class="text-sm font-medium text-neutral-450">{{ title }}</span>
- <div class="mt-1 mb-1.5 flex gap-x-4 text-2xl text-neutral-700 dark:text-neutral-400">
- <NNumberAnimation
- :to="value"
- show-separator
- :precision="precision"
- />
- </div>
- <div class="flex items-center">
- <div
- class="flex items-center gap-x-0.5 rounded-xs px-1.5 py-0.5 text-xs transition-[background-color,color]"
- :class="
- percentage > 0
- ? 'bg-green-100 text-green-600 dark:bg-green-500/20 dark:text-green-400'
- : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
- "
- >
- <span
- :class="percentage > 0 ? 'iconify ph--arrow-up' : 'iconify ph--arrow-down'"
- ></span>
- <span class="font-medium">{{ Math.abs(percentage) }}%</span>
- </div>
- <span class="ml-2 text-neutral-500 dark:text-neutral-400">{{ description }}</span>
- </div>
- </div>
- <div>
- <div
- class="grid place-items-center rounded-full p-3"
- :class="iconBgClass"
- >
- <span
- class="size-7"
- :class="iconClass"
- />
- </div>
- </div>
- </div>
- </div>
- <div class="grid grid-cols-1 gap-4 overflow-hidden max-sm:gap-2 lg:grid-cols-12">
- <div class="col-span-1 lg:col-span-8">
- <div
- class="rounded bg-naive-card px-5 pt-5 pb-4.5 shadow-xs transition-[background-color]"
- style="height: 400px"
- >
- <div
- ref="revenueChart"
- class="h-full"
- />
- </div>
- </div>
- <div class="col-span-1 lg:col-span-4">
- <div
- class="flex flex-col rounded bg-naive-card px-5 pt-5 pb-4.5 shadow-xs transition-[background-color]"
- style="height: 400px"
- >
- <div
- ref="revenueBarChart"
- class="flex-1"
- />
- <div
- ref="revenueBarChart2"
- style="height: 150px"
- />
- </div>
- </div>
- </div>
- <div class="grid grid-cols-1 gap-4 overflow-hidden max-sm:gap-2 lg:grid-cols-12">
- <div class="col-span-1 lg:col-span-5">
- <div
- class="rounded bg-naive-card px-5 pt-5 pb-3 shadow-xs transition-[background-color]"
- style="height: 340px"
- >
- <div
- ref="monthlyRadarChart"
- class="h-full"
- />
- </div>
- </div>
- <div class="col-span-1 lg:col-span-7">
- <div
- class="rounded bg-naive-card p-5 shadow-xs transition-[background-color]"
- style="height: 340px; position: relative"
- >
- <div
- ref="highestRevenueChart"
- class="h-full"
- />
- </div>
- </div>
- </div>
- </ContentWrapper>
- </template>
|