Files
sbnews/uniapp/src/packages_community/pages/user_profile.vue
T
2026-06-11 16:22:12 +08:00

673 lines
19 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="user-profile">
<!-- 顶部导航 -->
<view class="profile-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="profile-nav">
<view class="profile-nav__back" @tap="goBack">
<u-icon name="arrow-left" color="#fff" size="40" />
</view>
<view class="profile-nav__actions">
<view class="profile-nav__icon" @tap="handleShare">
<u-icon name="share" color="#fff" size="36" />
</view>
</view>
</view>
<!-- 用户信息 -->
<view v-if="userInfo" class="profile-info">
<view class="profile-info__row">
<image class="profile-info__avatar" :src="userInfo.avatar || defaultAvatar" mode="aspectFill" />
<view class="profile-info__detail">
<text class="profile-info__name">{{ userInfo.nickname || '匿名用户' }}</text>
<text class="profile-info__id">ID:{{ userInfo.sn || userInfo.id }}</text>
</view>
<view v-if="!userInfo.is_self" class="profile-info__actions">
<view class="profile-info__follow-btn"
:class="{ 'profile-info__follow-btn--active': userInfo.is_followed }" @tap="handleFollow">
<text>{{ userInfo.is_followed || userInfo.is_friend ? '已好友' : '加好友' }}</text>
</view>
<view v-if="userInfo.is_followed || userInfo.is_friend" class="profile-info__chat-btn"
@tap="handlePrivateChat">
<text>私聊</text>
</view>
</view>
</view>
<text class="profile-info__desc">已加入世博头条{{ userInfo.join_days }}获得{{ userInfo.like_count || 0
}}个赞</text>
</view>
</view>
<!-- Tab栏 -->
<view class="profile-tabs">
<view v-for="tab in tabs" :key="tab.key" class="profile-tabs__item"
:class="{ 'profile-tabs__item--active': activeTab === tab.key }" @tap="switchTab(tab.key)">
<text class="profile-tabs__label">{{ tab.label }}</text>
<text class="profile-tabs__count">{{ tab.count }}</text>
</view>
</view>
<!-- 内容列表 -->
<scroll-view scroll-y class="profile-content" @scrolltolower="loadMore">
<view v-if="activeTab === 'posts'">
<view v-for="item in postList" :key="item.id" class="article-card" @tap="goPostDetail(item.id)">
<view class="article-card__body">
<view class="article-card__text">
<text class="article-card__title">{{ item.content_short || item.content }}</text>
<view class="article-card__meta">
<text>{{ item.user?.nickname || '匿名' }}</text>
<text>{{ (item.comment_count || 0) }}评论</text>
<text>{{ formatTime(item.create_time) }}</text>
</view>
</view>
<view v-if="getTrumpSourceUrl(item)" class="article-card__thumb article-card__thumb--external"
@tap.stop="openSourceUrl(getTrumpSourceUrl(item))">
<text class="article-card__thumb-badge">T</text>
<text class="article-card__thumb-text">查看原帖</text>
</view>
<image v-else-if="item.images && item.images.length > 0" class="article-card__thumb"
:src="item.images[0]" mode="aspectFill" />
</view>
<view class="article-card__divider" />
</view>
</view>
<view v-if="activeTab === 'comments'" class="profile-empty">
<text>评论列表开发中...</text>
</view>
<view v-if="activeTab === 'following'" class="user-list">
<view v-for="item in followList" :key="item.id" class="user-list__item" @tap="goUserProfile(item.id)">
<image class="user-list__avatar" :src="item.avatar || defaultAvatar" mode="aspectFill" />
<text class="user-list__name">{{ item.nickname || '匿名' }}</text>
</view>
</view>
<view v-if="activeTab === 'fans'" class="user-list">
<view v-for="item in fansList" :key="item.id" class="user-list__item" @tap="goUserProfile(item.id)">
<image class="user-list__avatar" :src="item.avatar || defaultAvatar" mode="aspectFill" />
<view class="user-list__info">
<text class="user-list__name">{{ item.nickname || '匿名' }}</text>
<text v-if="item.is_mutual" class="user-list__mutual">互相关注</text>
</view>
</view>
</view>
<view v-if="loading" class="profile-loading">
<u-loading mode="circle" />
</view>
<view v-if="!loading && finished && currentList.length > 0" class="profile-finished">
<text>没有更多了</text>
</view>
<view v-if="!loading && currentList.length === 0" class="profile-empty">
<text>暂无内容</text>
</view>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getUserProfile, getCommunityPosts, toggleFollow } from '@/api/community'
import { getToken } from '@/utils/auth'
import { useAppStore } from '@/stores/app'
import request from '@/utils/request'
import { openChat } from '@/api/chat'
const appStore = useAppStore()
const defaultAvatar = computed(() => {
const logo = appStore.config?.website?.shop_logo
if (!logo) return '/static/images/avatar.png'
if (logo.startsWith('http')) return logo
return (appStore.config?.domain || '') + logo
})
const statusBarHeight = ref(0)
const userId = ref(0)
const userInfo = ref<any>(null)
const activeTab = ref('posts')
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const postList = ref<any[]>([])
const followList = ref<any[]>([])
const fansList = ref<any[]>([])
const tabs = computed(() => [
{ key: 'posts', label: '发布', count: userInfo.value?.post_count || 0 },
{ key: 'comments', label: '评论', count: userInfo.value?.comment_count || 0 },
{ key: 'following', label: '关注', count: userInfo.value?.follow_count || 0 },
{ key: 'fans', label: '粉丝', count: userInfo.value?.fans_count || 0 }
])
const currentList = computed(() => {
if (activeTab.value === 'posts') return postList.value
if (activeTab.value === 'following') return followList.value
if (activeTab.value === 'fans') return fansList.value
return []
})
onMounted(() => {
const sysInfo = uni.getSystemInfoSync()
// #ifdef APP-PLUS || MP
statusBarHeight.value = sysInfo.statusBarHeight || 44
// #endif
// #ifdef H5
statusBarHeight.value = 0
// #endif
})
onLoad((options) => {
const id = Number(options?.user_id || 0)
if (id > 0) {
userId.value = id
fetchProfile(id)
fetchPosts(true)
} else {
uni.showToast({ title: '参数错误', icon: 'none' })
setTimeout(() => uni.navigateBack(), 1500)
}
})
const fetchProfile = async (id: number) => {
try {
const res = await getUserProfile({ user_id: id })
userInfo.value = res
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
}
const shouldHidePost = (post: any) => {
const tags = Array.isArray(post?.tags) ? post.tags : []
const images = Array.isArray(post?.images) ? post.images : []
const content = typeof post?.content === 'string' ? post.content.trim() : ''
const contentShort = typeof post?.content_short === 'string' ? post.content_short.trim() : ''
return tags.includes('特朗普') && images.length > 0 && !content && !contentShort
}
const fetchPosts = async (reset = false) => {
if (reset) {
page.value = 1
finished.value = false
postList.value = []
}
if (finished.value) return
loading.value = true
try {
const res = await getCommunityPosts({
user_id: userId.value,
page_no: page.value,
page_size: 15
})
const rawList = res?.lists || []
const list = rawList.filter((item: any) => !shouldHidePost(item))
if (reset) {
postList.value = list
} else {
postList.value.push(...list)
}
if (rawList.length < 15) {
finished.value = true
} else {
page.value++
}
} catch (e) { /* */ } finally {
loading.value = false
}
}
const fetchFollowList = async () => {
loading.value = true
try {
const data = await request.get({
url: '/community/followList',
data: { user_id: userId.value, page_no: 1, page_size: 100 }
})
followList.value = data?.lists || []
} catch (e) { /* */ } finally {
loading.value = false
}
}
const fetchFansList = async () => {
loading.value = true
try {
const data = await request.get({
url: '/community/fansList',
data: { user_id: userId.value, page_no: 1, page_size: 100 }
})
fansList.value = data?.lists || []
} catch (e) { /* */ } finally {
loading.value = false
}
}
const switchTab = (key: string) => {
if (activeTab.value === key) return
activeTab.value = key
if (key === 'posts' && postList.value.length === 0) {
fetchPosts(true)
} else if (key === 'following' && followList.value.length === 0) {
fetchFollowList()
} else if (key === 'fans' && fansList.value.length === 0) {
fetchFansList()
}
}
const loadMore = () => {
if (activeTab.value === 'posts') {
fetchPosts()
}
}
const checkLogin = () => {
if (!getToken()) {
uni.showModal({
title: '提示',
content: '该操作需要登录,是否前往登录?',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/login' })
}
}
})
return false
}
return true
}
const handleFollow = async () => {
if (!checkLogin()) return
if (!userInfo.value) return
try {
const res = await toggleFollow({ follow_user_id: userId.value })
const followed = !!res?.is_followed
userInfo.value.is_followed = followed
userInfo.value.is_friend = followed
const relation = userInfo.value.relationship || {}
relation.is_followed_by_me = followed
relation.is_mutual = followed && !!relation.is_following_me
relation.relationship_text = relation.is_mutual ? '互相关注' : '对方还未添加你为好友'
userInfo.value.relationship = relation
if (followed) {
userInfo.value.fans_count = (userInfo.value.fans_count || 0) + 1
uni.showToast({ title: '已加好友', icon: 'success' })
} else {
userInfo.value.fans_count = Math.max(0, (userInfo.value.fans_count || 0) - 1)
uni.showToast({ title: '已取消好友', icon: 'none' })
}
} catch (e) { /* */ }
}
const handlePrivateChat = async () => {
if (!checkLogin()) return
if (!userId.value) return
try {
const chatSession = await openChat({ target_user_id: userId.value })
if (chatSession?.id) {
uni.navigateTo({ url: `/pages/private_chat/room?session_id=${chatSession.id}` })
}
} catch (e) {
// 请求层会提示后端返回信息
}
}
const handleShare = () => {
uni.showToast({ title: '分享功能开发中', icon: 'none' })
}
const goBack = () => {
uni.navigateBack()
}
const goPostDetail = (id: number) => {
uni.navigateTo({ url: `/packages_community/pages/post_detail?id=${id}` })
}
const goUserProfile = (id: number) => {
if (id === userId.value) return
uni.navigateTo({ url: `/packages_community/pages/user_profile?user_id=${id}` })
}
const getTrumpSourceUrl = (item: any) => {
if (!Array.isArray(item?.tags) || !item.tags.includes('特朗普')) return ''
if (item.source_url) return item.source_url
const originId = item.origin_id || ''
if (!originId.startsWith('truthsocial:')) return ''
const postId = originId.split(':')[1]
if (!postId) return ''
return `https://truthsocial.com/@realDonaldTrump/${postId}`
}
const openSourceUrl = (url: string) => {
if (!url) return
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(url)}`
})
}
const formatTime = (time: string) => {
if (!time) return ''
const d = new Date(time.replace(/-/g, '/'))
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${m}-${day} ${h}:${min}`
}
</script>
<style lang="scss" scoped>
.user-profile {
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
}
.profile-header {
background: linear-gradient(135deg, #0053d8 0%, #185dff 100%);
padding-bottom: 36rpx;
flex-shrink: 0;
}
.profile-nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
&__back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
&__actions {
display: flex;
gap: 20rpx;
}
&__icon {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.profile-info {
padding: 0 32rpx;
&__row {
display: flex;
align-items: center;
}
&__avatar {
width: 100rpx;
height: 100rpx;
border-radius: 28rpx;
flex-shrink: 0;
}
&__detail {
flex: 1;
margin-left: 20rpx;
}
&__name {
font-size: 36rpx;
font-weight: bold;
color: #fff;
display: block;
}
&__id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
margin-top: 4rpx;
display: block;
}
&__actions {
flex-shrink: 0;
margin-left: 20rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
&__follow-btn,
&__chat-btn {
padding: 8rpx 28rpx;
border-radius: 8rpx;
border: 1rpx solid rgba(255, 255, 255, 0.5);
white-space: nowrap;
text {
font-size: 24rpx;
color: #fff;
}
}
&__follow-btn {
background: rgba(255, 255, 255, 0.2);
&--active {
background: rgba(229, 237, 255, 0.3);
}
}
&__chat-btn {
background: #fff;
border-color: #fff;
text {
color: #185dff;
font-weight: 600;
}
}
&__desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-top: 24rpx;
padding-bottom: 8rpx;
display: block;
}
}
.profile-tabs {
display: flex;
background: #fff;
border-bottom: 1rpx solid #eee;
flex-shrink: 0;
&__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 0 20rpx;
position: relative;
&--active {
.profile-tabs__label {
color: #0053d8;
font-weight: bold;
}
.profile-tabs__count {
color: #0053d8;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #0053d8;
border-radius: 2rpx;
}
}
}
&__label {
font-size: 28rpx;
color: #666;
}
&__count {
font-size: 22rpx;
color: #999;
margin-top: 2rpx;
}
}
.profile-content {
flex: 1;
height: 0;
min-height: 0;
background: #fff;
box-sizing: border-box;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.article-card {
padding: 24rpx 32rpx 0;
&__body {
display: flex;
gap: 20rpx;
}
&__text {
flex: 1;
min-width: 0;
}
&__title {
font-size: 28rpx;
color: #000;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
&__meta {
display: flex;
gap: 20rpx;
margin-top: 16rpx;
text {
font-size: 24rpx;
color: #808080;
}
}
&__thumb {
width: 226rpx;
height: 160rpx;
border-radius: 24rpx;
flex-shrink: 0;
&--external {
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
}
}
&__thumb-badge {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.12);
color: #fff;
font-size: 30rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
&__thumb-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
&__divider {
height: 1rpx;
background: #e6e6e6;
margin-top: 24rpx;
}
}
.user-list {
padding: 0 32rpx;
&__item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
&__avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
}
&__info {
flex: 1;
margin-left: 20rpx;
}
&__name {
font-size: 28rpx;
color: #333;
margin-left: 20rpx;
}
&__mutual {
font-size: 22rpx;
color: #0053d8;
margin-left: 12rpx;
}
}
.profile-loading,
.profile-finished,
.profile-empty {
display: flex;
justify-content: center;
padding: 40rpx 0;
text {
font-size: 24rpx;
color: #999;
}
}
</style>