普通视图
JS基础篇-ES6+新增API
无障碍功能是必须的
为什么无障碍设施如此重要
无障碍设计是一项基本职责,而非次要考虑因素。每个人都应该能够使用您的应用,包括那些行动不便的人。包容性设计确保了用户能够平等地使用您的产品,无论他们是否有视力障碍、行动障碍或认知障碍。
在许多国家,无障碍设施也是一项法律要求(例如美国的 ADA、欧洲的 EN 301 549)。
a11y React 开发者最佳实践
1. 使用语义HTML
- 偏好本土元素
- 避免使用 div 进行交互式 UI — 屏幕阅读器会跳过它们
2. 确保键盘导航性
- 每个交互元素都应该可以通过 Tab 和 Enter 访问和操作
- 慎重使用 tabindex (避免 tabindex="0" 过载)
3. 需要时添加 ARIA 属性
- 使用 aria-label、aria-hidden、aria-live 为屏幕阅读器提供上下文
- 但是当语义 HTML 可以完成工作时,不要过度使用 ARIA
4. 为图片提供替代文本
- 对重要图片使用有意义的 alt=""
- 使用 alt="" 来隐藏装饰性的
5. 颜色对比度和焦点指示器
- 确保文本具有高对比度(根据 WCAG AA/AAA 检查)
- 不要删除焦点轮廓——如果需要,可以自定义它们
6. 表单错误处理
- 使用 aria-scribeby 链接表单错误
- 在模糊或提交时进行验证,而不仅仅是在更改时
确保年度合规性的工具
- axe DevTools(Chrome 扩展程序)——实时分析 WCAG 违规行为
- eslint-plugin-jsx-a11y — 查找缺失的角色、替代文本、标签陷阱
- Lighthouse (Chrome/CI) — 审核中的 a11y 评分
- 屏幕阅读器:NVDA(Windows)、VoiceOver(macOS)、ChromeVox
现实世界的可访问性审计技巧
- 仅使用键盘即可导航整个应用程序
- 使用屏幕阅读器浏览常见流程
- 使用 contrast-ratio.com 等工具测试颜色对比度
- 避免可能引发运动障碍的动画(尊重
prefers-reduced-motion
)
编写可测试的无障碍代码
- 使用自动化测试框架(如 Jest + Testing Library)确保交互元素的可访问性
- 示例:
getByRole('button', { name: /提交/i })
能验证按钮是否具备正确语义 - 利用
axe-core
集成到测试流程中,防止无障碍回归
组件库与设计系统中的无障碍策略
- 选择已通过 WCAG 检测的组件库(如 Reach UI、Radix UI)
- 在设计阶段加入辅助功能审核,比如组件状态下的焦点样式、键盘操作流
- 定义通用的 a11y 设计 token,例如:焦点边框、aria 属性规范、alt 文案策略
团队协作与文化建设
- 将无障碍视为团队代码评审中的一部分,而不是事后补救
- 对设计师、开发者、测试人员进行基础的无障碍培训
- 分享无障碍提升成果(如 Lighthouse 分数、用户正反馈),增强团队使命感
最终要点:以人为本,确保代码面向未来
Web 应该属于所有人
作为开发者,我们的代码承载着一种责任——让技术成为助力,而非障碍。
从一个有意义的 alt
文案,到为按钮保留焦点样式,哪怕是一个小小的调整,也可能极大改善一位用户的体验。
别等需求单提出来才去优化无障碍。把 a11y 当作质量保障的一部分,你做出的产品会更稳定、更持久,也更被信任。
基于uniapp+nodejs实现小程序登录功能
本系列教程,以【中二少年工具箱】小程序为案例demo,具体效果请在微信中搜索该小程序查看。或在微信输入框输入 【#小程序://中二少年工具箱/6buitXgPnjHV21r】
一、概述
1.1 技术选型:
小程序端:uniapp
后端:nodejs+midwayjs+typeorm
数据库:mysql
1.2 登录功能实现方案:
1.小程序端调用接口uni.login获取随机code
2.将随机code传递给后端接口,后端接口中调用小程序官方api,获取用户信息
3.后端将用户信息保存到数据库中用户信息表,并将保存结果返回给前端
4.前端缓存用户信息,并显示
二、小程序端实现
代码实现:
function getUserInfoByWx() {
isLoad.value = true
uni.login({
provider: 'weixin', //使用微信登录
success: function (loginRes) {
const userData = {
code: loginRes.code
}
// console.log('userData',userData);
getUserInfoByWxApi(userData).then(res => {
console.log(res)
if (res.success) {
userInfoStore.setUserInfo({
userName: res.data.userName,
openidWx: res.data.openidWx
})
} else {
openMessage({
text: '自动创建用户出错,请点击登录手动创建'
})
}
}).catch(err => {
console.log('eeeeeeeeeeeeeee', err)
openMessage({
text: '登录失败,请联系开发者'
})
})
.finally(() => {
// debugger
isLoad.value = false
uni.$emit('loginFinish');
})
},
fail() {
isLoad.value = false
}
});
}
代码解释:
1.isLoad:前端是否显示正在登录的动画。
2.uni.login:uniapp提供的登录api,可以生成各平台的临时code。
3.getUserInfoByWxApi:调用后端接口,将临时code作为参数传递给后端,后端再调用官方接口完成登录。
4.userInfoStore.setUserInfo登录成功后,在全局状态管理中保存用户信息
上面的代码,大部分都是和前端登录相关的业务代码,真正核心的是生成了临时code并传递给后端,因为调用官方接口只能在后端代码中运行。
三、后端实现
后端代码实现可分为两步,一是调用官方接口,获取小程序官方返回的用户信息;二是根据业务需求,将用户信息保存到我们的数据库中。
controller层代码实现;
@Post('/getUserInfoByWx')
async getUserInfoByWx(@Body() userData: { code: string }) {
const openidRs = await this.loginService.getOpenidWx(userData)
const openidKey = 'openidWx'
const rs = await this.loginService.getUserInfoByPlat(openidRs, openidKey)
return rs
}
上面代码的code就是小程序端传入的临时code,主要用于getOpenidWx方法中,获取调用官方接口后的返回结果。
3.1 调用官方接口
上面代码中的getOpenidWx方法即是调用官方接口:
const openidRs = await this.loginService.getOpenidWx(userData)
具体的service实现:
/*根据临时code,获取wx返回的登录信息*/
async getOpenidWx(userData:{code:string} | any): Promise<any> {
const url = 'https://api.weixin.qq.com/sns/jscode2session';
const data = {
appid: 'wx9cxxxxxxxxxxxxx',
secret: '66bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
js_code: userData.code,
grant_type: 'authorization_code'
}
const result = await this.httpService.get(url, {params: data})
return result
}
官方的文档如下:
结合文档和我写的接口示例,我为大家总结了关键点:
1.接口通过GET方式访问
2.参数包括appid、secret、js_code、grant_type都是通过url参数的方式传递。
3.appid+secret:开发者的身份认证,通过开发管理平台获取:
4.js_code:小程序端传递来的临时code
5.grant_type:照官网写,别问为什么。
返回结果中我们需要重点关注的参数是openid,这是每一个用户的唯一标识。
3.2 保存用户信息到数据库
上面controller层一共调用了两个方法,一个是上面的调用官方接口,另一个就是保存用户信息到数据库并返回用户信息:
const rs = await this.loginService.getUserInfoByPlat(openidRs, openidKey)
上面是兼容各平台的写法,我们可以忽略openidKey参数,只以微信小程序为例,具体的service实现为:
/**
* 根据各平台的openid获取用户信息,可用于首次登录时,自动注册*/
async getUserInfoByPlat(openidRs,openidKey:string){
// debugger
// 通过三方平台的
let rs:any={}
let findWxUserRs = new WxUser()
if (openidRs.status == 200 && (openidRs.data.openid || openidRs.data.data.openid)) {
const userInfo={}
userInfo[openidKey]=openidRs.data.openid || openidRs.data.data.openid
findWxUserRs = await this.wxUserService.getWxUserByUserInfo(userInfo) || new WxUser()
}
if (findWxUserRs && findWxUserRs.id) {
//用户已注册,获取用户信息
if(!findWxUserRs.userExtraEntity){
// 兼容旧数据,若没有extra信息,则创建
rs = await this.wxUserService.saveWxUser(findWxUserRs)
}else{
rs=findWxUserRs
}
} else {
//用户未注册,则保存并登录
// /*TODO:tt和wxopenid的层级不同,需要改造*/
findWxUserRs[openidKey] = openidRs.data.openid || openidRs.data.data.openid
rs = await this.wxUserService.saveWxUser(findWxUserRs)
}
return rs
}
代码解释:
1.openidRs是调用官方接口的方法返回的用户信息,如果它的status为200,并且openid有值,则说明调用官方接口成功。判断里之所以有两种层级,可能是因为某个平台的返回结果比较奇葩,代码过于久远,我也记不清了。
2.findWxUserRs是以openid作为筛选条件,筛选数据库的用户表,第一版代码不用像我写这么麻烦,我的openid可能分为openidWX,openidTT等等。第一次做这个功能,就在用户表里增加字段openid即可,然后根据这个字段筛选用户表。
3.如果以openid为筛选条件查到了用户信息,说明用户已注册,返回查询到的数据库中的用户信息。不用关心我的userExtraEntity对象判断,我的用户表结构发生过变化,为了兼容旧数据,这里做了判断。
4.如果以openid为筛选条件未查询到信息,则说明用户未注册,应该主动注册用户。注册用户的逻辑就因人而异了,我生成了随机的用户名称和用户id,再加上openid字段,保存了基本的用户信息。
5.保存成功后,返回用户信息。
我们要理清楚一个概念:用户信息。
通过官方接口获取的用户信息,是小程序官方提供返回结果,现阶段对我们最重要的是openid。
通过我们自己业务代码获取的用户信息,是返回数据库中的用户表信息。现阶段最重要的是用户名(userName)+id(表id,在我们业务中的唯一标识)+openid(用户在某小程序中的唯一标识)。
为什么不能用openid代替id成为我们业务表中的唯一标识,因为以后有可能还会集成其他小程序平台,openid在某小程序的各个场景中是唯一标识,但对于我们系统而言,它只是一个业务字段。
三、后端返回结果,前端显示
前端调用接口最终返回的结果,是保存在数据表中的用户信息:用户名+id+openid。
在上面前端代码中,成功调用接口后,主要做了两个操作:
userInfoStore.setUserInfo({
userName: res.data.userName,
openidWx: res.data.openidWx
})
...省略代码
uni.$emit('loginFinish');
代码解释:
1.userInfoStore.setUserInfo:是维护全局状态管理中的用户信息,并且使用pinia做成响应式,只要改变,小程序端的页面就会显示对应用户名。
2.uni.$emit('loginFinish')
:是一个事件通知机制。当登录模块所有操作完成,再触发loginFinish。其他组件中需要等待登录操作完成后才能执行的代码,需要严格控制执行顺序的代码,就在合适的位置插入uni.$on()监听。
总结
博主的大部分demo示例都会放到:中二少年学编程的示例项目。戳链接,查看示例效果。如果链接失效,请手动输入地址:lizetoolbox.top:8080/#/
本文知识点总结:
1.uni.login获取登录随机code,传递给后端接口。
2.后端代码中,调用官方接口获取平台返回的openid
3.将openid更新到数据库的用户信息表,如果没有该用户,则创建。
4.后端接口返回用户信息,在前端显示并执行后续操作
有任何前端项目、demo、教程需求,都可以联系博主,博主会视精力更新,免费的羊毛,不薅白不薅!~
大白兔
前言
继上次实现一个树苗生长的效果后,这次我就带大家来实现一个活蹦乱跳的大白兔的效果,纯CSS
实现,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。
效果预览
最终实现的相关效果如下。
HTML、CSS部分
这里是类名为.rabbit
的动画部分,相关代码如下。
.rabbit {
width: 5em;
height: 3em;
color: whitesmoke;
background:
radial-gradient(
circle at 4.2em 1.4em,
#333 0.15em,
transparent 0.15em
), /* eye */
currentColor;
border-radius: 70% 90% 60% 50%;
position: relative;
box-shadow: -0.2em 1em 0 -0.75em #333;
z-index: 1;
animation: hop 1s linear infinite;
}
这里定义了一个名为.rabbit
的类,用于创建一个卡通兔子图案的动画效果。设置元素的宽度为5em,高度为3em(em是相对单位,基于当前元素的字体大小)。设置文本颜色为whitesmoke(浅灰白色),这里也用作后续currentColor
的参考值。
使用了两层背景:
-
眼睛:通过
radial-gradient
创建一个圆形径向渐变,模拟兔子的眼睛:- 圆心位于
4.2em 1.4em
(相对元素左上角)。 - 从中心开始,
#333
(深灰色)覆盖半径为0.15em
,之后透明。
- 圆心位于
-
身体颜色:
currentColor
继承自color: whitesmoke
,因此兔子身体为浅灰白色。
设置元素的四个角为不同的圆角百分比,形成不规则的椭圆形,模拟兔子的头部形状。简单来说,这里创建了一个简单的卡通兔子头部以及浅灰白色的不规则圆形身体。
随后定义了一个名为hop
的动画,持续时间1s
,速度曲线linear
(匀速)并且无限循环。动画分为几个关键帧阶段,每个阶段设置了不同的 transform
(变形)和 box-shadow
(阴影)效果,使兔子看起来像是在跳跃并轻微旋转。相关代码如下。
@keyframes hop {
20% {
transform: rotate(-10deg) translate(1em, -2em);
box-shadow: -0.2em 3em 0 -1em #333;
}
40% {
transform: rotate(10deg) translate(3em, -4em);
box-shadow: -0.2em 3.25em 0 -1.1em #333;
}
60%, 75% {
transform: rotate(0deg) translate(4em, 0);
box-shadow: -0.2em 1em 0 -0.75em #333;
}
}
最终整体的动画效果如下。
-
0% → 20%
:兔子向左倾斜,开始跳跃。 -
20% → 40%
:兔子向右倾斜,达到跳跃最高点。 -
40% → 60%
:兔子恢复水平,开始下落。 -
60% → 75%
:兔子完成跳跃,回到地面(但水平位置向右移动4em
)。
这样兔子会在每次跳跃后回到起点,而不是一直向右移动。
最后就是兔子耳朵和眼睛的CSS部分,相关代码如下。
/* ears */
.rabbit::before {
content: '';
position: absolute;
width: 0.75em;
height: 2em;
background-color: currentColor;
border-radius: 50% 100% 0 0;
transform: rotate(-30deg);
top: -1em;
right: 1em;
border: 0.1em solid;
border-color: gainsboro transparent transparent gainsboro;
box-shadow: -0.5em 0 0 -0.1em;
}
/* tail and legs */
.rabbit::after {
content: '';
position: absolute;
width: 1em;
height: 1em;
background-color: currentColor;
border-radius: 50%;
left: -0.3em;
top: 0.5em;
box-shadow:
0.5em 2em 0,
4.2em 1.75em 0 -0.2em,
4.4em 1.9em 0 -0.2em;
animation: kick 1s 0.4s infinite linear;
}
这里为 .rabbit
元素添加了 耳朵(::before
) 和 尾巴/腿(::after
) 的伪元素,进一步完善了兔子的外观和动画效果。
整体效果就是绘制了一只左耳(旋转 -30deg
),并通过 box-shadow
生成右耳,耳朵内侧有浅灰色描边,增强立体感。带有一个圆形尾巴,生成两条前腿和一条后腿。最后应用 kick
动画,让腿在跳跃时摆动。
@keyframes kick {
60% {
box-shadow:
0.5em 1em 0,
4em 1em 0 -0.2em,
4em 1em 0 -0.2em;
}
}
在兔子跳跃时的上升阶段(0%–40%
),身体向上,腿部保持初始位置,下落阶段(60%
),腿部短暂上收,模拟蹬腿动作,最后的落地阶段(75%–100%
),腿部恢复原位。
这样,兔子会有更生动的跳跃效果! 🐰💨
总结
以上就是整个效果的实现过程了,纯 CSS
实现,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~
初学langchain 从0开始执行一个llm任务
symbol作为对象属性
疏通经脉: Bridge 联通逻辑层和渲染层
巧妙实现,用CSS实现的拖拽留言板卡片功能
带你快速了解关于图片懒加载
使用 ECharts 绘制仪表盘
数据可视化的仪表艺术
仪表盘是现代数据可视化中不可或缺的组件,它能直观展示关键指标的状态和进度。本文将全面介绍如何使用 ECharts 这一强大的开源可视化库创建各种仪表盘。从基础仪表到高级多指针仪表,通过代码示例让你快速掌握仪表盘的设计与应用。
环境准备与基础配置
1. 引入 ECharts
- 使用 npm:
npm install echarts
- CDN 引入:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
2. 准备容器
<div id="dashboard" style="width: 600px; height: 400px;"></div>
3. 初始化图表
const chart = echarts.init(document.getElementById('dashboard'));
基础仪表盘实现
const option = {
series: [{
type: 'gauge', // 核心:仪表盘类型
center: ['50%', '60%'], // 中心位置
startAngle: 180, // 起始角度
endAngle: 0, // 结束角度
min: 0, // 最小值
max: 100, // 最大值
splitNumber: 10, // 分割段数
// 仪表盘指针
pointer: {
show: true,
length: '60%',
width: 6
},
// 仪表盘详情(通常显示当前值)
detail: {
valueAnimation: true,
formatter: '{value}%',
fontSize: 20,
offsetCenter: [0, '20%']
},
// 数据配置
data: [{ value: 65 }], // 初始值
// 刻度配置
axisLine: {
lineStyle: {
width: 20, // 仪表盘宽度
color: [[0.3, '#67e0e3'], [0.7, '#37a2da'], [1, '#fd666d']]
}
},
// 刻度标签
axisLabel: {
distance: -30,
fontSize: 12
},
// 分割线
splitLine: {
length: 18
}
}]
};
chart.setOption(option);
个性化仪表盘设计技巧
1. 自定义色彩分段
axisLine: {
lineStyle: {
color: [
[0.2, '#52c41a'], // 0-20% 绿色
[0.5, '#faad14'], // 20-50% 黄色
[0.8, '#fa541c'], // 50-80% 橙色
[1, '#f5222d'] // 80-100% 红色
],
width: 18
}
}
2. 修改指针样式
pointer: {
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z', // 自定义形状
length: '50%',
width: 20,
itemStyle: {
color: 'auto' // 自动匹配当前分段色
}
}
3. 添加动画效果
detail: {
valueAnimation: true, // 数值动画
formatter: function(value) {
return '{value|' + value.toFixed(0) + '}{unit|%}'; // 富文本格式
},
rich: {
value: {
fontSize: 28,
fontWeight: 'bold',
color: '#4d4d4d'
},
unit: {
fontSize: 15,
color: '#999'
}
}
}
进阶应用:多指针仪表盘
const option = {
series: [{
type: 'gauge',
// ...基础配置...
pointer: {
// 隐藏默认指针
show: false
},
data: [
{ value: 35, name: '内存使用' },
{ value: 70, name: 'CPU使用' },
{ value: 85, name: '磁盘IO' }
],
// 配置多个指针
pointer: [
{
show: true,
length: '60%',
width: 6,
itemStyle: { color: '#37A2DA' }
},
{
show: true,
length: '50%',
width: 6,
itemStyle: { color: '#FF9F7F' },
offsetCenter: [0, '-10%']
},
{
show: true,
length: '40%',
width: 6,
itemStyle: { color: '#67E0E3' },
offsetCenter: [0, '-20%']
}
],
// 详细标签配置
detail: {
show: false // 禁用默认值显示
}
}],
// 添加图例显示多个指标
legend: {
data: ['内存使用', 'CPU使用', '磁盘IO'],
bottom: 10
},
// 添加标题
title: {
text: '服务器监控仪表盘',
left: 'center'
}
};
环形进度条仪表盘
const option = {
series: [{
type: 'gauge',
radius: '90%', // 半径大小
startAngle: 225, // 起点角度
endAngle: -45, // 终点角度
// 进度条样式
progress: {
show: true,
width: 20,
roundCap: true // 圆角端点
},
// 刻度配置
axisLine: {
roundCap: true,
lineStyle: {
width: 20,
color: [[1, '#f0f0f0']] // 背景色
}
},
// 自定义指针(圆点)
pointer: {
show: true,
icon: 'circle',
length: '0%',
width: 12,
itemStyle: {
color: '#1890ff'
}
},
// 添加标签
axisLabel: {
show: false
},
detail: {
valueAnimation: true,
fontSize: 28,
formatter: '{value}%',
color: 'auto',
offsetCenter: [0, '10%']
},
data: [{
value: 75
}]
}]
};
实时更新仪表盘数据
// 初始化图表
const chart = echarts.init(document.getElementById('dashboard'));
chart.setOption(option);
// 更新函数
function updateGauge(newValue) {
chart.setOption({
series: [{
data: [{ value: newValue }]
}]
});
}
// 模拟实时数据(每秒更新)
setInterval(() => {
const value = Math.round(Math.random() * 100);
updateGauge(value);
}, 1000);
// 响应式调整
window.addEventListener('resize', () => {
chart.resize();
});
业务场景应用示例:健康监测仪表盘
const healthOption = {
series: [{
type: 'gauge',
min: 0,
max: 100,
splitNumber: 5,
axisLabel: {
formatter: function(value) {
// 健康等级标签
if (value === 0) return '危险';
if (value === 25) return '较差';
if (value === 50) return '一般';
if (value === 75) return '良好';
if (value === 100) return '优秀';
return '';
},
color: '#666',
distance: -20
},
anchor: {
show: true,
size: 12,
itemStyle: {
borderWidth: 3
}
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v35c0,1.4-1.2,2.6-2.6,2.6h0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.5,0.7,2.9,0.7z',
length: '50%',
width: 8
},
data: [{
value: 88
}]
}]
};
性能优化与最佳实践
-
性能优化策略
// 关闭不必要的动画 animation: false, // 减少刷新频率 setOption(newOption, { lazyUpdate: true, notMerge: true }); // 使用Canvas渲染(默认) init(dom, canvas);
-
响应式布局
const resizeObserver = new ResizeObserver(() => { chart.resize(); }); resizeObserver.observe(document.getElementById('dashboard'));
-
无障碍访问
aria: { show: true, description: '健康指数仪表盘,当前值为88%,处于优秀水平' }
常见问题解决方案
1. 仪表盘不显示
- 检查容器尺寸是否正确设置
- 确认 echarts.init() 参数正确
- 验证数据格式是否符合要求
2. 自定义样式失效
- 确保属性路径正确(如 axisLine.lineStyle.color)
- 检查浏览器控制台错误信息
- 优先使用官方文档支持的配置项
3. 动画卡顿
- 减少数据刷新频率(使用节流)
- 避免过度复杂的图表设计
- 使用轻量级的 Canvas 渲染而非 SVG
打造专业级仪表盘
通过本文的学习,您已经掌握了:
- ECharts 基础仪表盘的创建方法
- 个性化仪表盘的设计技巧
- 多指针和环形进度条的实现
- 实时数据更新的最佳实践
- 性能优化与问题排查方法
仪表盘设计不仅需要技术实现,还需要考虑用户体验和数据故事讲述原则:
graph LR
A[明确业务目标] --> B[选择合适指标]
B --> C[设计视觉层次]
C --> D[优化色彩对比]
D --> E[确保响应式布局]
E --> F[测试与迭代]
ECharts 提供了丰富的配置选项,让开发者能够创建出既美观又实用的仪表盘组件。
拓展资源: