APP端:
因为APP端无法使用uni的camera组件,最开始考虑使用内嵌webview的方式,通过原生dom调用video渲染画面然后通过canvas截图。但是此方案兼容性在ios几乎为0,如果app只考虑安卓端的话可以采用此方案。后面又想用live-pusher组件来实现,但是发现快照api好像需要真实流地址才能截取图像。因为种种原因,也是安卓ios双端兼容性不佳。最终决定采用5+api实现。经实测5+api兼容性还算可以,但是毕竟是调用原生能力,肯定是没有原生开发那么丝滑的,难免会出现一些不可预测的兼容性问题。所以建议app和手机硬件交互强的话还是不要用uni开发了。不然真的是翻文档能翻死人。社区也找不到靠谱的解决方案。

5+api 文档
https://www.html5plus.org/doc/zh_cn/video.html#plus.video.createLivePusher

就是使用这个api调用原生的camera完成。并且可以直接在预览模式下完成快照,也不需要真实的推流地址。

<template><view class="content"><view class="c-footer"><view class="msg-box"><view class="msg">1、请保证本人验证。</view><view class="msg">2、请使头像正面位于画框中。</view><view class="msg">3、请使头像尽量清晰。</view><view class="msg">4、请保证眼镜不反光,双眼可见。</view><view class="msg">5、请保证无墨镜,口罩,面膜等遮挡物。</view><view class="msg">6、请不要化浓妆,不要戴帽子。</view></view><view class="but" @click="snapshotPusher" v-if="!cilckSwitch">采集本人人脸</view></view></view>
</template><script>import permission from '../../util/permission.js'export default {data() {return {type: '', //是否是补签拉起的人脸识别imgData: '',pusher: null,scanWin: null,faceInitTimeout: null,snapshTimeout: null,uploadFileTask: null,cilckSwitch: false, //防止多次点击};},methods: {//人脸比对handleFaceContrast(param) {uni.hideLoading()this.$http({url: '/API_AUTH/AppIaiFace/faceContrast.api',data: {...param,userid: uni.getStorageSync('userInfo').id}}).then(res => {console.log(res)if (res.data.compareResult == 1) {let pages = getCurrentPages(); //获取所有页面栈实例列表let nowPage = pages[pages.length - 1]; //当前页页面实例let prevPage = pages[pages.length - 2]; //上一页页面实例if (this.type == 'signOut') {prevPage.$vm.signOutXh = param.xh;prevPage.$vm.signOutPhotoPath = param.path} else {prevPage.$vm.xh = param.xh;prevPage.$vm.photoPath = param.path}uni.navigateBack()} else {uni.showToast({title: '人脸比对不通过,请重试',icon: 'none'})this.cilckSwitch = false}}).catch(err => {uni.showToast({title: '人脸比对不通过,请重试',icon: 'none'})this.cilckSwitch = false})},//初始化faceInit() {uni.showLoading({title: '请稍后...'})this.faceInitTimeout = setTimeout(async () => {//创建livepusherif (uni.getSystemInfoSync().platform === 'android') {const data1 = await permission.requestAndroidPermission("android.permission.RECORD_AUDIO")const data2 = await permission.requestAndroidPermission("android.permission.CAMERA")console.log(data1,data2,1111)if (data1 == 1 && data2 == 1) {this.pusherInit();}} else {this.pusherInit();}//覆盖在视频之上的内容,根据实际情况编写// this.scanWin = plus.webview.create('/hybrid/html/faceTip.html', '', {//  background: 'transparent'// });//新引入的webView显示// this.scanWin.show();//初始化上传本地文件的apithis.initUploader()}, 500);},//初始化播放器pusherInit() {const currentWebview = this.$mp.page.$getAppWebview();this.pusher = plus.video.createLivePusher('livepusher', {url: '',top: '0px',left: '0px',width: '100%',height: '50%',position: 'absolute',aspect: '9:16',muted: false,'z-index': 999999,'border-radius': '50%',});currentWebview.append(this.pusher);//反转摄像头this.pusher.switchCamera();//开始预览this.pusher.preview();uni.hideLoading()},//初始化读取本地文件initUploader() {let that = thisthis.uploadFileTask = plus.uploader.createUpload("完整的接口请求地址", {method: "POST",headers: {// 修改请求头Content-Type类型 此类型为文件上传"Content-Type": "multipart/form-data"}},// data:服务器返回的响应值 status: 网络请求状态码(data, status) => {// 请求上传文件成功if (status == 200) {console.log(data)// 获取data.responseText之后根据自己的业务逻辑做处理let result = JSON.parse(data.responseText);console.log(result.data.xh)that.handleFaceContrast({xh: result.data.xh,path: result.data.path})}// 请求上传文件失败else {uni.showToast({title: '上传图片失败',icon: 'none'})console.log("上传失败", status)that.cilckSwitch = falseuni.hideLoading()}});},//快照snapshotPusher() {if (this.cilckSwitch) {uni.showToast({title: '请勿频繁点击',icon: 'none'})return}this.cilckSwitch = trueuni.showLoading({title: '正在比对,请勿退出'})let that = thisthis.snapshTimeout = setTimeout(() => {this.pusher.snapshot(e => {// this.pusher.close();// this.scanWin.hide();//拿到本地文件路径var src = e.tempImagePath;this.uploadImg(src)//获取图片base64// this.getMinImage(src);},function(e) {plus.nativeUI.alert('snapshot error: ' + JSON.stringify(e));that.cilckSwitch = falseuni.hideLoading()});}, 500);},//调用原生能力读取本地文件并上传uploadImg(imgPath) {this.uploadFileTask.addFile('file://' + imgPath, {key: "file" // 填入图片文件对应的字段名});//添加其他表单字段(参数) 两个参数可能都只支持传字符串// uploadFileTask.addData("参数名", 参数值);this.uploadFileTask.start();},//获取图片base64getMinImage(imgPath) {plus.zip.compressImage({src: imgPath,dst: imgPath,overwrite: true,quality: 40},zipRes => {setTimeout(() => {var reader = new plus.io.FileReader();reader.onloadend = res => {var speech = res.target.result; //base64图片console.log(speech.length);console.log(speech)this.imgData = speech;};reader.readAsDataURL(plus.io.convertLocalFileSystemURL(zipRes.target));}, 1000);},function(error) {console.log('Compress error!', error);});},},onLoad(option) {//#ifdef APP-PLUSthis.type = option.typethis.faceInit();//#endif},onHide() {console.log('hide')this.faceInitTimeout && clearTimeout(this.faceInitTimeout);this.snapshTimeout && clearTimeout(this.snapshTimeout);// this.scanWin.hide();},onBackPress() {// let pages = getCurrentPages(); //获取所有页面栈实例列表// let nowPage = pages[pages.length - 1]; //当前页页面实例// let prevPage = pages[pages.length - 2]; //上一页页面实例// prevPage.$vm.xh = '11111';// prevPage.$vm.photoPath = '22222' this.faceInitTimeout && clearTimeout(this.faceInitTimeout);this.snapshTimeout && clearTimeout(this.snapshTimeout);// this.scanWin.hide();},onUnload() {this.faceInitTimeout && clearTimeout(this.faceInitTimeout);this.snapshTimeout && clearTimeout(this.snapshTimeout);// this.scanWin.hide();},};
</script><style lang="scss" scoped>.but {margin: 50rpx auto 0;border-radius: 10px;width: 80%;height: 100rpx;display: flex;align-items: center;justify-content: center;background-color: #008AFF;font-size: 16px;color: #FFFFFF;}.c-footer {width: 100%;position: fixed;top: 50%;left: 0;z-index: 10;padding: 30rpx 0;.msg-box {width: 80%;margin: 0 auto;text-align: left;.msg {margin-bottom: 15rpx;font-size: 13px;color: #666;}}}.img-data {width: 100%;height: auto;}.content {background-color: #fff;}
</style>

以上是完整的包含逻辑的代码。

关键代码
//初始化播放器pusherInit() {const currentWebview = this.$mp.page.$getAppWebview();this.pusher = plus.video.createLivePusher('livepusher', {url: '',top: '0px',left: '0px',width: '100%',height: '50%',position: 'absolute',aspect: '9:16',muted: false,'z-index': 999999,'border-radius': '50%',});currentWebview.append(this.pusher);//反转摄像头this.pusher.switchCamera();//开始预览this.pusher.preview();uni.hideLoading()},//快照snapshotPusher() {if (this.cilckSwitch) {uni.showToast({title: '请勿频繁点击',icon: 'none'})return}this.cilckSwitch = trueuni.showLoading({title: '正在比对,请勿退出'})let that = thisthis.snapshTimeout = setTimeout(() => {this.pusher.snapshot(e => { //拿到本地文件路径var src = e.tempImagePath;//这里因为接口参数需要加密,用base64的话加密出来的参数太大了,所以选择了直接读取本地文件上传文件流的方式。this.uploadImg(src)//获取图片base64// this.getMinImage(src);},function(e) {plus.nativeUI.alert('snapshot error: ' + JSON.stringify(e));that.cilckSwitch = falseuni.hideLoading()});}, 500);},//获取图片base64getMinImage(imgPath) {plus.zip.compressImage({src: imgPath,dst: imgPath,overwrite: true,quality: 40},zipRes => {setTimeout(() => {var reader = new plus.io.FileReader();reader.onloadend = res => {var speech = res.target.result; //base64图片console.log(speech.length);console.log(speech)this.imgData = speech;};reader.readAsDataURL(plus.io.convertLocalFileSystemURL(zipRes.target));}, 1000);},function(error) {console.log('Compress error!', error);});},//初始化读取本地文件initUploader() {let that = thisthis.uploadFileTask = plus.uploader.createUpload("完整的接口请求地址", {method: "POST",headers: {// 修改请求头Content-Type类型 此类型为文件上传"Content-Type": "multipart/form-data"}},// data:服务器返回的响应值 status: 网络请求状态码(data, status) => {// 请求上传文件成功if (status == 200) {console.log(data)// 获取data.responseText之后根据自己的业务逻辑做处理let result = JSON.parse(data.responseText);console.log(result.data.xh)that.handleFaceContrast({xh: result.data.xh,path: result.data.path})}// 请求上传文件失败else {uni.showToast({title: '上传图片失败',icon: 'none'})console.log("上传失败", status)that.cilckSwitch = falseuni.hideLoading()}});},//调用原生能力读取本地文件并上传uploadImg(imgPath) {this.uploadFileTask.addFile('file://' + imgPath, {key: "file" // 填入图片文件对应的字段名});//添加其他表单字段(参数) 两个参数可能都只支持传字符串// uploadFileTask.addData("参数名", 参数值);this.uploadFileTask.start();},

以上就是关键的代码。
接下来补充几个坑的地方。创建出来的livepusher层级很高,无法在同一页面被别的元素遮挡。所以想要在他上面写样式是行不通了。只能再创建一个webview。然后将这个webview覆盖在livepusher上,达到人脸识别页面的样式。

//覆盖在视频之上的内容,根据实际情况编写this.scanWin = plus.webview.create('/hybrid/html/faceTip.html', '', {background: 'transparent'});//新引入的webView显示this.scanWin.show();//新引入的webView影藏this.scanWin.hide();

这种方案在ios基本没问题。至少目前没遇到过。但是安卓就一言难尽了。首先这个组件默认调起的是后置摄像头,这显然不符合我们的需求。但是官方提供的文档里也没有明确支持可以配置优先调起哪个摄像头。好早提供了一个switchCamera的api可以翻转摄像头。

但是在安卓系统上,尤其是鸿蒙系统,调用这个api就会导致程序闪退,而且发生频率还特别高。这个问题至今不知道该怎么解决。除了闪退问题,安卓还存在一个麻烦事儿,那就是首次进入app,翻转摄像头的api没有用,拉起的还是后置摄像头。但是后续再进入app就无此问题了。后面折腾来折腾去,发现好像是首次进入拉起授权弹窗的时候才会出现这种问题。
然后写了个定时器做测试,五秒之后再拉起摄像头再去翻转摄像头。然后再五秒内赶紧把授权给同意了。结果发现翻转竟然生效了。
然后决定再渲染推流元素之前先让用户通过权限授权,然后再拉起摄像头。 也就是上文完整代码中的

 //创建livepusherif (uni.getSystemInfoSync().platform === 'android') {const data1 = await permission.requestAndroidPermission("android.permission.RECORD_AUDIO")const data2 = await permission.requestAndroidPermission("android.permission.CAMERA")console.log(data1,data2,1111)if (data1 == 1 && data2 == 1) {this.pusherInit();}} else {this.pusherInit();}

具体的意思就不过多赘述了,自行看permission的文档。或者看他的代码。很简单
permission下载地址
https://ext.dcloud.net.cn/plugin?id=594
以上就是调用原生能力拉起摄像头实现快照功能的所有内容了。

下面也记录一下web端如果实现这种功能,毕竟当时搞出来也不容易,但是最终还是败在了兼容性上

方案的话大致有两种,一种是借助tracking js 有兴趣的可以了解一下,一个web端人脸识别库。他可以识别画面中是否出现人脸。以及一下更高级的功能我就没有去探索了。有需要的可以自行研究

<!doctype html>
<html><head><meta charset="utf-8"><title>人脸识别</title><script src="../html/js/tracking-min.js"></script><script src="../html/js/face-min.js"></script><style>video,canvas {position: absolute;}</style></head><body><div class="demo-container"><video id="video" width="320" height="240" preload autoplay loop muted></video><canvas id="canvas" width="320" height="240"></canvas></div><script>window.onload = function() {console.log(123123123)// 视频显示var video = document.getElementById('video');//   绘图var canvas = document.getElementById('canvas');var context = canvas.getContext('2d');var time = 10000;var tracker = new tracking.ObjectTracker('face');//   设置识别的放大比例tracker.setInitialScale(4);//   设置步长tracker.setStepSize(2);//   边缘密度tracker.setEdgesDensity(0.1);//   启动摄像头,并且识别视频内容var trackerTask = tracking.track('#video', tracker, {camera: true});var flag = true;tracker.on('track', function(event) {if (event.data.length === 0) {console.log('未检测到人脸')context.clearRect(0, 0, canvas.width, canvas.height);} else if (event.data.length > 1) { console.log('检测到多张人脸')} else {context.clearRect(0, 0, canvas.width, canvas.height);event.data.forEach(function(rect) {context.strokeStyle = '#ff0000';context.strokeRect(rect.x, rect.y, rect.width, rect.height);context.fillStyle = "#ff0000";//console.log(rect.x, rect.width, rect.y, rect.height);});if (flag) {console.log("拍照");context.drawImage(video, 0, 0, 320, 240);saveAsLocalImage();context.clearRect(0, 0, canvas.width, canvas.height);flag = false;setTimeout(function() {flag = true;}, time);} else {//console.log("冷却中");}}});};function saveAsLocalImage() {var myCanvas = document.getElementById("canvas");// here is the most important part because if you dont replace you will get a DOM 18 exception.  // var image = myCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream;Content-Disposition: attachment;filename=foobar.png");  var image = myCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream");// window.location.href = image; // it will save locally  // create temporary link  returnvar tmpLink = document.createElement('a');tmpLink.download = 'image.png'; // set the name of the download file tmpLink.href = image;// temporarily add link to body and initiate the download  document.body.appendChild(tmpLink);tmpLink.click();document.body.removeChild(tmpLink);}</script></body></html>

另外一种就是纯video+canvas截取一张视频中的画面。

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>人脸采集</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" type="text/css" href="./css/index.css" /><script src="./js/jq.js" type="text/javascript" charset="utf-8"></script><!-- uni 的 SDK --><script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/vConsole/3.0.0/vconsole.min.js" type="text/javascript"charset="utf-8"></script><script>// init vConsole,app中查看var vConsole = new VConsole();// console.log('Hello world');</script><style>.mui-content {margin: 0 auto;text-align: center;border: 0px solid red;}/*摄像头翻转180度*/video {transform: rotateY(180deg);-webkit-transform: rotateY(180deg);/* Safari 和 Chrome */-moz-transform: rotateY(180deg);}</style></head><body class='body'><div class="mui-content"><div style="margin: 40px auto;"><!-- <input type="file" id='image' accept="image/*" capture='user'> --><video id="video" style="margin: 0 auto; border-radius: 150px;"></video><canvas id='canvas' width="300" height="300"style="border: 0px solid red;margin: auto; display: none;"></canvas></div><div class="msg-box"><div class="msg">1、请保证本人验证。</div><div class="msg">2、请使头像正面位于画框中。</div><div class="msg">3、请使头像尽量清晰。</div><div class="msg">4、请保证眼镜不反光,双眼可见。</div><div class="msg">5、请保证无墨镜,口罩,面膜等遮挡物。</div><div class="msg">6、请不要化浓妆,不要戴帽子。</div></div><div style="width: 80%; position: absolute; bottom: 20px; left: 50%; transform: translate(-50%, -50%);"><div class="but" id="start">采集本人人脸</div></div></div></body><script type="text/javascript">var video, canvas, vendorUrl, interval, videoHeight, videoWidth, time = 0;// 获取webview页面数据// var data = JSON.parse(getUrlParam('data'))// var info = data.info;// var userInfo = data.userInfo;// const userId = data.userId; // 当前登录用户id$(function() {// 初始化initVideo()// uni.app事件document.addEventListener('UniAppJSBridgeReady', function() {uni.getEnv(function(res) {console.log('当前环境:' + JSON.stringify(res));});setInterval(() => {uni.postMessage({data: {action: 'postMessage'}});}, 1000)});})// 获取url携带的数据function getUrlParam(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");var r = window.location.search.substr(1).match(reg);if (r != null) return unescape(r[2]);return null;}// 摄像头初始化function initVideo() {console.log('摄像头初始化')video = document.getElementById("video");videoHeight = 300videoWidth = 300setTimeout(() => {console.log(navigator)navigator.mediaDevices.getUserMedia({video: {width: {ideal: videoWidth,max: videoWidth},height: {ideal: videoHeight,max: videoHeight},facingMode: 'user', //前置摄像头// facingMode: { exact: "environment" }, //后置摄像头frameRate: {ideal: 30,min: 10}}}).then(videoSuccess).catch(videoError);if (navigator.mediaDevices.getUserMedia ||navigator.getUserMedia ||navigator.webkitGetUserMedia ||navigator.mozGetUserMedia ||navigator.mediaCapabilities) {console.log('调用用户媒体设备, 访问摄像头')//调用用户媒体设备, 访问摄像头getUserMedia({video: {width: {ideal: videoWidth,max: videoWidth},height: {ideal: videoHeight,max: videoHeight},facingMode: 'user', //前置摄像头// facingMode: { exact: "environment" }, //后置摄像头frameRate: {ideal: 30,min: 10}}},videoSuccess,videoError);} else {}}, 300);}// 获取用户设备function getUserMedia(constraints, success, error) {if (navigator.mediaDevices.getUserMedia) {//最新的标准APInavigator.mediaDevices.getUserMedia(constraints).then(success).catch(error);} else if (navigator.webkitGetUserMedia) {//webkit核心浏览器navigator.webkitGetUserMedia(constraints, success, error);} else if (navigator.mozGetUserMedia) {//firfox浏览器navigator.mozGetUserMedia(constraints, success, error);} else if (navigator.getUserMedia) {//旧版APInavigator.getUserMedia(constraints, success, error);}}// 开始有画面function videoSuccess(stream) {//this.mediaStreamTrack = stream;console.log("=====stream")video.srcObject = stream;video.play();//$("#max-bg").css('background-color', 'rgba(0,0,0,0)')// 这里处理我的的东西}function videoError(error) {alert('摄像头获取错误')console.log('摄像头获取错误')setTimeout(() => {initVideo()}, 6000)}// 单次拍照function getFaceImgBase64() {canvas = document.getElementById('canvas');//绘制canvas图形canvas.getContext('2d').drawImage(video, 0, 0, 300, 300);//把canvas图像转为img图片var bdata = canvas.toDataURL("image/jpeg");//img.src = canvas.toDataURL("image/png");return bdata.split(',')[1]; //照片压缩成base位数据}$('#start').on('click', function() {time = 0;console.log("开始人脸采集,请正对屏幕");faceGather();})// 人脸采集function faceGather() {const faceImgBase64 = getFaceImgBase64();console.log(faceImgBase64);}</script>
</html>

uniapp人脸识别解决方案相关推荐

  1. 企业人脸识别解决方案,让员工考勤更高效

    随着我国经济的快速发展,各行各业都涌现出了很多公司,而上下班打卡是考勤中最常见的方式.国内的考勤系统大多是以指纹打卡.APP打卡为主,这些打卡方式需要人员用手操作,但是一旦公司人员过多,指纹打卡就显得 ...

  2. 多路人脸识别解决方案

    方案背景 传统的安防需要配置多台监控设备进行多个摄像头监控,人脸核查时比较依赖人力,并且视频画面技术较差,追踪目标困难,容易造成人力和物力的消耗. 该方案采用的Cluster Server R1集群服 ...

  3. 智慧校园人脸识别解决方案

    学校放假,正式建设改造好阶段,学生的安全一直是学校非常重视的,怎么创造一个安全的校园环境,保证学生老师的学习.校园生活.除了学校的管理之外,还需要设备的支持,要做到无接触式进出,刷脸进入教室,实验室, ...

  4. 人脸识别解决方案有哪些?

    人脸识别解决方案有哪些?人脸识别通行解决方案有哪些?人脸识别闸机解决方案有哪些? 相关解决方案有:大华.海康.中控等.

  5. RK3568行业平板人脸识别解决方案

    方案背景 随着人工智能的发展,人脸识别的应用场景需求越来越多,但是这些应用落地往往需要复杂且漫长的软硬件研发.而RK3568行业平板人脸识别解决方案,是包含了硬件.系统.算法.应用软件的一站式解决方案 ...

  6. 人脸识别太常见?好用才行,利尔达推出一体化人脸识别解决方案

    人脸识别技术对输入的人脸图像或者视频流进行处理,是一种通过提取每张人脸中的特征信息进行身份鉴别的前沿识别技术. 伴随着智能化时代的悄然到来,人脸识别技术日益普及并被大规模地应用于日常生活场景中.从安防 ...

  7. Face_recognition与人脸识别解决方案

    Face_recognition 与人脸识别解决方案 很久之前做的,好像是从github上参考一老外的,用到了当前比较火的face_recognition第三方库,我在此基础上做了一些改进 现在可以在 ...

  8. 宿舍人脸识别解决方案的用处有哪些

    大学校园具有开放性,通常会有其他学校的人员或是社会上的人员进入学校,使校园中容易发生安全隐患.随着智慧校园的发展,学校引入人脸识别技术应用,通过智能技术提高校园管理.例如说在宿舍管理方面的应用,校园宿 ...

  9. 马拉松赛事中应用人脸识别解决方案

    马拉松参赛的人员众多,以往会出现替跑等事件的发生,如今在马拉松赛事中使用到人脸识别技术,通过智能技术保证赛事的公平公正性.在马拉松赛事应用人脸识别解决方案可以实现什么操作呢? 参赛选手检录 在参赛选手 ...

最新文章

  1. 我的ExtJS学习之路 ——4
  2. 面试Nginx的几个常见问题(
  3. wp7 给TextBox设置圆角边框
  4. mysql memory leak_解决memory leak问题
  5. jcmd 命令_jcmd:一个可以全部统治的JDK命令行工具
  6. 操作系统之内存管理:4、基本地址变换机构(段氏、页式、段页式)
  7. php获取当前页面数据,ThinkPHP如何获取当前页面URL信息?
  8. 55 - 算法 -动态规划 -数塔问题 感觉都是数组建模 递推方法规则
  9. python查找文件名_Python实现的根据文件名查找数据文件功能示例
  10. Channel shutdown: channel error; protocol method
  11. Windows Django 开发环境搭建
  12. 无法进入一个空框_DeNoise AI无法从Photoshop作为插件启动?
  13. 如何选择适合自己的项目管理方法
  14. 南洋生活,聊聊新加坡的房地产
  15. 【工具推荐】进程查看管理工具——Process Explorer
  16. 易到CEO巩振兵被曝本周已离职 其称“在开会”
  17. SQL Server设置SQL Server身份验证
  18. Java图像处理--------RGB调色面板
  19. CCD摄像头相关知识
  20. 震惊!90%的简历,竟然都犯过这些错误……

热门文章

  1. C语言课设会员计费系统(大作业)
  2. 响应式导航(从水平到垂直)的分析与实现
  3. 【花雕动手做】有趣好玩的音乐可视化系列项目(28)--LED乒乓球灯
  4. 期刊勘误、关注和撤稿:科研作者该知道的信息(转载)
  5. 在线制作流程图的实用工具网站
  6. mysql故障应急_Mysql错误:紧急救助!!!
  7. 停滞在一个圈子,决定人生的高低![深度文章]
  8. 精选6个超实用的Word技巧,每一个大有用处!
  9. 智慧交通数字孪生IOC系统
  10. Win2003系统安装SQL Sever2000后1433端口未开放的解释