feat: 新增抖音核销功能(首页入口+核销页+购卡须知弹窗)

- 首页金刚区新增「抖音核销」入口,未登录拦截跳转登录页
- 新增核销页:按手机号查券(有券单选 / 无券手输券号并校验纯数字)、绑卡信息、人脸上传、证件类型选择
- 购卡须知弹窗(单图 aspx.png),确认核销后模拟成功并跳转旅游卡页
- 核销页背景对齐设计稿改为白底,原生窗口底色同步置白
- 接口层 DOUYIN_BACKEND_READY 开关,后端未就绪时返回模拟数据
- 修正金刚区图标域名 hntgov.net -> hn12301.net

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-07-30 11:12:16 +08:00
co-authored by Claude Opus 4.8
parent 8cc76dec6c
commit d1bff755d9
4 changed files with 983 additions and 4 deletions
+98
View File
@@ -0,0 +1,98 @@
// xuchao: 抖音核销接口层。接口契约依赖后端 /mp-api/douyin/*;后端尚未提供时由 DOUYIN_BACKEND_READY 控制直接返回模拟数据以便测试。
// xuchao: 后端就绪后只需将 DOUYIN_BACKEND_READY 置为 true,即可走真实请求(无需改其它代码)。
import { post } from '../../../api/request'
// xuchao: 后端 /mp-api/douyin/* 是否已就绪。false=用模拟数据测试;true=走真实后端。
const DOUYIN_BACKEND_READY = false
/** 券码实名信息(用于回填绑卡表单) */
export interface VoucherRealnameInfo {
voucher_id: string
realname: string
card_id: string
/** 证件类型文案,如「身份证」 */
card_type: string
mobile: string
}
/** 单张券码(对齐小泥人 voucher-list 的 ret 元素) */
export interface VoucherItem {
id: string
order_id: string
/** 券码,列表展示与提交均使用此值 */
code: string
/** 1=未使用 3=已使用 5=已取消 */
status: number
mobile: string
product_name: string
merchant_name: string
voucherRealnameInfo?: VoucherRealnameInfo[]
}
export interface QueryVoucherListParams {
mobile: string
/** 1=未使用,默认查未使用 */
status?: number
}
export interface ActivateAndBindParams {
voucherId: string
voucherCode: string
quantity: number
mobile: string
userName: string
idType: string
idNo: string
faceImg: string
}
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
// xuchao: 模拟券码数据(后端未就绪时使用),含一条带实名信息用于验证回填。
const MOCK_VOUCHERS: VoucherItem[] = [
{
id: '1001',
order_id: 'DY202607280001',
code: '20260728192093019',
status: 1,
mobile: '',
product_name: '赣湘全域旅游年卡',
merchant_name: '爱上萍乡',
voucherRealnameInfo: [
{ voucher_id: '1001', realname: '张三', card_id: '360302199001011234', card_type: '身份证', mobile: '' },
],
},
{
id: '1002',
order_id: 'DY202607280002',
code: '20260728192093011',
status: 1,
mobile: '',
product_name: '赣湘全域旅游年卡',
merchant_name: '爱上萍乡',
},
]
/**
* xuchao: 查询当前手机号可核销的券码列表。
* 后端接口:POST /mp-api/douyin/voucherList,入参 { mobile, status }。
*/
export async function queryVoucherList(params: QueryVoucherListParams): Promise<VoucherItem[]> {
if (!DOUYIN_BACKEND_READY) {
await delay(300)
return MOCK_VOUCHERS.map((v) => ({ ...v, mobile: params.mobile }))
}
return post<VoucherItem[]>('/mp-api/douyin/voucherList', params)
}
/**
* xuchao: 激活且绑定(核销)。
* 后端接口:POST /mp-api/douyin/activateAndBind,入参见 ActivateAndBindParams。
*/
export async function activateAndBind(params: ActivateAndBindParams): Promise<void> {
if (!DOUYIN_BACKEND_READY) {
await delay(800)
return
}
await post<null>('/mp-api/douyin/activateAndBind', params)
}
+863
View File
@@ -0,0 +1,863 @@
<template>
<view class="page">
<!-- 头部:装饰渐变 + 标题 -->
<view class="hero">
<text class="hero-title">抖音购卡订单</text>
<view class="hero-pill">
<text class="hero-pill-text">订单核销</text>
</view>
</view>
<scroll-view scroll-y class="body">
<!-- 第 1 步:确认抖音下单信息 -->
<view class="section">
<view class="step-head">
<text class="step-no">第 1 步:</text>
<text class="step-title">确认抖音下单信息</text>
</view>
<text class="step-desc">
请确认在抖音平台认证的号码与微信手机号一致,如号码不一致请在抖音订单详情页面复制券号,并粘贴在券号输入框
</text>
<view class="field">
<text class="field-label">当前微信手机号:</text>
<view class="field-box">
<text class="field-value">{{ maskedPhone }}</text>
</view>
</view>
<view class="field">
<text class="field-label">抖音的订单券号:</text>
<!-- 有券号:单选列表 -->
<template v-if="hasVoucher">
<text class="hint-red">当前微信手机号,可核销券号如下。请选择券号核销</text>
<view
v-for="item in vouchers"
:key="item.id"
class="field-box field-box--row"
@tap="selectVoucher(item)"
>
<text class="code-text">{{ item.code }}</text>
<view class="radio" :class="{ 'radio--on': selectedId === item.id }">
<view v-if="selectedId === item.id" class="radio-dot" />
</view>
</view>
</template>
<!-- 无券号:手输券号 -->
<template v-else>
<text class="hint-red">无可核销订单!请输入订单券号核销</text>
<view class="field-box">
<input
class="field-input"
v-model="manualCode"
type="number"
placeholder="请粘贴抖音订单券号"
placeholder-class="placeholder"
/>
</view>
</template>
</view>
</view>
<!-- 第 2 步:填写绑卡信息 -->
<view class="section">
<view class="step-head">
<text class="step-no">第 2 步:</text>
<text class="step-title">填写绑卡信息</text>
</view>
<text class="step-desc">只能绑定当前微信手机号,若需绑定其他请使用其他号码登录</text>
<view class="field">
<text class="field-label">绑卡人姓名:</text>
<view class="field-box">
<input
class="field-input"
v-model="form.userName"
placeholder="请输入持卡人真实姓名"
placeholder-class="placeholder"
/>
</view>
</view>
<view class="field">
<text class="field-label">证件类型:</text>
<view class="field-box field-box--row" @tap="showIdTypePicker = true">
<text class="field-value field-value--black">{{ idTypeLabel }}</text>
<view class="chevron-down" />
</view>
</view>
<view class="field">
<text class="field-label">证件号码:</text>
<view class="field-box">
<input
class="field-input"
v-model="form.idNo"
:placeholder="idPlaceholder"
placeholder-class="placeholder"
:maxlength="docType === '1' ? 18 : 30"
/>
</view>
</view>
<view class="field">
<text class="field-label">人脸信息:</text>
<view class="face-row">
<view class="face-box" @tap="faceImg ? previewFace() : chooseImage()">
<image v-if="faceImg" class="face-preview" :src="faceImg" mode="aspectFill" />
<text v-else class="face-plus">+</text>
<view v-if="faceImg" class="face-remove" @tap.stop="faceImg = ''">
<view class="face-remove__x" />
</view>
</view>
<view class="face-tips">
<text>录入人脸仅用于园区入园身份核验。</text>
<text>禁止上传艺术写真、美颜滤镜修图照片。</text>
<text>建议拍摄光线均匀、五官完整的正面照。</text>
<text>面部无遮挡,不要逆光、画面昏暗。</text>
<text>照片不达标会导致入园识别失败。</text>
</view>
</view>
</view>
</view>
<!-- 核销&绑定规则 -->
<view class="rules">
<view class="rules-line" />
<text class="rules-title">核销&amp;绑定规则</text>
<view class="rules-body">
<text>1.旅游年卡核销成功后,券码失效,不可再次兑换不可退款。</text>
<text>2.核销绑定只能绑定在当前微信手机号。</text>
<text>3.若需要绑定其他手机号,请用其他号码登录。</text>
<text>4.人脸照片请上传人脸正面照片,不要使用艺术照。</text>
<text>5.如有疑问请联系客服:0731-88912301</text>
</view>
</view>
<view class="bottom-placeholder" />
</scroll-view>
<!-- 底部固定按钮 -->
<view class="footer-bar">
<view class="footer-btn" :class="{ 'footer-btn--disabled': submitting }" @tap="onConfirmVerify">
<text class="footer-btn-text">确认核销</text>
</view>
</view>
<!-- 购卡须知弹窗:内容为单张可滑动图片 -->
<view v-if="showNotice" class="modal-mask" @tap="showNotice = false">
<view class="modal-panel" @tap.stop>
<text class="modal-title">购卡须知</text>
<scroll-view scroll-y class="modal-scroll">
<image
class="modal-img"
src="https://aliyunoss.hn12301.net/pxmall/aspx.png"
mode="widthFix"
@tap="previewNotice"
/>
</scroll-view>
<view class="modal-divider" />
<view class="modal-footer">
<view class="modal-foot-btn" @tap="showNotice = false">
<text class="modal-foot-text">取消</text>
</view>
<view class="modal-foot-split" />
<view class="modal-foot-btn" @tap="doActivate">
<text class="modal-foot-text modal-foot-text--red">确认核销</text>
</view>
</view>
</view>
</view>
<!-- 证件类型选择(与绑卡页一致) -->
<uni-popup
ref="idTypePopup"
type="bottom"
:mask-click="true"
mask-background="transparent"
@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.value"
class="id-type-item"
:class="{ 'id-type-item--active': docType === item.value }"
@tap="selectIdType(item.value)"
>
<text class="id-type-item__text">{{ item.label }}</text>
<view v-if="docType === item.value" class="id-type-item__check" />
</view>
</view>
</view>
</uni-popup>
<!-- 隐藏 canvas,用于图片压缩 -->
<canvas
canvas-id="dyCanvas"
id="dyCanvas"
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, computed, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '../../../store/user'
import { uploadImage } from '../api/card'
import {
queryVoucherList,
activateAndBind,
type VoucherItem,
} from '../api/douyin'
const userStore = useUserStore()
const phone = ref((uni.getStorageSync('phone') as string) || '')
const maskedPhone = computed(() =>
phone.value ? phone.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '-'
)
const vouchers = ref<VoucherItem[]>([])
const selectedId = ref('')
const manualCode = ref('')
const hasVoucher = computed(() => vouchers.value.length > 0)
const selectedVoucher = computed(() => vouchers.value.find((v) => v.id === selectedId.value) || null)
const form = reactive({ userName: '', idNo: '' })
const faceImg = ref('')
const submitting = ref(false)
const showNotice = ref(false)
// xuchao: 证件类型与绑卡页 bind.vue 保持一致
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] || '请输入证件号码'
})
const showIdTypePicker = ref(false)
const idTypePopup = ref<any>(null)
watch(showIdTypePicker, (v) => (v ? idTypePopup.value?.open() : idTypePopup.value?.close()))
function selectIdType(value: string) {
docType.value = value
showIdTypePicker.value = false
}
onShow(() => {
phone.value = (uni.getStorageSync('phone') as string) || ''
if (!userStore.isLogin) {
uni.navigateTo({ url: '/pages/login/login' })
return
}
fetchVouchers()
})
/** xuchao: 按当前手机号查询可核销券码(status=1 未使用) */
async function fetchVouchers() {
if (!phone.value) {
vouchers.value = []
return
}
try {
vouchers.value = await queryVoucherList({ mobile: phone.value, status: 1 })
} catch {
vouchers.value = []
}
selectedId.value = ''
manualCode.value = ''
}
/** xuchao: 选中券码;若带实名信息则回填绑卡表单(可编辑) */
function selectVoucher(item: VoucherItem) {
selectedId.value = item.id
const info = item.voucherRealnameInfo?.[0]
if (info) {
if (info.realname) form.userName = info.realname
if (info.card_id) form.idNo = info.card_id
const matched = idTypeOptions.find((o) => o.label === info.card_type)
if (matched) docType.value = matched.value
}
}
/** 当前用于提交的券码:有券取选中项,无券取手输 */
function currentCode(): string {
return hasVoucher.value ? selectedVoucher.value?.code || '' : manualCode.value.trim()
}
/** xuchao: 校验表单,返回错误文案,空串表示通过 */
function validate(): string {
if (hasVoucher.value) {
if (!selectedId.value) return '请选择要核销的券号'
} else {
const code = manualCode.value.trim()
if (!code) return '请输入订单券号'
if (!/^\d+$/.test(code)) return '券号格式不正确,仅限数字'
}
if (!form.userName.trim()) return '请输入绑卡人姓名'
if (!form.idNo.trim()) return '请输入证件号码'
if (docType.value === '1' && !/^\d{17}[\dXx]$/.test(form.idNo.trim())) return '身份证号格式不正确'
if (!faceImg.value) return '请上传人脸照片'
return ''
}
/** 点击底部确认核销:校验通过后弹出购卡须知 */
function onConfirmVerify() {
if (submitting.value) return
const err = validate()
if (err) {
uni.showToast({ title: err, icon: 'none' })
return
}
showNotice.value = true
}
/** 弹窗内确认核销:调用激活且绑定接口 */
async function doActivate() {
if (submitting.value) return
submitting.value = true
uni.showLoading({ title: '核销中...', mask: true })
try {
await activateAndBind({
voucherId: selectedVoucher.value?.id || '',
voucherCode: currentCode(),
quantity: 1,
mobile: phone.value,
userName: form.userName.trim(),
idType: docType.value,
idNo: form.idNo.trim(),
faceImg: faceImg.value,
})
uni.hideLoading()
showNotice.value = false
uni.showToast({ title: '核销成功', icon: 'success' })
setTimeout(() => uni.switchTab({ url: '/pages/qrcode/index' }), 1200)
} catch (e) {
// xuchao: 真实请求的错误提示由 request.ts 统一弹出,此处仅收尾,避免重复 toast
console.error('[douyin] activateAndBind failed:', e)
uni.hideLoading()
} finally {
submitting.value = false
}
}
/** 选择人脸照片 */
function chooseImage() {
uni.chooseImage({
count: 1,
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) {
uni.showLoading({ title: '处理中...' })
try {
const compressed = await compressImage(filePath)
uni.showLoading({ title: '上传中...' })
faceImg.value = await uploadImage(compressed)
uni.showToast({ title: '上传成功', icon: 'success' })
} catch {
uni.showToast({ title: '上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
/** xuchao: 等比压缩到 375px 内,复用绑卡页思路 */
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('dyCanvas')
ctx.drawImage(filePath, 0, 0, width, height)
ctx.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'dyCanvas',
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 previewFace() {
if (faceImg.value) uni.previewImage({ urls: [faceImg.value] })
}
function previewNotice() {
uni.previewImage({ urls: ['https://aliyunoss.hn12301.net/pxmall/aspx.png'] })
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #fff;
font-family: $font-family-body;
}
/* ===== Hero ===== */
.hero {
padding: 48rpx 40rpx 56rpx;
background: linear-gradient(180deg, #fff4e6 0%, #fdeede 55%, #fff 100%);
}
.hero-title {
display: block;
font-family: $font-family-display;
font-size: 48rpx;
font-weight: $font-weight-bold;
color: $uv-main-color;
line-height: 1.2;
}
.hero-pill {
display: inline-flex;
align-items: center;
margin-top: 20rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx 0 0 10rpx;
background: linear-gradient(90deg, #fac28a 0%, #ff4c17 100%);
}
.hero-pill-text {
font-family: $font-family-display;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: #fff;
}
/* ===== Body / Section ===== */
.body {
height: calc(100vh - 240rpx);
}
.section {
padding: 8rpx 32rpx 24rpx;
}
.step-head {
display: flex;
align-items: center;
gap: 12rpx;
}
.step-no {
font-family: $font-family-display;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: $uv-primary;
}
.step-title {
font-family: $font-family-display;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.step-desc {
display: block;
margin-top: 12rpx;
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 36rpx;
}
/* ===== Fields ===== */
.field {
margin-top: 28rpx;
}
.field-label {
display: block;
font-size: $font-size-sm;
color: $uv-main-color;
line-height: 48rpx;
}
.hint-red {
display: block;
margin-bottom: 6rpx;
font-size: $font-size-sm;
color: #ff383c;
line-height: 48rpx;
}
.field-box {
display: flex;
align-items: center;
min-height: 80rpx;
padding: 16rpx 24rpx;
background: $uv-bg-color;
border-radius: 12rpx;
}
.field-box + .field-box {
margin-top: 12rpx;
}
.field-box--row {
justify-content: space-between;
}
.field-value {
font-size: $font-size-sm;
color: $uv-tips-color;
}
.field-value--black {
color: $uv-main-color;
}
.field-input {
flex: 1;
font-size: $font-size-sm;
color: $uv-main-color;
}
.placeholder {
color: $uv-placeholder-color;
font-size: $font-size-sm;
}
.code-text {
flex: 1;
font-size: $font-size-sm;
color: $uv-tips-color;
}
/* ===== Radio ===== */
.radio {
width: 34rpx;
height: 34rpx;
border: 2rpx solid $uv-light-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.radio--on {
border-color: $uv-primary;
}
.radio-dot {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: $uv-primary;
}
/* ===== Chevron down ===== */
.chevron-down {
width: 14rpx;
height: 14rpx;
border-top: 2rpx solid $uv-placeholder-color;
border-right: 2rpx solid $uv-placeholder-color;
transform: rotate(135deg);
flex-shrink: 0;
}
/* ===== Face upload ===== */
.face-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.face-box {
position: relative;
width: 170rpx;
height: 170rpx;
flex-shrink: 0;
background: $uv-bg-color;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.face-plus {
font-size: 56rpx;
color: $uv-placeholder-color;
line-height: 1;
}
.face-preview {
width: 100%;
height: 100%;
}
.face-remove {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 36rpx;
height: 36rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.face-remove__x {
position: relative;
width: 16rpx;
height: 16rpx;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16rpx;
height: 2rpx;
background: #fff;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
.face-tips {
display: flex;
flex-direction: column;
flex: 1;
text {
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 36rpx;
}
}
/* ===== Rules ===== */
.rules {
margin: 16rpx 32rpx 0;
}
.rules-line {
height: 1rpx;
background: $uv-border-color;
}
.rules-title {
display: block;
margin-top: 24rpx;
text-align: center;
font-family: $font-family-display;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.rules-body {
margin-top: 12rpx;
display: flex;
flex-direction: column;
text {
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 40rpx;
}
}
.bottom-placeholder {
height: 40rpx;
}
/* ===== Footer bar ===== */
.footer-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: $z-sticky;
background: $uv-primary;
padding-bottom: env(safe-area-inset-bottom);
}
.footer-btn {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.footer-btn--disabled {
opacity: 0.6;
}
.footer-btn-text {
font-family: $font-family-display;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
color: #fff;
}
/* ===== Notice modal ===== */
.modal-mask {
position: fixed;
inset: 0;
z-index: $z-modal;
background: rgba(120, 120, 120, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.modal-panel {
width: 662rpx;
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
flex-direction: column;
}
.modal-title {
display: block;
text-align: center;
font-family: $font-family-display;
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $uv-main-color;
margin-bottom: 24rpx;
}
.modal-scroll {
height: 700rpx;
}
.modal-img {
width: 100%;
display: block;
}
.modal-divider {
height: 1rpx;
background: $uv-border-color;
margin-top: 24rpx;
}
.modal-footer {
display: flex;
align-items: center;
}
.modal-foot-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.modal-foot-split {
width: 1rpx;
height: 48rpx;
background: $uv-border-color;
}
.modal-foot-text {
font-size: $font-size-sm;
color: $uv-main-color;
&--red {
color: #ff383c;
}
}
/* ===== ID type popup ===== */
.popup-sheet {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid $uv-border-color;
}
.popup-title {
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.popup-close__icon {
position: relative;
width: 20rpx;
height: 20rpx;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20rpx;
height: 3rpx;
background: $uv-tips-color;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
.id-type-list {
padding: 16rpx 0;
}
.id-type-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 96rpx;
padding: 0 32rpx;
&--active {
background: rgba(60, 156, 255, 0.04);
}
}
.id-type-item__text {
font-size: $font-size-sm;
color: $uv-content-color;
}
.id-type-item--active .id-type-item__text {
color: $uv-primary;
}
.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>