798 lines
24 KiB
Vue
798 lines
24 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- Tab 筛选栏 -->
|
||
<scroll-view scroll-x class="tab-bar">
|
||
<view class="tab-inner">
|
||
<view
|
||
v-for="tab in tabs"
|
||
:key="tab.value"
|
||
class="tab-item"
|
||
:class="{ 'tab-item--active': activeTab === tab.value }"
|
||
@tap="activeTab = tab.value"
|
||
>
|
||
<text class="tab-text">{{ tab.label }}</text>
|
||
<view v-if="activeTab === tab.value" class="tab-underline" />
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 列表区域 -->
|
||
<scroll-view
|
||
scroll-y
|
||
class="list-area"
|
||
refresher-enabled
|
||
:refresher-triggered="refreshing"
|
||
@refresherrefresh="onRefresh"
|
||
@scrolltolower="onLoadMore"
|
||
>
|
||
<!-- 空状态 -->
|
||
<view v-if="filteredRecords.length === 0" class="empty-state">
|
||
<view class="empty-icon">
|
||
<view class="empty-cal" />
|
||
<view class="empty-cal__line" />
|
||
</view>
|
||
<text class="empty-text">暂无预约记录,快去预约景区</text>
|
||
<view class="empty-btn" @tap="goReserve">
|
||
<text class="empty-btn__text">去预约</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 记录列表 -->
|
||
<view v-else class="record-list">
|
||
<view
|
||
v-for="r in filteredRecords"
|
||
:key="r.id"
|
||
class="record-card"
|
||
>
|
||
<!-- 头部 -->
|
||
<view class="record-header">
|
||
<text class="record-name">{{ r.scenicName }}</text>
|
||
<view class="status-tag" :class="`status-tag--${r.status}`">
|
||
<text class="status-tag__text">{{ statusLabel(r.status) }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 卡种+卡号 -->
|
||
<view class="record-card-info">
|
||
<text class="record-card-type">{{ r.cardType }}</text>
|
||
<text class="record-card-no">卡号:{{ r.cardNo }}</text>
|
||
</view>
|
||
<!-- 预约入园日期 -->
|
||
<view class="record-info">
|
||
<text class="record-date">预约入园日期:<text class="record-date--value">{{ formatDateOnly(r.date) }}</text></text>
|
||
</view>
|
||
<!-- 操作 -->
|
||
<view class="record-actions">
|
||
<view
|
||
v-if="r.canCancel && r.status !== 'cancelled'"
|
||
class="btn-sm btn-sm--outline"
|
||
@tap="showCancelModal(r)"
|
||
>
|
||
<text class="btn-sm__text--outline">取消预约</text>
|
||
</view>
|
||
<view
|
||
v-if="r.showEntryCode && r.status !== 'cancelled'"
|
||
class="btn-sm btn-sm--outline"
|
||
@tap="showCode(r)"
|
||
>
|
||
<text class="btn-sm__text--outline">查看入园码</text>
|
||
</view>
|
||
<view
|
||
class="btn-sm btn-sm--outline"
|
||
@tap="viewDetail(r)"
|
||
>
|
||
<text class="btn-sm__text--outline">查看详情</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="filteredRecords.length > 0" class="load-more">
|
||
<text v-if="loading" class="load-more__text">加载中...</text>
|
||
<text v-else-if="noMore" class="load-more__text">没有更多记录了</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 入园码弹窗 -->
|
||
<uni-popup ref="codePopup" type="center" :mask-click="true" @close="closeCode">
|
||
<view class="code-modal">
|
||
<text class="code-modal__title">入园码</text>
|
||
<view class="qr-box">
|
||
<image v-if="codeSrc" class="qr-img" :src="codeSrc" mode="widthFix" />
|
||
</view>
|
||
<text class="code-modal__desc">入园时请出示此二维码+本人身份证</text>
|
||
<text class="code-modal__countdown">{{ countdown }}秒后自动刷新</text>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 取消预约弹窗 -->
|
||
<uni-popup ref="cancelPopup" type="center" :mask-click="true">
|
||
<view class="cancel-modal">
|
||
<text class="cancel-modal__title">取消预约</text>
|
||
<view class="cancel-modal__body">
|
||
<text class="cancel-modal__text">确定取消 {{ formatReserveDate(cancelRecord?.date) }}{{ cancelRecord?.scenicName }} 的预约吗?</text>
|
||
</view>
|
||
<view class="cancel-modal__actions">
|
||
<view class="btn-modal btn-modal--outline" @tap="cancelPopup?.close()">
|
||
<text class="btn-modal__text--outline">再想想</text>
|
||
</view>
|
||
<view class="btn-modal btn-modal--primary" @tap="confirmCancel">
|
||
<text class="btn-modal__text--primary">确认取消</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 详情弹窗 -->
|
||
<uni-popup ref="detailPopup" type="center" :mask-click="true">
|
||
<view class="detail-sheet">
|
||
<view class="sheet-header">
|
||
<text class="sheet-title">使用详情</text>
|
||
<view class="sheet-close" @tap="detailPopup?.close()">
|
||
<view class="sheet-close__icon" />
|
||
</view>
|
||
</view>
|
||
<scroll-view scroll-y class="sheet-body">
|
||
<view class="sheet-row">
|
||
<text class="sheet-label">预约景区</text>
|
||
<text class="sheet-value">{{ detailRecord?.scenicName }}</text>
|
||
</view>
|
||
<view class="sheet-row">
|
||
<text class="sheet-label">预约入园日期</text>
|
||
<text class="sheet-value sheet-value--date">{{ formatDateOnly(detailRecord?.date) }}</text>
|
||
</view>
|
||
<view v-if="detailRecord?.session" class="sheet-row">
|
||
<text class="sheet-label">预约场次</text>
|
||
<text class="sheet-value">{{ detailRecord.session }}</text>
|
||
</view>
|
||
<view class="sheet-row">
|
||
<text class="sheet-label">预约人</text>
|
||
<text class="sheet-value">{{ detailRecord?.userName }}</text>
|
||
</view>
|
||
<view class="sheet-row">
|
||
<text class="sheet-label">预约状态</text>
|
||
<text class="sheet-value">{{ detailStatusLabel(detailRecord?.status || '') }}</text>
|
||
</view>
|
||
<view class="sheet-row">
|
||
<text class="sheet-label">预约申请时间</text>
|
||
<text class="sheet-value">{{ detailRecord?.applyTime }}</text>
|
||
</view>
|
||
<view v-if="detailRecord?.verifyTime" class="sheet-row">
|
||
<text class="sheet-label">核销时间</text>
|
||
<text class="sheet-value">{{ detailRecord.verifyTime }}</text>
|
||
</view>
|
||
<view v-if="detailRecord?.cancelTime" class="sheet-row">
|
||
<text class="sheet-label">取消时间</text>
|
||
<text class="sheet-value">{{ detailRecord.cancelTime }}</text>
|
||
</view>
|
||
<!-- 人脸图片 -->
|
||
<view v-if="detailRecord?.photo" class="sheet-photo">
|
||
<text class="sheet-label">人脸图片</text>
|
||
<image class="sheet-photo-avatar" :src="detailRecord.photo" mode="aspectFill" @tap="previewPhoto(detailRecord.photo)" />
|
||
</view>
|
||
<!-- 核销人脸照片 -->
|
||
<view v-if="detailRecord?.verifyPhoto" class="sheet-photo">
|
||
<text class="sheet-label">核销照片</text>
|
||
<image class="sheet-photo-avatar" :src="detailRecord.verifyPhoto" mode="aspectFill" @tap="previewPhoto(detailRecord.verifyPhoto)" />
|
||
</view>
|
||
<view v-if="detailRecord?.status !== 'used' && detailRecord?.cardInstruction" class="sheet-tip">
|
||
<text class="sheet-tip__text">{{ detailRecord.cardInstruction }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { post } from '../../../../api/request'
|
||
import { BASE_URL } from '../../../../config'
|
||
|
||
type RecordStatus = 'pending' | 'cancelled' | 'used'
|
||
|
||
interface ReserveRecord {
|
||
id: string
|
||
scenicName: string
|
||
date: string
|
||
session?: string
|
||
status: RecordStatus
|
||
userName: string
|
||
maskedId: string
|
||
cardType: string
|
||
cardNo: string
|
||
applyTime: string
|
||
cancelTime?: string
|
||
verifyTime?: string
|
||
verifyPhoto?: string
|
||
photo?: string
|
||
cardInstruction?: string
|
||
cardModelId?: string
|
||
canCancel?: boolean
|
||
showEntryCode?: boolean
|
||
}
|
||
|
||
const tabs = [
|
||
{ label: '全部', value: '' },
|
||
{ label: '待使用', value: 'pending' },
|
||
{ label: '已入园', value: 'used' },
|
||
{ label: '已取消', value: 'cancelled' },
|
||
]
|
||
|
||
const activeTab = ref('')
|
||
const loading = ref(false)
|
||
const refreshing = ref(false)
|
||
const noMore = ref(false)
|
||
const pageNo = ref(1)
|
||
const pageSize = 10
|
||
const codePopup = ref<any>(null)
|
||
const cancelPopup = ref<any>(null)
|
||
const detailPopup = ref<any>(null)
|
||
const codeSrc = ref('')
|
||
const detailRecord = ref<ReserveRecord | null>(null)
|
||
const cancelRecord = ref<ReserveRecord | null>(null)
|
||
const countdown = ref(20)
|
||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
onUnmounted(() => {
|
||
clearTimer()
|
||
})
|
||
|
||
const records = ref<ReserveRecord[]>([])
|
||
|
||
const filteredRecords = computed(() => records.value)
|
||
|
||
function statusLabel(status: string) {
|
||
const map: Record<string, string> = { pending: '待使用', cancelled: '已取消', used: '已入园' }
|
||
return map[status] || status
|
||
}
|
||
|
||
function detailStatusLabel(status: number | string) {
|
||
const map: Record<string, string> = { '1': '预约成功', '2': '预约失败', '3': '已取消', '4': '已入园' }
|
||
return map[String(status)] || String(status)
|
||
}
|
||
|
||
async function fetchRecords(append = false) {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
const phone = (uni.getStorageSync('phone') as string) || ''
|
||
const res = await post<any>('/mpReserve/queryUsageRecordList', {
|
||
phone,
|
||
status: activeTab.value,
|
||
pageNo: pageNo.value,
|
||
pageSize,
|
||
})
|
||
const list = (res.list || res.records || []).map((item: any) => ({
|
||
id: item.id || '',
|
||
scenicName: item.scenicName || item.resourceName || '',
|
||
date: item.appTime || item.date || '',
|
||
session: item.sessionTime || item.session || '',
|
||
status: item.status || 'pending',
|
||
userName: item.name || item.userName || '',
|
||
maskedId: item.identityNo ? `${item.identityNo.slice(0, 6)}****${item.identityNo.slice(-4)}` : '',
|
||
cardType: item.cardModelName || item.cardType || '',
|
||
cardNo: item.cardNum || item.cardNo || '',
|
||
applyTime: item.createTime || item.applyTime || '',
|
||
cancelTime: item.cancelTime || '',
|
||
verifyTime: item.verifyTime || '',
|
||
verifyPhoto: item.verifyPhoto || '',
|
||
photo: item.photo || '',
|
||
cardInstruction: item.cardInstruction || '',
|
||
cardModelId: item.cardModelId || item.thirdId || '',
|
||
canCancel: !!item.canCancel,
|
||
showEntryCode: !!item.showEntryCode,
|
||
}))
|
||
if (append) {
|
||
records.value.push(...list)
|
||
} else {
|
||
records.value = list
|
||
}
|
||
noMore.value = list.length < pageSize
|
||
} catch (_e) {
|
||
records.value = []
|
||
noMore.value = true
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onLoad(() => {
|
||
fetchRecords()
|
||
})
|
||
|
||
watch(activeTab, () => {
|
||
pageNo.value = 1
|
||
fetchRecords()
|
||
})
|
||
|
||
/** 将预约日期(如 2026-08-05)格式化为「2026 年 8 月 5 日」 */
|
||
function formatReserveDate(date?: string): string {
|
||
const m = (date || '').match(/(\d{4})[-/年.](\d{1,2})[-/月.](\d{1,2})/)
|
||
if (!m) return date || ''
|
||
return `${m[1]} 年 ${Number(m[2])} 月 ${Number(m[3])} 日`
|
||
}
|
||
|
||
/** 日期去除时分秒(如 2026-08-05 10:00:00 → 2026-08-05) */
|
||
function formatDateOnly(date?: string): string {
|
||
if (!date) return ''
|
||
return date.split(' ')[0]
|
||
}
|
||
|
||
function showCancelModal(r: ReserveRecord) {
|
||
cancelRecord.value = r
|
||
cancelPopup.value?.open()
|
||
}
|
||
|
||
async function confirmCancel() {
|
||
cancelPopup.value?.close()
|
||
if (cancelRecord.value) {
|
||
try {
|
||
await post<any>(`${BASE_URL}/zhy-api/cardBag/cancelAppoint`, {
|
||
appointId: cancelRecord.value.id,
|
||
channel: 'Channel_SZH',
|
||
})
|
||
const idx = records.value.findIndex(o => o.id === cancelRecord.value!.id)
|
||
if (idx !== -1) {
|
||
records.value[idx].status = 'cancelled'
|
||
records.value[idx].cancelTime = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||
}
|
||
uni.showToast({ title: '已取消预约', icon: 'success' })
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e?.message || '取消失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
|
||
async function showCode(r: ReserveRecord) {
|
||
codeSrc.value = ''
|
||
currentRecord.value = r
|
||
await fetchCode(r)
|
||
codePopup.value?.open()
|
||
startTimer()
|
||
}
|
||
|
||
function closeCode() {
|
||
clearTimer()
|
||
}
|
||
|
||
function clearTimer() {
|
||
if (refreshTimer) {
|
||
clearInterval(refreshTimer)
|
||
refreshTimer = null
|
||
}
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
}
|
||
|
||
function startTimer() {
|
||
clearTimer()
|
||
countdown.value = 20
|
||
const id = currentRecord.value?.id
|
||
if (!id) return
|
||
refreshTimer = setInterval(() => fetchCode(currentRecord.value!), 20000)
|
||
countdownTimer = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) countdown.value = 20
|
||
}, 1000)
|
||
}
|
||
|
||
const currentRecord = ref<ReserveRecord | null>(null)
|
||
|
||
async function fetchCode(r: ReserveRecord) {
|
||
countdown.value = 20
|
||
try {
|
||
const res = await post<any>(`${BASE_URL}/zhy-api/cardBag/getQrByAppointmentId?appointmentId=${r.id}`)
|
||
if (res.qrType == 1) {
|
||
codeSrc.value = res.qrCode.startsWith('data:') ? res.qrCode : 'data:image/png;base64,' + res.qrCode
|
||
} else if (res.qrType == 2) {
|
||
codeSrc.value = res.qrUrl
|
||
} else {
|
||
await fetchQrByCard(r)
|
||
}
|
||
} catch (_e) {
|
||
await fetchQrByCard(r)
|
||
}
|
||
}
|
||
|
||
async function fetchQrByCard(r: ReserveRecord) {
|
||
try {
|
||
const phone = (uni.getStorageSync('phone') as string) || ''
|
||
const res = await post<any>(`${BASE_URL}/zhy-api/cardBag/qrAndUserInfo`, {
|
||
cardNum: r.cardNo,
|
||
channel: 'Channel_SZH',
|
||
mobile: phone,
|
||
scenicId: '',
|
||
thirdId: r.cardModelId,
|
||
})
|
||
if (res.qrCode) {
|
||
codeSrc.value = res.qrCode.startsWith('data:') ? res.qrCode : 'data:image/png;base64,' + res.qrCode
|
||
} else {
|
||
uni.showToast({ icon: 'none', title: '暂无核销码' })
|
||
}
|
||
} catch (_e) {
|
||
uni.showToast({ icon: 'none', title: '获取入园码失败' })
|
||
}
|
||
}
|
||
|
||
async function viewDetail(r: ReserveRecord) {
|
||
try {
|
||
const phone = (uni.getStorageSync('phone') as string) || ''
|
||
const res = await post<any>('/mpReserve/getAppointmentDetail', { phone, appointId: r.id })
|
||
if (res) {
|
||
detailRecord.value = {
|
||
id: res.id || r.id,
|
||
scenicName: res.resourceName || r.scenicName,
|
||
date: res.appTime || r.date,
|
||
session: res.sessionTime || r.session,
|
||
status: res.status || r.status || 'pending',
|
||
userName: res.name || r.userName,
|
||
maskedId: res.identityNo ? `${res.identityNo.slice(0, 6)}****${res.identityNo.slice(-4)}` : r.maskedId,
|
||
cardType: res.cardModelName || r.cardType,
|
||
cardNo: res.cardNum || r.cardNo,
|
||
applyTime: res.createTime || r.applyTime,
|
||
cancelTime: res.cancelTime || r.cancelTime,
|
||
verifyTime: res.verifyTime || r.verifyTime,
|
||
verifyPhoto: res.verifyPhoto || r.verifyPhoto,
|
||
photo: res.photo || r.photo,
|
||
cardInstruction: res.cardInstruction || r.cardInstruction || '',
|
||
cardModelId: res.cardModelId || r.cardModelId || '',
|
||
}
|
||
}
|
||
} catch (_e) {
|
||
detailRecord.value = r
|
||
}
|
||
detailPopup.value?.open()
|
||
}
|
||
|
||
async function onRefresh() {
|
||
refreshing.value = true
|
||
pageNo.value = 1
|
||
await fetchRecords()
|
||
refreshing.value = false
|
||
}
|
||
|
||
async function onLoadMore() {
|
||
if (loading.value || noMore.value) return
|
||
pageNo.value++
|
||
await fetchRecords(true)
|
||
}
|
||
|
||
function goReserve() {
|
||
uni.navigateTo({ url: '/packages/content/pages/scenic-list' })
|
||
}
|
||
|
||
function previewPhoto(url: string) {
|
||
uni.previewImage({ urls: [url] })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
background: $color-background;
|
||
font-family: $font-family-body;
|
||
}
|
||
|
||
/* ===== Tab ===== */
|
||
.tab-bar {
|
||
white-space: nowrap;
|
||
background: $color-card;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.tab-inner {
|
||
display: flex;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20rpx 16rpx;
|
||
position: relative;
|
||
min-width: 120rpx;
|
||
}
|
||
|
||
.tab-text {
|
||
font-family: $font-family-body;
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-medium;
|
||
color: $uv-tips-color;
|
||
}
|
||
|
||
.tab-item--active .tab-text {
|
||
color: $uv-primary;
|
||
font-weight: $font-weight-bold;
|
||
}
|
||
|
||
.tab-underline {
|
||
width: 32rpx;
|
||
height: 4rpx;
|
||
border-radius: 2rpx;
|
||
background: $uv-primary;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
/* ===== List ===== */
|
||
.list-area {
|
||
height: calc(100vh - 88rpx);
|
||
}
|
||
|
||
.record-list {
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
/* ===== Record Card ===== */
|
||
.record-card {
|
||
background: $color-card;
|
||
border-radius: $radius-lg;
|
||
padding: 28rpx;
|
||
margin-top: 20rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.record-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.record-name {
|
||
font-family: $font-family-body;
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-medium;
|
||
color: $uv-main-color;
|
||
flex: 1;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.status-tag {
|
||
padding: 4rpx 14rpx;
|
||
border-radius: 8rpx;
|
||
flex-shrink: 0;
|
||
|
||
&--pending { background: #e8f3ff; }
|
||
&--cancelled { background: $uv-bg-color; }
|
||
&--used { background: #f0fff4; }
|
||
}
|
||
|
||
.status-tag__text {
|
||
font-family: $font-family-body;
|
||
font-size: 22rpx;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.status-tag--pending .status-tag__text { color: $uv-primary; }
|
||
.status-tag--cancelled .status-tag__text { color: $color-status-cancel; }
|
||
.status-tag--used .status-tag__text { color: $color-status-success; }
|
||
|
||
.record-card-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.record-card-type {
|
||
font-family: $font-family-body;
|
||
font-size: 22rpx;
|
||
color: $uv-primary;
|
||
background: rgba(60, 156, 255, 0.06);
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
|
||
.record-card-no {
|
||
font-family: $font-family-body;
|
||
font-size: 22rpx;
|
||
color: $uv-placeholder-color;
|
||
}
|
||
|
||
.record-info {
|
||
padding-bottom: 12rpx;
|
||
border-bottom: 1rpx solid $uv-border-color;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.record-date {
|
||
font-family: $font-family-body;
|
||
font-size: $font-size-sm;
|
||
color: $uv-tips-color;
|
||
}
|
||
|
||
.record-date--value {
|
||
color: $uv-error;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.record-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.btn-sm {
|
||
height: 52rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 26rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&--outline {
|
||
border: 2rpx solid $uv-border-color;
|
||
&:active { transform: scale(0.97); background: $uv-bg-color; }
|
||
}
|
||
}
|
||
|
||
.btn-sm__text--outline {
|
||
font-family: $font-family-body;
|
||
font-size: 22rpx;
|
||
font-weight: $font-weight-medium;
|
||
color: $uv-tips-color;
|
||
}
|
||
|
||
/* ===== Empty ===== */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 160rpx 48rpx 0;
|
||
}
|
||
|
||
.empty-icon { width: 120rpx; height: 120rpx; position: relative; margin-bottom: 32rpx; }
|
||
.empty-cal { width: 80rpx; height: 72rpx; border: 4rpx solid $uv-disabled-color; border-radius: 8rpx; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); }
|
||
.empty-cal__line { width: 80rpx; height: 4rpx; background: $uv-disabled-color; position: absolute; bottom: 48rpx; left: 50%; transform: translateX(-50%); }
|
||
|
||
.empty-text { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-placeholder-color; margin-bottom: 32rpx; }
|
||
|
||
.empty-btn {
|
||
height: 72rpx; padding: 0 56rpx; border-radius: 36rpx; background: $uv-primary;
|
||
display: flex; align-items: center; justify-content: center;
|
||
&:active { transform: scale(0.97); }
|
||
}
|
||
|
||
.empty-btn__text { font-family: $font-family-body; font-size: $font-size-sm; font-weight: $font-weight-bold; color: #ffffff; }
|
||
|
||
/* ===== Load More ===== */
|
||
.load-more { text-align: center; padding: 16rpx 0 120rpx; }
|
||
.load-more__text { font-family: $font-family-body; font-size: 22rpx; color: $uv-placeholder-color; }
|
||
|
||
/* ===== Code Modal ===== */
|
||
.code-modal {
|
||
width: 80vw; max-width: 700rpx; background: $color-card; border-radius: $radius-lg; padding: 40rpx;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
}
|
||
|
||
.code-modal__title { font-family: $font-family-body; font-size: 32rpx; font-weight: $font-weight-bold; color: $uv-main-color; margin-bottom: 32rpx; }
|
||
|
||
.qr-box {
|
||
width: 100%; background: #F8FAFC;
|
||
border-radius: 16rpx; display: flex; align-items: center;
|
||
justify-content: center; border: 1rpx solid #E2E8F0; padding: 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.qr-img { width: 100%; height: auto; }
|
||
|
||
.code-modal__desc { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-tips-color; }
|
||
|
||
.code-modal__tip {
|
||
font-family: $font-family-body; font-size: $font-size-sm; color: $uv-warning;
|
||
font-weight: $font-weight-medium; margin-top: 16rpx;
|
||
}
|
||
|
||
.code-modal__countdown {
|
||
font-family: $font-family-body;
|
||
font-size: 24rpx;
|
||
color: $uv-placeholder-color;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
/* ===== Cancel Modal ===== */
|
||
.cancel-modal {
|
||
width: 600rpx; background: $color-card; border-radius: $radius-lg; padding: 40rpx;
|
||
}
|
||
|
||
.cancel-modal__title {
|
||
display: block; font-family: $font-family-body; font-size: 32rpx; font-weight: $font-weight-bold;
|
||
color: $uv-main-color; text-align: center; margin-bottom: 24rpx;
|
||
}
|
||
|
||
.cancel-modal__body { margin-bottom: 32rpx; }
|
||
|
||
.cancel-modal__text {
|
||
font-family: $font-family-body; font-size: $font-size-sm; color: $uv-content-color; line-height: 1.7;
|
||
}
|
||
|
||
.cancel-modal__actions { display: flex; gap: 24rpx; }
|
||
|
||
.btn-modal {
|
||
flex: 1; height: 72rpx; border-radius: 36rpx; display: flex;
|
||
align-items: center; justify-content: center; transition: all $transition-fast;
|
||
|
||
&--outline {
|
||
border: 2rpx solid $uv-border-color;
|
||
&:active { transform: scale(0.97); background: $uv-bg-color; }
|
||
}
|
||
&--primary {
|
||
background: $uv-primary;
|
||
&:active { transform: scale(0.97); }
|
||
}
|
||
}
|
||
|
||
.btn-modal__text--outline { font-family: $font-family-body; font-size: $font-size-sm; font-weight: $font-weight-medium; color: $uv-tips-color; }
|
||
.btn-modal__text--primary { font-family: $font-family-body; font-size: $font-size-sm; font-weight: $font-weight-bold; color: #ffffff; }
|
||
|
||
/* ===== Detail Sheet ===== */
|
||
.detail-sheet {
|
||
width: 640rpx; background: $color-card; border-radius: $radius-lg; max-height: 75vh;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
|
||
.sheet-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 32rpx; border-bottom: 1rpx solid $uv-border-color; flex-shrink: 0;
|
||
}
|
||
|
||
.sheet-title { font-family: $font-family-body; font-size: $font-size-lg; font-weight: $font-weight-bold; color: $uv-main-color; }
|
||
|
||
.sheet-close {
|
||
width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center;
|
||
&:active { opacity: 0.6; }
|
||
}
|
||
|
||
.sheet-close__icon {
|
||
width: 20rpx; height: 20rpx; position: relative;
|
||
&::before, &::after {
|
||
content: ''; position: absolute; top: 50%; left: 50%;
|
||
width: 20rpx; height: 3rpx; background: $uv-tips-color; border-radius: 2rpx;
|
||
}
|
||
&::before { transform: translate(-50%, -50%) rotate(45deg); }
|
||
&::after { transform: translate(-50%, -50%) rotate(-45deg); }
|
||
}
|
||
|
||
.sheet-body { padding: 32rpx; flex: 1; }
|
||
|
||
.sheet-row {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 14rpx 0; border-bottom: 1rpx solid $uv-border-color;
|
||
}
|
||
|
||
.sheet-label { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-main-color; }
|
||
.sheet-value { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-content-color; text-align: right; }
|
||
.sheet-value--date { color: $uv-error; font-weight: $font-weight-medium; }
|
||
|
||
.sheet-photo {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14rpx 0;
|
||
border-bottom: 1rpx solid $uv-border-color;
|
||
}
|
||
|
||
.sheet-photo-avatar {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.sheet-tip {
|
||
margin-top: 24rpx; padding: 20rpx; background: #fff2f2; border-radius: 12rpx;
|
||
}
|
||
|
||
.sheet-tip__text { font-family: $font-family-body; font-size: $font-size-sm; color: $uv-error; line-height: 1.5; }
|
||
</style>
|