|
@@ -131,13 +131,37 @@
|
|
|
<!-- 底部留白 -->
|
|
<!-- 底部留白 -->
|
|
|
<view class="bottom-spacer"></view>
|
|
<view class="bottom-spacer"></view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 图片识别结果确认弹窗 -->
|
|
|
|
|
+ <view class="recognize-overlay" v-if="showRecognizeModal" @tap="cancelRecognize">
|
|
|
|
|
+ <view class="recognize-modal" @tap.stop>
|
|
|
|
|
+ <view class="recognize-header">
|
|
|
|
|
+ <text class="recognize-title">🍽️ 识别结果</text>
|
|
|
|
|
+ <text class="recognize-count">{{ recognizedFoods.length }} 种食材</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="recognize-body">
|
|
|
|
|
+ <view class="food-chip" v-for="(f, idx) in recognizedFoods" :key="getFoodsKey(f, idx)">
|
|
|
|
|
+ <text class="food-chip-name">{{ f.name }}</text>
|
|
|
|
|
+ <text class="food-chip-cat" v-if="f.category && f.category !== 'unknown'">{{ f.category }}</text>
|
|
|
|
|
+ <text class="food-chip-del" @tap="removeRecognizedFood(idx)">×</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="recognize-hint">
|
|
|
|
|
+ <text>可手动删除不需要的食材,确认后自动填入食物清单</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="recognize-footer">
|
|
|
|
|
+ <view class="btn-cancel" @tap="cancelRecognize"><text>取消</text></view>
|
|
|
|
|
+ <view class="btn-confirm" @tap="confirmRecognize"><text>✓ 确认填入</text></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import config from '../../config'
|
|
import config from '../../config'
|
|
|
import { parseDate } from '../../utils/format.js'
|
|
import { parseDate } from '../../utils/format.js'
|
|
|
-import { sttTranscribe } from '../../utils/api.js'
|
|
|
|
|
|
|
+import { sttTranscribe, recognizeDietFood } from '../../utils/api.js'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
data() {
|
|
data() {
|
|
@@ -151,6 +175,8 @@ export default {
|
|
|
recorderManager: null,
|
|
recorderManager: null,
|
|
|
history: [],
|
|
history: [],
|
|
|
todayText: '',
|
|
todayText: '',
|
|
|
|
|
+ showRecognizeModal: false,
|
|
|
|
|
+ recognizedFoods: [],
|
|
|
mealTypes: [
|
|
mealTypes: [
|
|
|
{ label: '早餐', value: 'breakfast' },
|
|
{ label: '早餐', value: 'breakfast' },
|
|
|
{ label: '午餐', value: 'lunch' },
|
|
{ label: '午餐', value: 'lunch' },
|
|
@@ -205,6 +231,10 @@ export default {
|
|
|
uni.showLoading({ title: '语音识别中...', mask: true })
|
|
uni.showLoading({ title: '语音识别中...', mask: true })
|
|
|
sttTranscribe(res.tempFilePath).then(function(text) {
|
|
sttTranscribe(res.tempFilePath).then(function(text) {
|
|
|
self.form.voiceText = text
|
|
self.form.voiceText = text
|
|
|
|
|
+ // 语音转写成功:若食物清单为空则自动填入
|
|
|
|
|
+ if (!self.form.foodItems || !self.form.foodItems.trim()) {
|
|
|
|
|
+ self.form.foodItems = text.trim()
|
|
|
|
|
+ }
|
|
|
uni.hideLoading()
|
|
uni.hideLoading()
|
|
|
// 保存时停止录音 → 语音已就绪,继续提交
|
|
// 保存时停止录音 → 语音已就绪,继续提交
|
|
|
if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
|
|
if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
|
|
@@ -265,10 +295,36 @@ export default {
|
|
|
count: 9 - self.form.photos.length,
|
|
count: 9 - self.form.photos.length,
|
|
|
sizeType: ['compressed'],
|
|
sizeType: ['compressed'],
|
|
|
sourceType: ['album', 'camera'],
|
|
sourceType: ['album', 'camera'],
|
|
|
- success: function(res) {
|
|
|
|
|
- res.tempFilePaths.forEach(function(path) {
|
|
|
|
|
- if (self.form.photos.length < 9) self.form.photos.push(path)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ success: async function(res) {
|
|
|
|
|
+ for (var i = 0; i < res.tempFilePaths.length; i++) {
|
|
|
|
|
+ if (self.form.photos.length >= 9) break
|
|
|
|
|
+ var path = res.tempFilePaths[i]
|
|
|
|
|
+ self.form.photos.push(path)
|
|
|
|
|
+ try {
|
|
|
|
|
+ var url = await self.uploadFile(path)
|
|
|
|
|
+ if (url) {
|
|
|
|
|
+ var recognizeRes = await new Promise(function(resolve) {
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: config.API_BASE_URL + '/api/diet/record/recognize',
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: { image_url: url },
|
|
|
|
|
+ header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
|
|
|
|
|
+ success: function(r) { resolve(r) },
|
|
|
|
|
+ fail: function() { resolve({ data: { code: 500 } }) }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ if (recognizeRes.data && recognizeRes.data.code === 200 && recognizeRes.data.data && recognizeRes.data.data.foods) {
|
|
|
|
|
+ var foods = recognizeRes.data.data.foods
|
|
|
|
|
+ for (var f = 0; f < foods.length; f++) {
|
|
|
|
|
+ self.recognizedFoods.push({ name: foods[f].name, category: foods[f].category })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { /* ignore */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (self.recognizedFoods.length > 0) {
|
|
|
|
|
+ self.showRecognizeResult()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
@@ -299,6 +355,38 @@ export default {
|
|
|
deleteVoice: function() {
|
|
deleteVoice: function() {
|
|
|
this.form.voicePath = ''
|
|
this.form.voicePath = ''
|
|
|
},
|
|
},
|
|
|
|
|
+ showRecognizeResult: function() {
|
|
|
|
|
+ this.showRecognizeModal = true
|
|
|
|
|
+ },
|
|
|
|
|
+ confirmRecognize: function() {
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ var names = []
|
|
|
|
|
+ for (var i = 0; i < self.recognizedFoods.length; i++) {
|
|
|
|
|
+ if (self.recognizedFoods[i].name && self.recognizedFoods[i].name.trim()) {
|
|
|
|
|
+ names.push(self.recognizedFoods[i].name.trim())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (names.length > 0) {
|
|
|
|
|
+ var newText = names.join('、')
|
|
|
|
|
+ if (self.form.foodItems && self.form.foodItems.trim()) {
|
|
|
|
|
+ self.form.foodItems = self.form.foodItems + '、' + newText
|
|
|
|
|
+ } else {
|
|
|
|
|
+ self.form.foodItems = newText
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ self.showRecognizeModal = false
|
|
|
|
|
+ self.recognizedFoods = []
|
|
|
|
|
+ },
|
|
|
|
|
+ cancelRecognize: function() {
|
|
|
|
|
+ this.showRecognizeModal = false
|
|
|
|
|
+ this.recognizedFoods = []
|
|
|
|
|
+ },
|
|
|
|
|
+ removeRecognizedFood: function(idx) {
|
|
|
|
|
+ this.recognizedFoods.splice(idx, 1)
|
|
|
|
|
+ },
|
|
|
|
|
+ getFoodsKey: function(item, idx) {
|
|
|
|
|
+ return item.name || ('food' + idx)
|
|
|
|
|
+ },
|
|
|
uploadFile: function(filePath) {
|
|
uploadFile: function(filePath) {
|
|
|
var self = this
|
|
var self = this
|
|
|
return new Promise(function(resolve, reject) {
|
|
return new Promise(function(resolve, reject) {
|
|
@@ -477,4 +565,47 @@ success: function(res) {
|
|
|
|
|
|
|
|
/* ===== 底部留白 ===== */
|
|
/* ===== 底部留白 ===== */
|
|
|
.bottom-spacer { height: 40rpx; }
|
|
.bottom-spacer { height: 40rpx; }
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 识别结果弹窗 ===== */
|
|
|
|
|
+.recognize-overlay {
|
|
|
|
|
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
|
|
|
+ background: rgba(0,0,0,0.45); z-index: 999;
|
|
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.recognize-modal {
|
|
|
|
|
+ width: 600rpx; background: #fff; border-radius: 28rpx;
|
|
|
|
|
+ overflow: hidden; box-shadow: 0 16rpx 48rpx rgba(0,0,0,0.18);
|
|
|
|
|
+}
|
|
|
|
|
+.recognize-header {
|
|
|
|
|
+ display: flex; align-items: center; justify-content: space-between;
|
|
|
|
|
+ padding: 28rpx 32rpx 20rpx; border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+.recognize-title { font-size: 32rpx; font-weight: 600; color: #333; }
|
|
|
|
|
+.recognize-count { font-size: 24rpx; color: #F97316; font-weight: 500; }
|
|
|
|
|
+.recognize-body { padding: 24rpx 32rpx; max-height: 420rpx; overflow-y: auto; }
|
|
|
|
|
+.food-chip {
|
|
|
|
|
+ display: inline-flex; align-items: center; gap: 8rpx;
|
|
|
|
|
+ background: #FFF3E0; border: 1rpx solid #FFE0B2; border-radius: 24rpx;
|
|
|
|
|
+ padding: 8rpx 20rpx; margin: 6rpx 8rpx 6rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.food-chip-name { font-size: 26rpx; color: #E65100; font-weight: 500; }
|
|
|
|
|
+.food-chip-cat { font-size: 20rpx; color: #999; }
|
|
|
|
|
+.food-chip-del {
|
|
|
|
|
+ width: 32rpx; height: 32rpx; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+ background: #eee; border-radius: 50%; color: #999; font-size: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.recognize-hint {
|
|
|
|
|
+ margin-top: 16rpx; padding: 12rpx 16rpx; background: #FAFAFA;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.recognize-hint text { font-size: 22rpx; color: #999; }
|
|
|
|
|
+.recognize-footer {
|
|
|
|
|
+ display: flex; padding: 20rpx 32rpx 32rpx; gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-cancel, .btn-confirm {
|
|
|
|
|
+ flex: 1; text-align: center; padding: 18rpx 0; border-radius: 14rpx;
|
|
|
|
|
+ font-size: 28rpx; font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-cancel { background: #f5f5f5; color: #666; }
|
|
|
|
|
+.btn-confirm { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.25); }
|
|
|
</style>
|
|
</style>
|