Commit e898d322 by licheng

test: 高频大数据波形图验证

parent 7f91efbd
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"diagram-js": "^12.8.0", "diagram-js": "^12.8.0",
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.0", "echarts": "^6.0.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.9.1", "element-plus": "2.9.1",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
......
...@@ -69,11 +69,11 @@ importers: ...@@ -69,11 +69,11 @@ importers:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
echarts: echarts:
specifier: ^5.5.0 specifier: ^6.0.0
version: 5.5.1 version: 6.0.0
echarts-wordcloud: echarts-wordcloud:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0(echarts@5.5.1) version: 2.1.0(echarts@6.0.0)
element-plus: element-plus:
specifier: 2.9.1 specifier: 2.9.1
version: 2.9.1(vue@3.5.12(typescript@5.3.3)) version: 2.9.1(vue@3.5.12(typescript@5.3.3))
...@@ -3015,8 +3015,8 @@ packages: ...@@ -3015,8 +3015,8 @@ packages:
peerDependencies: peerDependencies:
echarts: ^5.0.1 echarts: ^5.0.1
echarts@5.5.1: echarts@6.0.0:
resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
ejs@3.1.10: ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
...@@ -5153,8 +5153,8 @@ packages: ...@@ -5153,8 +5153,8 @@ packages:
zeebe-bpmn-moddle@1.7.0: zeebe-bpmn-moddle@1.7.0:
resolution: {integrity: sha512-eZ6OXSt0c4n9V/oN/46gTlwDIS3GhWQLt9jbM5uS/YryB4yN8wdrrKrtw+TpyNy0SSKWXNDHyC83nCA2blPO3Q==} resolution: {integrity: sha512-eZ6OXSt0c4n9V/oN/46gTlwDIS3GhWQLt9jbM5uS/YryB4yN8wdrrKrtw+TpyNy0SSKWXNDHyC83nCA2blPO3Q==}
zrender@5.6.0: zrender@6.0.0:
resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
snapshots: snapshots:
...@@ -8303,14 +8303,14 @@ snapshots: ...@@ -8303,14 +8303,14 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
echarts-wordcloud@2.1.0(echarts@5.5.1): echarts-wordcloud@2.1.0(echarts@6.0.0):
dependencies: dependencies:
echarts: 5.5.1 echarts: 6.0.0
echarts@5.5.1: echarts@6.0.0:
dependencies: dependencies:
tslib: 2.3.0 tslib: 2.3.0
zrender: 5.6.0 zrender: 6.0.0
ejs@3.1.10: ejs@3.1.10:
dependencies: dependencies:
...@@ -10542,6 +10542,6 @@ snapshots: ...@@ -10542,6 +10542,6 @@ snapshots:
zeebe-bpmn-moddle@1.7.0: {} zeebe-bpmn-moddle@1.7.0: {}
zrender@5.6.0: zrender@6.0.0:
dependencies: dependencies:
tslib: 2.3.0 tslib: 2.3.0
...@@ -62,7 +62,10 @@ const styles = computed(() => { ...@@ -62,7 +62,10 @@ const styles = computed(() => {
const initChart = () => { const initChart = () => {
if (unref(elRef) && props.options) { if (unref(elRef) && props.options) {
echartRef = echarts.init(unref(elRef) as HTMLElement) echartRef = echarts.init(unref(elRef) as HTMLElement, null, {
renderer: 'canvas',
useDirtyRect: true // 启用脏矩形优化
})
echartRef?.setOption(unref(options)) echartRef?.setOption(unref(options))
} }
} }
...@@ -71,7 +74,13 @@ watch( ...@@ -71,7 +74,13 @@ watch(
() => options.value, () => options.value,
(options) => { (options) => {
if (echartRef) { if (echartRef) {
echartRef?.setOption(options) const startTime = performance.now()
echartRef.setOption({
xAxis: { data: options.xAxis.data },
series: options.series
})
const endTime = performance.now()
console.log(`ECharts 更新耗时: ${(endTime - startTime).toFixed(2)}ms`)
} }
}, },
{ {
...@@ -81,10 +90,12 @@ watch( ...@@ -81,10 +90,12 @@ watch(
const resizeHandler = debounce(() => { const resizeHandler = debounce(() => {
if (echartRef) { if (echartRef) {
const startTime = performance.now()
echartRef.resize() echartRef.resize()
const endTime = performance.now()
console.log(`ECharts 重绘耗时: ${(endTime - startTime).toFixed(2)}ms`)
} }
}, 100) }, 100)
const contentResizeHandler = async (e: TransitionEvent) => { const contentResizeHandler = async (e: TransitionEvent) => {
if (e.propertyName === 'width') { if (e.propertyName === 'width') {
resizeHandler() resizeHandler()
......
<template> <template>
<div> <div>
<el-card shadow="never"> index
<el-skeleton :loading="loading" animated> <el-button type="primary">Primary Button</el-button>
<el-row :gutter="16" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-20px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }},20℃ - 32℃!
</div>
</div>
</div>
</el-col>
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
/>
</div>
<el-divider direction="vertical" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<el-divider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
/>
</div>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</div> </div>
<el-row class="mt-8px" :gutter="8" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card
shadow="hover"
class="mr-5px mt-5px cursor-pointer"
@click="handleProjectClick(item.message)"
>
<div class="flex items-center">
<Icon
:icon="item.icon"
:size="25"
class="mr-8px"
:style="{ color: item.color }"
/>
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
<el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template> </template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
// import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
import { useRouter } from 'vue-router'
defineOptions({ name: 'Index' })
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
// const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'simple-icons:springboot',
message: 'github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date('2025-01-02'),
color: '#6DB33F'
},
{
name: 'yudao-ui-admin-vue3',
icon: 'ep:element-plus',
message: 'github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus 管理后台',
time: new Date('2025-02-03'),
color: '#409EFF'
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'icon-park-outline:mall-bag',
message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
personal: 'Vue3 + uniapp 商城手机端',
time: new Date('2025-03-04'),
color: '#ff4d4f'
},
{
name: 'yudao-cloud',
icon: 'material-symbols:cloud-outline',
message: 'github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date('2025-04-05'),
color: '#1890ff'
},
{
name: 'yudao-ui-admin-vben',
icon: 'devicon:antdesign',
message: 'github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben5(antd) 管理后台',
time: new Date('2025-05-06'),
color: '#e18525'
},
{
name: 'yudao-ui-admin-uniapp',
icon: 'ant-design:mobile',
message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp 管理手机端',
time: new Date('2025-06-01'),
color: '#2979ff'
}
]
projects = Object.assign(projects, data)
}
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21,Vue 2/3',
type: '技术兼容性',
keys: ['JDK', 'Vue'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '架构灵活性',
keys: ['Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '开源免授权',
keys: ['无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
type: '广泛企业认可',
keys: ['最广泛', '10w+'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 获取快捷入口 <script setup lang="ts">
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: '首页',
icon: 'ion:home-outline',
url: '/',
color: '#1fdaca'
},
{
name: '商城中心',
icon: 'ep:shop',
url: '/mall/home',
color: '#ff6b6b'
},
{
name: 'AI 大模型',
icon: 'tabler:ai',
url: '/ai/chat',
color: '#7c3aed'
},
{
name: 'ERP 系统',
icon: 'simple-icons:erpnext',
url: '/erp/home',
color: '#3fb27f'
},
{
name: 'CRM 系统',
icon: 'simple-icons:civicrm',
url: '/crm/backlog',
color: '#4daf1bc9'
},
{
name: 'IoT 物联网',
icon: 'fa-solid:hdd',
url: '/iot/home',
color: '#1a73e8'
}
]
shortcut = Object.assign(shortcut, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
}
const handleProjectClick = (message: string) => {
window.open(`https://${message}`, '_blank')
}
const handleShortcutClick = (url: string) => {
router.push(url)
}
getAllApi()
</script> </script>
<style lang="scss" scoped></style>
<template> <template>
<el-row :class="prefixCls" :gutter="20" justify="space-between"> <el-card class="mb-20px" shadow="hover">
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24"> <el-skeleton :loading="loading" :rows="4" animated>
<el-card class="mb-20px" shadow="hover"> <Echart :height="350" :options="lineOptionsData" />
<el-skeleton :loading="loading" :rows="2" animated> </el-skeleton>
<template #default> </el-card>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:peoples" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.newUser') }}
</div>
<CountTo
:duration="2600"
:end-val="102400"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:message" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.unreadInformation') }}
</div>
<CountTo
:duration="2600"
:end-val="81212"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:money" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.transactionAmount') }}
</div>
<CountTo
:duration="2600"
:end-val="9280"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:shopping" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.totalShopping') }}
</div>
<CountTo
:duration="2600"
:end-val="13600"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" justify="space-between">
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="pieOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="barOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="4" animated>
<Echart :height="350" :options="lineOptionsData" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { set } from 'lodash-es' import { set } from 'lodash-es'
import { EChartsOption } from 'echarts' import { EChartsOption } from 'echarts'
import 'echarts/lib/component/dataZoom'
import { useDesign } from '@/hooks/web/useDesign' import { lineOptions } from './echarts-data'
import type { AnalysisTotalTypes } from './types'
import { barOptions, lineOptions, pieOptions } from './echarts-data'
defineOptions({ name: 'Home2' }) defineOptions({ name: 'Home2' })
const { t } = useI18n() const { t } = useI18n()
const loading = ref(true) const loading = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('panel')
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
let totalState = reactive<AnalysisTotalTypes>({
users: 0,
messages: 0,
moneys: 0,
shoppings: 0
})
const getCount = async () => {
const data = {
users: 102400,
messages: 81212,
moneys: 9280,
shoppings: 13600
}
totalState = Object.assign(totalState, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
set(pieOptionsData, 'series.data', data)
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
let data: any[] = [] // 波形图数据
// 每月销售总额 // 初始数据
const getMonthlySales = async () => { const getLineData = async () => {
const data = [ // data = createData(1000000, 1)
{ estimate: 100, actual: 120, name: 'analysis.january' },
{ estimate: 120, actual: 82, name: 'analysis.february' },
{ estimate: 161, actual: 91, name: 'analysis.march' },
{ estimate: 134, actual: 154, name: 'analysis.april' },
{ estimate: 105, actual: 162, name: 'analysis.may' },
{ estimate: 160, actual: 140, name: 'analysis.june' },
{ estimate: 165, actual: 145, name: 'analysis.july' },
{ estimate: 114, actual: 250, name: 'analysis.august' },
{ estimate: 163, actual: 134, name: 'analysis.september' },
{ estimate: 185, actual: 56, name: 'analysis.october' },
{ estimate: 118, actual: 99, name: 'analysis.november' },
{ estimate: 123, actual: 123, name: 'analysis.december' }
]
set( set(
lineOptionsData, lineOptionsData,
'xAxis.data', 'xAxis.data',
...@@ -241,79 +30,169 @@ const getMonthlySales = async () => { ...@@ -241,79 +30,169 @@ const getMonthlySales = async () => {
) )
set(lineOptionsData, 'series', [ set(lineOptionsData, 'series', [
{ {
name: t('analysis.estimate'), name: 'data1',
smooth: true, smooth: false, // 是否平滑曲线
type: 'line',
data: data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line', type: 'line',
itemStyle: {}, showSymbol: false, // 不显示数据点
data: data.map((v) => v.actual), progressive: 1000, // 每次增量渲染的数据量
animationDuration: 2800, progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
animationEasing: 'quadraticOut' lineStyle: {
width: 1 // 细线
},
sampling: 'lttb', // 使用 LTTB 采样算法降低数据量
// samplingThreshold: 10000, // 采样阈值,超过此值将进行采样
animation: false, // 动画
animationDuration: 1000, // 动画持续时间
animationEasing: 'linear', // 动画缓动效果
data: data.map((v) => v.data1)
} }
// {
// name: 'data2',
// smooth: false, // 是否平滑曲线
// type: 'line',
// showSymbol: false, // 不显示数据点
// progressive: 1000, // 每次增量渲染的数据量
// progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
// animation: false, // 关闭动画
// lineStyle: {
// width: 0.5 // 细线
// },
// sampling: 'lttb', // 使用 LTTB 采样算法
// samplingThreshold: 10000, // 采样阈值,超过此值将进行采样
// data: data.map((v) => v.data2)
// }
]) ])
} }
const connect = () => {
// 连接到 WebSocket 服务器(替换成你的服务器地址)
const socket = new WebSocket('ws://localhost:8080')
// 监听连接成功
socket.addEventListener('open', (event) => {
console.log('WebSocket 连接成功')
})
// 接收服务器消息
socket.addEventListener('message', (event) => {
const newData = JSON.parse(event.data)
// 更新图表数据
const newXAxisData = [...lineOptionsData!.xAxis!.data, ...newData.map((item) => item.name)]
console.log(newXAxisData.length)
set(lineOptionsData, 'xAxis.data', newXAxisData)
set(lineOptionsData, 'series', [
{
name: 'data1',
smooth: false,
type: 'line',
// 启用大数据优化
progressive: 1000,
progressiveThreshold: 5000,
showSymbol: false, // 不显示数据点
lineStyle: {
width: 0.5 // 细线
},
sampling: 'lttb', // 使用 LTTB 采样算法
animation: false, // 动画
animationDuration: 1000, // 动画持续时间
animationEasing: 'linear', // 动画缓动效果
data: [...lineOptionsData!.series![0].data, ...newData.map((item) => item.data1)]
}
// {
// name: 'data2',
// smooth: false,
// type: 'line',
// itemStyle: {},
// // 启用大数据优化
// progressive: 1000,
// progressiveThreshold: 5000,
// lineStyle: {
// width: 0.5 // 细线
// },
// data: [...lineOptionsData.series[1].data, ...newData.map((item) => item.data2)]
// }
])
})
}
const getAllApi = async () => { const getAllApi = async () => {
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()]) await getLineData()
loading.value = false loading.value = false
connect()
} }
getAllApi() getAllApi()
</script>
<style lang="scss" scoped> interface DataItem {
$prefix-cls: #{$namespace}-panel; [key: string]: number | string
name: string
}
.#{$prefix-cls} { function formatDate(date: Date, format: string): string {
&__item { const pad = (num: number) => num.toString().padStart(2, '0')
&--peoples { return format
color: #40c9c6; .replace('HH', pad(date.getHours()))
} .replace('mm', pad(date.getMinutes()))
.replace('ss', pad(date.getSeconds()))
}
&--message { function createData(
color: #36a3f7; count: number,
} dataCount: 1 | 2 | 3 = 2,
minValue: number = 50,
maxValue: number = 250
): DataItem[] {
const result: DataItem[] = []
const now = new Date()
// 生成时间点(从当前时间往前递减)
const timeStamps: Date[] = []
for (let i = count - 1; i >= 0; i--) {
const time = new Date(now.getTime() - i * 1000)
timeStamps.push(time)
}
&--money { // 初始化各数据项的值和方向
color: #f4516c; const dataValues: Record<string, number> = {}
} const dataDirections: Record<string, number> = {}
&--shopping { for (let i = 1; i <= dataCount; i++) {
color: #34bfa3; const key = `data${i}`
} dataValues[key] = minValue + Math.random() * (maxValue - minValue) // 初始随机值
dataDirections[key] = Math.random() > 0.5 ? 1 : -1 // 随机初始方向
}
&:hover { for (let i = 0; i < count; i++) {
:deep(.#{$namespace}-icon) { const item: DataItem = { name: formatDate(timeStamps[i], 'HH:mm:ss') }
color: #fff !important;
}
.#{$prefix-cls}__item--icon { // 为每个数据项生成波形数据
transition: all 0.38s ease-out; for (let j = 1; j <= dataCount; j++) {
} const key = `data${j}`
const change = 10 + Math.random() * 30 // 变化幅度10~40(可调整)
.#{$prefix-cls}__item--peoples { // 随机决定是否改变方向(更自然的波动)
background: #40c9c6; if (Math.random() < 0.2) {
// 20%概率改变方向
dataDirections[key] *= -1
} }
.#{$prefix-cls}__item--message { dataValues[key] += dataDirections[key] * change
background: #36a3f7;
}
.#{$prefix-cls}__item--money { // 限制在合理范围内(但不强制反转)
background: #f4516c; dataValues[key] = Math.max(
} minValue * 0.9, // 允许稍微低于最小值(避免卡边界)
Math.min(maxValue * 1.1, dataValues[key]) // 允许稍微高于最大值
)
.#{$prefix-cls}__item--shopping { item[key] = Math.round(dataValues[key])
background: #34bfa3;
}
} }
result.push(item)
} }
return result
} }
</style> </script>
<style lang="scss" scoped></style>
...@@ -4,24 +4,12 @@ const { t } = useI18n() ...@@ -4,24 +4,12 @@ const { t } = useI18n()
export const lineOptions: EChartsOption = { export const lineOptions: EChartsOption = {
title: { title: {
text: t('analysis.monthlySales'), text: '波形图',
left: 'center' left: 'center'
}, },
xAxis: { xAxis: {
data: [ type: 'category',
t('analysis.january'), data: [],
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false, boundaryGap: false,
axisTick: { axisTick: {
show: false show: false
...@@ -30,7 +18,7 @@ export const lineOptions: EChartsOption = { ...@@ -30,7 +18,7 @@ export const lineOptions: EChartsOption = {
grid: { grid: {
left: 20, left: 20,
right: 20, right: 20,
bottom: 20, bottom: 50,
top: 80, top: 80,
containLabel: true containLabel: true
}, },
...@@ -42,31 +30,57 @@ export const lineOptions: EChartsOption = { ...@@ -42,31 +30,57 @@ export const lineOptions: EChartsOption = {
padding: [5, 10] padding: [5, 10]
}, },
yAxis: { yAxis: {
type: 'value',
axisTick: { axisTick: {
show: false show: false
} }
}, },
legend: { legend: {
data: [t('analysis.estimate'), t('analysis.actual')], data: ['data1'],
top: 50 top: 50
}, },
series: [ toolbox: {
feature: {
dataZoom: {
// yAxisIndex: 'none'
show: true
},
restore: {},
saveAsImage: {}
}
},
// 缩放图例(需导入 dataZoom)
dataZoom: [
{ {
name: t('analysis.estimate'), type: 'inside', // 内部缩放
smooth: true, start: 0,
type: 'line', end: 100
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
}, },
{ {
name: t('analysis.actual'), start: 0,
smooth: true, end: 100
}
],
animation: false, // 禁用动画
progressive: 1000, // 每次增量渲染的数据量
progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
series: [
{
name: 'data1',
smooth: false, // 是否平滑曲线
type: 'line', type: 'line',
itemStyle: {}, showSymbol: false, // 不显示数据点
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123], progressive: 1000, // 每次增量渲染的数据量
animationDuration: 2800, progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
animationEasing: 'quadraticOut' animation: false, // 关闭动画
lineStyle: {
width: 1 // 细线
},
sampling: 'lttb', // 使用 LTTB 采样算法
// samplingThreshold: 10000, // 采样阈值,超过此值将进行采样
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123]
// animationDuration: 2800, // 动画持续时间
// animationEasing: 'cubicInOut' // 动画缓动效果
} }
] ]
} }
......
<template>
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="4" animated>
<Echart :height="350" :options="lineOptionsData" />
</el-skeleton>
</el-card>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import 'echarts/lib/component/dataZoom'
import { lineOptions } from './echarts-data'
defineOptions({ name: 'Home2' })
const { t } = useI18n()
const loading = ref(true)
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
let data: any[] = [] // 波形图数据
// 初始数据
const getLineData = async () => {
// data = createData(1000000, 1)
set(
lineOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: 'data1',
smooth: false, // 是否平滑曲线
type: 'line',
showSymbol: false, // 不显示数据点
progressive: 1000, // 每次增量渲染的数据量
progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
lineStyle: {
width: 1 // 细线
},
sampling: 'lttb', // 使用 LTTB 采样算法降低数据量
// samplingThreshold: 10000, // 采样阈值,超过此值将进行采样
animation: false, // 动画
animationDuration: 1000, // 动画持续时间
animationEasing: 'linear', // 动画缓动效果
data: data.map((v) => v.data1)
}
// {
// name: 'data2',
// smooth: false, // 是否平滑曲线
// type: 'line',
// showSymbol: false, // 不显示数据点
// progressive: 1000, // 每次增量渲染的数据量
// progressiveThreshold: 5000, // 总数据量超过此值触发增量渲染
// animation: false, // 关闭动画
// lineStyle: {
// width: 0.5 // 细线
// },
// sampling: 'lttb', // 使用 LTTB 采样算法
// samplingThreshold: 10000, // 采样阈值,超过此值将进行采样
// data: data.map((v) => v.data2)
// }
])
}
const connect = () => {
// 连接到 WebSocket 服务器
const socket = new WebSocket('ws://localhost:8080')
// 监听连接成功
socket.addEventListener('open', (event) => {
console.log('WebSocket 连接成功')
})
// 接收服务器消息
socket.addEventListener('message', (event) => {
const newData = JSON.parse(event.data)
// 更新图表数据
const newXAxisData = [...lineOptionsData!.xAxis!.data, ...newData.map((item) => item.name)]
console.log(newXAxisData.length)
set(lineOptionsData, 'xAxis.data', newXAxisData)
set(lineOptionsData, 'series', [
{
name: 'data1',
smooth: false,
type: 'line',
// 启用大数据优化
progressive: 1000,
progressiveThreshold: 5000,
showSymbol: false, // 不显示数据点
lineStyle: {
width: 0.5 // 细线
},
sampling: 'lttb', // 使用 LTTB 采样算法
animation: false, // 动画
animationDuration: 1000, // 动画持续时间
animationEasing: 'linear', // 动画缓动效果
data: [...lineOptionsData!.series![0].data, ...newData.map((item) => item.data1)]
}
// {
// name: 'data2',
// smooth: false,
// type: 'line',
// itemStyle: {},
// // 启用大数据优化
// progressive: 1000,
// progressiveThreshold: 5000,
// lineStyle: {
// width: 0.5 // 细线
// },
// data: [...lineOptionsData.series[1].data, ...newData.map((item) => item.data2)]
// }
])
})
}
const getAllApi = async () => {
await getLineData()
loading.value = false
connect()
}
getAllApi()
interface DataItem {
[key: string]: number | string
name: string
}
function formatDate(date: Date, format: string): string {
const pad = (num: number) => num.toString().padStart(2, '0')
return format
.replace('HH', pad(date.getHours()))
.replace('mm', pad(date.getMinutes()))
.replace('ss', pad(date.getSeconds()))
}
function createData(
count: number,
dataCount: 1 | 2 | 3 = 2,
minValue: number = 50,
maxValue: number = 250
): DataItem[] {
const result: DataItem[] = []
const now = new Date()
// 生成时间点(从当前时间往前递减)
const timeStamps: Date[] = []
for (let i = count - 1; i >= 0; i--) {
const time = new Date(now.getTime() - i * 1000)
timeStamps.push(time)
}
// 初始化各数据项的值和方向
const dataValues: Record<string, number> = {}
const dataDirections: Record<string, number> = {}
for (let i = 1; i <= dataCount; i++) {
const key = `data${i}`
dataValues[key] = minValue + Math.random() * (maxValue - minValue) // 初始随机值
dataDirections[key] = Math.random() > 0.5 ? 1 : -1 // 随机初始方向
}
for (let i = 0; i < count; i++) {
const item: DataItem = { name: formatDate(timeStamps[i], 'HH:mm:ss') }
// 为每个数据项生成波形数据
for (let j = 1; j <= dataCount; j++) {
const key = `data${j}`
const change = 10 + Math.random() * 30 // 变化幅度10~40(可调整)
// 随机决定是否改变方向(更自然的波动)
if (Math.random() < 0.2) {
// 20%概率改变方向
dataDirections[key] *= -1
}
dataValues[key] += dataDirections[key] * change
// 限制在合理范围内(但不强制反转)
dataValues[key] = Math.max(
minValue * 0.9, // 允许稍微低于最小值(避免卡边界)
Math.min(maxValue * 1.1, dataValues[key]) // 允许稍微高于最大值
)
item[key] = Math.round(dataValues[key])
}
result.push(item)
}
return result
}
</script>
<style lang="scss" scoped></style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment