访客页面
This commit is contained in:
1 parent
f1e4ba7d31
commit
f8b5e3c96b
2 files changed
+712
No files matched your search
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<view class="verify-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
</view>
|
||||
<view class="title">访客核销</view>
|
||||
<view class="right-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 扫码按钮 -->
|
||||
<view class="scan-section">
|
||||
<view class="scan-btn" @tap="handleScan">
|
||||
<text class="scan-icon">📷</text>
|
||||
<text class="scan-text">扫描访客二维码</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近核销记录 -->
|
||||
<view class="verify-history" v-if="recentVerifyList.length > 0">
|
||||
<view class="section-title">最近核销记录</view>
|
||||
<view class="history-list">
|
||||
<view v-for="(item, index) in recentVerifyList" :key="index" class="history-item">
|
||||
<view class="visitor-info">
|
||||
<text class="name">{{ item.name }}</text>
|
||||
<text class="time">{{ formatDateTime(item.verifyTime) }}</text>
|
||||
</view>
|
||||
<view class="visit-info">
|
||||
<text class="dept">{{ item.deptName }}</text>
|
||||
<text class="status success">已核销</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import visitorApi from '@/sheep/api/integrated/visitors'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const recentVerifyList = ref<any[]>([])
|
||||
|
||||
// 格式化时间
|
||||
const formatDateTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
// 处理扫码
|
||||
const handleScan = () => {
|
||||
uni.scanCode({
|
||||
scanType: ['qrCode'],
|
||||
success: async (res) => {
|
||||
try {
|
||||
// 假设扫描结果就是token
|
||||
const token = res.result
|
||||
const { code, msg } = await visitorApi.verifyVisitor({ token })
|
||||
|
||||
if (code === 0) {
|
||||
uni.showToast({
|
||||
title: '核销成功',
|
||||
icon: 'success'
|
||||
})
|
||||
// 刷新最近核销记录
|
||||
// getRecentVerifyList()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: msg || '核销失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '核销失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.verify-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
.back-btn {
|
||||
width: 60rpx;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 38rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.right-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-section {
|
||||
padding: 60rpx 30rpx;
|
||||
|
||||
.scan-btn {
|
||||
background: #fff;
|
||||
height: 200rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.scan-icon {
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.scan-text {
|
||||
font-size: 32rpx;
|
||||
color: #323233;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verify-history {
|
||||
padding: 0 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.visitor-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.visit-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.dept {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 26rpx;
|
||||
|
||||
&.success {
|
||||
color: #07C160;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
</view>
|
||||
<view class="title">详情</view>
|
||||
<view class="right-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 访客卡片 -->
|
||||
<view class="visit-card">
|
||||
<!-- 公司Logo -->
|
||||
<view class="company-logo">
|
||||
<image src="/static/images/company-logo.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 访问信息 -->
|
||||
<view class="visit-info">
|
||||
<view class="visit-time">{{ formatDate(detail.vstartDate) }} {{ formatTime(detail.vstartDate) }}</view>
|
||||
<view class="visit-location">{{ detail.deptName }}</view>
|
||||
<view class="visit-address"></view>
|
||||
</view>
|
||||
|
||||
<!-- 访客信息 -->
|
||||
<view class="visitor-info">
|
||||
<view class="info-row">
|
||||
<text class="label">邀请人</text>
|
||||
<text class="value">{{ detail.name }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">访客</text>
|
||||
<text class="value">{{ detail.userName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<view class="qr-code">
|
||||
<canvas type="2d" id="qrcode" canvas-id="qrcode" class="qr-canvas"></canvas>
|
||||
<text class="qr-tip">请于门卫或前台进行预访登记</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展开更多按钮 -->
|
||||
<view class="expand-btn" @tap="toggleExpand">
|
||||
<text>{{ isExpanded ? '收起' : '查看详细信息' }}</text>
|
||||
<text :class="['expand-icon', { 'expanded': isExpanded }]">{{ isExpanded ? '∧' : '∨' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<view class="detail-content" v-show="isExpanded">
|
||||
<!-- 原有的详细信息内容 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
<view class="section-item">
|
||||
<text class="label">拜访时间</text>
|
||||
<text class="value">{{ formatDateTime(detail.vstartDate) }} - {{ formatDateTime(detail.vendDate, true) }}</text>
|
||||
</view>
|
||||
<view class="section-item">
|
||||
<text class="label">访客姓名</text>
|
||||
<text class="value">{{ detail.name }}</text>
|
||||
</view>
|
||||
<view class="section-item">
|
||||
<text class="label">联系方式</text>
|
||||
<text class="value">{{ detail.mobile }}</text>
|
||||
</view>
|
||||
<view class="section-item">
|
||||
<text class="label">所属公司</text>
|
||||
<text class="value">{{ detail.companyName }}</text>
|
||||
</view>
|
||||
<view class="section-item">
|
||||
<text class="label">拜访对象</text>
|
||||
<text class="value">{{ detail.userName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 随行人员信息 -->
|
||||
<view class="detail-section" v-if="detail.isOtherPeople === '1' && detail.visitorsPeopleAppVos?.length">
|
||||
<view class="section-title">随行人员信息</view>
|
||||
<view v-for="(person, index) in detail.visitorsPeopleAppVos" :key="index" class="companion-item">
|
||||
<view class="section-item">
|
||||
<text class="label">访客姓名</text>
|
||||
<text class="value">{{ person.name }}</text>
|
||||
</view>
|
||||
<view class="section-item">
|
||||
<text class="label">手机号码</text>
|
||||
<text class="value">{{ person.mobile }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 车辆信息 -->
|
||||
<view class="detail-section" v-if="detail.isDrive === '1' && detail.visitorsCarAppVos?.length">
|
||||
<view class="section-title">车辆信息</view>
|
||||
<view v-for="(car, index) in detail.visitorsCarAppVos" :key="index" class="section-item">
|
||||
<text class="label">车牌号</text>
|
||||
<text class="value">{{ car.carNumber }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审核信息 -->
|
||||
<view class="detail-section" v-if="detail.checkStatus === '1' && detail.checkReason">
|
||||
<view class="section-title">审核信息</view>
|
||||
<view class="section-item">
|
||||
<text class="label">审核意见</text>
|
||||
<text class="value">{{ detail.checkReason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交信息 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-item">
|
||||
<text class="label">提交时间</text>
|
||||
<text class="value">{{ formatDateTime(detail.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import visitorApi from '@/sheep/api/integrated/visitors'
|
||||
import dayjs from 'dayjs'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const detail = ref<any>({})
|
||||
const isExpanded = ref(false)
|
||||
|
||||
// 格式化日期(年月日)
|
||||
const formatDate = (time: string) => {
|
||||
if (!time) return ''
|
||||
const weekMap: { [key: string]: string } = {
|
||||
'Mon': '一',
|
||||
'Tue': '二',
|
||||
'Wed': '三',
|
||||
'Thu': '四',
|
||||
'Fri': '五',
|
||||
'Sat': '六',
|
||||
'Sun': '日'
|
||||
}
|
||||
const date = dayjs(time)
|
||||
const weekEn = date.format('ddd')
|
||||
const weekCn = weekMap[weekEn] || weekEn
|
||||
return `${date.format('M月D日')} 周${weekCn}`
|
||||
}
|
||||
|
||||
// 格式化时间(时分)
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
return dayjs(time).format('HH:mm')
|
||||
}
|
||||
|
||||
// 格式化完整时间
|
||||
const formatDateTime = (time: string, onlyTime = false) => {
|
||||
if (!time) return ''
|
||||
return onlyTime ? dayjs(time).format('HH:mm') : dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
const generateQRCode = async (token: string) => {
|
||||
try {
|
||||
// #ifdef H5
|
||||
const base64 = await QRCode.toDataURL(token, {
|
||||
width: 200,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff'
|
||||
}
|
||||
})
|
||||
if (base64) {
|
||||
const img = new Image()
|
||||
img.src = base64
|
||||
img.style.width = '400rpx'
|
||||
img.style.height = '400rpx'
|
||||
const container = document.querySelector('.qr-code')
|
||||
const canvas = document.querySelector('#qrcode')
|
||||
if (container && canvas) {
|
||||
const div = document.createElement('div')
|
||||
div.style.width = '400rpx'
|
||||
div.style.height = '400rpx'
|
||||
div.style.marginBottom = '20rpx'
|
||||
div.appendChild(img)
|
||||
container.replaceChild(div, canvas)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const query = uni.createSelectorQuery()
|
||||
query
|
||||
.select('#qrcode')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (res[0] && res[0].node) {
|
||||
const canvas = res[0].node
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 设置canvas大小
|
||||
const dpr = uni.getSystemInfoSync().pixelRatio
|
||||
canvas.width = 200 * dpr
|
||||
canvas.height = 200 * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
// 生成二维码
|
||||
QRCode.toCanvas(canvas, token, {
|
||||
width: 200,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 展开/收起详情
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
// 获取详情数据
|
||||
const getDetail = async () => {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const { token } = currentPage.$page?.options || {}
|
||||
|
||||
if (!token) {
|
||||
uni.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { code, data } = await visitorApi.getVisitorDetail({ token })
|
||||
if (code === 0 && data) {
|
||||
detail.value = data
|
||||
// 延迟生成二维码,确保canvas已经渲染
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
generateQRCode(token)
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
setTimeout(() => {
|
||||
generateQRCode(token)
|
||||
}, 300)
|
||||
// #endif
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取详情失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
.back-btn {
|
||||
width: 60rpx;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 38rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.right-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.visit-card {
|
||||
margin: 30rpx;
|
||||
padding: 40rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.company-logo {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin: 0 auto 30rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.visit-info {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.visit-time {
|
||||
font-size: 36rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.visit-location {
|
||||
font-size: 32rpx;
|
||||
color: #323233;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.visit-address {
|
||||
font-size: 28rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.visitor-info {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 40rpx 0;
|
||||
|
||||
.qr-canvas {
|
||||
width: 400rpx !important;
|
||||
height: 400rpx !important;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.qr-tip {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx;
|
||||
margin: 0 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1989FA;
|
||||
|
||||
.expand-icon {
|
||||
margin-left: 8rpx;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 20rpx;
|
||||
border-left: 4rpx solid #1989FA;
|
||||
}
|
||||
}
|
||||
|
||||
.section-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.companion-item {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user