999 lines
21 KiB
Vue
999 lines
21 KiB
Vue
<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
|
||
|
||
uni.showLoading({
|
||
title: this.enableAIVideo ? '正在创建视频任务...' : '发布中...'
|
||
});
|
||
|
||
try {
|
||
const imageUrls = this.selectedImages;
|
||
|
||
// 如果开启了AI视频生成,创建视频任务
|
||
let videoTaskId = '';
|
||
if (this.enableAIVideo) {
|
||
try {
|
||
// 构建视频生成提示词
|
||
const mealLabel = this.mealTypes.find(m => m.value === this.selectedMealType)?.label || '饮食';
|
||
const videoPrompt = this.remark || `健康${mealLabel}打卡`;
|
||
|
||
const taskParams = {
|
||
uid: String(this.uid || ''),
|
||
prompt: videoPrompt,
|
||
image_urls: imageUrls,
|
||
remove_watermark: true
|
||
};
|
||
|
||
let videoTaskRes;
|
||
if (imageUrls.length > 0) {
|
||
// 图生视频:使用第一张图片作为参考图
|
||
videoTaskRes = await api.createImageToVideoTask({
|
||
...taskParams,
|
||
imageUrl: imageUrls[0]
|
||
});
|
||
} else {
|
||
// 文生视频
|
||
videoTaskRes = await api.createTextToVideoTask(taskParams);
|
||
}
|
||
|
||
if (videoTaskRes && videoTaskRes.code === 200 && videoTaskRes.data) {
|
||
videoTaskId = videoTaskRes.data;
|
||
console.log('视频生成任务已提交,taskId:', videoTaskId);
|
||
} else {
|
||
console.warn('视频任务创建返回异常:', videoTaskRes);
|
||
}
|
||
} catch (videoError) {
|
||
console.error('创建视频任务失败:', videoError);
|
||
// 视频任务失败不阻断打卡提交,只提示
|
||
uni.showToast({
|
||
title: '视频任务创建失败,打卡将继续提交',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
// 等待toast显示
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
}
|
||
}
|
||
|
||
// 更新loading提示
|
||
uni.showLoading({ title: '提交打卡中...' });
|
||
|
||
// 提交打卡
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const result = await submitCheckin({
|
||
mealType: this.selectedMealType,
|
||
date: today,
|
||
photosJson: JSON.stringify(imageUrls),
|
||
notes: this.remark,
|
||
taskId: videoTaskId,
|
||
enableAIVideo: this.enableAIVideo,
|
||
enableAIAnalysis: false
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
// 显示成功提示
|
||
const points = result.data?.points || 0;
|
||
const successMsg = videoTaskId
|
||
? `打卡成功!视频生成中...`
|
||
: `打卡成功!获得${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>
|
||
|