
【MoodVine】Taro+react实现心情热力图
taro+react实现类似github贡献日历的心情热力图,taro小程序WXMLRT_$gwx:./base.wxml:template:27:20: Template tmpl_0_div not found.问题解决方案
心情数组每条包含三个属性
count
当天的心情,用在气泡提示里。date
日期,保存为时间戳。level
情绪状态(对应0-4五种心情)。
容器
GitHub 热力图中的小方块是 10x10 的正方形,53 行 x 7 列,间隔 3px。其它的区域大小都根据内容决定,设为auto
即可,故容器的 CSS:
.container {
display: grid;
grid-template-columns: auto repeat(53, 10px);
grid-template-rows: auto repeat(7, 10px) auto;
gap: 3px;
/* 让整个组件自适应大小,而不是撑满一行 */
width: fit-content;
/* 其它间距、字体等样式直接照抄 GitHub */
font-size: 12px;
padding: 14px;
border: solid 1px #D1D9E0;
border-radius: 0.375rem;
}
中间的方块
由于网格布局能够自动排列元素,所以直接把小方块往里塞就行。另外为了省事,本项目直接用了title
代替 GitHub 里自定义的气泡提示。
// 生成气泡提示的内容,主要就是处理英语就的复数,中文就没这破事。
function getTooltip(oneDay, date) {
const s = date.toISOString().split("T")[0];
switch (oneDay.count) {
case 0:
return `No contributions on ${s}`;
case 1:
return `1 contribution on ${s}`;
default:
return `${oneDay.count} contributions on ${s}`;
}
}
export default function ContributionCalendar(props) {
const firstDate = new Date(props.contributions[0].date);
const startRow = firstDate.getDay();
const tiles = commits.map((c, i) => {
const date = new Date(c.date);
return (
<i
className={styles.tile}
key={i}
data-level={c.level}
title={getTooltip(c, date)}
/>
);
});
// 第一格不一定是周日,此时前面会有空白,需要设置下起始行。
tiles[0] = React.cloneElement(tiles[0], {
style: { gridRow: startRow + 1 },
});
return (
<div className={styles.container}>
<div className={styles.tiles}>{tiles}</div>
</div>
);
};
CSS 方面有一点需要注意,这里的表头并未填满,月份和星期之间是有空的,如果把方块也放在一起会乱。
对于这个问题,有一个subgrid
属性专门解决它,通过设置display: grid
以及grid-template-*: subgrid
元素将划定父级网格的一个子区域,这个区域继承父级的网格,同时子元素的排布被限制在区域内,使得它们不会漏到表头里。
/* 中间的格子区域,使用子网格划定布局范围 */
.tiles {
/* 子区域范围为 2-9 行,2-55 列 */
grid-row: 2/9;
grid-column: 2/55;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
/* 按列方向依次放置方块 */
grid-auto-flow: column;
}
.tile {
display: block;
width: 10px;
height: 10px;
border-radius: 2px;
outline: 1px solid rgba(27, 35, 36, 0.06);
outline-offset: -1px;
&[data-level="0"] { background: #EBEDF0; }
&[data-level="1"] { background: #9be9a8; }
&[data-level="2"] { background: #40C463; }
&[data-level="3"] { background: #30a14e; }
&[data-level="4"] { background: #216e39; }
}
月份
月份标签是在第一行(周末)所处的月变动的位置显示,计算的部分可以写进已有的循环里,判断Date.getMonth
的值有无变化即可。
const MONTH = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
export default function ContributionCalendar(props) {
const firstDate = new Date(props.contributions[0].date);
const startRow = firstDate.getDay();
const months = [];
let latestMonth = -1;
const tiles = commits.map((c, i) => {
const date = new Date(c.date);
const month = date.getMonth();
// 在星期天的月份出现变化的列上面显示月份。
if (date.getDay() === 0 && month !== latestMonth) {
// 计算月份对应的列,从 1 开始、左上角格子留空所以 +2
const gridColumn = 2 + Math.floor((i + startRow) / 7);
latestMonth = month;
months.push(
<div
className={styles.month}
key={i}
style={{ gridColumn }}
>
{MONTH[date.getMonth()]}
</div>,
);
}
return (
<i
className={styles.tile}
key={i}
data-level={c.level}
title={getTooltip(c, date)}
/>
);
});
// 第一格不一定是周日,此时前面会有空白,需要设置下起始行。
tiles[0] = React.cloneElement(tiles[0], {
style: { gridRow: startRow + 1 },
});
// 如果第一格不是周日,则首月可能跑到第二列,需要再检查下。
if (MONTH[firstDate.getMonth()] === months[0].props.children) {
months[0].props.style.gridColumn = 2;
}
// 俩月份之间至少隔三格,避免重叠,只可能出现在第一个月。
if (months[1].props.style.gridColumn - months[0].props.style.gridColumn < 3) {
months[0] = null;
}
// 如果最后一个月在最后一格,则会超出布局范围,故隐藏。
if (months.at(-1).props.style.gridColumn > 53) {
months[months.length - 1] = null;
}
return (
<div className={styles.container}>
{months}
<div className={styles.tiles}>{tiles}</div>
</div>
);
}
/* 月份标签元素,放在第一行即 row: 1/2 */
.month {
grid-row: 1/2;
/* 负边距抵消 grid 的 gap */
margin-bottom: -3px;
}
星期
左侧有三个星期标签,分别是一三五。周一所在的行为第三行(从 1 开始 + 表头 + 周末),再往下两行为周三,然后是周五。
由于只有固定的三个元素,所以可以用+
选择符来匹配,实际的代码:
<div className={styles.container}>
{months}
<span className={styles.week}>Mon</span>
<span className={styles.week}>Wed</span>
<span className={styles.week}>Fri</span>
<div className={styles.tiles}>{tiles}</div>
</div>
/* 星期标签,放在第一列即 column: 1/2 */
.week {
grid-column: 1/2;
line-height: 10px;
margin-right: 3px;
/* 第一个元素放在第三行 */
grid-row-start: 3;
/* 第二个(周三)在第五行 */
& + .week {
grid-row-start: 5;
}
/* 第三个(周五)在第七行 */
& + .week + .week {
grid-row-start: 7;
}
}
由于上述是基于一年数据进行展示的,在小程序端并不美观,所以后续进行了修改,改为6个月的了;
Bug记录
TypeError: Cannot read property 'month' of undefined
配置后首先报错,原因是import styles from "./MoodCalendar.scss";
中的styles是undefined,后来改成了普通导入,在需要使用className的地方直接使用”month”而非{styles.month},问题解决
Page route 错误
这个错误表示 小程序在切换页面路由时,尝试操作了一个已经销毁或不存在的页面(WebView)。具体来说:
webviewId 76
:框架为每个页面分配的唯一标识,此处76
代表某个页面的 WebView ID。not the current page
:当前显示的页面已经不是该 WebView ID 对应的页面,通常是因为页面已被提前销毁或跳转逻辑存在冲突。
清除缓存后报错解除
WXMLRT_$gwx:./base.wxml:template:27:20: Template tmpl_0_div
not found.
使用
compiler: 'webpack5',
compiler: {
type: 'webpack5',
prebundle: {
enable: false,
force: true,
},
},
无法解决,查询资料后得知,这属于 Taro 框架在编译 React 到小程序时出现的模板渲染问题。原因出在组件return了HTML原生标签,需要将这些元素替换为Taro组件
替换 HTML 原生标签:
jsx
复制
// 将原有 HTML 元素替换为 Taro 组件
return (
<View {...rest} className={clsx("container", className)}>
{months}
<Text className="week">Mon</Text>
<Text className="week">Wed</Text>
<Text className="week">Fri</Text>
<View className="tiles">{tiles}</View>
<View className="total">
{total} contributions in the last two months
</View>
<View className="legend">
Less
<View className="tile" data-level={0}/>
...
</View>
</View>
);
调整动态元素生成逻辑:
jsx
复制
// 修改月份标签生成(确保始终有效元素)
months.push(
MONTH[month] ? (
<Text
className="month"
key={`month-${i}-${month}`}
style={{ gridColumn }}
>
{MONTH[month]}
</Text>
) : null
);
// 使用过滤去除 null
{months.filter(Boolean)}
避免 cloneElement:
jsx
复制
// 替换原有克隆逻辑const firstTileStyle = tiles.length > 0 ? { gridRow: startRow + 1 } : {};
const tiles = filteredContribs.map((c, i) => (
<View
className="tile"
key={`tile-${i}`}
data-level={c.level}
title={getTooltip(c, new Date(c.date))}
style={i === 0 ? firstTileStyle : {}}
/>
));
日期处理兼容性调整:
jsx
复制
// 统一使用 Taro 的日期处理import Taro from '@tarojs/taro';
const now = Taro.getSystemInfoSync().timeStamp ?
new Date(Taro.getSystemInfoSync().timeStamp) :
new Date();
到此热力图可以正常显示
更多推荐
所有评论(0)