2026-02-28 05:40:21 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="checkin-publish-page">
|
|
|
|
|
|
<!-- 内容区域 -->
|
|
|
|
|
|
<scroll-view class="content-scroll" scroll-y>
|
|
|
|
|
|
<!-- 上传照片卡片 -->
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
|
<view class="card-header">
|
|
|
|
|
|
<text class="card-title">上传照片</text>
|
|
|
|
|
|
<text class="photo-count">{{ selectedImages.length }}/6</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="photo-grid">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="photo-item"
|
|
|
|
|
|
v-for="(image, index) in selectedImages"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
>
|
|
|
|
|
|
<image class="photo-image" :src="image" mode="aspectFill"></image>
|
|
|
|
|
|
<view class="photo-delete" @click="removeImage(index)">
|
|
|
|
|
|
<text class="delete-icon">×</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="photo-add-btn"
|
|
|
|
|
|
v-if="selectedImages.length < 6"
|
|
|
|
|
|
@click="chooseImage"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text class="add-icon">+</text>
|
|
|
|
|
|
<text class="add-text">添加照片</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="photo-tip">
|
|
|
|
|
|
<!-- <text class="tip-icon">📷</text> -->
|
|
|
|
|
|
<text class="tip-text">拍摄清晰的照片能帮您更准确地识别食物营养成分</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 选择餐次卡片 -->
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
|
<view class="card-header">
|
|
|
|
|
|
<text class="card-title">选择餐次</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="meal-type-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="meal-type-item"
|
|
|
|
|
|
v-for="(meal, index) in mealTypes"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
:class="{ active: selectedMealType === meal.value }"
|
|
|
|
|
|
@click="selectMealType(meal.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>{{ meal.label }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 备注说明卡片 -->
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
|
<view class="card-header">
|
|
|
|
|
|
<text class="card-title">一句话说明(可选)</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
class="remark-input"
|
|
|
|
|
|
v-model="remark"
|
|
|
|
|
|
placeholder="记录今天的饮食感受、烹饪方法、食材用量等..."
|
|
|
|
|
|
maxlength="500"
|
|
|
|
|
|
@input="onRemarkInput"
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
<view class="remark-footer">
|
|
|
|
|
|
<text class="char-count">{{ remark.length }}/500</text>
|
|
|
|
|
|
<view class="voice-btn" :class="{ recording: isRecording }" @click="handleVoiceRemark">
|
|
|
|
|
|
<text class="voice-icon">{{ isRecording ? '⏹️' : '🎤' }}</text>
|
|
|
|
|
|
<text class="voice-text">{{ isRecording ? `停止录音 (${recordDuration}s)` : '语音录入' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 生成AI营养分析视频卡片 -->
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
|
<view class="card-header">
|
|
|
|
|
|
<view class="ai-header-left">
|
|
|
|
|
|
<text class="ai-icon">🎬</text>
|
|
|
|
|
|
<text class="card-title">生成打卡视频分享到社区</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<switch
|
|
|
|
|
|
class="ai-switch"
|
|
|
|
|
|
:checked="enableAIVideo"
|
|
|
|
|
|
@change="toggleAIVideo"
|
|
|
|
|
|
color="#ff6b35"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="ai-features" v-if="enableAIVideo">
|
|
|
|
|
|
<view class="feature-item">
|
|
|
|
|
|
<text class="feature-check">✓</text>
|
|
|
|
|
|
<text class="feature-text">AI分析营养成分</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="feature-item">
|
|
|
|
|
|
<text class="feature-check">✓</text>
|
|
|
|
|
|
<text class="feature-text">生成专业营养分析视频</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="feature-item">
|
|
|
|
|
|
<text class="feature-check">✓</text>
|
|
|
|
|
|
<text class="feature-text">分享到社区,获得更多积分</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部安全距离 -->
|
|
|
|
|
|
<view class="safe-bottom"></view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部操作栏 -->
|
|
|
|
|
|
<view class="bottom-bar">
|
|
|
|
|
|
<view class="cancel-btn" @click="handleCancel">
|
|
|
|
|
|
<text>取消</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="publish-btn" @click="handlePublish">
|
|
|
|
|
|
<text>发布打卡</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import api from '@/api/models-api.js';
|
|
|
|
|
|
import { submitCheckin } from '@/api/tool.js';
|
|
|
|
|
|
import { mapGetters } from 'vuex';
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
...mapGetters(['userInfo','uid'])
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
selectedImages: [],
|
|
|
|
|
|
selectedMealType: 'breakfast',
|
|
|
|
|
|
mealTypes: [
|
|
|
|
|
|
{ label: '早餐', value: 'breakfast' },
|
|
|
|
|
|
{ label: '午餐', value: 'lunch' },
|
|
|
|
|
|
{ label: '晚餐', value: 'dinner' },
|
|
|
|
|
|
{ label: '加餐', value: 'snack' }
|
|
|
|
|
|
],
|
|
|
|
|
|
remark: '',
|
|
|
|
|
|
enableAIVideo: false,
|
|
|
|
|
|
|
|
|
|
|
|
// 语音输入相关
|
|
|
|
|
|
isRecording: false,
|
|
|
|
|
|
recorderManager: null,
|
|
|
|
|
|
recordDuration: 0,
|
|
|
|
|
|
recordTimer: null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
|
// 初始化录音管理器
|
|
|
|
|
|
this.initRecorder();
|
|
|
|
|
|
|
|
|
|
|
|
// 根据参数处理
|
|
|
|
|
|
if (options.type === 'video') {
|
|
|
|
|
|
this.enableAIVideo = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理一键复制/借鉴打卡
|
|
|
|
|
|
if (options.mode === 'copy') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const copyData = uni.getStorageSync('checkin_copy_data');
|
|
|
|
|
|
if (copyData) {
|
|
|
|
|
|
this.selectedImages = copyData.images || [];
|
|
|
|
|
|
this.selectedMealType = copyData.mealType || 'breakfast';
|
|
|
|
|
|
this.remark = copyData.notes || '';
|
|
|
|
|
|
|
|
|
|
|
|
// 清除缓存
|
|
|
|
|
|
uni.removeStorageSync('checkin_copy_data');
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '已加载打卡内容',
|
|
|
|
|
|
icon: 'success'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('读取复制数据失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
|
|
|
|
|
// 清理录音计时器
|
|
|
|
|
|
this.stopRecordTimer();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果正在录音,停止录音
|
|
|
|
|
|
if (this.isRecording && this.recorderManager) {
|
|
|
|
|
|
this.recorderManager.stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
// 初始化录音管理器
|
|
|
|
|
|
initRecorder() {
|
|
|
|
|
|
// #ifdef MP-WEIXIN || APP-PLUS
|
|
|
|
|
|
this.recorderManager = uni.getRecorderManager();
|
|
|
|
|
|
|
|
|
|
|
|
// 录音开始事件
|
|
|
|
|
|
this.recorderManager.onStart(() => {
|
|
|
|
|
|
console.log('录音开始');
|
|
|
|
|
|
this.recordDuration = 0;
|
|
|
|
|
|
this.startRecordTimer();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 录音结束事件
|
|
|
|
|
|
this.recorderManager.onStop((res) => {
|
|
|
|
|
|
console.log('录音结束', res);
|
|
|
|
|
|
this.stopRecordTimer();
|
|
|
|
|
|
|
|
|
|
|
|
if (res.duration < 1000) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '录音时间太短',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理录音结果
|
|
|
|
|
|
this.handleRecordResult(res);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 录音错误事件
|
|
|
|
|
|
this.recorderManager.onError((err) => {
|
|
|
|
|
|
console.error('录音错误:', err);
|
|
|
|
|
|
this.isRecording = false;
|
|
|
|
|
|
this.stopRecordTimer();
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '录音失败,请重试',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
console.log('H5环境暂不支持录音功能');
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 开始录音计时
|
|
|
|
|
|
startRecordTimer() {
|
|
|
|
|
|
this.recordTimer = setInterval(() => {
|
|
|
|
|
|
this.recordDuration++;
|
|
|
|
|
|
if (this.recordDuration >= 60) {
|
|
|
|
|
|
// 录音超过60秒自动停止
|
|
|
|
|
|
this.stopVoiceInput();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 停止录音计时
|
|
|
|
|
|
stopRecordTimer() {
|
|
|
|
|
|
if (this.recordTimer) {
|
|
|
|
|
|
clearInterval(this.recordTimer);
|
|
|
|
|
|
this.recordTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.recordDuration = 0;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 处理语音备注按钮点击
|
|
|
|
|
|
handleVoiceRemark() {
|
|
|
|
|
|
if (this.isRecording) {
|
|
|
|
|
|
this.stopVoiceInput();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.startVoiceInput();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 开始语音输入
|
|
|
|
|
|
startVoiceInput() {
|
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: 'H5环境暂不支持语音输入,请使用小程序或APP',
|
|
|
|
|
|
showCancel: false
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifdef MP-WEIXIN || APP-PLUS
|
|
|
|
|
|
if (!this.recorderManager) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '录音功能初始化失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查录音权限
|
|
|
|
|
|
uni.authorize({
|
|
|
|
|
|
scope: 'scope.record',
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
this.startRecording();
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '需要录音权限',
|
|
|
|
|
|
content: '请在设置中开启录音权限',
|
|
|
|
|
|
confirmText: '去设置',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
uni.openSetting();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 开始录音
|
|
|
|
|
|
startRecording() {
|
|
|
|
|
|
this.isRecording = true;
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '开始录音',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 1000
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 开始录音
|
|
|
|
|
|
this.recorderManager.start({
|
|
|
|
|
|
duration: 60000, // 最长录音60秒
|
|
|
|
|
|
sampleRate: 16000,
|
|
|
|
|
|
numberOfChannels: 1,
|
|
|
|
|
|
encodeBitRate: 96000,
|
|
|
|
|
|
format: 'mp3'
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 停止语音输入
|
|
|
|
|
|
stopVoiceInput() {
|
|
|
|
|
|
if (!this.isRecording) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isRecording = false;
|
|
|
|
|
|
|
|
|
|
|
|
// #ifdef MP-WEIXIN || APP-PLUS
|
|
|
|
|
|
if (this.recorderManager) {
|
|
|
|
|
|
this.recorderManager.stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '正在识别...',
|
|
|
|
|
|
icon: 'loading',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 处理录音结果
|
|
|
|
|
|
async handleRecordResult(res) {
|
|
|
|
|
|
console.log('录音文件路径:', res.tempFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 上传录音文件
|
|
|
|
|
|
// 按照 oneclick.vue 的逻辑,使用 api.uploadFile 上传音频
|
|
|
|
|
|
const uploadRes = await api.uploadFile(res.tempFilePath, {
|
|
|
|
|
|
model: 'audio',
|
|
|
|
|
|
pid: '8'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!uploadRes || uploadRes.code !== 200) {
|
|
|
|
|
|
throw new Error('录音文件上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const audioUrl = uploadRes.data.fullUrl;
|
|
|
|
|
|
console.log('录音文件上传成功,URL:', audioUrl);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 识别语音
|
|
|
|
|
|
const recognizedText = await this.recognizeSpeech(audioUrl);
|
|
|
|
|
|
|
|
|
|
|
|
if (recognizedText) {
|
|
|
|
|
|
// 将识别结果添加到备注中
|
|
|
|
|
|
if (this.remark) {
|
|
|
|
|
|
this.remark += ' ' + recognizedText;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.remark = recognizedText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '识别成功',
|
|
|
|
|
|
icon: 'success',
|
|
|
|
|
|
duration: 1500
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('语音识别流程失败:', error);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '识别失败,请重试',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 语音识别(对接腾讯云语音识别服务)
|
|
|
|
|
|
async recognizeSpeech(audioUrl) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 创建语音识别任务
|
|
|
|
|
|
console.log('创建语音识别任务, URL:', audioUrl);
|
|
|
|
|
|
const createTaskRes = await api.createAsrTask({
|
|
|
|
|
|
url: audioUrl,
|
|
|
|
|
|
engineModelType: '16k_zh',
|
|
|
|
|
|
channelNum: 1,
|
|
|
|
|
|
resTextFormat: 0,
|
|
|
|
|
|
sourceType: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!createTaskRes || createTaskRes.code !== 200) {
|
|
|
|
|
|
throw new Error('创建识别任务失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const taskId = createTaskRes.data.taskId;
|
|
|
|
|
|
console.log('识别任务创建成功,任务ID:', taskId);
|
|
|
|
|
|
|
|
|
|
|
|
// 轮询查询识别结果
|
|
|
|
|
|
const result = await this.pollAsrResult(taskId);
|
|
|
|
|
|
|
|
|
|
|
|
// 解析识别文本
|
|
|
|
|
|
return this.parseAsrResult(result);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('语音识别请求失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 轮询查询ASR识别结果
|
|
|
|
|
|
async pollAsrResult(taskId, maxAttempts = 30, interval = 2000) {
|
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (attempts < maxAttempts) {
|
|
|
|
|
|
attempts++;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const statusRes = await api.queryAsrStatus(taskId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!statusRes || statusRes.code !== 200) {
|
|
|
|
|
|
throw new Error('查询识别状态失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const status = statusRes.data.status;
|
|
|
|
|
|
|
|
|
|
|
|
// status: 0-等待处理, 1-处理中, 2-识别成功, 3-识别失败
|
|
|
|
|
|
if (status === 2) {
|
|
|
|
|
|
return statusRes.data;
|
|
|
|
|
|
} else if (status === 3) {
|
|
|
|
|
|
throw new Error(statusRes.data.errorMsg || '识别失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待一段时间后继续查询
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('查询识别状态出错:', error);
|
|
|
|
|
|
if (attempts >= maxAttempts) throw error;
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Error('识别超时,请重试');
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 解析ASR识别结果文本
|
|
|
|
|
|
parseAsrResult(data) {
|
|
|
|
|
|
if (!data || !data.result) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let text = data.result;
|
|
|
|
|
|
// 去除时间戳标记
|
|
|
|
|
|
text = text.replace(/\[\d+:\d+\.\d+,\d+:\d+\.\d+\]\s*/g, '');
|
|
|
|
|
|
// 去除多余的空格和换行
|
|
|
|
|
|
text = text.replace(/\n+/g, ' ').trim();
|
|
|
|
|
|
text = text.replace(/\s+/g, ' ');
|
|
|
|
|
|
|
|
|
|
|
|
return text;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async chooseImage() {
|
|
|
|
|
|
const maxCount = 6 - this.selectedImages.length;
|
|
|
|
|
|
if (maxCount <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
|
|
|
uni.chooseImage({
|
|
|
|
|
|
count: maxCount,
|
|
|
|
|
|
sizeType: ['compressed'],
|
|
|
|
|
|
sourceType: ['album', 'camera'],
|
|
|
|
|
|
success: resolve,
|
|
|
|
|
|
fail: reject
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
uni.showLoading({
|
|
|
|
|
|
title: '正在上传...',
|
|
|
|
|
|
mask: true
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
for (const filePath of res.tempFilePaths) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const uploadResult = await api.uploadFile(filePath, {
|
|
|
|
|
|
model: 'checkin',
|
|
|
|
|
|
pid: '7'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (uploadResult && uploadResult.code === 200) {
|
|
|
|
|
|
// 直接保存完整的URL
|
|
|
|
|
|
this.selectedImages.push(uploadResult.data.fullUrl);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('上传失败:', uploadResult);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '部分图片上传失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
|
console.error('上传异常:', uploadError);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
console.error('选择图片失败', error);
|
|
|
|
|
|
if (!error.errMsg || !error.errMsg.includes('cancel')) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '选择图片失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
removeImage(index) {
|
|
|
|
|
|
this.selectedImages.splice(index, 1)
|
|
|
|
|
|
},
|
|
|
|
|
|
selectMealType(value) {
|
|
|
|
|
|
this.selectedMealType = value
|
|
|
|
|
|
},
|
|
|
|
|
|
onRemarkInput(e) {
|
|
|
|
|
|
this.remark = e.detail.value
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
toggleAIVideo(e) {
|
|
|
|
|
|
this.enableAIVideo = e.detail.value
|
|
|
|
|
|
},
|
|
|
|
|
|
handleCancel() {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定要取消发布吗?',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
async handlePublish() {
|
|
|
|
|
|
if (this.selectedImages.length === 0) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '请至少上传一张照片',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 防止重复提交
|
|
|
|
|
|
if (this._publishing) return
|
|
|
|
|
|
this._publishing = true
|
|
|
|
|
|
|
2026-03-04 12:21:29 +08:00
|
|
|
|
uni.showLoading({ title: '发布中...' });
|
|
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const imageUrls = this.selectedImages;
|
|
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
|
|
const result = await submitCheckin({
|
|
|
|
|
|
mealType: this.selectedMealType,
|
|
|
|
|
|
date: today,
|
|
|
|
|
|
photosJson: JSON.stringify(imageUrls),
|
|
|
|
|
|
notes: this.remark,
|
|
|
|
|
|
enableAIVideo: this.enableAIVideo,
|
|
|
|
|
|
enableAIAnalysis: false
|
|
|
|
|
|
});
|
2026-03-04 12:21:29 +08:00
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
uni.hideLoading();
|
2026-03-04 12:21:29 +08:00
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
const points = result.data?.points || 0;
|
2026-03-04 12:21:29 +08:00
|
|
|
|
const taskId = result.data?.taskId;
|
|
|
|
|
|
const successMsg = (this.enableAIVideo && taskId)
|
|
|
|
|
|
? '打卡成功!视频生成中...'
|
2026-02-28 05:40:21 +08:00
|
|
|
|
: `打卡成功!获得${points}积分`;
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: successMsg,
|
|
|
|
|
|
icon: 'success',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟跳转
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
uni.redirectTo({
|
|
|
|
|
|
url: '/pages/tool/dietary-records'
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
console.error('发布失败:', error);
|
|
|
|
|
|
const msg = typeof error === 'string' ? error : (error && (error.message || error.msg));
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: msg || '发布失败,请重试',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: msg ? 2500 : 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this._publishing = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.checkin-publish-page {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #f4f5f7;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 内容滚动区域 */
|
|
|
|
|
|
.content-scroll {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-bottom: 120rpx;
|
|
|
|
|
|
padding: 32rpx 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 卡片样式 */
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border: 1rpx solid #d0dbea;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
margin: 0 32rpx 32rpx;
|
|
|
|
|
|
padding: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #2e3e5c;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-count {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #9fa5c0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.ai-icon {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 照片网格 */
|
|
|
|
|
|
.photo-grid {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-item {
|
|
|
|
|
|
width: 195rpx;
|
|
|
|
|
|
height: 195rpx;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: #f4f5f7;
|
|
|
|
|
|
|
|
|
|
|
|
.photo-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-delete {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 8rpx;
|
|
|
|
|
|
top: 8rpx;
|
|
|
|
|
|
width: 48rpx;
|
|
|
|
|
|
height: 48rpx;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
.delete-icon {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-add-btn {
|
|
|
|
|
|
width: 195rpx;
|
|
|
|
|
|
height: 195rpx;
|
|
|
|
|
|
border: 2rpx solid #d0dbea;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
background: #f4f5f7;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.add-icon {
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
color: #9fa5c0;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #9fa5c0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-tip {
|
|
|
|
|
|
background: #e3f2fd;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.tip-icon {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tip-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #1976d2;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 餐次选择 */
|
|
|
|
|
|
.meal-type-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meal-type-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 70rpx;
|
|
|
|
|
|
border: 2rpx solid #d0dbea;
|
|
|
|
|
|
border-radius: 50rpx;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #9fa5c0;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
background: #fff5f0;
|
|
|
|
|
|
border-color: #ff6b35;
|
|
|
|
|
|
color: #ff6b35;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 备注说明 */
|
|
|
|
|
|
.remark-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 200rpx;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border: 1rpx solid #d0dbea;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #2e3e5c;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.remark-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.char-count {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #9fa5c0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-btn {
|
|
|
|
|
|
background: linear-gradient(135deg, #ff8c52 0%, #ff6b35 100%);
|
|
|
|
|
|
border-radius: 50rpx;
|
|
|
|
|
|
padding: 12rpx 32rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
.voice-icon {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-text {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.recording {
|
|
|
|
|
|
background: linear-gradient(135deg, #ff4d4f 0%, #f5222d 100%);
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(245, 34, 45, 0.3);
|
|
|
|
|
|
|
|
|
|
|
|
.voice-text {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-icon {
|
|
|
|
|
|
animation: pulse 1s infinite;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.5; }
|
|
|
|
|
|
100% { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* AI视频功能 */
|
|
|
|
|
|
.ai-switch {
|
|
|
|
|
|
transform: scale(0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-features {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.feature-check {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #ff6b35;
|
|
|
|
|
|
width: 24rpx;
|
|
|
|
|
|
height: 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #3e5481;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 底部安全距离 */
|
|
|
|
|
|
.safe-bottom {
|
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 底部操作栏 */
|
|
|
|
|
|
.bottom-bar {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
height: 120rpx;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border-top: 1rpx solid #d0dbea;
|
|
|
|
|
|
padding: 24rpx 32rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24rpx;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cancel-btn {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 96rpx;
|
|
|
|
|
|
border: 2rpx solid #d0dbea;
|
|
|
|
|
|
border-radius: 40rpx;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
text {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #3e5481;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-btn {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 96rpx;
|
|
|
|
|
|
background: #ff6b35;
|
|
|
|
|
|
border-radius: 40rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
|
|
|
|
|
|
|
|
|
|
|
|
text {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|