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'; import autoHeight from '../../../../monitor/components/Charts/autoHeight';
/* eslint no-return-assign: 0 */ /* eslint no-return-assign: 0 */
@ -12,57 +12,25 @@ export type WaterWaveProps = {
percent: number; percent: number;
style?: React.CSSProperties; style?: React.CSSProperties;
}; };
class WaterWave extends Component<WaterWaveProps> {
state = { const WaterWave: React.FC<WaterWaveProps> = ({
radio: 1, title,
}; color = '#1890FF',
timer: number = 0; height = 1,
root: HTMLDivElement | undefined | null = null; percent,
node: HTMLCanvasElement | undefined | null = null; }) => {
componentDidMount() { const [radio, setRadio] = useState(1);
this.renderChart(); const timerRef = useRef<number>(0);
this.resize(); const rootRef = useRef<HTMLDivElement>(null);
window.addEventListener( const nodeRef = useRef<HTMLCanvasElement>(null);
'resize',
() => { const renderChart = (type?: string) => {
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 data = percent / 100; const data = percent / 100;
cancelAnimationFrame(this.timer); cancelAnimationFrame(timerRef.current);
if (!this.node || (data !== 0 && !data)) { if (!nodeRef.current || (data !== 0 && !data)) {
return; return;
} }
const canvas = this.node; const canvas = nodeRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) { if (!ctx) {
return; return;
@ -179,47 +147,69 @@ class WaterWave extends Component<WaterWaveProps> {
sp += 0.07; sp += 0.07;
drawSin(); drawSin();
} }
this.timer = requestAnimationFrame(render); timerRef.current = requestAnimationFrame(render);
}; };
render(); render();
} };
render() {
const { radio } = this.state; const resize = () => {
const { percent, title, height = 1 } = this.props; if (rootRef.current) {
return ( 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 <div
ref={(n) => { style={{
this.root = n; width: height,
height,
overflow: 'hidden',
}} }}
>
<canvas ref={nodeRef} width={height * 2} height={height * 2} />
</div>
<div
style={{ style={{
transform: `scale(${radio})`, width: height,
}} }}
> >
<div {title && <span>{title}</span>}
style={{ <h4>{percent}%</h4>
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>
</div> </div>
); </div>
} );
} };
export default autoHeight()(WaterWave); 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'; 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() { /** 减少使用 dangerouslySetInnerHTML */
this.renderToHtml(); const Yuan: React.FC<{ children: string | number }> = ({ children }) => {
} const spanRef = useRef<HTMLSpanElement>(null);
renderToHtml = () => { useEffect(() => {
const { children } = this.props; if (spanRef.current) {
if (this.main) { spanRef.current.innerHTML = yuan(children);
this.main.innerHTML = yuan(children);
} }
}; }, [children]);
return <span ref={spanRef} />;
};
render() { export default Yuan;
return (
<span
ref={(ref) => {
this.main = ref;
}}
/>
);
}
}

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'; import autoHeight from '../autoHeight';
/* eslint no-return-assign: 0 */ /* eslint no-return-assign: 0 */
@ -12,57 +12,25 @@ export type WaterWaveProps = {
percent: number; percent: number;
style?: React.CSSProperties; style?: React.CSSProperties;
}; };
class WaterWave extends Component<WaterWaveProps> {
state = { const WaterWave: React.FC<WaterWaveProps> = ({
radio: 1, title,
}; color = '#1890FF',
timer: number = 0; height = 1,
root: HTMLDivElement | undefined | null = null; percent,
node: HTMLCanvasElement | undefined | null = null; }) => {
componentDidMount() { const [radio, setRadio] = useState(1);
this.renderChart(); const timerRef = useRef<number>(0);
this.resize(); const rootRef = useRef<HTMLDivElement>(null);
window.addEventListener( const nodeRef = useRef<HTMLCanvasElement>(null);
'resize',
() => { const renderChart = (type?: string) => {
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 data = percent / 100; const data = percent / 100;
cancelAnimationFrame(this.timer); cancelAnimationFrame(timerRef.current);
if (!this.node || (data !== 0 && !data)) { if (!nodeRef.current || (data !== 0 && !data)) {
return; return;
} }
const canvas = this.node; const canvas = nodeRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) { if (!ctx) {
return; return;
@ -179,47 +147,69 @@ class WaterWave extends Component<WaterWaveProps> {
sp += 0.07; sp += 0.07;
drawSin(); drawSin();
} }
this.timer = requestAnimationFrame(render); timerRef.current = requestAnimationFrame(render);
}; };
render(); render();
} };
render() {
const { radio } = this.state; const resize = () => {
const { percent, title, height = 1 } = this.props; if (rootRef.current) {
return ( 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 <div
ref={(n) => { style={{
this.root = n; width: height,
height,
overflow: 'hidden',
}} }}
>
<canvas ref={nodeRef} width={height * 2} height={height * 2} />
</div>
<div
style={{ style={{
transform: `scale(${radio})`, width: height,
}} }}
> >
<div {title && <span>{title}</span>}
style={{ <h4>{percent}%</h4>
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>
</div> </div>
); </div>
} );
} };
export default autoHeight()(WaterWave); 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> = export type IReactComponent<P = any> = React.ComponentClass<P> | React.FC<P>;
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
function computeHeight(node: HTMLDivElement) { function computeHeight(node: HTMLDivElement) {
const { style } = node; const { style } = node;
@ -37,42 +35,35 @@ type AutoHeightProps = {
function autoHeight() { function autoHeight() {
return <P extends AutoHeightProps>( return <P extends AutoHeightProps>(
WrappedComponent: React.ComponentClass<P> | React.FC<P>, WrappedComponent: React.ComponentClass<P> | React.FC<P>,
): React.ComponentClass<P> => { ): React.FC<P & AutoHeightProps> => {
class AutoHeightComponent extends React.Component<P & AutoHeightProps> { const AutoHeightComponent: React.FC<P & AutoHeightProps> = (props) => {
state = { const [computedHeight, setComputedHeight] = useState(0);
computedHeight: 0, const rootRef = useRef<HTMLDivElement>(null);
};
root: HTMLDivElement | null = null; useEffect(() => {
const { height } = props;
componentDidMount() { if (!height && rootRef.current) {
const { height } = this.props; let h = getAutoHeight(rootRef.current);
if (!height && this.root) { setComputedHeight(h);
let h = getAutoHeight(this.root);
this.setState({ computedHeight: h });
if (h < 1) { if (h < 1) {
h = getAutoHeight(this.root); h = getAutoHeight(rootRef.current);
this.setState({ computedHeight: h }); setComputedHeight(h);
} }
} }
} }, [props]);
const { height } = props;
const h = height || computedHeight;
handleRoot = (node: HTMLDivElement) => { return (
this.root = node; <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; return AutoHeightComponent;
}; };
} }
export default autoHeight; export default autoHeight;

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

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