CRM_26-08-05#story#9126,车辆管理、服务商管理系统需求
This commit is contained in:
@@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<div class="cameraGuide" v-if="visible">
|
||||
<!-- ===== 拍摄引导页 ===== -->
|
||||
<div class="cameraPage" v-show="step==='guide'">
|
||||
<video ref="video" class="cameraVideo" autoplay playsinline muted></video>
|
||||
|
||||
<div class="cameraNav">
|
||||
<van-icon name="arrow-left" class="backBtn" @click="handleClose" />
|
||||
<span class="navTitle">{{ navTitle }}</span>
|
||||
</div>
|
||||
|
||||
<div class="guideLayer">
|
||||
<!-- 顶部主指令卡 -->
|
||||
<div class="guideHero">
|
||||
<div class="guideHeroIcon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<path d="M3 9h18"></path>
|
||||
<circle cx="8" cy="13.5" r="1.6"></circle>
|
||||
<path d="M11 12.5h7M11 15h5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="guideHeroText">
|
||||
<div class="guideHeroTitle">{{ heroTitle }}</div>
|
||||
<div class="guideHeroSub">从卡套内拿出,避免反光</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 取景框 -->
|
||||
<div class="guideFrameOuter">
|
||||
<span class="corner tl"></span>
|
||||
<span class="corner tr"></span>
|
||||
<span class="corner bl"></span>
|
||||
<span class="corner br"></span>
|
||||
<div class="guideFrameBorder"></div>
|
||||
<div class="guideHole">
|
||||
<!-- 主页模板:标题 + 印章 -->
|
||||
<div v-if="type==='front'" class="licenseTemplate ltFrontPage">
|
||||
<div class="ltInner">
|
||||
<div class="ltTitle">
|
||||
<div class="ltTitleLine">中华人民共和国机动车行驶证</div>
|
||||
</div>
|
||||
<div class="ltSeal">印章</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 副页模板:条形码 + 编号 -->
|
||||
<div v-else-if="type==='back'" class="licenseTemplate ltBackPage">
|
||||
<div class="ltInner">
|
||||
<div class="ltBarcodeArea">
|
||||
<div class="ltBarcodeStrip">
|
||||
<div v-for="(bar, i) in barcodeBars" :key="i" class="ltBar" :class="bar"></div>
|
||||
</div>
|
||||
<div class="ltBarcodeNum">3701001234567</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 车辆照片模板:标题 + 车辆轮廓 -->
|
||||
<div v-else class="licenseTemplate ltCarPage">
|
||||
<div class="ltInner">
|
||||
<div class="ltTitle">
|
||||
<div class="ltTitleLine">车辆照片</div>
|
||||
</div>
|
||||
<div class="ltCarOutline">
|
||||
<svg viewBox="0 0 140 70" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 46 L20 26 Q22 20 30 19 L88 16 Q96 16 102 22 L116 34 Q126 36 128 42 L128 48 Q128 52 124 52 L116 52"></path>
|
||||
<path d="M14 46 L14 50 Q14 52 18 52 L30 52"></path>
|
||||
<circle cx="42" cy="52" r="9"></circle>
|
||||
<circle cx="102" cy="52" r="9"></circle>
|
||||
<path d="M51 52 L93 52"></path>
|
||||
<path d="M58 19 L60 33 L34 34"></path>
|
||||
<path d="M92 18 L94 32 L114 33"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部取景要点 -->
|
||||
<div class="guideTips">
|
||||
<div class="tipItem ok">
|
||||
<span class="tipIco">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 8V4h4M16 4h4v4M20 16v4h-4M8 20H4v-4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="tipLabel">四角入框</span>
|
||||
</div>
|
||||
<div class="tipItem ok">
|
||||
<span class="tipIco">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12h16M7 7h10M7 17h10"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="tipLabel">平放正向</span>
|
||||
</div>
|
||||
<div class="tipItem warn">
|
||||
<span class="tipIco">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M5 19l2-2"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="tipLabel">避免反光</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shootBar">
|
||||
<div class="shootBtn" @click="takePhoto"></div>
|
||||
<div class="shootLabel">点击拍照</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 预览确认页 ===== -->
|
||||
<div class="previewPage" v-show="step==='preview'">
|
||||
<div class="cameraNav">
|
||||
<van-icon name="arrow-left" class="backBtn" @click="handlePreviewBack" />
|
||||
<span class="navTitle">照片预览</span>
|
||||
</div>
|
||||
<div class="previewImgWrap">
|
||||
<img class="previewImg" :src="previewDataURL" alt="预览" />
|
||||
</div>
|
||||
<div class="previewBtns">
|
||||
<button class="previewBtn btnRetake" @click="retake">重拍</button>
|
||||
<button class="previewBtn btnConfirm" @click="confirmPhoto">确认使用</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 拍摄引导组件:WebRTC 取景 + 引导框 + 分页面证件模板 + 预览确认
|
||||
// props: type(front/back/car), visible
|
||||
// emit: confirm({blob, dataURL})、cancel、fallback(权限拒绝/相机不可用,父组件降级系统选择器)
|
||||
export default {
|
||||
name: 'CameraGuide',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'front'
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const heights = ['tall', 'short', 'medium wide', 'tall', 'short', 'medium', 'tall wide', 'short',
|
||||
'medium', 'tall', 'short wide', 'medium', 'tall', 'short', 'medium wide', 'tall',
|
||||
'short', 'medium', 'tall wide', 'short', 'medium', 'tall', 'short wide', 'medium',
|
||||
'tall', 'short', 'medium wide', 'tall', 'short', 'medium', 'tall wide', 'short',
|
||||
'medium', 'tall', 'short wide', 'medium', 'tall', 'short', 'medium wide', 'tall']
|
||||
return {
|
||||
step: 'guide', // guide 拍摄引导 | preview 预览确认
|
||||
stream: null,
|
||||
previewBlob: null,
|
||||
previewDataURL: '',
|
||||
barcodeBars: heights
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
navTitle() {
|
||||
return { front: '拍摄行驶证主页', back: '拍摄行驶证副页', car: '拍摄行驶证车辆照片' }[this.type] || '拍摄行驶证'
|
||||
},
|
||||
heroTitle() {
|
||||
return { front: '请将主页对准取景框', back: '请将副页对准取景框', car: '请将车辆对准取景框' }[this.type] || '请将证件对准取景框'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
this.step = 'guide'
|
||||
this.previewBlob = null
|
||||
this.previewDataURL = ''
|
||||
this.$nextTick(() => this.startCamera())
|
||||
} else {
|
||||
this.stopCamera()
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopCamera()
|
||||
},
|
||||
methods: {
|
||||
async startCamera() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' },
|
||||
audio: false
|
||||
})
|
||||
// 等待期间组件已被关闭,立即释放
|
||||
if (!this.visible) {
|
||||
stream.getTracks().forEach(t => t.stop())
|
||||
return
|
||||
}
|
||||
this.stream = stream
|
||||
await this.$nextTick()
|
||||
const video = this.$refs.video
|
||||
if (video) {
|
||||
video.srcObject = stream
|
||||
video.play().catch(() => {})
|
||||
}
|
||||
} catch (e) {
|
||||
// 权限拒绝或相机不可用 → 通知父组件降级为系统选择器
|
||||
console.error('getUserMedia error:', e)
|
||||
this.$emit('fallback')
|
||||
}
|
||||
},
|
||||
stopCamera() {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(t => t.stop())
|
||||
this.stream = null
|
||||
}
|
||||
},
|
||||
takePhoto() {
|
||||
const video = this.$refs.video
|
||||
if (!video || !video.videoWidth) {
|
||||
this.$toast('相机尚未就绪,请稍候')
|
||||
return
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||
const dataURL = canvas.toDataURL('image/jpeg', 0.92)
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) {
|
||||
this.$toast('拍摄失败,请重试')
|
||||
return
|
||||
}
|
||||
this.previewBlob = blob
|
||||
this.previewDataURL = dataURL
|
||||
this.step = 'preview'
|
||||
}, 'image/jpeg', 0.92)
|
||||
},
|
||||
retake() {
|
||||
// 重拍回到引导页,相机流保持
|
||||
this.step = 'guide'
|
||||
},
|
||||
confirmPhoto() {
|
||||
this.$emit('confirm', { blob: this.previewBlob, dataURL: this.previewDataURL })
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('cancel')
|
||||
},
|
||||
handlePreviewBack() {
|
||||
// 预览页返回 = 放弃拍摄,关闭并释放相机流
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cameraGuide {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3000;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
.cameraPage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(160deg, #F7F9FC 0%, #EEF1F6 50%, #E6EAF1 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cameraVideo {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
}
|
||||
.cameraNav {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 52px;
|
||||
padding-left: 14px;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
background: #2C395F;
|
||||
color: #fff;
|
||||
.backBtn {
|
||||
font-size: 22px;
|
||||
width: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.navTitle {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 18px;
|
||||
font-weight: bolder;
|
||||
letter-spacing: .5px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
.guideLayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 80px;
|
||||
padding-bottom: 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.guideHero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E9E9EA;
|
||||
border-radius: 6px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 24px;
|
||||
max-width: 322px;
|
||||
width: calc(100% - 48px);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 10px rgba(20, 30, 50, .08);
|
||||
.guideHeroIcon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
background: rgba(53, 77, 147, .10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #354D93;
|
||||
svg {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
}
|
||||
.guideHeroTitle {
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
.guideHeroSub {
|
||||
color: #5B606E;
|
||||
font-size: 12.5px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
/* 取景框:宽约为屏宽 80%,宽高比约 1.43:1 */
|
||||
.guideFrameOuter {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
max-width: 340px;
|
||||
padding-top: 69.93%; /* 宽高比约 1.43:1 */
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 4px 16px rgba(20, 30, 50, .12));
|
||||
}
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-color: #354D93;
|
||||
border-style: solid;
|
||||
z-index: 10;
|
||||
animation: cornerFadeIn .45s ease both;
|
||||
&.tl { top: -4px; left: -4px; border-width: 4px 0 0 4px; border-radius: 4px 0 0 0; }
|
||||
&.tr { top: -4px; right: -4px; border-width: 4px 4px 0 0; border-radius: 0 4px 0 0; }
|
||||
&.bl { bottom: -4px; left: -4px; border-width: 0 0 4px 4px; border-radius: 0 0 0 4px; }
|
||||
&.br { bottom: -4px; right: -4px; border-width: 0 4px 4px 0; border-radius: 0 0 4px 0; }
|
||||
}
|
||||
@keyframes cornerFadeIn {
|
||||
from { opacity: 0; transform: scale(1.15); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.guideFrameBorder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 3px solid #C2C8D2;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 9;
|
||||
}
|
||||
.guideHole {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 证件模板叠层(仅视觉辅助,不拦截事件) */
|
||||
.licenseTemplate {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 6;
|
||||
border: 1.5px solid rgba(53, 77, 147, .55);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
background: rgba(53, 77, 147, .06);
|
||||
}
|
||||
.ltInner {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ltTitle {
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ltTitleLine {
|
||||
font-size: 10px;
|
||||
color: rgba(53, 77, 147, .9);
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ltSeal {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1.5px dashed rgba(255, 85, 59, .85);
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 5.5px;
|
||||
color: rgba(255, 85, 59, .85);
|
||||
}
|
||||
.ltFrontPage .ltInner {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ltBackPage .ltInner {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ltBarcodeArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
padding-bottom: 6px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.ltBarcodeStrip {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 28px;
|
||||
gap: 1px;
|
||||
.ltBar {
|
||||
background: rgba(53, 77, 147, .6);
|
||||
width: 1.5px;
|
||||
border-radius: 0.5px;
|
||||
&.wide { width: 2.8px; }
|
||||
&.short { height: 50%; }
|
||||
&.medium { height: 70%; }
|
||||
&.tall { height: 100%; }
|
||||
}
|
||||
}
|
||||
.ltBarcodeNum {
|
||||
font-size: 8px;
|
||||
color: rgba(53, 77, 147, .9);
|
||||
letter-spacing: 2px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
.ltCarPage .ltCarOutline {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 60%;
|
||||
opacity: .6;
|
||||
color: #354D93;
|
||||
}
|
||||
}
|
||||
.guideTips {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
padding: 0 14px;
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tipItem {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 6px;
|
||||
padding: 11px 4px 9px;
|
||||
border: 1px solid #E9E9EA;
|
||||
box-shadow: 0 2px 8px rgba(20, 30, 50, .06);
|
||||
.tipIco {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
&.ok .tipIco {
|
||||
background: rgba(53, 77, 147, .10);
|
||||
color: #354D93;
|
||||
}
|
||||
&.warn .tipIco {
|
||||
background: rgba(255, 85, 59, .12);
|
||||
color: #FF553B;
|
||||
}
|
||||
.tipLabel {
|
||||
color: #5B606E;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.shootBar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
padding-bottom: calc(32px + env(safe-area-inset-bottom));
|
||||
background: #FFFFFF;
|
||||
border-top: 1px solid #E9E9EA;
|
||||
}
|
||||
.shootBtn {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #354D93;
|
||||
background: #FFFFFF;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 2px rgba(53, 77, 147, .22), 0 6px 16px rgba(20, 30, 50, .18);
|
||||
&:active {
|
||||
transform: scale(.92);
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
background: #354D93;
|
||||
}
|
||||
}
|
||||
.shootLabel {
|
||||
color: #5B606E;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
/* ===== 预览确认页 ===== */
|
||||
.previewPage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #F5F5F5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 20;
|
||||
}
|
||||
.previewImgWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.previewImg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.previewBtns {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 0 20px 32px;
|
||||
padding-bottom: calc(32px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.previewBtn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
&:active {
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
.btnRetake {
|
||||
background: #FFFFFF;
|
||||
color: #354D93;
|
||||
border: 1px solid #354D93;
|
||||
}
|
||||
.btnConfirm {
|
||||
background: #354D93;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="step-bar">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="step-item"
|
||||
:class="getStepClass(index)"
|
||||
>
|
||||
<div class="step-circle" @click="onStepClick(index)">
|
||||
<span v-if="getStatus(index) === 'finish'" class="step-check">✓</span>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="step-label">{{ step }}</div>
|
||||
<div v-if="index < steps.length - 1" class="step-line" :class="{'step-line-done': getStatus(index) === 'finish'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StepBar',
|
||||
props: {
|
||||
current: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => ['基本信息', '添加企微', '车辆信息', '驾驶员信息']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatus(index) {
|
||||
const stepNum = index + 1;
|
||||
if (stepNum < this.current) return 'finish';
|
||||
if (stepNum === this.current) return 'process';
|
||||
return 'wait';
|
||||
},
|
||||
getStepClass(index) {
|
||||
return `step-${this.getStatus(index)}`;
|
||||
},
|
||||
onStepClick(index) {
|
||||
const stepNum = index + 1;
|
||||
if (this.getStatus(index) === 'finish') {
|
||||
this.$emit('step-click', stepNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.step-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 14px 10px 10px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-finish .step-circle {
|
||||
background: #0E76F4;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-process .step-circle {
|
||||
background: #0E76F4;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(14, 118, 244, 0.2);
|
||||
}
|
||||
|
||||
.step-wait .step-circle {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 11px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-finish .step-label {
|
||||
color: #0E76F4;
|
||||
}
|
||||
|
||||
.step-process .step-label {
|
||||
color: #0E76F4;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-wait .step-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: calc(50% + 13px);
|
||||
right: calc(-50% + 13px);
|
||||
height: 2px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.step-line-done {
|
||||
background: #0E76F4;
|
||||
}
|
||||
|
||||
.step-check {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user