Browse Source

refactor: convert class components to functional components

- Convert Yuan.tsx to FC with useRef + useEffect
- Convert MonitorMap to FC with useState + useEffect
- Convert autoHeight HOC to functional component
- Convert WaterWave to FC with proper animation cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pull/11631/head
afc163 7 months ago
parent
commit
33833440e0
  1. 158
      src/pages/dashboard/analysis/components/Charts/WaterWave/index.tsx
  2. 40
      src/pages/dashboard/analysis/utils/Yuan.tsx
  3. 158
      src/pages/dashboard/monitor/components/Charts/WaterWave/index.tsx
  4. 57
      src/pages/dashboard/monitor/components/Charts/autoHeight.tsx
  5. 267
      src/pages/dashboard/monitor/components/Map/index.tsx

158
src/pages/dashboard/analysis/components/Charts/WaterWave/index.tsx

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { useEffect, useRef, useState } from 'react';
import autoHeight from '../../../../monitor/components/Charts/autoHeight';
/* eslint no-return-assign: 0 */
@ -12,57 +12,25 @@ export type WaterWaveProps = {
percent: number;
style?: React.CSSProperties;
};
class WaterWave extends Component<WaterWaveProps> {
state = {
radio: 1,
};
timer: number = 0;
root: HTMLDivElement | undefined | null = null;
node: HTMLCanvasElement | undefined | null = null;
componentDidMount() {
this.renderChart();
this.resize();
window.addEventListener(
'resize',
() => {
requestAnimationFrame(() => this.resize());
},
{
passive: true,
},
);
}
componentDidUpdate(props: WaterWaveProps) {
const { percent } = this.props;
if (props.percent !== percent) {
// 不加这个会造成绘制缓慢
this.renderChart('update');
}
}
componentWillUnmount() {
cancelAnimationFrame(this.timer);
if (this.node) {
this.node.innerHTML = '';
}
window.removeEventListener('resize', this.resize);
}
resize = () => {
if (this.root) {
const { height = 1 } = this.props;
const { offsetWidth } = this.root.parentNode as HTMLElement;
this.setState({
radio: offsetWidth < height ? offsetWidth / height : 1,
});
}
};
renderChart(type?: string) {
const { percent, color = '#1890FF' } = this.props;
const WaterWave: React.FC<WaterWaveProps> = ({
title,
color = '#1890FF',
height = 1,
percent,
}) => {
const [radio, setRadio] = useState(1);
const timerRef = useRef<number>(0);
const rootRef = useRef<HTMLDivElement>(null);
const nodeRef = useRef<HTMLCanvasElement>(null);
const renderChart = (type?: string) => {
const data = percent / 100;
cancelAnimationFrame(this.timer);
if (!this.node || (data !== 0 && !data)) {
cancelAnimationFrame(timerRef.current);
if (!nodeRef.current || (data !== 0 && !data)) {
return;
}
const canvas = this.node;
const canvas = nodeRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
@ -179,47 +147,69 @@ class WaterWave extends Component<WaterWaveProps> {
sp += 0.07;
drawSin();
}
this.timer = requestAnimationFrame(render);
timerRef.current = requestAnimationFrame(render);
};
render();
}
render() {
const { radio } = this.state;
const { percent, title, height = 1 } = this.props;
return (
};
const resize = () => {
if (rootRef.current) {
const { offsetWidth } = rootRef.current.parentNode as HTMLElement;
setRadio(offsetWidth < height ? offsetWidth / height : 1);
}
};
useEffect(() => {
renderChart();
resize();
const handleResize = () => {
requestAnimationFrame(resize);
};
window.addEventListener('resize', handleResize, { passive: true });
return () => {
window.removeEventListener('resize', handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
renderChart('update');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [percent]);
useEffect(() => {
return () => {
cancelAnimationFrame(timerRef.current);
};
}, []);
return (
<div
ref={rootRef}
style={{
transform: `scale(${radio})`,
}}
>
<div
ref={(n) => {
this.root = n;
style={{
width: height,
height,
overflow: 'hidden',
}}
>
<canvas ref={nodeRef} width={height * 2} height={height * 2} />
</div>
<div
style={{
transform: `scale(${radio})`,
width: height,
}}
>
<div
style={{
width: height,
height,
overflow: 'hidden',
}}
>
<canvas
ref={(n) => {
this.node = n;
}}
width={height * 2}
height={height * 2}
/>
</div>
<div
style={{
width: height,
}}
>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
);
}
}
</div>
);
};
export default autoHeight()(WaterWave);

40
src/pages/dashboard/analysis/utils/Yuan.tsx

@ -1,33 +1,17 @@
import React from 'react';
import { useEffect, useRef } from 'react';
import { yuan } from '../components/Charts';
/** 减少使用 dangerouslySetInnerHTML */
export default class Yuan extends React.Component<{
children: string | number;
}> {
main: HTMLSpanElement | undefined | null = null;
componentDidMount() {
this.renderToHtml();
}
componentDidUpdate() {
this.renderToHtml();
}
/** 减少使用 dangerouslySetInnerHTML */
const Yuan: React.FC<{ children: string | number }> = ({ children }) => {
const spanRef = useRef<HTMLSpanElement>(null);
renderToHtml = () => {
const { children } = this.props;
if (this.main) {
this.main.innerHTML = yuan(children);
useEffect(() => {
if (spanRef.current) {
spanRef.current.innerHTML = yuan(children);
}
};
}, [children]);
return <span ref={spanRef} />;
};
render() {
return (
<span
ref={(ref) => {
this.main = ref;
}}
/>
);
}
}
export default Yuan;

158
src/pages/dashboard/monitor/components/Charts/WaterWave/index.tsx

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { useEffect, useRef, useState } from 'react';
import autoHeight from '../autoHeight';
/* eslint no-return-assign: 0 */
@ -12,57 +12,25 @@ export type WaterWaveProps = {
percent: number;
style?: React.CSSProperties;
};
class WaterWave extends Component<WaterWaveProps> {
state = {
radio: 1,
};
timer: number = 0;
root: HTMLDivElement | undefined | null = null;
node: HTMLCanvasElement | undefined | null = null;
componentDidMount() {
this.renderChart();
this.resize();
window.addEventListener(
'resize',
() => {
requestAnimationFrame(() => this.resize());
},
{
passive: true,
},
);
}
componentDidUpdate(props: WaterWaveProps) {
const { percent } = this.props;
if (props.percent !== percent) {
// 不加这个会造成绘制缓慢
this.renderChart('update');
}
}
componentWillUnmount() {
cancelAnimationFrame(this.timer);
if (this.node) {
this.node.innerHTML = '';
}
window.removeEventListener('resize', this.resize);
}
resize = () => {
if (this.root) {
const { height = 1 } = this.props;
const { offsetWidth } = this.root.parentNode as HTMLElement;
this.setState({
radio: offsetWidth < height ? offsetWidth / height : 1,
});
}
};
renderChart(type?: string) {
const { percent, color = '#1890FF' } = this.props;
const WaterWave: React.FC<WaterWaveProps> = ({
title,
color = '#1890FF',
height = 1,
percent,
}) => {
const [radio, setRadio] = useState(1);
const timerRef = useRef<number>(0);
const rootRef = useRef<HTMLDivElement>(null);
const nodeRef = useRef<HTMLCanvasElement>(null);
const renderChart = (type?: string) => {
const data = percent / 100;
cancelAnimationFrame(this.timer);
if (!this.node || (data !== 0 && !data)) {
cancelAnimationFrame(timerRef.current);
if (!nodeRef.current || (data !== 0 && !data)) {
return;
}
const canvas = this.node;
const canvas = nodeRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
@ -179,47 +147,69 @@ class WaterWave extends Component<WaterWaveProps> {
sp += 0.07;
drawSin();
}
this.timer = requestAnimationFrame(render);
timerRef.current = requestAnimationFrame(render);
};
render();
}
render() {
const { radio } = this.state;
const { percent, title, height = 1 } = this.props;
return (
};
const resize = () => {
if (rootRef.current) {
const { offsetWidth } = rootRef.current.parentNode as HTMLElement;
setRadio(offsetWidth < height ? offsetWidth / height : 1);
}
};
useEffect(() => {
renderChart();
resize();
const handleResize = () => {
requestAnimationFrame(resize);
};
window.addEventListener('resize', handleResize, { passive: true });
return () => {
window.removeEventListener('resize', handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
renderChart('update');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [percent]);
useEffect(() => {
return () => {
cancelAnimationFrame(timerRef.current);
};
}, []);
return (
<div
ref={rootRef}
style={{
transform: `scale(${radio})`,
}}
>
<div
ref={(n) => {
this.root = n;
style={{
width: height,
height,
overflow: 'hidden',
}}
>
<canvas ref={nodeRef} width={height * 2} height={height * 2} />
</div>
<div
style={{
transform: `scale(${radio})`,
width: height,
}}
>
<div
style={{
width: height,
height,
overflow: 'hidden',
}}
>
<canvas
ref={(n) => {
this.node = n;
}}
width={height * 2}
height={height * 2}
/>
</div>
<div
style={{
width: height,
}}
>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
);
}
}
</div>
);
};
export default autoHeight()(WaterWave);

57
src/pages/dashboard/monitor/components/Charts/autoHeight.tsx

@ -1,8 +1,6 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
export type IReactComponent<P = any> =
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
export type IReactComponent<P = any> = React.ComponentClass<P> | React.FC<P>;
function computeHeight(node: HTMLDivElement) {
const { style } = node;
@ -37,42 +35,35 @@ type AutoHeightProps = {
function autoHeight() {
return <P extends AutoHeightProps>(
WrappedComponent: React.ComponentClass<P> | React.FC<P>,
): React.ComponentClass<P> => {
class AutoHeightComponent extends React.Component<P & AutoHeightProps> {
state = {
computedHeight: 0,
};
): React.FC<P & AutoHeightProps> => {
const AutoHeightComponent: React.FC<P & AutoHeightProps> = (props) => {
const [computedHeight, setComputedHeight] = useState(0);
const rootRef = useRef<HTMLDivElement>(null);
root: HTMLDivElement | null = null;
componentDidMount() {
const { height } = this.props;
if (!height && this.root) {
let h = getAutoHeight(this.root);
this.setState({ computedHeight: h });
useEffect(() => {
const { height } = props;
if (!height && rootRef.current) {
let h = getAutoHeight(rootRef.current);
setComputedHeight(h);
if (h < 1) {
h = getAutoHeight(this.root);
this.setState({ computedHeight: h });
h = getAutoHeight(rootRef.current);
setComputedHeight(h);
}
}
}
}, [props]);
const { height } = props;
const h = height || computedHeight;
handleRoot = (node: HTMLDivElement) => {
this.root = node;
};
return (
<div ref={rootRef}>
{h > 0 && <WrappedComponent {...props} height={h} />}
</div>
);
};
render() {
const { height } = this.props;
const { computedHeight } = this.state;
const h = height || computedHeight;
return (
<div ref={this.handleRoot}>
{h > 0 && <WrappedComponent {...this.props} height={h} />}
</div>
);
}
}
return AutoHeightComponent;
};
}
export default autoHeight;

267
src/pages/dashboard/monitor/components/Map/index.tsx

@ -1,6 +1,6 @@
import { PageLoading } from '@ant-design/pro-components';
import { HeatmapLayer, MapboxScene, PointLayer } from '@antv/l7-react';
import * as React from 'react';
import { useEffect, useState } from 'react';
const colors = [
'#eff3ff',
@ -11,143 +11,140 @@ const colors = [
'#2171b5',
'#084594',
];
export default class MonitorMap extends React.Component {
state = {
data: null,
grid: null,
loading: false,
};
public async componentDidMount() {
const [geoData, gridData] = await Promise.all([
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/c5dba875-b6ea-4e88-b778-66a862906c93.json',
).then((d) => d.json()),
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/8990e8b4-c58e-419b-afb9-8ea3daff2dd1.json',
).then((d) => d.json()),
]);
this.setState({
data: geoData,
grid: gridData,
loading: true,
});
}
export default function MonitorMap() {
const [data, setData] = useState<Record<string, unknown>[] | null>(null);
const [grid, setGrid] = useState<Record<string, unknown>[] | null>(null);
const [loading, setLoading] = useState(false);
public render() {
const { data, grid, loading } = this.state;
return loading === false ? (
<PageLoading />
) : (
<MapboxScene
map={{
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'blank',
zoom: 1,
}}
style={{
position: 'relative',
width: '100%',
height: '452px',
}}
>
{grid && (
<HeatmapLayer
key="1"
source={{
data: grid,
transforms: [
{
type: 'hexagon',
size: 800000,
field: 'capacity',
method: 'sum',
},
],
}}
color={{
values: '#ddd',
}}
shape={{
values: 'hexagon',
}}
style={{
coverage: 0.7,
opacity: 0.8,
}}
/>
)}
{data && [
<PointLayer
key="2"
options={{
autoFit: true,
}}
source={{
data,
}}
scale={{
values: {
color: {
field: 'cum_conf',
type: 'quantile',
},
size: {
field: 'cum_conf',
type: 'log',
},
useEffect(() => {
async function fetchData() {
const [geoData, gridData] = await Promise.all([
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/c5dba875-b6ea-4e88-b778-66a862906c93.json',
).then((d) => d.json()),
fetch(
'https://gw.alipayobjects.com/os/bmw-prod/8990e8b4-c58e-419b-afb9-8ea3daff2dd1.json',
).then((d) => d.json()),
]);
setData(geoData);
setGrid(gridData);
setLoading(true);
}
fetchData();
}, []);
return loading === false ? (
<PageLoading />
) : (
<MapboxScene
map={{
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'blank',
zoom: 1,
}}
style={{
position: 'relative',
width: '100%',
height: '452px',
}}
>
{grid && (
<HeatmapLayer
key="1"
source={{
data: grid,
transforms: [
{
type: 'hexagon',
size: 800000,
field: 'capacity',
method: 'sum',
},
}}
color={{
field: 'cum_conf',
values: colors,
}}
shape={{
values: 'circle',
}}
active={{
option: {
color: '#0c2c84',
],
}}
color={{
values: '#ddd',
}}
shape={{
values: 'hexagon',
}}
style={{
coverage: 0.7,
opacity: 0.8,
}}
/>
)}
{data && [
<PointLayer
key="2"
options={{
autoFit: true,
}}
source={{
data,
}}
scale={{
values: {
color: {
field: 'cum_conf',
type: 'quantile',
},
}}
size={{
field: 'cum_conf',
values: [0, 30],
}}
style={{
opacity: 0.8,
}}
/>,
<PointLayer
key="5"
source={{
data,
}}
color={{
values: '#fff',
}}
shape={{
field: 'Short_Name_ZH',
values: 'text',
}}
filter={{
field: 'cum_conf',
values: (v) => {
return v > 2000;
size: {
field: 'cum_conf',
type: 'log',
},
}}
size={{
values: 12,
}}
style={{
opacity: 1,
strokeOpacity: 1,
strokeWidth: 0,
}}
/>,
]}
</MapboxScene>
);
}
},
}}
color={{
field: 'cum_conf',
values: colors,
}}
shape={{
values: 'circle',
}}
active={{
option: {
color: '#0c2c84',
},
}}
size={{
field: 'cum_conf',
values: [0, 30],
}}
style={{
opacity: 0.8,
}}
/>,
<PointLayer
key="5"
source={{
data,
}}
color={{
values: '#fff',
}}
shape={{
field: 'Short_Name_ZH',
values: 'text',
}}
filter={{
field: 'cum_conf',
values: (v) => {
return v > 2000;
},
}}
size={{
values: 12,
}}
style={{
opacity: 1,
strokeOpacity: 1,
strokeWidth: 0,
}}
/>,
]}
</MapboxScene>
);
}

Loading…
Cancel
Save