report-upload.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  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. if (detail.needBind) {
  371. self.showMemberSelector = true
  372. self.loadFamilyMembers()
  373. }
  374. // 从解析结果自动填充报告日期
  375. if (detail.extractedReportDate) {
  376. self.form.reportDate = detail.extractedReportDate
  377. }
  378. }).catch(function(e) {
  379. var msg = (e && e.message) || '解析失败'
  380. uni.showToast({ title: msg, icon: 'none' })
  381. }).finally(function() {
  382. self.parsingPreview = false
  383. })
  384. },
  385. loadFamilyMembers() {
  386. var self = this
  387. // 从Vuex store获取家庭成员
  388. var store = uni.getStorageSync('familyChildren')
  389. if (store) {
  390. try {
  391. this.familyMembers = typeof store === 'string' ? JSON.parse(store) : store
  392. } catch (e) {
  393. this.familyMembers = []
  394. }
  395. }
  396. },
  397. selectMember(member) {
  398. this.selectedMemberId = member.userId || member.id
  399. this.parsedInfo = this.parsedInfo || {}
  400. this.parsedInfo.needManualSelect = false
  401. this.showMemberSelector = false
  402. },
  403. removeFile() {
  404. this.selectedFile = null
  405. this.parsedInfo = null
  406. this.showMemberSelector = false
  407. this.selectedMemberId = null
  408. },
  409. formatFileSize(bytes) {
  410. if (!bytes) return ''
  411. if (bytes < 1024) return bytes + 'B'
  412. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
  413. return (bytes / 1024 / 1024).toFixed(1) + 'MB'
  414. },
  415. goBack() {
  416. uni.navigateBack()
  417. },
  418. doSubmit() {
  419. if (this.submitting) return
  420. var self = this
  421. // 舌诊:拍照后分析
  422. if (self.isTongue) {
  423. if (!self.photoPath) {
  424. uni.showToast({ title: '请先拍摄舌象', icon: 'none' })
  425. return
  426. }
  427. self.analyzeTongue()
  428. return
  429. }
  430. if (self.selectedFile) {
  431. if (self.parsedInfo && self.parsedInfo.needManualSelect && !self.selectedMemberId) {
  432. uni.showToast({ title: '请先选择成员', icon: 'none' })
  433. return
  434. }
  435. if (!self.parsedInfo || !self.parsedInfo.draftId) {
  436. uni.showToast({ title: '请先等待解析完成', icon: 'none' })
  437. return
  438. }
  439. var payloadStr = encodeURIComponent(JSON.stringify(self.parsedInfo.payload || {}))
  440. var candidatesStr = (self.parsedInfo.candidates && self.parsedInfo.candidates.length > 0)
  441. ? encodeURIComponent(JSON.stringify(self.parsedInfo.candidates))
  442. : ''
  443. var params = 'draftId=' + self.parsedInfo.draftId
  444. + '&extractedName=' + encodeURIComponent(self.parsedInfo.personName || '')
  445. + '&extractedGender=' + encodeURIComponent(self.parsedInfo.extractedGender || '')
  446. + '&extractedAge=' + (self.parsedInfo.extractedAge || '')
  447. + '&payload=' + payloadStr
  448. + '&needBind=' + (self.parsedInfo.needManualSelect ? 'true' : 'false')
  449. + '&matchConfidence=' + (self.parsedInfo.confidence || '')
  450. + '&matchLabel=' + encodeURIComponent(self.parsedInfo.matchLabel || '')
  451. + '&candidates=' + candidatesStr
  452. + '&extractedReportDate=' + encodeURIComponent(self.parsedInfo.reportDate || '')
  453. if (self.parsedInfo.matchedMemberId) {
  454. params += '&matchedMemberId=' + self.parsedInfo.matchedMemberId
  455. }
  456. if (self.childId) {
  457. params += '&childId=' + self.childId
  458. }
  459. if (self.familyId) {
  460. params += '&familyId=' + self.familyId
  461. }
  462. uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
  463. return
  464. }
  465. self.submitting = true
  466. uni.showLoading({ title: '提交中...' })
  467. createHealthReport({
  468. reportType: self.form.reportType,
  469. reportDate: self.form.reportDate,
  470. overallScore: self.form.overallScore,
  471. description: self.form.description
  472. }).then(function(res) {
  473. uni.hideLoading()
  474. if (res.code === 200) {
  475. uni.showToast({ title: '提交成功', icon: 'success' })
  476. var reportId = res.data && res.data.id
  477. if (reportId) {
  478. setTimeout(function() {
  479. // 需要调研问卷的类型走问卷页
  480. var surveyTypes = ['gut_flora', 'physical_exam', 'blood_test', 'urine_test']
  481. if (surveyTypes.indexOf(self.form.reportType) !== -1) {
  482. uni.redirectTo({
  483. url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (self.childId || '')
  484. })
  485. } else {
  486. uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
  487. }
  488. }, 500)
  489. } else {
  490. setTimeout(function() {
  491. uni.navigateBack()
  492. }, 500)
  493. }
  494. } else {
  495. uni.showToast({ title: res.message || '提交失败', icon: 'none' })
  496. }
  497. }).catch(function(e) {
  498. uni.hideLoading()
  499. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  500. console.log('创建报告失败', e)
  501. }).finally(function() {
  502. self.submitting = false
  503. })
  504. }
  505. }
  506. }
  507. </script>
  508. <style scoped>
  509. .upload-container {
  510. min-height: 100vh;
  511. background: #F5F7FA;
  512. }
  513. /* ===== 导航栏 ===== */
  514. .nav-bar {
  515. position: sticky;
  516. top: 0;
  517. background: #fff;
  518. display: flex;
  519. flex-direction: row;
  520. align-items: center;
  521. justify-content: space-between;
  522. padding: 20rpx 30rpx;
  523. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  524. z-index: 100;
  525. }
  526. .nav-back {
  527. font-size: 32rpx;
  528. color: #5B9BD5;
  529. }
  530. .nav-title {
  531. font-size: 32rpx;
  532. font-weight: bold;
  533. color: #333;
  534. }
  535. .nav-placeholder {
  536. width: 80rpx;
  537. }
  538. /* ===== 提示条 ===== */
  539. .tip-banner {
  540. display: flex;
  541. flex-direction: row;
  542. align-items: center;
  543. background: #E3F2FD;
  544. margin: 20rpx 30rpx;
  545. padding: 20rpx 24rpx;
  546. border-radius: 16rpx;
  547. }
  548. .tip-icon {
  549. font-size: 32rpx;
  550. margin-right: 16rpx;
  551. flex-shrink: 0;
  552. }
  553. .tip-text {
  554. font-size: 24rpx;
  555. color: #1565C0;
  556. line-height: 1.5;
  557. }
  558. /* ===== 表单 ===== */
  559. .form-section {
  560. margin: 0 30rpx;
  561. }
  562. .form-item {
  563. background: #fff;
  564. border-radius: 16rpx;
  565. padding: 24rpx;
  566. margin-bottom: 20rpx;
  567. }
  568. .form-label {
  569. font-size: 26rpx;
  570. font-weight: bold;
  571. color: #333;
  572. display: block;
  573. margin-bottom: 16rpx;
  574. }
  575. /* 类型选择 */
  576. .type-selector {
  577. display: flex;
  578. flex-direction: row;
  579. gap: 20rpx;
  580. }
  581. .type-option {
  582. flex: 1;
  583. display: flex;
  584. flex-direction: column;
  585. align-items: center;
  586. padding: 24rpx;
  587. border-radius: 16rpx;
  588. background: #F5F7FA;
  589. border: 2rpx solid transparent;
  590. }
  591. .type-selected {
  592. background: #E3F2FD;
  593. border-color: #5B9BD5;
  594. }
  595. .type-icon {
  596. font-size: 40rpx;
  597. margin-bottom: 8rpx;
  598. }
  599. .type-name {
  600. font-size: 26rpx;
  601. color: #333;
  602. font-weight: 500;
  603. }
  604. /* 文件上传 */
  605. .file-upload-area {
  606. border: 2rpx dashed #CBD5E1;
  607. border-radius: 16rpx;
  608. padding: 32rpx 20rpx;
  609. text-align: center;
  610. background: #FAFBFC;
  611. cursor: pointer;
  612. }
  613. .file-upload-area:active {
  614. background: #F1F5F9;
  615. }
  616. .file-upload-placeholder {
  617. display: flex;
  618. flex-direction: column;
  619. align-items: center;
  620. }
  621. .file-upload-icon {
  622. font-size: 48rpx;
  623. margin-bottom: 12rpx;
  624. }
  625. .file-upload-text {
  626. font-size: 28rpx;
  627. color: #5B9BD5;
  628. font-weight: 500;
  629. }
  630. .file-upload-hint {
  631. font-size: 22rpx;
  632. color: #94A3B8;
  633. margin-top: 8rpx;
  634. }
  635. .file-info {
  636. display: flex;
  637. flex-direction: row;
  638. align-items: center;
  639. background: #E3F2FD;
  640. border-radius: 12rpx;
  641. padding: 16rpx 20rpx;
  642. margin-top: 16rpx;
  643. }
  644. .file-name {
  645. flex: 1;
  646. font-size: 24rpx;
  647. color: #1565C0;
  648. overflow: hidden;
  649. text-overflow: ellipsis;
  650. white-space: nowrap;
  651. }
  652. .file-size {
  653. font-size: 22rpx;
  654. color: #64748B;
  655. margin: 0 16rpx;
  656. flex-shrink: 0;
  657. }
  658. .file-remove {
  659. font-size: 24rpx;
  660. color: #EF4444;
  661. flex-shrink: 0;
  662. padding: 4rpx 8rpx;
  663. }
  664. /* 日期 */
  665. .date-display {
  666. display: flex;
  667. flex-direction: row;
  668. align-items: center;
  669. justify-content: space-between;
  670. padding: 16rpx 20rpx;
  671. background: #F5F7FA;
  672. border-radius: 12rpx;
  673. font-size: 28rpx;
  674. color: #333;
  675. }
  676. .date-arrow {
  677. font-size: 32rpx;
  678. color: #999;
  679. }
  680. /* 评分滑块 */
  681. .score-input-wrap {
  682. display: flex;
  683. flex-direction: row;
  684. align-items: center;
  685. }
  686. .score-slider {
  687. flex: 1;
  688. }
  689. .score-value {
  690. font-size: 28rpx;
  691. font-weight: bold;
  692. color: #5B9BD5;
  693. margin-left: 16rpx;
  694. min-width: 80rpx;
  695. text-align: center;
  696. }
  697. /* 备注 */
  698. .note-input {
  699. width: 100%;
  700. min-height: 160rpx;
  701. background: #F5F7FA;
  702. border-radius: 12rpx;
  703. padding: 20rpx;
  704. font-size: 26rpx;
  705. color: #333;
  706. box-sizing: border-box;
  707. }
  708. /* 提交按钮 */
  709. .submit-btn {
  710. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  711. color: #fff;
  712. font-size: 30rpx;
  713. font-weight: bold;
  714. text-align: center;
  715. padding: 28rpx;
  716. border-radius: 16rpx;
  717. margin-top: 10rpx;
  718. }
  719. .submit-btn:active {
  720. opacity: 0.85;
  721. }
  722. .btn-disabled {
  723. opacity: 0.5;
  724. }
  725. /* ===== PDF解析预览 ===== */
  726. .parsed-preview {
  727. background: #E8F5E9;
  728. border-radius: 16rpx;
  729. margin: 0 30rpx 20rpx;
  730. padding: 20rpx 24rpx;
  731. border: 2rpx solid #A5D6A7;
  732. }
  733. .parsed-title {
  734. font-size: 24rpx;
  735. font-weight: bold;
  736. color: #2E7D32;
  737. margin-bottom: 12rpx;
  738. }
  739. .parsed-row {
  740. display: flex;
  741. flex-direction: row;
  742. align-items: center;
  743. margin-bottom: 8rpx;
  744. }
  745. .parsed-label {
  746. font-size: 24rpx;
  747. color: #388E3C;
  748. width: 140rpx;
  749. flex-shrink: 0;
  750. }
  751. .parsed-value {
  752. font-size: 24rpx;
  753. color: #1B5E20;
  754. font-weight: 500;
  755. }
  756. .match-status {
  757. display: flex;
  758. flex-direction: row;
  759. align-items: center;
  760. margin-top: 12rpx;
  761. padding: 12rpx 16rpx;
  762. background: #FFF3E0;
  763. border-radius: 8rpx;
  764. }
  765. .match-status.success {
  766. background: #E8F5E9;
  767. }
  768. .match-icon {
  769. font-size: 24rpx;
  770. margin-right: 8rpx;
  771. }
  772. .match-text {
  773. font-size: 24rpx;
  774. color: #E65100;
  775. }
  776. .match-status.success .match-text {
  777. color: #2E7D32;
  778. }
  779. /* ===== 家庭成员选择器 ===== */
  780. .member-selector {
  781. margin: 0 30rpx 20rpx;
  782. }
  783. .member-list {
  784. display: flex;
  785. flex-direction: row;
  786. flex-wrap: wrap;
  787. gap: 16rpx;
  788. }
  789. .member-item {
  790. display: flex;
  791. flex-direction: column;
  792. align-items: center;
  793. padding: 20rpx 24rpx;
  794. background: #F5F7FA;
  795. border-radius: 16rpx;
  796. border: 2rpx solid transparent;
  797. min-width: 120rpx;
  798. }
  799. .member-selected {
  800. background: #E3F2FD;
  801. border-color: #5B9BD5;
  802. }
  803. .member-avatar {
  804. width: 64rpx;
  805. height: 64rpx;
  806. border-radius: 50%;
  807. background: #5B9BD5;
  808. color: #fff;
  809. font-size: 28rpx;
  810. font-weight: bold;
  811. display: flex;
  812. align-items: center;
  813. justify-content: center;
  814. margin-bottom: 8rpx;
  815. }
  816. .member-name {
  817. font-size: 24rpx;
  818. color: #333;
  819. }
  820. /* ===== 舌诊拍照区 ===== */
  821. .photo-area {
  822. display: flex;
  823. justify-content: center;
  824. margin-bottom: 16rpx;
  825. }
  826. .photo-placeholder {
  827. width: 400rpx;
  828. height: 400rpx;
  829. border: 4rpx dashed #CBD5E1;
  830. border-radius: 24rpx;
  831. display: flex;
  832. flex-direction: column;
  833. align-items: center;
  834. justify-content: center;
  835. background: #fff;
  836. }
  837. .photo-icon {
  838. font-size: 80rpx;
  839. margin-bottom: 16rpx;
  840. }
  841. .photo-text {
  842. font-size: 28rpx;
  843. color: #64748B;
  844. }
  845. .photo-sub {
  846. font-size: 22rpx;
  847. color: #94A3B8;
  848. margin-top: 12rpx;
  849. text-align: center;
  850. padding: 0 20rpx;
  851. }
  852. .photo-preview {
  853. width: 400rpx;
  854. height: 400rpx;
  855. border-radius: 24rpx;
  856. }
  857. .photo-analyzing {
  858. width: 400rpx;
  859. height: 400rpx;
  860. border-radius: 24rpx;
  861. background: #f5f5f5;
  862. display: flex;
  863. align-items: center;
  864. justify-content: center;
  865. }
  866. .analyzing-text {
  867. font-size: 28rpx;
  868. color: #5B9BD5;
  869. }
  870. .photo-retake {
  871. text-align: center;
  872. font-size: 24rpx;
  873. color: #5B9BD5;
  874. margin-top: 8rpx;
  875. }
  876. /* ===== 底部 ===== */
  877. .bottom-spacer {
  878. height: 60rpx;
  879. }
  880. </style>