report-upload.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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">上传体检报告或肠道检测报告,以便为您解读并提供个性化营养方案</text>
  13. </view>
  14. <!-- 表单 -->
  15. <view class="form-section">
  16. <!-- 上传方式:并排卡片 -->
  17. <view class="upload-methods">
  18. <view class="upload-card card-file" @click="pickFile">
  19. <view class="upload-card-icon">📄</view>
  20. <text class="upload-card-title">从聊天选择文件</text>
  21. <text class="upload-card-desc">PDF / Word / 图片<br/>体检报告、肠道检测报告</text>
  22. </view>
  23. <view class="upload-card card-photo" @click="takePhoto">
  24. <view class="upload-card-icon">📷</view>
  25. <text class="upload-card-title">拍照上传</text>
  26. <text class="upload-card-desc">拍摄舌象照片<br/>或单页纸质报告</text>
  27. </view>
  28. </view>
  29. <!-- 责任声明 -->
  30. <view class="form-item disclaimer-box">
  31. <view class="disclaimer-check" @click="agreedToTerms = !agreedToTerms">
  32. <view class="disclaimer-checkbox" :class="{ 'disclaimer-checked': agreedToTerms }">
  33. <text class="disclaimer-checkmark" v-if="agreedToTerms">✓</text>
  34. </view>
  35. <text class="disclaimer-label">我已阅读并同意以下声明</text>
  36. </view>
  37. <view class="disclaimer-content">
  38. <text class="disclaimer-text">我同意平台使用本报告数据进行分析,为我提供个性化健康解决方案;未经我授权,平台不会以任何形式将我的报告内容提供给其他任何个人或机构。</text>
  39. </view>
  40. </view>
  41. <!-- 上传中 -->
  42. <view class="form-item" v-if="uploading">
  43. <text class="form-label">上传中...</text>
  44. <view class="analyzing-status">
  45. <text class="analyzing-text">正在上传文件,即将进入分析页面...</text>
  46. </view>
  47. </view>
  48. </view>
  49. <!-- 底部占位 -->
  50. <view class="bottom-spacer"></view>
  51. <!-- 菌群测评商品 -->
  52. <view class="product-section" v-if="recommendedProducts.length > 0">
  53. <view class="product-header">
  54. <text class="product-header-icon">📦</text>
  55. <text class="product-header-title">为家人购买检测服务</text>
  56. </view>
  57. <view class="product-grid">
  58. <view
  59. class="product-card"
  60. v-for="item in recommendedProducts"
  61. :key="item.id"
  62. @click="goToProduct(item.id)"
  63. >
  64. <image class="product-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
  65. <text class="product-name">{{ item.name }}</text>
  66. <text class="product-price">{{ formatPriceWithSymbol(item.price) }}</text>
  67. <view class="product-btn">
  68. <text class="product-btn-text">去购买</text>
  69. </view>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. </template>
  75. <script>
  76. import { getProductsByDomain, uploadReportOnly } from '../../utils/api.js'
  77. import config from '@/config.js'
  78. export default {
  79. data() {
  80. return {
  81. memberId: null,
  82. uploading: false,
  83. selectedFile: null,
  84. photoPath: null,
  85. agreedToTerms: false,
  86. // 购买推荐
  87. recommendedProducts: [],
  88. loadingProducts: false
  89. }
  90. },
  91. onLoad: function(options) {
  92. this.memberId = options.memberId ? parseInt(options.memberId) : null
  93. this.loadRecommendProducts()
  94. },
  95. onShow: function() {
  96. this.loadRecommendProducts()
  97. },
  98. methods: {
  99. // 未勾选声明时弹窗确认
  100. ensureAgreed(isPhoto) {
  101. if (this.agreedToTerms) {
  102. this.startPick(isPhoto)
  103. return
  104. }
  105. var self = this
  106. uni.showModal({
  107. title: '数据使用声明',
  108. content: '上传报告即代表您同意平台使用报告数据进行分析并提供个性化健康解决方案;平台承诺未经您授权,不会以任何形式将您的报告提供给其他任何个人或机构。点击「同意并继续」即表示您已阅读并同意。',
  109. confirmText: '同意并继续',
  110. cancelText: '暂不',
  111. success: function(res) {
  112. if (res.confirm) {
  113. self.agreedToTerms = true
  114. self.startPick(isPhoto)
  115. }
  116. }
  117. })
  118. },
  119. startPick(isPhoto) {
  120. var self = this
  121. if (isPhoto) {
  122. uni.chooseImage({
  123. count: 1,
  124. sourceType: ['camera', 'album'],
  125. success: function(res) {
  126. self.photoPath = res.tempFilePaths[0]
  127. self.uploadAndGoConfirm(self.photoPath, 'image')
  128. }
  129. })
  130. } else {
  131. uni.chooseMessageFile({
  132. count: 1,
  133. type: 'file',
  134. extension: ['pdf'],
  135. success: function(res) {
  136. var files = res.tempFiles
  137. if (files && files.length > 0) {
  138. self.selectedFile = files[0]
  139. self.uploadAndGoConfirm(files[0].path, '')
  140. }
  141. },
  142. fail: function(e) {
  143. console.log('选择文件取消或失败', e)
  144. }
  145. })
  146. }
  147. },
  148. // ===== 拍照上传 =====
  149. takePhoto() {
  150. this.ensureAgreed(true)
  151. },
  152. // ===== 文件上传 =====
  153. pickFile() {
  154. this.ensureAgreed(false)
  155. },
  156. // 上传文件 → 自动跳转确认页(确认页按 draftId 触发解析并自动刷新显示结果)
  157. uploadAndGoConfirm(filePath, type) {
  158. var self = this
  159. if (!filePath) return
  160. self.uploading = true
  161. uni.showLoading({ title: '上传中...' })
  162. uploadReportOnly(filePath, type).then(function(res) {
  163. uni.hideLoading()
  164. self.uploading = false
  165. var draftId = res.data && res.data.draftId
  166. if (!draftId) {
  167. uni.showToast({ title: (res && res.message) || '上传失败', icon: 'none' })
  168. return
  169. }
  170. // 只传 draftId,确认页自行解析
  171. uni.redirectTo({ url: '/pages/health/report-confirm?draftId=' + draftId })
  172. }).catch(function(err) {
  173. uni.hideLoading()
  174. self.uploading = false
  175. var msg = (err && err.message) || '上传失败,请重试'
  176. uni.showToast({ title: msg, icon: 'none' })
  177. })
  178. },
  179. loadRecommendProducts: function() {
  180. var self = this
  181. self.loadingProducts = true
  182. // 直接按身体维度加载推荐商品(商城已有的 domain 筛选器)
  183. getProductsByDomain('body', 1, 4).then(function(res) {
  184. if (res && res.data && res.data.records) {
  185. self.recommendedProducts = res.data.records
  186. } else {
  187. self.recommendedProducts = []
  188. }
  189. }).catch(function() {
  190. self.recommendedProducts = []
  191. }).finally(function() {
  192. self.loadingProducts = false
  193. })
  194. },
  195. goToProduct: function(productId) {
  196. // 直接进入结算页,跳过商品详情页
  197. var product = this.recommendedProducts.find(function(p) { return p.id === productId })
  198. if (product) {
  199. var productData = {
  200. productId: product.id,
  201. productName: product.name,
  202. coverImage: product.coverImage || '',
  203. unitPrice: product.price || 0,
  204. quantity: 1
  205. }
  206. uni.setStorageSync('checkoutItems', [productData])
  207. uni.navigateTo({ url: '/pages/shop/checkout/checkout?from=report_upload&id=' + productId })
  208. } else {
  209. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + productId })
  210. }
  211. },
  212. formatPriceWithSymbol: function(cents) {
  213. if (cents === null || cents === undefined) return '¥0'
  214. return '¥' + (cents / 100).toFixed(2)
  215. },
  216. goBack() {
  217. uni.navigateBack()
  218. },
  219. getImageUrl: function(path) {
  220. return config.API_BASE_URL + path
  221. }
  222. }
  223. }
  224. </script>
  225. <style scoped>
  226. .upload-container {
  227. min-height: 100vh;
  228. background: #F5F7FA;
  229. }
  230. /* ===== 导航栏 ===== */
  231. .nav-bar {
  232. position: sticky;
  233. top: 0;
  234. background: #fff;
  235. display: flex;
  236. flex-direction: row;
  237. align-items: center;
  238. justify-content: space-between;
  239. padding: 20rpx 30rpx;
  240. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  241. z-index: 100;
  242. }
  243. .nav-back {
  244. font-size: 32rpx;
  245. color: #5B9BD5;
  246. }
  247. .nav-title {
  248. font-size: 32rpx;
  249. font-weight: bold;
  250. color: #333;
  251. }
  252. .nav-placeholder {
  253. width: 80rpx;
  254. }
  255. /* ===== 提示条 ===== */
  256. .tip-banner {
  257. display: flex;
  258. flex-direction: row;
  259. align-items: center;
  260. background: #E3F2FD;
  261. margin: 20rpx 30rpx;
  262. padding: 20rpx 24rpx;
  263. border-radius: 16rpx;
  264. }
  265. .tip-icon {
  266. font-size: 32rpx;
  267. margin-right: 16rpx;
  268. flex-shrink: 0;
  269. }
  270. .tip-text {
  271. font-size: 24rpx;
  272. color: #1565C0;
  273. line-height: 1.5;
  274. }
  275. /* ===== 表单 ===== */
  276. .form-section {
  277. margin: 0 30rpx;
  278. }
  279. .form-item {
  280. background: #fff;
  281. border-radius: 16rpx;
  282. padding: 24rpx;
  283. margin-bottom: 20rpx;
  284. }
  285. .form-label {
  286. font-size: 26rpx;
  287. font-weight: bold;
  288. color: #333;
  289. display: block;
  290. margin-bottom: 16rpx;
  291. }
  292. /* 上传中状态 */
  293. .analyzing-status {
  294. display: flex;
  295. flex-direction: row;
  296. align-items: center;
  297. }
  298. .analyzing-text {
  299. font-size: 26rpx;
  300. color: #5B9BD5;
  301. }
  302. /* ===== 底部 ===== */
  303. .bottom-spacer {
  304. height: 60rpx;
  305. }
  306. /* ===== 责任声明 ===== */
  307. .disclaimer-box {
  308. background: #FFF8F0;
  309. border: 2rpx solid #FDE68A;
  310. }
  311. .disclaimer-check {
  312. display: flex;
  313. flex-direction: row;
  314. align-items: center;
  315. margin-bottom: 12rpx;
  316. }
  317. .disclaimer-checkbox {
  318. width: 36rpx;
  319. height: 36rpx;
  320. border-radius: 6rpx;
  321. border: 2rpx solid #CBD5E1;
  322. background: #fff;
  323. display: flex;
  324. align-items: center;
  325. justify-content: center;
  326. flex-shrink: 0;
  327. margin-right: 12rpx;
  328. }
  329. .disclaimer-checked {
  330. background: #5B9BD5;
  331. border-color: #5B9BD5;
  332. }
  333. .disclaimer-checkmark {
  334. font-size: 24rpx;
  335. color: #fff;
  336. font-weight: bold;
  337. }
  338. .disclaimer-label {
  339. font-size: 24rpx;
  340. color: #333;
  341. font-weight: 500;
  342. }
  343. .disclaimer-content {
  344. background: #FFFDF5;
  345. border-radius: 10rpx;
  346. padding: 16rpx;
  347. margin-left: 48rpx;
  348. }
  349. .disclaimer-text {
  350. font-size: 22rpx;
  351. color: #92400E;
  352. line-height: 1.6;
  353. }
  354. /* ===== 上传方式:并排卡片 ===== */
  355. .upload-methods {
  356. display: flex;
  357. flex-direction: row;
  358. gap: 20rpx;
  359. margin-bottom: 20rpx;
  360. }
  361. .upload-card {
  362. flex: 1;
  363. display: flex;
  364. flex-direction: column;
  365. align-items: center;
  366. justify-content: center;
  367. padding: 40rpx 20rpx;
  368. border-radius: 20rpx;
  369. text-align: center;
  370. min-height: 240rpx;
  371. }
  372. .upload-card:active {
  373. opacity: 0.85;
  374. }
  375. .upload-card-icon {
  376. width: 96rpx;
  377. height: 96rpx;
  378. border-radius: 24rpx;
  379. font-size: 48rpx;
  380. display: flex;
  381. align-items: center;
  382. justify-content: center;
  383. margin-bottom: 20rpx;
  384. }
  385. .upload-card-title {
  386. font-size: 28rpx;
  387. font-weight: 600;
  388. color: #333;
  389. margin-bottom: 10rpx;
  390. }
  391. .upload-card-desc {
  392. font-size: 22rpx;
  393. color: #94A3B8;
  394. line-height: 1.5;
  395. }
  396. .card-file {
  397. background: #F0F7FF;
  398. border: 2rpx dashed #93C5FD;
  399. }
  400. .card-file .upload-card-icon {
  401. background: linear-gradient(135deg, #3B82F6, #1D4ED8);
  402. color: #fff;
  403. box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.3);
  404. }
  405. .card-photo {
  406. background: #FFF7ED;
  407. border: 2rpx solid #FED7AA;
  408. }
  409. .card-photo .upload-card-icon {
  410. background: linear-gradient(135deg, #FB923C, #EA580C);
  411. color: #fff;
  412. box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.25);
  413. }
  414. /* ===== 菌群测评商品 ===== */
  415. .product-section {
  416. margin: 0 30rpx 30rpx;
  417. }
  418. .product-header {
  419. display: flex;
  420. flex-direction: row;
  421. align-items: center;
  422. margin-bottom: 16rpx;
  423. }
  424. .product-header-icon {
  425. font-size: 28rpx;
  426. margin-right: 8rpx;
  427. }
  428. .product-header-title {
  429. font-size: 26rpx;
  430. font-weight: bold;
  431. color: #333;
  432. }
  433. .product-grid {
  434. display: flex;
  435. flex-direction: row;
  436. flex-wrap: wrap;
  437. gap: 16rpx;
  438. }
  439. .product-card {
  440. width: calc(50% - 8rpx);
  441. background: #fff;
  442. border-radius: 16rpx;
  443. padding: 20rpx;
  444. box-sizing: border-box;
  445. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  446. }
  447. .product-card:active {
  448. opacity: 0.85;
  449. }
  450. .product-cover {
  451. width: 100%;
  452. height: 160rpx;
  453. border-radius: 10rpx;
  454. background: #F1F5F9;
  455. margin-bottom: 12rpx;
  456. }
  457. .product-name {
  458. display: block;
  459. font-size: 24rpx;
  460. color: #333;
  461. font-weight: 500;
  462. line-height: 1.3;
  463. overflow: hidden;
  464. text-overflow: ellipsis;
  465. white-space: nowrap;
  466. margin-bottom: 6rpx;
  467. }
  468. .product-price {
  469. display: block;
  470. font-size: 26rpx;
  471. color: #EF4444;
  472. font-weight: bold;
  473. margin-bottom: 12rpx;
  474. }
  475. .product-btn {
  476. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  477. border-radius: 8rpx;
  478. padding: 12rpx 0;
  479. text-align: center;
  480. }
  481. .product-btn-text {
  482. font-size: 24rpx;
  483. color: #fff;
  484. font-weight: 500;
  485. }
  486. </style>