diff --git a/src/packages/user/api/card.ts b/src/packages/user/api/card.ts index d7a2c73..2d48297 100644 --- a/src/packages/user/api/card.ts +++ b/src/packages/user/api/card.ts @@ -69,20 +69,20 @@ export function checkFaceFlag(cardNo: string): Promise { } /** 上传图片到 OSS */ -export function uploadImage(filePath: string): Promise { +export function uploadImage(filePath: string, endpoint = '/open-api/oss/upload'): Promise { return new Promise((resolve, reject) => { uni.uploadFile({ - url: `${BASE_URL}/open-api/oss/upload`, + url: `${BASE_URL}${endpoint}`, filePath, name: 'file', success: (res) => { if (res.statusCode === 200) { try { const data = JSON.parse(res.data as string) - if (data.errcode === 0 && data.data?.fileUrl) { + if ((data.errcode === 0 || data.code === 200) && data.data?.fileUrl) { resolve(data.data.fileUrl) } else { - reject(new Error(data.errmsg || '上传失败')) + reject(new Error(data.errmsg || data.msg || '上传失败')) } } catch { reject(new Error('解析响应失败')) } } else { diff --git a/src/packages/user/pages/bind.vue b/src/packages/user/pages/bind.vue index 3d85fb9..2674135 100644 --- a/src/packages/user/pages/bind.vue +++ b/src/packages/user/pages/bind.vue @@ -115,6 +115,35 @@ + + + 照片拍摄要求 + + + 图片仅允许1张人脸,多人脸直接驳回 + + + + 图片不能无人脸、侧脸过大、遮挡严重(口罩/帽子遮大半脸) + + + + 不允许全身、远景、远距离人像照片 + + + + 不允许脸部极致特写、贴脸拍摄无留白 + + + + 图片清晰,无严重模糊、逆光、黑影、反光 + + + + 不允许翻拍屏幕、截图、证件复印件照片 + + + @@ -237,11 +266,16 @@ async function handleImage(filePath: string) { try { const compressedPath = await compressImage(filePath) uni.showLoading({ title: '上传中...' }) - const url = await uploadImage(compressedPath) + const url = await uploadImage(compressedPath, '/open-api/oss/uploadWithFaceCheck') photoUrl.value = url uni.showToast({ title: '上传成功', icon: 'success' }) - } catch { - uni.showToast({ title: '上传失败', icon: 'none' }) + } catch (e) { + uni.showModal({ + title: '上传失败', + content: (e as Error).message || '上传失败', + showCancel: false, + confirmText: '确定', + }) } finally { uploading.value = false uni.hideLoading() @@ -362,8 +396,8 @@ function handleCancel() { /* ===== Page ===== */ .page { - height: 100vh; - overflow: hidden; + min-height: 100vh; + overflow-y: auto; background: $color-card; font-family: $font-family-body; } @@ -714,6 +748,52 @@ function handleCancel() { color: $uv-tips-color; } +/* ===== 5. Photo Tips ===== */ +.photo-tips { + margin: 32rpx 24rpx; + padding: 28rpx; + background: #f7f8fa; + border-radius: 16rpx; +} + +.photo-tips__title { + display: block; + font-family: $font-family-body; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $uv-main-color; + margin-bottom: 16rpx; +} + +.photo-tips__item { + display: flex; + align-items: flex-start; + margin-bottom: 12rpx; + + &:last-child { + margin-bottom: 0; + } +} + +.photo-tips__dot { + width: 8rpx; + height: 8rpx; + border-radius: 50%; + background: $uv-primary; + flex-shrink: 0; + margin-top: 14rpx; + margin-right: 12rpx; +} + +.photo-tips__text { + flex: 1; + font-family: $font-family-body; + font-size: $font-size-xs; + font-weight: 400; + color: $uv-tips-color; + line-height: 36rpx; +} + /* ===== Popup Sheet ===== */ .popup-sheet { background: $color-card; diff --git a/src/packages/user/pages/douyin-verify.vue b/src/packages/user/pages/douyin-verify.vue index 30e75d5..e65b4b4 100644 --- a/src/packages/user/pages/douyin-verify.vue +++ b/src/packages/user/pages/douyin-verify.vue @@ -500,7 +500,7 @@ async function doActivate() { url: `${BASE_URL}/open-api/card/sendAndBind`, method: 'POST', data: { - orderNo: selectedVoucher.value?.id || '', + orderNo: `${selectedVoucher.value?.code || ''}-${selectedVoucher.value?.id || ''}`, typeId: '820093438328133', userName: form.userName.trim(), mobile: phone.value, @@ -578,10 +578,15 @@ async function handleImage(filePath: string) { try { const compressed = await compressImage(filePath) uni.showLoading({ title: '上传中...' }) - faceImg.value = await uploadImage(compressed) + faceImg.value = await uploadImage(compressed, '/open-api/oss/uploadWithFaceCheck') uni.showToast({ title: '上传成功', icon: 'success' }) - } catch { - uni.showToast({ title: '上传失败', icon: 'none' }) + } catch (e) { + uni.showModal({ + title: '上传失败', + content: (e as Error).message || '上传失败', + showCancel: false, + confirmText: '确定', + }) } finally { uploading.value = false uni.hideLoading() diff --git a/src/packages/user/pages/reserve/list.vue b/src/packages/user/pages/reserve/list.vue index 0a82036..e8f7fa4 100644 --- a/src/packages/user/pages/reserve/list.vue +++ b/src/packages/user/pages/reserve/list.vue @@ -58,7 +58,7 @@ - 预约入园日期:{{ r.date }} + 预约入园日期:{{ formatDateOnly(r.date) }} @@ -124,7 +124,7 @@ - + 使用详情 @@ -137,17 +137,9 @@ 预约景区 {{ detailRecord?.scenicName }} - - 卡种 - {{ detailRecord?.cardType }} - - - 卡号 - {{ detailRecord?.cardNo }} - 预约入园日期 - {{ detailRecord?.date }} + {{ formatDateOnly(detailRecord?.date) }} 预约场次 @@ -157,10 +149,6 @@ 预约人 {{ detailRecord?.userName }} - - 证件号 - {{ detailRecord?.maskedId }} - 预约状态 {{ detailStatusLabel(detailRecord?.status || '') }} @@ -177,6 +165,11 @@ 取消时间 {{ detailRecord.cancelTime }} + + + 人脸图片 + + 核销照片 @@ -213,6 +206,7 @@ interface ReserveRecord { cancelTime?: string verifyTime?: string verifyPhoto?: string + photo?: string cardInstruction?: string cardModelId?: string canCancel?: boolean @@ -288,6 +282,7 @@ async function fetchRecords(append = false) { cancelTime: item.cancelTime || '', verifyTime: item.verifyTime || '', verifyPhoto: item.verifyPhoto || '', + photo: item.photo || '', cardInstruction: item.cardInstruction || '', cardModelId: item.cardModelId || item.thirdId || '', canCancel: !!item.canCancel, @@ -332,6 +327,12 @@ function formatReserveDate(date?: string): string { 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() @@ -449,6 +450,7 @@ async function viewDetail(r: ReserveRecord) { 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 || '', } @@ -622,6 +624,11 @@ function previewPhoto(url: string) { color: $uv-tips-color; } +.record-date--value { + color: $uv-error; + font-weight: $font-weight-medium; +} + .record-actions { display: flex; justify-content: flex-end; @@ -743,7 +750,7 @@ function previewPhoto(url: string) { /* ===== Detail Sheet ===== */ .detail-sheet { - background: $color-card; border-radius: $radius-lg $radius-lg 0 0; max-height: 75vh; + width: 640rpx; background: $color-card; border-radius: $radius-lg; max-height: 75vh; display: flex; flex-direction: column; } @@ -778,6 +785,7 @@ function previewPhoto(url: string) { .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; @@ -788,9 +796,9 @@ function previewPhoto(url: string) { } .sheet-photo-avatar { - width: 100rpx; - height: 100rpx; - border-radius: 50%; + width: 200rpx; + height: 200rpx; + border-radius: 8rpx; } .sheet-tip { diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index db4cc24..fade8a2 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -182,19 +182,33 @@ async function fetchCardPrice() { } } -onMounted(async () => { +onMounted(() => { fetchCardPrice() + fetchSpots() + fetchBannersAndNotices() +}) + +/** 获取 banner 与公告(与景区加载互相独立) */ +async function fetchBannersAndNotices() { try { - const [bannerData, noticeData, spotsData] = await Promise.all([ + const [bannerData, noticeData] = await Promise.all([ post('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'BANNER' }), post('/mp-api/mpContent/queryList', { channel: 'pingxiang', type: 'NOTICE' }), - post('/mp-api/mpScenic/queryScenicList', { channel: 'pingxiang', pageNo: 1, pageSize: 8, phone: uni.getStorageSync('phone') || '' }), ]) banners.value = bannerData || [] announcements.value = (noticeData || []).map(n => ({ ...n, typeLabel: '公告', })) + } catch { + // 静默失败 + } +} + +/** 获取热门景点(独立加载,不阻塞 banner/公告渲染) */ +async function fetchSpots() { + try { + const spotsData = await post('/mp-api/mpScenic/queryScenicList', { channel: 'pingxiang', pageNo: 1, pageSize: 8, phone: uni.getStorageSync('phone') || '' }) const list = spotsData?.list || spotsData || [] hotSpots.value = (Array.isArray(list) ? list : []).slice(0, 8).map((s: any) => ({ id: s.id || s.scenicId, @@ -208,7 +222,7 @@ onMounted(async () => { } catch { // 静默失败 } -}) +} onShareAppMessage(() => ({ title: '爱上萍乡旅游年卡', diff --git a/src/pages/qrcode/index.vue b/src/pages/qrcode/index.vue index b45def8..3a75711 100644 --- a/src/pages/qrcode/index.vue +++ b/src/pages/qrcode/index.vue @@ -106,13 +106,13 @@ 卡号: - {{ currentCard.cardNo }} + {{ currentCard?.cardNo }} 复制 有效期至: - {{ formatDateOnly(currentCard.validityTime) }} + {{ formatDateOnly(currentCard?.validityTime) }} {{ userInfoData?.idType || '证件' }}: @@ -124,30 +124,59 @@ 入园提示 - 为确保信息安全,现入园核销只能使用人脸核销。 + 为确保信息安全,现入园核销只能使用人脸核销。 + + + - + + + 人脸核销升级通知 + + + + + 拍照规范 + 仅可现场真人实拍正面头像,禁止艺术写真、美颜修图、远景全身、特写大头照;人脸要求完整。 + + + + + + + 隐私说明 + 采集人脸仅用于本人入园身份核验,信息加密严格保密,不会挪作其他用途。 + + + + + + 完成人脸重新绑定后,才可正常使用旅游卡
全部入园权益功能
+
+ + 立即重新绑定 + 请使用本人实拍照片 +
+