python timepicker_基于react开发的时间选择组件(TimePicker)
自从学习了react后一直琢磨写点项目什么之类的来练练手,恰好公司现在的项目还不是很大,足够我用react进行重构。最开始的时候是想自己去开发组件,后来发现有更好的antd这个东东就放弃了自己开发组件直接将antd的组件拿过来用,感觉挺好用的,直到最近对时间选择器TimePicker这个组件进行实践的时候,发现这个组件的api特别难用(貌似还有bug,应该是我的错觉。。。),所以决定自己开发一个类似的时间选择器来锻炼自己。
言归正传,下面讲解一下我自定义的时间选择器TimePicker开发过程中遇到的坑和目前最终的实现思路(嗯,本人很菜,不吝啬指点...)。
首先构建静态的时间结构,我这里只支持小时和分钟。结构分为上下两层,上层输入框支持直接输入时间,下层选择栏用于给用户提供点击选择,关键代码实现如下:
{
// 你的小时循环遍历
}
{
// 你的分钟循环遍历
}
很简单的一段静态结构,css方面抄了antd的一点,自己写了一点,此处不在过多的去描述,各位童鞋可以自己去写适合自己的。嗯,静态结构搭建完毕后下面就是关键的三个部分的描述,也就是输入框的匹配,点击小时和分钟后的事件描述。
第一块:首先来讲解一下小时和分钟的实现的思路和步骤:
当用户点击小时或分钟时,优先需要判断用户点击值是否合法的有效的,比如:你将这个小时和分钟禁用掉了,这时候用户点击肯定是无效的;
其次就是需要判断用户在点击时是否已经点击过其他的小时或着分钟的元素,如果存在点击过的元素,我们需要将上一次存放点击的元素的变量的选中效果进行清除,然后将此次的点击设为选中,并且将此次点击的元素对象和元素值使用变量进行存放;
然后我们玩一个简易的点击选中的元素速度动画(个人兴趣,你也可以不用动画)将选中的元素置顶,
顺手判断一下小时和分钟是否同时存在点击,存在则设置输入框的值,不推荐使用state设置值,存放的变量也不推荐使用state(结束时解释);
最后由于我向用户提供了一个onchange方法,所以需要判断一下用户是否设置了onchange,若设置则执行用户的onchange。
关键代码实现如下:
// 以小时为例:
hourClick = (e) => {
let target = e.target, obj = this,
selectedHour = target.innerHTML,
selectedMinute = obj.selectedMinute;
if(target.className === "my-TimePicker-time-selected-disabled"){
// 判断用户点击值是否合法,此处我时判断是否为禁用状态
return ;
}
if(obj.prevHourSelected){
// 判断用户是否已经点击过其他元素,有则将选中清空
obj.prevHourSelected.className = "";
}
// 将此次点击设为选中
target.className = "你的选中类名";
// 将此次点击设置为上一次选中
obj.prevHourSelected = target;
obj.selectedHour = selectedHour;
obj.animateTop(selectedHour, selectedMinute, obj.time); // 简易的点击速度动画
if(selectedMinute !== ""){
// 判断用户的是否同时点击过分钟,若存在则设置输入框的值
}
if(obj.props.onChange){
// 用户的回调函数执行
}
}
第二块,输入框输入时间时的实现思路和步骤:
去除用户输入的值的前后空格,然后判断用户值的长度是否为0,如果输入为0则用户执行了删除操作,此时我们将用户之前输入的值设置的对应的存放变量和选中效果统统清空,然后执行用户回调(如果有的话);
对用户值的长度进行判断是否等于5,因为我是HH:mm的格式,所以长度为5,为什么要进行长度验证,因为input的onchange方法当用户改变值时都会执行方法,我做了正则判断,不通过就会清空,所以需要在长度等于5时才进行正则验证;
通过长度验证后,进行正则验证,不通过就清空值,通过后开始进行设置小时和分钟的选中,同时清掉上一次选中的效果和存放的变量值,接着判断输入的值是否合法,比如你不能输入已经禁用的值,通过后将此次输入的值设置为选中,并存放到变量中,又是判断一下是否有回调;
关键代码实现如下:
if(value.length !== 0){
if(value.trim().length === 5){ // 符合长度要求后进行正则验证
if(obj.reg.test(value)){
let hour = +value.split(":")[0], minute = +value.split(":")[1];
// 获取输入值所在的选项的样式
...
if(obj.prevHourSelected){ // 如果上一次存在选中的时间就清空选中样式
xxx.className = "";
}
if(obj.prevMinuteSelected){ // 如果上一次存在选中的时间就清空选中样式
xxx.className = "";
}
// 如果所选值不是禁止选中,就添加选中样式,并且启用滚动效果
if(hourClass.indexOf("my-TimePicker-time-selected-disabled") === -1 && minuteClass.indexOf("my-TimePicker-time-selected-disabled") === -1){
xxx.className = hourClass + " my-TimePicker-time-selected";
xxx.className = minuteClass + " my-TimePicker-time-selected";
obj.animateTop(hour, minute, obj.time);
//添加此次输入为上一次选中
obj.prevHourSelected = xxx;
obj.prevMinuteSelected = xxx;
obj.selectedHour = hour;
obj.selectedMinute = minute;
if(obj.props.onChange){ // 用户是否绑定了自定义onChange
// 执行回调
}
}else{ // 输入为禁用数时清空值
input(输入框的对象).value = "";
}
}
}
}else{
// 删除操作,清空所有值,选中和变量存放到值
}
第三块,用户设置默认值的设置,componentDidMount进行判断和设置,然后每次用户更新state是在shouldComponentUpdate判断是否值是否合法(此处代码和之前较相似不在贴出关键代码实现):
componentDidMount设置值,并将值设置为选中和存放到已经为选中的变量中,然后判断是否回调;
shouldComponentUpdate中判断用户的值是否合法,合法返回true并执行componentWillReceiveProps,否则返回false;
第四块,componentWillReceiveProps,用户在外部更新组件到state后的操作,根据个人需求来,我这里主要描述用户设置或选择的值是否为禁用的值
拿到用户设置或选择的小时和分钟的值(怎么拿?之前强调过,用户每次输入或者选择的时候我会将值存放到对应的变量中,此处直接获取对应变量的值);
判断用户设置的小时和分钟是否处于禁用状态(怎么知道是禁用,我要求用户传入要禁用的值的数组),非禁用则执行setState来更新小时和分钟的列表,否则清空所有的操作状态和值;
解释不用state设置值:使用state设置文本框值,会出现无法删除文本框的值(不知道你们会不会有这种情况,我这里会有,所以我没有用state),应该是跟react的虚拟dom有关,用户在界面上的操作在js没有更新state的情况下应该会始终保持state值不变。
写在最后:本人react菜鸟一枚,第一次分享自己做的东西的思路难免出现大量的纰漏或者疏忽,望谅解,所以也是希望和各位相互交流,如有大神指点本人表示热烈欢迎。本人不吝啬各位指点,无论水平高低,只要有好的想法或者更好的实现方式都可以和本人交流。。。欢迎回复我。。。
组件代码示例:
import React from 'react';
import ReactDOM from 'react-dom';
class MyTimePicker extends React.Component {
constructor(props){
super(props);
this.valueHeight = 28; // 时间值的高度
this.reg = /^((20|21|22|23|[0-1]\d)\:[0-5][0-9])$/; // 验证时间格式是否正确
this.handleHour = null; // 小时滚动定时器
this.handleMinute = null; // 分钟滚动定时器
this.prevHourSelected = null; // 上一级选择的小时
this.prevMinuteSelected = null; // 上一次选择的分钟
this.selectedHour = ""; // 选中的小时
this.selectedMinute = ""; // 选中的分钟
this.time = 50; // 滚动时间频率
if(props.onChange){ // 如果用户使用了自定义onChange,就将用户的onChange赋值给本地的setChange属性
this.setChange = props.onChange;
}
}
state = {
disabledHours: this.props.disabledHours, // 禁用的小时数组
disabledMinutes: this.props.disabledMinutes, // 禁用的分钟数组
isShow: false, // 是否显示选择框
}
setChange(hour, minute){} // 构造本地的change方法接受用户自定义方法
range(start, end){ // 构造时间
if(typeof start !== "number" || typeof end !== "number" || start >= end){
return [];
}
let arr = [];
for(let i = start; i <= end; i++){
arr.push(i);
}
return arr;
}
onChange(e){
e.stopPropagation();
let obj = this, value = e.target.value.trim();
if(value.length !== 0){
if(value.trim().length === 5){ // 符合长度要求后进行正则验证
if(obj.reg.test(value)){
let hour = +value.split(":")[0], minute = +value.split(":")[1];
// 获取输入值所在的选项的样式
let hourClass = obj.refs['my-Timepicker-hour'].children[0].children[hour].className;
let minuteClass = obj.refs['my-Timepicker-minute'].children[0].children[minute].className;
if(obj.prevHourSelected){ // 如果上一次存在选中的时间就清空选中样式
obj.refs['my-Timepicker-hour'].children[0].children[+obj.prevHourSelected.innerHTML].className = "";
}
if(obj.prevMinuteSelected){ // 如果上一次存在选中的时间就清空选中样式
obj.refs['my-Timepicker-minute'].children[0].children[+obj.prevMinuteSelected.innerHTML].className = "";
}
// 如果所选值不是禁止选中,就添加选中样式,并且启用滚动效果
if(hourClass.indexOf("my-TimePicker-time-selected-disabled") === -1 &&
minuteClass.indexOf("my-TimePicker-time-selected-disabled") === -1){
obj.refs['my-Timepicker-hour'].children[0].children[hour].className = hourClass + " my-TimePicker-time-selected";
obj.refs['my-Timepicker-minute'].children[0].children[minute].className = minuteClass + " my-TimePicker-time-selected";
obj.animateTop(hour, minute, obj.time);
//添加此次输入为上一次选中
obj.prevHourSelected = obj.refs['my-Timepicker-hour'].children[0].children[hour];
obj.prevMinuteSelected = obj.refs['my-Timepicker-minute'].children[0].children[minute];
obj.selectedHour = hour;
obj.selectedMinute = minute;
if(obj.props.onChange){ // 用户是否绑定了自定义onChange
obj.setChange(hour, minute);
}
}else{ // 输入为禁用数时清空值
obj.refs['my-Timepicker-text'].value = "";
}
}else{ // 置空
e.target.value = "";
}
}
}else{
obj.prevHourSelected = null; // 上一级选择的小时
obj.prevMinuteSelected = null; // 上一次选择的分钟
obj.selectedHour = ""; // 选中的小时
obj.selectedMinute = ""; // 选中的分钟
let hourList = obj.refs['my-Timepicker-hour'].children[0].children,
minuteList = obj.refs['my-Timepicker-minute'].children[0].children;
for(let i = 0, len = hourList.length; i < len; i++){
if(hourList[i].className === "my-TimePicker-time-selected"){
hourList[i].className = "";
break;
}
}
for(let i = 0, len = minuteList.length; i < len; i++){
if(minuteList[i].className === "my-TimePicker-time-selected"){
minuteList[i].className = "";
break;
}
}
obj.animateTop(0, 0, obj.time);
if(obj.props.onChange){ // 用户是否绑定了自定义onChange
obj.setChange("", "");
}
}
}
/*
* 小时/分钟点击效果
* 参数&变量说明:
* 参数:e -> 小时选中对象(当前或者上一次)
* 变量:selectedHour -> 当前选中小时,selectedMinute -> 当前选中分钟
* 1、如果是禁止选择的时间,return
* 2、如果上一次存在选中的时间就清空选中样式
* 3、通过前两次判断后添加当前选择为选中,设置当前选中小时
* 4、添加本次选中为上次选中
* 5、添加选中动画
* 6、如果小时和分钟同时选中,则设置文本框值
* 7、如果小时和分钟同时选中则启用回调
*/
hourClick = (e) => {
let target = e.target, obj = this,
selectedHour = target.innerHTML,
selectedMinute = obj.selectedMinute;
if(target.className === "my-TimePicker-time-selected-disabled"){
return ;
}
if(obj.prevHourSelected){
obj.prevHourSelected.className = "";
}
target.className = "my-TimePicker-time-selected";
obj.prevHourSelected = target;
obj.selectedHour = selectedHour;
obj.animateTop(selectedHour, selectedMinute, obj.time);
if(selectedMinute !== ""){
obj.refs['my-Timepicker-text'].value = selectedHour+":"+selectedMinute;
}
if(obj.props.onChange){
obj.setChange(+selectedHour, selectedMinute);
}
}
minuteClick = (e) => {
let target = e.target, obj = this,
selectedHour = obj.selectedHour,
selectedMinute = target.innerHTML;
if(target.className === "my-TimePicker-time-selected-disabled"){
return ;
}
if(obj.prevMinuteSelected){
obj.prevMinuteSelected.className = "";
}
target.className = "my-TimePicker-time-selected";
obj.prevMinuteSelected = target;
obj.selectedMinute = selectedMinute;
obj.animateTop(selectedHour, selectedMinute, obj.time);
if(selectedHour !== ""){
obj.refs['my-Timepicker-text'].value = selectedHour+":"+selectedMinute;
}
if(obj.props.onChange){
obj.setChange(selectedHour, +selectedMinute);
}
}
animateTop(hour, minute, time){ // 时间滚动效果
let obj = this,
curHourTop = obj.refs['my-Timepicker-hour'].scrollTop, // 当前小时的滚动高度
curMinuteTop = obj.refs['my-Timepicker-minute'].scrollTop; // 当前分钟的滚动高度
clearInterval(obj.handleHour);
clearInterval(obj.handleMinute);
// 不为空就转成数字
hour = (hour === "")?"":+hour;
minute = (minute === "")?"":+minute;
// 判断是否进行动画
if(curHourTop !== obj.valueHeight*hour && hour !== ""){
obj.handleHour = setInterval(() => {
let getTop = obj.refs['my-Timepicker-hour'].scrollTop, // 实时获取滚动高度
result = getTop - obj.valueHeight*hour, // 实时计算滚动高度差
speed = Math.floor(result/3); // 实时计算滚动的正负速度
if(speed !== 0){ // 如果滚动高度差绝对值大于0,始终滚动
obj.refs['my-Timepicker-hour'].scrollTop = getTop - speed;
}else{ // 反之,停止滚动
obj.refs['my-Timepicker-hour'].scrollTop = obj.valueHeight*hour; // 速度等于0时,手动修正动画误差
clearInterval(obj.handleHour);
}
}, time);
}
if(curMinuteTop !== obj.valueHeight*minute && minute !== ""){
obj.handleMinute = setInterval(() => {
let getTop = obj.refs['my-Timepicker-minute'].scrollTop, // 实时获取滚动高度
result = getTop - obj.valueHeight*minute, // 实时计算滚动高度差
speed = Math.floor(result/2); // 实时计算滚动的正负速度
if(speed !== 0){ // 如果速度不等于0,始终滚动
obj.refs['my-Timepicker-minute'].scrollTop = getTop - speed;
}else{ // 反之,停止滚动
obj.refs['my-Timepicker-minute'].scrollTop = obj.valueHeight*minute; // 速度等于0时,手动修正动画误差
clearInterval(obj.handleMinute);
}
}, time);
}
}
componentDidMount(){
let obj = this, value = obj.props.defaultValue;
if(value !== "" && obj.reg.test(value)){
let hour = +value.split(":")[0],
minute = +value.split(":")[1],
hourObj = obj.refs['my-Timepicker-hour'].children[0].children[hour],
minuteObj = obj.refs['my-Timepicker-minute'].children[0].children[minute],
hourClass = hourObj.className,
minuteClass = minuteObj.className;
obj.refs['my-Timepicker-text'].value = obj.props.defaultValue;
obj.prevHourSelected = hourObj; // 上一级选择的小时
obj.prevMinuteSelected = minuteObj; // 上一次选择的分钟
obj.selectedHour = hour; // 选中的小时
obj.selectedMinute = minute; // 选中的分钟
obj.animateTop(hour, minute, obj.time);
if(obj.props.onChange){
obj.setChange(hour, minute);
}
}
}
// 已加载组件首次渲染完毕后,父组件更新state自动调用次方法重新传入props,接受值后刷新禁用值, 判断选中的值是否在禁用中存在
componentWillReceiveProps(nextProps){
let obj = this, getHour = obj.selectedHour, getMinute = obj.selectedMinute;
if(nextProps.disabledHours.indexOf(getHour) === -1 && nextProps.disabledMinutes.indexOf(getMinute) === -1){
this.setState({
disabledHours: nextProps.disabledHours,
disabledMinutes: nextProps.disabledMinutes,
});
}else{
obj.reset();
}
}
reset = () => { // 重置所有值和选择
let obj = this;
obj.refs['my-Timepicker-text'].value = "";
obj.prevHourSelected = null; // 上一级选择的小时
obj.prevMinuteSelected = null; // 上一次选择的分钟
obj.selectedHour = ""; // 选中的小时
obj.selectedMinute = ""; // 选中的分钟
let hourList = obj.refs['my-Timepicker-hour'].children[0].children,
minuteList = obj.refs['my-Timepicker-minute'].children[0].children;
obj.setState({
disabledHours: [],
disabledMinutes: [],
});
obj.animateTop(0, 0, obj.time);
}
onMouseMove = (e) => {
this.setState({ isShow: true });
}
onMouseOut = (e) => {
this.setState({ isShow: false });
}
render(){
let obj = this;
return(
ref="my-Timepicker-text" className="ant-time-picker-input"
onChange={obj.onChange.bind(this)} />
{
obj.range(0, 23).map((cur, index) => {
let getHour = cur, setClass = "";
obj.state.disabledHours.map((cur1, index1) => {
if(getHour === cur1){
setClass = "my-TimePicker-time-selected-disabled"
}
})
return
{(cur < 10)?"0"+cur:cur}
})
}
{
obj.range(0, 59).map((cur, index) => {
let getHour = cur, setClass = "";
obj.state.disabledMinutes.map((cur1, index1) => {
if(getHour === cur1){
setClass = "my-TimePicker-time-selected-disabled"
}
})
return
{(cur < 10)?"0"+cur:cur}
})
}
)
}
}
MyTimePicker.propTypes = {
id: React.PropTypes.string,
defaultValue: React.PropTypes.string, // 默认值
placeholder: React.PropTypes.string,
disabledHours: React.PropTypes.array, // 禁用的小时数组
disabledMinutes: React.PropTypes.array // 禁用的分钟数组
};
MyTimePicker.defaultProps = {
id: "",
defaultValue: "",
placeholder: "请选择时间",
disabledHours: [],
disabledMinutes: []
}
export default MyTimePicker;
使用实例:
/*
* 参数说明:hour -> 回调小时,minute -> 回调分钟
* 变量说明:obj -> 当前上下文, endHourArr -> 禁用结束小时数组,endMinuteArr -> 禁用结束分钟数组,
* startMinuteArr -> 禁用开始分钟数组,preEndHour -> 上一次选中的结束小时
* preEndMinute -> 上一次选中的结束分钟
* 方法说明:选中时间后的回调方法,start和end是开始与结束,两个方法功能相同
* 1、只选中时间时,返回禁用的小时(不包括选中的小时)
* 2、判断时间和分钟是否同时选中,返回禁用的小时数组,根据分钟的位置决定小时是否进退
* 3、在时间和分钟同时选中时,继续判断是否存在已经选中的开始/结束小时并判断是否和当前选中小时相等,返回禁用的分钟数组
* 4、判断开始/结束小时和分钟是否选中,并判断开始/结束小时否和当前小时相等,返回当前元素需要禁用的分钟数组
*/
changeStartTime(hour, minute){
let obj = this,
endHourArr = [],
endMinuteArr = [],
startMinuteArr = [],
preEndHour = obj.endHour,
preEndMinute = obj.endMinute;
if(hour !== ""){
endHourArr = obj.range(0, +hour, "
}
if(hour !== "" && minute !== ""){
endHourArr = (+minute === 59)?obj.range(0, +hour):obj.range(0, +hour, "
if(preEndHour !== "" && parseInt(preEndHour) === parseInt(hour)){
endMinuteArr = obj.range(0, +minute);
}
}
if(hour !== "" && preEndHour !== "" && preEndMinute !== "" && parseInt(preEndHour) === parseInt(hour)){
startMinuteArr = obj.range(+preEndMinute, 59);
}
obj.startHour = hour;
obj.startMinute = minute;
obj.setState({
disabledStartMinutes: startMinuteArr,
disabledEndHours: endHourArr,
disabledEndMinutes: endMinuteArr,
});
}
python timepicker_基于react开发的时间选择组件(TimePicker)相关推荐
- Mdebug:基于React开发的移动web调试工具
作者:thinkchen,腾讯 PCG 高级前端开发工程师 mdebug是腾讯新闻 TNTWEB 团队推出的基于React开发的新的web调试工具, 沉淀自腾讯新闻微信手 q 双插件多年的移动 web ...
- 一个基于 React 开发的PC端音乐App
?一个基于 React 开发的PC端音乐App. 同时支持 Mac 与 Windows 系统.下载地址 项目使用 electron 作为外壳,webpack 作为打包工具,核心技术包括 React + ...
- value数字 vue_基于Vue开发数字输入框组件
随着 vue 越来越火热, 相关组件库也非常多啦, 只用轮子怎么够, 还是要造起来!!! 1.概述 vue组件开发的api:props.events和slots 2.组件代码 效果: (1)index ...
- 【工作流引擎】Flowable流程设计器 基于bpmnjs开发的vue组件
[工作流引擎]Flowable流程设计器 基于bpmnjs开发的vue组件 设计器介绍 集成设计器 设计器介绍 bpmn.js官网 bpmn.js 是一个BPMN2.0渲染工具包和web建模器, 使得 ...
- 基于React开发范式的思考:写在Lesx发布之际
例子:lesx-example webpack loader: lesx-loader 一些背景 现在前端框架已经呈现出React.Angular.Vue三足鼎立的局势,对于三者的对比以及技术选型的思 ...
- 基于Vue开发一个日历组件
最近在做一个类似课程表的需求,需要自制一个日历来支持功能及展现,就顺便研究一下应该怎么开发日历组件. 更新 2.23修复了2026年2月份会渲染多一行的bug,谢谢@深蓝一人童鞋提出的bug,解决方案 ...
- vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享)
语雀原文 有更好的排版体验~ 这篇文章主要讲述笔者开发 v-gantt 甘特图组件的经过. 起源 公司项目有个甘特图的需求. 笔者考察了世面上 常见的甘特图组件 后,本着 我上我也行 的心态,以及考虑 ...
- 基于react开发package.json的配置
项目依赖 react网页开发的3件套: react, react-dom, react-router-dom, redux, react-redux react的UI组件库: antd(pc端), a ...
- 基于React开发一个音乐播放器
同时支持 Mac 与 Windows 系统. 下载地址 掘金链接 项目使用 electron 作为外壳,webpack 作为打包工具,核心技术包括 React + Redux + React-rout ...
- 使用基于模型设计开发AUTOSAR软件组件
本文翻译的是Mathworks公司撰写的Development of AUTOSAR Software Components with Model-Based Design,希望与大家一起共同学习进步 ...
最新文章
- pandas.DataFrame.to_dict()的使用详解
- String长度有限制吗?是多少?还好我看过
- cxGrid导出Excel货币符号问题
- 数字取整或保留小数四舍五入的正确写法
- 4.从单应矩阵中分离得到内参和外参(需要拍摄n=3张标定图片)
- EOS资源模型(2)资源使用
- java dump分析工具_Java虚拟机详解(八)------虚拟机监控和分析工具(2)——可视化...
- qemu-kvm部署虚拟机
- python爬取appstore的评论数据的步骤_python数据抓取分析
- 做中国女人难,做中国女装更难
- 【OSGI】Error osgi xx Invalid value for DynamicImport-Package dynamic.import.pack
- 阶乘之和计算_浅谈积分计算的技巧
- 手把手教你设计交友网站【3】
- Macbook Pro休眠唤醒后后台运行程序被关闭的解决方法
- gsp计算机管理权限,新gsp计算机权限设置
- MATLAB中能对三角函数降幂嘛,三角函数降幂公式是什么
- 本地极验滑块识别DLL/本地通用验证码识别DLL/文字点选/图标点选/本地识别DLL
- RFM模型实现用户分层
- 职称英语计算机考试取消,2020年职称英语考试取消了吗
- DataGrid 动态绑定URL地址,在WebConfig中配置
热门文章
- 通信专业顶刊_通信类权威SCI期刊(部分)
- buuctf-misc部分wp(更新一下)
- 毕节市搜索引擎优化_毕节市网站建设58同城
- 在Linux下进入目录,目录下创建、修改、删除文件所需权限
- GPS NMEA协议,0183 定位数据格式 	双模定位:GNXXX GPS+BD 完整版
- SONY重拳出击,开始涉足移动领域----Playstation Mobile必然崛起
- nextjs中阿里icon库的引入使用
- android极光推送设置消息类型,详解极光推送的 4 种消息形式—— Android 篇
- 汇编篇 :关于地址总线与数据总线的换算
- 微信朋友圈 腾讯服务器,朋友圈@微信能得一面红旗?腾讯服务器一度宕机