Files
msh-system/msh_single_uniapp/pages/tool/checkin-detail.vue
scottpan 314a29ea70 feat: 打卡详情页一键分享到社区功能
- 新增后端接口 POST /api/front/tool/checkin/{checkinId}/share-to-community
- 实现 shareCheckinToCommunity 方法,复制打卡数据到社区帖子表
- 前端修改 handleCopyCheckin 方法,直接调用后端接口创建帖子
- 支持将打卡图片、描述、营养分析数据复制到社区
- 添加重复分享检查,防止重复创建帖子
- 创建成功后显示提示并可跳转到社区详情页
2026-03-08 00:40:01 +08:00

541 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="checkin-detail-page">
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 图片轮播区域 -->
<view class="image-carousel">
<swiper
class="swiper"
:indicator-dots="true"
:autoplay="false"
:current="currentImageIndex"
@change="onImageChange"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#ffffff"
>
<swiper-item v-for="(image, index) in checkinData.images" :key="index">
<image class="carousel-image" :src="image" mode="aspectFill"></image>
</swiper-item>
</swiper>
<!-- 餐次标签 -->
<view class="meal-tag">
<text class="meal-icon">{{ getMealIcon(checkinData.mealType) }}</text>
<text class="meal-text">{{ getMealText(checkinData.mealType) }}</text>
</view>
<!-- 图片页码 -->
<view class="image-counter">
{{ currentImageIndex + 1 }} / {{ checkinData.images.length }}
</view>
</view>
<!-- 视频播放区域 -->
<view class="video-section" v-if="checkinData.videoUrl">
<view class="section-header">
<text class="header-icon">🎬</text>
<text class="header-title">打卡视频</text>
</view>
<video
class="checkin-video"
:src="checkinData.videoUrl"
controls
:show-center-play-btn="true"
:enable-progress-gesture="true"
object-fit="contain"
:poster="checkinData.images && checkinData.images.length > 0 ? checkinData.images[0] : ''"
></video>
</view>
<!-- 视频生成中状态 -->
<view class="video-generating" v-else-if="checkinData.enableAIVideo && checkinData.taskId">
<text class="generating-icon"></text>
<text class="generating-text">视频生成中请稍后刷新查看...</text>
</view>
<!-- 帖子内容区域 -->
<view class="post-content-section">
<!-- 作者信息 -->
<view class="author-section">
<view class="author-info">
<view class="author-avatar">
<image :src="userInfo.avatar" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="author-details">
<view class="author-name">{{ userInfo.nickname }}</view>
<view class="post-time">{{ checkinData.createTime }}</view>
</view>
</view>
<view class="points-badge" v-if="checkinData.points > 0">
<text>+{{ checkinData.points }}积分</text>
</view>
</view>
<!-- 帖子描述 -->
<view class="post-description">
<text>{{ checkinData.notes || '暂无描述' }}</text>
</view>
</view>
<!-- 营养统计卡片 -->
<view class="nutrition-stats-card" v-if="checkinData.aiAnalysis">
<view class="stats-header">
<view class="stats-title">
<text class="title-icon">📊</text>
<text class="title-text">AI 营养分析</text>
</view>
</view>
<view class="ai-analysis-content">
<text>{{ checkinData.aiAnalysis }}</text>
</view>
</view>
<!-- 分享到社区按钮 -->
<view class="copy-checkin-btn" @click="handleCopyCheckin">
<text class="btn-icon">🎬</text>
<text class="btn-text">分享到社区</text>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</scroll-view>
</view>
</template>
<script>
import { getCheckinDetail, shareCheckinToCommunity } from '@/api/tool.js';
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['userInfo'])
},
data() {
return {
currentImageIndex: 0,
checkinData: {
id: '',
images: [],
mealType: '',
createTime: '',
notes: '',
points: 0,
aiAnalysis: '',
videoUrl: '',
taskId: '',
enableAIVideo: false,
videoStatus: 0
}
}
},
onLoad(options) {
if (options.id) {
this.loadCheckinDetail(options.id);
} else if (options.item) {
try {
const item = JSON.parse(decodeURIComponent(options.item));
this.formatCheckinData(item);
} catch (e) {
console.error('解析打卡数据失败:', e);
uni.showToast({
title: '数据加载失败',
icon: 'none'
});
}
}
},
methods: {
async loadCheckinDetail(id) {
uni.showLoading({
title: '加载中...'
});
try {
const res = await getCheckinDetail(id);
if (res && res.code === 200) {
this.formatCheckinData(res.data);
} else {
throw new Error(res.message || '获取详情失败');
}
} catch (e) {
console.error('获取详情失败:', e);
uni.showToast({
title: '获取详情失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
formatCheckinData(item) {
let images = [];
if (item.photos) {
try {
if (typeof item.photos === 'string') {
if (item.photos.startsWith('[')) {
images = JSON.parse(item.photos);
} else {
images = [item.photos];
}
} else if (Array.isArray(item.photos)) {
images = item.photos;
}
} catch (e) {
console.error('解析图片失败:', e);
if (typeof item.photos === 'string') {
images = [item.photos];
}
}
}
this.checkinData = {
id: item.id,
images: images,
mealType: item.mealType,
createTime: item.date || item.createTime,
notes: item.notes,
points: item.points || 0,
aiAnalysis: item.aiAnalysis || item.nutritionScore,
videoUrl: item.videoUrl || '',
taskId: item.taskId || '',
enableAIVideo: item.enableAIVideo || false,
videoStatus: item.videoStatus || 0
};
},
onImageChange(e) {
this.currentImageIndex = e.detail.current
},
getMealText(type) {
const map = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐'
};
return map[type] || type;
},
getMealIcon(type) {
const map = {
breakfast: '🌅',
lunch: '☀️',
dinner: '🌙',
snack: '🍪'
};
return map[type] || '🍽️';
},
async handleCopyCheckin() {
if (!this.checkinData.id) {
uni.showToast({
title: '打卡记录ID不存在',
icon: 'none'
});
return;
}
uni.showLoading({
title: '分享中...',
mask: true
});
try {
const res = await shareCheckinToCommunity(this.checkinData.id);
uni.hideLoading();
if (res && res.code === 200) {
const postId = res.data && res.data.postId;
uni.showModal({
title: '分享成功',
content: '您的打卡记录已成功分享到社区',
confirmText: '去查看',
cancelText: '知道了',
success: (modalRes) => {
if (modalRes.confirm && postId) {
uni.navigateTo({
url: '/pages/tool/community-detail?id=' + postId
});
}
}
});
} else {
throw new Error(res.message || '分享失败');
}
} catch (e) {
uni.hideLoading();
console.error('分享到社区失败:', e);
uni.showToast({
title: e.message || '分享失败,请重试',
icon: 'none'
});
}
}
}
}
</script>
<style lang="scss" scoped>
.checkin-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #fafafa 0%, #ffffff 100%);
display: flex;
flex-direction: column;
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
}
/* 图片轮播区域 */
.image-carousel {
width: 100%;
height: 750rpx;
position: relative;
background: #f4f5f7;
.swiper {
width: 100%;
height: 100%;
}
.carousel-image {
width: 100%;
height: 100%;
}
.meal-tag {
position: absolute;
left: 32rpx;
top: 32rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50rpx;
padding: 8rpx 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.meal-icon {
font-size: 32rpx;
}
.meal-text {
font-size: 28rpx;
color: #3e5481;
}
}
.image-counter {
position: absolute;
right: 32rpx;
bottom: 32rpx;
background: rgba(0, 0, 0, 0.7);
border-radius: 50rpx;
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
/* 帖子内容区域 */
.post-content-section {
padding: 32rpx;
background: #ffffff;
}
.author-section {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.author-info {
display: flex;
align-items: center;
gap: 24rpx;
}
.author-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
background: #f5f5f5;
.avatar-img {
width: 100%;
height: 100%;
}
}
.author-details {
display: flex;
flex-direction: column;
gap: 8rpx;
.author-name {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
}
.post-time {
font-size: 24rpx;
color: #9fa5c0;
}
}
.points-badge {
background: linear-gradient(135deg, #ff8c5a 0%, #ff6b35 100%);
border-radius: 50rpx;
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
.post-description {
font-size: 30rpx;
color: #3e5481;
line-height: 1.6;
white-space: pre-wrap;
}
/* 营养统计卡片 */
.nutrition-stats-card {
margin: 10rpx 32rpx;
padding: 32rpx;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 50%, #ffffff 100%);
border: 2rpx solid rgba(255, 136, 68, 0.3);
border-radius: 32rpx;
box-shadow: 0 16rpx 60rpx rgba(255, 136, 68, 0.15);
}
.stats-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.stats-title {
display: flex;
align-items: center;
gap: 16rpx;
.title-icon {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #ff9966 0%, #ff8844 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.1);
}
.title-text {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
}
}
.ai-analysis-content {
font-size: 28rpx;
color: #3e5481;
line-height: 1.6;
}
// 视频播放区域
.video-section {
margin: 24rpx 32rpx;
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
.section-header {
padding: 24rpx 32rpx;
display: flex;
align-items: center;
gap: 12rpx;
border-bottom: 2rpx solid #f5f5f5;
.header-icon {
font-size: 32rpx;
}
.header-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
}
}
.checkin-video {
width: 100%;
height: 400rpx;
background: #000000;
}
}
// 视频生成中状态
.video-generating {
margin: 24rpx 32rpx;
padding: 40rpx;
background: #f9f9f9;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
.generating-icon {
font-size: 48rpx;
animation: rotate 2s linear infinite;
}
.generating-text {
font-size: 24rpx;
color: #999999;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 一键借鉴打卡按钮 */
.copy-checkin-btn {
margin: 40rpx 32rpx;
height: 94rpx;
background: linear-gradient(135deg, #ff8844 0%, #ff6611 50%, #ff7722 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-shadow: 0 16rpx 60rpx rgba(255, 107, 53, 0.3);
.btn-icon {
font-size: 36rpx;
}
.btn-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
/* 底部安全距离 */
.safe-bottom {
height: 40rpx;
}
</style>