| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import QRCode from 'qrcode' |
| | | import { fetchMyApprovalTodos, fetchMyVisitorCodes, fetchVisitRecords } from '@/api/appointment' |
| | | import { useMiniAuthStore } from '@/store/auth' |
| | | import type { MiniMyVisitorCodeOrderItem, MiniMyVisitorCodeTicketItem } from '@/types/appointment' |
| | | |
| | | const { safeAreaInsets } = uni.getSystemInfoSync() |
| | | interface VisitorCodeDisplayTicket extends MiniMyVisitorCodeTicketItem { |
| | | qrCode: string |
| | | } |
| | | |
| | | // 弹窗显示状态 |
| | | const showLoginPopup = ref(false) |
| | | interface VisitorCodeDisplayOrder extends Omit<MiniMyVisitorCodeOrderItem, 'qrCodes'> { |
| | | timeText: string |
| | | qrCodes: VisitorCodeDisplayTicket[] |
| | | } |
| | | |
| | | // 登录状态 |
| | | const isLoggedIn = ref(true) |
| | | |
| | | // 用户角色: 'employee' 员工 | 'visitor' 普通访客 |
| | | const userRole = ref<'employee' | 'visitor'>('visitor') |
| | | |
| | | // 访客码数据 |
| | | const visitorList = ref([ |
| | | { |
| | | id: 1, |
| | | name: '王敏', |
| | | department: '管理部', |
| | | area: 'AAAAA', |
| | | time: '2023-10-10 11:30至2023-10-15 18:30', |
| | | qrCode: '/static/home/成都喜望科技有限公司.png', |
| | | visitorName: '王琦', |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '李华', |
| | | department: '技术部', |
| | | area: 'BBBBB', |
| | | time: '2023-11-01 09:00至2023-11-01 18:00', |
| | | qrCode: '/static/home/成都喜望科技有限公司.png', |
| | | visitorName: '张三', |
| | | }, |
| | | ]) |
| | | const currentVisitorIndex = ref(0) |
| | | const systemInfo = uni.getSystemInfoSync() |
| | | const { safeAreaInsets } = systemInfo |
| | | const parkFlowBottomPx = Math.max(24, Math.round(systemInfo.windowHeight / 8)) |
| | | const router = useRouter() |
| | | const authStore = useMiniAuthStore() |
| | | const visitorOrders = ref<VisitorCodeDisplayOrder[]>([]) |
| | | const currentOrderIndex = ref(0) |
| | | const loadingVisitorCodes = ref(false) |
| | | const qrRenderToken = ref(0) |
| | | const ticketIndexMap = ref<Record<number, number>>({}) |
| | | const approvalTodoCount = ref(0) |
| | | const visitRecordCount = ref(0) |
| | | |
| | | // 切换访客码 |
| | | function handlePrevVisitor() { |
| | | if (currentVisitorIndex.value > 0) { |
| | | currentVisitorIndex.value-- |
| | | const profile = computed(() => authStore.profile) |
| | | const isEmployee = computed(() => profile.value?.userType === 'EMPLOYEE') |
| | | const userRoleText = computed(() => { |
| | | if (!profile.value) { |
| | | return '' |
| | | } |
| | | if (profile.value.userType === 'EMPLOYEE') { |
| | | return profile.value.deptLeaderFlag ? '部门领导' : '员工' |
| | | } |
| | | return '普通访客' |
| | | }) |
| | | const secondaryText = computed(() => { |
| | | if (!profile.value) { |
| | | return '' |
| | | } |
| | | if (profile.value.userType === 'EMPLOYEE') { |
| | | return profile.value.deptName || '未分配部门' |
| | | } |
| | | return profile.value.companyName || '' |
| | | }) |
| | | const currentOrder = computed(() => visitorOrders.value[currentOrderIndex.value] || null) |
| | | const currentTicket = computed(() => { |
| | | if (!currentOrder.value) { |
| | | return null |
| | | } |
| | | const index = ticketIndexMap.value[currentOrder.value.orderId] ?? 0 |
| | | return currentOrder.value.qrCodes[index] || currentOrder.value.qrCodes[0] || null |
| | | }) |
| | | |
| | | function handlePrevOrder() { |
| | | if (currentOrderIndex.value > 0) { |
| | | currentOrderIndex.value-- |
| | | } |
| | | } |
| | | |
| | | function handleNextVisitor() { |
| | | if (currentVisitorIndex.value < visitorList.value.length - 1) { |
| | | currentVisitorIndex.value++ |
| | | function handleNextOrder() { |
| | | if (currentOrderIndex.value < visitorOrders.value.length - 1) { |
| | | currentOrderIndex.value++ |
| | | } |
| | | } |
| | | |
| | | function onSwiperChange(e: any) { |
| | | currentVisitorIndex.value = e.detail.current |
| | | function onSwiperChange(e: { detail?: { current?: number } }) { |
| | | currentOrderIndex.value = e.detail?.current || 0 |
| | | } |
| | | |
| | | // 下载访客码 |
| | | function handleDownloadVisitorCode(qrCodeUrl: string) { |
| | | // 小程序环境中获取图片信息并保存到相册 |
| | | uni.showLoading({ title: '保存中...' }) |
| | | function currentTicketIndex(orderId: number) { |
| | | return ticketIndexMap.value[orderId] ?? 0 |
| | | } |
| | | |
| | | // 这里如果是本地路径如 /static/xxx,可以直接用。如果后续换成网络图,需要先 uni.downloadFile |
| | | // 为了演示,统一走 uni.getImageInfo 以校验文件存在并保存 |
| | | uni.getImageInfo({ |
| | | src: qrCodeUrl, |
| | | success: (image) => { |
| | | uni.saveImageToPhotosAlbum({ |
| | | filePath: image.path, |
| | | success: () => { |
| | | uni.hideLoading() |
| | | uni.showToast({ |
| | | title: '保存成功', |
| | | icon: 'success', |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | uni.hideLoading() |
| | | // 拒绝授权后会走 fail |
| | | if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') { |
| | | uni.showModal({ |
| | | title: '提示', |
| | | content: '需要您授权保存相册', |
| | | success(res) { |
| | | if (res.confirm) { |
| | | uni.openSetting({ |
| | | success(settingdata) { |
| | | if (settingdata.authSetting['scope.writePhotosAlbum']) { |
| | | uni.showToast({ title: '授权成功,请重新点击下载', icon: 'none' }) |
| | | } |
| | | else { |
| | | uni.showToast({ title: '授权失败,无法保存', icon: 'none' }) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | else { |
| | | uni.showToast({ |
| | | title: '保存失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | }, |
| | | function handlePrevTicket(orderId: number) { |
| | | const currentIndex = currentTicketIndex(orderId) |
| | | if (currentIndex <= 0) { |
| | | return |
| | | } |
| | | ticketIndexMap.value = { |
| | | ...ticketIndexMap.value, |
| | | [orderId]: currentIndex - 1, |
| | | } |
| | | } |
| | | |
| | | function handleNextTicket(orderId: number) { |
| | | const order = visitorOrders.value.find(item => item.orderId === orderId) |
| | | const currentIndex = currentTicketIndex(orderId) |
| | | if (!order || currentIndex >= order.qrCodes.length - 1) { |
| | | return |
| | | } |
| | | ticketIndexMap.value = { |
| | | ...ticketIndexMap.value, |
| | | [orderId]: currentIndex + 1, |
| | | } |
| | | } |
| | | |
| | | function formatVisitTime(order: MiniMyVisitorCodeOrderItem) { |
| | | return order.visitTimeText || '暂无有效期' |
| | | } |
| | | |
| | | function formatDateTime(value?: string | null) { |
| | | if (!value) { |
| | | return '' |
| | | } |
| | | return value.replace('T', ' ').slice(0, 16) |
| | | } |
| | | |
| | | function buildVisitorOrders(orders: MiniMyVisitorCodeOrderItem[]): VisitorCodeDisplayOrder[] { |
| | | return orders.map(order => ({ |
| | | ...order, |
| | | timeText: formatVisitTime(order), |
| | | qrCodes: order.qrCodes.map((ticket: MiniMyVisitorCodeTicketItem) => ({ |
| | | ...ticket, |
| | | qrCode: '', |
| | | })), |
| | | })) |
| | | } |
| | | |
| | | async function loadVisitorCodes() { |
| | | loadingVisitorCodes.value = true |
| | | const currentToken = qrRenderToken.value + 1 |
| | | qrRenderToken.value = currentToken |
| | | try { |
| | | const orders = await fetchMyVisitorCodes() |
| | | const displayOrders = buildVisitorOrders(orders) |
| | | visitorOrders.value = displayOrders |
| | | currentOrderIndex.value = 0 |
| | | ticketIndexMap.value = Object.fromEntries(displayOrders.map(item => [item.orderId, 0])) |
| | | await nextTick() |
| | | const hydratedOrders = await Promise.all(displayOrders.map(async order => ({ |
| | | ...order, |
| | | qrCodes: await Promise.all(order.qrCodes.map(async ticket => ({ |
| | | ...ticket, |
| | | qrCode: await createQrImage(ticket.payload), |
| | | }))), |
| | | }))) |
| | | if (qrRenderToken.value === currentToken) { |
| | | visitorOrders.value = hydratedOrders |
| | | } |
| | | } |
| | | catch (error) { |
| | | visitorOrders.value = [] |
| | | ticketIndexMap.value = {} |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载访客码失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | if (qrRenderToken.value === currentToken) { |
| | | loadingVisitorCodes.value = false |
| | | } |
| | | } |
| | | } |
| | | |
| | | async function loadEmployeeDashboardCounts() { |
| | | if (!isEmployee.value) { |
| | | approvalTodoCount.value = 0 |
| | | visitRecordCount.value = 0 |
| | | return |
| | | } |
| | | try { |
| | | const [todoResult, visitRecordResult] = await Promise.all([ |
| | | fetchMyApprovalTodos({ page: 1, size: 1 }), |
| | | fetchVisitRecords({ page: 1, size: 1, futureOnly: true }), |
| | | ]) |
| | | approvalTodoCount.value = todoResult.total |
| | | visitRecordCount.value = visitRecordResult.total |
| | | } |
| | | catch (error) { |
| | | approvalTodoCount.value = 0 |
| | | visitRecordCount.value = 0 |
| | | console.warn('load employee dashboard counts failed', error) |
| | | } |
| | | } |
| | | |
| | | async function createQrImage(payload: string) { |
| | | // #ifdef H5 |
| | | return QRCode.toDataURL(payload, { |
| | | errorCorrectionLevel: 'M', |
| | | margin: 1, |
| | | width: 300, |
| | | }) |
| | | // #endif |
| | | |
| | | const qr = QRCode.create(payload, { |
| | | errorCorrectionLevel: 'M', |
| | | margin: 1, |
| | | }) |
| | | const size = 300 |
| | | const moduleCount = qr.modules.size |
| | | const tileSize = size / moduleCount |
| | | const canvasId = 'visitorQrCanvas' |
| | | |
| | | return new Promise<string>((resolve, reject) => { |
| | | const context = uni.createCanvasContext(canvasId) |
| | | context.setFillStyle('#FFFFFF') |
| | | context.fillRect(0, 0, size, size) |
| | | context.setFillStyle('#000000') |
| | | |
| | | for (let row = 0; row < moduleCount; row += 1) { |
| | | for (let col = 0; col < moduleCount; col += 1) { |
| | | const index = row * moduleCount + col |
| | | if (!qr.modules.data[index]) { |
| | | continue |
| | | } |
| | | const x = Math.round(col * tileSize) |
| | | const y = Math.round(row * tileSize) |
| | | const width = Math.ceil((col + 1) * tileSize) - x |
| | | const height = Math.ceil((row + 1) * tileSize) - y |
| | | context.fillRect(x, y, width, height) |
| | | } |
| | | } |
| | | |
| | | context.draw(false, () => { |
| | | uni.canvasToTempFilePath({ |
| | | canvasId, |
| | | width: size, |
| | | height: size, |
| | | destWidth: size * 2, |
| | | destHeight: size * 2, |
| | | success: res => resolve(res.tempFilePath), |
| | | fail: reject, |
| | | }) |
| | | }, |
| | | fail: () => { |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | function handleDownloadVisitorCode(qrCodeUrl: string) { |
| | | if (!qrCodeUrl) { |
| | | uni.showToast({ |
| | | title: '访客码生成中', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | |
| | | uni.showLoading({ title: '保存中...' }) |
| | | void resolveSaveFilePath(qrCodeUrl) |
| | | .then(filePath => saveImageToAlbum(filePath)) |
| | | .catch((error) => { |
| | | uni.hideLoading() |
| | | uni.showToast({ |
| | | title: '获取图片失败', |
| | | title: error instanceof Error ? error.message : '保存失败', |
| | | icon: 'none', |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | function resolveSaveFilePath(src: string) { |
| | | return new Promise<string>((resolve, reject) => { |
| | | if (!src) { |
| | | reject(new Error('访客码生成中')) |
| | | return |
| | | } |
| | | |
| | | if (src.startsWith('http://') || src.startsWith('https://')) { |
| | | uni.downloadFile({ |
| | | url: src, |
| | | success: (res) => { |
| | | if (res.statusCode === 200 && res.tempFilePath) { |
| | | resolve(res.tempFilePath) |
| | | return |
| | | } |
| | | reject(new Error('下载图片失败')) |
| | | }, |
| | | fail: () => reject(new Error('下载图片失败')), |
| | | }) |
| | | return |
| | | } |
| | | |
| | | if (src.startsWith('wxfile://') || src.startsWith('file://') || src.startsWith('/')) { |
| | | resolve(src) |
| | | return |
| | | } |
| | | |
| | | uni.getImageInfo({ |
| | | src, |
| | | success: image => resolve(image.path), |
| | | fail: () => reject(new Error('获取图片失败')), |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | function saveImageToAlbum(filePath: string) { |
| | | uni.saveImageToPhotosAlbum({ |
| | | filePath, |
| | | success: () => { |
| | | uni.hideLoading() |
| | | uni.showToast({ |
| | | title: '保存成功', |
| | | icon: 'success', |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | uni.hideLoading() |
| | | const errMsg = err?.errMsg || '' |
| | | if (errMsg.includes('auth deny') || errMsg.includes('authorize no response')) { |
| | | uni.showModal({ |
| | | title: '提示', |
| | | content: '需要您授权保存相册', |
| | | success(res) { |
| | | if (res.confirm) { |
| | | uni.openSetting({ |
| | | success(settingdata) { |
| | | if (settingdata.authSetting['scope.writePhotosAlbum']) { |
| | | uni.showToast({ title: '授权成功,请重新点击下载', icon: 'none' }) |
| | | } |
| | | else { |
| | | uni.showToast({ title: '授权失败,无法保存', icon: 'none' }) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | }, |
| | | }) |
| | | return |
| | | } |
| | | uni.showToast({ |
| | | title: '保存失败', |
| | | icon: 'none', |
| | | }) |
| | | console.error('save visitor code failed', err) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 触发弹窗显示 |
| | | function handleShowLoginPopup() { |
| | | showLoginPopup.value = true |
| | | function handleOpenParkFlow() { |
| | | uni.navigateTo({ |
| | | url: `/pages/aboutUs/index?title=${encodeURIComponent('入园流程')}&code=CONTENT_PARK_FLOW_HTML`, |
| | | }) |
| | | } |
| | | |
| | | // 取消弹窗 |
| | | function handleCancelLogin() { |
| | | showLoginPopup.value = false |
| | | function handleOpenApprovalTodo() { |
| | | uni.navigateTo({ url: '/pages/reviewList/index' }) |
| | | } |
| | | |
| | | // 确认登录(获取手机号回调) |
| | | function handleConfirmLogin(e: any) { |
| | | showLoginPopup.value = false |
| | | if (e.detail.errMsg === 'getPhoneNumber:ok') { |
| | | // 用户同意授权,这里可以拿到 e.detail.code,发送给后端换取手机号 |
| | | console.log('获取手机号成功:', e.detail) |
| | | isLoggedIn.value = true // 模拟登录成功状态 |
| | | uni.showToast({ |
| | | title: '登录成功', |
| | | icon: 'success', |
| | | }) |
| | | } |
| | | else { |
| | | // 用户拒绝授权 |
| | | console.log('用户拒绝授权手机号:', e.detail.errMsg) |
| | | uni.showToast({ |
| | | title: '已取消登录', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | function handleOpenVisitRecords() { |
| | | uni.navigateTo({ |
| | | url: '/pages/reception/index', |
| | | }) |
| | | } |
| | | |
| | | function buildVisitorCodeSharePath(orderUuid?: string | null) { |
| | | const uuid = orderUuid || currentOrder.value?.orderUuid |
| | | return uuid ? `/pages/visitorCode/index?uuid=${encodeURIComponent(uuid)}` : '/pages/index/index' |
| | | } |
| | | |
| | | onShareAppMessage((res) => { |
| | | const orderUuid = res?.target?.dataset?.orderUuid as string | undefined |
| | | const visitedUserDisplay = res?.target?.dataset?.visitedUserDisplay as string | undefined |
| | | return { |
| | | title: visitedUserDisplay ? `${visitedUserDisplay}的访客码` : '访客码分享', |
| | | path: buildVisitorCodeSharePath(orderUuid), |
| | | } |
| | | }) |
| | | |
| | | onShow(() => { |
| | | authStore.restoreSession() |
| | | void loadVisitorCodes() |
| | | void loadEmployeeDashboardCounts() |
| | | }) |
| | | |
| | | definePage({ |
| | | name: 'home', |
| | |
| | | |
| | | <template> |
| | | <view class="relative"> |
| | | <!-- 背景图 --> |
| | | <image src="/static/home/bg.png" class="absolute left-0 top-0 z-0 h-[1624rpx] w-full" mode="scaleToFill" /> |
| | | |
| | | <!-- 内容区 --> |
| | | <view class="relative z-4" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }"> |
| | | <!-- 导航栏标题 --> |
| | | <view class="mb-[64rpx] h-[88rpx] flex items-center px-[32rpx]"> |
| | | <text class="text-[36rpx] text-white font-medium"> |
| | | 预约小程序 |
| | | </text> |
| | | </view> |
| | | |
| | | <!-- 欢迎使用区 --> |
| | | <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]" @click="!isLoggedIn && handleShowLoginPopup()"> |
| | | <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]"> |
| | | <image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" /> |
| | | <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]"> |
| | | <template v-if="!isLoggedIn"> |
| | | <text>欢迎使用,</text> |
| | | <view class="login-text relative"> |
| | | 请登录 |
| | | <view class="login-text__underline absolute bottom-0 left-0 h-[4rpx] w-full bg-white" /> |
| | | </view> |
| | | </template> |
| | | <template v-else> |
| | | <text>王丽</text> |
| | | </template> |
| | | <view class="flex flex-col"> |
| | | <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]"> |
| | | <text>{{ profile?.displayName || '用户' }}</text> |
| | | </view> |
| | | <text class="mt-[8rpx] text-[24rpx] text-white/90"> |
| | | {{ userRoleText }}<template v-if="secondaryText"> · {{ secondaryText }}</template> |
| | | </text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 待办/待接待区 --> |
| | | <view v-if="userRole === 'employee'" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;"> |
| | | <view v-if="isEmployee" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;"> |
| | | <view |
| | | class="rounded-[16rpx] p-[24rpx] pb-[26rpx]" |
| | | style="background: linear-gradient(180deg, #CCE4FE 0%, #FAFCFD 40%, #FDFDFD 100%); box-shadow: 0 4rpx 8rpx 0 rgba(0, 104, 255, 0.2);border: 1rpx solid #fff;" |
| | | > |
| | | <view class="flex gap-[30rpx]"> |
| | | <!-- 待办 --> |
| | | <view |
| | | class="h-[126rpx] w-[312rpx] flex rounded-[20rpx] pl-[38rpx] pt-[20rpx]" |
| | | style="background: url('/static/home/toDo_bg.png') no-repeat center center; |
| | | background-size: 100%;box-sizing: border-box; |
| | | box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)" |
| | | @click="handleOpenApprovalTodo" |
| | | > |
| | | <image src="/static/home/toDo.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" /> |
| | | <view class="mt-[8rpx] flex flex-col"> |
| | |
| | | 待办 |
| | | </text> |
| | | <text class="text-[32rpx] text-[#056DFF] font-bold"> |
| | | - |
| | | {{ approvalTodoCount }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <!-- 待接待 --> |
| | | <view |
| | | class="h-[126rpx] w-[312rpx] flex items-center rounded-[20rpx] pl-[38rpx] pt-[20rpx]" |
| | | style="background: url('/static/home/wait_bg.png') no-repeat center center; |
| | | background-size: 100%;box-sizing: border-box; |
| | | box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)" |
| | | @click="handleOpenVisitRecords" |
| | | > |
| | | <image src="/static/home/wait.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" /> |
| | | <view class="mt-[8rpx] flex flex-col"> |
| | |
| | | 待接待 |
| | | </text> |
| | | <text class="text-[32rpx] text-[#F86911] font-bold"> |
| | | - |
| | | {{ visitRecordCount }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 功能区 --> |
| | | <view class="flex gap-[16rpx] pl-[24rpx] pr-[22rpx]"> |
| | | <!-- 预约 --> |
| | | <view |
| | | class="h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]" |
| | | style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)" |
| | |
| | | 预约 |
| | | </text> |
| | | </view> |
| | | <!-- 预约记录 & 常用来访人 --> |
| | | <view class="flex flex-1 flex-col gap-[16rpx]"> |
| | | <view |
| | | class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]" |
| | | <view |
| | | class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]" |
| | | style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)" |
| | | @click="() => { router.push({ name: 'record' }) }" |
| | | > |
| | |
| | | <view |
| | | class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]" |
| | | style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)" |
| | | @click="uni.navigateTo({ url: '/pages/choosePerson/index?mode=manage' })" |
| | | > |
| | | <image src="/static/home/visitor.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" /> |
| | | <text class="text-[28rpx] text-[#000000]"> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- 登录后:我的访客码区域 --> |
| | | <view v-if="isLoggedIn" class="mt-[32rpx] px-[24rpx]"> |
| | | <!-- 头部标题与切换 --> |
| | | |
| | | <view class="mt-[32rpx] px-[24rpx]"> |
| | | <view class="mb-[20rpx] flex items-center justify-between"> |
| | | <text class="text-[34rpx] text-[#1a1a1a]"> |
| | | 我的访客码 |
| | | </text> |
| | | <view class="flex items-center"> |
| | | <view v-if="visitorOrders.length > 0" class="flex items-center"> |
| | | <image |
| | | src="/static/home/chevron_left.png" |
| | | class="h-[40rpx] w-[40rpx] pr-[24rpx]" |
| | | @click="handlePrevVisitor" |
| | | @click="handlePrevOrder" |
| | | /> |
| | | <text class="text-[28rpx] text-[#999999] line-height-[40rpx]"> |
| | | {{ currentVisitorIndex + 1 }}/{{ visitorList.length }} |
| | | {{ currentOrderIndex + 1 }}/{{ visitorOrders.length }} |
| | | </text> |
| | | <image |
| | | src="/static/home/chevron_right.png" |
| | | class="h-[40rpx] w-[40rpx] pl-[24rpx]" |
| | | @click="handleNextVisitor" |
| | | @click="handleNextOrder" |
| | | /> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 访客码卡片轮播 --> |
| | | <view |
| | | v-if="loadingVisitorCodes" |
| | | class="h-[420rpx] flex items-center justify-center rounded-[12rpx] bg-white text-[28rpx] text-[#999999]" |
| | | style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);" |
| | | > |
| | | 加载访客码中... |
| | | </view> |
| | | |
| | | <view |
| | | v-else-if="visitorOrders.length === 0" |
| | | class="h-[420rpx] flex items-center justify-center rounded-[12rpx] bg-white text-[28rpx] text-[#999999]" |
| | | style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);" |
| | | > |
| | | 暂无可展示的访客码 |
| | | </view> |
| | | |
| | | <swiper |
| | | class="min-h-[840rpx] w-full" |
| | | :current="currentVisitorIndex" |
| | | v-else |
| | | class="min-h-[760rpx] w-full" |
| | | :current="currentOrderIndex" |
| | | style="box-shadow: 0 6rpx 10rpx 0 rgba(0, 104, 255, 0.16);" |
| | | @change="onSwiperChange" |
| | | > |
| | | <swiper-item v-for="(item) in visitorList" :key="item.id"> |
| | | <swiper-item v-for="order in visitorOrders" :key="order.orderId"> |
| | | <view |
| | | class="h-full w-full flex flex-col rounded-[12rpx] bg-white py-[24rpx]" |
| | | style="box-sizing: border-box;overflow: hidden;" |
| | | > |
| | | <!-- 顶部信息 --> |
| | | <view class="flex flex-col items-center"> |
| | | <text class="mb-[16rpx] text-[28rpx] text-[#1a1a1a]"> |
| | | {{ item.name }}({{ item.department }}) |
| | | <text class="mb-[16rpx] px-[32rpx] text-center text-[28rpx] text-[#1a1a1a]"> |
| | | {{ order.visitedUserDisplay }} |
| | | </text> |
| | | <view |
| | | class="mb-[28rpx] mt-[16rpx] h-[48rpx] w-full flex items-center justify-center" |
| | | class="mb-[28rpx] mt-[16rpx] h-[48rpx] w-full flex items-center justify-center px-[20rpx]" |
| | | style="background: linear-gradient(90deg, rgba(0,104,255,0) 0%, rgba(0,104,255) 50%, rgba(0,104,255,0) 100%);" |
| | | > |
| | | <text class="text-[24rpx] text-white"> |
| | | {{ item.time }} |
| | | <text class="truncate text-[24rpx] text-white"> |
| | | {{ order.timeText }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 二维码区 --> |
| | | <view class="relative flex items-center justify-center px-[64rpx]"> |
| | | <image src="/static/home/left.png" class="absolute left-[32rpx] h-[48rpx] w-[24rpx]" @click="handlePrevVisitor" /> |
| | | <image :src="item.qrCode" class="h-[448rpx] w-[448rpx]" mode="aspectFit" /> |
| | | <image src="/static/home/right.png" class="absolute right-[32rpx] h-[48rpx] w-[24rpx]" @click="handleNextVisitor" /> |
| | | <view class="relative flex items-center justify-center px-[72rpx]"> |
| | | <image |
| | | v-if="order.qrCodes.length > 1" |
| | | src="/static/home/left.png" |
| | | class="absolute left-[32rpx] h-[48rpx] w-[24rpx]" |
| | | @click="handlePrevTicket(order.orderId)" |
| | | /> |
| | | <image |
| | | :src="currentOrder?.orderId === order.orderId ? currentTicket?.qrCode : order.qrCodes[0]?.qrCode" |
| | | class="h-[360rpx] w-[360rpx]" |
| | | mode="aspectFit" |
| | | /> |
| | | <image |
| | | v-if="order.qrCodes.length > 1" |
| | | src="/static/home/right.png" |
| | | class="absolute right-[32rpx] h-[48rpx] w-[24rpx]" |
| | | @click="handleNextTicket(order.orderId)" |
| | | /> |
| | | </view> |
| | | |
| | | <!-- 访客名称 --> |
| | | <view class="mt-[20rpx] flex justify-center pb-[36rpx]"> |
| | | <view class="mt-[12rpx] flex flex-col items-center justify-center gap-[8rpx] pb-[24rpx]"> |
| | | <text class="text-[24rpx] text-[rgba(26,26,26,0.5)]"> |
| | | {{ item.visitorName }}的访客码 |
| | | {{ (currentOrder?.orderId === order.orderId ? currentTicket?.visitorName : order.qrCodes[0]?.visitorName) }}的访客码 |
| | | </text> |
| | | <text |
| | | v-if="order.accessAreaText" |
| | | class="px-[40rpx] text-center text-[22rpx] text-[rgba(26,26,26,0.45)]" |
| | | > |
| | | 通行区域:{{ order.accessAreaText }} |
| | | </text> |
| | | <text v-if="order.qrCodes.length > 1" class="text-[22rpx] text-[rgba(26,26,26,0.45)]"> |
| | | {{ currentTicketIndex(order.orderId) + 1 }}/{{ order.qrCodes.length }} |
| | | </text> |
| | | </view> |
| | | |
| | | <!-- 底部操作 --> |
| | | <view class="h-[112rpx] flex items-center justify-center gap-[32rpx] pt-[28rpx]" style="border-top: 2rpx dashed #D8D8D8;border-radius: 12rpx;"> |
| | | <view class="w-[144rpx] flex flex-col items-center justify-center" @click="handleDownloadVisitorCode(item.qrCode)"> |
| | | <view class="h-[104rpx] flex items-center justify-center gap-[32rpx] pt-[20rpx]" style="border-top: 2rpx dashed #D8D8D8;border-radius: 12rpx;"> |
| | | <view |
| | | class="w-[144rpx] flex flex-col items-center justify-center" |
| | | @click="handleDownloadVisitorCode(currentOrder?.orderId === order.orderId ? currentTicket?.qrCode || '' : order.qrCodes[0]?.qrCode || '')" |
| | | > |
| | | <image src="/static/home/download.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" /> |
| | | <text class="text-[24rpx] text-[#1a1a1a]"> |
| | | 下载访客码 |
| | |
| | | <button |
| | | class="m-0 w-[144rpx] flex flex-col items-center justify-center border-none bg-transparent p-0 line-height-normal after:border-none" |
| | | open-type="share" |
| | | :data-order-uuid="order.orderUuid" |
| | | :data-visited-user-display="order.visitedUserDisplay" |
| | | > |
| | | <image src="/static/home/share.png" class="mb-[8rpx] h-[40rpx] w-[40rpx]" /> |
| | | <text class="text-[24rpx] text-[#1a1a1a]"> |
| | |
| | | </swiper> |
| | | </view> |
| | | |
| | | <!-- 备案号 (登录后显示在底部) --> |
| | | <view v-if="isLoggedIn" class="mt-[48rpx] flex justify-center pb-[32rpx]"> |
| | | <view class="mt-[48rpx] flex justify-center pb-[32rpx]"> |
| | | <text class="text-[24rpx] text-[#999999]"> |
| | | 备案号:2435345342 |
| | | </text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 悬浮流程图标 --> |
| | | <image src="/static/home/process.png" class="fixed bottom-[482rpx] right-[-42rpx] z-5 h-[128rpx] w-[128rpx]" /> |
| | | |
| | | <!-- 未登录提示弹窗 --> |
| | | <wd-popup v-model="showLoginPopup" custom-class="login-popup" position="center"> |
| | | <view |
| | | class="box-border w-[590rpx] flex flex-col items-center px-[40rpx] pb-[32rpx] pt-[32rpx]" |
| | | style="background: linear-gradient(180deg, #D1E6FE 0%, #FFFFFF 34%, #FFFFFF 100%);" |
| | | > |
| | | <!-- 标题 --> |
| | | <text class="text-[36rpx] text-[#000000] font-bold leading-[50rpx]"> |
| | | 提示 |
| | | </text> |
| | | |
| | | <!-- 图标 --> |
| | | <image src="/static/home/tips.png" class="mt-[56rpx] h-[202rpx] w-[202rpx]" mode="aspectFit" /> |
| | | |
| | | <!-- 提示文案 --> |
| | | <text class="mt-[14rpx] text-[32rpx] text-[#000000] leading-[44rpx]"> |
| | | 您还未登录,请先登录 |
| | | </text> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="mt-[32rpx] w-full flex justify-between gap-[24rpx]"> |
| | | <view class="h-[80rpx] flex flex-1 items-center justify-center rounded-[40rpx] bg-[#F5F5F5]" @click="handleCancelLogin"> |
| | | <text class="text-[32rpx] text-[#989898] font-medium"> |
| | | 再想想 |
| | | </text> |
| | | </view> |
| | | <button |
| | | class="m-0 h-[80rpx] flex flex-1 items-center justify-center rounded-[40rpx] border-none bg-[#0C6FFF] p-0 after:border-none" |
| | | open-type="getPhoneNumber" |
| | | @getphonenumber="handleConfirmLogin" |
| | | > |
| | | <text class="text-[32rpx] text-[#FFFFFF] font-medium"> |
| | | 去登录 |
| | | </text> |
| | | </button> |
| | | </view> |
| | | </view> |
| | | </wd-popup> |
| | | <image |
| | | src="/static/home/process.png" |
| | | class="fixed right-[-42rpx] z-5 h-[128rpx] w-[128rpx]" |
| | | :style="{ bottom: `${parkFlowBottomPx}px` }" |
| | | @click="handleOpenParkFlow" |
| | | /> |
| | | <canvas canvas-id="visitorQrCanvas" class="qr-canvas" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | font-family: 'YouSheBiaoTiHei', sans-serif; |
| | | } |
| | | |
| | | .login-text { |
| | | display: inline-block; |
| | | position: relative; |
| | | border-bottom: 4rpx solid #fff; |
| | | padding-bottom: 2rpx; |
| | | } |
| | | |
| | | :deep(.login-popup) { |
| | | border-radius: 32rpx; |
| | | .qr-canvas { |
| | | position: fixed; |
| | | left: -9999px; |
| | | top: -9999px; |
| | | width: 300px; |
| | | height: 300px; |
| | | pointer-events: none; |
| | | } |
| | | </style> |