report-upload.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <template>
  2. <view class="page">
  3. <view class="header">
  4. <view class="back-btn" @click="goBack">
  5. <text class="back-icon">&#x2190;</text>
  6. <text class="back-text">返回</text>
  7. </view>
  8. <text class="header-title">DAN 报告上传</text>
  9. </view>
  10. <!-- Step 1: 选择维度 + 选择文件 -->
  11. <view class="section" v-if="step === 1">
  12. <view class="dimension-select">
  13. <text class="dimension-label">报告类型</text>
  14. <view class="dimension-options">
  15. <view
  16. :class="['dimension-option', dimension === 'mind' ? 'active' : '']"
  17. @click="dimension = 'mind'">
  18. <text class="dimension-name">&#x2764; 心维度 (A2)</text>
  19. </view>
  20. <view
  21. :class="['dimension-option', dimension === 'wisdom' ? 'active' : '']"
  22. @click="dimension = 'wisdom'">
  23. <text class="dimension-name">&#x1F9E0; 智维度 (B4)</text>
  24. </view>
  25. </view>
  26. </view>
  27. <view class="upload-area" @click="chooseFile">
  28. <view class="upload-icon" v-if="!selectedFile">&#x1F4C4;</view>
  29. <view class="upload-icon" v-else>&#x2705;</view>
  30. <text class="upload-text" v-if="!selectedFile">点击上传 PDF 文件</text>
  31. <text class="upload-text" v-else>{{ selectedFile.name }}</text>
  32. <text class="upload-hint">支持 PDF 格式的 DAN 测评报告</text>
  33. </view>
  34. <view class="action-bar">
  35. <view class="upload-btn" @click="startUpload" v-if="selectedFile">
  36. <text class="upload-btn-text">上传并解析</text>
  37. </view>
  38. </view>
  39. </view>
  40. <!-- Step 2: 预览解析结果 -->
  41. <view class="section" v-if="step === 2">
  42. <view class="preview-header">
  43. <text class="preview-title">解析预览</text>
  44. <text class="preview-desc">请确认以下数据是否准确</text>
  45. </view>
  46. <view class="preview-card" v-if="previewData">
  47. <view class="preview-row">
  48. <text class="preview-label">维度</text>
  49. <text class="preview-value">{{ dimension === 'mind' ? '心维度 (A2)' : '智维度 (B4)' }}</text>
  50. </view>
  51. <view class="preview-row">
  52. <text class="preview-label">解析概要</text>
  53. <text class="preview-value">{{ previewData.summary || '无' }}</text>
  54. </view>
  55. </view>
  56. <!-- 解析项列表 -->
  57. <view class="items-section" v-if="previewData && previewData.items && previewData.items.length > 0">
  58. <text class="items-title">数据项 ({{ previewData.items.length }})</text>
  59. <view class="item-card" v-for="(item, idx) in previewData.items" :key="idx">
  60. <view class="item-header">
  61. <text class="item-name">{{ item.name || item.code }}</text>
  62. <text class="item-category">{{ item.category }}</text>
  63. </view>
  64. <view class="item-value-bar">
  65. <view class="item-value-fill" :style="'width:' + item.value + '%'"></view>
  66. </view>
  67. <text class="item-value-text">{{ item.value }}/100</text>
  68. </view>
  69. </view>
  70. <!-- 建议 -->
  71. <view class="suggestions-card" v-if="previewData && previewData.suggestions">
  72. <text class="suggestions-title">成长建议</text>
  73. <text class="suggestions-text">{{ previewData.suggestions }}</text>
  74. </view>
  75. <view class="action-bar">
  76. <view class="confirm-btn" @click="doConfirm" v-if="!confirming">
  77. <text class="confirm-btn-text">确认并保存</text>
  78. </view>
  79. <view class="confirm-btn disabled" v-else>
  80. <text class="confirm-btn-text">保存中...</text>
  81. </view>
  82. <view class="retry-btn" @click="resetUpload">
  83. <text class="retry-btn-text">重新上传</text>
  84. </view>
  85. </view>
  86. </view>
  87. <!-- Step 3: 完成 -->
  88. <view class="section" v-if="step === 3">
  89. <view class="success-card">
  90. <text class="success-icon">&#x2705;</text>
  91. <text class="success-title">报告已保存</text>
  92. <text class="success-desc">DAN 测评数据已更新至五维能量体系</text>
  93. </view>
  94. <view class="action-bar">
  95. <view class="upload-btn" @click="goBack">
  96. <text class="upload-btn-text">返回</text>
  97. </view>
  98. </view>
  99. </view>
  100. <!-- Loading -->
  101. <view class="loading-overlay" v-if="loading">
  102. <view class="loading-box">
  103. <text class="loading-text">解析中...</text>
  104. </view>
  105. </view>
  106. </view>
  107. </template>
  108. <script>
  109. import { danParsePreview, danConfirmReport } from '../../utils/api.js'
  110. export default {
  111. data() {
  112. return {
  113. step: 1,
  114. dimension: 'mind',
  115. selectedFile: null,
  116. previewData: null,
  117. draftId: null,
  118. loading: false,
  119. confirming: false
  120. }
  121. },
  122. methods: {
  123. goBack: function() {
  124. uni.navigateBack()
  125. },
  126. chooseFile: function() {
  127. var self = this
  128. uni.chooseMessageFile({
  129. count: 1,
  130. type: 'file',
  131. extension: ['pdf'],
  132. success: function(res) {
  133. if (res.tempFiles && res.tempFiles.length > 0) {
  134. self.selectedFile = res.tempFiles[0]
  135. }
  136. },
  137. fail: function() {
  138. // 降级:使用相册/相机
  139. uni.chooseImage({
  140. count: 1,
  141. success: function(res) {
  142. if (res.tempFiles && res.tempFiles.length > 0) {
  143. self.selectedFile = res.tempFiles[0]
  144. }
  145. }
  146. })
  147. }
  148. })
  149. },
  150. startUpload: function() {
  151. if (!this.selectedFile) return
  152. var self = this
  153. self.loading = true
  154. var memberId = this.$store.state && this.$store.state.currentChildId
  155. ? this.$store.state.currentChildId
  156. : uni.getStorageSync('currentChildId')
  157. danParsePreview(self.selectedFile.path || self.selectedFile.tempFilePath, self.dimension, memberId)
  158. .then(function(res) {
  159. self.loading = false
  160. if (res.code === 200 && res.data) {
  161. self.previewData = res.data
  162. self.draftId = res.data.draftId
  163. self.step = 2
  164. } else {
  165. uni.showToast({ title: res.message || '解析失败', icon: 'none' })
  166. }
  167. })
  168. .catch(function(err) {
  169. self.loading = false
  170. uni.showToast({ title: err && err.message ? err.message : '上传失败', icon: 'none' })
  171. })
  172. },
  173. doConfirm: function() {
  174. var self = this
  175. self.confirming = true
  176. var memberId = this.$store.state && this.$store.state.currentChildId
  177. ? this.$store.state.currentChildId
  178. : uni.getStorageSync('currentChildId')
  179. danConfirmReport({
  180. draftId: self.draftId,
  181. dimension: self.dimension,
  182. memberId: memberId
  183. })
  184. .then(function(res) {
  185. self.confirming = false
  186. if (res.code === 200) {
  187. self.step = 3
  188. } else {
  189. uni.showToast({ title: res.message || '保存失败', icon: 'none' })
  190. }
  191. })
  192. .catch(function(err) {
  193. self.confirming = false
  194. uni.showToast({ title: '保存失败', icon: 'none' })
  195. })
  196. },
  197. resetUpload: function() {
  198. this.step = 1
  199. this.selectedFile = null
  200. this.previewData = null
  201. this.draftId = null
  202. }
  203. }
  204. }
  205. </script>
  206. <style>
  207. .page {
  208. min-height: 100vh;
  209. background: #F5F5F7;
  210. padding-bottom: 40rpx;
  211. }
  212. .header {
  213. display: flex;
  214. align-items: center;
  215. padding: 88rpx 32rpx 24rpx;
  216. background: linear-gradient(135deg, #667EEA, #764BA2);
  217. }
  218. .back-btn {
  219. display: flex;
  220. align-items: center;
  221. margin-right: 24rpx;
  222. padding: 8rpx 16rpx;
  223. }
  224. .back-icon {
  225. font-size: 36rpx;
  226. color: #fff;
  227. margin-right: 8rpx;
  228. }
  229. .back-text {
  230. font-size: 28rpx;
  231. color: #fff;
  232. }
  233. .header-title {
  234. font-size: 36rpx;
  235. font-weight: 700;
  236. color: #fff;
  237. }
  238. .section {
  239. margin: 24rpx 24rpx 0;
  240. }
  241. .dimension-select {
  242. background: #fff;
  243. border-radius: 16rpx;
  244. padding: 32rpx;
  245. }
  246. .dimension-label {
  247. font-size: 28rpx;
  248. font-weight: 600;
  249. color: #333;
  250. margin-bottom: 20rpx;
  251. display: block;
  252. }
  253. .dimension-options {
  254. display: flex;
  255. gap: 20rpx;
  256. }
  257. .dimension-option {
  258. flex: 1;
  259. padding: 24rpx;
  260. border-radius: 12rpx;
  261. background: #F5F5F7;
  262. text-align: center;
  263. border: 2rpx solid transparent;
  264. }
  265. .dimension-option.active {
  266. background: #EEF2FF;
  267. border-color: #6366F1;
  268. }
  269. .dimension-name {
  270. font-size: 28rpx;
  271. color: #333;
  272. font-weight: 500;
  273. }
  274. .dimension-option.active .dimension-name {
  275. color: #6366F1;
  276. }
  277. .upload-area {
  278. background: #fff;
  279. border-radius: 16rpx;
  280. padding: 60rpx 32rpx;
  281. text-align: center;
  282. margin-top: 24rpx;
  283. border: 2rpx dashed #D1D5DB;
  284. }
  285. .upload-icon {
  286. font-size: 64rpx;
  287. margin-bottom: 16rpx;
  288. }
  289. .upload-text {
  290. font-size: 30rpx;
  291. color: #333;
  292. display: block;
  293. margin-bottom: 8rpx;
  294. }
  295. .upload-hint {
  296. font-size: 24rpx;
  297. color: #999;
  298. }
  299. .action-bar {
  300. margin-top: 32rpx;
  301. display: flex;
  302. gap: 20rpx;
  303. }
  304. .upload-btn {
  305. flex: 1;
  306. background: #6366F1;
  307. border-radius: 12rpx;
  308. padding: 24rpx;
  309. text-align: center;
  310. }
  311. .upload-btn-text {
  312. font-size: 30rpx;
  313. color: #fff;
  314. font-weight: 600;
  315. }
  316. .preview-header {
  317. margin-bottom: 24rpx;
  318. }
  319. .preview-title {
  320. font-size: 32rpx;
  321. font-weight: 700;
  322. color: #333;
  323. display: block;
  324. }
  325. .preview-desc {
  326. font-size: 24rpx;
  327. color: #999;
  328. margin-top: 8rpx;
  329. }
  330. .preview-card {
  331. background: #fff;
  332. border-radius: 16rpx;
  333. padding: 24rpx;
  334. }
  335. .preview-row {
  336. display: flex;
  337. justify-content: space-between;
  338. padding: 12rpx 0;
  339. border-bottom: 1rpx solid #F3F4F6;
  340. }
  341. .preview-row:last-child {
  342. border-bottom: none;
  343. }
  344. .preview-label {
  345. font-size: 26rpx;
  346. color: #666;
  347. }
  348. .preview-value {
  349. font-size: 26rpx;
  350. color: #333;
  351. font-weight: 500;
  352. }
  353. .items-section {
  354. margin-top: 24rpx;
  355. }
  356. .items-title {
  357. font-size: 28rpx;
  358. font-weight: 600;
  359. color: #333;
  360. margin-bottom: 16rpx;
  361. display: block;
  362. }
  363. .item-card {
  364. background: #fff;
  365. border-radius: 12rpx;
  366. padding: 20rpx;
  367. margin-bottom: 12rpx;
  368. }
  369. .item-header {
  370. display: flex;
  371. justify-content: space-between;
  372. margin-bottom: 12rpx;
  373. }
  374. .item-name {
  375. font-size: 26rpx;
  376. color: #333;
  377. font-weight: 500;
  378. }
  379. .item-category {
  380. font-size: 22rpx;
  381. color: #999;
  382. background: #F3F4F6;
  383. padding: 4rpx 12rpx;
  384. border-radius: 8rpx;
  385. }
  386. .item-value-bar {
  387. height: 16rpx;
  388. background: #F3F4F6;
  389. border-radius: 8rpx;
  390. overflow: hidden;
  391. }
  392. .item-value-fill {
  393. height: 100%;
  394. background: linear-gradient(90deg, #667EEA, #764BA2);
  395. border-radius: 8rpx;
  396. }
  397. .item-value-text {
  398. font-size: 24rpx;
  399. color: #666;
  400. margin-top: 8rpx;
  401. display: block;
  402. text-align: right;
  403. }
  404. .suggestions-card {
  405. background: #FFFBEB;
  406. border-radius: 16rpx;
  407. padding: 24rpx;
  408. margin-top: 24rpx;
  409. }
  410. .suggestions-title {
  411. font-size: 28rpx;
  412. font-weight: 600;
  413. color: #92400E;
  414. display: block;
  415. margin-bottom: 12rpx;
  416. }
  417. .suggestions-text {
  418. font-size: 26rpx;
  419. color: #78350F;
  420. line-height: 1.6;
  421. }
  422. .confirm-btn {
  423. flex: 1;
  424. background: #10B981;
  425. border-radius: 12rpx;
  426. padding: 24rpx;
  427. text-align: center;
  428. }
  429. .confirm-btn.disabled {
  430. opacity: 0.6;
  431. }
  432. .confirm-btn-text {
  433. font-size: 30rpx;
  434. color: #fff;
  435. font-weight: 600;
  436. }
  437. .retry-btn {
  438. padding: 24rpx 32rpx;
  439. border-radius: 12rpx;
  440. border: 2rpx solid #D1D5DB;
  441. text-align: center;
  442. }
  443. .retry-btn-text {
  444. font-size: 28rpx;
  445. color: #666;
  446. }
  447. .success-card {
  448. background: #fff;
  449. border-radius: 16rpx;
  450. padding: 60rpx 32rpx;
  451. text-align: center;
  452. margin-top: 120rpx;
  453. }
  454. .success-icon {
  455. font-size: 80rpx;
  456. display: block;
  457. margin-bottom: 24rpx;
  458. }
  459. .success-title {
  460. font-size: 36rpx;
  461. font-weight: 700;
  462. color: #10B981;
  463. display: block;
  464. margin-bottom: 12rpx;
  465. }
  466. .success-desc {
  467. font-size: 28rpx;
  468. color: #666;
  469. }
  470. .loading-overlay {
  471. position: fixed;
  472. top: 0; left: 0; right: 0; bottom: 0;
  473. background: rgba(0,0,0,0.5);
  474. display: flex;
  475. align-items: center;
  476. justify-content: center;
  477. z-index: 999;
  478. }
  479. .loading-box {
  480. background: #fff;
  481. border-radius: 16rpx;
  482. padding: 48rpx 64rpx;
  483. }
  484. .loading-text {
  485. font-size: 30rpx;
  486. color: #333;
  487. }
  488. </style>