Files
hejiu/components/FeedCard.vue
T
cg 81e7a3afa5 feat(event): 添加酒局报名审核功能并优化前端组件
- 新增 ApproveEvent API 接口用于审核报名用户
- 在 EventCard 组件中显示待审核状态(⏳ 待审核)
- 实现酒局发起人审核报名用户的完整流程
- 添加用户协议和隐私政策页面
- 优化 FeedCard 组件中的图片展示逻辑
- 修复连续打卡天数计算问题
- 更新 HaveADrink SDK 至 1.0.15 版本
2026-08-16 14:50:50 +08:00

356 lines
8.6 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="feed-card" @click="$emit('item-click', feed)">
<!-- 头部:头像+昵称+时间 -->
<view class="feed-header">
<view class="feed-avatar-wrap">
<image v-if="feed.avatar" class="feed-avatar" :src="feed.avatar" mode="aspectFill"></image>
<view v-else class="feed-avatar feed-avatar-ph">
<text class="feed-avatar-text">{{ feed.nickname ? feed.nickname[0] : '酒' }}</text>
</view>
</view>
<view class="feed-meta">
<text class="feed-name">{{ feed.nickname }}</text>
<text class="feed-time">{{ feed.time }}</text>
</view>
</view>
<!-- 正文 -->
<text class="feed-text">{{ feed.text }}</text>
<!-- 饮酒标签 -->
<view class="feed-drinks" v-if="feed.drinks && feed.drinks.length">
<view class="feed-drink-tag" v-for="(d, i) in feed.drinks" :key="i">
<image v-if="isIconPath(getCatIcon(d.category))" :src="getCatIcon(d.category)" class="feed-drink-icon-img" mode="aspectFit"></image>
<text v-else class="feed-drink-emoji">{{ getCatIcon(d.category) }}</text>
<text class="feed-drink-name">{{ d.name }} {{ d.amount }}</text>
</view>
</view>
<!-- 感受标签 -->
<view class="feed-feeling" v-if="feed.feeling">
<text class="feed-feeling-text">{{ feelingLabel(feed.feeling) }}</text>
</view>
<!-- 图片:按原图比例自适应高度,极端比例才裁剪 -->
<view class="feed-images" v-if="feed.images && feed.images.length">
<image
v-for="(img, i) in feed.images"
:key="i"
class="feed-img"
:src="img"
mode="aspectFill"
:style="imgStyle(i)"
@click.stop="previewImage(i)"
></image>
</view>
<!-- 操作栏 -->
<view class="feed-actions">
<view class="feed-action" :class="{ active: feed.liked }" @click.stop="$emit('like', feed)">
<text class="feed-action-icon">{{ feed.liked ? '❤️' : '🤍' }}</text>
<text class="feed-action-num">{{ feed.likes || '' }}</text>
</view>
<view class="feed-action" @click.stop="$emit('comment', feed)">
<text class="feed-action-icon">💬</text>
<text class="feed-action-num">{{ feed.comments || '' }}</text>
</view>
<button class="feed-action share-btn" open-type="share" @click.stop="$emit('share', feed)">
<text class="feed-action-icon">📤</text>
</button>
<!-- 删除:仅自己的动态显示 -->
<view v-if="canDelete" class="feed-action feed-delete" @click.stop="$emit('delete', feed)">
<text class="feed-action-icon">🗑️</text>
<text class="feed-delete-text">删除</text>
</view>
</view>
</view>
</template>
<script>
import { getCatIcon, isIconPath } from '../common/utils'
export default {
name: 'FeedCard',
props: {
feed: { type: Object, default: () => ({}) },
// 是否允许删除(仅自己的动态传 true)
canDelete: { type: Boolean, default: false }
},
emits: ['item-click', 'like', 'comment', 'share', 'delete'],
data() {
return {
// 图片宽高比缓存:url -> width/height
imgRatios: {}
}
},
watch: {
'feed.images': {
handler(list) { this.loadImgMeta(list) },
immediate: true
}
},
methods: {
getCatIcon,
isIconPath,
/** 拉取图片真实尺寸,用于按比例计算展示高度 */
loadImgMeta(list) {
if (!list || !list.length) return
list.forEach(url => {
if (!url || this.imgRatios[url] !== undefined) return
uni.getImageInfo({
src: url,
success: res => {
if (res.width && res.height) {
this.imgRatios[url] = res.width / res.height
}
}
})
})
},
/** 每行图片数量:4张用2x2,其余每行最多3张 */
perRow() {
const n = (this.feed.images || []).length
return n === 4 ? 2 : Math.min(n || 1, 3)
},
/** 列宽(rpx):750 - 页面边距64 - 卡片内边距96 - 行间gap */
colWidth() {
const gap = 16
const total = 750 - 64 - 96
const cols = this.perRow()
return (total - gap * (cols - 1)) / cols
},
/** 单张图片的目标高度:按真实比例,夹在上下限内 */
targetHeight(ratio, colW, minH, maxH) {
const r = ratio && ratio > 0 ? ratio : 1
return Math.max(minH, Math.min(colW / r, maxH))
},
/** 同行图片统一高度(取同行目标高度的均值) */
rowHeight(rowIdx) {
const list = this.feed.images || []
const cols = this.perRow()
const colW = this.colWidth()
const single = cols === 1
const minH = single ? 300 : 180
const maxH = single ? 720 : 400
const start = rowIdx * cols
const items = list.slice(start, start + cols)
let sum = 0
items.forEach(url => {
sum += this.targetHeight(this.imgRatios[url], colW, minH, maxH)
})
return Math.round(sum / items.length)
},
/** 点击图片预览大图(支持左右滑动切换) */
previewImage(i) {
const urls = this.feed.images || []
if (!urls.length) return
uni.previewImage({
urls,
current: urls[i]
})
},
imgStyle(i) {
const cols = this.perRow()
const gap = 16
const width = cols === 1 ? '100%' : `calc(${(100 / cols).toFixed(4)}% - ${((gap * (cols - 1)) / cols).toFixed(2)}rpx)`
return {
width,
height: this.rowHeight(Math.floor(i / cols)) + 'rpx'
}
},
feelingLabel(f) {
const val = String(f).toLowerCase()
const map = {
tipsy: '😌 微醺',
buzzed: '😊 到位',
drunk: '🥴 醉了',
blackout: '😵 断片',
sober: '😌 清醒'
}
return map[val] || '🍻 喝了点'
}
}
}
</script>
<style lang="scss" scoped>
.feed-card {
background: $bg-card;
border-radius: $radius-xl;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
padding: $sp-xl;
margin-bottom: $sp-md;
}
.feed-header {
display: flex;
align-items: center;
gap: $sp-md;
margin-bottom: $sp-lg;
}
.feed-avatar-wrap {
flex-shrink: 0;
}
.feed-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.feed-avatar-ph {
background: linear-gradient(135deg, rgba(232,168,56,0.2), rgba(139,133,184,0.2));
display: flex;
align-items: center;
justify-content: center;
}
.feed-avatar-text {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $amber;
}
.feed-meta {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.feed-name {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.feed-time {
font-size: $fs-xs;
color: $text-tertiary;
}
.feed-text {
display: block;
font-size: $fs-base;
color: $text-primary;
line-height: $lh-loose;
margin-bottom: $sp-md;
}
.feed-drinks {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-bottom: $sp-md;
}
.feed-drink-tag {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 20rpx;
background: $bg-elevated;
border-radius: $radius-full;
border: 1rpx solid var(--border-micro, rgba(255,255,255,0.05));
}
.feed-drink-emoji {
font-size: $fs-sm;
}
.feed-drink-icon-img {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
.feed-drink-name {
font-size: $fs-xs;
color: $text-secondary;
}
.feed-feeling {
margin-bottom: $sp-md;
}
.feed-feeling-text {
font-size: $fs-xs;
color: $text-tertiary;
padding: 6rpx 16rpx;
background: rgba(232,168,56,0.06);
border-radius: $radius-full;
}
.feed-images {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-bottom: $sp-md;
}
.feed-img {
border-radius: $radius-md;
display: block;
/* 比例未获取前的占位尺寸,避免布局跳动 */
width: 33%;
height: 220rpx;
background: $bg-elevated;
}
.feed-actions {
display: flex;
align-items: center;
gap: $sp-xl;
padding-top: $sp-md;
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
}
/* 删除按钮靠右 */
.feed-delete {
margin-left: auto;
}
.feed-delete-text {
font-size: $fs-sm;
color: $text-tertiary;
}
.feed-action {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 0;
&.active .feed-action-num {
color: $coral;
}
&:active {
opacity: 0.7;
}
}
.feed-action-icon {
font-size: $fs-base;
}
.feed-action-num {
font-size: $fs-sm;
color: $text-tertiary;
}
/* 分享按钮:重置 button 默认样式,保持与其他操作项视觉一致 */
.feed-action.share-btn {
margin: 0;
padding: 8rpx 0;
background: transparent;
line-height: 1;
border-radius: 0;
font-size: inherit;
&::after {
border: none;
}
}
</style>