You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

547 line
22 KiB

  1. <template>
  2. <view class="page">
  3. <!-- 患者基本信息(只读) -->
  4. <view class="section">
  5. <view class="section-title">
  6. <u-icon name="account-fill" size="18" color="#0e63e3" />
  7. <text>基本信息</text>
  8. </view>
  9. <view class="info-compact">
  10. <view class="info-compact-row">
  11. <text class="info-compact-item">姓名:{{ patient.name }}</text>
  12. <text class="info-compact-item">性别:{{ patient.gender }}</text>
  13. </view>
  14. <view class="info-compact-row">
  15. <text class="info-compact-item">身份证:{{ maskedIdCard }}</text>
  16. <text class="info-compact-item">手机号:{{ maskedPhone }}</text>
  17. </view>
  18. <view class="info-compact-row">
  19. <text class="info-compact-item">联系地址:{{ patient.region_text }} {{ patient.address }}</text>
  20. </view>
  21. <view class="info-compact-row">
  22. <text class="info-compact-item">医院:{{ patient.hospital || '—' }}</text>
  23. <text class="info-compact-item">癌种:{{ patient.tag || '—' }}</text>
  24. </view>
  25. </view>
  26. </view>
  27. <!-- 平台收件信息 -->
  28. <view class="section">
  29. <view class="section-title">
  30. <u-icon name="order" size="18" color="#0e63e3" />
  31. <text>平台收件信息</text>
  32. </view>
  33. <view class="receiver-info">
  34. <view class="receiver-row">
  35. <text class="receiver-label">收件地址</text>
  36. <text class="receiver-value">{{ sampleReceiverInfo.address || '—' }}</text>
  37. </view>
  38. <view class="receiver-row">
  39. <text class="receiver-label">收件人</text>
  40. <text class="receiver-value">{{ sampleReceiverInfo.receiver || '—' }}</text>
  41. </view>
  42. <view class="receiver-row">
  43. <text class="receiver-label">电话</text>
  44. <text class="receiver-value">{{ sampleReceiverInfo.phone || '—' }}</text>
  45. </view>
  46. </view>
  47. <view class="copy-receiver" @tap="copySampleReceiverInfo">复制送检信息</view>
  48. </view>
  49. <!-- 送检信息 -->
  50. <view class="section">
  51. <view class="section-title">
  52. <u-icon name="file-text" size="18" color="#fa541c" />
  53. <text>送检信息</text>
  54. <text v-if="sampleInfoStatusText" class="status-pill" :class="sampleInfoStatusClass">{{ sampleInfoStatusText }}</text>
  55. </view>
  56. <view v-if="isSampleReturned" class="return-info">
  57. <text class="return-title">样品已经寄回</text>
  58. <text class="return-text">回寄物流单号:{{ returnTrackingNo || '—' }}</text>
  59. <text v-if="returnTime" class="return-time">回寄时间:{{ returnTime }}</text>
  60. <text class="copy-return-no" @tap="copyReturnTrackingNo">复制单号</text>
  61. </view>
  62. <view class="form-group">
  63. <text class="form-label">送检样本类型{{ sampleRequired ? '' : '(选填)' }}</text>
  64. <u-checkbox-group v-model="form.sample_types" placement="row" :wrap="true" @change="onSampleTypesChange">
  65. <u-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name" :name="st.name"
  66. :disabled="isSampleInfoLocked" activeColor="#0E63E3" :customStyle="{ marginRight: '24rpx', marginBottom: '16rpx' }" />
  67. </u-checkbox-group>
  68. </view>
  69. <view class="form-group" v-if="showWaxReturn">
  70. <text class="form-label">{{ needReturnNames }}是否需寄回</text>
  71. <u-radio-group v-model="form.wax_return" placement="row">
  72. <u-radio label="是" :name="1" :disabled="isSampleInfoLocked" activeColor="#0E63E3" :customStyle="{ marginRight: '40rpx' }" />
  73. <u-radio label="否" :name="0" :disabled="isSampleInfoLocked" activeColor="#0E63E3" />
  74. </u-radio-group>
  75. </view>
  76. <template v-if="form.wax_return === 1 && showWaxReturn">
  77. <view class="form-group">
  78. <view style="display:flex;align-items:center;justify-content:space-between;">
  79. <text class="form-label" style="margin-bottom:0;">收件人姓名</text>
  80. <text :class="['fill-self-btn', isSampleInfoLocked ? 'disabled' : '']" @tap="fillSelfReturn">本人接收</text>
  81. </view>
  82. <u-input v-model="form.return_name" :disabled="isSampleInfoLocked" placeholder="请输入收件人姓名" border="surround" :customStyle="{ marginTop: '16rpx' }" />
  83. </view>
  84. <view class="form-group">
  85. <text class="form-label">收件人电话</text>
  86. <u-input v-model="form.return_phone" :disabled="isSampleInfoLocked" type="number" placeholder="请输入收件人电话" border="surround" maxlength="11" />
  87. </view>
  88. <view class="form-group">
  89. <text class="form-label">收件地址</text>
  90. <view :class="['region-row', isSampleInfoLocked ? 'disabled' : '']" @tap="openRegionPicker">
  91. <text :class="['region-text', returnRegionText ? '' : 'placeholder']">{{ returnRegionText || '请选择省/市/区' }}</text>
  92. <text class="arrow">›</text>
  93. </view>
  94. <u-input v-model="form.return_address" :disabled="isSampleInfoLocked" placeholder="详细门牌号" border="surround"
  95. :customStyle="{ marginTop: '16rpx' }" />
  96. </view>
  97. </template>
  98. <template v-if="(form.sample_types && form.sample_types.length) || sampleRequired">
  99. <view class="form-group">
  100. <text class="form-label">报告接收邮箱</text>
  101. <u-input v-model="form.report_email" :disabled="isSampleInfoLocked" placeholder="请输入邮箱地址" border="surround" />
  102. </view>
  103. <view class="form-group">
  104. <text class="form-label">送检样本物流单号</text>
  105. <u-input v-model="form.sample_tracking_no" :disabled="isSampleInfoLocked" placeholder="请输入物流单号" border="surround" />
  106. </view>
  107. <view class="form-group">
  108. <text class="form-label">送检单照片(可上传多张)</text>
  109. <view class="upload-row">
  110. <view class="upload-item" v-for="(photo, idx) in form.sample_photos" :key="'sp'+idx">
  111. <image class="upload-img" :src="photo" mode="aspectFill" @tap="previewSamplePhoto(idx)" />
  112. <view v-if="!isSampleInfoLocked" class="upload-del" @tap="form.sample_photos.splice(idx, 1)">×</view>
  113. </view>
  114. <view v-if="!isSampleInfoLocked" class="upload-box" @tap="chooseSamplePhoto">
  115. <text class="upload-icon">+</text>
  116. <text class="upload-text">上传图片</text>
  117. </view>
  118. </view>
  119. </view>
  120. </template>
  121. </view>
  122. <!-- 提交按钮 -->
  123. <view class="btn-wrap">
  124. <view class="agree-row" @tap="toggleAgree">
  125. <u-checkbox-group>
  126. <u-checkbox :checked="agreed" :disabled="isSampleInfoLocked" shape="circle" activeColor="#0E63E3" size="18" />
  127. </u-checkbox-group>
  128. <text class="agree-text">请阅读并同意</text>
  129. <text class="agree-link" @tap.stop="openNotice">《患者告知书》</text>
  130. </view>
  131. <u-button :text="submitButtonText" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" />
  132. </view>
  133. <!-- 地区选择器 -->
  134. <u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
  135. @cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />
  136. </view>
  137. </template>
  138. <script setup>
  139. import { ref, reactive, computed } from 'vue'
  140. import { onLoad } from '@dcloudio/uni-app'
  141. import { get, post, upload } from '@/utils/request.js'
  142. const patient = ref({})
  143. const form = reactive({
  144. sample_types: [],
  145. wax_return: 0,
  146. return_name: '',
  147. return_phone: '',
  148. return_province_code: '',
  149. return_city_code: '',
  150. return_district_code: '',
  151. return_address: '',
  152. report_email: '',
  153. sample_tracking_no: '',
  154. sample_photos: []
  155. })
  156. const submitting = ref(false)
  157. const agreed = ref(false)
  158. const sampleTypeList = ref([])
  159. const sampleRequired = ref(false)
  160. const sampleReceiverInfo = ref({ address: '', receiver: '', phone: '' })
  161. const sampleInfoStatus = ref(0)
  162. const returnTrackingNo = ref('')
  163. const returnTime = ref('')
  164. const isSampleReturned = computed(() => sampleInfoStatus.value === 2)
  165. const isSampleInfoLocked = computed(() => sampleInfoStatus.value === 1 || sampleInfoStatus.value === 2)
  166. const sampleInfoStatusText = computed(() => {
  167. if (sampleInfoStatus.value === 2) return '已寄回'
  168. if (sampleInfoStatus.value === 1) return '已生效'
  169. return ''
  170. })
  171. const sampleInfoStatusClass = computed(() => sampleInfoStatus.value === 2 ? 'returned' : 'effective')
  172. const submitButtonText = computed(() => {
  173. if (sampleInfoStatus.value === 2) return '样品已寄回'
  174. if (sampleInfoStatus.value === 1) return '送检信息已生效'
  175. return '提交送检信息'
  176. })
  177. // 地区数据
  178. const allRegions = ref([])
  179. const regionColumns = ref([[], [], []])
  180. const regionDefaultIndex = ref([0, 0, 0])
  181. const showRegionPicker = ref(false)
  182. const maskedIdCard = computed(() => {
  183. const v = patient.value.id_card || ''
  184. if (v.length === 18) return v.slice(0, 3) + '****' + v.slice(-4)
  185. return v
  186. })
  187. const maskedPhone = computed(() => {
  188. const v = patient.value.phone || ''
  189. if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4)
  190. return v
  191. })
  192. const showWaxReturn = computed(() => {
  193. if (!form.sample_types || !form.sample_types.length) return false
  194. return form.sample_types.some(name => {
  195. const st = sampleTypeList.value.find(s => s.name === name)
  196. return st && st.need_return
  197. })
  198. })
  199. const needReturnNames = computed(() => {
  200. if (!form.sample_types || !form.sample_types.length) return ''
  201. const names = form.sample_types.filter(name => {
  202. const st = sampleTypeList.value.find(s => s.name === name)
  203. return st && st.need_return
  204. })
  205. return names.join('、')
  206. })
  207. const returnRegionText = computed(() => {
  208. const parts = []
  209. if (form.return_province_code) {
  210. const p = allRegions.value.find(r => r.code === form.return_province_code)
  211. if (p) parts.push(p.name)
  212. }
  213. if (form.return_city_code) {
  214. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  215. if (prov && prov.children) {
  216. const c = prov.children.find(r => r.code === form.return_city_code)
  217. if (c) parts.push(c.name)
  218. }
  219. }
  220. if (form.return_district_code) {
  221. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  222. if (prov && prov.children) {
  223. const city = prov.children.find(r => r.code === form.return_city_code)
  224. if (city && city.children) {
  225. const d = city.children.find(r => r.code === form.return_district_code)
  226. if (d) parts.push(d.name)
  227. }
  228. }
  229. }
  230. return parts.join(' ')
  231. })
  232. const onSampleTypesChange = () => {
  233. if (isSampleInfoLocked.value) return
  234. if (!showWaxReturn.value) {
  235. form.wax_return = 0
  236. }
  237. if (!form.sample_types || form.sample_types.length === 0) {
  238. form.report_email = ''
  239. form.sample_tracking_no = ''
  240. form.sample_photos = []
  241. form.wax_return = 0
  242. form.return_name = ''
  243. form.return_phone = ''
  244. form.return_province_code = ''
  245. form.return_city_code = ''
  246. form.return_district_code = ''
  247. form.return_address = ''
  248. }
  249. }
  250. const fillSelfReturn = () => {
  251. if (isSampleInfoLocked.value) return showLockedTip()
  252. form.return_name = patient.value.name || ''
  253. form.return_phone = patient.value.phone || ''
  254. form.return_province_code = patient.value.province_code || ''
  255. form.return_city_code = patient.value.city_code || ''
  256. form.return_district_code = patient.value.district_code || ''
  257. form.return_address = patient.value.address || ''
  258. }
  259. const openRegionPicker = () => {
  260. if (isSampleInfoLocked.value) return showLockedTip()
  261. showRegionPicker.value = true
  262. }
  263. onLoad(async () => {
  264. await loadRegions()
  265. await loadPatientInfo()
  266. await loadSampleTypes()
  267. })
  268. const loadPatientInfo = async () => {
  269. try {
  270. const res = await get('/api/mp/sampleInfo')
  271. if (res.data) {
  272. patient.value = res.data.patient || {}
  273. // 回显送检信息
  274. form.sample_types = res.data.sample_types || []
  275. form.wax_return = res.data.wax_return || 0
  276. form.return_name = res.data.return_name || ''
  277. form.return_phone = res.data.return_phone || ''
  278. form.return_province_code = res.data.return_province_code || ''
  279. form.return_city_code = res.data.return_city_code || ''
  280. form.return_district_code = res.data.return_district_code || ''
  281. form.return_address = res.data.return_address || ''
  282. form.report_email = res.data.report_email || ''
  283. form.sample_tracking_no = res.data.sample_tracking_no || ''
  284. form.sample_photos = res.data.sample_photos || []
  285. sampleInfoStatus.value = Number(res.data.sample_info_status) || 0
  286. returnTrackingNo.value = res.data.return_tracking_no || ''
  287. returnTime.value = res.data.return_time || ''
  288. sampleReceiverInfo.value = res.data.sample_receiver_info || { address: '', receiver: '', phone: '' }
  289. if (sampleInfoStatus.value === 1) {
  290. setTimeout(() => showLockedTip(), 300)
  291. }
  292. }
  293. } catch (e) {}
  294. }
  295. const loadSampleTypes = async () => {
  296. try {
  297. const res = await get('/common/sampleTypes')
  298. sampleTypeList.value = (res.data && res.data.list) || []
  299. sampleRequired.value = (res.data && res.data.required) || false
  300. } catch (e) {}
  301. }
  302. const loadRegions = async () => {
  303. try {
  304. const res = await get('/common/regions')
  305. allRegions.value = res.data || []
  306. buildRegionColumns()
  307. } catch (e) {}
  308. }
  309. const buildRegionColumns = (pIdx = 0, cIdx = 0) => {
  310. const provinces = allRegions.value
  311. const col0 = provinces.map(p => p.name)
  312. const cities = (provinces[pIdx] && provinces[pIdx].children) || []
  313. const col1 = cities.map(c => c.name)
  314. const districts = (cities[cIdx] && cities[cIdx].children) || []
  315. const col2 = districts.map(d => d.name)
  316. regionColumns.value = [col0, col1, col2]
  317. }
  318. const onRegionChange = (e) => {
  319. const { columnIndex, index } = e
  320. if (columnIndex === 0) {
  321. buildRegionColumns(index, 0)
  322. regionDefaultIndex.value = [index, 0, 0]
  323. } else if (columnIndex === 1) {
  324. const pIdx = regionDefaultIndex.value[0]
  325. buildRegionColumns(pIdx, index)
  326. regionDefaultIndex.value = [pIdx, index, 0]
  327. }
  328. }
  329. const onRegionConfirm = (e) => {
  330. if (isSampleInfoLocked.value) return
  331. const idxs = e.indexs || e.index || [0, 0, 0]
  332. const provinces = allRegions.value
  333. const prov = provinces[idxs[0]]
  334. const city = prov && prov.children ? prov.children[idxs[1]] : null
  335. const dist = city && city.children ? city.children[idxs[2]] : null
  336. form.return_province_code = prov ? prov.code : ''
  337. form.return_city_code = city ? city.code : ''
  338. form.return_district_code = dist ? dist.code : ''
  339. showRegionPicker.value = false
  340. }
  341. const chooseSamplePhoto = () => {
  342. if (isSampleInfoLocked.value) return showLockedTip()
  343. uni.chooseImage({
  344. count: 9 - form.sample_photos.length,
  345. sizeType: ['compressed'],
  346. sourceType: ['album', 'camera'],
  347. success: async (res) => {
  348. for (const filePath of res.tempFilePaths) {
  349. try {
  350. const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' })
  351. if (uploadRes.data && uploadRes.data.url) {
  352. form.sample_photos.push(uploadRes.data.url)
  353. }
  354. } catch (e) {}
  355. }
  356. }
  357. })
  358. }
  359. const previewSamplePhoto = (idx) => {
  360. uni.previewImage({ urls: form.sample_photos, current: idx })
  361. }
  362. const openNotice = () => {
  363. uni.navigateTo({ url: '/pages/content/content?key=patient_information_sheet' })
  364. }
  365. const getContactPhone = () => sampleReceiverInfo.value.contact_phone || ''
  366. const getCleanPhone = () => getContactPhone().replace(/[^\d+]/g, '')
  367. const contactPlatform = () => {
  368. const phone = getContactPhone()
  369. if (!phone) return uni.showToast({ title: '暂无联系电话', icon: 'none' })
  370. // #ifdef H5
  371. uni.setClipboardData({
  372. data: phone,
  373. success: () => uni.showToast({ title: '电话已复制', icon: 'success' })
  374. })
  375. // #endif
  376. // #ifndef H5
  377. uni.makePhoneCall({ phoneNumber: getCleanPhone() })
  378. // #endif
  379. }
  380. const showLockedTip = () => {
  381. if (isSampleReturned.value) {
  382. uni.showModal({
  383. title: '提示',
  384. content: `样品已经寄回,回寄物流单号:${returnTrackingNo.value || '—'}。`,
  385. cancelText: '知道了',
  386. confirmText: '复制单号',
  387. success: (res) => {
  388. if (res.confirm) copyReturnTrackingNo()
  389. }
  390. })
  391. return
  392. }
  393. const phone = getContactPhone()
  394. if (!phone) {
  395. uni.showModal({
  396. title: '提示',
  397. content: '送检信息已生效,如需修改请联系平台重置修改权限。',
  398. showCancel: false,
  399. confirmText: '知道了'
  400. })
  401. return
  402. }
  403. uni.showModal({
  404. title: '提示',
  405. content: `送检信息已生效,如需修改请联系平台【${phone}】重置修改权限。`,
  406. cancelText: '知道了',
  407. confirmText: '联系平台',
  408. success: (res) => {
  409. if (res.confirm) contactPlatform()
  410. }
  411. })
  412. }
  413. const copyReturnTrackingNo = () => {
  414. if (!returnTrackingNo.value) return uni.showToast({ title: '暂无单号', icon: 'none' })
  415. uni.setClipboardData({
  416. data: returnTrackingNo.value,
  417. success: () => uni.showToast({ title: '单号已复制', icon: 'success' })
  418. })
  419. }
  420. const copySampleReceiverInfo = () => {
  421. const info = sampleReceiverInfo.value
  422. const text = [
  423. `收件地址:${info.address || ''}`,
  424. `收件人:${info.receiver || ''}`,
  425. `电话:${info.phone || ''}`
  426. ].join('\n')
  427. uni.setClipboardData({
  428. data: text,
  429. success: () => uni.showToast({ title: '已复制', icon: 'success' })
  430. })
  431. }
  432. const toggleAgree = () => {
  433. if (isSampleInfoLocked.value) return showLockedTip()
  434. agreed.value = !agreed.value
  435. }
  436. const handleSubmit = async () => {
  437. if (isSampleInfoLocked.value) {
  438. return showLockedTip()
  439. }
  440. if (!agreed.value) {
  441. return uni.showToast({ title: '请阅读并同意《患者告知书》', icon: 'none' })
  442. }
  443. if (sampleRequired.value && (!form.sample_types || form.sample_types.length === 0)) {
  444. return uni.showToast({ title: '请选择送检样本类型', icon: 'none' })
  445. }
  446. if (form.wax_return === 1 && showWaxReturn.value) {
  447. if (!form.return_name) return uni.showToast({ title: '请输入收件人姓名', icon: 'none' })
  448. if (!form.return_phone) return uni.showToast({ title: '请输入收件人电话', icon: 'none' })
  449. if (!form.return_province_code) return uni.showToast({ title: '请选择收件地址', icon: 'none' })
  450. if (!form.return_address) return uni.showToast({ title: '请输入收件详细地址', icon: 'none' })
  451. }
  452. if (form.sample_types && form.sample_types.length > 0) {
  453. if (!form.report_email) return uni.showToast({ title: '请输入报告接收邮箱', icon: 'none' })
  454. if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.report_email)) return uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
  455. if (!form.sample_tracking_no) return uni.showToast({ title: '请输入送检样本物流单号', icon: 'none' })
  456. if (!form.sample_photos || form.sample_photos.length === 0) return uni.showToast({ title: '请上传送检单照片', icon: 'none' })
  457. }
  458. submitting.value = true
  459. try {
  460. await post('/api/mp/saveSampleInfo', { ...form })
  461. sampleInfoStatus.value = 1
  462. uni.showToast({ title: '提交成功', icon: 'success' })
  463. setTimeout(() => uni.navigateBack(), 1500)
  464. } catch (e) {
  465. if (e && e.data && [1, 2].includes(Number(e.data.sample_info_status))) {
  466. sampleInfoStatus.value = Number(e.data.sample_info_status)
  467. returnTrackingNo.value = e.data.return_tracking_no || returnTrackingNo.value
  468. returnTime.value = e.data.return_time || returnTime.value
  469. if (e.data.sample_receiver_info) sampleReceiverInfo.value = e.data.sample_receiver_info
  470. return showLockedTip()
  471. }
  472. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  473. } finally {
  474. submitting.value = false
  475. }
  476. }
  477. </script>
  478. <style lang="scss" scoped>
  479. .page { min-height: 100vh; background: #f4f4f5; padding: 24rpx; padding-bottom: 260rpx; }
  480. .section { background: #fff; border-radius: 10rpx; padding: 32rpx; margin-bottom: 24rpx; border: 1rpx solid #ebeef5; }
  481. .section-title { display: flex; align-items: center; gap: 12rpx; font-size: 32rpx; font-weight: 600; color: #333; margin-bottom: 28rpx; }
  482. .status-pill { margin-left: auto; font-size: 22rpx; font-weight: 500; padding: 4rpx 14rpx; border-radius: 6rpx; &.effective { color: #52c41a; background: #f6ffed; border: 1rpx solid #b7eb8f; } &.returned { color: #0e63e3; background: #f0f5ff; border: 1rpx solid #adc6ff; } }
  483. .info-compact { padding-bottom: 8rpx; }
  484. .info-compact-row { display: flex; gap: 32rpx; margin-bottom: 12rpx; &:last-child { margin-bottom: 0; } }
  485. .info-compact-item { font-size: 26rpx; color: #666; }
  486. .return-info { background: #f0f5ff; border-left: 6rpx solid #0e63e3; border-radius: 8rpx; padding: 24rpx; margin-bottom: 20rpx; }
  487. .return-title { display: block; font-size: 30rpx; font-weight: 600; color: #303133; margin-bottom: 12rpx; }
  488. .return-text { display: block; font-size: 28rpx; color: #0e63e3; font-weight: 600; line-height: 1.5; }
  489. .return-time { display: block; font-size: 24rpx; color: #909399; margin-top: 8rpx; }
  490. .copy-return-no { display: block; text-align: right; font-size: 26rpx; color: #0e63e3; margin-top: 12rpx; }
  491. .receiver-info { padding-bottom: 8rpx; }
  492. .receiver-row { display: flex; gap: 20rpx; margin-bottom: 14rpx; &:last-child { margin-bottom: 0; } }
  493. .receiver-label { width: 120rpx; flex-shrink: 0; font-size: 26rpx; color: #909399; }
  494. .receiver-value { flex: 1; font-size: 26rpx; color: #303133; line-height: 1.6; }
  495. .copy-receiver { margin-top: 20rpx; color: #0e63e3; font-size: 26rpx; text-align: right; }
  496. .form-group { padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: none; } }
  497. .form-label { font-size: 28rpx; color: #555; margin-bottom: 16rpx; display: block; }
  498. .region-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border: 1rpx solid #ddd; border-radius: 8rpx; &.disabled { background: #f7f8fa; } }
  499. .region-text { font-size: 28rpx; color: #333; &.placeholder { color: #c0c4cc; } }
  500. .arrow { font-size: 28rpx; color: #c0c4cc; }
  501. .fill-self-btn { font-size: 24rpx; color: #0e63e3; padding: 6rpx 16rpx; border: 1rpx solid #d0e0ff; border-radius: 20rpx; background: #f0f5ff; &.disabled { color: #c0c4cc; border-color: #e4e7ed; background: #f7f8fa; } }
  502. .upload-row { display: flex; gap: 16rpx; flex-wrap: wrap; }
  503. .upload-item { position: relative; width: 180rpx; height: 180rpx; }
  504. .upload-img { width: 180rpx; height: 180rpx; border-radius: 8rpx; border: 1rpx solid #eee; }
  505. .upload-del { position: absolute; top: -10rpx; right: -10rpx; width: 40rpx; height: 40rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; display: flex; align-items: center; justify-content: center; }
  506. .upload-box { width: 180rpx; height: 180rpx; border: 2rpx dashed #ccc; border-radius: 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fafafa; }
  507. .upload-icon { font-size: 56rpx; color: #ccc; }
  508. .upload-text { font-size: 22rpx; color: #999; margin-top: 4rpx; }
  509. .btn-wrap { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 32rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.06); z-index: 100; }
  510. .agree-row { display: flex; align-items: center; margin-bottom: 16rpx; padding-left: 4rpx; }
  511. .agree-text { font-size: 24rpx; color: #666; margin-left: 8rpx; }
  512. .agree-link { font-size: 24rpx; color: #0e63e3; }
  513. </style>