report-upload.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. <template>
  2. <view class="upload-container">
  3. <!-- 顶部导航 -->
  4. <view class="nav-bar">
  5. <text class="nav-back" @click="goBack">‹ 返回</text>
  6. <text class="nav-title">上传健康报告</text>
  7. <view class="nav-placeholder"></view>
  8. </view>
  9. <!-- 说明提示 -->
  10. <view class="tip-banner">
  11. <text class="tip-icon">📋</text>
  12. <text class="tip-text">上传体检报告或肠道检测报告,AI 将为您解读并提供个性化营养方案</text>
  13. </view>
  14. <!-- PDF解析预览(提取到信息时显示) -->
  15. <view class="parsed-preview" v-if="parsedInfo">
  16. <view class="parsed-title">PDF解析结果</view>
  17. <view class="parsed-row" v-if="parsedInfo.personName">
  18. <text class="parsed-label">被检测人</text>
  19. <text class="parsed-value">{{ parsedInfo.personName }}</text>
  20. </view>
  21. <view class="parsed-row" v-if="parsedInfo.reportNumber">
  22. <text class="parsed-label">报告编号</text>
  23. <text class="parsed-value">{{ parsedInfo.reportNumber }}</text>
  24. </view>
  25. <view class="parsed-row" v-if="parsedInfo.reportDate">
  26. <text class="parsed-label">报告日期</text>
  27. <text class="parsed-value">{{ parsedInfo.reportDate }}</text>
  28. </view>
  29. <!-- 匹配状态 -->
  30. <view class="match-status" v-if="parsedInfo.needManualSelect">
  31. <text class="match-icon">⚠️</text>
  32. <text class="match-text">无法自动匹配到家庭成员,请手动选择</text>
  33. </view>
  34. <view class="match-status success" v-else-if="parsedInfo.matchedMemberName">
  35. <text class="match-icon">✅</text>
  36. <text class="match-text">已自动匹配: {{ parsedInfo.matchedMemberName }}</text>
  37. </view>
  38. </view>
  39. <!-- 家庭成员选择器(需要手动选择时显示) -->
  40. <view class="form-item member-selector" v-if="showMemberSelector">
  41. <text class="form-label">选择成员</text>
  42. <view class="member-list">
  43. <view
  44. class="member-item"
  45. v-for="member in familyMembers"
  46. :key="member.id"
  47. :class="{ 'member-selected': selectedMemberId === member.userId }"
  48. @click="selectMember(member)"
  49. >
  50. <text class="member-avatar">{{ member.nickname ? member.nickname.substring(0, 1) : '?' }}</text>
  51. <text class="member-name">{{ member.nickname || '未知' }}</text>
  52. </view>
  53. </view>
  54. </view>
  55. <!-- 表单 -->
  56. <view class="form-section">
  57. <!-- 报告类型 -->
  58. <view class="form-item">
  59. <text class="form-label">报告类型</text>
  60. <view class="type-selector">
  61. <view
  62. class="type-option"
  63. :class="{ 'type-selected': form.reportType === 'physical_exam' }"
  64. @click="selectType('physical_exam')"
  65. >
  66. <text class="type-icon">🏥</text>
  67. <text class="type-name">体检报告</text>
  68. </view>
  69. <view
  70. class="type-option"
  71. :class="{ 'type-selected': form.reportType === 'gut_flora' }"
  72. @click="selectType('gut_flora')"
  73. >
  74. <text class="type-icon">🔬</text>
  75. <text class="type-name">肠道检测</text>
  76. </view>
  77. <view
  78. class="type-option"
  79. :class="{ 'type-selected': form.reportType === 'tongue' }"
  80. @click="selectType('tongue')"
  81. >
  82. <text class="type-icon">👅</text>
  83. <text class="type-name">舌诊拍照</text>
  84. </view>
  85. </view>
  86. </view>
  87. <!-- 舌诊拍照(仅 tongue 类型显示) -->
  88. <view class="form-item" v-if="isTongue">
  89. <text class="form-label">拍摄舌象</text>
  90. <view class="photo-area">
  91. <view class="photo-placeholder" v-if="!photoPath && !analyzing" @click="takePhoto">
  92. <text class="photo-icon">📷</text>
  93. <text class="photo-text">点击拍摄舌象</text>
  94. <text class="photo-sub">自然光线下拍摄舌面,避免有色光线</text>
  95. </view>
  96. <image class="photo-preview" v-else-if="photoPath && !analyzing" :src="photoPath" mode="aspectFit" @click="takePhoto"></image>
  97. <view class="photo-analyzing" v-else>
  98. <text class="analyzing-text">AI 舌象分析中...</text>
  99. </view>
  100. </view>
  101. <view class="photo-retake" v-if="photoPath && !analyzing">
  102. <text @click="takePhoto">点击重新拍摄</text>
  103. </view>
  104. </view>
  105. <!-- PDF文件上传(非舌诊类型显示) -->
  106. <view class="form-item" v-if="!isTongue">
  107. <text class="form-label">上传PDF文件(选填)</text>
  108. <view class="file-upload-area" @click="pickFile">
  109. <view class="file-upload-placeholder" v-if="!selectedFile">
  110. <text class="file-upload-icon">📄</text>
  111. <text class="file-upload-text">点击选择PDF文件</text>
  112. <text class="file-upload-hint">支持从聊天记录中选择</text>
  113. </view>
  114. <view class="file-uploading" v-else>
  115. <text class="file-upload-icon">⏳</text>
  116. <text class="file-upload-text">正在解析...</text>
  117. </view>
  118. </view>
  119. <!-- 已选择的文件 -->
  120. <view class="file-info" v-if="selectedFile && !parsingPreview">
  121. <text class="file-name">{{ selectedFile.name }}</text>
  122. <text class="file-size">{{ formatFileSize(selectedFile.size) }}</text>
  123. <text class="file-remove" @click.stop="removeFile">移除</text>
  124. </view>
  125. </view>
  126. <!-- 报告日期 -->
  127. <view class="form-item">
  128. <text class="form-label">报告日期</text>
  129. <view class="date-display" @click="showDatePicker = true">
  130. <text>{{ form.reportDate || '请选择日期' }}</text>
  131. <text class="date-arrow">›</text>
  132. </view>
  133. </view>
  134. <!-- 综合评分 -->
  135. <view class="form-item">
  136. <text class="form-label">综合评分(选填)</text>
  137. <view class="score-input-wrap">
  138. <slider
  139. class="score-slider"
  140. min="0"
  141. max="100"
  142. step="1"
  143. :value="form.overallScore"
  144. @changing="onScoreChanging"
  145. @change="onScoreChange"
  146. activeColor="#5B9BD5"
  147. backgroundColor="#E2E8F0"
  148. block-size="16"
  149. show-value
  150. />
  151. <text class="score-value">{{ form.overallScore || 0 }}分</text>
  152. </view>
  153. </view>
  154. <!-- 备注 -->
  155. <view class="form-item">
  156. <text class="form-label">备注(选填)</text>
  157. <textarea
  158. class="note-input"
  159. v-model="form.description"
  160. placeholder="请输入报告摘要或备注信息..."
  161. maxlength="500"
  162. />
  163. </view>
  164. <!-- 提交按钮 -->
  165. <view class="submit-btn" :class="{ 'btn-disabled': !canSubmit }" @click="doSubmit">
  166. <text v-if="analyzing">分析中...</text>
  167. <text v-else-if="isTongue">开始分析</text>
  168. <text v-else-if="submitting">提交中...</text>
  169. <text v-else>提交报告</text>
  170. </view>
  171. </view>
  172. <!-- 底部占位 -->
  173. <view class="bottom-spacer"></view>
  174. </view>
  175. </template>
  176. <script>
  177. import { createHealthReport, parseReportPreview } from '../../utils/api.js'
  178. import config from '@/config.js'
  179. export default {
  180. data() {
  181. return {
  182. childId: null,
  183. familyId: null,
  184. form: {
  185. reportType: 'physical_exam',
  186. reportDate: '',
  187. overallScore: 0,
  188. description: ''
  189. },
  190. showDatePicker: false,
  191. submitting: false,
  192. analyzing: false,
  193. selectedFile: null,
  194. parsingPreview: false,
  195. parsedInfo: null,
  196. showMemberSelector: false,
  197. selectedMemberId: null,
  198. familyMembers: [],
  199. // 舌诊
  200. photoPath: null,
  201. tongueResult: null
  202. }
  203. },
  204. computed: {
  205. isTongue() {
  206. return this.form.reportType === 'tongue'
  207. },
  208. canSubmit() {
  209. if (this.isTongue) return this.photoPath && !this.analyzing
  210. if (this.selectedFile) {
  211. return this.parsedInfo && this.parsedInfo.draftId && !this.parsedInfo.needManualSelect
  212. }
  213. return this.form.reportType && this.form.reportDate
  214. }
  215. },
  216. onLoad: function(options) {
  217. this.childId = options.childId ? parseInt(options.childId) : null
  218. if (options.reportType && ['physical_exam', 'gut_flora', 'tongue'].indexOf(options.reportType) !== -1) {
  219. this.form.reportType = options.reportType
  220. }
  221. // 不默认填充日期 — 待解析后从报告中提取
  222. this.form.reportDate = ''
  223. },
  224. onShow: function() {
  225. // 每次显示时从store获取familyId和家庭成员
  226. this.loadFamilyInfo()
  227. },
  228. methods: {
  229. selectType(type) {
  230. if (this.form.reportType === type) return
  231. this.form.reportType = type
  232. // 切类型时清理其他类型的状态
  233. if (type === 'tongue') {
  234. this.selectedFile = null
  235. this.parsedInfo = null
  236. } else {
  237. this.photoPath = null
  238. this.tongueResult = null
  239. this.analyzing = false
  240. }
  241. },
  242. loadFamilyInfo() {
  243. var self = this
  244. // 从Vuex获取familyId和家庭成员
  245. var userInfo = uni.getStorageSync('userInfo')
  246. if (userInfo) {
  247. try {
  248. var info = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
  249. this.familyId = info.familyId || null
  250. // 加载家庭成员列表(从本地存储或请求)
  251. var storedMembers = uni.getStorageSync('familyMembers')
  252. if (storedMembers) {
  253. try {
  254. this.familyMembers = typeof storedMembers === 'string' ? JSON.parse(storedMembers) : storedMembers
  255. } catch (e) {
  256. this.familyMembers = []
  257. }
  258. }
  259. } catch (e) {
  260. console.log('解析userInfo失败', e)
  261. }
  262. }
  263. },
  264. // ===== 舌诊拍照 =====
  265. takePhoto() {
  266. var self = this
  267. uni.chooseImage({
  268. count: 1,
  269. sourceType: ['camera', 'album'],
  270. success: function(res) {
  271. self.photoPath = res.tempFilePaths[0]
  272. self.tongueResult = null
  273. }
  274. })
  275. },
  276. analyzeTongue() {
  277. if (!this.photoPath || this.analyzing) return
  278. var self = this
  279. this.analyzing = true
  280. var memberId = this.childId || ''
  281. uni.uploadFile({
  282. url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
  283. filePath: this.photoPath,
  284. name: 'file',
  285. header: {
  286. 'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
  287. },
  288. success: function(res) {
  289. var data = JSON.parse(res.data)
  290. if (data.code === 200 && data.data) {
  291. self.tongueResult = data.data
  292. // 跳转到确认页
  293. var payload = {
  294. type: 'tongue',
  295. indicators: (data.data.indicators || []).map(function(ind) {
  296. return { code: ind.code, name: ind.name, value: ind.value }
  297. }),
  298. summary: { overallAssessment: data.data.overallAssessment || '' }
  299. }
  300. var params = 'draftId=' + data.data.recordId
  301. + '&payload=' + encodeURIComponent(JSON.stringify(payload))
  302. + '&reportType=tongue'
  303. + (self.childId ? '&childId=' + self.childId : '')
  304. + (self.familyId ? '&familyId=' + self.familyId : '')
  305. uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
  306. } else {
  307. uni.showToast({ title: data.message || '分析失败', icon: 'none' })
  308. }
  309. },
  310. fail: function() {
  311. uni.showToast({ title: '上传失败', icon: 'none' })
  312. },
  313. complete: function() {
  314. self.analyzing = false
  315. }
  316. })
  317. },
  318. onScoreChanging(e) {
  319. this.form.overallScore = e.detail.value
  320. },
  321. onScoreChange(e) {
  322. this.form.overallScore = e.detail.value
  323. },
  324. pickFile() {
  325. var self = this
  326. // 重置解析状态
  327. this.parsedInfo = null
  328. this.showMemberSelector = false
  329. this.selectedMemberId = null
  330. // 从微信聊天记录中选择文件
  331. uni.chooseMessageFile({
  332. count: 1,
  333. type: 'file',
  334. extension: ['pdf'],
  335. success: function(res) {
  336. var files = res.tempFiles
  337. if (files && files.length > 0) {
  338. self.selectedFile = files[0]
  339. // 如果选择了文件且有familyId,先预览解析
  340. if (self.familyId) {
  341. self.previewParse()
  342. }
  343. }
  344. },
  345. fail: function(e) {
  346. console.log('选择文件取消或失败', e)
  347. }
  348. })
  349. },
  350. previewParse() {
  351. var self = this
  352. if (!this.selectedFile || !this.familyId) return
  353. this.parsingPreview = true
  354. parseReportPreview(this.selectedFile.path, this.childId, this.familyId).then(function(res) {
  355. var detail = res.data
  356. self.parsedInfo = {
  357. draftId: detail.draftId,
  358. personName: detail.extractedName || '',
  359. extractedGender: detail.extractedGender || '',
  360. extractedAge: detail.extractedAge || '',
  361. reportNumber: detail.extractedReportNumber,
  362. reportDate: detail.extractedReportDate,
  363. needManualSelect: detail.needBind,
  364. payload: detail.payload || {},
  365. matchedMemberId: detail.matchedMemberId,
  366. confidence: detail.confidence,
  367. matchLabel: detail.matchLabel,
  368. candidates: detail.candidates || []
  369. }
  370. // 从解析结果自动填充报告日期
  371. if (detail.extractedReportDate) {
  372. self.form.reportDate = detail.extractedReportDate
  373. }
  374. if (detail.needBind) {
  375. self.showMemberSelector = true
  376. self.loadFamilyMembers()
  377. }
  378. // 从解析结果自动填充报告日期
  379. if (detail.extractedReportDate) {
  380. self.form.reportDate = detail.extractedReportDate
  381. }
  382. }).catch(function(e) {
  383. var msg = (e && e.message) || '解析失败'
  384. uni.showToast({ title: msg, icon: 'none' })
  385. }).finally(function() {
  386. self.parsingPreview = false
  387. })
  388. },
  389. loadFamilyMembers() {
  390. var self = this
  391. // 从Vuex store获取家庭成员
  392. var store = uni.getStorageSync('familyChildren')
  393. if (store) {
  394. try {
  395. this.familyMembers = typeof store === 'string' ? JSON.parse(store) : store
  396. } catch (e) {
  397. this.familyMembers = []
  398. }
  399. }
  400. },
  401. selectMember(member) {
  402. this.selectedMemberId = member.userId || member.id
  403. this.parsedInfo = this.parsedInfo || {}
  404. this.parsedInfo.needManualSelect = false
  405. this.showMemberSelector = false
  406. },
  407. removeFile() {
  408. this.selectedFile = null
  409. this.parsedInfo = null
  410. this.showMemberSelector = false
  411. this.selectedMemberId = null
  412. },
  413. formatFileSize(bytes) {
  414. if (!bytes) return ''
  415. if (bytes < 1024) return bytes + 'B'
  416. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
  417. return (bytes / 1024 / 1024).toFixed(1) + 'MB'
  418. },
  419. goBack() {
  420. uni.navigateBack()
  421. },
  422. doSubmit() {
  423. if (this.submitting) return
  424. var self = this
  425. // 舌诊:拍照后分析
  426. if (self.isTongue) {
  427. if (!self.photoPath) {
  428. uni.showToast({ title: '请先拍摄舌象', icon: 'none' })
  429. return
  430. }
  431. self.analyzeTongue()
  432. return
  433. }
  434. if (self.selectedFile) {
  435. if (self.parsedInfo && self.parsedInfo.needManualSelect && !self.selectedMemberId) {
  436. uni.showToast({ title: '请先选择成员', icon: 'none' })
  437. return
  438. }
  439. if (!self.parsedInfo || !self.parsedInfo.draftId) {
  440. uni.showToast({ title: '请先等待解析完成', icon: 'none' })
  441. return
  442. }
  443. var payloadStr = encodeURIComponent(JSON.stringify(self.parsedInfo.payload || {}))
  444. var candidatesStr = (self.parsedInfo.candidates && self.parsedInfo.candidates.length > 0)
  445. ? encodeURIComponent(JSON.stringify(self.parsedInfo.candidates))
  446. : ''
  447. var params = 'draftId=' + self.parsedInfo.draftId
  448. + '&extractedName=' + encodeURIComponent(self.parsedInfo.personName || '')
  449. + '&extractedGender=' + encodeURIComponent(self.parsedInfo.extractedGender || '')
  450. + '&extractedAge=' + (self.parsedInfo.extractedAge || '')
  451. + '&payload=' + payloadStr
  452. + '&needBind=' + (self.parsedInfo.needManualSelect ? 'true' : 'false')
  453. + '&matchConfidence=' + (self.parsedInfo.confidence || '')
  454. + '&matchLabel=' + encodeURIComponent(self.parsedInfo.matchLabel || '')
  455. + '&candidates=' + candidatesStr
  456. + '&extractedReportDate=' + encodeURIComponent(self.parsedInfo.reportDate || '')
  457. if (self.parsedInfo.matchedMemberId) {
  458. params += '&matchedMemberId=' + self.parsedInfo.matchedMemberId
  459. }
  460. if (self.childId) {
  461. params += '&childId=' + self.childId
  462. }
  463. if (self.familyId) {
  464. params += '&familyId=' + self.familyId
  465. }
  466. uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
  467. return
  468. }
  469. self.submitting = true
  470. uni.showLoading({ title: '提交中...' })
  471. createHealthReport({
  472. reportType: self.form.reportType,
  473. reportDate: self.form.reportDate,
  474. overallScore: self.form.overallScore,
  475. description: self.form.description
  476. }).then(function(res) {
  477. uni.hideLoading()
  478. if (res.code === 200) {
  479. uni.showToast({ title: '提交成功', icon: 'success' })
  480. var reportId = res.data && res.data.id
  481. if (reportId) {
  482. setTimeout(function() {
  483. if (self.form.reportType === 'gut_flora') {
  484. uni.redirectTo({
  485. url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (self.childId || '')
  486. })
  487. } else {
  488. uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
  489. }
  490. }, 500)
  491. } else {
  492. setTimeout(function() {
  493. uni.navigateBack()
  494. }, 500)
  495. }
  496. } else {
  497. uni.showToast({ title: res.message || '提交失败', icon: 'none' })
  498. }
  499. }).catch(function(e) {
  500. uni.hideLoading()
  501. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  502. console.log('创建报告失败', e)
  503. }).finally(function() {
  504. self.submitting = false
  505. })
  506. }
  507. }
  508. }
  509. </script>
  510. <style scoped>
  511. .upload-container {
  512. min-height: 100vh;
  513. background: #F5F7FA;
  514. }
  515. /* ===== 导航栏 ===== */
  516. .nav-bar {
  517. position: sticky;
  518. top: 0;
  519. background: #fff;
  520. display: flex;
  521. flex-direction: row;
  522. align-items: center;
  523. justify-content: space-between;
  524. padding: 20rpx 30rpx;
  525. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  526. z-index: 100;
  527. }
  528. .nav-back {
  529. font-size: 32rpx;
  530. color: #5B9BD5;
  531. }
  532. .nav-title {
  533. font-size: 32rpx;
  534. font-weight: bold;
  535. color: #333;
  536. }
  537. .nav-placeholder {
  538. width: 80rpx;
  539. }
  540. /* ===== 提示条 ===== */
  541. .tip-banner {
  542. display: flex;
  543. flex-direction: row;
  544. align-items: center;
  545. background: #E3F2FD;
  546. margin: 20rpx 30rpx;
  547. padding: 20rpx 24rpx;
  548. border-radius: 16rpx;
  549. }
  550. .tip-icon {
  551. font-size: 32rpx;
  552. margin-right: 16rpx;
  553. flex-shrink: 0;
  554. }
  555. .tip-text {
  556. font-size: 24rpx;
  557. color: #1565C0;
  558. line-height: 1.5;
  559. }
  560. /* ===== 表单 ===== */
  561. .form-section {
  562. margin: 0 30rpx;
  563. }
  564. .form-item {
  565. background: #fff;
  566. border-radius: 16rpx;
  567. padding: 24rpx;
  568. margin-bottom: 20rpx;
  569. }
  570. .form-label {
  571. font-size: 26rpx;
  572. font-weight: bold;
  573. color: #333;
  574. display: block;
  575. margin-bottom: 16rpx;
  576. }
  577. /* 类型选择 */
  578. .type-selector {
  579. display: flex;
  580. flex-direction: row;
  581. gap: 20rpx;
  582. }
  583. .type-option {
  584. flex: 1;
  585. display: flex;
  586. flex-direction: column;
  587. align-items: center;
  588. padding: 24rpx;
  589. border-radius: 16rpx;
  590. background: #F5F7FA;
  591. border: 2rpx solid transparent;
  592. }
  593. .type-selected {
  594. background: #E3F2FD;
  595. border-color: #5B9BD5;
  596. }
  597. .type-icon {
  598. font-size: 40rpx;
  599. margin-bottom: 8rpx;
  600. }
  601. .type-name {
  602. font-size: 26rpx;
  603. color: #333;
  604. font-weight: 500;
  605. }
  606. /* 文件上传 */
  607. .file-upload-area {
  608. border: 2rpx dashed #CBD5E1;
  609. border-radius: 16rpx;
  610. padding: 32rpx 20rpx;
  611. text-align: center;
  612. background: #FAFBFC;
  613. cursor: pointer;
  614. }
  615. .file-upload-area:active {
  616. background: #F1F5F9;
  617. }
  618. .file-upload-placeholder {
  619. display: flex;
  620. flex-direction: column;
  621. align-items: center;
  622. }
  623. .file-upload-icon {
  624. font-size: 48rpx;
  625. margin-bottom: 12rpx;
  626. }
  627. .file-upload-text {
  628. font-size: 28rpx;
  629. color: #5B9BD5;
  630. font-weight: 500;
  631. }
  632. .file-upload-hint {
  633. font-size: 22rpx;
  634. color: #94A3B8;
  635. margin-top: 8rpx;
  636. }
  637. .file-info {
  638. display: flex;
  639. flex-direction: row;
  640. align-items: center;
  641. background: #E3F2FD;
  642. border-radius: 12rpx;
  643. padding: 16rpx 20rpx;
  644. margin-top: 16rpx;
  645. }
  646. .file-name {
  647. flex: 1;
  648. font-size: 24rpx;
  649. color: #1565C0;
  650. overflow: hidden;
  651. text-overflow: ellipsis;
  652. white-space: nowrap;
  653. }
  654. .file-size {
  655. font-size: 22rpx;
  656. color: #64748B;
  657. margin: 0 16rpx;
  658. flex-shrink: 0;
  659. }
  660. .file-remove {
  661. font-size: 24rpx;
  662. color: #EF4444;
  663. flex-shrink: 0;
  664. padding: 4rpx 8rpx;
  665. }
  666. /* 日期 */
  667. .date-display {
  668. display: flex;
  669. flex-direction: row;
  670. align-items: center;
  671. justify-content: space-between;
  672. padding: 16rpx 20rpx;
  673. background: #F5F7FA;
  674. border-radius: 12rpx;
  675. font-size: 28rpx;
  676. color: #333;
  677. }
  678. .date-arrow {
  679. font-size: 32rpx;
  680. color: #999;
  681. }
  682. /* 评分滑块 */
  683. .score-input-wrap {
  684. display: flex;
  685. flex-direction: row;
  686. align-items: center;
  687. }
  688. .score-slider {
  689. flex: 1;
  690. }
  691. .score-value {
  692. font-size: 28rpx;
  693. font-weight: bold;
  694. color: #5B9BD5;
  695. margin-left: 16rpx;
  696. min-width: 80rpx;
  697. text-align: center;
  698. }
  699. /* 备注 */
  700. .note-input {
  701. width: 100%;
  702. min-height: 160rpx;
  703. background: #F5F7FA;
  704. border-radius: 12rpx;
  705. padding: 20rpx;
  706. font-size: 26rpx;
  707. color: #333;
  708. box-sizing: border-box;
  709. }
  710. /* 提交按钮 */
  711. .submit-btn {
  712. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  713. color: #fff;
  714. font-size: 30rpx;
  715. font-weight: bold;
  716. text-align: center;
  717. padding: 28rpx;
  718. border-radius: 16rpx;
  719. margin-top: 10rpx;
  720. }
  721. .submit-btn:active {
  722. opacity: 0.85;
  723. }
  724. .btn-disabled {
  725. opacity: 0.5;
  726. }
  727. /* ===== PDF解析预览 ===== */
  728. .parsed-preview {
  729. background: #E8F5E9;
  730. border-radius: 16rpx;
  731. margin: 0 30rpx 20rpx;
  732. padding: 20rpx 24rpx;
  733. border: 2rpx solid #A5D6A7;
  734. }
  735. .parsed-title {
  736. font-size: 24rpx;
  737. font-weight: bold;
  738. color: #2E7D32;
  739. margin-bottom: 12rpx;
  740. }
  741. .parsed-row {
  742. display: flex;
  743. flex-direction: row;
  744. align-items: center;
  745. margin-bottom: 8rpx;
  746. }
  747. .parsed-label {
  748. font-size: 24rpx;
  749. color: #388E3C;
  750. width: 140rpx;
  751. flex-shrink: 0;
  752. }
  753. .parsed-value {
  754. font-size: 24rpx;
  755. color: #1B5E20;
  756. font-weight: 500;
  757. }
  758. .match-status {
  759. display: flex;
  760. flex-direction: row;
  761. align-items: center;
  762. margin-top: 12rpx;
  763. padding: 12rpx 16rpx;
  764. background: #FFF3E0;
  765. border-radius: 8rpx;
  766. }
  767. .match-status.success {
  768. background: #E8F5E9;
  769. }
  770. .match-icon {
  771. font-size: 24rpx;
  772. margin-right: 8rpx;
  773. }
  774. .match-text {
  775. font-size: 24rpx;
  776. color: #E65100;
  777. }
  778. .match-status.success .match-text {
  779. color: #2E7D32;
  780. }
  781. /* ===== 家庭成员选择器 ===== */
  782. .member-selector {
  783. margin: 0 30rpx 20rpx;
  784. }
  785. .member-list {
  786. display: flex;
  787. flex-direction: row;
  788. flex-wrap: wrap;
  789. gap: 16rpx;
  790. }
  791. .member-item {
  792. display: flex;
  793. flex-direction: column;
  794. align-items: center;
  795. padding: 20rpx 24rpx;
  796. background: #F5F7FA;
  797. border-radius: 16rpx;
  798. border: 2rpx solid transparent;
  799. min-width: 120rpx;
  800. }
  801. .member-selected {
  802. background: #E3F2FD;
  803. border-color: #5B9BD5;
  804. }
  805. .member-avatar {
  806. width: 64rpx;
  807. height: 64rpx;
  808. border-radius: 50%;
  809. background: #5B9BD5;
  810. color: #fff;
  811. font-size: 28rpx;
  812. font-weight: bold;
  813. display: flex;
  814. align-items: center;
  815. justify-content: center;
  816. margin-bottom: 8rpx;
  817. }
  818. .member-name {
  819. font-size: 24rpx;
  820. color: #333;
  821. }
  822. /* ===== 舌诊拍照区 ===== */
  823. .photo-area {
  824. display: flex;
  825. justify-content: center;
  826. margin-bottom: 16rpx;
  827. }
  828. .photo-placeholder {
  829. width: 400rpx;
  830. height: 400rpx;
  831. border: 4rpx dashed #CBD5E1;
  832. border-radius: 24rpx;
  833. display: flex;
  834. flex-direction: column;
  835. align-items: center;
  836. justify-content: center;
  837. background: #fff;
  838. }
  839. .photo-icon {
  840. font-size: 80rpx;
  841. margin-bottom: 16rpx;
  842. }
  843. .photo-text {
  844. font-size: 28rpx;
  845. color: #64748B;
  846. }
  847. .photo-sub {
  848. font-size: 22rpx;
  849. color: #94A3B8;
  850. margin-top: 12rpx;
  851. text-align: center;
  852. padding: 0 20rpx;
  853. }
  854. .photo-preview {
  855. width: 400rpx;
  856. height: 400rpx;
  857. border-radius: 24rpx;
  858. }
  859. .photo-analyzing {
  860. width: 400rpx;
  861. height: 400rpx;
  862. border-radius: 24rpx;
  863. background: #f5f5f5;
  864. display: flex;
  865. align-items: center;
  866. justify-content: center;
  867. }
  868. .analyzing-text {
  869. font-size: 28rpx;
  870. color: #5B9BD5;
  871. }
  872. .photo-retake {
  873. text-align: center;
  874. font-size: 24rpx;
  875. color: #5B9BD5;
  876. margin-top: 8rpx;
  877. }
  878. /* ===== 底部 ===== */
  879. .bottom-spacer {
  880. height: 60rpx;
  881. }
  882. </style>