fix(workbench): replay profile radar animation

This commit is contained in:
caoxiaozhu
2026-06-03 17:31:12 +08:00
parent 9c24a852e7
commit 67b81a1bd8
3 changed files with 47 additions and 56 deletions

View File

@@ -6,7 +6,7 @@
width="min(1040px, calc(100vw - 48px))" width="min(1040px, calc(100vw - 48px))"
:show-close="false" :show-close="false"
:lock-scroll="true" :lock-scroll="true"
:destroy-on-close="false" destroy-on-close
class="expense-profile-dialog" class="expense-profile-dialog"
modal-class="expense-profile-dialog-overlay" modal-class="expense-profile-dialog-overlay"
body-class="expense-profile-dialog-body" body-class="expense-profile-dialog-body"

View File

@@ -3,12 +3,13 @@
</template> </template>
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue' import { computed, shallowRef } from 'vue'
import { RadarChart as EChartsRadarChart } from 'echarts/charts' import { RadarChart as EChartsRadarChart } from 'echarts/charts'
import { RadarComponent, TooltipComponent } from 'echarts/components' import { RadarComponent, TooltipComponent } from 'echarts/components'
import { init, use } from 'echarts/core' import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers' import { CanvasRenderer } from 'echarts/renderers'
import { useEcharts } from '../../composables/useEcharts.js'
import { useThemeColors } from '../../composables/useThemeColors.js' import { useThemeColors } from '../../composables/useThemeColors.js'
use([RadarComponent, EChartsRadarChart, TooltipComponent, CanvasRenderer]) use([RadarComponent, EChartsRadarChart, TooltipComponent, CanvasRenderer])
@@ -32,8 +33,6 @@ const props = defineProps({
const themeColors = useThemeColors() const themeColors = useThemeColors()
const chartElement = shallowRef(null) const chartElement = shallowRef(null)
let chartInstance = null
let resizeObserver = null
const normalizedItems = computed(() => const normalizedItems = computed(() =>
props.items.map((item, index) => ({ props.items.map((item, index) => ({
@@ -62,8 +61,11 @@ const chartOptions = computed(() => {
return { return {
backgroundColor: 'transparent', backgroundColor: 'transparent',
animationDuration: 760, animation: true,
animationDuration: 980,
animationDurationUpdate: 760,
animationEasing: 'cubicOut', animationEasing: 'cubicOut',
animationEasingUpdate: 'cubicOut',
color: [primary], color: [primary],
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
@@ -123,6 +125,11 @@ const chartOptions = computed(() => {
{ {
name: props.label, name: props.label,
type: 'radar', type: 'radar',
animation: true,
animationDuration: 980,
animationDurationUpdate: 760,
animationEasing: 'cubicOut',
animationEasingUpdate: 'cubicOut',
symbol: 'circle', symbol: 'circle',
symbolSize: 7, symbolSize: 7,
data: [ data: [
@@ -169,56 +176,7 @@ const chartOptions = computed(() => {
} }
}) })
onMounted(() => { useEcharts(chartElement, chartOptions)
renderChart()
bindResize()
})
onBeforeUnmount(() => {
unbindResize()
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
watch(chartOptions, () => {
nextTick(renderChart)
}, { deep: true })
function renderChart() {
if (!chartElement.value) {
return
}
if (!chartInstance) {
chartInstance = init(chartElement.value, null, { renderer: 'canvas' })
}
chartInstance.setOption(chartOptions.value, true)
chartInstance.resize()
}
function bindResize() {
if (!chartElement.value) {
return
}
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => {
chartInstance?.resize()
})
resizeObserver.observe(chartElement.value)
}
window.addEventListener('resize', handleResize)
}
function unbindResize() {
resizeObserver?.disconnect()
resizeObserver = null
window.removeEventListener('resize', handleResize)
}
function handleResize() {
chartInstance?.resize()
}
function formatTooltip() { function formatTooltip() {
const rows = normalizedItems.value.map((item) => ( const rows = normalizedItems.value.map((item) => (

View File

@@ -0,0 +1,33 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const modal = readFileSync(
fileURLToPath(new URL('../src/components/business/ExpenseProfileDetailModal.vue', import.meta.url)),
'utf8'
)
const radarChart = readFileSync(
fileURLToPath(new URL('../src/components/charts/RadarChart.vue', import.meta.url)),
'utf8'
)
test('expense profile modal remounts the behavior radar when opened', () => {
assert.match(modal, /destroy-on-close/)
assert.match(modal, /<RadarChart/)
assert.match(modal, /:key="radarRenderKey"/)
assert.match(modal, /const radarRenderKey = ref\(0\)/)
assert.match(modal, /watch\([\s\S]*\(\) => props\.visible[\s\S]*radarRenderKey\.value \+= 1/)
assert.match(modal, /scheduleRadarFrame/)
})
test('radar chart uses the shared echarts lifecycle and enables entrance animation', () => {
assert.match(radarChart, /import \{ useEcharts \} from '\.\.\/\.\.\/composables\/useEcharts\.js'/)
assert.match(radarChart, /useEcharts\(chartElement, chartOptions\)/)
assert.match(radarChart, /animation: true/)
assert.match(radarChart, /animationDuration: 980/)
assert.match(radarChart, /animationDurationUpdate: 760/)
assert.match(radarChart, /animationEasing: 'cubicOut'/)
assert.doesNotMatch(radarChart, /import \{[^}]*init[^}]*\} from 'echarts\/core'/)
})