report-upload.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  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. var now = new Date()
  223. var y = now.getFullYear()
  224. var m = ('' + (now.getMonth() + 1)).padStart(2, '0')
  225. var d = ('' + now.getDate()).padStart(2, '0')
  226. this.form.reportDate = y + '-' + m + '-' + d
  227. },
  228. onShow: function() {
  229. // 每次显示时从store获取familyId和家庭成员
  230. this.loadFamilyInfo()
  231. },
  232. methods: {
  233. selectType(type) {
  234. if (this.form.reportType === type) return
  235. this.form.reportType = type
  236. // 切类型时清理其他类型的状态
  237. if (type === 'tongue') {
  238. this.selectedFile = null
  239. this.parsedInfo = null
  240. } else {
  241. this.photoPath = null
  242. this.tongueResult = null
  243. this.analyzing = false
  244. }
  245. },
  246. loadFamilyInfo() {
  247. var self = this
  248. // 从Vuex获取familyId和家庭成员
  249. var userInfo = uni.getStorageSync('userInfo')
  250. if (userInfo) {
  251. try {
  252. var info = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
  253. this.familyId = info.familyId || null
  254. // 加载家庭成员列表(从本地存储或请求)
  255. var storedMembers = uni.getStorageSync('familyMembers')
  256. if (storedMembers) {
  257. try {
  258. this.familyMembers = typeof storedMembers === 'string' ? JSON.parse(storedMembers) : storedMembers
  259. } catch (e) {
  260. this.familyMembers = []
  261. }
  262. }
  263. } catch (e) {
  264. console.log('解析userInfo失败', e)
  265. }
  266. }
  267. },
  268. // ===== 舌诊拍照 =====
  269. takePhoto() {
  270. var self = this
  271. uni.chooseImage({
  272. count: 1,
  273. sourceType: ['camera', 'album'],
  274. success: function(res) {
  275. self.photoPath = res.tempFilePaths[0]
  276. self.tongueResult = null
  277. }
  278. })
  279. },
  280. analyzeTongue() {
  281. if (!this.photoPath || this.analyzing) return
  282. var self = this
  283. this.analyzing = true
  284. var memberId = this.childId || ''
  285. uni.uploadFile({
  286. url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
  287. filePath: this.photoPath,
  288. name: 'file',
  289. header: {
  290. 'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '')
  291. },
  292. success: function(res) {
  293. var data = JSON.parse(res.data)
  294. if (data.code === 200 && data.data) {
  295. self.tongueResult = data.data
  296. // 跳转到确认页
  297. var payload = {
  298. type: 'tongue',
  299. indicators: (data.data.indicators || []).map(function(ind) {
  300. return { code: ind.code, name: ind.name, value: ind.value }
  301. }),
  302. summary: { overallAssessment: data.data.overallAssessment || '' }
  303. }
  304. var params = 'draftId=' + data.data.recordId
  305. + '&payload=' + encodeURIComponent(JSON.stringify(payload))
  306. + '&reportType=tongue'
  307. + (self.childId ? '&childId=' + self.childId : '')
  308. + (self.familyId ? '&familyId=' + self.familyId : '')
  309. uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
  310. } else {
  311. uni.showToast({ title: data.message || '分析失败', icon: 'none' })
  312. }
  313. },
  314. fail: function() {
  315. uni.showToast({ title: '上传失败', icon: 'none' })
  316. },
  317. complete: function() {
  318. self.analyzing = false
  319. }
  320. })
  321. },
  322. onScoreChanging(e) {
  323. this.form.overallScore = e.detail.value
  324. },
  325. onScoreChange(e) {
  326. this.form.overallScore = e.detail.value
  327. },
  328. pickFile() {
  329. var self = this
  330. // 重置解析状态
  331. this.parsedInfo = null
  332. this.showMemberSelector = false
  333. this.selectedMemberId = null
  334. // 从微信聊天记录中选择文件
  335. uni.chooseMessageFile({
  336. count: 1,
  337. type: 'file',
  338. extension: ['pdf'],
  339. success: function(res) {
  340. var files = res.tempFiles
  341. if (files && files.length > 0) {
  342. self.selectedFile = files[0]
  343. // 如果选择了文件且有familyId,先预览解析
  344. if (self.familyId) {
  345. self.previewParse()
  346. }
  347. }
  348. },
  349. fail: function(e) {
  350. console.log('选择文件取消或失败', e)
  351. }
  352. })
  353. },
  354. previewParse() {
  355. var self = this
  356. if (!this.selectedFile || !this.familyId) return
  357. this.parsingPreview = true
  358. parseReportPreview(this.selectedFile.path, this.childId, this.familyId).then(function(res) {
  359. var detail = res.data
  360. self.parsedInfo = {
  361. draftId: detail.draftId,
  362. personName: detail.extractedName || '',
  363. extractedGender: detail.extractedGender || '',
  364. extractedAge: detail.extractedAge || '',
  365. reportNumber: detail.extractedReportNumber,
  366. reportDate: detail.extractedReportDate,
  367. needManualSelect: detail.needBind,
  368. payload: detail.payload || {},
  369. matchedMemberId: detail.matchedMemberId,
  370. confidence: detail.confidence,
  371. matchLabel: detail.matchLabel,
  372. candidates: detail.candidates || []
  373. }
  374. if (detail.needBind) {
  375. self.showMemberSelector = true
  376. self.loadFamilyMembers()
  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. if (self.parsedInfo.matchedMemberId) {
  453. params += '&matchedMemberId=' + self.parsedInfo.matchedMemberId
  454. }
  455. if (self.childId) {
  456. params += '&childId=' + self.childId
  457. }
  458. if (self.familyId) {
  459. params += '&familyId=' + self.familyId
  460. }
  461. uni.navigateTo({ url: '/pages/health/report-confirm?' + params })
  462. return
  463. }
  464. self.submitting = true
  465. uni.showLoading({ title: '提交中...' })
  466. createHealthReport({
  467. reportType: self.form.reportType,
  468. reportDate: self.form.reportDate,
  469. overallScore: self.form.overallScore,
  470. description: self.form.description
  471. }).then(function(res) {
  472. uni.hideLoading()
  473. if (res.code === 200) {
  474. uni.showToast({ title: '提交成功', icon: 'success' })
  475. var reportId = res.data && res.data.id
  476. if (reportId) {
  477. setTimeout(function() {
  478. if (self.form.reportType === 'gut_flora') {
  479. uni.redirectTo({
  480. url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (self.childId || '')
  481. })
  482. } else {
  483. uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
  484. }
  485. }, 500)
  486. } else {
  487. setTimeout(function() {
  488. uni.navigateBack()
  489. }, 500)
  490. }
  491. } else {
  492. uni.showToast({ title: res.message || '提交失败', icon: 'none' })
  493. }
  494. }).catch(function(e) {
  495. uni.hideLoading()
  496. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  497. console.log('创建报告失败', e)
  498. }).finally(function() {
  499. self.submitting = false
  500. })
  501. }
  502. }
  503. }
  504. </script>
  505. <style scoped>
  506. .upload-container {
  507. min-height: 100vh;
  508. background: #F5F7FA;
  509. }
  510. /* ===== 导航栏 ===== */
  511. .nav-bar {
  512. position: sticky;
  513. top: 0;
  514. background: #fff;
  515. display: flex;
  516. flex-direction: row;
  517. align-items: center;
  518. justify-content: space-between;
  519. padding: 20rpx 30rpx;
  520. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  521. z-index: 100;
  522. }
  523. .nav-back {
  524. font-size: 32rpx;
  525. color: #5B9BD5;
  526. }
  527. .nav-title {
  528. font-size: 32rpx;
  529. font-weight: bold;
  530. color: #333;
  531. }
  532. .nav-placeholder {
  533. width: 80rpx;
  534. }
  535. /* ===== 提示条 ===== */
  536. .tip-banner {
  537. display: flex;
  538. flex-direction: row;
  539. align-items: center;
  540. background: #E3F2FD;
  541. margin: 20rpx 30rpx;
  542. padding: 20rpx 24rpx;
  543. border-radius: 16rpx;
  544. }
  545. .tip-icon {
  546. font-size: 32rpx;
  547. margin-right: 16rpx;
  548. flex-shrink: 0;
  549. }
  550. .tip-text {
  551. font-size: 24rpx;
  552. color: #1565C0;
  553. line-height: 1.5;
  554. }
  555. /* ===== 表单 ===== */
  556. .form-section {
  557. margin: 0 30rpx;
  558. }
  559. .form-item {
  560. background: #fff;
  561. border-radius: 16rpx;
  562. padding: 24rpx;
  563. margin-bottom: 20rpx;
  564. }
  565. .form-label {
  566. font-size: 26rpx;
  567. font-weight: bold;
  568. color: #333;
  569. display: block;
  570. margin-bottom: 16rpx;
  571. }
  572. /* 类型选择 */
  573. .type-selector {
  574. display: flex;
  575. flex-direction: row;
  576. gap: 20rpx;
  577. }
  578. .type-option {
  579. flex: 1;
  580. display: flex;
  581. flex-direction: column;
  582. align-items: center;
  583. padding: 24rpx;
  584. border-radius: 16rpx;
  585. background: #F5F7FA;
  586. border: 2rpx solid transparent;
  587. }
  588. .type-selected {
  589. background: #E3F2FD;
  590. border-color: #5B9BD5;
  591. }
  592. .type-icon {
  593. font-size: 40rpx;
  594. margin-bottom: 8rpx;
  595. }
  596. .type-name {
  597. font-size: 26rpx;
  598. color: #333;
  599. font-weight: 500;
  600. }
  601. /* 文件上传 */
  602. .file-upload-area {
  603. border: 2rpx dashed #CBD5E1;
  604. border-radius: 16rpx;
  605. padding: 32rpx 20rpx;
  606. text-align: center;
  607. background: #FAFBFC;
  608. cursor: pointer;
  609. }
  610. .file-upload-area:active {
  611. background: #F1F5F9;
  612. }
  613. .file-upload-placeholder {
  614. display: flex;
  615. flex-direction: column;
  616. align-items: center;
  617. }
  618. .file-upload-icon {
  619. font-size: 48rpx;
  620. margin-bottom: 12rpx;
  621. }
  622. .file-upload-text {
  623. font-size: 28rpx;
  624. color: #5B9BD5;
  625. font-weight: 500;
  626. }
  627. .file-upload-hint {
  628. font-size: 22rpx;
  629. color: #94A3B8;
  630. margin-top: 8rpx;
  631. }
  632. .file-info {
  633. display: flex;
  634. flex-direction: row;
  635. align-items: center;
  636. background: #E3F2FD;
  637. border-radius: 12rpx;
  638. padding: 16rpx 20rpx;
  639. margin-top: 16rpx;
  640. }
  641. .file-name {
  642. flex: 1;
  643. font-size: 24rpx;
  644. color: #1565C0;
  645. overflow: hidden;
  646. text-overflow: ellipsis;
  647. white-space: nowrap;
  648. }
  649. .file-size {
  650. font-size: 22rpx;
  651. color: #64748B;
  652. margin: 0 16rpx;
  653. flex-shrink: 0;
  654. }
  655. .file-remove {
  656. font-size: 24rpx;
  657. color: #EF4444;
  658. flex-shrink: 0;
  659. padding: 4rpx 8rpx;
  660. }
  661. /* 日期 */
  662. .date-display {
  663. display: flex;
  664. flex-direction: row;
  665. align-items: center;
  666. justify-content: space-between;
  667. padding: 16rpx 20rpx;
  668. background: #F5F7FA;
  669. border-radius: 12rpx;
  670. font-size: 28rpx;
  671. color: #333;
  672. }
  673. .date-arrow {
  674. font-size: 32rpx;
  675. color: #999;
  676. }
  677. /* 评分滑块 */
  678. .score-input-wrap {
  679. display: flex;
  680. flex-direction: row;
  681. align-items: center;
  682. }
  683. .score-slider {
  684. flex: 1;
  685. }
  686. .score-value {
  687. font-size: 28rpx;
  688. font-weight: bold;
  689. color: #5B9BD5;
  690. margin-left: 16rpx;
  691. min-width: 80rpx;
  692. text-align: center;
  693. }
  694. /* 备注 */
  695. .note-input {
  696. width: 100%;
  697. min-height: 160rpx;
  698. background: #F5F7FA;
  699. border-radius: 12rpx;
  700. padding: 20rpx;
  701. font-size: 26rpx;
  702. color: #333;
  703. box-sizing: border-box;
  704. }
  705. /* 提交按钮 */
  706. .submit-btn {
  707. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  708. color: #fff;
  709. font-size: 30rpx;
  710. font-weight: bold;
  711. text-align: center;
  712. padding: 28rpx;
  713. border-radius: 16rpx;
  714. margin-top: 10rpx;
  715. }
  716. .submit-btn:active {
  717. opacity: 0.85;
  718. }
  719. .btn-disabled {
  720. opacity: 0.5;
  721. }
  722. /* ===== PDF解析预览 ===== */
  723. .parsed-preview {
  724. background: #E8F5E9;
  725. border-radius: 16rpx;
  726. margin: 0 30rpx 20rpx;
  727. padding: 20rpx 24rpx;
  728. border: 2rpx solid #A5D6A7;
  729. }
  730. .parsed-title {
  731. font-size: 24rpx;
  732. font-weight: bold;
  733. color: #2E7D32;
  734. margin-bottom: 12rpx;
  735. }
  736. .parsed-row {
  737. display: flex;
  738. flex-direction: row;
  739. align-items: center;
  740. margin-bottom: 8rpx;
  741. }
  742. .parsed-label {
  743. font-size: 24rpx;
  744. color: #388E3C;
  745. width: 140rpx;
  746. flex-shrink: 0;
  747. }
  748. .parsed-value {
  749. font-size: 24rpx;
  750. color: #1B5E20;
  751. font-weight: 500;
  752. }
  753. .match-status {
  754. display: flex;
  755. flex-direction: row;
  756. align-items: center;
  757. margin-top: 12rpx;
  758. padding: 12rpx 16rpx;
  759. background: #FFF3E0;
  760. border-radius: 8rpx;
  761. }
  762. .match-status.success {
  763. background: #E8F5E9;
  764. }
  765. .match-icon {
  766. font-size: 24rpx;
  767. margin-right: 8rpx;
  768. }
  769. .match-text {
  770. font-size: 24rpx;
  771. color: #E65100;
  772. }
  773. .match-status.success .match-text {
  774. color: #2E7D32;
  775. }
  776. /* ===== 家庭成员选择器 ===== */
  777. .member-selector {
  778. margin: 0 30rpx 20rpx;
  779. }
  780. .member-list {
  781. display: flex;
  782. flex-direction: row;
  783. flex-wrap: wrap;
  784. gap: 16rpx;
  785. }
  786. .member-item {
  787. display: flex;
  788. flex-direction: column;
  789. align-items: center;
  790. padding: 20rpx 24rpx;
  791. background: #F5F7FA;
  792. border-radius: 16rpx;
  793. border: 2rpx solid transparent;
  794. min-width: 120rpx;
  795. }
  796. .member-selected {
  797. background: #E3F2FD;
  798. border-color: #5B9BD5;
  799. }
  800. .member-avatar {
  801. width: 64rpx;
  802. height: 64rpx;
  803. border-radius: 50%;
  804. background: #5B9BD5;
  805. color: #fff;
  806. font-size: 28rpx;
  807. font-weight: bold;
  808. display: flex;
  809. align-items: center;
  810. justify-content: center;
  811. margin-bottom: 8rpx;
  812. }
  813. .member-name {
  814. font-size: 24rpx;
  815. color: #333;
  816. }
  817. /* ===== 舌诊拍照区 ===== */
  818. .photo-area {
  819. display: flex;
  820. justify-content: center;
  821. margin-bottom: 16rpx;
  822. }
  823. .photo-placeholder {
  824. width: 400rpx;
  825. height: 400rpx;
  826. border: 4rpx dashed #CBD5E1;
  827. border-radius: 24rpx;
  828. display: flex;
  829. flex-direction: column;
  830. align-items: center;
  831. justify-content: center;
  832. background: #fff;
  833. }
  834. .photo-icon {
  835. font-size: 80rpx;
  836. margin-bottom: 16rpx;
  837. }
  838. .photo-text {
  839. font-size: 28rpx;
  840. color: #64748B;
  841. }
  842. .photo-sub {
  843. font-size: 22rpx;
  844. color: #94A3B8;
  845. margin-top: 12rpx;
  846. text-align: center;
  847. padding: 0 20rpx;
  848. }
  849. .photo-preview {
  850. width: 400rpx;
  851. height: 400rpx;
  852. border-radius: 24rpx;
  853. }
  854. .photo-analyzing {
  855. width: 400rpx;
  856. height: 400rpx;
  857. border-radius: 24rpx;
  858. background: #f5f5f5;
  859. display: flex;
  860. align-items: center;
  861. justify-content: center;
  862. }
  863. .analyzing-text {
  864. font-size: 28rpx;
  865. color: #5B9BD5;
  866. }
  867. .photo-retake {
  868. text-align: center;
  869. font-size: 24rpx;
  870. color: #5B9BD5;
  871. margin-top: 8rpx;
  872. }
  873. /* ===== 底部 ===== */
  874. .bottom-spacer {
  875. height: 60rpx;
  876. }
  877. </style>