feat(自主填报): 拆分驻店/跑店填报并新增跑店填报页

「自主填报」原本只有一种表单。本次拆成两种类型:

1. 新增选择填报类型页 packageUser/self-report/select
   - 按设计稿:hero 页头 + 两张类型卡(驻店填报 / 跑店填报)+ 确认选择
   - 入口(首页、我的)由直接进 create 改为先进 select

2. 原自主填报页改为「驻店填报」
   - 仅改导航标题,表单内容不变

3. 新增跑店填报页 packageUser/self-report/visit
   - 复制现有跑店/巡店任务(pages/task/detail 的 cateId=4 分支)的填报页并改造:
     保留巡店列表与下方内容,去掉列表上方的任务卡片/总进度/任务详情;
     打卡、摆台照、销量、小票、门店评分一并不保留
   - 巡店列表内「连锁筛选」「门店筛选」是两个并列的独立输入框,
     各自弹各自的选择器(不串联),样式与驻店填报逐项对齐;
     换连锁会清空已选门店
   - 每条巡店可新增/删除,独立获取定位 + 打卡拍照(最多 9 张)
   - 定位不强制(巡店任务里是强制的,这里放开)
   - 底部「保存草稿」+「提交填报」,草稿交互与驻店填报一致:
     进页面询问是否加载、提交成功后清除;storage key 与驻店分开
   - 驳回后重新提交复用同一页面(mode=resubmit)

4. 填报记录页加「驻店 / 跑店」类型切换
   - 两表分表查询,各自分页;跑店卡片展示门店数、巡店列表(门店/定位/照片)
   - 连锁与门店关键词筛选落到巡店列表的每一行

5. 药审核加「跑店填报」类型
   - 新增 AuditVisitReportItem,展开时按 id 拉详情(列表不下发 store_list)
   - 跑店没有销量小计,审核通过需手填奖金,故补奖金输入弹层

顺带重构:
- 门店解析(扁平列表 / 连锁嵌套两种结构的兼容、门店明细不入页面 data
  的性能处理)抽到 util/store-picker,create 与 visit 共用一份,
  避免两页各自维护导致解析口径不一致
This commit is contained in:
2026-10-11 22:50:15 +08:00
parent 4fa29ea4c6
commit aff1e52e3e
11 changed files with 2264 additions and 140 deletions
+33 -111
View File
@@ -1,6 +1,6 @@
<template>
<view class="self-report-page">
<AppNavbar :title="isResubmit ? '重新提交填报' : '自主填报'" showBack/>
<AppNavbar :title="isResubmit ? '重新提交填报' : '驻店填报'" showBack/>
<view class="sr-merchant-row" @tap="openChainPicker">
<view class="sr-merchant-label">连锁筛选</view>
@@ -268,6 +268,15 @@ import {
clearSalesHandoff,
cloneSalesItems
} from "@/util/sales-handoff";
import {
normalizeMerchant as sharedNormalizeMerchant,
pickFirstText as sharedPickFirstText,
unwrapList as sharedUnwrapList,
normalizeMerchantChain as sharedNormalizeMerchantChain,
groupMerchantListByChain as sharedGroupMerchantListByChain,
flattenMerchantsFromChains as sharedFlattenMerchantsFromChains,
filterMerchantStores as sharedFilterMerchantStores
} from "@/util/store-picker";
export default {
components: {Login, AppNavbar},
@@ -761,123 +770,36 @@ export default {
this.currentCity = location.city_str || ''
this.currentAddress = location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join('')
},
// 连锁/门店解析统一走 util/store-picker:跑店填报(visit.vue)用同一份逻辑,
// 两页各自维护一份迟早会出现解析口径不一致。
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
const lat = Number(item.lat ?? item.latitude ?? item.merchantLat ?? item.merchant_lat)
const lng = Number(item.lng ?? item.longitude ?? item.merchantLng ?? item.merchant_lng)
const address = [item.province, item.city, item.region, item.area, item.address].filter(Boolean).join('')
return {
id,
text,
chainId: item.chainId ?? item.chain_id ?? '',
chainName: item.chainName || item.chain || item.chainAbb || '',
lat: Number.isFinite(lat) ? lat : null,
lng: Number.isFinite(lng) ? lng : null,
province: item.province || item.province_str || '',
city: item.city || item.city_str || '',
address
}
return sharedNormalizeMerchant(item)
},
pickFirstText(source, keys = []) {
return sharedPickFirstText(source, keys)
},
unwrapList(data) {
return sharedUnwrapList(data)
},
normalizeMerchantChain(item, index) {
return sharedNormalizeMerchantChain(item, index)
},
groupMerchantListByChain(list) {
return sharedGroupMerchantListByChain(list)
},
flattenMerchantsFromChains(chainList) {
return sharedFlattenMerchantsFromChains(chainList)
},
// 本页的门店搜索关键词存在 data 里(merchantStoreSearchKeyword),
// 与 visit.vue 由调用方传入关键词的签名不同,这里保留本页签名。
filterMerchantStores(merchants) {
return sharedFilterMerchantStores(merchants, this.merchantStoreSearchKeyword)
},
normalizeChainIdForApi(rawChainId) {
const text = String(rawChainId || '').trim()
if (!text || text.startsWith('chain_')) return ''
return text
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i += 1) {
const value = source[keys[i]]
if (value !== undefined && value !== null && String(value).trim()) {
return String(value).trim()
}
}
return ''
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
for (let i = 0; i < keys.length; i += 1) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
},
normalizeMerchantChain(item, index) {
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? chainName
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
let rawChildren = []
for (let i = 0; i < childCandidates.length; i += 1) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
let merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
.map(merchant => Object.assign({}, merchant, {chainId, chainName}))
if (!merchants.length) {
const selfMerchant = this.normalizeMerchant(item)
if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
merchants = [Object.assign({}, selfMerchant, {chainId, chainName})]
}
}
return {
id: chainId,
text: chainName,
merchants
}
},
groupMerchantListByChain(list) {
const chainMap = {}
;(Array.isArray(list) ? list : []).forEach((item, index) => {
const merchant = this.normalizeMerchant(item)
if (merchant.id === undefined || merchant.id === null || String(merchant.id).trim() === '') return
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'chain_name'])
if (!chainName) return
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? chainName
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const key = String(chainId)
if (!chainMap[key]) {
chainMap[key] = {id: chainId, text: chainName, merchants: []}
}
chainMap[key].merchants.push(Object.assign({}, merchant, {chainId, chainName}))
})
return Object.keys(chainMap).map(key => chainMap[key])
},
flattenMerchantsFromChains(chainList) {
const list = []
;(Array.isArray(chainList) ? chainList : []).forEach(chain => {
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
merchants.forEach(merchant => {
if (!merchant || merchant.id === '' || merchant.id === null || merchant.id === undefined) return
list.push(Object.assign({}, merchant, {
chainId: merchant.chainId || chain.id || '',
chainName: merchant.chainName || chain.text || ''
}))
})
})
return list
},
filterMerchantStores(merchants) {
const list = Array.isArray(merchants) ? merchants : []
const keyword = (this.merchantStoreSearchKeyword || '').trim()
if (!keyword) return list
return list.filter(merchant => (merchant.text || '').includes(keyword))
},
applyMerchant(chain, merchant) {
if (!merchant) return
this.form.chainName = (chain && chain.text) || merchant.chainName || ''
+196 -15
View File
@@ -3,6 +3,16 @@
<AppNavbar title="填报记录" showBack/>
<view class="card subsection">
<view class="type-tabs">
<view
v-for="tab in typeTabs"
:key="tab.value"
:class="['tab', activeType === tab.value ? 'active' : '']"
@tap="changeType(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="status-tabs">
<view
v-for="tab in statusTabs"
@@ -45,21 +55,21 @@
<view v-for="item in filteredRecords" :key="item.id" class="record-card">
<view class="record-head">
<view>
<view class="record-title">{{ item.merchantName || '-' }}</view>
<view class="record-sub" v-if="item.chainName && item.chainName !== '-'">连锁:{{ item.chainName }}</view>
<view class="record-title">{{ cardTitle(item) }}</view>
<view class="record-sub" v-if="cardSubTitle(item)">{{ cardSubTitle(item) }}</view>
</view>
<view :class="['status-badge', statusClass(item.status)]">{{ statusText(item.status) }}</view>
</view>
<view class="record-grid">
<view class="record-kv">
<view v-if="activeType === 'store'" class="record-kv">
<text class="record-label">活动时间</text>
<text class="record-value">{{ item.activityTime || '-' }}</text>
</view>
<!-- <view class="record-kv">-->
<!-- <text class="record-label">奖金</text>-->
<!-- <text class="record-value text-red">¥{{ formatPrice(item.reward) }}</text>-->
<!-- </view>-->
<view class="record-kv">
<view v-else class="record-kv">
<text class="record-label">巡店门店数</text>
<text class="record-value">{{ item.storeCount || 0 }}家</text>
</view>
<view v-if="activeType === 'store'" class="record-kv">
<text class="record-label">评分</text>
<text class="record-value">{{ item.rating || '-' }}星</text>
</view>
@@ -77,6 +87,35 @@
<u-icon :name="isExpanded(item.id) ? 'arrow-up' : 'arrow-down'" size="20" color="#2fc6a7"/>
</view>
<view v-if="isExpanded(item.id)" class="record-detail">
<!-- 跑店填报:巡店列表(驻店填报没有门店列表,走下面的固定区块) -->
<block v-if="activeType === 'visit'">
<view v-if="!item.storeRows.length" class="detail-empty">暂无巡店记录</view>
<view v-for="(store, sIdx) in item.storeRows" :key="`store-${sIdx}`" class="store-block">
<view class="store-block-head">
<text class="store-block-title">巡店{{ sIdx + 1 }}</text>
<text class="store-block-time">{{ store.visitTime || '-' }}</text>
</view>
<view class="store-block-line">门店:{{ store.merchantName || '-' }}</view>
<view class="store-block-line">连锁:{{ store.chainName || '-' }}</view>
<view class="store-block-line">
定位:{{ store.location || '-' }}
<text v-if="store.lat && store.lng">({{ store.lat }}, {{ store.lng }})</text>
</view>
<view v-if="store.photos.length" class="media-images">
<image
v-for="(img, imgIdx) in store.photos"
:key="`store-${sIdx}-photo-${imgIdx}`"
class="media-image"
:src="img"
mode="aspectFill"
@tap="preview(img, store.photos)"
/>
</view>
<view v-else class="detail-empty">暂无巡店照片</view>
</view>
</block>
<block v-else>
<view class="record-section">
<view class="record-section-title">上下班打卡</view>
<view v-if="item.clockImages.length" class="media-images">
@@ -139,6 +178,7 @@
</view>
<view v-else class="detail-empty">暂无销量数据</view>
</view>
</block>
</view>
<view v-if="isRejected(item.status)" class="record-actions">
@@ -185,12 +225,19 @@ import AppNavbar from "@/components/AppNavbar.vue";
import Login from "@/components/Login.vue";
import {dictDataListByDictReq} from "@/api/common";
import {selfReportUserListReq} from "@/api/self-report";
import {visitReportUserListReq} from "@/api/visit-report";
import {getUserInfo} from "@/util/user";
export default {
components: {AppNavbar, Login},
data() {
return {
// 驻店填报 = yxe_self_report,跑店填报 = yxe_visit_report(两张表,分别拉取)
typeTabs: [
{label: '驻店填报', value: 'store'},
{label: '跑店填报', value: 'visit'}
],
activeType: 'store',
statusTabs: [
{label: '全部', value: 'all'},
{label: '待审核', value: 0},
@@ -226,10 +273,10 @@ export default {
if (this.activeStatus !== 'all' && Number(item.status) !== Number(this.activeStatus)) {
return false
}
if (this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
if (this.filterForm.chainId && !this.matchesItemChain(item)) {
return false
}
if (keyword && !String(item.merchantName || '').toLowerCase().includes(keyword)) {
if (keyword && !this.matchesItemKeyword(item, keyword)) {
return false
}
if ((startTs || endTs) && !Number.isFinite(item.submitTimestamp)) {
@@ -291,7 +338,8 @@ export default {
async fetchList() {
this.loading = true
try {
const {data} = await selfReportUserListReq({})
const req = this.activeType === 'visit' ? visitReportUserListReq : selfReportUserListReq
const {data} = await req({})
this.records = this.unwrapList(data)
} catch (error) {
this.records = []
@@ -321,6 +369,9 @@ export default {
}
},
normalizeRecord(item = {}) {
if (this.activeType === 'visit') {
return this.normalizeVisitRecord(item)
}
const createRaw = item.createTime || item.createdAt || item.date || ''
const clockPhotos = this.parseMaybeJson(item.clockPhotos, [])
const boothPhotos = this.parseMaybeJson(item.boothPhotos, [])
@@ -346,7 +397,48 @@ export default {
clockImages: this.normalizeMediaList(clockPhotos),
boothImages: this.normalizeMediaList(boothPhotos),
receiptImages: this.normalizeMediaList(receiptPhoto),
salesRows: this.buildSalesRows(salesGroups)
salesRows: this.buildSalesRows(salesGroups),
storeCount: 0,
storeRows: []
}
},
// 跑店填报没有单门店/活动时间/打卡/销量,卡片主体是巡店列表
normalizeVisitRecord(item = {}) {
const createRaw = item.createTime || item.createdAt || ''
const summaryRaw = this.parseMaybeJson(item.summary, item.summary || '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw || '')
const storeList = this.parseMaybeJson(item.storeList, [])
const storeRows = (Array.isArray(storeList) ? storeList : []).map(store => ({
merchantName: store && (store.merchantName || store.storeName) ? (store.merchantName || store.storeName) : '',
chainName: store && store.chainName ? store.chainName : '',
visitTime: store && (store.visitTime || store.time) ? (store.visitTime || store.time) : '',
location: store && store.location ? store.location : '',
lat: store && store.lat !== undefined ? store.lat : '',
lng: store && store.lng !== undefined ? store.lng : '',
photos: this.normalizeMediaList(store && store.photos)
}))
return {
id: item.id,
merchantId: '',
merchantName: '',
chainName: '-',
chainIdNormalized: '',
activityTime: '',
date: '',
status: item.status,
rejectReason: item.rejectReason || item.reject_reason || '',
reward: item.reward,
rating: '',
summary,
submitTime: this.formatDateTime(createRaw, createRaw || '-'),
submitTimestamp: this.parseDateToTimestamp(createRaw),
clockImages: [],
boothImages: [],
receiptImages: [],
salesRows: [],
// 门店数与主门店关键词都从列表内容推导,避免列表页再下发冗余字段
storeCount: Number(item.storeCount) || storeRows.length,
storeRows
}
},
buildSalesRows(groups) {
@@ -413,6 +505,21 @@ export default {
value
}
},
// 跑店记录没有单一门店/连锁,判定要落到巡店列表的每一行
matchesItemChain(item) {
const selectedId = this.filterForm.chainId
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
if (this.activeType === 'visit') {
return (item.storeRows || []).some(store => this.matchesSelectedChain('', store.chainName, selectedId))
}
return this.matchesSelectedChain(item.chainIdNormalized, item.chainName, selectedId)
},
matchesItemKeyword(item, keyword) {
if (this.activeType === 'visit') {
return (item.storeRows || []).some(store => String(store.merchantName || '').toLowerCase().includes(keyword))
}
return String(item.merchantName || '').toLowerCase().includes(keyword)
},
matchesSelectedChain(chainId, chainName, selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
@@ -477,9 +584,33 @@ export default {
},
handleResubmit(item) {
if (!item || item.id === undefined || item.id === null) return
uni.navigateTo({
url: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
})
const url = this.activeType === 'visit'
? `/packageUser/self-report/visit?id=${item.id}&mode=resubmit`
: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
uni.navigateTo({url})
},
cardTitle(item) {
if (this.activeType === 'visit') {
const names = (item.storeRows || [])
.map(store => store.merchantName)
.filter(Boolean)
if (names.length) {
return names.length > 2 ? `${names.slice(0, 2).join('、')} 等${names.length}家` : names.join('、')
}
return '跑店填报'
}
return item.merchantName || '-'
},
cardSubTitle(item) {
if (this.activeType === 'visit') return ''
return item.chainName && item.chainName !== '-' ? `连锁:${item.chainName}` : ''
},
changeType(value) {
if (this.activeType === value) return
this.activeType = value
this.expandedMap = {}
this.records = []
this.fetchList()
},
changeStatus(value) {
this.activeStatus = value
@@ -581,6 +712,14 @@ export default {
margin: 18rpx 20rpx 0;
}
.type-tabs {
display: flex;
gap: 12rpx;
padding-bottom: 16rpx;
margin-bottom: 16rpx;
border-bottom: 1rpx solid #eef2f7;
}
.status-tabs {
display: flex;
gap: 12rpx;
@@ -839,6 +978,48 @@ export default {
font-size: 24rpx;
}
.store-block {
padding: 18rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
background: #f9fafb;
border: 1rpx solid #eef2f7;
}
.store-block:last-child {
margin-bottom: 0;
}
.store-block-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.store-block-title {
font-size: 26rpx;
font-weight: 700;
color: #111827;
}
.store-block-time {
font-size: 22rpx;
color: #9ca3af;
}
.store-block-line {
margin-bottom: 8rpx;
font-size: 24rpx;
color: #4b5563;
line-height: 1.5;
word-break: break-all;
}
.store-block .media-images {
margin-top: 10rpx;
}
.sales-list {
display: flex;
flex-direction: column;
+268
View File
@@ -0,0 +1,268 @@
<template>
<view class="select-page">
<!-- 本页 hero 即页头(设计如此),不套 AppNavbar;
navigationStyle 是 custom,状态栏高度要自己让出来,否则内容冲顶。 -->
<view class="hero" :style="{paddingTop: `${heroPaddingTop}px`}">
<view class="hero-back" @tap="back">
<u-icon name="arrow-left" size="20" color="#0f2f52"/>
</view>
<view class="hero-text">
<view class="hero-title">自主填报</view>
<view class="hero-sub">真实记录 · 及时上报 · 共创佳绩</view>
</view>
<image class="hero-avatar" :src="heroAvatar" mode="aspectFit"/>
</view>
<view class="card block">
<view class="section-title-row">
<view class="section-title">请选择填报类型</view>
</view>
<view class="section-desc">请选择您本次要填写的工作类型</view>
<view
v-for="item in typeOptions"
:key="item.value"
:class="['type-card', form.type === item.value ? 'type-card-active' : '']"
@tap="form.type = item.value"
>
<image class="type-icon" :src="item.icon" mode="aspectFit"/>
<view class="type-body">
<view class="type-title">{{ item.title }}</view>
<view class="type-desc">{{ item.desc }}</view>
</view>
<view :class="['type-radio', form.type === item.value ? 'type-radio-active' : '']">
<view v-if="form.type === item.value" class="type-radio-dot"/>
</view>
</view>
</view>
<view class="select-actions">
<view class="primary-btn confirm" @tap="confirm">确认选择</view>
</view>
<view class="select-tip">① 请选择合适的填报类型,以便更准确地记录您的工作</view>
</view>
</template>
<script>
// 顶部插画:暂无独立设计稿资源,沿用首页自主填报入口所用的同色系插图
const HERO_AVATAR = 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/9c6c98843c2e30247355d86ea47552aa.png'
const ICON_STORE = 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/0fd31838127cd57b1c1db44f2c749353.png'
const ICON_VISIT = 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260127/f5fbb2e4be31bcc830156f174e7931b8.png'
export default {
data() {
return {
heroAvatar: HERO_AVATAR,
// 状态栏高度:本页自绘页头,得自己避让,否则内容会顶到状态栏
statusBarHeight: 0,
form: {
type: 'store'
},
typeOptions: [
{
value: 'store',
title: '驻店填报',
desc: '适用于在门店驻店,进行商品陈列、销售等工作的场景',
icon: ICON_STORE
},
{
value: 'visit',
title: '跑店填报',
desc: '适用于外出拜访门店、客户或其他工作场景',
icon: ICON_VISIT
}
]
}
},
computed: {
// 内联 padding-top 会覆盖 .hero 里 padding 简写的上边距,
// 所以这里把原有 24rpx(≈12px)一并算进去,视觉与设计稿一致。
heroPaddingTop() {
return this.statusBarHeight + 12
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
},
methods: {
back() {
if (getCurrentPages().length === 1) {
uni.switchTab({url: '/pages/index/index'})
return
}
uni.navigateBack()
},
confirm() {
const type = this.form.type
const url = type === 'visit'
? '/packageUser/self-report/visit'
: '/packageUser/self-report/create'
uni.navigateTo({
url,
fail: () => {
uni.showToast({title: '页面打开失败', icon: 'none'})
}
})
}
}
}
</script>
<style lang="scss" scoped>
.select-page {
min-height: 100vh;
background: #eef8f5;
padding-bottom: 60rpx;
}
.hero {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 40rpx 36rpx;
background: linear-gradient(180deg, #d9f5ec 0%, #eef8f5 100%);
}
/* 本页没有 AppNavbar,返回入口自绘;随流排布,避免与标题重叠 */
.hero-back {
flex-shrink: 0;
margin-right: 20rpx;
width: 64rpx;
height: 64rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.hero-text {
flex: 1;
}
.hero-title {
display: flex;
align-items: center;
font-size: 56rpx;
font-weight: 800;
color: #0f2f52;
letter-spacing: 4rpx;
}
.hero-sub {
margin-top: 14rpx;
font-size: 24rpx;
color: #5c6c84;
letter-spacing: 2rpx;
}
.hero-avatar {
width: 180rpx;
height: 150rpx;
flex-shrink: 0;
}
.block {
margin: 0 24rpx;
}
.section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-desc {
margin: -6rpx 0 24rpx;
font-size: 24rpx;
color: #9aa3b2;
}
.type-card {
display: flex;
align-items: center;
gap: 20rpx;
padding: 26rpx 22rpx;
margin-bottom: 22rpx;
border-radius: 20rpx;
border: 3rpx solid #e6eaf0;
background: #f8fafc;
}
.type-card:last-child {
margin-bottom: 0;
}
.type-card-active {
border-color: #2fc6a7;
background: #f1fbf8;
}
.type-icon {
width: 108rpx;
height: 96rpx;
flex-shrink: 0;
}
.type-body {
flex: 1;
}
.type-title {
font-size: 34rpx;
font-weight: 700;
color: #1f2937;
}
.type-desc {
margin-top: 12rpx;
font-size: 24rpx;
color: #7b8794;
line-height: 1.5;
}
.type-radio {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 3rpx solid #d3d9e2;
background: #ffffff;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.type-radio-active {
border-color: #2fc6a7;
}
.type-radio-dot {
width: 22rpx;
height: 22rpx;
border-radius: 50%;
background: #2fc6a7;
}
.select-actions {
margin: 48rpx 24rpx 0;
}
.confirm {
height: 92rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 12rpx 26rpx rgba(47, 198, 167, 0.28);
}
.select-tip {
margin-top: 26rpx;
text-align: center;
font-size: 22rpx;
color: #9aa3b2;
}
</style>
File diff suppressed because it is too large Load Diff