report-upload.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. <template>
  2. <view class="upload-container">
  3. <!-- 说明提示 -->
  4. <view class="tip-banner">
  5. <text class="tip-icon">📋</text>
  6. <text class="tip-text">{{ tipText }}</text>
  7. </view>
  8. <!-- 报告类型 -->
  9. <view class="form-section type-section">
  10. <text class="form-label">请选择报告类型</text>
  11. <view class="type-methods">
  12. <view
  13. class="type-card type-gut"
  14. :class="{ 'type-card-active': reportKind === 'gut' }"
  15. @click="selectKind('gut')">
  16. <text class="type-card-icon">🦠</text>
  17. <text class="type-card-title">菌群检测报告</text>
  18. <text class="type-card-desc">肠道菌群 PDF,自动解析菌群指标</text>
  19. </view>
  20. <view
  21. class="type-card type-a2"
  22. :class="{ 'type-card-active': reportKind === 'a2' }"
  23. @click="selectKind('a2')">
  24. <text class="type-card-icon">🧠</text>
  25. <text class="type-card-title">素质测评 A2</text>
  26. <text class="type-card-desc">核心素养 PDF,解析大五人格等维度</text>
  27. </view>
  28. </view>
  29. </view>
  30. <!-- A2 需指定成员 -->
  31. <view class="form-section" v-if="reportKind === 'a2'">
  32. <view class="member-box">
  33. <text class="form-label">报告属于哪位成员</text>
  34. <view class="member-list" v-if="familyMembers.length > 0">
  35. <view
  36. class="member-item"
  37. v-for="(member, mIdx) in familyMembers"
  38. :key="getMemberKey(member)"
  39. :class="member._selected ? 'member-selected' : ''"
  40. hover-class="member-hover"
  41. @click="selectMemberByIndex(mIdx)">
  42. <text class="member-avatar">{{ memberInitial(member) }}</text>
  43. <text class="member-name">{{ member.nickname || '未命名' }}</text>
  44. </view>
  45. </view>
  46. <text class="member-empty" v-else>暂无家庭成员,请先在家庭中添加</text>
  47. </view>
  48. </view>
  49. <!-- 上传方式 -->
  50. <view class="form-section">
  51. <view class="upload-methods">
  52. <view class="upload-card card-chat" @click="pickFromChat">
  53. <view class="upload-card-icon">💬</view>
  54. <text class="upload-card-title">从聊天选取</text>
  55. <text class="upload-card-desc">{{ reportKind === 'a2' ? '微信聊天中的 A2 PDF' : '微信聊天中的 PDF/图片' }}</text>
  56. </view>
  57. <view class="upload-card card-photo" @click="takePhoto">
  58. <view class="upload-card-icon">📷</view>
  59. <text class="upload-card-title">拍照上传</text>
  60. <text class="upload-card-desc">{{ reportKind === 'a2' ? 'A2 仅支持 PDF 文件' : '拍摄纸质报告或舌象' }}</text>
  61. </view>
  62. </view>
  63. <!-- 采集中面板 -->
  64. <view class="collecting-panel" v-if="collecting">
  65. <view class="collecting-card">
  66. <view class="collecting-icon">
  67. <view class="spinner" v-if="!collectError"></view>
  68. <text class="collecting-fail-mark" v-else>!</text>
  69. </view>
  70. <view class="collecting-info">
  71. <text class="collecting-title">{{ collectError ? '采集失败' : '数据采集中' }}</text>
  72. <text class="collecting-file">{{ collectFileName || '报告文件' }}</text>
  73. <text class="collecting-hint">{{ collectError || '后台正在识别报告类型并解析,完成后自动入库' }}</text>
  74. </view>
  75. </view>
  76. <view class="collecting-actions">
  77. <view class="collecting-btn" @click="goReportList">去报告列表查看</view>
  78. <view class="collecting-cancel" @click="cancelCollect">取消</view>
  79. </view>
  80. </view>
  81. <!-- 上传中 -->
  82. <view class="form-item" v-if="uploading">
  83. <text class="form-label">上传中...</text>
  84. <view class="analyzing-status">
  85. <text class="analyzing-text">{{ uploadingText }}</text>
  86. </view>
  87. </view>
  88. <!-- 责任声明 -->
  89. <view class="form-item disclaimer-box">
  90. <view class="disclaimer-check" @click="toggleAgree">
  91. <view class="disclaimer-checkbox" :class="{ 'disclaimer-checked': agreedToTerms }">
  92. <text class="disclaimer-checkmark" v-if="agreedToTerms">✓</text>
  93. </view>
  94. <text class="disclaimer-label">我已阅读并同意以下声明</text>
  95. </view>
  96. <view class="disclaimer-content">
  97. <text class="disclaimer-text">我同意平台使用本报告数据进行分析,为我提供个性化健康解决方案;未经我授权,平台不会以任何形式将我的报告内容提供给其他任何个人或机构。</text>
  98. </view>
  99. </view>
  100. </view>
  101. <!-- 上传说明 -->
  102. <view class="guide-section">
  103. <view class="guide-header">
  104. <text class="guide-header-icon">📖</text>
  105. <text class="guide-header-title">支持的报告类型</text>
  106. </view>
  107. <view class="guide-list">
  108. <view class="guide-item">
  109. <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📄</text></view>
  110. <view class="guide-info">
  111. <text class="guide-name">菌群检测报告</text>
  112. <text class="guide-desc">肠道菌群 PDF,后台采集解析菌群指标</text>
  113. </view>
  114. </view>
  115. <view class="guide-item">
  116. <view class="guide-icon-box guide-icon-purple"><text class="guide-icon">🧠</text></view>
  117. <view class="guide-info">
  118. <text class="guide-name">素质测评 A2</text>
  119. <text class="guide-desc">核心素养 PDF,解析认知、情绪、大五人格等</text>
  120. </view>
  121. </view>
  122. <view class="guide-item">
  123. <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📄</text></view>
  124. <view class="guide-info">
  125. <text class="guide-name">体检报告</text>
  126. <text class="guide-desc">血常规、尿常规、生化检验等,支持 PDF 或清晰照片</text>
  127. </view>
  128. </view>
  129. <view class="guide-item">
  130. <view class="guide-icon-box guide-icon-orange"><text class="guide-icon">👅</text></view>
  131. <view class="guide-info">
  132. <text class="guide-name">舌象照片</text>
  133. <text class="guide-desc">拍摄舌象照片,AI 辅助舌诊分析</text>
  134. </view>
  135. </view>
  136. <view class="guide-item">
  137. <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">⚖️</text></view>
  138. <view class="guide-info">
  139. <text class="guide-name">体脂秤报告</text>
  140. <text class="guide-desc">拍摄体脂秤显示屏,自动提取体重、BMI、体脂率等</text>
  141. </view>
  142. </view>
  143. </view>
  144. <view class="guide-tips">
  145. <text class="guide-tips-title">温馨提示</text>
  146. <text class="guide-tips-item">· 请先选择报告类型,再从聊天选取文件</text>
  147. <text class="guide-tips-item">· 素质测评 A2 仅支持 PDF,需指定家庭成员</text>
  148. <text class="guide-tips-item">· 请确保报告清晰完整,以便准确解析</text>
  149. <text class="guide-tips-item">· 确认入库后将自动生成成长方案,可再分解为每日任务</text>
  150. </view>
  151. </view>
  152. </view>
  153. </template>
  154. <script>
  155. import { uploadReportOnly, getReportCollectStatus, danParsePreview, danConfirmReport, getFamilyMemberList } from '../../utils/api.js'
  156. export default {
  157. data() {
  158. return {
  159. memberId: null,
  160. reportKind: '',
  161. uploadType: null, // 'tongue' | null;从 URL query reportType 读取
  162. familyMembers: [],
  163. uploading: false,
  164. agreedToTerms: false,
  165. uploadingText: '正在上传文件...',
  166. collectDraftId: null,
  167. collectTimer: null,
  168. collecting: false,
  169. collectFileName: '',
  170. collectError: '',
  171. collectPollCount: 0
  172. }
  173. },
  174. computed: {
  175. tipText: function() {
  176. if (this.reportKind === 'a2') {
  177. return '已选择素质测评 A2,请指定成员后上传核心素养 PDF'
  178. }
  179. if (this.reportKind === 'gut') {
  180. return '已选择菌群检测报告,上传后后台自动解析菌群指标'
  181. }
  182. return '请先选择报告类型:菌群检测,或素质测评 A2'
  183. }
  184. },
  185. onLoad: function(options) {
  186. this.memberId = options.memberId ? parseInt(options.memberId) : null
  187. if (options.type === 'a2' || options.reportKind === 'a2') {
  188. this.reportKind = 'a2'
  189. } else if (options.type === 'gut' || options.reportKind === 'gut') {
  190. this.reportKind = 'gut'
  191. }
  192. if (options.reportType === 'tongue' || options.type === 'tongue') {
  193. this.uploadType = 'tongue'
  194. }
  195. this.loadFamilyMembers()
  196. },
  197. onUnload: function() {
  198. this.stopCollectPolling()
  199. },
  200. methods: {
  201. selectKind: function(kind) {
  202. this.reportKind = kind
  203. },
  204. resolveMemberId: function(member) {
  205. if (!member) return null
  206. var raw = member.id || member.userId
  207. if (raw === 0 || raw === '0') return 0
  208. if (!raw) return null
  209. var id = parseInt(raw)
  210. return isNaN(id) ? null : id
  211. },
  212. selectMemberByIndex: function(idx) {
  213. var list = this.familyMembers
  214. if (!list || idx < 0 || idx >= list.length) return
  215. var id = this.resolveMemberId(list[idx])
  216. if (id === null) {
  217. uni.showToast({ title: '该成员缺少编号,无法选择', icon: 'none' })
  218. return
  219. }
  220. this.memberId = id
  221. this.markSelectedMember(id)
  222. },
  223. markSelectedMember: function(memberId) {
  224. var list = this.familyMembers || []
  225. var target = memberId === null || memberId === undefined ? null : parseInt(memberId)
  226. for (var i = 0; i < list.length; i++) {
  227. var cur = this.resolveMemberId(list[i])
  228. this.$set(list[i], '_selected', target !== null && !isNaN(target) && cur === target)
  229. }
  230. },
  231. getMemberKey: function(member) {
  232. var id = this.resolveMemberId(member)
  233. return id === null ? 'm0' : ('m' + id)
  234. },
  235. memberInitial: function(member) {
  236. var name = (member && member.nickname) || '?'
  237. return name.substring(0, 1)
  238. },
  239. loadFamilyMembers: function() {
  240. var self = this
  241. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  242. var list = (res && res.data) || []
  243. self.familyMembers = list instanceof Array ? list : []
  244. if (!self.memberId && self.familyMembers.length === 1) {
  245. self.memberId = self.resolveMemberId(self.familyMembers[0])
  246. }
  247. self.markSelectedMember(self.memberId)
  248. }).catch(function() {
  249. self.familyMembers = []
  250. })
  251. },
  252. ensureTypeSelected: function() {
  253. if (this.reportKind === 'gut' || this.reportKind === 'a2') {
  254. return true
  255. }
  256. uni.showToast({ title: '请先选择报告类型', icon: 'none' })
  257. return false
  258. },
  259. ensureA2Member: function() {
  260. if (this.memberId) return true
  261. uni.showToast({ title: '请先选择报告所属成员', icon: 'none' })
  262. return false
  263. },
  264. toggleAgree: function() {
  265. this.agreedToTerms = !this.agreedToTerms
  266. },
  267. ensureAgreed: function(callback) {
  268. if (this.agreedToTerms) {
  269. callback()
  270. return
  271. }
  272. var self = this
  273. uni.showModal({
  274. title: '数据使用声明',
  275. content: '上传报告即代表您同意平台使用本报告数据进行分析,为您提供个性化健康解决方案;未经您授权,平台不会以任何形式将您的报告内容提供给其他任何个人或机构。点击「同意继续」即表示您已阅读并同意。',
  276. confirmText: '同意继续',
  277. cancelText: '暂不',
  278. success: function(res) {
  279. if (res.confirm) {
  280. self.agreedToTerms = true
  281. callback()
  282. }
  283. }
  284. })
  285. },
  286. pickFromChat: function() {
  287. var self = this
  288. if (!this.ensureTypeSelected()) return
  289. if (this.reportKind === 'a2' && !this.ensureA2Member()) return
  290. this.ensureAgreed(function() {
  291. if (self.reportKind === 'a2') {
  292. self.chooseChatFile('file', '聊天文件')
  293. return
  294. }
  295. // type='all' 的选择器默认停留在图片 tab,用户难以发现可切换选文件。
  296. // 改为先弹 actionSheet 让用户明确选择图片或文件,分别走对应 type。
  297. uni.showActionSheet({
  298. itemList: ['聊天中的文件(PDF等)', '聊天中的图片'],
  299. success: function(res) {
  300. if (res.tapIndex === 0) {
  301. self.chooseChatFile('file', '聊天文件')
  302. } else if (res.tapIndex === 1) {
  303. self.chooseChatFile('image', '聊天图片')
  304. }
  305. },
  306. fail: function() {
  307. // 用户取消,静默处理
  308. }
  309. })
  310. })
  311. },
  312. chooseChatFile: function(type, fallbackName) {
  313. var self = this
  314. uni.chooseMessageFile({
  315. count: 1,
  316. type: type,
  317. success: function(res) {
  318. var files = res.tempFiles
  319. if (files && files.length > 0) {
  320. self.uploadReport(files[0].path, files[0].name || fallbackName)
  321. }
  322. },
  323. fail: function() {
  324. // 用户取消或失败,静默处理
  325. }
  326. })
  327. },
  328. takePhoto: function() {
  329. var self = this
  330. if (!this.ensureTypeSelected()) return
  331. if (this.reportKind === 'a2') {
  332. uni.showToast({ title: '素质测评 A2 请上传 PDF 文件', icon: 'none' })
  333. return
  334. }
  335. this.ensureAgreed(function() {
  336. uni.chooseImage({
  337. count: 1,
  338. sourceType: ['camera', 'album'],
  339. success: function(res) {
  340. if (res.tempFilePaths && res.tempFilePaths.length > 0) {
  341. self.uploadReport(res.tempFilePaths[0], '照片')
  342. }
  343. }
  344. })
  345. })
  346. },
  347. uploadReport: function(filePath, fileName) {
  348. if (!filePath) return
  349. if (this.reportKind === 'a2') {
  350. this.uploadA2Report(filePath, fileName)
  351. return
  352. }
  353. var self = this
  354. self.uploading = true
  355. self.uploadingText = '正在上传文件...'
  356. uni.showLoading({ title: '上传中...' })
  357. // 菌群检测:走健康报告异步采集
  358. uploadReportOnly(filePath, self.uploadType).then(function(res) {
  359. uni.hideLoading()
  360. self.uploading = false
  361. var data = res.data || {}
  362. // 舌诊照片:后端直接返回 recordId + 舌诊指标,不走 draft/collect 流程
  363. if (data.type === 'tongue' || data.recordId) {
  364. var tonguePayload = {
  365. type: 'tongue',
  366. recordId: data.recordId,
  367. indicators: data.indicators || [],
  368. summary: { overallAssessment: data.overallAssessment || '' }
  369. }
  370. var url = '/pages/health/report-confirm?payload=' + encodeURIComponent(JSON.stringify(tonguePayload))
  371. if (self.memberId) url += '&memberId=' + self.memberId
  372. uni.redirectTo({ url: url })
  373. return
  374. }
  375. // 处理重复上传:duplicate=true
  376. if (data.duplicate) {
  377. if (data.collecting) {
  378. self.collectDraftId = data.draftId
  379. self.collecting = true
  380. self.collectError = data.message || '该报告正在解析中,请稍候'
  381. self.collectFileName = fileName || '报告文件'
  382. self.startCollectPolling(data.draftId)
  383. } else {
  384. // 已完成,跳转详情
  385. var reportId = data.reportId
  386. if (reportId) {
  387. self.goToDetail(reportId, 'gut_flora')
  388. }
  389. }
  390. uni.showToast({ title: data.message || '该报告已上传过', icon: 'none', duration: 3000 })
  391. return
  392. }
  393. var draftId = data.draftId
  394. if (!draftId) {
  395. uni.showToast({ title: (res && res.message) || '上传失败', icon: 'none' })
  396. return
  397. }
  398. self.collectDraftId = draftId
  399. self.collecting = true
  400. self.collectError = ''
  401. self.collectFileName = fileName || '报告文件'
  402. self.startCollectPolling(draftId)
  403. }).catch(function(err) {
  404. uni.hideLoading()
  405. self.uploading = false
  406. var msg = (err && err.message) || '上传失败,请重试'
  407. uni.showToast({ title: msg, icon: 'none' })
  408. })
  409. },
  410. uploadA2Report: function(filePath, fileName) {
  411. if (!this.ensureA2Member()) return
  412. var name = (fileName || '').toLowerCase()
  413. if (name && name.indexOf('.pdf') < 0 && name !== '聊天文件') {
  414. uni.showToast({ title: '素质测评 A2 仅支持 PDF', icon: 'none' })
  415. return
  416. }
  417. var self = this
  418. self.uploading = true
  419. self.uploadingText = '正在解析 A2 报告...'
  420. uni.showLoading({ title: '解析中...' })
  421. danParsePreview(filePath, 'mind', self.memberId).then(function(res) {
  422. var data = (res && res.data) || {}
  423. var uploadId = data.uploadId || data.draftId
  424. if (!uploadId) {
  425. uni.hideLoading()
  426. self.uploading = false
  427. uni.showToast({ title: (res && res.message) || 'A2 解析失败', icon: 'none' })
  428. return
  429. }
  430. var extra = {}
  431. try {
  432. extra = typeof data.extraJson === 'string' ? JSON.parse(data.extraJson) : (data.extraJson || {})
  433. } catch (e) {
  434. extra = {}
  435. }
  436. var childName = extra.name || ''
  437. var assessmentDate = extra.reportDate || ''
  438. if (assessmentDate && assessmentDate.length >= 10) {
  439. assessmentDate = assessmentDate.substring(0, 10)
  440. }
  441. return danConfirmReport({
  442. draftId: uploadId,
  443. childName: childName,
  444. assessmentDate: assessmentDate
  445. }).then(function(confirmRes) {
  446. uni.hideLoading()
  447. self.uploading = false
  448. var confirmData = (confirmRes && confirmRes.data) || {}
  449. var reportId = confirmData.uploadId || uploadId
  450. uni.showToast({ title: 'A2 报告已入库', icon: 'success' })
  451. setTimeout(function() {
  452. uni.redirectTo({ url: '/pages/dan-assessment/report-result?reportId=' + reportId })
  453. }, 600)
  454. })
  455. }).catch(function(err) {
  456. uni.hideLoading()
  457. self.uploading = false
  458. var msg = (err && err.message) || 'A2 解析失败,请重试'
  459. uni.showToast({ title: msg, icon: 'none', duration: 3000 })
  460. })
  461. },
  462. stopCollectPolling: function() {
  463. if (this.collectTimer) {
  464. clearInterval(this.collectTimer)
  465. this.collectTimer = null
  466. }
  467. },
  468. startCollectPolling: function(draftId) {
  469. var self = this
  470. self.stopCollectPolling()
  471. self.collectPollCount = 0
  472. self.pollCollectStatus(draftId)
  473. self.collectTimer = setInterval(function() {
  474. self.pollCollectStatus(draftId)
  475. }, 4000)
  476. },
  477. pollCollectStatus: function(draftId) {
  478. var self = this
  479. self.collectPollCount = (self.collectPollCount || 0) + 1
  480. // 约 6 分钟仍未完成:停轮询,提示去列表看进度
  481. if (self.collectPollCount > 90) {
  482. self.stopCollectPolling()
  483. self.collectError = '解析耗时较长,可去报告列表查看进度,无需重复上传'
  484. return
  485. }
  486. getReportCollectStatus(draftId).then(function(res) {
  487. var data = res.data || {}
  488. var status = data.parseStatus
  489. if (status === 'completed') {
  490. self.stopCollectPolling()
  491. self.collecting = false
  492. uni.showToast({ title: '报告已采集完成', icon: 'success', duration: 2000 })
  493. var reportId = data.reportId
  494. var reportType = data.reportType || 'gut_flora'
  495. setTimeout(function() {
  496. if (reportId) {
  497. // 统一跳转到健康报告详情页(后端指纹识别后统一入库到 health_report 表)
  498. if (reportType === 'gut_flora') {
  499. uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
  500. } else {
  501. uni.redirectTo({ url: '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId })
  502. }
  503. } else {
  504. uni.redirectTo({ url: '/pages/health/report-list' })
  505. }
  506. }, 600)
  507. } else if (status === 'failed') {
  508. self.stopCollectPolling()
  509. self.collectError = data.parseError || '报告采集失败,请重试'
  510. uni.showToast({ title: self.collectError, icon: 'none', duration: 3000 })
  511. }
  512. }).catch(function() {
  513. // 轮询失败静默,下次继续
  514. })
  515. },
  516. goReportList: function() {
  517. this.stopCollectPolling()
  518. uni.redirectTo({ url: '/pages/health/report-list' })
  519. },
  520. goToDetail: function(reportId, reportType) {
  521. var url
  522. if (reportType === 'gut_flora') {
  523. url = '/pages/health/gut-flora-detail?reportId=' + reportId
  524. } else {
  525. url = '/pages/health/report-detail?reportType=' + (reportType || 'gut_flora') + '&reportId=' + reportId
  526. }
  527. uni.redirectTo({ url: url })
  528. },
  529. cancelCollect: function() {
  530. this.stopCollectPolling()
  531. this.collecting = false
  532. this.collectDraftId = null
  533. this.collectFileName = ''
  534. this.collectError = ''
  535. }
  536. }
  537. }
  538. </script>
  539. <style scoped>
  540. .upload-container {
  541. min-height: 100vh;
  542. background: #F5F7FA;
  543. padding-bottom: env(safe-area-inset-bottom);
  544. }
  545. .tip-banner {
  546. display: flex;
  547. flex-direction: row;
  548. align-items: center;
  549. background: #E3F2FD;
  550. margin: 20rpx 30rpx;
  551. padding: 20rpx 24rpx;
  552. border-radius: 16rpx;
  553. }
  554. .tip-icon {
  555. font-size: 32rpx;
  556. margin-right: 16rpx;
  557. flex-shrink: 0;
  558. }
  559. .tip-text {
  560. font-size: 24rpx;
  561. color: #1565C0;
  562. line-height: 1.5;
  563. }
  564. .form-section {
  565. margin: 0 30rpx;
  566. }
  567. .type-section {
  568. margin-bottom: 20rpx;
  569. }
  570. .type-methods {
  571. display: flex;
  572. flex-direction: row;
  573. gap: 16rpx;
  574. }
  575. .type-card {
  576. flex: 1;
  577. display: flex;
  578. flex-direction: column;
  579. align-items: flex-start;
  580. padding: 24rpx 20rpx;
  581. border-radius: 16rpx;
  582. background: #fff;
  583. border: 2rpx solid #E2E8F0;
  584. }
  585. .type-card-active {
  586. border-color: #5B9BD5;
  587. background: #F0F7FF;
  588. }
  589. .type-gut.type-card-active {
  590. border-color: #22C55E;
  591. background: #F0FDF4;
  592. }
  593. .type-a2.type-card-active {
  594. border-color: #8B5CF6;
  595. background: #F5F3FF;
  596. }
  597. .type-card-icon {
  598. font-size: 40rpx;
  599. margin-bottom: 10rpx;
  600. }
  601. .type-card-title {
  602. font-size: 26rpx;
  603. font-weight: 600;
  604. color: #1E293B;
  605. margin-bottom: 6rpx;
  606. }
  607. .type-card-desc {
  608. font-size: 20rpx;
  609. color: #94A3B8;
  610. line-height: 1.5;
  611. }
  612. .member-box {
  613. background: #fff;
  614. border-radius: 16rpx;
  615. padding: 24rpx;
  616. margin-bottom: 20rpx;
  617. }
  618. .member-list {
  619. display: flex;
  620. flex-direction: row;
  621. flex-wrap: wrap;
  622. gap: 16rpx;
  623. }
  624. .member-item {
  625. display: flex;
  626. flex-direction: column;
  627. align-items: center;
  628. padding: 16rpx 20rpx;
  629. background: #F5F7FA;
  630. border-radius: 16rpx;
  631. border: 2rpx solid transparent;
  632. min-width: 120rpx;
  633. }
  634. .member-selected {
  635. background: #EDE9FE;
  636. border-color: #8B5CF6;
  637. }
  638. .member-hover {
  639. opacity: 0.85;
  640. }
  641. .member-avatar {
  642. width: 64rpx;
  643. height: 64rpx;
  644. border-radius: 50%;
  645. background: #8B5CF6;
  646. color: #fff;
  647. font-size: 28rpx;
  648. font-weight: bold;
  649. text-align: center;
  650. line-height: 64rpx;
  651. margin-bottom: 8rpx;
  652. }
  653. .member-name {
  654. font-size: 24rpx;
  655. color: #333;
  656. }
  657. .member-empty {
  658. font-size: 24rpx;
  659. color: #94A3B8;
  660. }
  661. .form-item {
  662. background: #fff;
  663. border-radius: 16rpx;
  664. padding: 24rpx;
  665. margin-bottom: 20rpx;
  666. }
  667. .form-label {
  668. font-size: 26rpx;
  669. font-weight: bold;
  670. color: #333;
  671. display: block;
  672. margin-bottom: 16rpx;
  673. }
  674. /* 上传方式卡片 */
  675. .upload-methods {
  676. display: flex;
  677. flex-direction: row;
  678. gap: 16rpx;
  679. margin-bottom: 20rpx;
  680. }
  681. .upload-card {
  682. flex: 1;
  683. display: flex;
  684. flex-direction: column;
  685. align-items: center;
  686. justify-content: center;
  687. padding: 32rpx 12rpx;
  688. border-radius: 20rpx;
  689. text-align: center;
  690. min-height: 200rpx;
  691. }
  692. .upload-card:active {
  693. opacity: 0.85;
  694. }
  695. .upload-card-icon {
  696. width: 80rpx;
  697. height: 80rpx;
  698. border-radius: 20rpx;
  699. font-size: 40rpx;
  700. display: flex;
  701. align-items: center;
  702. justify-content: center;
  703. margin-bottom: 16rpx;
  704. }
  705. .upload-card-title {
  706. font-size: 26rpx;
  707. font-weight: 600;
  708. color: #333;
  709. margin-bottom: 8rpx;
  710. }
  711. .upload-card-desc {
  712. font-size: 20rpx;
  713. color: #94A3B8;
  714. line-height: 1.5;
  715. }
  716. .card-chat {
  717. background: #F0F7FF;
  718. border: 2rpx dashed #93C5FD;
  719. }
  720. .card-chat .upload-card-icon {
  721. background: linear-gradient(135deg, #3B82F6, #1D4ED8);
  722. color: #fff;
  723. }
  724. .card-photo {
  725. background: #FFF7ED;
  726. border: 2rpx solid #FED7AA;
  727. }
  728. .card-photo .upload-card-icon {
  729. background: linear-gradient(135deg, #FB923C, #EA580C);
  730. color: #fff;
  731. }
  732. /* 采集中面板 */
  733. .collecting-panel {
  734. margin-bottom: 20rpx;
  735. }
  736. .collecting-card {
  737. display: flex;
  738. flex-direction: row;
  739. align-items: flex-start;
  740. background: #FFF8E6;
  741. border: 1rpx solid #F5D78E;
  742. border-radius: 16rpx;
  743. padding: 28rpx 24rpx;
  744. }
  745. .collecting-icon {
  746. width: 56rpx;
  747. height: 56rpx;
  748. border-radius: 50%;
  749. background: #F0B429;
  750. display: flex;
  751. align-items: center;
  752. justify-content: center;
  753. margin-right: 20rpx;
  754. flex-shrink: 0;
  755. }
  756. .spinner {
  757. width: 28rpx;
  758. height: 28rpx;
  759. border: 4rpx solid rgba(255,255,255,0.4);
  760. border-top-color: #fff;
  761. border-radius: 50%;
  762. animation: collecting-spin 1s linear infinite;
  763. }
  764. @keyframes collecting-spin {
  765. 0% { transform: rotate(0deg); }
  766. 100% { transform: rotate(360deg); }
  767. }
  768. .collecting-fail-mark {
  769. font-size: 32rpx;
  770. color: #fff;
  771. font-weight: bold;
  772. }
  773. .collecting-info {
  774. flex: 1;
  775. display: flex;
  776. flex-direction: column;
  777. min-width: 0;
  778. }
  779. .collecting-title {
  780. font-size: 30rpx;
  781. font-weight: 600;
  782. color: #7A5C00;
  783. }
  784. .collecting-file {
  785. font-size: 24rpx;
  786. color: #A08A45;
  787. margin-top: 6rpx;
  788. overflow: hidden;
  789. text-overflow: ellipsis;
  790. white-space: nowrap;
  791. }
  792. .collecting-hint {
  793. font-size: 22rpx;
  794. color: #C0A75A;
  795. margin-top: 8rpx;
  796. line-height: 1.5;
  797. }
  798. .collecting-actions {
  799. display: flex;
  800. flex-direction: row;
  801. gap: 16rpx;
  802. margin-top: 16rpx;
  803. }
  804. .collecting-btn {
  805. flex: 1;
  806. height: 72rpx;
  807. line-height: 72rpx;
  808. text-align: center;
  809. background: #5B9BD5;
  810. color: #fff;
  811. font-size: 28rpx;
  812. border-radius: 12rpx;
  813. }
  814. .collecting-cancel {
  815. padding: 0 24rpx;
  816. height: 72rpx;
  817. line-height: 72rpx;
  818. text-align: center;
  819. background: #fff;
  820. border: 1rpx solid #D1D5DB;
  821. color: #666;
  822. font-size: 28rpx;
  823. border-radius: 12rpx;
  824. }
  825. /* 上传中状态 */
  826. .analyzing-status {
  827. display: flex;
  828. flex-direction: row;
  829. align-items: center;
  830. }
  831. .analyzing-text {
  832. font-size: 26rpx;
  833. color: #5B9BD5;
  834. }
  835. /* 责任声明 */
  836. .disclaimer-box {
  837. background: #FFF8F0;
  838. border: 2rpx solid #FDE68A;
  839. }
  840. .disclaimer-check {
  841. display: flex;
  842. flex-direction: row;
  843. align-items: center;
  844. margin-bottom: 12rpx;
  845. }
  846. .disclaimer-checkbox {
  847. width: 36rpx;
  848. height: 36rpx;
  849. border-radius: 6rpx;
  850. border: 2rpx solid #CBD5E1;
  851. background: #fff;
  852. display: flex;
  853. align-items: center;
  854. justify-content: center;
  855. flex-shrink: 0;
  856. margin-right: 12rpx;
  857. }
  858. .disclaimer-checked {
  859. background: #5B9BD5;
  860. border-color: #5B9BD5;
  861. }
  862. .disclaimer-checkmark {
  863. font-size: 24rpx;
  864. color: #fff;
  865. font-weight: bold;
  866. }
  867. .disclaimer-label {
  868. font-size: 24rpx;
  869. color: #333;
  870. font-weight: 500;
  871. }
  872. .disclaimer-content {
  873. background: #FFFDF5;
  874. border-radius: 10rpx;
  875. padding: 16rpx;
  876. margin-left: 48rpx;
  877. }
  878. .disclaimer-text {
  879. font-size: 22rpx;
  880. color: #92400E;
  881. line-height: 1.6;
  882. }
  883. /* 上传说明 */
  884. .guide-section {
  885. margin: 0 30rpx 30rpx;
  886. background: #fff;
  887. border-radius: 20rpx;
  888. padding: 28rpx 24rpx;
  889. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  890. }
  891. .guide-header {
  892. display: flex;
  893. flex-direction: row;
  894. align-items: center;
  895. margin-bottom: 20rpx;
  896. }
  897. .guide-header-icon {
  898. font-size: 32rpx;
  899. margin-right: 12rpx;
  900. }
  901. .guide-header-title {
  902. font-size: 28rpx;
  903. font-weight: 600;
  904. color: #1E293B;
  905. }
  906. .guide-item {
  907. display: flex;
  908. flex-direction: row;
  909. align-items: flex-start;
  910. margin-bottom: 20rpx;
  911. }
  912. .guide-icon-box {
  913. width: 64rpx;
  914. height: 64rpx;
  915. border-radius: 16rpx;
  916. display: flex;
  917. align-items: center;
  918. justify-content: center;
  919. margin-right: 16rpx;
  920. flex-shrink: 0;
  921. }
  922. .guide-icon {
  923. font-size: 32rpx;
  924. }
  925. .guide-icon-blue { background: #DBEAFE; }
  926. .guide-icon-green { background: #DCFCE7; }
  927. .guide-icon-orange { background: #FFEDD5; }
  928. .guide-icon-purple { background: #EDE9FE; }
  929. .guide-icon-teal { background: #CCFBF1; }
  930. .guide-info {
  931. flex: 1;
  932. }
  933. .guide-name {
  934. font-size: 26rpx;
  935. font-weight: 600;
  936. color: #334155;
  937. display: block;
  938. margin-bottom: 4rpx;
  939. }
  940. .guide-desc {
  941. font-size: 22rpx;
  942. color: #94A3B8;
  943. line-height: 1.5;
  944. }
  945. .guide-tips {
  946. margin-top: 8rpx;
  947. padding-top: 16rpx;
  948. border-top: 1rpx solid #F1F5F9;
  949. }
  950. .guide-tips-title {
  951. font-size: 24rpx;
  952. font-weight: 600;
  953. color: #64748B;
  954. display: block;
  955. margin-bottom: 8rpx;
  956. }
  957. .guide-tips-item {
  958. font-size: 22rpx;
  959. color: #94A3B8;
  960. display: block;
  961. line-height: 1.7;
  962. }
  963. </style>