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>
欢迎交流关于睡眠质量图的各种实现方式~
不经常在线,有问题可在微信公众号或者掘金社区私信留言
更多内容可关注
我的公众号悬空八只脚