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

816 lines
20 KiB
Vue
Raw Normal View History

<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>