第一个版本

This commit is contained in:
lcj
2026-07-06 09:17:41 +08:00
parent 1b4dcabad2
commit 203d129ffb
37 changed files with 2625 additions and 1199 deletions
+176 -48
View File
@@ -3,10 +3,10 @@
<!-- 1. 头部标题区 -->
<view class="header">
<view class="header-title">
<text class="header-title__bind">绑定</text>
<text class="header-title__card">赣湘全域旅游年卡/超级玩家卡</text>
<text class="header-title__bind">{{ faceBindMode ? '人脸' : '绑定' }}</text>
<text class="header-title__card">{{ faceBindMode ? '绑定' : '赣湘全域旅游年卡/超级玩家卡' }}</text>
</view>
<text class="header-desc">请务必填写您的真实信息,以便于景区实名制预约</text>
<text class="header-desc">{{ faceBindMode ? '请上传持卡人照片完成人脸绑定' : '请务必填写您的真实信息,以便于景区实名制预约' }}</text>
</view>
<!-- 2. 白色圆角表单卡片 -->
@@ -19,6 +19,8 @@
v-model="form.cardNo"
placeholder="请输入卡号"
placeholder-class="placeholder"
:disabled="faceBindMode"
@blur="onCardNoBlur"
/>
</view>
<view class="divider" />
@@ -31,6 +33,7 @@
v-model="form.name"
placeholder="请输入持卡人姓名"
placeholder-class="placeholder"
:disabled="faceBindMode"
/>
</view>
<view class="divider" />
@@ -43,19 +46,19 @@
v-model="form.phone"
type="number"
maxlength="11"
placeholder="请输入持卡人手机号"
disabled
placeholder-class="placeholder"
/>
</view>
<view class="divider" />
<!-- 证件类型 -->
<view class="form-row" @tap="showIdTypePicker = true">
<!-- 证件类型(绑卡模式显示) -->
<view v-if="!faceBindMode" class="form-row" @tap="showIdTypePicker = true">
<text class="form-label">证件类型</text>
<text class="form-value">{{ docType }}</text>
<text class="form-value">{{ idTypeLabel }}</text>
<view class="chevron-right" />
</view>
<view class="divider" />
<view v-if="!faceBindMode" class="divider" />
<!-- 证件号码 -->
<view class="form-row">
@@ -65,16 +68,17 @@
v-model="form.idCard"
:placeholder="idPlaceholder"
placeholder-class="placeholder"
:maxlength="docType === '身份证' ? 18 : 30"
:maxlength="docType === '1' ? 18 : 30"
:disabled="faceBindMode"
/>
</view>
<view class="divider" />
<!-- 持卡人照片 -->
<view class="form-row form-row--photo">
<view v-if="needFaceBind" class="form-row form-row--photo">
<text class="form-label form-label--top">持卡人照片</text>
<view class="upload-area" @tap="photoUrl ? previewPhoto() : choosePhoto()">
<image v-if="photoUrl" class="upload-preview" :src="photoUrl" mode="aspectFill" />
<view class="upload-area" @tap="photoUrl ? previewPhoto() : chooseImage()">
<image v-if="photoUrl" class="upload-preview" :src="photoUrl" mode="aspectFit" />
<view v-else class="upload-placeholder">
<view class="upload-icon" />
<text class="upload-text">点击上传</text>
@@ -86,8 +90,8 @@
</view>
</view>
<!-- 3. 协议勾选区域 -->
<view class="agreement" @tap="agreed = !agreed">
<!-- 3. 协议勾选区域(绑卡模式显示) -->
<view v-if="!faceBindMode" class="agreement" @tap="agreed = !agreed">
<view class="checkbox" :class="{ 'checkbox--checked': agreed }">
<view v-if="agreed" class="checkbox__check" />
</view>
@@ -105,7 +109,7 @@
:disabled="submitting"
@tap="handleSubmit"
>
<text class="btn-confirm__text">{{ submitting ? '提交中...' : '确认绑卡' }}</text>
<text class="btn-confirm__text">{{ submitting ? '提交中...' : (faceBindMode ? '确认绑定人脸' : '确认绑卡') }}</text>
</button>
<button class="btn-cancel" @tap="handleCancel">
<text class="btn-cancel__text">取消</text>
@@ -124,13 +128,13 @@
<view class="id-type-list">
<view
v-for="item in idTypeOptions"
:key="item"
:key="item.value"
class="id-type-item"
:class="{ 'id-type-item--active': docType === item }"
@tap="selectIdType(item)"
:class="{ 'id-type-item--active': docType === item.value }"
@tap="selectIdType(item.value)"
>
<text class="id-type-item__text">{{ item }}</text>
<view v-if="docType === item" class="id-type-item__check" />
<text class="id-type-item__text">{{ item.label }}</text>
<view v-if="docType === item.value" class="id-type-item__check" />
</view>
</view>
</view>
@@ -151,28 +155,45 @@
</view>
</uni-popup>
<!-- 隐藏 canvas,用于图片压缩 -->
<canvas
canvas-id="myCanvas"
id="myCanvas"
style="position:absolute;left:0;top:-10000rpx;width:375px;height:375px;z-index:-1;opacity:0"
/>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { checkFaceFlag, uploadImage, bindCard as apiBindCard, bindFaceImg } from '../api/card'
const faceBindMode = ref(false)
const form = reactive({
cardNo: '',
name: '',
phone: '18684839400',
phone: (uni.getStorageSync('phone') as string) || '',
idCard: ''
})
const docType = ref('身份证')
const idTypeOptions = ['身份证', '护照', '港澳通行证', '台胞证']
const idTypeOptions = [
{ value: '1', label: '身份证' },
{ value: '2', label: '护照' },
{ value: '3', label: '港澳居民来往内地通行证' },
{ value: '4', label: '台湾居民来往大陆通行证' },
]
const docType = ref('1')
const idTypeLabel = computed(() => idTypeOptions.find(i => i.value === docType.value)?.label || '')
const idPlaceholder = computed(() => {
const map: Record<string, string> = {
'身份证': '请输入身份证号码',
'护照': '请输入护照号码',
'港澳通行证': '请输入通行证号码',
'台胞证': '请输入台胞证号码'
'1': '请输入身份证号码',
'2': '请输入护照号码',
'3': '请输入通行证号码',
'4': '请输入台胞证号码',
}
return map[docType.value] || '请输入证件号码'
})
@@ -180,11 +201,34 @@ const idPlaceholder = computed(() => {
const photoUrl = ref('')
const agreed = ref(false)
const submitting = ref(false)
const uploading = ref(false)
const needFaceBind = ref(false)
const showNotice = ref(false)
const showIdTypePicker = ref(false)
const noticePopup = ref<any>(null)
const idTypePopup = ref<any>(null)
onLoad(async (options) => {
if (options?.faceBind === '1') {
faceBindMode.value = true
form.cardNo = options.cardNo || ''
form.name = options.userName || ''
form.phone = options.mobile || form.phone
const idType = options.idType || ''
docType.value = idType || '1'
form.idCard = options.idNo || ''
needFaceBind.value = true
} else if (options?.redeemCode) {
form.cardNo = options.redeemCode
try {
const res = await checkFaceFlag(options.redeemCode)
needFaceBind.value = !!res
} catch {
needFaceBind.value = false
}
}
})
watch(showNotice, (v) => {
if (v) noticePopup.value?.open()
else noticePopup.value?.close()
@@ -195,22 +239,85 @@ watch(showIdTypePicker, (v) => {
else idTypePopup.value?.close()
})
function selectIdType(item: string) {
docType.value = item
function selectIdType(value: string) {
docType.value = value
showIdTypePicker.value = false
}
function choosePhoto() {
uni.chooseMedia({
async function onCardNoBlur() {
if (!form.cardNo.trim()) return
try {
const res = await checkFaceFlag(form.cardNo.trim())
needFaceBind.value = !!res
} catch {
needFaceBind.value = false
}
}
function chooseImage() {
if (uploading.value) return
uni.chooseImage({
count: 1,
mediaType: ['image'],
sourceType: ['camera'],
success: (res) => {
photoUrl.value = res.tempFiles[0].tempFilePath
sizeType: ['original'],
sourceType: ['album', 'camera'],
success: (res) => handleImage(res.tempFilePaths[0]),
fail: (err) => {
if (!err.errMsg.includes('cancel')) console.log('选择图片失败:', err)
}
})
}
async function handleImage(filePath: string) {
uploading.value = true
uni.showLoading({ title: '处理中...' })
try {
const compressedPath = await compressImage(filePath)
uni.showLoading({ title: '上传中...' })
const url = await uploadImage(compressedPath)
photoUrl.value = url
uni.showToast({ title: '上传成功', icon: 'success' })
} catch {
uni.showToast({ title: '上传失败', icon: 'none' })
} finally {
uploading.value = false
uni.hideLoading()
}
}
function compressImage(filePath: string): Promise<string> {
return new Promise((resolve) => {
const maxSize = 375
uni.getImageInfo({
src: filePath,
success: (info) => {
let { width, height } = info
if (width <= maxSize && height <= maxSize) { resolve(filePath); return }
if (width > height) {
height = Math.round((height / width) * maxSize)
width = maxSize
} else {
width = Math.round((width / height) * maxSize)
height = maxSize
}
const ctx = uni.createCanvasContext('myCanvas')
ctx.drawImage(filePath, 0, 0, width, height)
ctx.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'myCanvas', x: 0, y: 0,
width, height, destWidth: width, destHeight: height,
quality: 0.8,
success: (r) => resolve(r.tempFilePath),
fail: () => resolve(filePath),
})
}, 200)
})
},
fail: () => resolve(filePath)
})
})
}
function previewPhoto() {
uni.previewImage({ urls: [photoUrl.value] })
}
@@ -224,37 +331,58 @@ function validateForm() {
if (!form.name.trim()) return '请输入姓名'
if (!form.phone.trim()) return '请输入联系电话'
if (!/^1[3-9]\d{9}$/.test(form.phone)) return '手机号格式不正确'
if (!form.idCard.trim()) return `请输入${docType.value}号码`
if (docType.value === '身份证' && !/^\d{17}[\dXx]$/.test(form.idCard)) return '身份证号格式不正确'
if (!photoUrl.value) return '请上传持卡人照片'
if (!form.idCard.trim()) return `请输入${idTypeLabel.value}号码`
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idCard)) return '身份证号格式不正确'
if (needFaceBind.value && !photoUrl.value) return '请上传持卡人照片'
return ''
}
async function handleSubmit() {
if (!agreed.value) {
if (!faceBindMode.value && !agreed.value) {
uni.showToast({ title: '请先阅读并知晓用卡须知', icon: 'none' })
return
}
const err = validateForm()
if (err) {
uni.showToast({ title: err, icon: 'none' })
return
if (faceBindMode.value) {
if (!photoUrl.value) {
uni.showToast({ title: '请上传持卡人照片', icon: 'none' })
return
}
} else {
const err = validateForm()
if (err) {
uni.showToast({ title: err, icon: 'none' })
return
}
}
submitting.value = true
uni.showLoading({ title: '提交中...', mask: true })
try {
await new Promise(r => setTimeout(r, 1500))
const apiFn = faceBindMode.value ? bindFaceImg : apiBindCard
await apiFn({
cardNum: form.cardNo.trim(),
userName: form.name.trim(),
mobile: form.phone.trim(),
idType: docType.value,
idNo: form.idCard.trim(),
img: photoUrl.value,
smsCode: '',
thirdId: '',
bindType: 'SELF',
bindSource: 'zfb',
channel: '',
activeCode: '',
})
uni.hideLoading()
uni.showToast({ title: '绑卡成功', icon: 'success' })
uni.showToast({ title: faceBindMode.value ? '人脸绑定成功' : '绑卡成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
uni.switchTab({ url: '/pages/qrcode/index' })
}, 1500)
} catch {
} catch (err: any) {
uni.hideLoading()
uni.showToast({ title: '绑卡失败,请重试', icon: 'none' })
uni.showToast({ title: err.message || (faceBindMode.value ? '人脸绑定失败' : '绑卡失败'), icon: 'none' })
} finally {
submitting.value = false
}