React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3

React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3

FBI WARNING:
The complete demo will be posted at the end of the series, so no need to worry.

还剩下最后一步:让睡眠阶段的区域形状上更加贴合边线,颜色上做些渐变。

首先想到的是切图,其次是自己去绘制这个区域。根据区域和区域左右两条线的样式,如果把线在下区域在上看做上升趋势up,反之则是下降趋势down,这样让UI切了四张图。

svg格式的图片可以直接在画布上展示。

typescript 复制代码
import SvgSleepUpUp from '../../svg/sleep_upup.svg';
import SvgSleepUpDown from '../../svg/sleep_updown.svg';
import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
import SvgSleepDownUp from '../../svg/sleep_downup.svg';

根据上面提到的up-down来定义一个映射表

typescript 复制代码
// 图片映射表
const SVG_MAP = {
  [`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
  [`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
  [`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
  [`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
};

还是通过在calcPointData的时候,替代原先Rect矩形区域即可,当然需要计算下矩形区域左右两条边线的样式来确定用哪哪个图片

typescript 复制代码
if (index - 1 >= 0) {
  const prevPoint = points[index - 1];
  const isSameStage = point.stage === prevPoint.stage;
  areaData.areaWidth += point.x - prevPoint.x;
  let deltaY1 = 0;
  let deltaY2 = 0;
  if (!isSameStage) {
    if (point.stage > prevPoint.stage) {
      areaData.areaRightUpDown = UpDownEnum.UP;
      deltaY1 = -AREA_HEIGHT;
    } else {
      areaData.areaRightUpDown = UpDownEnum.DOWN;
      deltaY2 = -AREA_HEIGHT;
    }
    areaData.areaEndIndex = index;
    console.log('areaData.areaEndIndex = ' + index);
    const SvgComponent =
      SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];
    const beginIndex = areaData.areaBeginIndex;
    const endIndex = areaData.areaEndIndex;
    areaDataList.push(areaData);
    // 输出
    svgsTemp.push(
      <G
        key={`icon1-${index}`}
        transform={`translate(${areaData.aeraX}, ${areaData.aeraY})`}>
        <SvgComponent
          key={`connector-${areaData.areaIndex}`}
          x={areaData.aeraX}
          y={areaData.aeraY}
          width={areaData.areaWidth}
          height={areaData.areaHeight}
          fill={`url(#gradient${areaData.areaStage})`}
          preserveAspectRatio="none" // 禁用比例保持
          transform={`translate(0 0)`} // 消除SVG内置偏移
        />
      </G>,

效果上还有点生硬,记得svg里可以设置允许缩放拉伸的范围。svg图片添加渐变色试了下似乎无效,需要后面再找找办法。后续正式开发的时候根据标注调整下,再细化下切图样式还有较大提升空间。正式开发到这个功能要到五一后了,届时再来交流下效果,先附上完整的demo代码与几个svg图片源码。源码里还有个滑块与指针可以拖拽选选择,会返回选中的睡眠阶段的开始时间点与结束时间点,用于显示当前选中的睡眠阶段信息。这块和绘制关系不大,看下源码即可了解。

完整源码

typescript 复制代码
// 基于react-native-svg实现的绘制睡眠阶段图标
import {color} from 'echarts';
import React, {
  JSX,
  useMemo,
  useState,
  useCallback,
  useEffect,
  useRef,
} from 'react';
import {View, Dimensions, Text, StyleSheet, PanResponder, InteractionManager} from 'react-native';
import Svg, {
  G,
  Rect,
  LinearGradient,
  Defs,
  Filter,
  FeDropShadow,
  Circle,
  Line,
  Stop,
  Path,
  Image,
  Polygon,
} from 'react-native-svg';
import SvgSleepUpUp from '../../svg/sleep_upup.svg';
import SvgSleepUpDown from '../../svg/sleep_updown.svg';
import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
import SvgSleepDownUp from '../../svg/sleep_downup.svg';
// 组件目前默认按照屏幕宽度为基准进行布局
const {width: SCREEN_WIDTH} = Dimensions.get('window');
const SHOW_DATA_POINT = false; // 是否在图标展示数据点(调试阶段可以更清晰核对数据与图是否一致)
const MARGIN = 24; // 左右间隙,用于支持底部指针图标的左右拖拽
const POINT_RADIUS = 4; // 数据点的弧度
const AREA_HEIGHT = 30; // 阶段形状的高度
const CHART_HEIGHT = AREA_HEIGHT * 9; // 图表的整体高度,可以调整
// const AREA_LINE_HEIGHT = 20; // 阶段形状线的高度
// 自定义形状配置字典
const STAGE_CONFIG = {
  0: {
    gradient: ['#8314f3', '#3800FF'], // 渐变色设置
    useColorTransform: true, // 是否使用颜色变换
    transformX1: '0%',
    transformX2: '0%',
    transformY1: '0%',
    transformY2: '100%',
    shadow: '#3800FF', // 阴影颜色
    color: '#3800FF', // 默认颜色
    label: '深睡眠', // 文字标签
  },
  1: {
    gradient: ['#d248b0', '#ba2eec'],
    useColorTransform: true,
    transformX1: '0%',
    transformX2: '0%',
    transformY1: '0%',
    transformY2: '100%',
    color: '#ba2eec',
    shadow: '#ba2eec',
    label: '浅睡眠',
  },
  2: {
    gradient: ['#e05891', '#f86d5a'],
    useColorTransform: true,
    transformX1: '0%',
    transformX2: '0%',
    transformY1: '0%',
    transformY2: '100%',
    shadow: '#f86d5a',
    color: '#f86d5a',
    label: '快速眼动',
  },
  3: {
    gradient: ['#fb8b44', '#fcbb29'],
    useColorTransform: true,
    transformX1: '0%',
    transformX2: '0%',
    transformY1: '0%',
    transformY2: '100%',
    shadow: '#fcbb29',
    color: '#fcbb29',
    label: '清醒',
  },
};
// 示例数据
const sleepData: SleepStageModel[] = [
  {time: '0:00', stage: 3},
  {time: '5:00', stage: 3},
  {time: '10:00', stage: 1},
  {time: '15:00', stage: 1},
  {time: '20:00', stage: 3},
  {time: '25:00', stage: 0},
  {time: '30:00', stage: 0},
  {time: '35:00', stage: 0},
  {time: '40:00', stage: 0},
  {time: '45:00', stage: 1},
  {time: '50:00', stage: 1},
  {time: '55:00', stage: 3},
  {time: '60:00', stage: 3},
  {time: '65:00', stage: 2},
  {time: '70:00', stage: 2},
  {time: '75:00', stage: 1},
  {time: '80:00', stage: 3},
  {time: '85:00', stage: 2},
  {time: '0:00', stage: 3},
  {time: '5:00', stage: 3},
  {time: '10:00', stage: 1},
  {time: '15:00', stage: 1},
  {time: '20:00', stage: 3},
  {time: '25:00', stage: 0},
  // {time: '30:00', stage: 0},
  // {time: '35:00', stage: 0},
  // {time: '40:00', stage: 0},
  // {time: '45:00', stage: 1},
  // {time: '50:00', stage: 1},
  // {time: '55:00', stage: 3},
  // {time: '60:00', stage: 3},
  // {time: '65:00', stage: 2},
  // {time: '70:00', stage: 2},
  // {time: '75:00', stage: 1},
  // {time: '80:00', stage: 3},
  // {time: '85:00', stage: 2},
  // ...更多数据
];
enum SleepStageEnum {
  Deep = 0, // 0 深睡眠
  Light, // 1 浅睡眠
  REM, // 2 快速眼动
  AWAKE, // 3 清醒
}
enum UpDownEnum {
  NONE = 'none', // none 未设置
  UP = 'up', // up 上升
  DOWN = 'down', // down 下降
}
// 图片映射表
const SVG_MAP = {
  [`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
  [`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
  [`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
  [`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
};
interface SleepStageModel {
  time: string; // 采集时间点
  stage: SleepStageEnum; // 睡眠阶段
  [key: string]: any; // fixme:允许任意数量的其他属性
}
interface SleepAreaData {
  areaIndex: number; // 区域索引
  aeraX: number; // 区域X坐标
  aeraY: number; // 区域Y坐标
  areaWidth: number; // 区域宽度
  areaHeight: number; // 区域宽度
  areaColor: any | null; // 区域颜色
  areaStoke: any | null; // 区域阴影
  areaLeftUpDown: UpDownEnum; // 区域左边上升下降趋势
  areaRightUpDown: UpDownEnum; // 区域左边上升下降趋势
  areaStage: number; // 睡眠阶段
  areaBeginIndex: number; // 区域开始索引
  areaEndIndex: number; // 区域结束索引
}
const SleepStageChart = ({data}: {data: SleepStageModel[]}) => {
  // 添加滑块位置状态
  const [sliderPosition, setSliderPosition] = useState(MARGIN);
  // 滑块是否在拖拽的判定
  const [isDragging, setIsDragging] = useState(false);
  // 睡眠区域数据,用于计算这段睡眠的详细数据
  let areaDataList: SleepAreaData[] = [];
  // 添加 ref 存储实时位置(针对目前在android设备上滑块拖动不如ios上丝滑)
  const sliderPositionRef = useRef(MARGIN);
  // 添加节流标识(针对目前在android设备上滑块拖动不如ios上丝滑)
  const lastUpdate = useRef(Date.now());
  const THROTTLE_DELAY = 16; // 约 60fps
  const panResponder = useMemo(
    () =>
      PanResponder.create({
        onStartShouldSetPanResponder: () => true,
        onMoveShouldSetPanResponder: () => true,
        onPanResponderGrant: () => {
          setIsDragging(true);
        },
        onPanResponderMove: (_, gestureState) => {
          // const newX = Math.min(SCREEN_WIDTH - MARGIN, gestureState.moveX);
          // const newPosition = Math.max(MARGIN, newX);
          // setSliderPosition(newPosition);
          // // 区域计算
          // for (let i = 0; i < areaDataList.length - 1; i++) {
          //   if (
          //     points[areaDataList[i].areaBeginIndex].x <= newX &&
          //     points[areaDataList[i].areaEndIndex].x >= newX
          //   ) {
          //     setCurrentAreaIndex(i);
          //     break;
          //   }
          // }
          const now = Date.now();
          const newX = Math.min(SCREEN_WIDTH - MARGIN, gestureState.moveX);
          const newPosition = Math.max(MARGIN, newX);

          // 使用 ref 存储实时位置
          sliderPositionRef.current = newPosition;

          // 添加节流逻辑
          if (now - lastUpdate.current >= THROTTLE_DELAY) {
            updateSliderPosition(newPosition);
            lastUpdate.current = now;

            // 区域计算
            for (let i = 0; i < areaDataList.length - 1; i++) {
              if (
                points[areaDataList[i].areaBeginIndex].x <= newX &&
                points[areaDataList[i].areaEndIndex].x >= newX
              ) {
                setCurrentAreaIndex(i);
                break;
              }
            }
          }
        },
        onPanResponderRelease: () => {
          setIsDragging(false);
                    // 结束时确保显示最终位置
                    updateSliderPosition(sliderPositionRef.current);
        },
      }),
    [],
  );
  // requestAnimationFrame优化视图更新(针对目前在android设备上滑块拖动不如ios上丝滑)
  const updateSliderPosition = (position: number) => {
    requestAnimationFrame(() => {
      setSliderPosition(position);
    });
  };
  // 添加当前选中区域的状态
  const [currentAreaIndex, setCurrentAreaIndex] = useState<number | null>(null);
  const points = useMemo(
    () =>
      data.map((item, index) => ({
        x: (index * (SCREEN_WIDTH - MARGIN * 2)) / (data.length - 1) + MARGIN,
        y: CHART_HEIGHT - (item.stage * 2 + 1) * AREA_HEIGHT - AREA_HEIGHT,
        stage: item.stage,
        data: item,
      })),
    [data],
  );
  // 添加点击处理函数
  const handleAreaPress = useCallback(
    (beginIndex: number, endIndex: number) => {
      InteractionManager.runAfterInteractions(() => {
        console.log('点击区域 - 阶段数据:', beginIndex, '-', endIndex);
              // 这里可以添加更多处理逻辑...
      });
    },
    [],
  ); // 空依赖数组,因为函数不依赖任何外部变量
  const initAreaData = () => {
    let areaData: SleepAreaData = {
      areaIndex: -1,
      aeraX: -1,
      aeraY: -1,
      areaWidth: 0,
      areaHeight: AREA_HEIGHT,
      areaColor: null,
      areaStoke: null,
      areaLeftUpDown: UpDownEnum.NONE,
      areaRightUpDown: UpDownEnum.NONE,
      areaStage: 0,
      areaBeginIndex: 0,
      areaEndIndex: 0,
    };
    return areaData;
  };
  // 修改 useEffect,移除自动吸附逻辑
  useEffect(() => {
    console.log('选中了' + currentAreaIndex);
  }, [currentAreaIndex]);
  // 添加绘制参考线的函数
  const renderReferenceLines = () => {
    const lines: JSX.Element[] = [];
    // 计算需要绘制的虚线数量
    const lineCount = Math.floor(CHART_HEIGHT / AREA_HEIGHT);
    for (let i = 0; i <= lineCount; i++) {
      const y = CHART_HEIGHT - i * AREA_HEIGHT;
      lines.push(
        <Line
          key={`reference-line-${i}`}
          x1={MARGIN}
          y1={y}
          x2={SCREEN_WIDTH - MARGIN}
          y2={y}
          stroke="red"
          strokeWidth="1"
          strokeDasharray="4 4"
        />,
      );
    }
    // 添加两条y轴边线
    lines.push(
      <Line
        key={`reference-yline-0`}
        x1={MARGIN}
        y1={0}
        x2={MARGIN}
        y2={CHART_HEIGHT}
        stroke="red"
        strokeWidth="1"
        strokeDasharray="4 4"
      />,
    );
    lines.push(
      <Line
        key={`reference-yline-1`}
        x1={SCREEN_WIDTH - MARGIN}
        y1={0}
        x2={SCREEN_WIDTH - MARGIN}
        y2={CHART_HEIGHT}
        stroke="red"
        strokeWidth="1"
        strokeDasharray="4 4"
      />,
    );
    return lines;
  };
  const calcPointData = () => {
    let areaData = initAreaData();
    let svgsTemp: JSX.Element[] = [];
    points.map((point, index) => {
      if (index == 0) {
        areaData.areaIndex = index;
        areaData.aeraX = point.x;
        areaData.aeraY = point.y;
        areaData.areaWidth = 0;
        areaData.areaColor = `url(#gradient${point.stage})`;
        areaData.areaStoke = STAGE_CONFIG[point.stage].shadow;
        areaData.areaLeftUpDown = UpDownEnum.UP;
        areaData.areaStage = point.stage;
        areaData.areaBeginIndex = index;
      }
      if (index - 1 >= 0) {
        const prevPoint = points[index - 1];
        const isSameStage = point.stage === prevPoint.stage;
        areaData.areaWidth += point.x - prevPoint.x;
        let deltaY1 = 0;
        let deltaY2 = 0;
        if (!isSameStage) {
          if (point.stage > prevPoint.stage) {
            areaData.areaRightUpDown = UpDownEnum.UP;
            deltaY1 = -AREA_HEIGHT;
          } else {
            areaData.areaRightUpDown = UpDownEnum.DOWN;
            deltaY2 = -AREA_HEIGHT;
          }
          areaData.areaEndIndex = index;
          console.log('areaData.areaEndIndex = ' + index);
          const SvgComponent =
            SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];
          const beginIndex = areaData.areaBeginIndex;
          const endIndex = areaData.areaEndIndex;
          areaDataList.push(areaData);
          // 输出
          svgsTemp.push(
            <G
              key={`icon1-${index}`}
              transform={`translate(${areaData.aeraX}, ${areaData.aeraY})`}>
              <SvgComponent
                key={`connector-${areaData.areaIndex}`}
                x={areaData.aeraX}
                y={areaData.aeraY}
                width={areaData.areaWidth}
                height={areaData.areaHeight}
                fill={`url(#gradient${areaData.areaStage})`}
                preserveAspectRatio="none" // 禁用比例保持
                transform={`translate(0 0)`} // 消除SVG内置偏移
              />
            </G>,
            // <Rect
            //   key={`connector-${areaData.areaIndex}`}
            //   x={areaData.aeraX}
            //   y={areaData.aeraY}
            //   width={areaData.areaWidth}
            //   height={areaData.areaHeight}
            //   stroke={areaData.areaStoke}
            //   rx={5}
            //   ry={5}
            //   fill={`url(#gradient${areaData.areaStage})`}
            // />,
          );
          {
            /* 添加透明的可点击背景 */
          }
          svgsTemp.push(
            <Rect
              key={`touchable-${index}`}
              x={areaData.aeraX}
              y={0} // 使背景从顶部开始
              width={areaData.areaWidth}
              height={CHART_HEIGHT + MARGIN} // 使用整个画布的高度
              fill="transparent"
              onPress={() => handleAreaPress(beginIndex, endIndex)}
              // 可选:添加点击反馈效果
              // opacity={selectedArea === areaData.areaIndex ? 0.1 : 0}
            />,
          );
          // 出一条线
          if (index != data.length - 1) {
            svgsTemp.push(
              <Line
                key={`line-${index}`}
                x1={point.x}
                y1={point.y - deltaY1}
                x2={point.x}
                y2={areaData.aeraY - deltaY2}
                stroke="#999"
                strokeWidth="1"
                strokeDasharray="4 2"
              />,
            );
          }
          // 重新开始绘制矩形
          areaData = initAreaData();
          areaData.areaIndex = index;
          areaData.aeraX = point.x;
          areaData.aeraY = point.y;
          areaData.areaWidth = 0;
          areaData.areaColor = STAGE_CONFIG[point.stage].color;
          areaData.areaStoke = STAGE_CONFIG[point.stage].shadow;
          areaData.areaStage = point.stage;
          areaData.areaBeginIndex = index;
          if (point.stage > prevPoint.stage) {
            areaData.areaLeftUpDown = UpDownEnum.UP;
          } else {
            areaData.areaLeftUpDown = UpDownEnum.DOWN;
          }
        }
      }
      if (index == points.length - 1) {
        areaData.areaRightUpDown = UpDownEnum.DOWN;
        areaData.areaEndIndex = index;
        const beginIndex = areaData.areaBeginIndex;
        const endIndex = areaData.areaEndIndex;
        const SvgComponent =
          SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];
        // 输出
        areaDataList.push(areaData);
        svgsTemp.push(
          <G
            key={`icon-${index}`}
            transform={`translate(${areaData.aeraX}, ${areaData.aeraY})`}>
            <SvgComponent
              key={`connector-${areaData.areaIndex}`}
              x={areaData.aeraX}
              y={areaData.aeraY}
              width={areaData.areaWidth}
              height={areaData.areaHeight}
              fill={`url(#gradient${areaData.areaStage})`}
              style={{fill: `url(#gradient${areaData.areaStage})`}}
              preserveAspectRatio="none" // 禁用比例保持
              transform={`translate(0 0)`} // 消除SVG内置偏移
            />
          </G>,
            // <Rect
            //   key={`connector-${areaData.areaIndex}`}
            //   x={areaData.aeraX}
            //   y={areaData.aeraY}
            //   width={areaData.areaWidth}
            //   height={areaData.areaHeight}
            //   stroke={areaData.areaStoke}
            //   fill={areaData.areaColor}
            //   rx={5}
            //   ry={5}
            // />,
        );
        // 添加透明的可点击背景
        svgsTemp.push(
          <Rect
            key={`touchable1-${index}`}
            x={areaData.aeraX}
            y={0} // 使背景从顶部开始
            width={areaData.areaWidth}
            height={CHART_HEIGHT + MARGIN} // 使用整个画布的高度
            fill="transparent"
            onPress={() => handleAreaPress(beginIndex, endIndex)}
            // 可选:添加点击反馈效果
            // opacity={selectedArea === areaData.areaIndex ? 0.1 : 0}
          />,
        );
      }
    });
    return svgsTemp;
  };
  return (
    <View style={styles.container}>
      {/* 日 周 月 */}
      {/* 阶段图例 */}
      <View style={styles.legend}>
        {Object.entries(STAGE_CONFIG).map(([stage, config]) => (
          <View key={stage} style={styles.legendItem}>
            <View style={[styles.legendDot, {backgroundColor: config.color}]} />
            <Text style={styles.legendText}>{config.label}</Text>
          </View>
        ))}
      </View>
      {/* 可视化图表 */}
      <Svg height={CHART_HEIGHT + 80} width={SCREEN_WIDTH}>
        {/* 添加参考线 */}
        {renderReferenceLines()}
        {/* 区域的渐变定义 */}
        <Defs>
          {Object.entries(STAGE_CONFIG).map(([stage, config]) => (
            <LinearGradient
              key={`gradient${stage}`}
              id={`gradient${stage}`}
              x1="0%"
              y1="0%"
              x2="0%"
              y2="100%">
              <Stop offset="0%" stopColor={config.gradient[0]} />
              <Stop offset="100%" stopColor={config.gradient[1]} />
            </LinearGradient>
          ))}
        </Defs>
        {/* 连接线绘制 */}
        {data.length > 1 && calcPointData()}
        {/* 数据点,在睡眠图里不表示,打开可以更清楚的观察数据(用于调试阶段) */}
        {SHOW_DATA_POINT &&
          points.map((point, index) => (
            <G key={`point-${index}`}>
              <Circle
                cx={point.x}
                cy={point.y}
                r={POINT_RADIUS}
                fill={STAGE_CONFIG[point.stage].color}
                stroke="white"
                strokeWidth={1}
              />
            </G>
          ))}
        {/* 添加指示线 */}
        <Line
          x1={sliderPosition}
          y1={0}
          x2={sliderPosition}
          y2={CHART_HEIGHT}
          stroke="#666"
          strokeWidth={1}
          strokeDasharray="4 4"
        />
        {/* 添加滑块 */}
        { <G transform={`translate(0, ${CHART_HEIGHT + 20})`}>
          {/* 滑块轨道 */}
          <Line
            x1={MARGIN}
            y1={0}
            x2={SCREEN_WIDTH - MARGIN}
            y2={0}
            stroke="#E5E5E5"
            strokeWidth={2}
          />
          {/* 滑块拖拽的圆圈 */}
          <Circle
            cx={sliderPosition}
            cy={0}
            r={10}
            fill="white"
            stroke="#666"
            strokeWidth={2}
            {...panResponder.panHandlers}
          />
        </G>}
      </Svg>
    </View>
  );
};
const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    borderRadius: 0,
    padding: 0,
    margin: 0,
    shadowColor: '#000',
    shadowOffset: {width: 0, height: 2},
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  legend: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 16,
  },
  legendItem: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  legendDot: {
    width: 12,
    height: 12,
    borderRadius: 6,
    marginRight: 4,
  },
  legendText: {
    color: '#666',
    fontSize: 12,
  },
  sliderContainer: {
    position: 'relative',
    height: 40,
    marginTop: 10,
  },
  slider: {
    position: 'absolute',
    width: 20,
    height: 20,
    borderRadius: 10,
    backgroundColor: 'white',
    borderWidth: 2,
    borderColor: '#666',
    transform: [{translateX: -10}, {translateY: -10}],
  },
});
export {SleepStageChart};
const DefaultExport = () => <SleepStageChart data={sleepData} />;
export default DefaultExport;

几个svg图片的源码。

sleep_downdown.svg

typescript 复制代码
<svg width="50" height="109" viewBox="0 0 50 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 76.4332C0 86.3743 8.05888 94.4332 18 94.4332H31.1754C37.5872 94.4332 43.5149 97.8439 46.7372 103.387L50 109L50 30.1383V30.1383C50 22.3706 43.7031 16.0737 35.9355 16.0737H16.292C8.59136 16.0737 1.98308 10.5882 0.565468 3.01919L0 0V76.4332Z" fill="url(#paint0_linear_771_1197)"/>
<defs>
<linearGradient id="paint0_linear_771_1197" x1="25" y1="94.4332" x2="25" y2="-7.03582e-05" gradientUnits="userSpaceOnUse">
<stop stop-color="#C041FC"/>
<stop offset="1" stop-color="#EC5AC4"/>
</linearGradient>
</defs>
</svg>

sleep_downup.svg

typescript 复制代码
<svg width="118" height="94" viewBox="0 0 118 94" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 76C0 85.9411 8.05887 94 18 94H100C109.941 94 118 85.9411 118 76V0L115.343 6.25499C112.834 12.1634 107.036 16 100.617 16H17.5065C11.1815 16 5.45021 12.274 2.88333 6.49333L0 0V76Z" fill="url(#paint0_linear_771_1198)"/>
<defs>
<linearGradient id="paint0_linear_771_1198" x1="59" y1="94" x2="59" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#541BFF"/>
<stop offset="1" stop-color="#A336FB"/>
</linearGradient>
</defs>
</svg>

sleep_updown.svg

typescript 复制代码
<svg width="156" height="94" viewBox="0 0 156 94" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 18C0 8.05887 8.05887 0 18 0H138C147.941 0 156 8.05888 156 18V94L151.601 86.1662C148.769 81.1224 143.435 78 137.65 78H18.5532C12.8783 78 7.62803 81.006 4.75509 85.8999L0 94V18Z" fill="url(#paint0_linear_771_1194)"/>
<defs>
<linearGradient id="paint0_linear_771_1194" x1="78" y1="0" x2="78" y2="94" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8759"/>
<stop offset="1" stop-color="#F865A6"/>
</linearGradient>
</defs>
</svg>

sleep_upup.svg

typescript 复制代码
<svg width="163" height="109" viewBox="0 0 163 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_780_1311)">
<path d="M163 76.4332C163 86.3743 154.941 94.4332 145 94.4332H20.401C11.187 94.4332 2.991 100.286 0 109V30.1383C0 22.3706 6.297 16.0737 14.065 16.0737H144.208C149.785 16.0737 154.958 13.1707 157.864 8.41155L163 0V76.4332Z" fill="url(#paint0_linear_780_1311)"/>
</g>
<defs>
<linearGradient id="paint0_linear_780_1311" x1="81.5" y1="94.4332" x2="81.5" y2="-6.75439e-05" gradientUnits="userSpaceOnUse">
<stop stop-color="#C041FC"/>
<stop offset="1" stop-color="#EC5AC4"/>
</linearGradient>
<clipPath id="clip0_780_1311">
<rect width="163" height="109" fill="white" transform="matrix(-1 0 0 1 163 0)"/>
</clipPath>
</defs>
</svg>

欢迎交流关于睡眠质量图的各种实现方式~


不经常在线,有问题可在微信公众号或者掘金社区私信留言

更多内容可关注

我的公众号悬空八只脚

相关推荐
二流小码农1 小时前
鸿蒙开发:如何更新对象数组
android·ios·harmonyos
君莫笑111113 小时前
从零到一教你在鸿蒙中实现微信分享--全流程
前端·harmonyos
天生我材必有用_吴用6 小时前
鸿蒙开发入门到进阶:从布局基础到组件实战
前端·harmonyos·arkts
坚果的博客1 天前
坚果派已适配的鸿蒙版flutter库【持续更新】
flutter·华为·开源·harmonyos
NapleC1 天前
HarmonyOS:Navigation实现导航之页面设置和路由操作
华为·harmonyos·navigation
HMSCore1 天前
HarmonyOS SDK助力鸿蒙版今日水印相机,真实地址防护再升级
harmonyos
风中飘爻1 天前
鸿蒙生态:鸿蒙生态校园行心得
华为·harmonyos
Otaku_尻男1 天前
HarmonyOS 自定义RenderNode 绘图实战
android·面试·harmonyos