待整理笔记
长列表优化
业务开发中存在长列表显示需求,会出现几千条数据的情况,也就意味着要一次性渲染几千条数据,会造成明显的卡顿,这时就有必要对聊天列表进行性能优化来提升用户体验
对于这种需求,可能很多人首先想到的就是使用懒加载进行性能优化,但是对于很长的列表来说懒加载有三个致命的缺点:
- 如果一直加载到底, 那么最终还是会出现大量的DOM节点,导致滚动不流畅
- 想要定位到某一个位置的数据会非常困难
- 滚动条无法正确反映操作者当前浏览的信息在全部列表中的位置。而且大量数据加载,一次给我加载十几条,滚到底太慢了
懒加载无法满足真正的长列表展示,那么如果真正要解决此类问题该怎么办?还有一种思路就是:列表局部渲染,又被称为虚拟滚动
虚拟滚动
<template>
<div>
<input type="text" v-model.number="dataLength" />条
<div class="virtual-scroller" @scroll="onScroll" :style="{ height: 600 + 'px' }">
<div class="phantom" :style="{ height: dataLength * itemHeight + 'px' }">
<ul :style="{ 'margin-top': `${scrollTop}px` }">
<li v-for="item in visibleList" :key="item.name" :style="{ height: `${itemHeight}px`, 'line-height': `${itemHeight}px` }">
<div>
<div>{{ item.name }}</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
let itemHeight = ref(60)
let dataLength = ref(500000)
let startIndex = ref(0)
let endIndex = ref(10)
let scrollTop = ref(0)
const dataList = computed(() => {
const newDataList = [...Array(dataLength.value || 0).keys()].map((v, i) => ({
name: `第${i + 1}项`,
height: itemHeight.value
}))
return newDataList
})
const visibleList = computed(() => {
console.log(dataList.value)
return dataList.value.slice(startIndex.value, endIndex.value)
})
function onScroll(e: any) {
const s = e.target.scrollTop
scrollTop.value = s
startIndex.value = Math.floor(s / itemHeight.value)
endIndex.value = startIndex.value + 10
}
</script>
<style scoped>
.virtual-scroller {
height: 600px;
overflow: auto;
}
.phantom {
overflow: hidden;
}
</style>
<template>
<div>
<input type="text" v-model.number="dataLength" />条
<div class="virtual-scroller" @scroll="onScroll" :style="{ height: 600 + 'px' }">
<div class="phantom" :style="{ height: dataLength * itemHeight + 'px' }">
<ul :style="{ 'margin-top': `${scrollTop}px` }">
<li v-for="item in visibleList" :key="item.name" :style="{ height: `${itemHeight}px`, 'line-height': `${itemHeight}px` }">
<div>
<div>{{ item.name }}</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
let itemHeight = ref(60)
let dataLength = ref(500000)
let startIndex = ref(0)
let endIndex = ref(10)
let scrollTop = ref(0)
const dataList = computed(() => {
const newDataList = [...Array(dataLength.value || 0).keys()].map((v, i) => ({
name: `第${i + 1}项`,
height: itemHeight.value
}))
return newDataList
})
const visibleList = computed(() => {
console.log(dataList.value)
return dataList.value.slice(startIndex.value, endIndex.value)
})
function onScroll(e: any) {
const s = e.target.scrollTop
scrollTop.value = s
startIndex.value = Math.floor(s / itemHeight.value)
endIndex.value = startIndex.value + 10
}
</script>
<style scoped>
.virtual-scroller {
height: 600px;
overflow: auto;
}
.phantom {
overflow: hidden;
}
</style>
vue中data是函数而不是对象
JS中的对象是引用类型的数据,当多个实例引用同一个对象的时候,只要其中一个实例对该对象进行操作的话,其他实例中的数据也会发生变化;而在Vue中更多的是组件复用思想,需要每个组件都有该组件自己的数据,这样组件之间才不会相互影响干扰。因此,Vue组件中的数据不能写成对象的形式,要写成函数的形式;组件中data写成一个函数,数据以函数返回值来定义,每次组件复用的时候就会返回一个新的data,这样每个组件都有属于该组件的专有空间,各个组件有各自的数据,这样就不会干扰其他组件的运行操作
vue2双向绑定
Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图.
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
在Vue中其实就是通过Object.defineProperty来劫持对象属性的setter和getter操作,并“种下”一个监听器,当数据发生变化的时候发出通知。
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)
vue优点?
答:轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十kb; 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习; 双向数据绑定:保留了angular的特点,在数据操作方面更为简单; 组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势; 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作; 虚拟DOM:dom操作是非常耗费性能的, 不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式; 运行速度更快:相比较与react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势。
vue父组件向子组件传递数据?
答:通过props
子组件向父组件传递事件?
答:$emit方法
v-show和v-if指令的共同点和不同点?
答: 共同点:都能控制元素的显示和隐藏; 不同点:实现本质方法不同,v-show本质就是通过控制css中的display设置为none,控制隐藏,只会编译一次;v-if是动态的向DOM树内添加或者删除DOM元素,若初始值为false,就不会编译了。而且v-if不停的销毁和创建比较消耗性能。 总结:如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大)。
如何让CSS只在当前组件中起作用?
答:在组件中的style前面加上scoped
keep-alive 的作用是什么?
答:keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
如何获取dom?
答:ref="domName" 用法:this.$refs.domName
说出几种vue当中的指令和它的用法?
答:v-model双向数据绑定; v-for循环; v-if v-show 显示与隐藏; v-on事件;v-once: 只绑定一次。
vue-loader是什么?使用它的用途有哪些?
答:vue文件的一个加载器,将template/js/style转换成js模块。 用途:js可以写es6、style样式可以scss或less、template可以加jade等
为什么使用key?
答:需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。 作用主要是为了高效的更新虚拟DOM。
axios及安装?
答:请求后台资源的模块。npm install axios --save装好, js中使用import进来,然后.get或.post。返回在.then函数中如果成功,失败则是在.catch函数中。
v-modal的使用。
答:v-model用于表单数据的双向绑定,其实它就是一个语法糖,这个背后就做了两个操作: v-bind绑定一个value属性; v-on指令给当前元素绑定input事件
请说出vue.cli项目中src目录每个文件夹和文件的用法?
答:assets文件夹是放静态资源;components是放组件;router是定义路由相关的配置; app.vue是一个应用主组件;main.js是入口文件。
分别简述computed和watch的使用场景
答:computed: 当一个属性受多个属性影响的时候就需要用到computed 最典型的栗子: 购物车商品结算的时候 watch: 当一条数据影响多条数据的时候就需要用watch 栗子:搜索数据
v-on可以监听多个方法吗?
答:可以,例子:
$nextTick的使用
答:当你修改了data的值然后马上获取这个dom元素的值,是不能获取到更新后的值, 你需要使用$nextTick这个回调,让修改后的data值渲染更新到dom元素之后在获取,才能成功。
vue组件中data为什么必须是一个函数?
答:因为JavaScript的特性所导致,在component中,data必须以函数的形式存在,不可以是对象。 组件中的data写成一个函数,数据以函数返回值的形式定义,这样每次复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱。而单纯的写成对象形式,就是所有的组件实例共用了一个data,这样改一个全都改了。
渐进式框架的理解
答:主张最少;可以根据不同的需求选择不同的层级;
Vue中双向数据绑定是如何实现的?
答:vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现的, 也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变; 核心:关于VUE双向数据绑定,其核心是 Object.defineProperty()方法。
单页面应用和多页面应用区别及优缺点
答:单页面应用(SPA),通俗一点说就是指只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多应用于pc端。 多页面(MPA),就是指一个应用中有多个页面,页面跳转时是整页刷新 单页面的优点: 用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小;前后端分离;页面效果会比较炫酷(比如切换页面内容时的专场动画)。 单页面缺点: 不利于seo;导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理);初次加载时耗时多;页面复杂度提高很多。
v-if和v-for的优先级
答:当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。所以,不推荐v-if和v-for同时使用。 如果v-if和v-for一起用的话,vue中的的会自动提示v-if应该放到外层去。
assets和static的区别
答:相同点:assets和static两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点 不相同点:assets中存放的静态资源文件在项目打包时,也就是运行npm run build时会将assets中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html一同上传至服务器。static中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于assets中打包后的文件提交较大点。在服务器中就会占据更大的空间。 建议:将项目中template需要的样式文件js文件等都可以放置在assets中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在static中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
vue常用的修饰符
答:.stop:等同于JavaScript中的event.stopPropagation(),防止事件冒泡; .prevent:等同于JavaScript中的event.preventDefault(),防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播); .capture:与事件冒泡的方向相反,事件捕获由外到内; .self:只会触发自己范围内的事件,不包含子元素; .once:只会触发一次。
vue的两个核心点
答:数据驱动、组件系统 数据驱动:ViewModel,保证数据和视图的一致性。 组件系统:应用类UI可以看作全部是由组件树构成的。
vue和jQuery的区别
答:jQuery是使用选择器($)选取DOM对象,对其进行赋值、取值、事件绑定等操作,其实和原生的HTML的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。比如需要获取label标签的内容:$("lable").val();,它还是依赖DOM元素的值。 Vue则是通过Vue对象将数据和View完全分离开来了。对数据进行操作不再需要引用相应的DOM对象,可以说数据和View是分离的,他们通过Vue对象这个vm实现相互的绑定。这就是传说中的MVVM。
引进组件的步骤
答: 在template中引入组件; 在script的第一行用import引入路径; 用component中写上组件名称。
delete和Vue.delete删除数组的区别
答:delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。Vue.delete 直接删除了数组 改变了数组的键值。
SPA首屏加载慢如何解决
答:安装动态懒加载所需插件;使用CDN资源。
Vue-router跳转和location.href有什么区别
vue slot
答:简单来说,假如父组件需要在子组件内放一些DOM,那么这些DOM是显示、不显示、在哪个地方显示、如何显示,就是slot分发负责的活。
vue项目是打包了一个js文件,一个css文件,还是有多个文件?
答:根据vue-cli脚手架规范,一个js文件,一个CSS文件。
Vue里面router-link在电脑上有用,在安卓上没反应怎么解决?
答:Vue路由在Android机上有问题,babel问题,安装babel polypill插件解决。
Vue2中注册在router-link上事件无效解决方法
答: 使用@click.native。原因:router-link会阻止click事件,.native指直接监听一个原生事件。
RouterLink在IE和Firefox中不起作用(路由不跳转)的问题
答: 方法一:只用a标签,不适用button标签;方法二:使用button标签和Router.navigate方法
axios的特点有哪些
答:从浏览器中创建XMLHttpRequests; node.js创建http请求; 支持Promise API; 拦截请求和响应; 转换请求数据和响应数据; 取消请求; 自动换成json。 axios中的发送字段的参数是data跟params两个,两者的区别在于params是跟请求地址一起发送的,data的作为一个请求体进行发送 params一般适用于get请求,data一般适用于post put 请求。
params和query的区别
答:用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是this.$route.query.name和this.$route.params.name。 url地址显示:query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示 注意点:query刷新不会丢失query里面的数据 params刷新 会 丢失 params里面的数据。
vue初始化页面闪动问题
答:使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。 首先:在css里加上[v-cloak] { display: none; }。 如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
vue更新数组时触发视图更新的方法
答:push();pop();shift();unshift();splice(); sort();reverse()
vue修改打包后静态资源路径的修改
答:cli2版本:将 config/index.js 里的 assetsPublicPath 的值改为 './' 。 build: { ... assetsPublicPath: './', ... } cli3版本:在根目录下新建vue.config.js 文件,然后加上以下内容:(如果已经有此文件就直接修改) module.exports = { publicPath: '', // 相对于 HTML 页面(目录相同) }
生命周期函数面试题
1.什么是 vue 生命周期?有什么作用?
答:每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做 生命周期钩子 的函数,这给了用户在不同阶段添加自己的代码的机会。(ps:生命周期钩子就是生命周期函数)例如,如果要通过某些插件操作DOM节点,如想在页面渲染完后弹出广告窗, 那我们最早可在mounted 中进行。
2.第一次页面加载会触发哪几个钩子?
答:beforeCreate, created, beforeMount, mounted
3.简述每个周期具体适合哪些场景
答:beforeCreate:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法 create:data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作 beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的 mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。 如果我们想要通过插件操作页面上的DOM节点,最早可以在和这个阶段中进行 beforeUpdate: 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的, 页面还没有和最新的数据保持同步 updated:页面显示的数据和data中的数据已经保持同步了,都是最新的 beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁 destroyed: 这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。
4.created和mounted的区别
答:created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。 mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
5.vue获取数据在哪个周期函数
答:一般 created/beforeMount/mounted 皆可. 比如如果你要操作 DOM , 那肯定 mounted 时候才能操作.
6.请详细说下你对vue生命周期的理解?
答:总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。 创建前/后: 在beforeCreated阶段,vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,$el还没有。 载入前/后:在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。 更新前/后:当data变化时,会触发beforeUpdate和updated方法。 销毁前/后:在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。
vue路由知识
1.mvvm 框架是什么?
答:vue是实现了双向数据绑定的mvvm框架,当视图改变更新模型层,当模型层改变更新视图层。在vue中,使用了双向绑定技术,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。
2.vue-router 是什么?它有哪些组件
答:vue用来写路由一个插件。router-link、router-view
3.active-class 是哪个组件的属性?
答:vue-router模块的router-link组件。children数组来定义子路由
4.怎么定义 vue-router 的动态路由? 怎么获取传过来的值?
答:在router目录下的index.js文件中,对path属性加上/:id。 使用router对象的params.id。
5.vue-router 有哪几种导航钩子?
答:三种, 第一种:是全局导航钩子:router.beforeEach(to,from,next),作用:跳转前进行判断拦截。 第二种:组件内的钩子 第三种:单独路由独享组件
6.$route 和 $router 的区别
答:$router是VueRouter的实例,在script标签中想要导航到不同的URL,使用$router.push方法。返回上一个历史history用$router.to(-1) $route为当前router跳转对象。里面可以获取当前路由的name,path,query,parmas等。
7.vue-router的两种模式
答:hash模式:即地址栏 URL 中的 # 符号; history模式:window.history对象打印出来可以看到里边提供的方法和记录长度。利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)。
8.vue-router实现路由懒加载( 动态加载路由 )
答:三种方式 第一种:vue异步组件技术 ==== 异步加载,vue-router配置路由 , 使用vue的异步组件技术 , 可以实现按需加载 .但是,这种情况下一个组件生成一个js文件。 第二种:路由懒加载(使用import)。 第三种:webpack提供的require.ensure(),vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
vuex知识
vuex是什么?怎么使用?哪种功能场景使用它?
vue框架中状态管理。在main.js引入store,注入。 新建了一个目录store.js,….. export 。 场景有:单页应用中,组件之间的状态。音乐播放、登录状态、加入购物车
2.vuex有哪几种属性?
有五种,分别是 State、 Getter、Mutation 、Action、 Module state => 基本数据(数据源存放地) getters => 从基本数据派生出来的数据 mutations => 提交更改数据的方法,同步 actions => 像一个装饰器,包裹mutations,使之可以异步。 modules => 模块化Vuex
3.Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?
如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里 如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用
功能实现
前端人脸识别
<!-- 人脸识别 -->
<template>
<!-- v-show="faceSure" -->
<!-- :class="faceSure?'':'hideFalse'" -->
<div class="entryFace selfTestTracking" v-if="face">
<div class="bgNo" :class="isBg ? 'bg' : ''">
<div class="title">请将面部对准识别区域</div>
<div class="ts">
提示:在采集面人脸时摘掉口罩、帽子等遮挡面部的物品会使识别率更高。
</div>
</div>
<video
id="videoCamera"
width="1920"
height="1080"
preload
autoplay
loop
muted
x5-video-player-type="h5-page"
></video>
<canvas id="canvas" ref="canvas" width="1920" height="1080"></canvas>
<canvas
id="canvas-face"
width="1920"
height="1080"
style="display: none;"
></canvas>
<div class="bottom" v-show="showBtn">
<div class="left-btn" @click="exit">返回</div>
</div>
</div>
</template>
<script>
function dataURLtoFile(dataurl, filename) {
//将base64转换为文件
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, {
type: mime
})
}
import tracking from '@/assets/js/ytjTracking.js'
import '@/assets/js/face-min.js'
import axios from 'axios'
import { updateFace } from '@/util/api.js'
import { Toast } from 'vant'
export default {
name: 'testTracking',
data() {
return {
trackerTask: null,
trackering: null,
mediaStreamTrack: null,
faceSure: false,
loading: false,
clientWidth: '',
clientHeight: '',
offsetY: '',
offsetX: '',
change: '',
videoIdArr: [],
isBg: false,
userInfo: {},
showBtn: false
}
},
props: {
face: Boolean
},
mounted() {
// 获取用户信息
this.userInfo = JSON.parse(localStorage.getItem('UserInfo'))
console.log('config.videoId:' + config.videoId)
this.getCompetence(config.videoId)
},
methods: {
exit() {
console.log('关闭了啊')
this.mediaStreamTrack.srcObject.getTracks()[0].stop() // 关闭摄像头
this.trackerTask.stop() // 停止侦测
this.$parent.exit()
},
getCompetence(videoId) {
let flag = true
const _this = this
const video = (this.mediaStreamTrack =
document.getElementById('videoCamera'))
const canvasUpload = document.getElementById('canvas-face')
const contextUpload = canvasUpload.getContext('2d')
const tracker = new window.tracking.ObjectTracker('face')
contextUpload.translate(1920, 0)
contextUpload.scale(-1, 1)
tracker.setInitialScale(4)
tracker.setStepSize(2)
tracker.setEdgesDensity(0.1)
// 启动摄像头初始化
this.trackerTask = window.tracking.track(
'#videoCamera',
tracker,
{
camera: true
},
1920,
1080,
videoId
)
tracker.on('track', function (event) {
_this.showBtn = true
if (event.data.length) {
// 会不停的去检测人脸,所以这里需要做个锁
if (flag) {
// 裁剪出人脸并绘制
// contextUpload.clearRect(0, 0, 1920, 1080);
contextUpload.drawImage(video, 0, 0, 1920, 1080)
// contextUpload.drawImage(video,690,235,1230,700,1230,0,1230,700);
// contextUpload.drawImage(video,690,235,1230,700,1230,0,1230,700);
flag = false
// 人脸的basa64
const dataURL = canvasUpload.toDataURL('image/jpeg')
console.log(dataURL)
// img文件
var cc = dataURLtoFile(dataURL, 'name.png')
// 获取FormData对象
let fd = new FormData()
// 将图片传入formdata
fd.append('file', cc)
axios
.post(process.env.VUE_APP_URL + 'common/upload', fd, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((res) => {
_this.userInfo.headImgPath = res.data.data[0].fileUrl
if (res.data.status == 200) {
// 上传人脸
updateFace('post', _this.userInfo).then((res1) => {
if (res1.data.status == 200) {
console.log('关闭了啊')
// 关闭摄像头
_this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
_this.trackerTask.stop()
// console.log(res.data.data[0].fileUrl)
_this.$parent.acquisitionSuccess(
res.data.data[0].fileUrl
)
} else {
Toast(res1.data.msg)
setTimeout(() => {
flag = true
console.log('关闭了啊')
// 关闭摄像头
_this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
_this.trackerTask.stop()
_this.$parent.exit()
}, 3000)
}
})
} else {
Toast(res.data.message)
}
// this.imgUrlListValue.push(res.data.urls[0].image) //这里上传到指定的图片服务器,成功后会返回图片的url
})
.catch((err) => {})
}
}
})
}
},
destroyed() {
console.log('关闭了啊')
// 关闭摄像头
this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
this.trackerTask.stop()
const canvasUpload = document.getElementById('canvas-face')
const contextUpload = canvasUpload.getContext('2d')
// contextUpload.clearRect(0, 0, this.clientWidth, this.clientHeight);
}
}
</script>
<style lang="less" scoped>
@rem: 192;
.hideFalse {
visibility: hidden;
}
.bgNo {
background: url('../assets/imgs/selfService/faceBg.png') no-repeat;
background-size: 100% 100%;
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
z-index: 99;
.title {
font-size: unit(60 / @rem, rem);
text-align: center;
color: #333;
margin-top: unit(64 / @rem, rem);
}
.ts {
font-size: unit(36 / @rem, rem);
text-align: center;
color: #f99501;
margin-top: unit(24 / @rem, rem);
}
}
.bg {
background: url('../assets/imgs/selfService/faceBgScuess.png') no-repeat;
background-size: 100% 100%;
}
.entryFace {
width: 100%;
height: 100vh;
margin: 0px auto;
position: fixed;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
background: #000;
z-index: 999;
#canvas {
display: block;
position: absolute;
top: 0px;
left: 0px;
z-index: 99;
}
#videoCamera {
display: block;
position: absolute;
top: 0px;
left: 0px;
background: none;
transform: rotateY(180deg);
}
#canvas-face {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.loadingBox {
background-color: rgba(0, 0, 0, 0.8);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(20 / @rem, rem) unit(40 / @rem, rem);
border-radius: unit(10 / @rem, rem);
}
button {
position: absolute;
top: 20px;
left: 20px;
}
.bottom {
position: absolute;
bottom: unit(60 / @rem, rem);
left: unit(80 / @rem, rem);
width: unit(1760 / @rem, rem);
z-index: 99999;
overflow: hidden;
.left-btn {
float: left;
width: unit(320 / @rem, rem);
height: unit(80 / @rem, rem);
background: #d1440d;
text-align: center;
line-height: unit(80 / @rem, rem);
color: #e18e74;
font-size: unit(48 / @rem, rem);
font-weight: bold;
border-radius: unit(20 / @rem, rem);
}
.right-btn {
float: right;
width: unit(320 / @rem, rem);
height: unit(80 / @rem, rem);
text-align: center;
line-height: unit(80 / @rem, rem);
color: #fff;
font-size: unit(48 / @rem, rem);
font-weight: bold;
background: #f2ae0d;
border-radius: unit(20 / @rem, rem);
}
}
}
</style>
<!-- 人脸识别 -->
<template>
<!-- v-show="faceSure" -->
<!-- :class="faceSure?'':'hideFalse'" -->
<div class="entryFace selfTestTracking" v-if="face">
<div class="bgNo" :class="isBg ? 'bg' : ''">
<div class="title">请将面部对准识别区域</div>
<div class="ts">
提示:在采集面人脸时摘掉口罩、帽子等遮挡面部的物品会使识别率更高。
</div>
</div>
<video
id="videoCamera"
width="1920"
height="1080"
preload
autoplay
loop
muted
x5-video-player-type="h5-page"
></video>
<canvas id="canvas" ref="canvas" width="1920" height="1080"></canvas>
<canvas
id="canvas-face"
width="1920"
height="1080"
style="display: none;"
></canvas>
<div class="bottom" v-show="showBtn">
<div class="left-btn" @click="exit">返回</div>
</div>
</div>
</template>
<script>
function dataURLtoFile(dataurl, filename) {
//将base64转换为文件
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, {
type: mime
})
}
import tracking from '@/assets/js/ytjTracking.js'
import '@/assets/js/face-min.js'
import axios from 'axios'
import { updateFace } from '@/util/api.js'
import { Toast } from 'vant'
export default {
name: 'testTracking',
data() {
return {
trackerTask: null,
trackering: null,
mediaStreamTrack: null,
faceSure: false,
loading: false,
clientWidth: '',
clientHeight: '',
offsetY: '',
offsetX: '',
change: '',
videoIdArr: [],
isBg: false,
userInfo: {},
showBtn: false
}
},
props: {
face: Boolean
},
mounted() {
// 获取用户信息
this.userInfo = JSON.parse(localStorage.getItem('UserInfo'))
console.log('config.videoId:' + config.videoId)
this.getCompetence(config.videoId)
},
methods: {
exit() {
console.log('关闭了啊')
this.mediaStreamTrack.srcObject.getTracks()[0].stop() // 关闭摄像头
this.trackerTask.stop() // 停止侦测
this.$parent.exit()
},
getCompetence(videoId) {
let flag = true
const _this = this
const video = (this.mediaStreamTrack =
document.getElementById('videoCamera'))
const canvasUpload = document.getElementById('canvas-face')
const contextUpload = canvasUpload.getContext('2d')
const tracker = new window.tracking.ObjectTracker('face')
contextUpload.translate(1920, 0)
contextUpload.scale(-1, 1)
tracker.setInitialScale(4)
tracker.setStepSize(2)
tracker.setEdgesDensity(0.1)
// 启动摄像头初始化
this.trackerTask = window.tracking.track(
'#videoCamera',
tracker,
{
camera: true
},
1920,
1080,
videoId
)
tracker.on('track', function (event) {
_this.showBtn = true
if (event.data.length) {
// 会不停的去检测人脸,所以这里需要做个锁
if (flag) {
// 裁剪出人脸并绘制
// contextUpload.clearRect(0, 0, 1920, 1080);
contextUpload.drawImage(video, 0, 0, 1920, 1080)
// contextUpload.drawImage(video,690,235,1230,700,1230,0,1230,700);
// contextUpload.drawImage(video,690,235,1230,700,1230,0,1230,700);
flag = false
// 人脸的basa64
const dataURL = canvasUpload.toDataURL('image/jpeg')
console.log(dataURL)
// img文件
var cc = dataURLtoFile(dataURL, 'name.png')
// 获取FormData对象
let fd = new FormData()
// 将图片传入formdata
fd.append('file', cc)
axios
.post(process.env.VUE_APP_URL + 'common/upload', fd, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((res) => {
_this.userInfo.headImgPath = res.data.data[0].fileUrl
if (res.data.status == 200) {
// 上传人脸
updateFace('post', _this.userInfo).then((res1) => {
if (res1.data.status == 200) {
console.log('关闭了啊')
// 关闭摄像头
_this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
_this.trackerTask.stop()
// console.log(res.data.data[0].fileUrl)
_this.$parent.acquisitionSuccess(
res.data.data[0].fileUrl
)
} else {
Toast(res1.data.msg)
setTimeout(() => {
flag = true
console.log('关闭了啊')
// 关闭摄像头
_this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
_this.trackerTask.stop()
_this.$parent.exit()
}, 3000)
}
})
} else {
Toast(res.data.message)
}
// this.imgUrlListValue.push(res.data.urls[0].image) //这里上传到指定的图片服务器,成功后会返回图片的url
})
.catch((err) => {})
}
}
})
}
},
destroyed() {
console.log('关闭了啊')
// 关闭摄像头
this.mediaStreamTrack.srcObject.getTracks()[0].stop()
// 停止侦测
this.trackerTask.stop()
const canvasUpload = document.getElementById('canvas-face')
const contextUpload = canvasUpload.getContext('2d')
// contextUpload.clearRect(0, 0, this.clientWidth, this.clientHeight);
}
}
</script>
<style lang="less" scoped>
@rem: 192;
.hideFalse {
visibility: hidden;
}
.bgNo {
background: url('../assets/imgs/selfService/faceBg.png') no-repeat;
background-size: 100% 100%;
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
z-index: 99;
.title {
font-size: unit(60 / @rem, rem);
text-align: center;
color: #333;
margin-top: unit(64 / @rem, rem);
}
.ts {
font-size: unit(36 / @rem, rem);
text-align: center;
color: #f99501;
margin-top: unit(24 / @rem, rem);
}
}
.bg {
background: url('../assets/imgs/selfService/faceBgScuess.png') no-repeat;
background-size: 100% 100%;
}
.entryFace {
width: 100%;
height: 100vh;
margin: 0px auto;
position: fixed;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
background: #000;
z-index: 999;
#canvas {
display: block;
position: absolute;
top: 0px;
left: 0px;
z-index: 99;
}
#videoCamera {
display: block;
position: absolute;
top: 0px;
left: 0px;
background: none;
transform: rotateY(180deg);
}
#canvas-face {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.loadingBox {
background-color: rgba(0, 0, 0, 0.8);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(20 / @rem, rem) unit(40 / @rem, rem);
border-radius: unit(10 / @rem, rem);
}
button {
position: absolute;
top: 20px;
left: 20px;
}
.bottom {
position: absolute;
bottom: unit(60 / @rem, rem);
left: unit(80 / @rem, rem);
width: unit(1760 / @rem, rem);
z-index: 99999;
overflow: hidden;
.left-btn {
float: left;
width: unit(320 / @rem, rem);
height: unit(80 / @rem, rem);
background: #d1440d;
text-align: center;
line-height: unit(80 / @rem, rem);
color: #e18e74;
font-size: unit(48 / @rem, rem);
font-weight: bold;
border-radius: unit(20 / @rem, rem);
}
.right-btn {
float: right;
width: unit(320 / @rem, rem);
height: unit(80 / @rem, rem);
text-align: center;
line-height: unit(80 / @rem, rem);
color: #fff;
font-size: unit(48 / @rem, rem);
font-weight: bold;
background: #f2ae0d;
border-radius: unit(20 / @rem, rem);
}
}
}
</style>
签字版
<template>
<div class="drop" :class="signature ? '' : 'hide'">
<div class="signature-box" v-if="showSignatureArea">
<p class="hint">温馨提示:请填写核对确认信息无误后,在下方手写区域签名,并提交申请。</p>
<div class="canvas-box" id="canvasBox">
<canvas id="canvas"></canvas>
<span>签字区</span>
</div>
<div class="canvas-btn-group">
<div class="btn close" @click="hideSignature">返回检查</div>
<div class="btn rewriting" @click="clear"><img src="../assets/imgs/selfService/u1398.png" />清除重写</div>
<div class="btn submit" @click="submit">提交申请</div>
</div>
</div>
<!-- 退役报道显现-->
<div class="tips" v-show="isSuccess && Ddata.type == 1">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的【信息采集】材料已经提交成功,我们会在24⼩时内进⾏审核,审核通过后会第⼀时间通知到您,
<span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务,欢迎回家!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
<!--诉求服务显示-->
<div class="tips" v-show="isSuccess && Ddata.type == 2">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的【诉求服务】材料已经提交成功,我们会在48⼩时进⾏回复,
<span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务,保障每个退役军⼈合理合规诉求,让崇⾼落地!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
<!-- 教育培训显示-->
<div class="train-tip" v-show="isSuccess && Ddata.type == 3">
<img class="icon" src="@/assets/imgs/selfService/success.png" />
<div class="tit">提交成功!</div>
<p class="txt">
请近期随时关注您的
<span class="orange">手机信息</span>
</p>
<p class="txt">我们会在信息材料审核结束后通知您审核结果</p>
<div class="btn" @click="ok">确认</div>
</div>
<div class="father">
<div class="son">
<div class="grandson"></div>
</div>
</div>
<!-- 审核类业务显示-->
<div class="tips" v-show="isSuccess && Ddata.type != 1 && Ddata.type != 2 && Ddata.type != 3">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的材料已经提交成功,我们会在24⼩时内进⾏审核,审核通过后会第⼀时间通知到您, <span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
</div>
</template>
<script>
import {saveInfo, educationTrainAdd} from "@/util/api.js"
import axios from "axios"
let isSubmit = false // 是否签字
class Draw {
constructor() {
const box = document.getElementById("canvasBox") // 获取父元素
this.clientWidth = box.clientWidth // 获取父元素宽
this.clientHeight = box.clientHeight // 获取父元素高
this.canvas = document.getElementById("canvas") // 获取canvas元素
this.Top = this.canvas.getBoundingClientRect().top // 这里父元素是相对定位,所以不能使用offsetTop和offsetLeft
this.Left = this.canvas.getBoundingClientRect().left // 而getBoundingClientRect只相对于可见窗口,可以使用
this.canvas.setAttribute("width", this.clientWidth) // 设置canvas为父元素canvasBox宽度
this.canvas.setAttribute("height", this.clientHeight) // 设置canvas为父元素canvasBox高度
this.cxt = this.canvas.getContext("2d") // 获取canvas实例
}
init() {
this.canvas.addEventListener("touchstart", event => {
this.drawBegin(event)
})
this.canvas.addEventListener("touchend", () => {
this.drawEnd()
})
this.clear() // 清空上一次签字
}
// 触发签字事件
drawBegin(e) {
this.cxt.strokeStyle = "#000"
this.cxt.beginPath() // 起始一条路径,或重置当前路径
this.cxt.moveTo(e.changedTouches[0].clientX - this.Left, e.changedTouches[0].clientY - this.Top)
this.canvas.addEventListener("touchmove", event => {
this.drawing(event)
})
}
// 滑动签字
drawing(e) {
this.cxt.lineTo(e.changedTouches[0].clientX - this.Left, e.changedTouches[0].clientY - this.Top) // 添加一个新点,然后在画布中创建从该点到最后指定点的线条
this.cxt.stroke() // 绘制已定义的路径
}
// 结束滑动
drawEnd() {
isSubmit = true
}
// 清空签字
clear() {
isSubmit = false
this.cxt.clearRect(0, 0, this.clientWidth, this.clientHeight)
}
// 保存canvas为图片
save() {
return this.canvas.toDataURL("image/png") // toDataURL用于将canvas对象转换为base64位编码
}
}
export default {
props: {
signature: Boolean, // 是否显示当前组件
Ddata: Object // 当前模块用户填写信息
},
data() {
return {
title: "", // 市局名
ServiceTelephone: "", // 市局联系电话
type: "", // 判断哪个模块调用签字组件
name: "", // 用户姓名
isSubmit: false, // 是否已签字
isSuccess: false, // 控制显示成功提交的弹窗显示
showSignatureArea: true // 控制签名区显示
}
},
mounted() {
this.draw = new Draw("canvas") // 实例Draw类
this.draw.init() // 初始化
this.title = config.title // 获取config.js里的市局名
this.ServiceTelephone = config.ServiceTelephone // 获取config.js里的服务电话
this.type = sessionStorage.getItem("type") // 判断哪个模块调用签字组件
this.name = JSON.parse(localStorage.getItem("UserInfo")).name // 用户姓名
},
methods: {
// 点击返回检查,调用父级方法关闭签字区域
hideSignature() {
this.$parent.hideSignature()
},
// 点击清除重写,清空签字区
clear() {
this.draw.clear()
},
//将base64转换为文件
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, {
type: mime
})
},
// 点击提交申请->上传签名图片->保存所有信息
async submit() {
if (!isSubmit) return this.$Toast("请签名")
const img = this.draw.save() // 保存成base64 图片格式
const cc = this.dataURLtoFile(img, "name.png") // 将base64转换为文件
const fd = new FormData() // new一个FormData对象
fd.append("file", cc) // 将图片传入Formdata
const {data: res} = await axios.post(process.env.VUE_APP_URL + "common/upload", fd, {headers: {"Content-Type": "application/x-www-form-urlencoded"}})
if (res.status == 200) {
// 上传签名成功
this.Ddata.jdRetireInfo.signUrl = res.data[0].fileUrl // 上传后的签名地址存到jdRetireInfo中
this.Ddata.jdRetireInfo.checkStatus = 2 // 0未提交材料 1已提交材料未签名 2已签名 3审核通过 4审核未通过
if (this.type == "TRAIN_TYPE") {
// 教育培训模块保存信息
educationTrainAdd("post", this.Ddata, "json").then(res => {
if (res.data.status == 200) {
this.showSignatureArea = false // 关闭签名区
this.isSuccess = true // 显示成功提交弹窗
}
})
} else {
// 其他模块保存信息
saveInfo(this.type, this.Ddata, "json").then(res1 => {
if (res1.data.status == 200) {
this.showSignatureArea = false // 关闭签名区
this.isSuccess = true // 显示成功提交弹窗
}
})
}
} else {
// 上传签名不成功的错误信息
this.$Toast(res.data.message)
}
},
// 提交成功后点击确认回到首页
ok() {
this.$parent.hideSignature()
this.$router.push({name: "selfHome"})
}
}
}
</script>
<style lang="less">
@rem: 192;
.hide {
visibility: hidden;
}
.drop {
width: 100vw;
height: 100vh;
position: fixed;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
background: rgba(0, 0, 0, 0.5);
z-index: 999;
.signature-box {
width: unit(1100 / @rem, rem);
height: unit(700 / @rem, rem);
background: #f2f2f2;
border-radius: unit(10 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(0 / @rem, rem) unit(50 / @rem, rem);
padding-top: unit(33 / @rem, rem);
box-sizing: border-box;
.hint {
color: #f99501;
font-weight: 700;
font-size: unit(18 / @rem, rem);
text-align: center;
margin-bottom: unit(38 / @rem, rem);
}
.canvas-box {
height: unit(460 / @rem, rem);
border: unit(1 / @rem, rem) solid #797979;
background: #fff;
margin-top: unit(15 / @rem, rem);
position: relative;
#canvas {
position: absolute;
top: 0px;
left: 0px;
}
span {
font-size: unit(72 / @rem, rem);
color: #cccccc;
display: block;
text-align: center;
line-height: unit(460 / @rem, rem);
position: absolute;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
z-index: 1;
display: block;
width: 100%;
}
}
.canvas-btn-group {
display: flex;
justify-content: space-between;
margin-top: unit(40 / @rem, rem);
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
border-radius: unit(10 / @rem, rem);
font-size: unit(30 / @rem, rem);
font-weight: bold;
text-align: center;
line-height: unit(60 / @rem, rem);
box-sizing: border-box;
}
.close {
background: #fff;
color: #f99501;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.08);
}
.rewriting {
background: #fff;
color: #f99501;
border: unit(3 / @rem, rem) solid #f99501;
line-height: unit(54 / @rem, rem);
img {
width: unit(29 / @rem, rem);
height: unit(31 / @rem, rem);
position: relative;
top: unit(7 / @rem, rem);
margin-right: unit(13 / @rem, rem);
}
}
.submit {
background: #f99501;
color: #fff;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.08);
}
}
}
}
.tips {
width: unit(1226 / @rem, rem);
height: unit(772 / @rem, rem);
background: #fff;
border-radius: unit(24 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(66 / @rem, rem) unit(171 / @rem, rem) unit(48 / @rem, rem) unit(171 / @rem, rem);
box-sizing: border-box;
.icon {
width: unit(377 / @rem, rem);
height: unit(237 / @rem, rem);
display: block;
margin: 0px auto;
}
.tit {
color: #333333;
font-size: unit(40 / @rem, rem);
font-weight: bold;
text-align: center;
margin-bottom: unit(24 / @rem, rem);
margin-top: unit(24 / @rem, rem);
}
.txt {
color: #333333;
font-size: unit(24 / @rem, rem);
line-height: unit(36 / @rem, rem);
font-weight: bold;
text-align: justify;
}
.indx {
text-indent: unit(48 / @rem, rem);
}
.g {
color: #3bba67;
}
.y {
color: #f99501;
}
.right {
text-align: right;
}
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
background: #f99501;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.15);
border-radius: unit(10 / @rem, rem);
text-align: center;
line-height: unit(60 / @rem, rem);
color: #fff;
font-size: unit(30 / @rem, rem);
margin: 0px auto;
margin-top: unit(37 / @rem, rem);
}
}
.train-tip {
width: unit(750 / @rem, rem);
height: unit(520 / @rem, rem);
background: #fff;
border-radius: unit(24 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
.icon {
width: unit(277 / @rem, rem);
height: unit(177 / @rem, rem);
display: block;
margin: 0px auto;
margin-top: unit(54 / @rem, rem);
}
.tit {
color: #333333;
font-size: unit(36 / @rem, rem);
font-weight: bold;
text-align: center;
padding-left: unit(45 / @rem, rem);
letter-spacing: unit(5 / @rem, rem);
margin-bottom: unit(18 / @rem, rem);
margin-top: unit(10 / @rem, rem);
}
.txt {
color: #333333;
padding-left: unit(5 / @rem, rem);
letter-spacing: unit(2 / @rem, rem);
font-size: unit(24 / @rem, rem);
font-weight: bold;
text-align: center;
margin-bottom: unit(4 / @rem, rem);
.orange {
color: #f99501;
}
}
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
background: #f99501;
padding-left: unit(5 / @rem, rem);
letter-spacing: unit(5 / @rem, rem);
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.15);
border-radius: unit(10 / @rem, rem);
text-align: center;
line-height: unit(60 / @rem, rem);
color: #fff;
font-size: unit(30 / @rem, rem);
margin: 0px auto;
margin-top: unit(37 / @rem, rem);
}
}
</style>
<template>
<div class="drop" :class="signature ? '' : 'hide'">
<div class="signature-box" v-if="showSignatureArea">
<p class="hint">温馨提示:请填写核对确认信息无误后,在下方手写区域签名,并提交申请。</p>
<div class="canvas-box" id="canvasBox">
<canvas id="canvas"></canvas>
<span>签字区</span>
</div>
<div class="canvas-btn-group">
<div class="btn close" @click="hideSignature">返回检查</div>
<div class="btn rewriting" @click="clear"><img src="../assets/imgs/selfService/u1398.png" />清除重写</div>
<div class="btn submit" @click="submit">提交申请</div>
</div>
</div>
<!-- 退役报道显现-->
<div class="tips" v-show="isSuccess && Ddata.type == 1">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的【信息采集】材料已经提交成功,我们会在24⼩时内进⾏审核,审核通过后会第⼀时间通知到您,
<span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务,欢迎回家!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
<!--诉求服务显示-->
<div class="tips" v-show="isSuccess && Ddata.type == 2">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的【诉求服务】材料已经提交成功,我们会在48⼩时进⾏回复,
<span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务,保障每个退役军⼈合理合规诉求,让崇⾼落地!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
<!-- 教育培训显示-->
<div class="train-tip" v-show="isSuccess && Ddata.type == 3">
<img class="icon" src="@/assets/imgs/selfService/success.png" />
<div class="tit">提交成功!</div>
<p class="txt">
请近期随时关注您的
<span class="orange">手机信息</span>
</p>
<p class="txt">我们会在信息材料审核结束后通知您审核结果</p>
<div class="btn" @click="ok">确认</div>
</div>
<div class="father">
<div class="son">
<div class="grandson"></div>
</div>
</div>
<!-- 审核类业务显示-->
<div class="tips" v-show="isSuccess && Ddata.type != 1 && Ddata.type != 2 && Ddata.type != 3">
<img class="icon" src="@/assets/imgs/selfService/success.png" alt />
<div class="tit">提交成功!</div>
<p class="txt">尊敬的战友:{{ name }}</p>
<p class="txt">您好!</p>
<p class="txt indx">
您的材料已经提交成功,我们会在24⼩时内进⾏审核,审核通过后会第⼀时间通知到您, <span class="g">请保持⼿机畅通!</span>如有疑问可以拨打退役军⼈服务中⼼电话: <span class="y">{{ ServiceTelephone }}</span
>进⾏咨询。
</p>
<p class="txt">我们会竭诚为您服务!</p>
<p class="txt right">{{ title }}退役军⼈事务局</p>
<div class="btn" @click="ok">确认</div>
</div>
</div>
</template>
<script>
import {saveInfo, educationTrainAdd} from "@/util/api.js"
import axios from "axios"
let isSubmit = false // 是否签字
class Draw {
constructor() {
const box = document.getElementById("canvasBox") // 获取父元素
this.clientWidth = box.clientWidth // 获取父元素宽
this.clientHeight = box.clientHeight // 获取父元素高
this.canvas = document.getElementById("canvas") // 获取canvas元素
this.Top = this.canvas.getBoundingClientRect().top // 这里父元素是相对定位,所以不能使用offsetTop和offsetLeft
this.Left = this.canvas.getBoundingClientRect().left // 而getBoundingClientRect只相对于可见窗口,可以使用
this.canvas.setAttribute("width", this.clientWidth) // 设置canvas为父元素canvasBox宽度
this.canvas.setAttribute("height", this.clientHeight) // 设置canvas为父元素canvasBox高度
this.cxt = this.canvas.getContext("2d") // 获取canvas实例
}
init() {
this.canvas.addEventListener("touchstart", event => {
this.drawBegin(event)
})
this.canvas.addEventListener("touchend", () => {
this.drawEnd()
})
this.clear() // 清空上一次签字
}
// 触发签字事件
drawBegin(e) {
this.cxt.strokeStyle = "#000"
this.cxt.beginPath() // 起始一条路径,或重置当前路径
this.cxt.moveTo(e.changedTouches[0].clientX - this.Left, e.changedTouches[0].clientY - this.Top)
this.canvas.addEventListener("touchmove", event => {
this.drawing(event)
})
}
// 滑动签字
drawing(e) {
this.cxt.lineTo(e.changedTouches[0].clientX - this.Left, e.changedTouches[0].clientY - this.Top) // 添加一个新点,然后在画布中创建从该点到最后指定点的线条
this.cxt.stroke() // 绘制已定义的路径
}
// 结束滑动
drawEnd() {
isSubmit = true
}
// 清空签字
clear() {
isSubmit = false
this.cxt.clearRect(0, 0, this.clientWidth, this.clientHeight)
}
// 保存canvas为图片
save() {
return this.canvas.toDataURL("image/png") // toDataURL用于将canvas对象转换为base64位编码
}
}
export default {
props: {
signature: Boolean, // 是否显示当前组件
Ddata: Object // 当前模块用户填写信息
},
data() {
return {
title: "", // 市局名
ServiceTelephone: "", // 市局联系电话
type: "", // 判断哪个模块调用签字组件
name: "", // 用户姓名
isSubmit: false, // 是否已签字
isSuccess: false, // 控制显示成功提交的弹窗显示
showSignatureArea: true // 控制签名区显示
}
},
mounted() {
this.draw = new Draw("canvas") // 实例Draw类
this.draw.init() // 初始化
this.title = config.title // 获取config.js里的市局名
this.ServiceTelephone = config.ServiceTelephone // 获取config.js里的服务电话
this.type = sessionStorage.getItem("type") // 判断哪个模块调用签字组件
this.name = JSON.parse(localStorage.getItem("UserInfo")).name // 用户姓名
},
methods: {
// 点击返回检查,调用父级方法关闭签字区域
hideSignature() {
this.$parent.hideSignature()
},
// 点击清除重写,清空签字区
clear() {
this.draw.clear()
},
//将base64转换为文件
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, {
type: mime
})
},
// 点击提交申请->上传签名图片->保存所有信息
async submit() {
if (!isSubmit) return this.$Toast("请签名")
const img = this.draw.save() // 保存成base64 图片格式
const cc = this.dataURLtoFile(img, "name.png") // 将base64转换为文件
const fd = new FormData() // new一个FormData对象
fd.append("file", cc) // 将图片传入Formdata
const {data: res} = await axios.post(process.env.VUE_APP_URL + "common/upload", fd, {headers: {"Content-Type": "application/x-www-form-urlencoded"}})
if (res.status == 200) {
// 上传签名成功
this.Ddata.jdRetireInfo.signUrl = res.data[0].fileUrl // 上传后的签名地址存到jdRetireInfo中
this.Ddata.jdRetireInfo.checkStatus = 2 // 0未提交材料 1已提交材料未签名 2已签名 3审核通过 4审核未通过
if (this.type == "TRAIN_TYPE") {
// 教育培训模块保存信息
educationTrainAdd("post", this.Ddata, "json").then(res => {
if (res.data.status == 200) {
this.showSignatureArea = false // 关闭签名区
this.isSuccess = true // 显示成功提交弹窗
}
})
} else {
// 其他模块保存信息
saveInfo(this.type, this.Ddata, "json").then(res1 => {
if (res1.data.status == 200) {
this.showSignatureArea = false // 关闭签名区
this.isSuccess = true // 显示成功提交弹窗
}
})
}
} else {
// 上传签名不成功的错误信息
this.$Toast(res.data.message)
}
},
// 提交成功后点击确认回到首页
ok() {
this.$parent.hideSignature()
this.$router.push({name: "selfHome"})
}
}
}
</script>
<style lang="less">
@rem: 192;
.hide {
visibility: hidden;
}
.drop {
width: 100vw;
height: 100vh;
position: fixed;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
background: rgba(0, 0, 0, 0.5);
z-index: 999;
.signature-box {
width: unit(1100 / @rem, rem);
height: unit(700 / @rem, rem);
background: #f2f2f2;
border-radius: unit(10 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(0 / @rem, rem) unit(50 / @rem, rem);
padding-top: unit(33 / @rem, rem);
box-sizing: border-box;
.hint {
color: #f99501;
font-weight: 700;
font-size: unit(18 / @rem, rem);
text-align: center;
margin-bottom: unit(38 / @rem, rem);
}
.canvas-box {
height: unit(460 / @rem, rem);
border: unit(1 / @rem, rem) solid #797979;
background: #fff;
margin-top: unit(15 / @rem, rem);
position: relative;
#canvas {
position: absolute;
top: 0px;
left: 0px;
}
span {
font-size: unit(72 / @rem, rem);
color: #cccccc;
display: block;
text-align: center;
line-height: unit(460 / @rem, rem);
position: absolute;
top: unit(0 / @rem, rem);
left: unit(0 / @rem, rem);
z-index: 1;
display: block;
width: 100%;
}
}
.canvas-btn-group {
display: flex;
justify-content: space-between;
margin-top: unit(40 / @rem, rem);
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
border-radius: unit(10 / @rem, rem);
font-size: unit(30 / @rem, rem);
font-weight: bold;
text-align: center;
line-height: unit(60 / @rem, rem);
box-sizing: border-box;
}
.close {
background: #fff;
color: #f99501;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.08);
}
.rewriting {
background: #fff;
color: #f99501;
border: unit(3 / @rem, rem) solid #f99501;
line-height: unit(54 / @rem, rem);
img {
width: unit(29 / @rem, rem);
height: unit(31 / @rem, rem);
position: relative;
top: unit(7 / @rem, rem);
margin-right: unit(13 / @rem, rem);
}
}
.submit {
background: #f99501;
color: #fff;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.08);
}
}
}
}
.tips {
width: unit(1226 / @rem, rem);
height: unit(772 / @rem, rem);
background: #fff;
border-radius: unit(24 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: unit(66 / @rem, rem) unit(171 / @rem, rem) unit(48 / @rem, rem) unit(171 / @rem, rem);
box-sizing: border-box;
.icon {
width: unit(377 / @rem, rem);
height: unit(237 / @rem, rem);
display: block;
margin: 0px auto;
}
.tit {
color: #333333;
font-size: unit(40 / @rem, rem);
font-weight: bold;
text-align: center;
margin-bottom: unit(24 / @rem, rem);
margin-top: unit(24 / @rem, rem);
}
.txt {
color: #333333;
font-size: unit(24 / @rem, rem);
line-height: unit(36 / @rem, rem);
font-weight: bold;
text-align: justify;
}
.indx {
text-indent: unit(48 / @rem, rem);
}
.g {
color: #3bba67;
}
.y {
color: #f99501;
}
.right {
text-align: right;
}
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
background: #f99501;
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.15);
border-radius: unit(10 / @rem, rem);
text-align: center;
line-height: unit(60 / @rem, rem);
color: #fff;
font-size: unit(30 / @rem, rem);
margin: 0px auto;
margin-top: unit(37 / @rem, rem);
}
}
.train-tip {
width: unit(750 / @rem, rem);
height: unit(520 / @rem, rem);
background: #fff;
border-radius: unit(24 / @rem, rem);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
.icon {
width: unit(277 / @rem, rem);
height: unit(177 / @rem, rem);
display: block;
margin: 0px auto;
margin-top: unit(54 / @rem, rem);
}
.tit {
color: #333333;
font-size: unit(36 / @rem, rem);
font-weight: bold;
text-align: center;
padding-left: unit(45 / @rem, rem);
letter-spacing: unit(5 / @rem, rem);
margin-bottom: unit(18 / @rem, rem);
margin-top: unit(10 / @rem, rem);
}
.txt {
color: #333333;
padding-left: unit(5 / @rem, rem);
letter-spacing: unit(2 / @rem, rem);
font-size: unit(24 / @rem, rem);
font-weight: bold;
text-align: center;
margin-bottom: unit(4 / @rem, rem);
.orange {
color: #f99501;
}
}
.btn {
width: unit(260 / @rem, rem);
height: unit(60 / @rem, rem);
background: #f99501;
padding-left: unit(5 / @rem, rem);
letter-spacing: unit(5 / @rem, rem);
box-shadow: 0px 0px unit(13 / @rem, rem) unit(3 / @rem, rem) rgba(62, 31, 32, 0.15);
border-radius: unit(10 / @rem, rem);
text-align: center;
line-height: unit(60 / @rem, rem);
color: #fff;
font-size: unit(30 / @rem, rem);
margin: 0px auto;
margin-top: unit(37 / @rem, rem);
}
}
</style>
打印 html 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<table border="1">
<tr>
<th>Month</th>
<th>Savings</th>
</tr>
<!--startprint-->
<tr>
<td>January</td>
<td>$100</td>
</tr>
<!--endprint-->
</table>
</p>
<script>
function doPrint() {
const html = window.document.body.innerHTML;
const start = "<!--startprint-->"; //开始打印标识字符串有17个字符
const end = "<!--endprint-->"; //结束打印标识字符串
let printHtml= html.substring(html.indexOf(start) + 17); //从开始打印标识之后的内容
printHtml = printHtml.substring(0, printHtml.indexOf(end)); //截取开始标识和结束标识之间的内容
window.document.body.innerHTML = printHtml //把需要打印的指定内容赋给body.innerHTML
window.print() //调用浏览器的打印功能打印指定区域
window.document.body.innerHTML = printHtml//重新给页面内容赋值;
}
doPrint()
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<table border="1">
<tr>
<th>Month</th>
<th>Savings</th>
</tr>
<!--startprint-->
<tr>
<td>January</td>
<td>$100</td>
</tr>
<!--endprint-->
</table>
</p>
<script>
function doPrint() {
const html = window.document.body.innerHTML;
const start = "<!--startprint-->"; //开始打印标识字符串有17个字符
const end = "<!--endprint-->"; //结束打印标识字符串
let printHtml= html.substring(html.indexOf(start) + 17); //从开始打印标识之后的内容
printHtml = printHtml.substring(0, printHtml.indexOf(end)); //截取开始标识和结束标识之间的内容
window.document.body.innerHTML = printHtml //把需要打印的指定内容赋给body.innerHTML
window.print() //调用浏览器的打印功能打印指定区域
window.document.body.innerHTML = printHtml//重新给页面内容赋值;
}
doPrint()
</script>
</body>
</html>
图片预览
<!DOCTYPE html>
<html>
<head>
<meta
name="viewport"
content="width=device-width,initial-scale=1, user-scalable=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no" />
<title>test</title>
<script>
//选择图片时预览功能
function imageshow(source) {
var file = source.files[0]
var imageid = source.id
if (window.FileReader) {
var fr = new FileReader()
fr.onloadend = function (e) {
document.getElementById('portrait' + imageid).src = e.target.result
}
fr.readAsDataURL(file)
}
document.getElementById('image' + imageid).style.display = 'none'
document.getElementById('show' + imageid).style.display = 'block'
}
</script>
</head>
<body>
<div>
<div id="image1">
<p>上传截图</p>
<input
type="file"
name="screenshot1"
id="1"
onchange="imageshow(this)"
/>
</div>
<div id="show1" style="display:none;">
<img src="" id="portrait1" width="100" height="70" />
</div>
<div id="image2">
<p>上传截图</p>
<input
type="file"
name="screenshot2"
id="2"
onchange="imageshow(this)"
/>
</div>
<div id="show2" style="display:none;">
<img src="" id="portrait2" width="100" height="70" />
</div>
<div id="image3">
<p>上传截图</p>
<input
type="file"
name="screenshot3"
id="3"
onchange="imageshow(this)"
/>
</div>
<div id="show3" style="display:none;">
<img src="" id="portrait3" width="100" height="70" />
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta
name="viewport"
content="width=device-width,initial-scale=1, user-scalable=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no" />
<title>test</title>
<script>
//选择图片时预览功能
function imageshow(source) {
var file = source.files[0]
var imageid = source.id
if (window.FileReader) {
var fr = new FileReader()
fr.onloadend = function (e) {
document.getElementById('portrait' + imageid).src = e.target.result
}
fr.readAsDataURL(file)
}
document.getElementById('image' + imageid).style.display = 'none'
document.getElementById('show' + imageid).style.display = 'block'
}
</script>
</head>
<body>
<div>
<div id="image1">
<p>上传截图</p>
<input
type="file"
name="screenshot1"
id="1"
onchange="imageshow(this)"
/>
</div>
<div id="show1" style="display:none;">
<img src="" id="portrait1" width="100" height="70" />
</div>
<div id="image2">
<p>上传截图</p>
<input
type="file"
name="screenshot2"
id="2"
onchange="imageshow(this)"
/>
</div>
<div id="show2" style="display:none;">
<img src="" id="portrait2" width="100" height="70" />
</div>
<div id="image3">
<p>上传截图</p>
<input
type="file"
name="screenshot3"
id="3"
onchange="imageshow(this)"
/>
</div>
<div id="show3" style="display:none;">
<img src="" id="portrait3" width="100" height="70" />
</div>
</div>
</body>
</html>
input 设置图标
方法一
将图标设置为 input 的背景
<style>
input{
border: none;
}
.box{
height: 50px;
background: yellow;
}
.box input{
width: 200px;
height: 30px;
border-radius: 15px;
margin: 10px 0;
background: url(image/search.gif) no-repeat;
background-color: white;
background-position: 3px;
padding-left: 30px;
border: 1px solid black;
outline: none;
}
</style>
</head>
<body>
<div class="box">
<input type="text" class="username" value="搜索"/>
</div>
</body>
<style>
input{
border: none;
}
.box{
height: 50px;
background: yellow;
}
.box input{
width: 200px;
height: 30px;
border-radius: 15px;
margin: 10px 0;
background: url(image/search.gif) no-repeat;
background-color: white;
background-position: 3px;
padding-left: 30px;
border: 1px solid black;
outline: none;
}
</style>
</head>
<body>
<div class="box">
<input type="text" class="username" value="搜索"/>
</div>
</body>
方法二 使用绝对定位,相对定位
<style >
.box{
width: 200px;
position: relative;
}
.box .icon-search{
background: url(image/search.gif) no-repeat;
width: 20px;
height: 20px;
position: absolute;
top: 6px;
left: 0;
}
.box .username{
padding-left: 30px;
height: 25px;
}
</style>
</head>
<body>
<div class="box">
<i class="icon-search"></i>
<input type="text" class="username" value="搜索"/>
</div>
</body>
<style >
.box{
width: 200px;
position: relative;
}
.box .icon-search{
background: url(image/search.gif) no-repeat;
width: 20px;
height: 20px;
position: absolute;
top: 6px;
left: 0;
}
.box .username{
padding-left: 30px;
height: 25px;
}
</style>
</head>
<body>
<div class="box">
<i class="icon-search"></i>
<input type="text" class="username" value="搜索"/>
</div>
</body>
input 正则校验
<template>
<form>
<input type="text" maxlength="10" placeholder="请输入用户名" v-model="user" required @input="checkUser">
<p v-if="userCorrect">{{userMes}}</p>
<input type="password" maxlength="10" placeholder="请输入密码" v-model="password" required @input="checkPassword">
<p v-if="passwordCorrect">{{passwordMes}}</p>
</form>
</template>
<script>
export default {
data () {
return {
user: '',
userCorrect: false,
userMes: '请输入正确用户名,长度在4-10个字符',
password: '',
passwordCorrect: false,
passwordMes: '请输入正确密码,长度在4-10个字符',
}
},
methods: {
checkUser () {
const pattern = /^[0-9]{4,10}$/
this.userCorrect = !pattern.test(this.user)
},
checkPassword () {
const pattern = /^[0-9]{4,10}$/
this.passwordCorrect = !pattern.test(this.password)
}
}
}
</script>
<style>
input {
display: block;
padding: 0 20px;
outline: none;
border: 1px solid #ccc;
width: 150px;
height: 40px;
transition: all 300ms;
}
/*input内容合法时:边框颜色是绿色*/
input:valid {
border-color: green;
box-shadow: inset 5px 0 0 green;
}
/* input内容非法时:边框颜色是红色*/
input:invalid {
border-color: red;
box-shadow: inset 5px 0 0 red;
}
button {
width: 190px;
height: 40px;
background-color: green;
color: white;
margin-top: 20px;
}
</style>
<template>
<form>
<input type="text" maxlength="10" placeholder="请输入用户名" v-model="user" required @input="checkUser">
<p v-if="userCorrect">{{userMes}}</p>
<input type="password" maxlength="10" placeholder="请输入密码" v-model="password" required @input="checkPassword">
<p v-if="passwordCorrect">{{passwordMes}}</p>
</form>
</template>
<script>
export default {
data () {
return {
user: '',
userCorrect: false,
userMes: '请输入正确用户名,长度在4-10个字符',
password: '',
passwordCorrect: false,
passwordMes: '请输入正确密码,长度在4-10个字符',
}
},
methods: {
checkUser () {
const pattern = /^[0-9]{4,10}$/
this.userCorrect = !pattern.test(this.user)
},
checkPassword () {
const pattern = /^[0-9]{4,10}$/
this.passwordCorrect = !pattern.test(this.password)
}
}
}
</script>
<style>
input {
display: block;
padding: 0 20px;
outline: none;
border: 1px solid #ccc;
width: 150px;
height: 40px;
transition: all 300ms;
}
/*input内容合法时:边框颜色是绿色*/
input:valid {
border-color: green;
box-shadow: inset 5px 0 0 green;
}
/* input内容非法时:边框颜色是红色*/
input:invalid {
border-color: red;
box-shadow: inset 5px 0 0 red;
}
button {
width: 190px;
height: 40px;
background-color: green;
color: white;
margin-top: 20px;
}
</style>
滚动底部请求数据
this.$nextTick(() => {
const body = this.$refs.instructions
body.onscroll = () => {
const scrollTop = body.scrollTop
const clientHeight = body.clientHeight
const scrollHeight = body.scrollHeight
if (scrollTop + clientHeight >= scrollHeight - 50) {
// 如果还有新的数据
if (!this.finished) {
this.page++ // 请求的页数
this.getList() // 发起网络请求
}
}
}
})
this.$nextTick(() => {
const body = this.$refs.instructions
body.onscroll = () => {
const scrollTop = body.scrollTop
const clientHeight = body.clientHeight
const scrollHeight = body.scrollHeight
if (scrollTop + clientHeight >= scrollHeight - 50) {
// 如果还有新的数据
if (!this.finished) {
this.page++ // 请求的页数
this.getList() // 发起网络请求
}
}
}
})
请求方法
getList () {
NewsList("post", {
pageSize: 10,
pageNum: this.page, // 页数参数
}).then(res => {
if (res.data.status == 200) {
this.con = [...this.con, ...res.data.data] // 合并新数据和旧数据
// 没有更多的数据了
if (res.data.data.length == 0) {
this.finished = true
}
}
})
}
getList () {
NewsList("post", {
pageSize: 10,
pageNum: this.page, // 页数参数
}).then(res => {
if (res.data.status == 200) {
this.con = [...this.con, ...res.data.data] // 合并新数据和旧数据
// 没有更多的数据了
if (res.data.data.length == 0) {
this.finished = true
}
}
})
}
轮播图
npm i -s swiper
npm i -s swiper
<template>
<div class="swiper-box">
<div class="swiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="(item, index) in swiperArr"
:key="index"
>
<img :src="item.clientFileUrl" />
</div>
</div>
<div class="swiper-pagination"></div>
</div>
<div class="bottom">
<div class="text">
<span>{{ swiperArr[swiperIndex].tittle }}</span>
</div>
<div class="dots">
<div
class="dot"
:class="swiperIndex == index ? 'active' : ''"
v-for="(item, index) in swiperArr"
:key="index"
></div>
</div>
</div>
</div>
</template>
<script>
import { BannerList } from '@/util/api.js'
import Swiper from 'swiper'
import 'swiper/swiper.min.css'
export default {
data() {
return {
swiperArr: [{ tittle: '' }], // 轮播数组
swiperIndex: 0
}
},
mounted() {
this.getList()
},
methods: {
async getList() {
const { data: res } = await BannerList(
'post',
{
type: 4
},
'json'
)
if (res.status == 200) {
const images = res.data.filter(
(item) =>
item.clientFileUrl.charAt(item.clientFileUrl.length - 1) == 'g'
)
this.swiperArr = images
const that = this
this.$nextTick(() => {
new Swiper('.swiper', {
autoplay: true, // 自动滑动,
loop: true, // 循环
observer: true, //修改swiper自己或子元素时,自动初始化swiper
observeParents: true, //修改swiper的父元素时,自动初始化swiper
pagination: {
el: '.swiper-pagination'
},
on: {
slideChangeTransitionStart: function () {
if (this.activeIndex > that.swiperArr.length) {
return (that.swiperIndex = 0)
}
if (this.activeIndex == 0) {
return (that.swiperIndex = that.swiperArr.length - 1)
}
if (that.swiperIndex <= that.swiperArr.length) {
that.swiperIndex = this.activeIndex - 1
}
}
}
})
})
}
}
}
}
</script>
<style lang="less">
.swiper-box {
width: 100%;
height: 100vh;
position: absolute;
top: 0;
overflow: hidden;
.swiper-slide {
overflow: hidden;
img {
width: 100%;
height: 100vh;
}
}
.bottom {
position: absolute;
bottom: 0px;
left: 0px;
height: 40px;
padding: 0px 40px;
width: 100%;
z-index: 99;
box-sizing: border-box;
background-color: #a70003;
.text {
float: left;
line-height: 40px;
color: #fff;
font-size: 20px;
}
.dots {
float: right;
display: flex;
align-items: center;
height: 40px;
.dot {
width: 10px;
height: 10px;
background: #909090;
border-radius: 50%;
margin-left: 10px;
}
.active {
background: #fff;
}
}
}
}
</style>
<template>
<div class="swiper-box">
<div class="swiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="(item, index) in swiperArr"
:key="index"
>
<img :src="item.clientFileUrl" />
</div>
</div>
<div class="swiper-pagination"></div>
</div>
<div class="bottom">
<div class="text">
<span>{{ swiperArr[swiperIndex].tittle }}</span>
</div>
<div class="dots">
<div
class="dot"
:class="swiperIndex == index ? 'active' : ''"
v-for="(item, index) in swiperArr"
:key="index"
></div>
</div>
</div>
</div>
</template>
<script>
import { BannerList } from '@/util/api.js'
import Swiper from 'swiper'
import 'swiper/swiper.min.css'
export default {
data() {
return {
swiperArr: [{ tittle: '' }], // 轮播数组
swiperIndex: 0
}
},
mounted() {
this.getList()
},
methods: {
async getList() {
const { data: res } = await BannerList(
'post',
{
type: 4
},
'json'
)
if (res.status == 200) {
const images = res.data.filter(
(item) =>
item.clientFileUrl.charAt(item.clientFileUrl.length - 1) == 'g'
)
this.swiperArr = images
const that = this
this.$nextTick(() => {
new Swiper('.swiper', {
autoplay: true, // 自动滑动,
loop: true, // 循环
observer: true, //修改swiper自己或子元素时,自动初始化swiper
observeParents: true, //修改swiper的父元素时,自动初始化swiper
pagination: {
el: '.swiper-pagination'
},
on: {
slideChangeTransitionStart: function () {
if (this.activeIndex > that.swiperArr.length) {
return (that.swiperIndex = 0)
}
if (this.activeIndex == 0) {
return (that.swiperIndex = that.swiperArr.length - 1)
}
if (that.swiperIndex <= that.swiperArr.length) {
that.swiperIndex = this.activeIndex - 1
}
}
}
})
})
}
}
}
}
</script>
<style lang="less">
.swiper-box {
width: 100%;
height: 100vh;
position: absolute;
top: 0;
overflow: hidden;
.swiper-slide {
overflow: hidden;
img {
width: 100%;
height: 100vh;
}
}
.bottom {
position: absolute;
bottom: 0px;
left: 0px;
height: 40px;
padding: 0px 40px;
width: 100%;
z-index: 99;
box-sizing: border-box;
background-color: #a70003;
.text {
float: left;
line-height: 40px;
color: #fff;
font-size: 20px;
}
.dots {
float: right;
display: flex;
align-items: center;
height: 40px;
.dot {
width: 10px;
height: 10px;
background: #909090;
border-radius: 50%;
margin-left: 10px;
}
.active {
background: #fff;
}
}
}
}
</style>