前端-胡立永 2 weeks ago
parent
commit
9d742ec9d6
13 changed files with 2104 additions and 17 deletions
  1. +1
    -1
      components/base/navbar.vue
  2. +2
    -2
      components/base/tabbar.vue
  3. +100
    -0
      components/list/square/waterfallContainer.vue
  4. +297
    -0
      components/list/square/waterfallItem.vue
  5. +1
    -1
      components/userShop/userShopCommission.vue
  6. +1
    -1
      config.js
  7. +27
    -5
      pages.json
  8. +12
    -1
      pages/index/center.vue
  9. +0
    -1
      pages/index/product.vue
  10. +371
    -0
      pages/index/square.vue
  11. +19
    -5
      pages_order/marketing/turntable.vue
  12. +476
    -0
      pages_order/profile/fansList.vue
  13. +797
    -0
      pages_order/profile/userProfile.vue

+ 1
- 1
components/base/navbar.vue View File

@ -2,7 +2,7 @@
<!-- <view class="navbar"
:style="{backgroundColor : bgColor}"> -->
<view class="title"
:style="{backgroundColor : bgColor,color}">
:style="{background : bgColor, color}">
<view class="left">
<uv-icon name="home"


+ 2
- 2
components/base/tabbar.vue View File

@ -46,8 +46,8 @@
{
"selectedIconPath": "/static/image/tabbar/order-a.png",
"iconPath": "/static/image/tabbar/order.png",
"pagePath": "/pages/index/product",
"title": "商城",
"pagePath": "/pages/index/square",
"title": "广场",
icon : 'integral',
},
{


+ 100
- 0
components/list/square/waterfallContainer.vue View File

@ -0,0 +1,100 @@
<template>
<view class="waterfall-container">
<view class="waterfall-columns">
<!-- 左列 -->
<view class="waterfall-column">
<waterfallItem
v-for="(item, index) in leftColumnData"
:key="'left_' + index"
:item="item"
@click="handleItemClick"
@like="handleItemLike"
/>
</view>
<!-- 右列 -->
<view class="waterfall-column">
<waterfallItem
v-for="(item, index) in rightColumnData"
:key="'right_' + index"
:item="item"
@click="handleItemClick"
@like="handleItemLike"
/>
</view>
</view>
</view>
</template>
<script>
import waterfallItem from './waterfallItem.vue'
export default {
components: {
waterfallItem
},
props: {
list: {
type: Array,
default: () => []
}
},
data() {
return {
leftColumnData: [],
rightColumnData: []
}
},
watch: {
list: {
handler(newList) {
this.distributeItems(newList)
},
immediate: true
}
},
methods: {
//
distributeItems(items) {
this.leftColumnData = []
this.rightColumnData = []
items.forEach((item, index) => {
//
if (index % 2 === 0) {
this.leftColumnData.push(item)
} else {
this.rightColumnData.push(item)
}
})
},
//
handleItemClick(item) {
this.$emit('item-click', item)
},
//
handleItemLike(item) {
this.$emit('item-like', item)
}
}
}
</script>
<style scoped lang="scss">
.waterfall-container {
padding: 20rpx;
.waterfall-columns {
display: flex;
gap: 20rpx;
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
</style>

+ 297
- 0
components/list/square/waterfallItem.vue View File

@ -0,0 +1,297 @@
<template>
<view class="waterfall-item" @click="handleClick">
<!-- 主图片 -->
<view class="main-image" v-if="mainImage">
<image :src="mainImage" mode="aspectFill" @click.stop="previewImage([mainImage])"></image>
<!-- 分类标签 -->
<view class="category-tag" v-if="item.classId_dictText">
#{{ item.classId_dictText }}
</view>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 标题/内容 -->
<view class="title" v-if="item.title" v-html="formatContent(item.title)"></view>
<!-- 地址信息 -->
<view class="address" v-if="item.address">
<uv-icon name="map-pin" size="24rpx" color="#999"></uv-icon>
<text>{{ item.address }}</text>
</view>
<!-- 用户信息 -->
<view class="user-info">
<view class="user-avatar">
<image :src="item.userImage" mode="aspectFill" @click.stop="previewImage([item.userImage])"></image>
</view>
<view class="user-details">
<view class="username">{{ item.userName }}</view>
<view class="user-tags">
<text class="tag" v-if="item.sex" :style="{'background-color': sexColors[item.sex] || '#999'}">{{ item.sex }}</text>
<text class="tag" v-if="item.yearDate">{{ item.yearDate }}</text>
<text class="auth-tag" v-if="item.isContent">{{ item.isContent }}</text>
</view>
</view>
</view>
<!-- 互动数据 -->
<view class="interaction">
<view class="interaction-item">
<uv-icon name="eye" size="24rpx" color="#999"></uv-icon>
<text>{{ item.isBrowse || 0 }}</text>
</view>
<view class="interaction-item">
<uv-icon name="chat" size="24rpx" color="#999"></uv-icon>
<text>{{ item.isComment || 0 }}</text>
</view>
<view class="interaction-item" @click.stop="handleLike">
<uv-icon name="thumb-up" size="24rpx" :color="isLiked ? '#ff4757' : '#999'"></uv-icon>
<text :style="{color: isLiked ? '#ff4757' : '#999'}">{{ item.isUp || 0 }}</text>
</view>
<!-- 发布时间 -->
<view class="publish-time">
{{ formatTime(item.createTime) }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => ({})
}
},
data() {
return {
isLiked: false,
sexColors: {
'男': '#4A90E2',
'女': '#FF69B4',
'其他': '#999'
}
}
},
computed: {
// -
mainImage() {
if (this.item.wxImage) {
return this.item.wxImage
}
if (this.item.image) {
const images = this.item.image.split(',').filter(img => img.trim())
return images.length > 0 ? images[0] : null
}
return null
}
},
methods: {
//
handleClick() {
this.$emit('click', this.item)
},
//
handleLike() {
this.isLiked = !this.isLiked
this.$emit('like', this.item)
},
//
previewImage(urls, current = 0) {
if (!urls || urls.length === 0) return
uni.previewImage({
urls: urls,
current: current
})
},
//
formatContent(content) {
if (!content) return ''
// 使utils
if (this.$utils && this.$utils.stringFormatHtml) {
return this.$utils.stringFormatHtml(content)
}
// HTML
return content.replace(/\n/g, '<br/>')
},
//
formatTime(timeStr) {
if (!timeStr) return ''
// ""
if (timeStr.includes('发布')) {
return timeStr
}
//
const now = new Date()
const time = new Date(timeStr)
const diff = now - time
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return '今天'
} else if (days === 1) {
return '昨天'
} else if (days < 7) {
return `${days}天前`
} else {
// -
const month = time.getMonth() + 1
const day = time.getDate()
return `${month}-${day}`
}
}
}
}
</script>
<style scoped lang="scss">
.waterfall-item {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
margin-bottom: 20rpx;
.main-image {
position: relative;
width: 100%;
image {
width: 100%;
height: 400rpx;
object-fit: cover;
}
.category-tag {
position: absolute;
top: 16rpx;
right: 16rpx;
background: rgba(255, 215, 0, 0.9);
color: #333;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
}
}
.content {
padding: 24rpx;
.title {
font-size: 28rpx;
line-height: 1.4;
color: #333;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.address {
display: flex;
align-items: center;
margin-bottom: 20rpx;
text {
font-size: 24rpx;
color: #666;
margin-left: 8rpx;
}
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.user-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
overflow: hidden;
margin-right: 16rpx;
image {
width: 100%;
height: 100%;
}
}
.user-details {
flex: 1;
.username {
font-size: 26rpx;
color: #333;
font-weight: 500;
margin-bottom: 6rpx;
}
.user-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
.tag {
font-size: 20rpx;
color: white;
background-color: #999;
padding: 4rpx 12rpx;
border-radius: 12rpx;
margin-right: 8rpx;
margin-bottom: 4rpx;
}
.auth-tag {
font-size: 20rpx;
color: white;
background-color: #ffd036;
padding: 4rpx 12rpx;
border-radius: 12rpx;
margin-right: 8rpx;
margin-bottom: 4rpx;
}
}
}
}
.interaction {
display: flex;
align-items: center;
justify-content: space-between;
.interaction-item {
display: flex;
align-items: center;
text {
font-size: 24rpx;
color: #999;
margin-left: 6rpx;
}
}
.publish-time {
font-size: 24rpx;
color: #999;
margin-left: auto;
}
}
}
}
</style>

+ 1
- 1
components/userShop/userShopCommission.vue View File

@ -9,7 +9,7 @@
{{ userInfo.price }}
</view>
</view>
s
<!-- <view class="font-menu"
v-if="purse">
<view @click="toRunningWater(index)"


+ 1
- 1
config.js View File

@ -7,7 +7,7 @@ import uvUI from '@/uni_modules/uv-ui-tools'
Vue.use(uvUI);
// 当前环境
const type = 'dev'
const type = 'prod'
// 环境配置


+ 27
- 5
pages.json View File

@ -23,6 +23,12 @@
{
"enablePullDownRefresh" : true
}
},
{
"path": "pages/index/square",
"style": {
"enablePullDownRefresh" : true
}
}
],
"preloadRule": {
@ -196,6 +202,22 @@
"navigationBarTitleText": "客服中心",
"enablePullDownRefresh": false
}
},
{
"path": "profile/userProfile",
"style": {
"navigationBarTitleText": "个人主页",
"enablePullDownRefresh": true,
"navigationStyle": "custom"
}
},
{
"path": "profile/fansList",
"style": {
"navigationBarTitleText": "粉丝关注",
"enablePullDownRefresh": true,
"navigationStyle": "custom"
}
}
]
}],
@ -207,13 +229,13 @@
"navigationStyle": "custom"
},
"uniIdRouter": {},
"condition" : { //
"current": 0, //(list )
"condition" : {
"current": 0,
"list": [
{
"name": "", //
"path": "", //
"query": "" //onLoad
"name": "",
"path": "",
"query": ""
}
]
}

+ 12
- 1
pages/index/center.vue View File

@ -17,7 +17,7 @@
<!-- 已登录状态 -->
<view v-else>
<view class="o">
<view class="headImage">
<view class="headImage" @click="goToMyProfile">
<image
:src="userInfo.headImage"
mode="aspectFill"></image>
@ -303,6 +303,17 @@
}
})
},
//
goToMyProfile() {
if (!this.isLogin) {
this.showLoginTip()
return
}
uni.navigateTo({
url: `/pages_order/profile/userProfile?userId=${this.userInfo.id}`
})
},
}
}


+ 0
- 1
pages/index/product.vue View File

@ -91,7 +91,6 @@
</view> -->
<tabber select="1" />
</view>
</template>


+ 371
- 0
pages/index/square.vue View File

@ -0,0 +1,371 @@
<template>
<view class="square-page">
<navbar title="广场"/>
<!-- 一级分类关注/发现 -->
<view class="primary-tabs">
<view
class="primary-tab-item"
:class="{active: currentPrimaryTab === 0}"
@click="switchPrimaryTab(0)"
>
关注
</view>
<view
class="primary-tab-item"
:class="{active: currentPrimaryTab === 1}"
@click="switchPrimaryTab(1)"
>
发现
</view>
</view>
<!-- 二级分类城市列表 -->
<!-- <view class="secondary-tabs">
<scroll-view scroll-x="true" class="city-scroll">
<view class="city-tabs">
<view
class="city-tab-item"
:class="{active: currentCityIndex === -1}"
@click="switchCity(-1, null)"
>
全部
</view>
<view
v-for="(city, index) in cityList"
:key="city.id || index"
class="city-tab-item"
:class="{active: currentCityIndex === index}"
@click="switchCity(index, city)"
>
{{ city.name || city.cityName }}
</view>
</view>
</scroll-view>
</view> -->
<!-- 使用uv-tabs组件的城市分类 -->
<view class="city-tabs-container">
<uv-tabs
:list="cityTabsList"
:current="currentCityTabIndex"
:activeStyle="{color: '#5baaff', fontWeight: 600}"
lineColor="#5baaff"
lineHeight="6rpx"
lineWidth="40rpx"
keyName="name"
@click="onCityTabClick"
/>
</view>
<!-- 瀑布流列表 -->
<view class="content-container">
<waterfallContainer
:list="List"
@item-click="onItemClick"
@item-like="onItemLike"
/>
<!-- 加载更多提示 -->
<view v-if="loadmore && List.length > 0" class="load-more">
<uv-loading-icon size="28"></uv-loading-icon>
<text class="load-more-text">加载更多...</text>
</view>
<!-- 没有更多数据提示 -->
<view v-if="!loadmore && List.length > 0" class="no-more">
<text> 没有更多了 </text>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && List.length === 0" class="empty-state">
<uv-empty
text="暂无动态"
textColor="#999"
icon="list"
iconColor="#ddd"
iconSize="120"
></uv-empty>
</view>
<!-- 加载状态 -->
<view v-if="loading && List.length === 0" class="loading-state">
<uv-loading-icon size="40"></uv-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<tabber select="1" />
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
import tabber from '@/components/base/tabbar.vue'
import waterfallContainer from '@/components/list/square/waterfallContainer.vue'
import mixinsList from '@/mixins/loadList.js'
import { mapState } from 'vuex'
export default {
mixins: [mixinsList],
components: {
navbar,
tabber,
waterfallContainer
},
data() {
return {
mixinsListApi: 'getPostPage', // 使API
currentPrimaryTab: 1, //
currentCityIndex: -1, //
currentCity: null,
currentCityTabIndex: 0 // uv-tabs
}
},
onLoad() {
//
this.$store.commit('getCityList')
//
this.initQueryParams()
},
onShow() {
this.refreshList()
},
onPullDownRefresh() {
this.refreshList()
},
onReachBottom() {
this.loadMore()
},
computed: {
...mapState(['cityList']),
// ""
cityTabsList() {
const allTab = { id: null, name: '全部' }
const cityTabs = this.cityList.map(city => ({
id: city.id || city.cityId,
name: city.name || city.cityName || city.title
}))
return [allTab, ...cityTabs]
}
},
methods: {
//
initQueryParams() {
//
this.queryParams = {
...this.queryParams,
type: this.currentPrimaryTab // 0: , 1:
}
//
if (this.currentCity) {
this.queryParams.cityId = this.currentCity.id || this.currentCity.cityId
}
},
//
switchPrimaryTab(index) {
if (this.currentPrimaryTab === index) return
this.currentPrimaryTab = index
this.queryParams.type = index
this.refreshList()
},
// uv-tabsonCityTabClick
// switchCity(index, city) {
// if (this.currentCityIndex === index) return
//
// this.currentCityIndex = index
// this.currentCity = city
//
// //
// if (city) {
// this.queryParams.cityId = city.id || city.cityId
// } else {
// delete this.queryParams.cityId
// }
//
// this.refreshList()
// },
//
onRefresh() {
this.refreshList()
},
//
onItemClick(item) {
this.$utils.navigateTo('/pages_order/post/postDetail?id=' + item.id)
},
//
onItemLike(item) {
console.log('点赞动态:', item.id)
// API
},
// uv-tabs
onCityTabClick(item) {
this.currentCityTabIndex = item.index
//
if (item.index === 0) {
this.currentCity = null
this.currentCityIndex = -1
delete this.queryParams.cityId
} else {
//
const cityData = this.cityList[item.index - 1] // 1""
this.currentCity = cityData
this.currentCityIndex = item.index - 1
this.queryParams.cityId = cityData.id || cityData.cityId
}
this.refreshList()
}
}
}
</script>
<style scoped lang="scss">
.square-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.primary-tabs {
background-color: #fff;
display: flex;
padding: 0 30rpx;
border-bottom: 1rpx solid #eee;
.primary-tab-item {
flex: 1;
text-align: center;
padding: 30rpx 0;
font-size: 32rpx;
color: #666;
position: relative;
&.active {
color: #5baaff;
font-weight: bold;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #5baaff;
border-radius: 3rpx;
}
}
}
}
//
// .secondary-tabs {
// background-color: #fff;
// border-bottom: 1rpx solid #eee;
//
// .city-scroll {
// padding: 20rpx 0;
//
// .city-tabs {
// display: flex;
// white-space: nowrap;
// padding: 0 30rpx;
//
// .city-tab-item {
// flex-shrink: 0;
// padding: 16rpx 32rpx;
// margin-right: 20rpx;
// background-color: #f8f9fa;
// border-radius: 30rpx;
// font-size: 28rpx;
// color: #666;
// border: 1rpx solid transparent;
//
// &.active {
// background-color: #5baaff;
// color: #fff;
// }
//
// &:last-child {
// margin-right: 30rpx;
// }
// }
// }
// }
// }
// uv-tabs
.city-tabs-container {
background-color: #fff;
border-bottom: 1rpx solid #eee;
padding: 0 20rpx;
}
.content-container {
padding: 0;
}
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.loading-state {
padding: 100rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
.load-more {
padding: 30rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.load-more-text {
margin-top: 15rpx;
font-size: 24rpx;
color: #999;
}
}
.no-more {
padding: 30rpx 0;
text-align: center;
text {
font-size: 24rpx;
color: #ccc;
}
}
// uv-tabs
// /deep/ .city-scroll::-webkit-scrollbar {
// display: none;
// }
</style>

+ 19
- 5
pages_order/marketing/turntable.vue View File

@ -126,6 +126,8 @@
],
isSpinning: false, //
rotateAngle: 0, //
totalSpinRounds: 0, //
currentAngle: 0, // 0-360
showResult: false, //
currentPrize: null, //
sectorAngle: 0, //
@ -242,12 +244,24 @@
const prizeIndex = this.prizes.findIndex(prize => prize.id == result.gift.id)
if (prizeIndex !== -1) {
//
const targetAngle = 360 - (prizeIndex * this.sectorAngle + this.sectorAngle / 2)
//
//
const prizeAngle = prizeIndex * this.sectorAngle + this.sectorAngle / 2
// 0
let targetAngle = -prizeAngle
// 0-360
if (targetAngle < 0) {
targetAngle += 360
}
//
const spinRounds = 5 // 5
const finalAngle = this.rotateAngle + spinRounds * 360 + targetAngle
this.rotateAngle = finalAngle
this.totalSpinRounds += spinRounds
this.currentAngle = targetAngle
// = * 360 +
this.rotateAngle = this.totalSpinRounds * 360 + this.currentAngle
//
setTimeout(() => {


+ 476
- 0
pages_order/profile/fansList.vue View File

@ -0,0 +1,476 @@
<template>
<view class="fans-list-page">
<navbar title="粉丝列表" leftClick @leftClick="$utils.navigateBack" />
<!-- 标签页切换 -->
<view class="tabs-container">
<uv-tabs
:list="tabsList"
:current="currentTab"
:activeStyle="{color: '#333', fontWeight: 600}"
lineColor="#5baaff"
lineHeight="6rpx"
lineWidth="40rpx"
keyName="name"
@click="onTabClick"
/>
</view>
<!-- 用户列表 -->
<view class="user-list">
<view
class="user-item"
v-for="(user, index) in userList"
:key="index"
@click="goToUserProfile(user)"
>
<view class="user-avatar">
<image :src="user.headImage" mode="aspectFill"></image>
</view>
<view class="user-info">
<view class="user-name">{{ user.nickName }}</view>
<view class="user-desc">
<view class="user-tags">
<text class="tag" v-if="user.sex">{{ user.sex }}</text>
<text class="tag" v-if="user.address">{{ user.address }}</text>
<text class="auth-tag" v-if="user.idCardOpen">{{ getAuthText(user.idCardOpen) }}</text>
</view>
</view>
</view>
<view class="user-action" v-if="!isCurrentUserPage">
<button
class="follow-btn"
:class="{followed: user.isFollowed}"
@click.stop="toggleFollow(user, index)"
>
{{ user.isFollowed ? '已关注' : '关注' }}
</button>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && userList.length === 0" class="empty-state">
<uv-empty
:text="emptyText"
icon="account"
iconSize="120"
></uv-empty>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<uv-loading-icon size="40"></uv-loading-icon>
<text>加载中...</text>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
userId: '', // ID
type: 'fans', // fans: , following:
currentTab: 0,
tabsList: [
{ name: '粉丝', type: 'fans' },
{ name: '关注', type: 'following' }
],
userList: [], //
loading: false,
//
mockFansData: [
{
id: '1',
nickName: '小美同学',
headImage: 'https://picsum.photos/100/100?random=1',
sex: '女',
address: '北京',
idCardOpen: 1,
isFollowed: false
},
{
id: '2',
nickName: '程序员小王',
headImage: 'https://picsum.photos/100/100?random=2',
sex: '男',
address: '上海',
idCardOpen: 2,
isFollowed: true
},
{
id: '3',
nickName: '设计师小李',
headImage: 'https://picsum.photos/100/100?random=3',
sex: '女',
address: '深圳',
idCardOpen: 0,
isFollowed: false
},
{
id: '4',
nickName: '产品经理老张',
headImage: 'https://picsum.photos/100/100?random=4',
sex: '男',
address: '广州',
idCardOpen: 1,
isFollowed: true
},
{
id: '5',
nickName: '运营小姐姐',
headImage: 'https://picsum.photos/100/100?random=5',
sex: '女',
address: '杭州',
idCardOpen: 2,
isFollowed: false
},
{
id: '6',
nickName: '前端工程师',
headImage: 'https://picsum.photos/100/100?random=6',
sex: '男',
address: '成都',
idCardOpen: 1,
isFollowed: true
},
{
id: '7',
nickName: 'UI设计师',
headImage: 'https://picsum.photos/100/100?random=7',
sex: '女',
address: '武汉',
idCardOpen: 0,
isFollowed: false
},
{
id: '8',
nickName: '后端大神',
headImage: 'https://picsum.photos/100/100?random=8',
sex: '男',
address: '西安',
idCardOpen: 2,
isFollowed: true
}
],
mockFollowingData: [
{
id: '9',
nickName: '技术大牛',
headImage: 'https://picsum.photos/100/100?random=9',
sex: '男',
address: '北京',
idCardOpen: 2,
isFollowed: true
},
{
id: '10',
nickName: '设计总监',
headImage: 'https://picsum.photos/100/100?random=10',
sex: '女',
address: '上海',
idCardOpen: 2,
isFollowed: true
},
{
id: '11',
nickName: '产品总监',
headImage: 'https://picsum.photos/100/100?random=11',
sex: '男',
address: '深圳',
idCardOpen: 1,
isFollowed: true
},
{
id: '12',
nickName: '运营总监',
headImage: 'https://picsum.photos/100/100?random=12',
sex: '女',
address: '广州',
idCardOpen: 2,
isFollowed: true
},
{
id: '13',
nickName: '架构师',
headImage: 'https://picsum.photos/100/100?random=13',
sex: '男',
address: '杭州',
idCardOpen: 1,
isFollowed: true
},
{
id: '14',
nickName: '资深设计师',
headImage: 'https://picsum.photos/100/100?random=14',
sex: '女',
address: '成都',
idCardOpen: 2,
isFollowed: true
}
]
}
},
computed: {
...mapState(['userInfo']),
//
pageTitle() {
return this.type === 'fans' ? '粉丝列表' : '关注列表'
},
//
emptyText() {
return this.type === 'fans' ? '暂无粉丝' : '暂无关注'
},
//
isCurrentUserPage() {
return this.userId === this.userInfo.id
}
},
onLoad(options) {
this.userId = options.userId || this.userInfo.id
this.type = options.type || 'fans'
// tab
this.currentTab = this.type === 'fans' ? 0 : 1
this.loadUserList()
},
onPullDownRefresh() {
this.loadUserList()
},
methods: {
//
goBack() {
uni.navigateBack()
},
//
onTabClick(item) {
this.currentTab = item.index
this.type = item.type
this.loadUserList()
},
//
getAuthText(status) {
const authTexts = ['审核中', '个人认证', '店铺认证']
return authTexts[status] || '未认证'
},
//
goToUserProfile(user) {
uni.navigateTo({
url: `/pages_order/profile/userProfile?userId=${user.id}`
})
},
//
toggleFollow(user, index) {
if (!uni.getStorageSync('token')) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
const isFollowed = !user.isFollowed
// API
setTimeout(() => {
//
this.userList[index].isFollowed = isFollowed
//
if (this.type === 'fans') {
const mockIndex = this.mockFansData.findIndex(item => item.id === user.id)
if (mockIndex !== -1) {
this.mockFansData[mockIndex].isFollowed = isFollowed
}
} else {
const mockIndex = this.mockFollowingData.findIndex(item => item.id === user.id)
if (mockIndex !== -1) {
this.mockFollowingData[mockIndex].isFollowed = isFollowed
}
}
uni.showToast({
title: isFollowed ? '关注成功' : '取消关注',
icon: 'success'
})
}, 500)
},
//
loadUserList() {
this.loading = true
//
setTimeout(() => {
this.loading = false
uni.stopPullDownRefresh()
// 使
if (this.type === 'fans') {
this.userList = [...this.mockFansData]
} else {
this.userList = [...this.mockFollowingData]
}
// API
console.log(`加载${this.type === 'fans' ? '粉丝' : '关注'}列表成功`)
}, 1000)
}
}
}
</script>
<style scoped lang="scss">
.fans-list-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 88rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
padding-top: var(--status-bar-height, 44rpx);
.nav-title {
color: #333;
font-size: 32rpx;
font-weight: 600;
}
.nav-left, .nav-right {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.tabs-container {
margin-top: 88rpx;
padding-top: var(--status-bar-height, 44rpx);
background: #fff;
border-bottom: 1rpx solid #eee;
padding-left: 20rpx;
padding-right: 20rpx;
}
.user-list {
padding: 20rpx;
.user-item {
display: flex;
align-items: center;
background: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.user-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
overflow: hidden;
margin-right: 30rpx;
image {
width: 100%;
height: 100%;
}
}
.user-info {
flex: 1;
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.user-desc {
.user-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
.tag {
background: #f0f0f0;
color: #666;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
}
.auth-tag {
background: #52c41a;
color: #fff;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
}
}
}
}
.user-action {
.follow-btn {
background: #5baaff;
color: #fff;
border: none;
padding: 16rpx 32rpx;
border-radius: 30rpx;
font-size: 24rpx;
&.followed {
background: #f0f0f0;
color: #666;
}
}
}
}
}
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
</style>

+ 797
- 0
pages_order/profile/userProfile.vue View File

@ -0,0 +1,797 @@
<template>
<view class="profile-page">
<!-- 顶部导航 -->
<!-- <navbar
:title="userProfile.nickName || '用户主页'"
:leftClick="true"
:moreClick="showMoreOptions"
bgColor="transparent"
color="#fff"
@leftClick="$utils.navigateBack"
/> -->
<!-- 用户信息头部 -->
<view class="profile-header">
<!-- 背景装饰 -->
<view class="header-bg"></view>
<!-- 返回按钮 -->
<view class="back-btn" @click="$utils.navigateBack">
<uv-icon name="arrow-left" size="30rpx" color="#fff"></uv-icon>
</view>
<!-- 用户基本信息 -->
<view class="user-info">
<view class="user-avatar">
<image :src="userProfile.headImage" mode="aspectFill" @click="previewAvatar"></image>
<!-- VIP标识 -->
<view class="vip-badge" v-if="userProfile.isPay">
<text>{{ getVipLevel(userProfile.isPay) }}</text>
</view>
</view>
<view class="user-details">
<view class="username">{{ userProfile.nickName }}</view>
<view class="user-tags">
<!-- 性别年龄 -->
<view class="tag gender-tag" v-if="userProfile.sex">
<uv-icon :name="sexIcons[userProfile.sex]" size="24rpx" :color="sexColors[userProfile.sex]"></uv-icon>
<text>{{ userProfile.sex }}</text>
<text v-if="userProfile.yearDate">{{ getAge() }}</text>
</view>
<!-- 地址 -->
<view class="tag location-tag" v-if="userProfile.address">
<uv-icon name="map-pin" size="20rpx" color="#666"></uv-icon>
<text>{{ userProfile.address }}</text>
</view>
<!-- 认证状态 -->
<view class="tag auth-tag" v-if="userProfile.idCardOpen">
<uv-icon name="checkmark-circle-fill" size="20rpx" color="#52c41a"></uv-icon>
<text>{{ getAuthText(userProfile.idCardOpen) }}</text>
</view>
</view>
<!-- 学校信息 -->
<view class="school-info" v-if="userProfile.czSchool || userProfile.gzSchool">
<text v-if="userProfile.czSchool">🎓 {{ userProfile.czSchool }}</text>
<text v-if="userProfile.gzSchool">🏫 {{ userProfile.gzSchool }}</text>
</view>
</view>
</view>
<!-- 数据统计 -->
<view class="stats-row">
<view class="stat-item" @click="showFans">
<view class="stat-number">{{ userProfile.intentionNum || 0 }}</view>
<view class="stat-label">粉丝</view>
</view>
<view class="stat-item" @click="showFollowing">
<view class="stat-number">{{ userProfile.followNum || 0 }}</view>
<view class="stat-label">关注</view>
</view>
<view class="stat-item" @click="showLikes">
<view class="stat-number">{{ userProfile.likeNum || 0 }}</view>
<view class="stat-label">获赞</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons" v-if="!isCurrentUser">
<button class="follow-btn" :class="{followed: isFollowed}" @click="toggleFollow">
<uv-icon :name="isFollowed ? 'checkmark' : 'plus'" size="24rpx"></uv-icon>
<text>{{ isFollowed ? '已关注' : '关注' }}</text>
</button>
<button class="message-btn" @click="sendMessage">
<uv-icon name="chat" size="24rpx"></uv-icon>
<text>私信</text>
</button>
</view>
</view>
<!-- 内容标签页 -->
<view class="content-tabs">
<uv-tabs
:list="contentTabs"
:current="currentTabIndex"
:activeStyle="{color: '#333', fontWeight: 600}"
lineColor="#5baaff"
lineHeight="6rpx"
lineWidth="40rpx"
keyName="name"
@click="onTabClick"
/>
</view>
<!-- 内容区域 -->
<view class="content-container">
<!-- 帖子 - 瀑布流展示 -->
<view v-if="currentTabIndex === 0" class="posts-content">
<waterfallContainer
v-if="postsList.length > 0"
:list="postsList"
@item-click="onPostClick"
@item-like="onPostLike"
/>
</view>
<!-- 租房信息 -->
<view v-else-if="currentTabIndex === 1" class="renting-content">
<rentingItem
v-for="(item, index) in rentingList"
:key="index"
:item="item"
@click="onRentingClick(item)"
/>
</view>
<!-- 招聘信息 -->
<view v-else-if="currentTabIndex === 2" class="work-content">
<workItem
v-for="(item, index) in workList"
:key="index"
:item="item"
@click="onWorkClick(item)"
/>
</view>
<!-- 店铺信息 -->
<view v-else-if="currentTabIndex === 3" class="shop-content">
<gourmetItem
v-for="(item, index) in shopList"
:key="index"
:item="item"
@click="onShopClick(item)"
/>
</view>
<!-- 加载更多提示 -->
<view v-if="showLoadMore" class="load-more-state">
<uv-loading-icon size="32"></uv-loading-icon>
<text>加载更多...</text>
</view>
<!-- 没有更多数据提示 -->
<view v-if="showNoMore" class="no-more-state">
<text> 没有更多了 </text>
</view>
</view>
<!-- 空状态 -->
<view v-if="showEmptyState" class="empty-state">
<uv-empty
:text="getEmptyText()"
:icon="getEmptyIcon()"
iconSize="120"
></uv-empty>
</view>
<!-- 初始加载状态 -->
<view v-if="loading && currentList.length === 0" class="loading-state">
<uv-loading-icon size="40"></uv-loading-icon>
<text>加载中...</text>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
import waterfallContainer from '@/components/list/square/waterfallContainer.vue'
import rentingItem from '@/components/list/renting/rentingItem.vue'
import workItem from '@/components/list/work/workItem.vue'
import gourmetItem from '@/components/list/gourmet/gourmetItem.vue'
import { mapState } from 'vuex'
export default {
components: {
navbar,
waterfallContainer,
rentingItem,
workItem,
gourmetItem
},
data() {
return {
userId: '', // ID
userProfile: {}, //
currentTabIndex: 0, //
contentTabs: [
{ name: '帖子', icon: 'grid' },
{ name: '租房', icon: 'home' },
{ name: '招聘', icon: 'search' },
{ name: '店铺', icon: 'shop' }
],
postsList: [], //
rentingList: [], //
workList: [], //
shopList: [], //
loading: false, //
loadingMore: false, //
isFollowed: false, //
//
pageParams: {
postsList: { pageNum: 1, hasMore: true },
rentingList: { pageNum: 1, hasMore: true },
workList: { pageNum: 1, hasMore: true },
shopList: { pageNum: 1, hasMore: true }
},
sexIcons: {
'男': 'mars',
'女': 'venus',
'其他': 'transgender'
},
sexColors: {
'男': '#4A90E2',
'女': '#FF69B4',
'其他': '#999'
}
}
},
computed: {
...mapState(['userInfo']),
//
isCurrentUser() {
return this.userInfo.id === this.userId
},
//
currentList() {
const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
return this[listMap[this.currentTabIndex]] || []
},
//
currentPageParams() {
const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
return this.pageParams[listMap[this.currentTabIndex]]
},
//
showEmptyState() {
return !this.loading && this.currentList.length === 0
},
//
showLoadMore() {
return this.currentList.length > 0 && this.currentPageParams.hasMore && this.loadingMore
},
//
showNoMore() {
return this.currentList.length > 0 && !this.currentPageParams.hasMore
}
},
onLoad(options) {
this.userId = options.userId || this.userInfo.id
this.loadUserProfile()
this.loadUserContent(false)
},
onPullDownRefresh() {
this.loadUserProfile()
this.refreshUserContent()
},
onReachBottom() {
this.loadMoreContent()
},
methods: {
//
showMoreOptions() {
uni.showActionSheet({
itemList: ['举报用户', '拉黑用户'],
success: (res) => {
if (res.tapIndex === 0) {
this.reportUser()
} else if (res.tapIndex === 1) {
this.blockUser()
}
}
})
},
//
previewAvatar() {
if (this.userProfile.headImage) {
uni.previewImage({
urls: [this.userProfile.headImage]
})
}
},
// VIP
getVipLevel(level) {
const levels = ['', 'VIP', 'SVIP']
return levels[level] || ''
},
//
getAge() {
if (!this.userProfile.yearDate) return ''
const birthYear = parseInt(this.userProfile.yearDate)
const currentYear = new Date().getFullYear()
return currentYear - birthYear
},
//
getAuthText(status) {
const authTexts = ['审核中', '个人认证', '店铺认证']
return authTexts[status] || '未认证'
},
//
onTabClick(item) {
this.currentTabIndex = item.index
this.refreshUserContent()
},
//
toggleFollow() {
if (!uni.getStorageSync('token')) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
this.isFollowed = !this.isFollowed
// /API
this.$api(this.isFollowed ? 'followUser' : 'unfollowUser', {
userId: this.userId
}, res => {
if (res.code === 200) {
uni.showToast({
title: this.isFollowed ? '关注成功' : '取消关注',
icon: 'success'
})
}
})
},
//
sendMessage() {
if (!uni.getStorageSync('token')) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages_order/chat/chatDetail?userId=${this.userId}`
})
},
//
showFans() {
uni.navigateTo({
url: `/pages_order/profile/fansList?userId=${this.userId}&type=fans`
})
},
//
showFollowing() {
uni.navigateTo({
url: `/pages_order/profile/fansList?userId=${this.userId}&type=following`
})
},
//
showLikes() {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
},
//
loadUserProfile() {
this.loading = true
this.$api('getUserProfile', { userId: this.userId }, res => {
this.loading = false
uni.stopPullDownRefresh()
if (res.code === 200) {
this.userProfile = res.result
//
if (!this.isCurrentUser) {
this.checkFollowStatus()
}
}
})
},
//
checkFollowStatus() {
this.$api('checkFollowStatus', { userId: this.userId }, res => {
if (res.code === 200) {
this.isFollowed = res.result.isFollowed
}
})
},
//
refreshUserContent() {
const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
const currentListKey = listMap[this.currentTabIndex]
//
this.pageParams[currentListKey].pageNum = 1
this.pageParams[currentListKey].hasMore = true
//
this[currentListKey] = []
//
this.loadUserContent(false)
},
//
loadUserContent(isLoadMore = false) {
const apiMap = [
'getUserPosts', //
'getUserRenting', //
'getUserWork', //
'getUserShop' //
]
const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
const currentListKey = listMap[this.currentTabIndex]
const currentPageParams = this.pageParams[currentListKey]
//
if (isLoadMore) {
this.loadingMore = true
} else {
this.loading = true
}
this.$api(apiMap[this.currentTabIndex], {
userId: this.userId,
pageNum: currentPageParams.pageNum,
pageSize: 20
}, res => {
//
this.loading = false
this.loadingMore = false
uni.stopPullDownRefresh()
if (res.code === 200) {
const newData = res.result.records || res.result || []
if (isLoadMore) {
//
this[currentListKey] = [...this[currentListKey], ...newData]
} else {
//
this[currentListKey] = newData
}
//
if (newData.length < 20) {
//
this.pageParams[currentListKey].hasMore = false
} else {
// +1
this.pageParams[currentListKey].pageNum += 1
}
} else {
//
if (isLoadMore && currentPageParams.pageNum > 1) {
this.pageParams[currentListKey].pageNum -= 1
}
}
})
},
//
loadMoreContent() {
//
if (!this.currentPageParams.hasMore || this.loadingMore || this.loading) {
return
}
//
if (this.currentList.length === 0) {
return
}
this.loadUserContent(true)
},
//
onPostClick(item) {
this.$utils.navigateTo(`/pages_order/post/postDetail?id=${item.id}`)
},
//
onPostLike(item) {
console.log('点赞帖子:', item.id)
},
//
onRentingClick(item) {
this.$utils.navigateTo(`/pages_order/renting/rentingDetail?id=${item.id}`)
},
//
onWorkClick(item) {
this.$utils.navigateTo(`/pages_order/work/workDetail?id=${item.id}`)
},
//
onShopClick(item) {
this.$utils.navigateTo(`/pages_order/gourmet/gourmetDetail?id=${item.id}`)
},
//
reportUser() {
uni.showToast({
title: '举报功能开发中',
icon: 'none'
})
},
//
blockUser() {
uni.showModal({
title: '确认拉黑该用户?',
success: (res) => {
if (res.confirm) {
// API
uni.showToast({
title: '拉黑功能开发中',
icon: 'none'
})
}
}
})
},
//
getEmptyText() {
const emptyTexts = ['暂无帖子', '暂无租房信息', '暂无招聘信息', '暂无店铺信息']
return emptyTexts[this.currentTabIndex] || '暂无数据'
},
//
getEmptyIcon() {
const emptyIcons = ['list', 'home', 'search', 'shop']
return emptyIcons[this.currentTabIndex] || 'list'
}
}
}
</script>
<style scoped lang="scss">
.profile-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.profile-header {
position: relative;
padding: 40rpx 30rpx 40rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding-top: 180rpx;
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.9) 100%);
}
.back-btn {
position: absolute;
top: 80rpx;
left: 30rpx;
z-index: 3;
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10rpx);
}
.user-info {
position: relative;
z-index: 2;
display: flex;
margin-bottom: 40rpx;
.user-avatar {
position: relative;
margin-right: 30rpx;
image {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
}
.vip-badge {
position: absolute;
bottom: -10rpx;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(45deg, #FFD700, #FFA500);
color: #333;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 600;
}
}
.user-details {
flex: 1;
color: #fff;
.username {
font-size: 36rpx;
font-weight: 600;
margin-bottom: 16rpx;
}
.user-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 16rpx;
.tag {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
text {
margin-left: 6rpx;
}
}
}
.school-info {
font-size: 24rpx;
opacity: 0.9;
text {
display: block;
margin-bottom: 6rpx;
}
}
}
}
.stats-row {
position: relative;
z-index: 2;
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
.stat-item {
text-align: center;
color: #fff;
.stat-number {
font-size: 40rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
opacity: 0.8;
}
}
}
.action-buttons {
position: relative;
z-index: 2;
display: flex;
gap: 20rpx;
button {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
border-radius: 50rpx;
font-size: 28rpx;
border: none;
text {
margin-left: 8rpx;
}
}
.follow-btn {
background: #fff;
color: #333;
&.followed {
background: rgba(255, 255, 255, 0.3);
color: #fff;
}
}
.message-btn {
background: rgba(255, 255, 255, 0.3);
color: #fff;
}
}
}
.content-tabs {
background: #fff;
border-bottom: 1rpx solid #eee;
padding: 0 20rpx;
}
.content-container {
min-height: 400rpx;
.posts-content,
.renting-content,
.work-content,
.shop-content {
padding: 20rpx 0;
}
}
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
.load-more-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
text {
margin-top: 15rpx;
font-size: 24rpx;
color: #666;
}
}
.no-more-state {
padding: 30rpx 0;
text-align: center;
text {
font-size: 24rpx;
color: #ccc;
}
}
</style>

Loading…
Cancel
Save