Commit 8f7acb3f authored by kang.nie@inzymeits.com's avatar kang.nie@inzymeits.com
Browse files

初始化代码

parent 2d7d3f82
Pipeline #3104 failed with stages
in 0 seconds
<template>
<div class="page">
<van-nav-bar
:title="$route.meta ? $route.meta.title : ''"
:fixed="true"
left-arrow
@click-left="handleToBeforePage"
/>
<keep-alive>
<router-view />
</keep-alive>
</div>
</template>
<script>
export default {
name: 'Layout',
methods: {
/**
* 跳转到上一个页面
*/
handleToBeforePage() {
this.$router.back()
}
}
}
</script>
<style lang="scss">
.page {
padding-top: 90px;
.van-nav-bar {
background-color: #F9F9F9;
.van-nav-bar__content {
height: 90px;
.van-nav-bar__left {
.van-icon {
color: rgba(0, 0, 0, 0.90);
}
}
.van-nav-bar__title {
color: #242424;
font-size: 30px;
font-weight: 500;
}
}
}
}
</style>
<template>
<div class="component-list">
<van-field
v-for="(item, index) in value"
:value="item"
:label="index + 1"
:key="item"
readonly
input-align="right"
@click="$emit('click', item, index)"
>
<template #button>
<i v-if="!disable" class="imageicon imageicon-del" @click.stop="handleDeleteData(index)" />
</template>
</van-field>
</div>
</template>
<script>
export default {
name: 'List',
props: {
value: {
type: Array,
default: () => []
},
disable: {
type: Boolean,
default: false
}
},
methods: {
/**
* 删除数据
* @param index 当前索引
*/
handleDeleteData(index) {
// 先拷贝一份数据
const value = [...this.value]
// 删除指定索引的数据
value.splice(index, 1)
// 开始双向绑定
this.$emit('input', value)
}
}
}
</script>
<style lang="scss">
.component-list {
.van-cell__title {
padding-left: 32px;
width: 100px;
}
.imageicon-del {
margin-bottom: -6px;
margin-left: 64px;
}
}
</style>
<template>
<div class="logout">
<img :src="exitImg" alt="退出登录" class="logout-btn" @click="dialogVisible= true">
<van-overlay :show="dialogVisible">
<div class="wrapper">
<div class="content">
<div class="logout-text">是否退出当前账号?</div>
<div>
<van-button type="primary" class="logout-cancel" round @click="dialogVisible= false">取消</van-button>
<van-button type="primary" class="logout-sure" round color="linear-gradient(120deg, #4F4CFB 0%, #376FF4 97%)" @click="logOut">确定</van-button>
</div>
</div>
</div>
</van-overlay>
</div>
</template>
<script>
import { removeToken } from '@/utils/auth'
import { logout } from '@/api/loginToC.js'
export default {
name: 'Logout',
data() {
return {
dialogVisible: false,
exitImg: require('@/assets/images/logout.png')
}
},
methods: {
// 登出
logOut() {
if (this.$route.name === 'User') {
logout().then(res => {
removeToken()
this.$router.go(0)
})
return
}
removeToken()
this.$router.go(0)
}
}
}
</script>
<style lang="scss" scoped>
.logout {
position: absolute;
top: 57px;
right: 0;
z-index: 5;
.logout-btn {
width: 140px;
height: 48px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.content {
width: 548px;
height: 366px;
background-image: linear-gradient(180deg, #CCDDFF 0%, #FFFFFF 36%);
border-radius: 16px;
padding: 0 36px;
.logout-text {
margin-top: 96px;
margin-bottom: 88px;
font-size: 36px;
color: #212026;
text-align: center;
font-weight: 500;
}
.logout-cancel {
width: 266px;
height: 96px;
line-height: 96px;
font-size: 30px;
background: linear-gradient(120deg, #EDF3FF 0%, #E9F0FF 97%);
color: #242424;
text-align: center;
font-weight: 500;
border: none;
}
.logout-sure {
width: 266px;
height: 96px;
line-height: 96px;
font-size: 32px;
color: #FFFFFF;
text-align: center;
font-weight: 500;
}
}
}
}
</style>
<template>
<div class="page-rnr-person-idcard-must-know">
<div class="page-rnr-person-idcard-must-know-title">拍照须知</div>
<div class="page-rnr-person-idcard-error-row">
<div class="page-rnr-person-idcard-error-column smaller">
<div class="page-rnr-person-idcard-title true">标准拍摄</div>
</div>
<div class="page-rnr-person-idcard-error-column lost">
<div class="page-rnr-person-idcard-title false">边框缺失</div>
</div>
<div class="page-rnr-person-idcard-error-column shadow">
<div class="page-rnr-person-idcard-title false">照片模糊</div>
</div>
<div class="page-rnr-person-idcard-error-column light">
<div class="page-rnr-person-idcard-title false">闪光强烈</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MustKnow'
}
</script>
<style lang="scss">
.page-rnr-person-idcard-must-know {
padding: 40px 24px 0;
.page-rnr-person-idcard-must-know-title {
align-items: center;
color: #242424;
display: flex;
font-size: 28px;
text-align: center;
font-weight: 400;
line-height: 40px;
&:after, &:before {
background: #DCDFE6;
height: 1px;
transform: scaleY(-1);
content: "";
display: block;
flex: 1;
}
&:before {
margin-right: 12px;
}
&:after {
margin-left: 12px;
}
}
.page-rnr-person-idcard-error-row {
display: flex;
margin-top: 24px;
justify-content: space-between;
.page-rnr-person-idcard-error-column {
background-size: 166px 104px;
background-repeat: no-repeat;
background-position: left top;
color: #242424;
font-size: 24px;
text-align: center;
font-weight: 400;
width: 166px;
padding-top: 120px;
text-align: center;
.page-rnr-person-idcard-title {
align-items: center;
display: flex;
justify-content: center;
&:before {
background-size: 100% 100%;
background-repeat: no-repeat;
content: "";
display: inline-block;
width: 30px;
height: 30px;
margin-right: 8px;
}
&.true {
&:before {
background-image: url(../../assets/rnr/ico_true.png);
}
}
&.false {
&:before {
background-image: url(../../assets/rnr/ico_false.png);
}
}
}
&.smaller {
background-image: url(../../assets/rnr/ico_idcard_smaller.png);
}
&.lost {
background-image: url(../../assets/rnr/ico_idcard_lose.png);
}
&.light {
background-image: url(../../assets/rnr/ico_idcard_light.png);
}
&.shadow {
background-image: url(../../assets/rnr/ico_idcard_shadow.png);
}
}
}
}
</style>
<template>
<div class="component-ocr-idcard">
<van-row>
<van-col :span="13">
<div class="title">{{ title }}</div>
<div :class="empty" class="component-ocr-idcard-step">STEP 1:通过照片OCR方式获取身份证信息</div>
</van-col>
<van-col :span="11"><div class="component-ocr-idcard-icon-reader" /></van-col>
</van-row>
<div class="component-ocr-idcard-btn-wrap">
<van-button :loading="loading" :disabled="remainTime < 30" plain block round hairline type="primary" color="#0761F3" @click="handleOcrScan">
<i v-if="remainTime === 30" class="page-rnr-userinfo-ocr" />{{ remainTime === 30 ? 'OCR' : `${remainTime}s后重试` }}
</van-button>
</div>
</div>
</template>
<script>
import { uploadIdcard } from './shared'
import { ocrIdcard } from '@/api/markup'
import { ocrIdcardInEnterprise } from '@/api/rnr'
export default {
name: 'IdcardOcr',
props: {
images: {
type: Array,
default: () => ([])
},
customer: {
type: Boolean,
default: false
},
empty: {
type: String,
default: 'normal'
},
title: {
type: String,
default: '方式二:OCR识别'
}
},
data() {
return {
remainTime: 30,
loading: false,
formData: {}
}
},
methods: {
/**
* ocr扫描识别
*/
async handleOcrScan() {
// 增加loading框
this.loading = true
try {
// 上传的方法
const uploadFn = uploadIdcard(this.customer ? ocrIdcard : ocrIdcardInEnterprise)
// 开始全部上传接口
const { data } = await uploadFn(this.images)
// 触发改变方法
this.$emit('change', { ...data })
// 开启禁用ocr倒计时
this.disableOcrScanTimer()
} catch (error) {
console.error(error)
}
// 接触loading状态
this.loading = false
},
/**
* 开始禁用ocr识别功能
*/
disableOcrScanTimer() {
// 如果当前有进行中的定时器
this.timer && clearTimeout(this.timer)
// 如果当前倒计时结束
if (this.remainTime === 0) {
this.remainTime = 30
return
}
// 开始减去数字
this.remainTime -= 1
// 开始倒计时
this.timer = setTimeout(this.disableOcrScanTimer.bind(this), 1000)
}
}
}
</script>
<style lang="scss">
.component-ocr-idcard {
background-color: #ffffff;
display: flex;
flex: 1;
flex-direction: column;
.title {
color: #242424;
font-size: 24px;
font-weight: 500;
line-height: 33px;
padding-left: 32px;
}
.component-ocr-idcard-step {
color: rgba(36, 36, 36, 0.4);
font-size: 24px;
font-weight: 400;
line-height: 30px;
margin-top: 28px;
padding-left: 32px;
&.small {
height: 60px;
}
}
.component-ocr-idcard-icon-reader {
background-image: url(../../assets/rnr/idcard-ocr.png);
background-size: auto 100%;
width: 100%;
height: 146px;
}
.component-ocr-idcard-btn-wrap {
margin-top: 20px;
padding: 0 32px;
.van-button {
font-size: 30px;
height: 88px;
.page-rnr-userinfo-ocr {
background-image: url(../../assets/rnr/icon_ocr.svg);
background-size: 100% 100%;
display: inline-block;
width: 30px;
height: 30px;
margin: 0 12px -2px 0;
}
}
}
}
</style>
import dayjs from 'dayjs'
import { Toast } from 'vant'
export const uploadIdcard = api => {
// 表格数据
const formData = {}
// 是否ocr识别错误
let error = void 0
// 返回对象
return async dataURLs => {
await Promise.all(
dataURLs.map(dataURL => {
return new Promise(async(resolve) => {
try {
// 如果先压缩图片
const ocrPic = dataURL.replace('data:image/jpeg;base64,', '').replace('data:image/png;base64,', '')
// 文件可访问路径和文件的uuid
const respData = await api({ ocrPic })
// 身份证信息
if (respData.side === 'back') {
formData.fullName = respData.fullName
formData.gender = respData.gender === '' ? 1 : 2
formData.certNumber = respData.certNumber
formData.certAddress = respData.address
} else {
// 过期时间格式化
const validDate = respData.validDate.split('-')
// 如果当前是长期
if (validDate.length === 2) {
formData.certEffectiveDate = validDate[0].replace(/\./g, '-')
formData.certExpirationDate = validDate[1].replace(/\./g, '-')
// 当前过期时间
const expireDateInDayjs = dayjs(formData.certExpirationDate, 'YYYY-MM-DD')
// 如果身份证读取的过期时间小于当前天数
if (expireDateInDayjs.isBefore(dayjs().startOf('date'))) {
Toast('当前身份证已过期,请注意更换')
formData.certExpirationDate = ''
}
} else {
formData.certExpirationDate = respData.validDate
}
}
} catch (e) {
error = [1011, 1809].includes(e.code) ? 'detect error' : 'system error'
}
resolve()
})
})
)
return { error, data: { ...formData }}
}
}
<template>
<section class="page-rnr">
<header class="page-rnr-header">
<div class="page-rnr-title"><slot name="slot-title">{{ title }}</slot></div>
<div class="page-rnr-back" @click="handleToHome">返回首页<van-icon name="wap-home-o" /></div>
</header>
<slot name="slot-before-main" />
<main class="page-rnr-main">
<slot />
<div class="page-rnr-main-fixed-btn-group" v-show="showOperation">
<slot name="slot-operation" />
</div>
</main>
</section>
</template>
<script>
export default {
name: 'Page',
props: {
title: {
type: String,
default: ''
},
showOperation: {
type: Boolean,
default: true
}
},
activated() {
this.$nextTick(() => {
window.scrollTo({ top: 0 })
})
},
methods: {
/**
* 跳转到首页
*/
handleToHome() {
this.$router.push({ name: this.$route.meta.type === 'user' ? 'UserHome' : 'Home' })
}
}
}
</script>
<style lang="scss">
.page-rnr {
background-image: url(../../assets/rnr/bg-header.png);
background-repeat: no-repeat;
background-size: 100% auto;
background-position: left top;
.page-rnr-header {
overflow: hidden;
padding: 58px 0 0 48px;
.page-rnr-title {
color: #FFFFFF;
font-size: 36px;
font-weight: 600;
float: left;
text-shadow: 0 2px 10px rgba(41,38,169,0.65);
line-height: 50px;
}
.page-rnr-back {
align-items: center;
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%);
border-radius: 100px 0px 0px 100px;
color: #FFFFFF;
display: flex;
font-size: 20px;
font-weight: 400;
float: right;
width: 140px;
height: 48px;
padding-left: 20px;
.van-icon {
font-weight: 700;
margin-left: 8px;
}
}
}
.page-rnr-main {
padding-bottom: 184px;
margin-top: 46px;
.page-rnr-main-progress {
color: #FFFFFF;
font-weight: 500;
padding: 0 0 20px 48px;
.page-rnr-main-progress-curr {
font-size: 46px;
}
.page-rnr-main-progress-total {
color: rgba(255, 255, 255, 0.4);
font-size: 26px;
}
.page-rnr-main-progress-remain {
font-size: 30px;
font-weight: 400;
margin-left: 16px;
}
}
.page-rnr-main-card {
background: #ffffff;
border-radius: 16px;
box-sizing: border-box;
margin: 0 24px 24px;
padding: 0px 32px 0;
.page-rnr-main-card-title {
color: #242424;
font-size: 32px;
font-weight: 500;
padding: 30px 0 0;
.page-rnr-icon {
background-size: 100% 100%;
background-repeat: no-repeat;
display: inline-block;
width: 40px;
height: 40px;
margin-right: 12px;
}
}
.page-rnr-main-btn {
color: #0761F3;
font-size: 24px;
font-weight: 400;
}
.van-list {
margin-top: 36px;
.page-rnr-main-btn {
font-size: 24px;
margin-left: 30px;
}
}
.van-uploader {
width: 100%;
.van-uploader__wrapper {
display: flex;
.van-uploader__preview {
margin: 0 24px 16px 0;
.van-uploader__preview-image {
width: 196px;
height: 196px;
}
.van-uploader__preview-delete {
background-color: rgba(0, 0, 0, .4);
border-radius: 20px;
display: flex;
width: 40px;
height: 40px;
.van-uploader__preview-delete-icon {
font-size: 60px;
top: -10px;
right: -10px;
}
}
&:nth-child(3) {
margin-right: 0px;
}
}
}
}
}
.page-rnr-main-fixed-btn-group {
background-color: #ffffff;
bottom: 0px;
height: 148px;
padding: 20px 32px;
position: fixed;
left: 0px;
right: 0px;
}
.van-cell {
background-color: transparent;
padding: 0;
&.van-field {
background-color: transparent;
padding: 44px 0;
position: relative;
.van-field__label {
color: #242424;
font-size: 32px;
font-weight: 400;
height: 45px;
}
.van-cell__value {
height: 45px;
.van-field__control {
color: #242424;
font-size: 32px;
}
&.van-field__value {
position: relative;
.van-field__error-message {
position: absolute;
left: -220px;
bottom: -90px;
}
}
}
}
&.van-cell--required {
.van-cell__title {
span {
padding-left: 20px;
}
}
&:before {
left: 0px;
font-size: 16px;
}
}
.van-cell__title {
span {
color: #242424;
font-size: 32px;
font-weight: 400;
}
}
.van-cell__label {
margin-top: 36px;
}
&:after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0px;
bottom: 0;
left: 0px;
border-bottom: 1PX solid #ebedf0 !important;
transform: scaleY(.5);
}
}
}
.van-button {
font-size: 30px;
}
}
::-webkit-input-placeholder {
color: #CACACA !important;
font-weight: 400;
}
:-moz-placeholder {
color: #CACACA !important;
font-weight: 400;
}
::-moz-placeholder {
color: #CACACA !important;
font-weight: 400;
}
:-ms-input-placeholder {
color: #CACACA !important;
font-weight: 400;
}
</style>
<template>
<div :id="paintAreaId" class="component-painter">
<slot />
</div>
</template>
<script>
import html2canvas from 'html2canvas'
export default {
name: 'Painter',
data() {
return {
paintAreaId: `paint-${Date.now()}${Math.floor(Math.random() * 1000)}`
}
},
methods: {
/**
* 绘制对象,导出绘制内容
*/
async draw() {
// 导出画布
const cvs = await html2canvas(
document.querySelector(`#${this.paintAreaId}`),
{ useCORS: true }
)
return new Promise((resolve, reject) => {
cvs.toBlob(blob => {
// 返回处理结果
resolve(new File([blob], `${Date.now()}.png`, { type: 'image/png' }))
})
})
}
}
}
</script>
<style lang="scss">
.component-painter {
background-color: #ffffff;
padding: 20px;
img {
width: 100%;
height: auto;
&.fixed-right {
width: auto;
height: 120px;
margin: -120px 0 0 52%;
}
}
}
</style>
<template>
<van-popup
:value="visible"
:close-on-click-overlay="false"
class="component-phone-verify"
@click-close-icon="$emit('update:visible', false)"
>
<div class="van-dialog__header">手机号验证</div>
<ErrorWrap :error-message="inputError.phone" :error="!!inputError.phone">
<van-field
id="phone"
v-model="phone"
:clearable="true"
clear-trigger="always"
type="tel"
label="车主手机号"
placeholder="请输入"
input-align="right"
@blur="validateField('phone')"
/>
</ErrorWrap>
<van-field
ref="smsInputRef"
id="verificationCode"
v-model="verificationCode"
:clearable="true"
clear-trigger="always"
label="验证码"
type="digit"
maxlength="6"
placeholder="请输入"
input-align="right"
>
<template #button>
<div :class="isSend ? 'disabled' : ''" class="page-rnr-main-btn" @click="handleSendSms">{{ isSend ? `${countTime}s后重发` : '获取验证码' }}</div>
</template>
</van-field>
<van-button
:disabled="!phone || !verificationCode"
:loading="loading"
type="primary"
class="page-rnr-main-btn-confirm"
block
round
color="linear-gradient(120deg, #4F4CFB 0%, #376FF4 97%)"
@click="handleConfirm"
>确定</van-button>
<div class="icon-close" @click="$emit('update:visible', false)" />
</van-popup>
</template>
<script>
import ErrorWrap from './ErrorWrap'
import { sendSms } from '@/api/rnr'
import { markupSendSms } from '@/api/markup'
import { Toast } from 'vant'
import { validate } from '../utils/validate'
export default {
name: 'PhoneVerify',
components: { ErrorWrap },
props: {
visible: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
customer: {
type: Boolean,
default: false
}
},
data() {
return {
isSend: false,
countTime: 59,
phone: this.$store.getters.loginUser.phone || '',
verificationCode: '',
inputError: {
phone: '',
verificationCode: ''
}
}
},
methods: {
/**
* 校验手机号输入
* @param fieldKey
*/
validateField(fieldKey) {
// 先清空错误信息
this.$set(this.inputError, fieldKey, '')
// 校验错误的key和消息
const [failKey, failMessage] = validate({ [fieldKey]: this[fieldKey] }, [fieldKey])
// 如果当前有错误信息
if (failKey) {
this.inputError[failKey] = failMessage
return false
}
return true
},
/**
* 发送短信验证码
*/
async handleSendSms() {
const validResult = this.validateField('phone')
// 如果电话号码未输入
if (!validResult) {
return
}
// 如果已经发送过短信
if (this.isSend) {
return
}
// 发送短信
await (this.customer ? markupSendSms : sendSms)({ phone: this.phone, bizType: 'rnr' })
// 提示
Toast('短信已经发送,请注意查收')
// 标记当前已经发送过了
this.isSend = true
// 将输入框获取焦点
this.$refs.smsInputRef.focus()
// 开始倒计时
this.countDown()
},
/**
* 开始倒计时
*/
countDown() {
// 如果倒计时结束
if (this.countTime === 1) {
this.isSend = false
this.countTime = 60
return
}
// 开始将时间-1
this.countTime--
// 1s后执行倒计时的内容
setTimeout(() => {
this.countDown()
}, 1000)
},
/**
* 如果手机号和验证码输入正确
*/
handleConfirm() {
// 获取当前输入的手机号和验证码
const { phone, verificationCode } = this
// 如果校验手机号失败
if (!this.validateField('phone')) {
return
}
// 触发绑定到submit的事件
this.$emit('submit', { phone, verificationCode })
}
}
}
</script>
<style lang="scss">
.component-phone-verify {
&.van-popup {
box-sizing: border-box;
border-radius: 16px;
width: 685px;
overflow-y: visible;
padding: 0 40px 50px;
.van-dialog__header {
align-items: center;
color: #242424;
display: flex;
font-size: 36px;
font-weight: 500;
padding: 60px 0 12px;
&:after, &:before {
background-color: rgba(220, 223, 230, 1);
content: "";
display: block;
width: 194px;
height: 1px;
}
&:before {
margin-right: 16px;
}
&:after {
margin-left: 16px;
}
}
.van-field__button {
padding-left: 30px;
.page-rnr-main-btn {
color: #0761F3;
font-size: 32px;
font-weight: 400;
}
}
.page-rnr-main-btn-confirm {
margin-top: 44px;
}
.icon-close {
background-image: url(../assets/rnr/ico_close_dialog.png);
background-size: 100% 100%;
bottom: -86px;
left: calc(50% - 28px);
width: 56px;
height: 56px;
position: absolute;
}
}
}
</style>
<template>
<div class="component-scan-card">
<div class="component-scan-card-title">
<div class="component-scan-card-title-tl">VIN码</div>
<div class="component-scan-card-title-tc">
<div v-if="successNum > 0" class="title-block success">成功:{{ successNum }}</div>
<div v-if="failNum > 0" class="title-block fail">失败:{{ failNum }}</div>
</div>
<div class="component-scan-card-title-tr">{{ total }}/30</div>
</div>
<slot name="content">
<div class="component-scan-card-placeholder" />
<div class="component-scan-card-h1">请手动添加车辆VIN码</div>
<!-- <div class="component-scan-card-h2">(最多添加30条信息)</div> -->
</slot>
<div class="component-scan-card-btn-wrapper">
<van-button plain hairline round color="#0761F3" @click="$emit('add')"><i class="imageicon imageicon-add" />添加</van-button>
<!-- <van-button v-if="!hideScan" plain hairline round block color="#0761F3" @click="handleScanCode"><i class="imageicon imageicon-scan" />扫码添加</van-button> -->
</div>
</div>
</template>
<script>
import JsBridge from '@/utils/bridge'
export default {
name: 'ScanCard',
props: {
successNum: {
type: Number,
default: 0
},
failNum: {
type: Number,
default: 0
},
total: {
type: Number,
default: 0
},
hideScan: {
type: Boolean,
default: false
}
},
methods: {
/**
* 开始扫码
*/
handleScanCode() {
JsBridge.scanCode(true, vin => {
this.$emit('scan', vin)
})
}
}
}
</script>
<style lang="scss">
.component-scan-card {
padding-bottom: 36px;
.component-scan-card-title {
align-items: center;
display: flex;
height: 46px;
padding: 30px 0 48px;
.component-scan-card-title-tl {
color: #242424;
font-size: 32px;
font-weight: 500;
flex-shrink: 0;
}
.component-scan-card-title-tc {
display: flex;
flex: 1;
padding-left: 24px;
.title-block {
border-radius: 23px;
color: #514040;
font-size: 26px;
line-height: 46px;
padding: 0 16px 0 40px;
position: relative;
&:before {
border-radius: 6px;
content: "";
display: block;
position: absolute;
width: 12px;
height: 12px;
left: 16px;
top: 18px;
}
&.success {
background-color: rgba(113, 200, 68, 0.07);
&:before {
background-color: #71C844;
}
}
&.fail {
background-color: rgba(255, 76, 79, 0.07);
margin-left: 12px;
&:before {
background-color: #FF4C4F;
}
}
}
}
.component-scan-card-title-tr {
color: #CACACA;
font-size: 32px;
flex-shrink: 0;
}
}
.component-scan-card-placeholder {
background-position: center;
background-image: url(../assets/rnr/ico_scan-placeholder.png);
background-size: auto 100%;
background-repeat: no-repeat;
height: 184px;
}
.component-scan-card-h1 {
color: #242424;
font-size: 32px;
text-align: center;
height: 44px;
margin-top: 24px;
}
.component-scan-card-h2 {
color: #CACACA;
font-size: 26px;
height: 35px;
text-align: center;
}
.component-scan-card-btn-wrapper {
display: flex;
margin-top: 48px;
.van-button {
flex: 1;
margin-left: 40px;
.van-button__text {
align-items: center;
display: flex;
justify-content: center;
.imageicon {
margin-right: 12px;
}
}
&:first-child {
margin-left: 0;
}
}
}
}
</style>
<script>
import JsBridge from '@/utils/bridge'
export default {
name: 'ScanCode',
props: {
display: {
type: String,
default: 'block'
}
},
data() {
return {
showUploadSheet: false
}
},
methods: {
handleScanCode(isBarcode) {
JsBridge.scanCode(isBarcode, vin => {
this.$emit('change', vin)
})
}
},
render() {
const ACTIONS = [
{ name: '扫条形码', value: true },
{ name: '扫二维码', value: false }
]
const defaultSlot = this.$scopedSlots.default()
return (
<div class='component-scan-code' style={{ display: this.display }}>
<div style={{ display: this.display, width: '100%' }} onClick={() => (this.showUploadSheet = true)}>
{defaultSlot}
</div>
<van-action-sheet value={this.showUploadSheet} onInput={value => (this.showUploadSheet = value)} actions={ACTIONS} cancel-text='取消' onSelect={option => this.handleScanCode(option.value)} />
</div>
)
}
}
</script>
<template>
<van-search
v-model="value"
:placeholder="placeholder"
show-action
class="component-search-input van-hairline--bottom"
@search="handleSearch"
@cancel="handleClear"
>
<template #action>
<div @click="handleSearch">搜索</div>
</template>
</van-search>
</template>
<script>
export default {
name: 'SearchInput',
props: {
placeholder: {
type: String,
default: '请输入搜索关键词'
}
},
data() {
return {
value: ''
}
},
methods: {
/**
* 设值
*/
setValue(value) {
this.value = value
},
/**
* 搜索方法
*/
handleSearch() {
this.$emit('search', this.value)
},
/**
* 清空操作
*/
handleClear() {
this.value = ''
}
}
}
</script>
<style lang="scss">
.component-search-input {
padding: 25px 0;
.van-search__content {
background-color: transparent;
padding-left: 0;
.van-cell.van-field {
padding: 0;
height: 45px;
}
}
.van-search__action {
color: #0761F3;
font-size: 30px;
text-align: center;
font-weight: 400;
}
}
</style>
<template>
<div class="component-sign" v-show="visible">
<SignCanvas ref="signRef" v-model="sign" :options="options" />
<div class="component-sign-action">
<div class="sign-tip">请在上方空白处,用正楷体横向书写您的名字</div>
<div class="component-sign-btn btn-light" @click="$emit('update:visible', false)">取消</div>
<div class="component-sign-btn btn-light" @click="handleClearCanvas">重签</div>
<div :class="sign ? '' : 'disable'" class="component-sign-btn btn-primary" @click="handleSubmit">提交签字</div>
</div>
</div>
</template>
<script>
import SignCanvas from 'sign-canvas'
export default {
name: 'Sign',
components: { SignCanvas },
props: {
/**
* 是否可见
*/
visible: {
type: Boolean,
default: true
}
},
data() {
// 屏幕宽度和高度
const windowWidth = document.documentElement.clientWidth || document.body.clientWidth
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight
return {
sign: '',
options: {
// 书写速度 [Number] 可选
lastWriteSpeed: 1,
// 下笔的宽度 [Number] 可选
lastWriteWidth: 4,
// 线条的边缘类型 [butt]平直的边缘 [round]圆形线帽 [square] 正方形线帽
lineCap: 'round',
// 线条交汇时边角的类型 [bevel]创建斜角 [round]创建圆角 [miter]创建尖角。
lineJoin: 'round',
// canvas宽高 [Number] 可选
canvasWidth: windowWidth,
// 高度 [Number] 可选
canvasHeight: windowHeight,
// 是否显示边框 [可选]
isShowBorder: false,
// 背景色 [String] 可选
bgColor: '#FFFFFF',
// 网格线宽度 [Number] 可选
// borderWidth: 1,
// 网格颜色 [String] 可选
// borderColor: "#ff787f",
// 基础轨迹宽度 [Number] 可选
writeWidth: 5,
// 写字模式最大线宽 [Number] 可选
maxWriteWidth: 30,
// 写字模式最小线宽 [Number] 可选
minWriteWidth: 5,
// 轨迹颜色 [String] 可选
writeColor: '#101010',
// 签名模式 [Boolean] 默认为非签名模式,有线框, 当设置为true的时候没有任何线框
isSign: true,
// 下载的图片格式 [String] 可选为 jpeg canvas本是透明背景的
imgType: 'png'
}
}
},
methods: {
/**
* 清空画布
*/
handleClearCanvas() {
this.$refs.signRef.canvasClear()
},
/**
* 提交方法
*/
async handleSubmit() {
// 如果当前没有签名
if (!this.sign) {
return
}
// 当前签名
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const img = new Image()
img.src = this.sign
img.onload = () => {
canvas.width = img.height
canvas.height = img.width
context.translate(canvas.width / 2, canvas.height / 2)
context.rotate(-90 * Math.PI / 180)
context.translate(canvas.width / 4, -canvas.height / 2)
context.drawImage(img, -canvas.width / 2, -canvas.height / 2, img.width, img.height)
// 将当前生成的签名图片返回
this.$emit('submit', canvas.toDataURL('image/png'))
// 关闭弹框
this.$emit('update:visible', false)
// 清空画板
this.handleClearCanvas()
}
}
}
}
</script>
<style lang="scss" scoped>
.component-sign {
background-color: #ffffff;
bottom: 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9;
.component-sign-action {
align-items: center;
border-top: 1px solid #DCDFE6;
bottom: 0;
display: flex;
left: 0;
top: -154px;
width: 100vh;
height: 154px;
position: fixed;
transform: rotate(90deg);
transform-origin: left bottom;
justify-content: center;
.sign-tip {
color: #242424;
font-size: 28px;
font-weight: 400;
opacity: 0.4;
margin-right: 70px;
}
.component-sign-btn {
align-items: center;
border-radius: 48px;
display: flex;
font-size: 30px;
font-weight: 500;
width: 254px;
height: 96px;
justify-content: center;
margin-left: 24px;
&.btn-light {
background-image: linear-gradient(120deg, #EDF3FF 0%, #E9F0FF 97%);
color: #242424;
}
&.btn-primary {
background-image: linear-gradient(120deg, #4F4CFB 0%, #376FF4 97%);
color: #ffffff;
}
&.disable {
opacity: 0.5;
}
}
}
}
</style>
<template>
<div class="component-time-line">
<slot />
</div>
</template>
<script>
export default {
name: 'TimeLine'
}
</script>
<style lang="scss">
.component-time-line {
position: relative;
&:before {
border-left: 2px dashed rgba(202, 202, 202, 1);
content: "";
display: block;
position: absolute;
width: 2px;
left: 14px;
top: 46px;
bottom: 0px;
}
}
</style>
<template>
<div class="component-time-line-item">
<div class="component-time-line-item-row">
<van-icon :name="icon" />
<div class="component-time-line-item-time">{{ time }}</div>
</div>
<div class="component-time-line-item-content">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'TimeLineItem',
props: {
icon: {
type: String,
default: 'clock'
},
time: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss">
.component-time-line-item {
.component-time-line-item-row {
align-items: center;
display: flex;
.van-icon {
color: #0761F3;
font-size: 28px;
}
.component-time-line-item-time {
color: #242424;
font-size: 34px;
font-weight: 500;
margin-left: 12px;
height: 48px;
line-height: 48px;
}
}
.component-time-line-item-content {
margin-top: 24px;
padding-left: 32px;
padding-bottom: 36px;
}
}
</style>
<template>
<div class="component-user-info">
<div v-if="certType === 'IDCARD'" class="page-rnr-main-card pb-36">
<Bluetooth v-if="showBluetooth" @change="handleDeviceRead" />
<OcrIdcard :class="showBluetooth ? 'pt-40' : ''" :images="certPic" :customer="customer" :title="`方式${showBluetooth ? '二' : '一'}:OCR识别`" @change="handleDeviceRead" />
</div>
<div class="page-rnr-main-card pd-32">
<FromRender
ref="formRenderRef"
v-model="formData"
:schema="SCHEMA"
@change="handleFormDataChange"
>
<template #long-term>
<van-checkbox
v-model="formData.longTerm"
class="long-term"
@change="handleLongTermChange"
>
长期有效
</van-checkbox>
</template>
</FromRender>
</div>
</div>
</template>
<script>
import cache from '@/utils/cache'
import dayjs from 'dayjs'
import FromRender from '@/components/FormRender'
import OcrIdcard from '@/components/Ocr/Idcard'
import Bluetooth from '@/components/Bluetooth'
import { isInApp } from '@/utils/bridge'
export default {
name: 'UserInfo',
components: { OcrIdcard, Bluetooth, FromRender },
props: {
value: {
type: Object,
default: () => ({})
},
certType: {
type: String,
default: ''
},
customer: {
type: Boolean,
default: false
},
showPhone: {
type: Boolean,
default: false
}
},
data() {
const { SEX_OPTIONS } = this.$store.getters.dict
return {
SCHEMA: Object.assign({
fullName: {
type: 'rnr-input',
label: '姓名',
rules: [
{ required: true }
]
},
gender: {
type: 'rnr-picker',
label: '性别',
rules: [
{ required: true }
],
options: SEX_OPTIONS
},
certNumber: {
type: 'rnr-input',
label: '证件号码',
rules: [
{ required: true },
{
validator: (value) => {
// 校验结果
let result = true
// 判断证件类型
switch (this.certType) {
case 'IDCARD':
result = /(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(value)
break
case 'HKIDCARD':
result = /^[H|M][0-9]{10}$/.test(value)
break
case 'TAIBAOZHENG':
result = /^([0-9]|[a-z]|[A-Z]){10}$/.test(value)
break
default:
break
}
return result
}
}
]
},
certAddress: {
type: 'rnr-input',
label: '证件地址',
rules: [
{ required: true }
]
},
certEffectiveDate: {
type: 'rnr-date-picker',
label: '证件起始时间',
maxDate: new Date(),
rules: [
{ required: true }
]
},
certExpirationDate: {
type: 'rnr-date-picker',
label: '证件有效期',
minDate: new Date(),
rules: [
{ required: true }
]
},
longTerm: {
type: 'slot',
default: false,
border: 'none',
slot: 'long-term'
},
contactAddress: {
type: 'rnr-input',
label: '通讯地址'
}
}, this.showPhone ? {
phone: {
type: 'rnr-input',
label: '手机号',
rules: [
{ required: true },
{ test: /^1\d{10}$/ }
]
}
} : {}),
formData: {
longTerm: false,
...(this.value || {})
},
isInApp,
certPic: []
}
},
computed: {
/**
* 是否展示蓝牙按钮
*/
showBluetooth() {
return !this.customer && isInApp
}
},
watch: {
/**
* 监听输入值的变动,如果有变动,则重新赋值
*/
value: {
handler() {
Object.keys(this.value).forEach(key => {
this.formData[key] = this.value[key]
// 如果当前是长期
if (key === 'certExpirationDate') {
this.formData.longTerm = this.value[key] === '长期'
}
})
},
deep: true
}
},
activated() {
// 上个页面传过来的证件照片
const { certPicKey } = this._routerRoot._route.query
// 证件照片
const certPic = cache.get(certPicKey)
// 每次进入页面都查找一次最新的证件照片
this.certPic = certPic ? certPic.map(item => item.content.slice(item.content.indexOf('base64,') + 7)) : []
},
methods: {
/**
* 当选择了长期或者非长期之后
*/
handleLongTermChange() {
if (this.formData.longTerm) {
this.$set(this.formData, 'certExpirationDate', '长期')
} else {
if (this.formData.certExpirationDate === '长期') {
this.$set(this.formData, 'certExpirationDate', '')
}
}
},
/**
* 表单数据发生变化,且当前不为长期
*/
handleFormDataChange(val, key) {
// 如果当前
if (key === 'certExpirationDate' && this.formData.certExpirationDate !== '长期') {
this.$set(this.formData, 'longTerm', false)
}
},
/**
* 校验输入项
*/
validate() {
// 校验第一个输入表单
this.$refs.formRenderRef.validate()
// 当前过期时间
const certExpirationDate = dayjs(this.formData.certExpirationDate)
const certEffectiveDate = dayjs(this.formData.certEffectiveDate)
// 校验成功
return {
...this.formData,
certExpirationDate: certExpirationDate.isValid() ? certExpirationDate.format('YYYY-MM-DD') : this.formData.certExpirationDate,
certEffectiveDate: certEffectiveDate.isValid() ? certEffectiveDate.format('YYYY-MM-DD') : this.formData.certEffectiveDate
}
},
/**
* ocr识别的内容
*/
handleDeviceRead(info) {
// 将ocr内容赋值给组件
this.formData = {
...this.formData,
...info
}
// 当前是否长期
this.formData.longTerm = info.certExpirationDate === '长期'
// 校验表单数据
this.$nextTick(this.$refs.formRenderRef.validate)
}
}
}
</script>
<style lang="scss">
.component-user-info {
.page-rnr-main-card {
.van-row {
align-items: stretch;
.van-col {
display: flex;
flex-direction: column;
.step-title {
color: rgba(36, 36, 36, 0.4);
font-size: 24px;
font-weight: 500;
flex-shrink: 0;
line-height: 32px;
height: 32px;
margin: 0 12px;
text-align: center;
}
}
}
.long-term {
margin-top: 12px;
}
.pt-40 {
padding-top: 40px;
}
&.pb-36 {
padding: 30px 0 36px 0 !important;
}
&.pd-32 {
padding: 0 32px;
}
}
}
</style>
<template>
<div class="page-rnr-main-card component-vin-scan">
<div class="page-rnr-person-card-car" />
<ErrorWrap :error-message="errorMessage" :error="!!errorMessage">
<van-field
v-model="vin"
:clearable="true"
clear-trigger="always"
label="VIN码"
maxlength="17"
placeholder="请输入"
input-align="right"
@input="handleVinInput()"
@blur="validateVin(vin)"
/>
</ErrorWrap>
<!-- <van-button v-if="!hideScan" class="page-rnr-btn" plain hairline round block color="#0761F3" @click="handleScanCode"><i class="imageicon imageicon-scan" />扫码添加</van-button> -->
</div>
</template>
<script>
import JsBridge from '@/utils/bridge'
import ErrorWrap from './ErrorWrap'
// vin码长度
const VIN_LENGTH = 17
export default {
name: 'VinScan',
components: { ErrorWrap },
props: {
hideScan: {
type: Boolean,
default: false
}
},
data() {
return {
vin: '',
errorMessage: ''
}
},
activated() {
const { vin } = this.$store.getters.loginUser
if (vin) {
this.validateVin(vin)
if (!this.errorMessage) {
this.vin = vin
this.handleVinInput()
}
}
},
methods: {
/**
* 开始扫码
*/
handleScanCode() {
JsBridge.scanCode(true, vin => {
// 校验数据
this.validateVin(vin)
// 如果当前没有错误信息
if (!this.errorMessage) {
// 记录当前vin
this.vin = vin
// 开始同步给父组件
this.handleVinInput()
}
})
},
/**
* vin输入的方法,当输入是17位,则触发change方法
*/
handleVinInput() {
// 如果当前输入的vin不是17位
if (this.vin.length !== 17) {
return
}
// 触发变更方法
this.$emit('change', this.vin)
},
/**
* 校验vin码
* @param vin 当前vin码
*/
validateVin(vin) {
// 如果没有输入vin码,或者vin码不合法
if (!vin) {
this.errorMessage = '请输入VIN码'
} else if (vin.length !== VIN_LENGTH) {
// 如果没有输入vin码,或者vin码不合法
this.errorMessage = '请输入正确的VIN码'
} else {
this.errorMessage = ''
}
}
}
}
</script>
<style lang="scss">
.component-vin-scan {
padding-bottom: 36px !important;
.page-rnr-person-card-car {
background-image: url(../assets/rnr/ico_car.png);
background-repeat: no-repeat;
background-size: 360px 184px;
background-position: top 40px center;
width: 360px;
height: 232px;
margin: 0 auto;
}
.page-rnr-btn {
margin-top: 24px;
.imageicon {
margin-bottom: -6px;
margin-right: 12px;
}
}
.van-field {
.van-field__label {
width: 120px;
}
.van-icon-clear {
padding: 0 14px;
margin-left: 36px;
}
}
}
</style>
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import Vant from 'vant'
import 'vant/lib/index.css'
import router from './router'
import store from './store'
import './utils/os'
import './permission'
import 'lib-flexible/flexible.js'
import 'normalize.css/normalize.css'
import './style/index.scss'
Vue.use(Vant)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
export default {
computed: {
/**
* 总流程长度
*/
totalFlow() {
// 如果当前是委托人
if (this.$store.getters.personRnr.isConsigner) {
// 新车是7步,二手车是8步
return this.$store.getters.personRnr.customerType === 0 ? 8 : 9
}
// 新车是5步,二手车是6步
return this.$store.getters.personRnr.customerType === 0 ? 5 : 6
},
/**
* 文件所处的流程
*/
currentFlow() {
return this.$route.query.step
}
},
methods: {
/**
* 跳转到下一页
* @param pageName 需要跳转的页名
*/
routerToNextPage(pageName, query) {
// 取出当前步骤
const { step } = this.$route.query
// 跳转到支付结果页
this.$router.push({ name: pageName, query: { step: +step + 1, ...(query || {}) }})
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment