refactor: move pages to subpackages to reduce main package size

This commit is contained in:
jacobxu666
2026-06-26 11:59:59 +08:00
parent 4b7c540343
commit 4acb2e6a9c
18 changed files with 113 additions and 108 deletions
+744
View File
@@ -0,0 +1,744 @@
<template>
<view class="page">
<!-- 1. 头部标题区 -->
<view class="header">
<view class="header-title">
<text class="header-title__bind">绑定</text>
<text class="header-title__card">赣湘全域旅游年卡/超级玩家卡</text>
</view>
<text class="header-desc">请务必填写您的真实信息,以便于景区实名制预约</text>
</view>
<!-- 2. 白色圆角表单卡片 -->
<view class="form-card">
<!-- 卡号 -->
<view class="form-row">
<text class="form-label">卡号</text>
<input
class="form-input"
v-model="form.cardNo"
placeholder="请输入卡号"
placeholder-class="placeholder"
/>
</view>
<view class="divider" />
<!-- 持卡人姓名 -->
<view class="form-row">
<text class="form-label">持卡人姓名</text>
<input
class="form-input"
v-model="form.name"
placeholder="请输入持卡人姓名"
placeholder-class="placeholder"
/>
</view>
<view class="divider" />
<!-- 持卡人手机 -->
<view class="form-row">
<text class="form-label">持卡人手机</text>
<input
class="form-input"
v-model="form.phone"
type="number"
maxlength="11"
placeholder="请输入持卡人手机号"
placeholder-class="placeholder"
/>
</view>
<view class="divider" />
<!-- 证件类型 -->
<view class="form-row" @tap="showIdTypePicker = true">
<text class="form-label">证件类型</text>
<text class="form-value">{{ docType }}</text>
<view class="chevron-right" />
</view>
<view class="divider" />
<!-- 证件号码 -->
<view class="form-row">
<text class="form-label">证件号码</text>
<input
class="form-input"
v-model="form.idCard"
:placeholder="idPlaceholder"
placeholder-class="placeholder"
:maxlength="docType === '身份证' ? 18 : 30"
/>
</view>
<view class="divider" />
<!-- 持卡人照片 -->
<view 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 v-else class="upload-placeholder">
<view class="upload-icon" />
<text class="upload-text">点击上传</text>
</view>
<view v-if="photoUrl" class="upload-remove" @tap.stop="removePhoto">
<view class="upload-remove__x" />
</view>
</view>
</view>
</view>
<!-- 3. 协议勾选区域 -->
<view class="agreement" @tap="agreed = !agreed">
<view class="checkbox" :class="{ 'checkbox--checked': agreed }">
<view v-if="agreed" class="checkbox__check" />
</view>
<view class="agreement__text">
<text class="agreement__label">我已阅读并知晓</text>
<text class="agreement__link" @tap.stop="showNotice = true">《用卡须知》</text>
</view>
</view>
<!-- 4. 底部操作按钮组 -->
<view class="btn-group">
<button
class="btn-confirm"
:class="{ 'btn-confirm--disabled': submitting }"
:disabled="submitting"
@tap="handleSubmit"
>
<text class="btn-confirm__text">{{ submitting ? '提交中...' : '确认绑卡' }}</text>
</button>
<button class="btn-cancel" @tap="handleCancel">
<text class="btn-cancel__text">取消</text>
</button>
</view>
<!-- 弹窗:证件类型选择 -->
<uni-popup ref="idTypePopup" type="bottom" :mask-click="true" @change="(e: any) => showIdTypePicker = e.show">
<view class="popup-sheet">
<view class="popup-header">
<text class="popup-title">选择证件类型</text>
<view class="popup-close" @tap="showIdTypePicker = false">
<view class="popup-close__icon" />
</view>
</view>
<view class="id-type-list">
<view
v-for="item in idTypeOptions"
:key="item"
class="id-type-item"
:class="{ 'id-type-item--active': docType === item }"
@tap="selectIdType(item)"
>
<text class="id-type-item__text">{{ item }}</text>
<view v-if="docType === item" class="id-type-item__check" />
</view>
</view>
</view>
</uni-popup>
<!-- 弹窗:用卡须知 -->
<uni-popup ref="noticePopup" type="bottom" :mask-click="true" @change="(e: any) => showNotice = e.show">
<view class="popup-sheet">
<view class="popup-header">
<text class="popup-title">用卡须知</text>
<view class="popup-close" @tap="showNotice = false">
<view class="popup-close__icon" />
</view>
</view>
<scroll-view scroll-y class="popup-body">
<text class="popup-content">1. 年卡仅限本人使用,不得转借他人。\n2. 入园时需出示有效身份证件及年卡二维码。\n3. 年卡有效期内可不限次数游览合作景区。\n4. 部分景区节假日需提前预约,请以景区公告为准。\n5. 年卡一经激活,不予退换。</text>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
const form = reactive({
cardNo: '',
name: '',
phone: '18684839400',
idCard: ''
})
const docType = ref('身份证')
const idTypeOptions = ['身份证', '护照', '港澳通行证', '台胞证']
const idPlaceholder = computed(() => {
const map: Record<string, string> = {
'身份证': '请输入身份证号码',
'护照': '请输入护照号码',
'港澳通行证': '请输入通行证号码',
'台胞证': '请输入台胞证号码'
}
return map[docType.value] || '请输入证件号码'
})
const photoUrl = ref('')
const agreed = ref(false)
const submitting = ref(false)
const showNotice = ref(false)
const showIdTypePicker = ref(false)
const noticePopup = ref<any>(null)
const idTypePopup = ref<any>(null)
watch(showNotice, (v) => {
if (v) noticePopup.value?.open()
else noticePopup.value?.close()
})
watch(showIdTypePicker, (v) => {
if (v) idTypePopup.value?.open()
else idTypePopup.value?.close()
})
function selectIdType(item: string) {
docType.value = item
showIdTypePicker.value = false
}
function choosePhoto() {
uni.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['camera'],
success: (res) => {
photoUrl.value = res.tempFiles[0].tempFilePath
}
})
}
function previewPhoto() {
uni.previewImage({ urls: [photoUrl.value] })
}
function removePhoto() {
photoUrl.value = ''
}
function validateForm() {
if (!form.cardNo.trim()) return '请输入卡号'
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 '请上传持卡人照片'
return ''
}
async function handleSubmit() {
if (!agreed.value) {
uni.showToast({ title: '请先阅读并知晓用卡须知', icon: 'none' })
return
}
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))
uni.hideLoading()
uni.showToast({ title: '绑卡成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1500)
} catch {
uni.hideLoading()
uni.showToast({ title: '绑卡失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
function handleCancel() {
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
}
</script>
<style lang="scss" scoped>
/* ===== Design Tokens ===== */
/* ===== Page ===== */
.page {
height: 100vh;
overflow: hidden;
background: $color-card;
font-family: $font-family-body;
}
/* ===== 1. Header ===== */
.header {
padding: 40rpx 32rpx 0;
text-align: center;
}
.header-title {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
line-height: 48rpx;
}
.header-title__bind {
font-family: $font-family-body;
font-size: $font-size-title;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.header-title__card {
font-family: $font-family-body;
font-size: $font-size-title;
font-weight: $font-weight-bold;
color: $uv-primary;
}
.header-desc {
display: block;
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-tips-color;
line-height: 36rpx;
margin-top: 12rpx;
text-align: center;
}
/* ===== 2. Form Card ===== */
.form-card {
margin: 24rpx 24rpx 0;
background: $color-card;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.form-row {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 32rpx;
}
.form-row--photo {
align-items: flex-start;
height: auto;
padding: 24rpx 32rpx;
}
.form-label {
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-main-color;
font-weight: 400;
line-height: 40rpx;
flex-shrink: 0;
width: 160rpx;
}
.form-label--top {
padding-top: 20rpx;
}
.form-input {
flex: 1;
height: 88rpx;
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
line-height: 40rpx;
text-align: right;
}
.placeholder {
font-family: $font-family-body;
color: $uv-placeholder-color;
font-size: $font-size-sm;
font-weight: 400;
}
.divider {
height: 1rpx;
background: $uv-border-color;
margin: 0 32rpx;
}
/* ===== Form Value + Chevron ===== */
.form-value {
flex: 1;
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
text-align: right;
}
.chevron-right {
width: 12rpx;
height: 12rpx;
border-top: 2rpx solid $uv-placeholder-color;
border-right: 2rpx solid $uv-placeholder-color;
transform: rotate(45deg);
flex-shrink: 0;
margin-left: 12rpx;
}
/* ===== Photo Upload ===== */
.upload-area {
width: 180rpx;
height: 180rpx;
border-radius: 16rpx;
background: #f7f7f7;
flex-shrink: 0;
margin-left: auto;
overflow: hidden;
position: relative;
transition: transform 0.15s ease;
&:active {
transform: scale(0.97);
}
}
.upload-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-icon {
width: 60rpx;
height: 60rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-bottom: 8rpx;
}
.upload-text {
font-family: $font-family-body;
font-size: 22rpx;
color: $uv-primary;
font-weight: 400;
}
.upload-preview {
width: 100%;
height: 100%;
}
.upload-remove {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.upload-remove__x {
width: 16rpx;
height: 16rpx;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16rpx;
height: 2rpx;
background: $color-card;
border-radius: 1rpx;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
/* ===== 3. Agreement ===== */
.agreement {
display: flex;
align-items: flex-start;
margin: 24rpx 24rpx;
}
.checkbox {
flex-shrink: 0;
width: 20rpx;
height: 20rpx;
border: 2rpx solid #dadbde;
border-radius: 50%;
margin-right: 12rpx;
margin-top: 8rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.checkbox--checked {
background: $uv-primary;
border-color: $uv-primary;
}
.checkbox__check {
width: 12rpx;
height: 12rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.agreement__text {
display: flex;
flex-wrap: wrap;
align-items: center;
line-height: 36rpx;
}
.agreement__label {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-tips-color;
}
.agreement__link {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-primary;
&:active {
opacity: 0.7;
}
}
/* ===== 4. Buttons ===== */
.btn-group {
display: flex;
gap: 24rpx;
padding: 0 24rpx;
}
.btn-confirm {
flex: 1;
height: 96rpx;
border-radius: 48rpx;
background: #3c9cff;
border: none;
padding: 0;
margin: 0;
line-height: normal;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(60, 156, 255, 0.28);
transition: all 0.2s ease;
&::after {
display: none;
}
&:active:not(.btn-confirm--disabled) {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(57, 138, 222, 0.2);
}
}
.btn-confirm--disabled {
background: $uv-bg-color;
box-shadow: none;
}
.btn-confirm__text {
font-family: $font-family-body;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: #ffffff;
letter-spacing: 2rpx;
}
.btn-confirm--disabled .btn-confirm__text {
color: $uv-placeholder-color;
}
.btn-cancel {
flex: 1;
height: 96rpx;
border-radius: 48rpx;
background: $color-card;
border: 2rpx solid #dadbde;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&::after {
display: none;
}
&:active {
transform: scale(0.98);
background: $uv-bg-color;
}
}
.btn-cancel__text {
font-family: $font-family-body;
font-size: $font-size-base;
font-weight: 400;
color: $uv-tips-color;
}
/* ===== Popup Sheet ===== */
.popup-sheet {
background: $color-card;
border-radius: 24rpx 24rpx 0 0;
max-height: 70vh;
display: flex;
flex-direction: column;
font-family: $font-family-body;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid $uv-border-color;
}
.popup-title {
font-family: $font-family-body;
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
line-height: 48rpx;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.6;
}
}
.popup-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);
}
}
.popup-body {
padding: 32rpx;
max-height: 55vh;
}
.popup-content {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
line-height: 1.8;
}
/* ===== ID Type Picker List ===== */
.id-type-list {
padding: 16rpx 0;
}
.id-type-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 96rpx;
padding: 0 32rpx;
transition: background 0.15s ease;
&:active {
background: $uv-bg-color;
}
&--active {
background: rgba(60, 156, 255, 0.04);
}
}
.id-type-item__text {
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: 400;
color: $uv-content-color;
line-height: 40rpx;
}
.id-type-item--active .id-type-item__text {
color: $uv-primary;
font-weight: 400;
}
.id-type-item__check {
width: 24rpx;
height: 24rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%233c9cff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
</style>