访客页面

This commit is contained in:
zjf committed 2025-04-17 15:40:50 +08:00
1 parent f1e4ba7d31
commit f8b5e3c96b
2 files changed
+712

No files matched your search

+226
View File
@@ -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>