Files
msh-system/msh_single_uniapp/pages/tool/checkin-copy.vue

516 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-copy-page">
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 原打卡记录卡片 -->
<view class="card">
<view class="record-info">
<text class="record-label">原打卡记录</text>
<text class="record-dot"></text>
<text class="record-date">{{ originalRecord.date }} {{ originalRecord.mealTypeLabel || getMealTypeLabel(originalRecord.mealType) }}</text>
</view>
</view>
<!-- 饮食照片卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">饮食照片</text>
<text class="photo-count">已选 {{ selectedPhotoCount }}/{{ originalRecord.photos.length }}</text>
</view>
<view class="photo-grid">
<view
class="photo-item"
v-for="(photo, index) in originalRecord.photos"
:key="index"
:class="{ selected: selectedPhotos.includes(index) }"
@click="togglePhoto(index)"
>
<image class="photo-image" :src="photo" mode="aspectFill"></image>
<view class="photo-check" v-if="selectedPhotos.includes(index)">
<text class="check-icon"></text>
</view>
</view>
</view>
<view class="photo-tip">
<text>点击照片可选择/取消选择</text>
</view>
</view>
<!-- 备注说明卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">备注说明</text>
<text class="editable-label">可修改</text>
</view>
<textarea
class="remark-input"
v-model="remark"
placeholder="说点什么..."
maxlength="200"
@input="onRemarkInput"
></textarea>
<view class="char-count">{{ remark.length }}/200</view>
</view>
<!-- 生成AI营养分析视频卡片 -->
<view class="card">
<view class="card-header">
<view class="ai-header-left">
<text class="ai-icon">🎬</text>
<text class="card-title">生成AI营养分析视频</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="confirm-btn" @click="handleConfirm">
<text>确认打卡</text>
</view>
</view>
</view>
</template>
<script>
import { getCheckinDetail, copyCheckin, learnCheckin } from '@/api/tool.js';
export default {
data() {
return {
sourceType: '', // 'record' 或 'post',标记来源类型
sourceId: '',
originalRecord: {
date: '',
mealType: '早餐',
photos: [],
remark: ''
},
selectedPhotos: [],
remark: '',
enableAIVideo: false,
loading: false,
submitting: false
}
},
computed: {
selectedPhotoCount() {
return this.selectedPhotos.length
}
},
onLoad(options) {
// 从URL参数获取原打卡记录数据
if (options.recordId) {
this.sourceType = 'record'
this.sourceId = options.recordId
this.loadOriginalRecord(options.recordId)
} else if (options.postId) {
this.sourceType = 'post'
this.sourceId = options.postId
this.loadOriginalRecord(options.postId)
}
},
methods: {
togglePhoto(index) {
const photoIndex = this.selectedPhotos.indexOf(index)
if (photoIndex > -1) {
this.selectedPhotos.splice(photoIndex, 1)
} else {
this.selectedPhotos.push(index)
}
this.selectedPhotos.sort((a, b) => a - b)
},
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 handleConfirm() {
if (this.selectedPhotos.length === 0) {
uni.showToast({
title: '请至少选择一张照片',
icon: 'none'
})
return
}
if (this.submitting) return
this.submitting = true
const selectedImages = this.selectedPhotos.map(index => this.originalRecord.photos[index])
const checkinData = {
photosJson: JSON.stringify(selectedImages),
mealType: this.originalRecord.mealType, // 英文值: breakfast/lunch/dinner/snack
notes: this.remark,
enableAIVideo: this.enableAIVideo
}
try {
if (this.sourceType === 'post') {
await learnCheckin(this.sourceId, checkinData)
} else {
await copyCheckin(this.sourceId, checkinData)
}
uni.showToast({
title: '打卡成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack({ delta: 2 })
}, 1500)
} catch (error) {
console.error('打卡失败:', error)
uni.showToast({
title: typeof error === 'string' ? error : '打卡失败,请重试',
icon: 'none'
})
} finally {
this.submitting = false
}
},
async loadOriginalRecord(id) {
this.loading = true
try {
const res = await getCheckinDetail(id)
const data = res.data || res
// 格式化日期
let dateStr = ''
if (data.date) {
const d = new Date(data.date)
dateStr = `${d.getMonth() + 1}${d.getDate()}`
} else if (data.createTime) {
const d = new Date(data.createTime.replace(/-/g, '/'))
dateStr = `${d.getMonth() + 1}${d.getDate()}`
}
// 解析照片(后端返回 photos 为 JSON 字符串)
let photos = []
if (data.photos) {
photos = typeof data.photos === 'string' ? JSON.parse(data.photos) : data.photos
} else if (data.images) {
photos = typeof data.images === 'string' ? JSON.parse(data.images) : data.images
} else if (data.imageList) {
photos = data.imageList
}
// 保留英文餐次类型用于提交(后端期望英文)
this.originalRecord = {
date: dateStr || '今日',
mealType: data.mealType || 'breakfast', // 保留英文值
mealTypeLabel: this.getMealTypeLabel(data.mealType), // 中文用于显示
photos: photos,
remark: data.notes || data.remark || data.description || ''
}
this.remark = this.originalRecord.remark
// 默认选中所有照片
this.selectedPhotos = this.originalRecord.photos.map((_, i) => i)
} catch (error) {
console.error('加载原打卡记录失败:', error)
uni.showToast({
title: '加载记录失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
getMealTypeLabel(mealType) {
const map = {
'breakfast': '早餐',
'lunch': '午餐',
'dinner': '晚餐',
'snack': '加餐'
}
return map[mealType] || mealType || '早餐'
}
}
}
</script>
<style lang="scss" scoped>
.checkin-copy-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;
}
/* 原打卡记录 */
.record-info {
display: flex;
align-items: center;
gap: 16rpx;
.record-label {
font-size: 28rpx;
color: #2e3e5c;
}
.record-dot {
font-size: 32rpx;
color: #ff6b35;
line-height: 1;
}
.record-date {
font-size: 28rpx;
color: #3e5481;
}
}
.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;
}
.editable-label {
font-size: 24rpx;
color: #9fa5c0;
}
.ai-header-left {
display: flex;
align-items: center;
gap: 16rpx;
.ai-icon {
font-size: 32rpx;
}
}
}
/* 照片网格 */
.photo-grid {
display: flex;
gap: 16rpx;
margin-bottom: 24rpx;
}
.photo-item {
width: 208rpx;
height: 208rpx;
border-radius: 24rpx;
overflow: hidden;
position: relative;
border: 2rpx solid #d0dbea;
transition: all 0.3s;
&.selected {
border-color: #ff6b35;
}
.photo-image {
width: 100%;
height: 100%;
}
.photo-check {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 107, 53, 0.2);
display: flex;
align-items: center;
justify-content: center;
.check-icon {
width: 64rpx;
height: 64rpx;
background: #ff6b35;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #ffffff;
font-weight: bold;
}
}
}
.photo-tip {
background: #e3f2fd;
border-radius: 16rpx;
padding: 16rpx;
text-align: center;
text {
font-size: 24rpx;
color: #1976d2;
}
}
/* 备注说明 */
.remark-input {
width: 95%;
min-height: 192rpx;
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 16rpx;
font-size: 28rpx;
color: #2e3e5c;
line-height: 1.6;
margin-bottom: 16rpx;
}
.char-count {
font-size: 24rpx;
color: #9fa5c0;
}
/* 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: 50rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 28rpx;
color: #3e5481;
font-weight: 500;
}
}
.confirm-btn {
flex: 1;
height: 96rpx;
background: #ff6b35;
border-radius: 50rpx;
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>