Files
msh-system/msh_single_uniapp/pages/tool/dietary-records.vue

816 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="history-container" :style="{ paddingTop: statusBarHeight + 'px' }">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="iconfont icon-fanhui"></text>
</view>
<view class="nav-title">饮食记录</view>
<view class="nav-right"> </view>
</view>
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', selectedTab === tab.value ? 'active' : '']"
@click="switchTab(tab.value)"
>
<text class="tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- 内容区 -->
<scroll-view scroll-y class="content-scroll" :style="{ height: scrollViewHeight }" @scrolltolower="loadMore">
<!-- 空状态 -->
<view class="empty-state" v-if="historyList.length === 0 && !isLoading">
<text class="empty-icon">🍽</text>
<text class="empty-text">暂无饮食记录</text>
<button class="empty-btn" @click="goToCheckin">去打卡</button>
</view>
<!-- 历史列表 -->
<view class="history-list" v-else>
<view
v-for="(item, index) in historyList"
:key="index"
class="history-item"
@click="viewDetail(item)"
>
<!-- 左侧图片预览 -->
<view class="item-images">
<image
v-if="item.photos && item.photos.length > 0"
:src="item.photos[0]"
mode="aspectFill"
class="preview-image"
></image>
<view class="no-image" v-else>
<text>无图</text>
</view>
<view class="image-count" v-if="item.photos && item.photos.length > 1">
<text class="count-text">+{{ item.photos.length - 1 }}</text>
</view>
<!-- 餐次标识 -->
<view class="status-badge status-completed">
<text class="status-text">{{ getMealTypeText(item.mealType) }}</text>
</view>
<!-- 视频状态标记 -->
<view class="video-status-badge" v-if="item.enableAIVideo">
<text v-if="item.videoUrl || item.videoStatus === 1" class="status-success">📹</text>
<text v-else-if="item.videoStatus === 2" class="status-failed"></text>
<text v-else class="status-pending"></text>
</view>
</view>
<!-- 右侧信息 -->
<view class="item-info">
<view class="info-header">
<text class="item-prompt">{{ item.notes || '健康饮食打卡' }}</text>
<!-- <view class="item-actions" @click.stop="showItemMenu(item)">
<text class="iconfont icon-gengduo"></text>
</view> -->
</view>
<view class="info-meta">
<view class="meta-tags">
<text class="meta-tag">{{ item.date }}</text>
<text class="meta-tag" v-if="item.points > 0">+{{ item.points }}积分</text>
<!-- 视频状态文字提示 -->
<text class="meta-tag video-tag" v-if="item.videoUrl || item.videoStatus === 1">有视频</text>
<text class="meta-tag video-tag generating" v-else-if="item.enableAIVideo && item.taskId && item.videoStatus === 0">生成中</text>
<text class="meta-tag video-tag failed" v-else-if="item.videoStatus === 2">视频失败</text>
</view>
</view>
<!-- AI分析状态 -->
<view class="item-remark" v-if="item.aiAnalysis">
<text class="remark-text">{{ item.aiAnalysis }}</text>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && historyList.length > 0">
<text class="load-text">{{ isLoading ? '加载中...' : '加载更多' }}</text>
</view>
</scroll-view>
<!-- 操作菜单 -->
<view class="action-sheet" v-if="showActionSheet" @click="showActionSheet = false">
<view class="action-content" @click.stop>
<view class="action-title">操作</view>
<view
v-for="(action, index) in currentActions"
:key="index"
class="action-item"
@click="handleAction(action.value)"
>
<text class="action-text">{{ action.label }}</text>
</view>
<view class="action-cancel" @click="showActionSheet = false">
<text>取消</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getCheckinList, getVideoTaskStatus } from '@/api/tool.js';
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['userInfo', 'uid'])
},
data() {
return {
statusBarHeight: 0,
scrollViewHeight: '100vh',
// Tab
selectedTab: '',
tabs: [
{ label: '全部', value: '' },
{ label: '早餐', value: 'breakfast' },
{ label: '午餐', value: 'lunch' },
{ label: '晚餐', value: 'dinner' },
{ label: '加餐', value: 'snack' },
],
// 历史记录
historyList: [],
// 分页
currentPage: 1,
pageSize: 10,
hasMore: true,
isLoading: false,
// 视频轮询
pollingTimer: null,
pollingCount: 0,
maxPollingCount: 60, // 最多轮询60次(5分钟)
// 操作菜单
showActionSheet: false,
currentItem: null,
currentActions: []
};
},
onLoad() {
this.initPage();
this.loadHistoryList();
},
onShow() {
// 从其他页面返回时重新检查
if (this.historyList.length > 0) {
this.loadHistoryList(true);
}
this.startPolling();
},
onUnload() {
this.stopPolling();
},
methods: {
initPage() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// 计算滚动区域高度
const navHeight = 44 + 44; // 导航+Tab
this.scrollViewHeight = `calc(100vh - ${this.statusBarHeight + navHeight}px)`;
},
// 返回
goBack() {
uni.navigateBack();
},
// 切换Tab
switchTab(tabValue) {
if (this.selectedTab === tabValue) return;
this.selectedTab = tabValue;
this.loadHistoryList(true);
},
// 加载历史记录列表
async loadHistoryList(refresh = false) {
if (this.isLoading) return;
if (refresh) {
this.currentPage = 1;
this.hasMore = true;
this.historyList = [];
}
if (!this.hasMore) return;
this.isLoading = true;
try {
const res = await getCheckinList({
mealType: this.selectedTab,
page: this.currentPage,
limit: this.pageSize
});
if (res && res.code === 200) {
const list = (res.data.list || res.data || []).map(item => {
// 解析photos
let photos = [];
if (item.photos) {
try {
// 如果是字符串尝试解析JSON
if (typeof item.photos === 'string') {
photos = JSON.parse(item.photos);
} else if (Array.isArray(item.photos)) {
photos = item.photos;
}
} catch (e) {
console.error('解析图片数据失败:', e);
// 如果解析失败可能是单个URL字符串或者是无法解析的字符串
if (typeof item.photos === 'string' && item.photos.startsWith('http')) {
photos = [item.photos];
}
}
}
return {
...item,
photos: photos,
date: item.createTime, // 使用 createTime 作为显示日期
points: item.points || 0, // 接口可能没有返回积分默认0
aiAnalysis: item.aiAnalysis || item.nutritionScore, // AI分析结果
taskId: item.taskId || null, // 视频任务ID
videoUrl: item.videoUrl || null, // 视频URL
videoStatus: item.videoStatus !== undefined ? item.videoStatus : null, // 视频状态
enableAIVideo: item.enableAIVideo || false // 是否启用AI视频
};
});
if (refresh) {
this.historyList = list;
} else {
this.historyList = [...this.historyList, ...list];
}
// 判断是否还有更多
if (list.length < this.pageSize) {
this.hasMore = false;
} else {
this.currentPage++;
}
// 启动轮询检查视频任务
this.startPolling();
} else {
throw new Error(res?.message || '获取记录失败');
}
} catch (error) {
console.error('加载历史记录失败:', error);
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
this.isLoading = false;
}
},
// 加载更多
loadMore() {
if (this.hasMore && !this.isLoading) {
this.loadHistoryList(false);
}
},
// 去打卡
goToCheckin() {
uni.navigateTo({
url: '/pages/tool/checkin-publish'
});
},
// 查看详情
viewDetail(item) {
uni.navigateTo({
url: `/pages/tool/checkin-detail?id=${item.id}`
});
},
// 显示管理菜单
showManageMenu() {
this.currentActions = [
{ label: '刷新列表', value: 'refresh' }
];
this.showActionSheet = true;
},
// 显示单项菜单
showItemMenu(item) {
this.currentItem = item;
this.currentActions = [
{ label: '查看详情', value: 'view' },
// { label: '删除', value: 'delete' } // 暂未实现删除接口
];
this.showActionSheet = true;
},
// 处理操作
handleAction(action) {
this.showActionSheet = false;
switch (action) {
case 'view':
this.viewDetail(this.currentItem);
break;
case 'refresh':
this.loadHistoryList(true);
break;
case 'delete':
uni.showToast({ title: '删除功能开发中', icon: 'none' });
break;
}
},
getMealTypeText(type) {
const map = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐'
};
return map[type] || type;
},
// 启动视频任务轮询
startPolling() {
if (this.pollingTimer) return;
const tasksToCheck = this.historyList.filter(item =>
item.enableAIVideo && item.taskId && !item.videoUrl && item.videoStatus !== 2
);
if (tasksToCheck.length === 0) {
return;
}
this.pollingTimer = setInterval(() => {
this.pollVideoTasks();
}, 5000); // 每5秒查询一次
},
// 轮询视频任务状态
async pollVideoTasks() {
const tasksToCheck = this.historyList.filter(item =>
item.enableAIVideo && item.taskId && !item.videoUrl && item.videoStatus !== 2
);
if (tasksToCheck.length === 0 || this.pollingCount >= this.maxPollingCount) {
this.stopPolling();
return;
}
this.pollingCount++;
for (const task of tasksToCheck) {
try {
const res = await getVideoTaskStatus(task.taskId);
if (res.code === 200 && res.data) {
const status = res.data.state;
if (status === 'success') {
// 任务成功刷新列表获取最新视频URL
this.loadHistoryList(true);
this.pollingCount = 0; // 重置计数
break;
} else if (status === 'fail') {
// 任务失败,刷新列表以更新失败状态,停止对该任务的轮询
this.loadHistoryList(true);
this.pollingCount = 0; // 重置计数
break;
}
}
} catch (error) {
console.error('查询视频任务失败:', error);
}
}
},
// 停止轮询
stopPolling() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
this.pollingCount = 0;
}
}
};
</script>
<style lang="scss" scoped>
.history-container {
width: 100%;
min-height: 100vh;
background-color: #f4f5f7;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
// 顶部导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background-color: #ffffff;
position: sticky;
top: 0;
z-index: 100;
.nav-left,
.nav-right {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 20px;
color: #333333;
}
}
.nav-title {
font-size: 14px;
color: #333333;
}
}
// Tab栏
.tab-bar {
display: flex;
height: 44px;
background-color: #ffffff;
border-bottom: 1px solid #eeeeee;
z-index: 99;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.tab-text {
font-size: 15px;
color: #999999;
transition: all 0.3s;
}
&.active {
.tab-text {
color: #ff6b35;
font-weight: 500;
font-size: 16px;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 3px;
background: #ff6b35;
border-radius: 2px;
}
}
}
}
// 内容滚动区
.content-scroll {
flex: 1;
padding: 16px;
width: 100%;
box-sizing: border-box;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
.empty-icon {
font-size: 60px;
margin-bottom: 24px;
}
.empty-text {
font-size: 15px;
color: #999999;
margin-bottom: 32px;
}
.empty-btn {
width: 160px;
height: 44px;
background: linear-gradient(135deg, #ff6b35 0%, #ff8c5a 100%);
border-radius: 22px;
font-size: 16px;
color: #ffffff;
border: none;
line-height: 44px;
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
&:active {
transform: scale(0.98);
}
}
}
// 历史列表
.history-list {
padding-bottom: 20px;
.history-item {
display: flex;
gap: 16px;
padding: 16px;
background: #ffffff;
border-radius: 16px;
margin-bottom: 16px;
min-height: 100px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
transition: all 0.3s;
&:active {
transform: scale(0.99);
background: #fafafa;
}
.item-images {
width: 96px;
height: 96px;
flex-shrink: 0;
position: relative;
border-radius: 12px;
overflow: hidden;
background: #f5f5f5;
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #cccccc;
font-size: 12px;
background-color: #f0f0f0;
}
.image-count {
position: absolute;
bottom: 4px;
right: 4px;
padding: 2px 6px;
background: rgba(0, 0, 0, 0.6);
border-radius: 4px;
.count-text {
font-size: 10px;
color: #ffffff;
}
}
.status-badge {
position: absolute;
top: 4px;
left: 4px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
.status-text {
font-size: 10px;
font-weight: 500;
color: #ff6b35;
}
}
.video-status-badge {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
text {
font-size: 12px;
}
}
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
padding: 2px 0;
.info-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
.item-prompt {
flex: 1;
font-size: 16px;
color: #333333;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-actions {
width: 24px;
height: 24px;
display: flex;
align-items: flex-start;
justify-content: center;
margin-top: -2px;
.iconfont {
font-size: 18px;
color: #999999;
}
}
}
.info-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
.meta-tags {
display: flex;
gap: 8px;
align-items: center;
.meta-tag {
font-size: 12px;
color: #999999;
background: #f5f5f5;
padding: 2px 8px;
border-radius: 4px;
&:last-child {
color: #ff6b35;
background: rgba(255, 107, 53, 0.1);
}
&.video-tag {
color: #52c41a;
background: rgba(82, 196, 26, 0.1);
&.generating {
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
&.failed {
color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
}
}
}
}
}
.item-remark {
margin-top: 8px;
padding: 8px;
background: #f9f9f9;
border-radius: 8px;
.remark-text {
font-size: 12px;
color: #666666;
line-height: 1.4;
}
}
}
}
}
// 加载更多
.load-more {
padding: 20px;
text-align: center;
.load-text {
font-size: 13px;
color: #999999;
}
}
// 操作菜单
.action-sheet {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 9999;
.action-content {
width: 100%;
background: #ffffff;
border-radius: 16px 16px 0 0;
padding-bottom: env(safe-area-inset-bottom);
overflow: hidden;
.action-title {
padding: 16px;
text-align: center;
font-size: 14px;
color: #999999;
border-bottom: 1px solid #eeeeee;
}
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
border-bottom: 1px solid #eeeeee;
background: #ffffff;
&:active {
background: #f9f9f9;
}
.action-text {
font-size: 16px;
color: #333333;
}
}
.action-cancel {
margin-top: 8px;
padding: 16px;
text-align: center;
font-size: 16px;
color: #333333;
background: #ffffff;
border-top: 1px solid #eeeeee;
&:active {
background: #f9f9f9;
}
}
}
}
</style>