第一个版本
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user