CheckinCard.vue 9.3 KB
<template>
    <div class="post-card shadow-md">
        <!-- Header -->
        <div class="post-header">
            <van-row>
                <van-col span="4">
                    <van-image round width="2.5rem" height="2.5rem"
                        :src="getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')"
                        fit="cover" />
                </van-col>
                <van-col span="17">
                    <div class="user-info">
                        <div class="username">
                            {{ post.user.name }}
                            <!-- Makeup Tag -->
                            <span v-if="post.user.is_makeup"
                                class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300">补打卡</span>
                            <slot name="header-tags"></slot>
                        </div>
                        <div class="post-time">{{ post.user.time }}</div>
                    </div>
                </van-col>
                <van-col span="3">
                    <div v-if="post.is_my" class="post-menu">
                        <slot name="menu">
                            <!-- Default menu items if needed, or left empty for parent to fill -->
                            <van-icon name="edit" @click="emit('edit', post)" class="mr-2" color="#4caf50" />
                            <van-icon name="delete-o" @click="emit('delete', post)" color="#f44336" />
                        </slot>
                    </div>
                    <slot name="header-right" v-else></slot>
                </van-col>
            </van-row>
        </div>

        <!-- Content -->
        <div class="post-content">
            <slot name="content-top"></slot>
            <PostCountModel :post-data="post" />
            <div class="post-text">{{ post.content }}</div>

            <!-- Media -->
            <div class="post-media">
                <!-- Images -->
                <div v-if="post.images.length" class="post-images">
                    <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index"
                        :src="getOptimizedUrl(image)" radius="5" @click="openImagePreview(index)" />
                </div>
                <van-image-preview v-if="post.images.length" v-model:show="showLocalImagePreview" :images="post.images"
                    :start-position="localStartPosition" :show-index="true" />

                <!-- Videos -->
                <div v-for="(v, idx) in post.videoList" :key="idx">
                    <!-- Cover -->
                    <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden"
                        style="aspect-ratio: 16/9; margin-bottom: 1rem;">
                        <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')"
                            :alt="post.content" class="w-full h-full object-contain" />
                        <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
                            @click="startPlay(v)">
                            <div
                                class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
                                <van-icon name="play-circle-o" class="text-white" size="40" />
                            </div>
                        </div>
                    </div>
                    <!-- Video Player -->
                    <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video"
                        :video-id="v.id || `video-${post.id}-${idx}`" :use-native-on-ios="false" class="post-video rounded-lg overflow-hidden"
                        :ref="(el) => setVideoRef(el, v.id)" @onPlay="(player) => handleVideoPlay(player, v)"
                        @onPause="handleVideoPause" />
                </div>

                <!-- Audio -->
                <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" :id="post.id"
                    :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" />
            </div>
        </div>

        <!-- Footer -->
        <div class="post-footer flex items-center justify-between">
            <!-- Left: Like -->
            <div class="flex items-center">
                <van-icon @click="emit('like', post)" name="good-job" class="like-icon"
                    :color="post.is_liked ? 'red' : ''" />
                <span class="like-count ml-1">{{ post.likes }}</span>
            </div>
            <!-- Right: Custom Actions -->
            <div class="flex items-center">
                <slot name="footer-right"></slot>
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref } from 'vue'
import PostCountModel from "@/components/count/postCountModel.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";

const props = defineProps({
    post: { type: Object, required: true },
    useCdnOptimization: { type: Boolean, default: false }
})

const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play'])

// Image Preview State
const showLocalImagePreview = ref(false)
const localStartPosition = ref(0)

const openImagePreview = (index) => {
    localStartPosition.value = index
    showLocalImagePreview.value = true
}

// Media Refs
const videoRefs = ref(new Map())
const audioRefs = ref(new Map())

const setVideoRef = (el, id) => {
    if (el) {
        videoRefs.value.set(id, el)
    } else {
        videoRefs.value.delete(id)
    }
}
const setAudioRef = (el, id) => {
    if (el) {
        audioRefs.value.set(id, el)
    } else {
        audioRefs.value.delete(id)
    }
}

// Optimization
const getOptimizedUrl = (url) => {
    if (!props.useCdnOptimization) return url
    if (!url || !url.includes('cdn.ipadbiz.cn')) return url
    if (url.includes('?')) return url
    return `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
}

// Video Logic
const startPlay = (v) => {
    // Pause other videos in this card
    props.post.videoList.forEach(item => {
        if (item.id !== v.id) item.isPlaying = false
    })
    v.isPlaying = true
}

const handleVideoPlay = (player, v) => {
    // Stop local audio
    stopLocalAudio()
    // Emit to parent to stop other cards
    emit('video-play', { post: props.post, player, videoId: v.id })
}

const handleVideoPause = () => {
    // do nothing
}

// Audio Logic
const handleAudioPlay = (player) => {
    // Stop local videos
    stopLocalVideos()
    // Emit to parent
    emit('audio-play', { post: props.post, player })
}

const stopLocalVideos = () => {
    videoRefs.value.forEach(player => {
        if (player && typeof player.pause === 'function') {
            player.pause()
        }
    })
    props.post.videoList.forEach(v => v.isPlaying = false)
}

const stopLocalAudio = () => {
    audioRefs.value.forEach(player => {
        if (player && typeof player.pause === 'function') {
            player.pause()
        }
    })
    // Also update isPlaying state if AudioPlayer doesn't handle it fully self-contained
    // But AudioPlayer usually manages its own state or we don't track audio isPlaying on post object for audio lists?
    // Looking at IndexCheckInPage: post.audio is array. AudioPlayer takes songs.
    // So we just pause the player component.
}

// Expose methods for parent
defineExpose({
    stopAllMedia: () => {
        stopLocalVideos()
        stopLocalAudio()
    },
    // Also expose post id for convenience
    id: props.post.id
})
</script>

<style lang="less" scoped>
.post-card {
    background: #fff;
    border-radius: 10px;
    padding: 1rem;
    margin-bottom: 1rem;

    .post-header {
        margin-bottom: 0.5rem;

        .user-info {
            display: flex;
            flex-direction: column;
            justify-content: center;
            height: 2.5rem;
            margin-left: 0.5rem;

            .username {
                font-weight: bold;
                font-size: 0.95rem;
                display: flex;
                align-items: center;
            }

            .post-time {
                font-size: 0.75rem;
                color: #999;
            }
        }

        .post-menu {
            display: flex;
            justify-content: flex-end;
            font-size: 1.2rem;
            color: #999;
        }
    }

    .post-content {
        .post-text {
            color: #666;
            margin-bottom: 1rem;
            white-space: pre-wrap;
            word-wrap: break-word;
        }

        .post-media {
            .post-images {
                display: flex;
                flex-wrap: wrap;
                gap: 0.5rem;
            }

            .post-video {
                margin: 1rem 0;
                width: 100%;
                border-radius: 8px;
                overflow: hidden;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            }

            .post-audio {
                margin: 1rem 0;
            }
        }
    }

    .post-footer {
        margin-top: 1rem;
        color: #666;

        .like-icon {
            margin-right: 0.25rem;
        }

        .like-count {
            font-size: 0.9rem;
        }
    }
}
</style>