将应用主色调从 '#BBF246' 更改为 '#87CEEB'(天空蓝),并更新所有相关组件和页面中的颜色引用。同时为多个页面添加统一的渐变背景,提升视觉效果和用户体验。新增压力分析模态框组件,并优化压力计组件的交互与显示逻辑。更新应用图标和启动图资源。
164 lines
5.0 KiB
TypeScript
164 lines
5.0 KiB
TypeScript
import { Colors } from '@/constants/Colors';
|
|
import React, { useMemo } from 'react';
|
|
import { StyleSheet, View } from 'react-native';
|
|
import Svg, * as SvgLib from 'react-native-svg';
|
|
|
|
export type RadarCategory = { key: string; label: string };
|
|
|
|
export type RadarChartSize = 'small' | 'medium' | 'large' | number;
|
|
|
|
export type RadarChartProps = {
|
|
categories: RadarCategory[];
|
|
values: number[]; // 与 categories 一一对应
|
|
maxValue?: number; // 默认 5
|
|
size?: RadarChartSize; // 组件宽高,默认 medium
|
|
levels?: number; // 网格层数,默认 5
|
|
showGrid?: boolean;
|
|
showLabels?: boolean; // 是否显示指标文字
|
|
};
|
|
|
|
// 预设尺寸配置
|
|
const SIZE_CONFIG = {
|
|
small: { size: 116, fontSize: 8, labelOffset: -4, radiusRatio: 0.35 },
|
|
medium: { size: 200, fontSize: 11, labelOffset: 15, radiusRatio: 0.38 },
|
|
large: { size: 280, fontSize: 13, labelOffset: 18, radiusRatio: 0.4 },
|
|
};
|
|
|
|
export function RadarChart({
|
|
categories,
|
|
values,
|
|
maxValue = 5,
|
|
size = 'medium',
|
|
levels = 5,
|
|
showGrid = true,
|
|
showLabels = true
|
|
}: RadarChartProps) {
|
|
// 解析尺寸配置
|
|
const config = useMemo(() => {
|
|
if (typeof size === 'number') {
|
|
// 自定义尺寸
|
|
const baseSize = Math.max(80, Math.min(400, size));
|
|
let fontSize = 11;
|
|
let labelOffset = 15;
|
|
let radiusRatio = 0.38;
|
|
|
|
// 根据尺寸自适应调整
|
|
if (baseSize < 140) {
|
|
fontSize = 9;
|
|
labelOffset = 12;
|
|
radiusRatio = 0.35;
|
|
} else if (baseSize > 240) {
|
|
fontSize = 13;
|
|
labelOffset = 18;
|
|
radiusRatio = 0.4;
|
|
}
|
|
|
|
return { size: baseSize, fontSize, labelOffset, radiusRatio };
|
|
}
|
|
|
|
// 预设尺寸
|
|
return SIZE_CONFIG[size] || SIZE_CONFIG.medium;
|
|
}, [size]);
|
|
|
|
const { size: actualSize, fontSize, labelOffset, radiusRatio } = config;
|
|
const radius = actualSize * radiusRatio;
|
|
const cx = actualSize / 2;
|
|
const cy = actualSize / 2;
|
|
const count = categories.length;
|
|
|
|
const points = useMemo(() => {
|
|
return categories.map((_, i) => {
|
|
const angle = (Math.PI * 2 * i) / count - Math.PI / 2; // 顶部起点
|
|
return {
|
|
angle,
|
|
x: (v: number) => cx + Math.cos(angle) * v,
|
|
y: (v: number) => cy + Math.sin(angle) * v
|
|
};
|
|
});
|
|
}, [categories, count, cx, cy]);
|
|
|
|
const valuePath = useMemo(() => {
|
|
const rValues = values.map((v) => Math.max(0, Math.min(maxValue, v)) / maxValue * radius);
|
|
const d = rValues
|
|
.map((rv, i) => `${i === 0 ? 'M' : 'L'} ${points[i].x(rv)} ${points[i].y(rv)}`)
|
|
.join(' ');
|
|
return `${d} Z`;
|
|
}, [values, maxValue, radius, points]);
|
|
|
|
const gridPolygons = useMemo(() => {
|
|
const polys: string[] = [];
|
|
for (let l = 1; l <= levels; l++) {
|
|
const r = (radius / levels) * l;
|
|
const p = points.map((p) => `${p.x(r)},${p.y(r)}`).join(' ');
|
|
polys.push(p);
|
|
}
|
|
return polys;
|
|
}, [levels, radius, points]);
|
|
|
|
// 检查是否有足够的空间显示文字
|
|
const hasEnoughSpace = actualSize >= 100;
|
|
|
|
return (
|
|
<View style={{ width: actualSize, height: actualSize }}>
|
|
<Svg width={actualSize} height={actualSize}>
|
|
<SvgLib.Defs>
|
|
<SvgLib.LinearGradient id="radarFill" x1="0" y1="0" x2="1" y2="1">
|
|
<SvgLib.Stop offset="0%" stopColor={Colors.light.accentGreen} stopOpacity={0.32} />
|
|
<SvgLib.Stop offset="100%" stopColor="#59C6FF" stopOpacity={0.28} />
|
|
</SvgLib.LinearGradient>
|
|
</SvgLib.Defs>
|
|
|
|
{showGrid && (
|
|
<SvgLib.G>
|
|
{gridPolygons.map((pointsStr, idx) => (
|
|
<SvgLib.Polygon
|
|
key={`grid-${idx}`}
|
|
points={pointsStr}
|
|
fill="none"
|
|
stroke="rgba(25,33,38,0.12)"
|
|
strokeWidth={1}
|
|
/>
|
|
))}
|
|
{points.map((p, i) => (
|
|
<SvgLib.Line
|
|
key={`axis-${i}`}
|
|
x1={cx}
|
|
y1={cy}
|
|
x2={p.x(radius)}
|
|
y2={p.y(radius)}
|
|
stroke="rgba(25,33,38,0.12)"
|
|
strokeWidth={1}
|
|
/>
|
|
))}
|
|
</SvgLib.G>
|
|
)}
|
|
|
|
{showLabels && hasEnoughSpace && categories.map((c, i) => {
|
|
const r = radius + labelOffset;
|
|
const x = points[i].x(r);
|
|
const y = points[i].y(r);
|
|
const anchor = Math.cos(points[i].angle) > 0.2 ? 'start' : Math.cos(points[i].angle) < -0.2 ? 'end' : 'middle';
|
|
const dy = Math.sin(points[i].angle) > 0.6 ? 10 : Math.sin(points[i].angle) < -0.6 ? -4 : 4;
|
|
|
|
return (
|
|
<SvgLib.Text
|
|
key={`label-${c.key}`}
|
|
x={x}
|
|
y={y + dy}
|
|
fontSize={fontSize}
|
|
fill="#192126"
|
|
textAnchor={anchor}
|
|
>
|
|
{c.label}
|
|
</SvgLib.Text>
|
|
);
|
|
})}
|
|
|
|
<SvgLib.Path d={valuePath} fill="url(#radarFill)" stroke="#A8DB3D" strokeWidth={2} />
|
|
<SvgLib.Circle cx={cx} cy={cy} r={3} fill="#A8DB3D" />
|
|
</Svg>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({}); |