Files
hejiu/components/FeedCard.vue
T
cg 019262289c feat(api): 升级 HaveADrink 依赖并实现用户头像上传功能
- 将 HaveADrink 依赖从 1.0.15 升级至 1.0.16 版本
- 新增 UpdateUserAvatar API 接口用于更新用户头像
- 添加 isValidAvatar 工具函数验证头像 URL 有效性
- 在登录流程中集成头像上传逻辑,支持微信头像选择
- 更新多个组件中的头像显示逻辑,过滤无效头像地址
- 优化 tabBar 主题应用逻辑,修复异步错误处理问题
- 修复微信新规范下头像选择的临时路径处理机制
2026-08-16 22:19:10 +08:00

357 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="isValidAvatar(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, isValidAvatar } 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,
isValidAvatar,
/** 拉取图片真实尺寸,用于按比例计算展示高度 */
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>