report-upload.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  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. if (self.form.reportType === 'gut_flora') {
  480. uni.redirectTo({
  481. url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (self.childId || '')
  482. })
  483. } else {
  484. uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
  485. }
  486. }, 500)
  487. } else {
  488. setTimeout(function() {
  489. uni.navigateBack()
  490. }, 500)
  491. }
  492. } else {
  493. uni.showToast({ title: res.message || '提交失败', icon: 'none' })
  494. }
  495. }).catch(function(e) {
  496. uni.hideLoading()
  497. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  498. console.log('创建报告失败', e)
  499. }).finally(function() {
  500. self.submitting = false
  501. })
  502. }
  503. }
  504. }
  505. </script>
  506. <style scoped>
  507. .upload-container {
  508. min-height: 100vh;
  509. background: #F5F7FA;
  510. }
  511. /* ===== 导航栏 ===== */
  512. .nav-bar {
  513. position: sticky;
  514. top: 0;
  515. background: #fff;
  516. display: flex;
  517. flex-direction: row;
  518. align-items: center;
  519. justify-content: space-between;
  520. padding: 20rpx 30rpx;
  521. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  522. z-index: 100;
  523. }
  524. .nav-back {
  525. font-size: 32rpx;
  526. color: #5B9BD5;
  527. }
  528. .nav-title {
  529. font-size: 32rpx;
  530. font-weight: bold;
  531. color: #333;
  532. }
  533. .nav-placeholder {
  534. width: 80rpx;
  535. }
  536. /* ===== 提示条 ===== */
  537. .tip-banner {
  538. display: flex;
  539. flex-direction: row;
  540. align-items: center;
  541. background: #E3F2FD;
  542. margin: 20rpx 30rpx;
  543. padding: 20rpx 24rpx;
  544. border-radius: 16rpx;
  545. }
  546. .tip-icon {
  547. font-size: 32rpx;
  548. margin-right: 16rpx;
  549. flex-shrink: 0;
  550. }
  551. .tip-text {
  552. font-size: 24rpx;
  553. color: #1565C0;
  554. line-height: 1.5;
  555. }
  556. /* ===== 表单 ===== */
  557. .form-section {
  558. margin: 0 30rpx;
  559. }
  560. .form-item {
  561. background: #fff;
  562. border-radius: 16rpx;
  563. padding: 24rpx;
  564. margin-bottom: 20rpx;
  565. }
  566. .form-label {
  567. font-size: 26rpx;
  568. font-weight: bold;
  569. color: #333;
  570. display: block;
  571. margin-bottom: 16rpx;
  572. }
  573. /* 类型选择 */
  574. .type-selector {
  575. display: flex;
  576. flex-direction: row;
  577. gap: 20rpx;
  578. }
  579. .type-option {
  580. flex: 1;
  581. display: flex;
  582. flex-direction: column;
  583. align-items: center;
  584. padding: 24rpx;
  585. border-radius: 16rpx;
  586. background: #F5F7FA;
  587. border: 2rpx solid transparent;
  588. }
  589. .type-selected {
  590. background: #E3F2FD;
  591. border-color: #5B9BD5;
  592. }
  593. .type-icon {
  594. font-size: 40rpx;
  595. margin-bottom: 8rpx;
  596. }
  597. .type-name {
  598. font-size: 26rpx;
  599. color: #333;
  600. font-weight: 500;
  601. }
  602. /* 文件上传 */
  603. .file-upload-area {
  604. border: 2rpx dashed #CBD5E1;
  605. border-radius: 16rpx;
  606. padding: 32rpx 20rpx;
  607. text-align: center;
  608. background: #FAFBFC;
  609. cursor: pointer;
  610. }
  611. .file-upload-area:active {
  612. background: #F1F5F9;
  613. }
  614. .file-upload-placeholder {
  615. display: flex;
  616. flex-direction: column;
  617. align-items: center;
  618. }
  619. .file-upload-icon {
  620. font-size: 48rpx;
  621. margin-bottom: 12rpx;
  622. }
  623. .file-upload-text {
  624. font-size: 28rpx;
  625. color: #5B9BD5;
  626. font-weight: 500;
  627. }
  628. .file-upload-hint {
  629. font-size: 22rpx;
  630. color: #94A3B8;
  631. margin-top: 8rpx;
  632. }
  633. .file-info {
  634. display: flex;
  635. flex-direction: row;
  636. align-items: center;
  637. background: #E3F2FD;
  638. border-radius: 12rpx;
  639. padding: 16rpx 20rpx;
  640. margin-top: 16rpx;
  641. }
  642. .file-name {
  643. flex: 1;
  644. font-size: 24rpx;
  645. color: #1565C0;
  646. overflow: hidden;
  647. text-overflow: ellipsis;
  648. white-space: nowrap;
  649. }
  650. .file-size {
  651. font-size: 22rpx;
  652. color: #64748B;
  653. margin: 0 16rpx;
  654. flex-shrink: 0;
  655. }
  656. .file-remove {
  657. font-size: 24rpx;
  658. color: #EF4444;
  659. flex-shrink: 0;
  660. padding: 4rpx 8rpx;
  661. }
  662. /* 日期 */
  663. .date-display {
  664. display: flex;
  665. flex-direction: row;
  666. align-items: center;
  667. justify-content: space-between;
  668. padding: 16rpx 20rpx;
  669. background: #F5F7FA;
  670. border-radius: 12rpx;
  671. font-size: 28rpx;
  672. color: #333;
  673. }
  674. .date-arrow {
  675. font-size: 32rpx;
  676. color: #999;
  677. }
  678. /* 评分滑块 */
  679. .score-input-wrap {
  680. display: flex;
  681. flex-direction: row;
  682. align-items: center;
  683. }
  684. .score-slider {
  685. flex: 1;
  686. }
  687. .score-value {
  688. font-size: 28rpx;
  689. font-weight: bold;
  690. color: #5B9BD5;
  691. margin-left: 16rpx;
  692. min-width: 80rpx;
  693. text-align: center;
  694. }
  695. /* 备注 */
  696. .note-input {
  697. width: 100%;
  698. min-height: 160rpx;
  699. background: #F5F7FA;
  700. border-radius: 12rpx;
  701. padding: 20rpx;
  702. font-size: 26rpx;
  703. color: #333;
  704. box-sizing: border-box;
  705. }
  706. /* 提交按钮 */
  707. .submit-btn {
  708. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  709. color: #fff;
  710. font-size: 30rpx;
  711. font-weight: bold;
  712. text-align: center;
  713. padding: 28rpx;
  714. border-radius: 16rpx;
  715. margin-top: 10rpx;
  716. }
  717. .submit-btn:active {
  718. opacity: 0.85;
  719. }
  720. .btn-disabled {
  721. opacity: 0.5;
  722. }
  723. /* ===== PDF解析预览 ===== */
  724. .parsed-preview {
  725. background: #E8F5E9;
  726. border-radius: 16rpx;
  727. margin: 0 30rpx 20rpx;
  728. padding: 20rpx 24rpx;
  729. border: 2rpx solid #A5D6A7;
  730. }
  731. .parsed-title {
  732. font-size: 24rpx;
  733. font-weight: bold;
  734. color: #2E7D32;
  735. margin-bottom: 12rpx;
  736. }
  737. .parsed-row {
  738. display: flex;
  739. flex-direction: row;
  740. align-items: center;
  741. margin-bottom: 8rpx;
  742. }
  743. .parsed-label {
  744. font-size: 24rpx;
  745. color: #388E3C;
  746. width: 140rpx;
  747. flex-shrink: 0;
  748. }
  749. .parsed-value {
  750. font-size: 24rpx;
  751. color: #1B5E20;
  752. font-weight: 500;
  753. }
  754. .match-status {
  755. display: flex;
  756. flex-direction: row;
  757. align-items: center;
  758. margin-top: 12rpx;
  759. padding: 12rpx 16rpx;
  760. background: #FFF3E0;
  761. border-radius: 8rpx;
  762. }
  763. .match-status.success {
  764. background: #E8F5E9;
  765. }
  766. .match-icon {
  767. font-size: 24rpx;
  768. margin-right: 8rpx;
  769. }
  770. .match-text {
  771. font-size: 24rpx;
  772. color: #E65100;
  773. }
  774. .match-status.success .match-text {
  775. color: #2E7D32;
  776. }
  777. /* ===== 家庭成员选择器 ===== */
  778. .member-selector {
  779. margin: 0 30rpx 20rpx;
  780. }
  781. .member-list {
  782. display: flex;
  783. flex-direction: row;
  784. flex-wrap: wrap;
  785. gap: 16rpx;
  786. }
  787. .member-item {
  788. display: flex;
  789. flex-direction: column;
  790. align-items: center;
  791. padding: 20rpx 24rpx;
  792. background: #F5F7FA;
  793. border-radius: 16rpx;
  794. border: 2rpx solid transparent;
  795. min-width: 120rpx;
  796. }
  797. .member-selected {
  798. background: #E3F2FD;
  799. border-color: #5B9BD5;
  800. }
  801. .member-avatar {
  802. width: 64rpx;
  803. height: 64rpx;
  804. border-radius: 50%;
  805. background: #5B9BD5;
  806. color: #fff;
  807. font-size: 28rpx;
  808. font-weight: bold;
  809. display: flex;
  810. align-items: center;
  811. justify-content: center;
  812. margin-bottom: 8rpx;
  813. }
  814. .member-name {
  815. font-size: 24rpx;
  816. color: #333;
  817. }
  818. /* ===== 舌诊拍照区 ===== */
  819. .photo-area {
  820. display: flex;
  821. justify-content: center;
  822. margin-bottom: 16rpx;
  823. }
  824. .photo-placeholder {
  825. width: 400rpx;
  826. height: 400rpx;
  827. border: 4rpx dashed #CBD5E1;
  828. border-radius: 24rpx;
  829. display: flex;
  830. flex-direction: column;
  831. align-items: center;
  832. justify-content: center;
  833. background: #fff;
  834. }
  835. .photo-icon {
  836. font-size: 80rpx;
  837. margin-bottom: 16rpx;
  838. }
  839. .photo-text {
  840. font-size: 28rpx;
  841. color: #64748B;
  842. }
  843. .photo-sub {
  844. font-size: 22rpx;
  845. color: #94A3B8;
  846. margin-top: 12rpx;
  847. text-align: center;
  848. padding: 0 20rpx;
  849. }
  850. .photo-preview {
  851. width: 400rpx;
  852. height: 400rpx;
  853. border-radius: 24rpx;
  854. }
  855. .photo-analyzing {
  856. width: 400rpx;
  857. height: 400rpx;
  858. border-radius: 24rpx;
  859. background: #f5f5f5;
  860. display: flex;
  861. align-items: center;
  862. justify-content: center;
  863. }
  864. .analyzing-text {
  865. font-size: 28rpx;
  866. color: #5B9BD5;
  867. }
  868. .photo-retake {
  869. text-align: center;
  870. font-size: 24rpx;
  871. color: #5B9BD5;
  872. margin-top: 8rpx;
  873. }
  874. /* ===== 底部 ===== */
  875. .bottom-spacer {
  876. height: 60rpx;
  877. }
  878. </style>