<template>
|
<AdminShell :show-bottom-nav="false">
|
<view class="detail-card">
|
<view class="detail-head">
|
<view class="head-left" @click="goBack">
|
<view class="back-btn">
|
<image :src="backIcon" class="back-icon" mode="aspectFit"></image>
|
</view>
|
<text class="page-title">访客登记详情</text>
|
</view>
|
|
<view class="status-pill" :class="detail.statusClass">
|
<image :src="currentStatusIcon" class="status-pill-icon" mode="aspectFit"></image>
|
<text class="status-pill-text" :class="detail.statusClass">{{ detail.status }}</text>
|
</view>
|
</view>
|
|
<view class="section-card">
|
<view class="section-head">
|
<image :src="visitorInfoIcon" class="section-icon" mode="aspectFit"></image>
|
<text class="section-title">访客信息</text>
|
</view>
|
|
<view class="visitor-layout">
|
<view class="capture-col">
|
<image :src="detail.faceImageUrl || visitorCapturePlaceholder" class="capture-image" mode="widthFix"></image>
|
<text class="capture-time">登记采集{{ '\n' }}{{ detail.registerCaptureTime }} 录入</text>
|
</view>
|
|
<view class="field-col">
|
<view class="detail-field" v-for="item in visitorLeftFields" :key="item.label">
|
<image :src="item.icon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">{{ item.label }}</text>
|
<text class="field-value">{{ item.value }}</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="field-col field-col-right">
|
<view class="detail-field" v-for="item in visitorRightFields" :key="item.label">
|
<image :src="item.icon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">{{ item.label }}</text>
|
<text class="field-value">{{ item.value }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="section-card">
|
<view class="section-head">
|
<image :src="hostInfoIcon" class="section-icon" mode="aspectFit"></image>
|
<text class="section-title">被访者</text>
|
</view>
|
|
<view class="host-layout">
|
<view class="host-item">
|
<image :src="hostPersonIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">被访人</text>
|
<text class="field-value">{{ detail.host }}</text>
|
</view>
|
</view>
|
|
<view class="host-item">
|
<image :src="hostReceiverIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">登记接待人</text>
|
<text class="field-value">{{ detail.receiver }}</text>
|
</view>
|
</view>
|
|
<view class="host-item host-method-item">
|
<image :src="hostMethodIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label host-method-label">接待人验证方式</text>
|
<view class="method-badge" :class="{ 'method-badge-warn': isCardVerifyMethod(detail.hostVerify) }">
|
<text class="method-badge-text" :class="{ 'method-badge-text-warn': isCardVerifyMethod(detail.hostVerify) }">{{ detail.hostVerify }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="section-card">
|
<view class="section-head">
|
<image :src="permissionIcon" class="section-icon" mode="aspectFit"></image>
|
<text class="section-title">访问权限</text>
|
</view>
|
|
<view class="permission-layout">
|
<view class="permission-title-row">
|
<image :src="areaIcon" class="field-icon" mode="aspectFit"></image>
|
<text class="field-label">可访问区域</text>
|
</view>
|
|
<view class="tag-row">
|
<view class="tag-chip" v-for="tag in detail.areaTags" :key="tag">
|
<image :src="tagCheckIcon" class="tag-check-icon" mode="aspectFit"></image>
|
<text class="tag-text">{{ tag }}</text>
|
</view>
|
</view>
|
|
<view class="permission-title-row">
|
<image :src="validityIcon" class="field-icon" mode="aspectFit"></image>
|
<text class="field-label">访问有效期</text>
|
</view>
|
|
<view class="validity-box">
|
<text class="validity-text">{{ detail.validityText }}</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="section-card">
|
<view class="section-head">
|
<image :src="signoutIcon" class="section-icon" mode="aspectFit"></image>
|
<text class="section-title">签离记录</text>
|
</view>
|
|
<view class="visitor-layout">
|
<view class="capture-col">
|
<image :src="detail.signoutImageUrl || signoutCapturePlaceholder" class="capture-image" mode="widthFix"></image>
|
<text class="capture-time">签离采集{{ '\n' }}{{ detail.signoutCaptureTime }} 录入</text>
|
</view>
|
|
<view class="signout-fields">
|
<view class="detail-field">
|
<image :src="signoutTimeIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">签离时间</text>
|
<text class="field-value">{{ detail.signoutTime }}</text>
|
</view>
|
</view>
|
|
<view class="detail-field">
|
<image :src="signoutReceiverIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">签离接待人</text>
|
<text class="field-value">{{ detail.signoutReceiver }}</text>
|
</view>
|
</view>
|
|
<view class="detail-field">
|
<image :src="signoutMethodIcon" class="field-icon" mode="aspectFit"></image>
|
<view class="field-copy">
|
<text class="field-label">接待人验证方式</text>
|
<view class="method-badge" :class="{ 'method-badge-warn': isCardVerifyMethod(detail.signoutVerify) }">
|
<text class="method-badge-text" :class="{ 'method-badge-text-warn': isCardVerifyMethod(detail.signoutVerify) }">{{ detail.signoutVerify }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</AdminShell>
|
</template>
|
|
<script setup>
|
import { computed, ref } from 'vue';
|
import { onLoad } from '@dcloudio/uni-app';
|
import { getVisitorRecordDetail } from '../../api/visitorRecord';
|
import { getHistoryRecordByRecordId } from '../../utils/db';
|
import AdminShell from '../../components/admin/AdminShell.vue';
|
import backIcon from '../../static/admin-detail/Ay837.png';
|
import visitorInfoIcon from '../../static/admin-detail/e7v1T.png';
|
import hostInfoIcon from '../../static/admin-detail/VkMZk.png';
|
import permissionIcon from '../../static/admin-detail/lvVVM.png';
|
import signoutIcon from '../../static/admin-detail/y2Wztk.png';
|
import nameIcon from '../../static/admin-detail/e1XWUs.png';
|
import registerTypeIcon from '../../static/admin-detail/bTKar.png';
|
import idCardIcon from '../../static/admin-detail/IiGss.png';
|
import recordNoIcon from '../../static/admin-detail/dDvxL.png';
|
import registerTimeIcon from '../../static/admin-detail/SRQAF.png';
|
import plateIcon from '../../static/admin-detail/y5iYAG.png';
|
import hostPersonIcon from '../../static/admin-detail/DT2hY.png';
|
import hostReceiverIcon from '../../static/admin-detail/X8vSXE.png';
|
import hostMethodIcon from '../../static/admin-detail/a6rRE.png';
|
import areaIcon from '../../static/admin-detail/e9zsTA.png';
|
import tagCheckIcon from '../../static/admin-detail/gDoHZ.png';
|
import validityIcon from '../../static/admin-detail/bZ9UH.png';
|
import signoutTimeIcon from '../../static/admin-detail/c1DOt.png';
|
import signoutReceiverIcon from '../../static/admin-detail/KpM7Y.png';
|
import signoutMethodIcon from '../../static/admin-detail/X8xBEc.png';
|
import visitorCapturePlaceholder from '../../static/admin-detail/aWmHf.png';
|
import signoutCapturePlaceholder from '../../static/admin-detail/jAROZ.png';
|
import statusGreenIcon from '../../static/admin-pages/HXHf5.png';
|
import statusBlueIcon from '../../static/admin-pages/O1Ob8a.png';
|
import statusOrangeIcon from '../../static/admin-pages/D1nht.png';
|
import statusPurpleIcon from '../../static/admin-pages/NUszJ.png';
|
|
const id = ref('');
|
const DETAIL_SNAPSHOT_STORAGE_PREFIX = 'admin_record_detail_snapshot_';
|
const createDefaultDetail = () => ({
|
name: '-',
|
status: '-',
|
statusClass: 'stay',
|
type: '-',
|
idCard: '-',
|
recordNo: '-',
|
registerTime: '-',
|
registerCaptureTime: '-',
|
plateNumber: '-',
|
host: '-',
|
receiver: '-',
|
hostVerify: '-',
|
areaTags: [],
|
validityText: '-',
|
signoutCaptureTime: '-',
|
signoutTime: '-',
|
signoutReceiver: '-',
|
signoutVerify: '-',
|
faceImageUrl: '',
|
signoutImageUrl: '',
|
});
|
|
const detail = ref(createDefaultDetail());
|
|
const getStatusInfo = (statusStr) => {
|
switch (statusStr) {
|
case 'VISITING':
|
return { text: '来访中', class: 'stay' };
|
case 'SIGNED_OUT':
|
return { text: '已签离', class: 'done' };
|
case 'PENDING_CONFIRM':
|
case 'OVERDUE':
|
return { text: '逾期滞留', class: 'warn' };
|
case 'OVERDUE_AUTO_SIGNED_OUT':
|
case 'AUTO_SIGNED_OUT':
|
return { text: '超期自动', class: 'idle' };
|
case 'CANCELLED':
|
return { text: '已取消', class: 'warn' };
|
default:
|
return { text: statusStr || '未知', class: 'stay' };
|
}
|
};
|
|
const getRegisterTypeInfo = (typeStr) => {
|
if (typeStr === 'APPOINTMENT') return '预约登记';
|
if (typeStr === 'ONSITE') return '现场登记';
|
return typeStr || '未知';
|
};
|
|
const isCardVerifyMethod = (verifyMethod = '') => {
|
return String(verifyMethod || '').includes('刷卡');
|
};
|
|
const normalizeDetailData = (data = {}) => {
|
const statusInfo = getStatusInfo(data.status);
|
const areaTags = Array.isArray(data.visitAreaNameList)
|
? data.visitAreaNameList
|
: ((data.visitAreaNames || '').split(',').map(tag => tag.trim()).filter(Boolean));
|
|
return {
|
name: data.visitorName || data.name || '-',
|
status: data.statusName || data.status || statusInfo.text,
|
statusClass: data.statusClass || statusInfo.class,
|
type: data.registerTypeName || data.type || getRegisterTypeInfo(data.registerType),
|
idCard: data.idCardMask || data.idCard || '-',
|
recordNo: data.registerNo || data.recordNo || data.recordId || '-',
|
registerTime: data.registerTime || '-',
|
registerCaptureTime: data.registerCollectTime || data.registerCaptureTime || data.registerTime || '-',
|
plateNumber: data.plateNo || data.plateNumber || '-',
|
host: data.intervieweeName || data.host || '-',
|
receiver: data.receiverName || data.signReceiverName || data.receiver || data.intervieweeName || '-',
|
hostVerify: data.receiverVerifyMethodName || data.receiverVerifyMethod || data.hostVerify || '-',
|
areaTags,
|
validityText: data.validTimeText || data.validityText || (data.validStartTime && data.validEndTime ? `${data.validStartTime} 至 ${data.validEndTime}` : '-'),
|
signoutCaptureTime: data.signCollectTime || data.signoutCaptureTime || data.signTime || '-',
|
signoutTime: data.signTime || data.signoutTime || '-',
|
signoutReceiver: data.signReceiverName || data.signoutReceiver || '-',
|
signoutVerify: data.signVerifyMethodName || data.signVerifyMethod || data.signoutVerify || '-',
|
faceImageUrl: data.faceImageUrl || '',
|
signoutImageUrl: data.signImageUrl || data.signoutImageUrl || '',
|
};
|
};
|
|
const loadDetail = async (recordId) => {
|
uni.showLoading({ title: '加载中...' });
|
try {
|
const res = await getVisitorRecordDetail({ recordId });
|
if (res && res.code === 200 && res.data) {
|
detail.value = normalizeDetailData(res.data);
|
} else {
|
throw new Error(res?.msg || '获取详情失败');
|
}
|
} catch (err) {
|
console.error('获取在线详情失败:', err);
|
uni.showToast({ title: '网络异常,获取详情失败', icon: 'none' });
|
} finally {
|
uni.hideLoading();
|
}
|
};
|
|
onLoad((options) => {
|
id.value = options?.id || '';
|
if (id.value) {
|
loadDetail(id.value);
|
}
|
});
|
|
const currentStatusIcon = computed(() => {
|
const iconMap = {
|
stay: statusGreenIcon,
|
done: statusBlueIcon,
|
warn: statusOrangeIcon,
|
idle: statusPurpleIcon,
|
};
|
|
return iconMap[detail.value.statusClass] || statusGreenIcon;
|
});
|
|
const visitorLeftFields = computed(() => [
|
{ label: '姓名', value: detail.value.name, icon: nameIcon },
|
{ label: '登记方式', value: detail.value.type, icon: registerTypeIcon },
|
{ label: '证件号码', value: detail.value.idCard, icon: idCardIcon },
|
]);
|
|
const visitorRightFields = computed(() => [
|
{ label: '登记编号', value: detail.value.recordNo, icon: recordNoIcon },
|
{ label: '登记时间', value: detail.value.registerTime, icon: registerTimeIcon },
|
{ label: '车牌号', value: detail.value.plateNumber, icon: plateIcon },
|
]);
|
|
const goBack = () => {
|
uni.navigateBack({
|
delta: 1,
|
fail: () => {
|
uni.reLaunch({
|
url: '/pages/admin-records/index',
|
});
|
},
|
});
|
};
|
</script>
|
|
<style scoped>
|
.detail-card {
|
width: 100%;
|
flex: 1;
|
min-height: 1738px;
|
border-radius: 38px;
|
border: 1px solid #E4ECF8;
|
background: #FFFFFF;
|
box-shadow: 0 16px 36px rgba(11, 23, 48, 0.07);
|
padding: 26px 24px 30px;
|
box-sizing: border-box;
|
display: flex;
|
flex-direction: column;
|
gap: 18px;
|
}
|
|
.detail-head {
|
height: 68px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.head-left {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
}
|
|
.back-btn {
|
width: 44px;
|
height: 44px;
|
border-radius: 22px;
|
border: 1px solid #E1EAF8;
|
background: #F5F8FD;
|
box-shadow: 0 6px 14px rgba(11, 23, 48, 0.03);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.back-icon {
|
width: 20px;
|
height: 20px;
|
}
|
|
.page-title {
|
font-family: Inter, sans-serif;
|
font-size: 26px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #1F2A44;
|
}
|
|
.status-pill {
|
min-width: 108px;
|
height: 36px;
|
padding: 0 14px;
|
box-sizing: border-box;
|
border-radius: 18px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 8px;
|
border: 1px solid transparent;
|
}
|
|
.status-pill.stay {
|
background: #EAF8F0;
|
border-color: #CDECD8;
|
}
|
|
.status-pill.done {
|
background: rgba(47, 107, 255, 0.12);
|
border-color: rgba(47, 107, 255, 0.22);
|
}
|
|
.status-pill.warn {
|
background: rgba(255, 138, 61, 0.12);
|
border-color: rgba(255, 138, 61, 0.22);
|
}
|
|
.status-pill.idle {
|
background: rgba(110, 106, 248, 0.12);
|
border-color: rgba(110, 106, 248, 0.22);
|
}
|
|
.status-pill-icon {
|
width: 16px;
|
height: 16px;
|
}
|
|
.status-pill-text {
|
font-family: Inter, sans-serif;
|
font-size: 14px;
|
font-weight: 700;
|
line-height: 1.2;
|
}
|
|
.status-pill-text.stay {
|
color: #2BB673;
|
}
|
|
.status-pill-text.done {
|
color: #2F6BFF;
|
}
|
|
.status-pill-text.warn {
|
color: #FF8A3D;
|
}
|
|
.status-pill-text.idle {
|
color: #6E6AF8;
|
}
|
|
.section-card {
|
border-radius: 24px;
|
border: 1px solid #E8EEF8;
|
background: linear-gradient(180deg, #F8FBFF 0%, #F4F8FD 100%);
|
padding: 22px;
|
box-sizing: border-box;
|
display: flex;
|
flex-direction: column;
|
gap: 18px;
|
}
|
|
.section-head {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.section-icon {
|
width: 18px;
|
height: 18px;
|
}
|
|
.section-title {
|
font-family: Inter, sans-serif;
|
font-size: 20px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #1F2A44;
|
}
|
|
.visitor-layout {
|
display: flex;
|
gap: 36px;
|
}
|
|
.capture-col {
|
width: 140px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.capture-image {
|
width: 110px;
|
flex-shrink: 0;
|
}
|
|
.capture-time {
|
font-family: Inter, sans-serif;
|
font-size: 12px;
|
font-weight: 400;
|
line-height: 1.3;
|
text-align: center;
|
color: #7B88A8;
|
white-space: pre-line;
|
}
|
|
.field-col,
|
.signout-fields {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 14px;
|
}
|
|
.field-col-right {
|
max-width: 320px;
|
}
|
|
.detail-field,
|
.host-item,
|
.permission-title-row {
|
display: flex;
|
/* align-items: center; */
|
gap: 10px;
|
}
|
|
.field-icon {
|
width: 16px;
|
height: 16px;
|
flex-shrink: 0;
|
}
|
|
.field-copy {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.field-label {
|
font-family: Inter, sans-serif;
|
font-size: 12px;
|
font-weight: 600;
|
line-height: 1.2;
|
color: #7B88A8;
|
}
|
|
.field-value,
|
.validity-text {
|
font-family: Inter, sans-serif;
|
font-size: 18px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #1F2A44;
|
}
|
|
.host-layout {
|
display: flex;
|
gap: 28px;
|
}
|
|
.host-item {
|
flex: 1;
|
}
|
|
.host-method-item {
|
align-items: flex-start;
|
}
|
|
.host-method-label {
|
font-size: 11px;
|
}
|
|
.method-badge {
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 16px;
|
border: 1px solid #CDECD8;
|
background: #EAF8F0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-sizing: border-box;
|
}
|
|
.method-badge-warn {
|
border-color: transparent;
|
background: #FFF2E7;
|
}
|
|
.method-badge-text {
|
font-family: Inter, sans-serif;
|
font-size: 11px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #2BB673;
|
}
|
|
.method-badge-text-warn {
|
font-size: 12px;
|
color: #FF8A3D;
|
}
|
|
.permission-layout {
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
}
|
|
.tag-row {
|
display: flex;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
|
.tag-chip {
|
height: 38px;
|
padding: 0 14px;
|
border-radius: 18px;
|
border: 1px solid #D8E5FF;
|
background: #EEF5FF;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 8px;
|
box-sizing: border-box;
|
}
|
|
.tag-check-icon {
|
width: 16px;
|
height: 16px;
|
flex-shrink: 0;
|
}
|
|
.tag-text {
|
font-family: Inter, sans-serif;
|
font-size: 14px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #2F6BFF;
|
}
|
|
.validity-box {
|
width: 100%;
|
border-radius: 18px;
|
border: 1px solid #E8EEF8;
|
background: linear-gradient(180deg, #F9FBFE 0%, #F3F7FC 100%);
|
padding: 12px 16px;
|
box-sizing: border-box;
|
}
|
</style>
|