record-detail.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <template>
  2. <view class="record-detail-page">
  3. <view class="nav-bar">
  4. <view class="nav-back" @tap="goBack">
  5. <text class="back-text">‹ 返回</text>
  6. </view>
  7. <text class="nav-title">记录详情</text>
  8. <view class="nav-placeholder"></view>
  9. </view>
  10. <scroll-view class="content" scroll-y v-if="record">
  11. <!-- 照片 -->
  12. <view class="photo-section" v-if="record.imageUrl">
  13. <image :src="record.imageUrl" mode="aspectFill" class="detail-photo" @tap="previewPhoto" />
  14. </view>
  15. <!-- 基本信息 -->
  16. <view class="card info-card">
  17. <view class="card-accent"></view>
  18. <view class="card-body">
  19. <text class="card-title">基本信息</text>
  20. <view class="info-row">
  21. <text class="info-label">餐次</text>
  22. <text class="info-value">{{ record.mealType }}</text>
  23. </view>
  24. <view class="info-row">
  25. <text class="info-label">记录方式</text>
  26. <text class="info-value">{{ record.recordMethod === 'photo' ? '📷 拍照识别' : '✏️ 手动记录' }}</text>
  27. </view>
  28. <view class="info-row">
  29. <text class="info-label">记录时间</text>
  30. <text class="info-value">{{ formatTime(record.createdAt) }}</text>
  31. </view>
  32. <view class="info-row" v-if="record.notes">
  33. <text class="info-label">备注</text>
  34. <text class="info-value">{{ record.notes }}</text>
  35. </view>
  36. </view>
  37. </view>
  38. <!-- AI识别结果 -->
  39. <view class="card ai-card" v-if="record.aiRecognizedFoods">
  40. <view class="card-accent" style="background-color: #2196F3;"></view>
  41. <view class="card-body">
  42. <text class="card-title">AI 识别结果</text>
  43. <view class="ai-item" v-for="(item, index) in aiFoods" :key="index">
  44. <text class="ai-name">{{ item.name }}</text>
  45. <view class="ai-confidence">
  46. <text class="confidence-bar" :style="'width:' + (item.confidence * 100) + '%'"></text>
  47. <text class="confidence-text">{{ (item.confidence * 100).toFixed(0) }}%</text>
  48. </view>
  49. </view>
  50. </view>
  51. </view>
  52. <!-- 用户确认食材 -->
  53. <view class="card foods-card">
  54. <view class="card-accent"></view>
  55. <view class="card-body">
  56. <text class="card-title">确认食材</text>
  57. <view class="food-tags">
  58. <view class="food-tag" v-for="(food, index) in confirmedFoods" :key="index">
  59. <text class="food-name">{{ food.name }}</text>
  60. </view>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- 操作按钮 -->
  65. <view class="action-area">
  66. <view class="btn-secondary" @tap="editRecord">编辑记录</view>
  67. <view class="btn-delete" @tap="deleteRecord">删除记录</view>
  68. </view>
  69. </scroll-view>
  70. <view class="empty-state" v-else>
  71. <text>加载中...</text>
  72. </view>
  73. </view>
  74. </template>
  75. <script>
  76. import { getDietDailyRecords } from '@/utils/api.js'
  77. export default {
  78. data() {
  79. return {
  80. recordId: null,
  81. record: null,
  82. aiFoods: [],
  83. confirmedFoods: []
  84. }
  85. },
  86. onLoad: function(options) {
  87. if (options.id) {
  88. this.recordId = parseInt(options.id)
  89. this.loadRecord()
  90. }
  91. },
  92. methods: {
  93. goBack: function() {
  94. uni.navigateBack()
  95. },
  96. loadRecord: function() {
  97. var self = this
  98. var memberId = uni.getStorageSync('currentMemberId')
  99. var today = self.formatDate(new Date())
  100. getDietDailyRecords({ memberId: memberId, date: today }).then(function(res) {
  101. if (res && res.records) {
  102. var record = res.records.find(function(r) { return r.id === self.recordId })
  103. if (record) {
  104. self.record = record
  105. self.parseAIFoods(record.aiRecognizedFoods)
  106. self.parseConfirmedFoods(record.userConfirmedFoods)
  107. }
  108. }
  109. })
  110. },
  111. parseAIFoods: function(json) {
  112. try {
  113. this.aiFoods = JSON.parse(json || '[]')
  114. } catch (e) {
  115. this.aiFoods = []
  116. }
  117. },
  118. parseConfirmedFoods: function(json) {
  119. try {
  120. this.confirmedFoods = JSON.parse(json || '[]')
  121. } catch (e) {
  122. this.confirmedFoods = []
  123. }
  124. },
  125. formatTime: function(timeStr) {
  126. if (!timeStr) return ''
  127. var d = new Date(timeStr)
  128. if (isNaN(d.getTime())) return timeStr
  129. var h = d.getHours().toString().padStart(2, '0')
  130. var m = d.getMinutes().toString().padStart(2, '0')
  131. return h + ':' + m
  132. },
  133. formatDate: function(date) {
  134. var d = date
  135. var m = (d.getMonth() + 1).toString().padStart(2, '0')
  136. var day = d.getDate().toString().padStart(2, '0')
  137. return d.getFullYear() + '-' + m + '-' + day
  138. },
  139. previewPhoto: function() {
  140. if (!this.record || !this.record.imageUrl) return
  141. uni.previewImage({
  142. urls: [this.record.imageUrl],
  143. current: this.record.imageUrl
  144. })
  145. },
  146. editRecord: function() {
  147. uni.showToast({ title: '编辑功能开发中', icon: 'none' })
  148. },
  149. deleteRecord: function() {
  150. var self = this
  151. uni.showModal({
  152. title: '确认删除',
  153. content: '确定要删除这条饮食记录吗?',
  154. success: function(res) {
  155. if (res.confirm) {
  156. uni.showToast({ title: '已删除', icon: 'success' })
  157. setTimeout(function() {
  158. uni.navigateBack()
  159. }, 1000)
  160. }
  161. }
  162. })
  163. }
  164. }
  165. }
  166. </script>
  167. <style scoped>
  168. .record-detail-page {
  169. min-height: 100vh;
  170. background-color: #F5F7FA;
  171. display: flex;
  172. flex-direction: column;
  173. }
  174. .nav-bar {
  175. display: flex;
  176. align-items: center;
  177. justify-content: center;
  178. height: 88rpx;
  179. padding-top: env(safe-area-inset-top);
  180. background-color: #FFFFFF;
  181. position: relative;
  182. border-bottom: 1rpx solid #f0f0f0;
  183. }
  184. .nav-back {
  185. position: absolute;
  186. left: 30rpx;
  187. top: 50%;
  188. transform: translateY(-50%);
  189. padding: 10rpx;
  190. }
  191. .back-text {
  192. font-size: 28rpx;
  193. color: #333;
  194. }
  195. .nav-title {
  196. font-size: 32rpx;
  197. font-weight: 600;
  198. color: #333;
  199. }
  200. .nav-placeholder {
  201. width: 80rpx;
  202. }
  203. .content {
  204. flex: 1;
  205. padding: 30rpx;
  206. }
  207. .photo-section {
  208. margin-bottom: 30rpx;
  209. border-radius: 20rpx;
  210. overflow: hidden;
  211. }
  212. .detail-photo {
  213. width: 100%;
  214. height: 400rpx;
  215. background-color: #f0f0f0;
  216. }
  217. .card {
  218. display: flex;
  219. background-color: #FFFFFF;
  220. border-radius: 20rpx;
  221. margin-bottom: 30rpx;
  222. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  223. overflow: hidden;
  224. }
  225. .card-accent {
  226. width: 8rpx;
  227. background-color: #FF8C42;
  228. flex-shrink: 0;
  229. }
  230. .card-body {
  231. flex: 1;
  232. padding: 30rpx;
  233. }
  234. .card-title {
  235. font-size: 28rpx;
  236. font-weight: 600;
  237. color: #333;
  238. margin-bottom: 20rpx;
  239. display: block;
  240. }
  241. .info-row {
  242. display: flex;
  243. justify-content: space-between;
  244. padding: 16rpx 0;
  245. border-bottom: 1rpx solid #f0f0f0;
  246. }
  247. .info-row:last-child {
  248. border-bottom: none;
  249. }
  250. .info-label {
  251. font-size: 26rpx;
  252. color: #666;
  253. }
  254. .info-value {
  255. font-size: 26rpx;
  256. color: #333;
  257. font-weight: 500;
  258. }
  259. .ai-item {
  260. display: flex;
  261. justify-content: space-between;
  262. align-items: center;
  263. padding: 16rpx 0;
  264. border-bottom: 1rpx solid #f0f0f0;
  265. }
  266. .ai-item:last-child {
  267. border-bottom: none;
  268. }
  269. .ai-name {
  270. font-size: 26rpx;
  271. color: #333;
  272. }
  273. .ai-confidence {
  274. display: flex;
  275. align-items: center;
  276. gap: 12rpx;
  277. width: 200rpx;
  278. }
  279. .confidence-bar {
  280. height: 8rpx;
  281. background-color: #2196F3;
  282. border-radius: 4rpx;
  283. flex: 1;
  284. }
  285. .confidence-text {
  286. font-size: 22rpx;
  287. color: #2196F3;
  288. white-space: nowrap;
  289. }
  290. .food-tags {
  291. display: flex;
  292. flex-wrap: wrap;
  293. gap: 16rpx;
  294. }
  295. .food-tag {
  296. padding: 12rpx 24rpx;
  297. background-color: #FFF5EB;
  298. border-radius: 30rpx;
  299. font-size: 24rpx;
  300. color: #FF8C42;
  301. }
  302. .action-area {
  303. display: flex;
  304. gap: 20rpx;
  305. padding: 40rpx 30rpx;
  306. }
  307. .btn-secondary {
  308. flex: 1;
  309. padding: 28rpx 0;
  310. background-color: #FFFFFF;
  311. border: 1rpx solid #ddd;
  312. border-radius: 16rpx;
  313. text-align: center;
  314. font-size: 28rpx;
  315. color: #666;
  316. }
  317. .btn-delete {
  318. flex: 1;
  319. padding: 28rpx 0;
  320. background-color: #FFF3E0;
  321. border-radius: 16rpx;
  322. text-align: center;
  323. font-size: 28rpx;
  324. color: #FF5722;
  325. }
  326. .empty-state {
  327. flex: 1;
  328. display: flex;
  329. align-items: center;
  330. justify-content: center;
  331. font-size: 28rpx;
  332. color: #999;
  333. }
  334. </style>