673 lines
19 KiB
Vue
673 lines
19 KiB
Vue
<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>
|