wangfukang 3 days ago
parent
commit
35d8680604
  1. 43
      components/tab-bar/delivery.vue
  2. 293
      components/tab-bar/myCenter.vue
  3. 2
      components/tab-bar/postList.vue
  4. 819
      components/tab-bar/release.vue
  5. 77
      components/tab-bar/tab-bar.vue

43
components/tab-bar/delivery.vue

@ -492,6 +492,8 @@
refreshLoading: false,
refreshLoadingStartTime: 0,
refreshLoadingTimer: null,
deliveryRequestId: 0,
loadStatus: 'more',
pickupCode: '',
pickupCodeContext: null,
pickupCodeSubmitting: false,
@ -642,11 +644,12 @@
}
uni.setStorageSync('deliveryStatusCounts', this.deliveryStatusCounts)
},
syncDeliveryStatusCountFromPage(total) {
syncDeliveryStatusCountFromPage(total, status) {
const count = Number(total) || 0
if (this.searchForm.status == 1) {
const currentStatus = status || this.searchForm.status
if (currentStatus == 1) {
this.deliveryStatusCounts.daiqu = count
} else if (this.searchForm.status == 2) {
} else if (currentStatus == 2) {
this.deliveryStatusCounts.daisong = count
}
uni.setStorageSync('deliveryStatusCounts', this.deliveryStatusCounts)
@ -723,6 +726,7 @@
if (uni.getStorageSync('worker')) {
this.searchForm.workerId = uni.getStorageSync('worker').workerId
}
this.searchForm.pageNum = 1
this.$forceUpdate()
this.loadWorkerRules()
this.getDelivery()
@ -897,31 +901,40 @@
getDelivery() {
if (!this.ensureLogin()) return
let that = this
const requestId = ++that.deliveryRequestId
const requestForm = Object.assign({}, that.searchForm)
const requestStatus = requestForm.status
const requestPageNum = Number(requestForm.pageNum) || 1
that.isLoadingPage = true;
this.tui.request("/mall/delivery/pagebyworker", "POST", this.searchForm, false, false).then((res) => {
that.loadStatus = 'loading';
this.tui.request("/mall/delivery/pagebyworker", "POST", requestForm, false, false).then((res) => {
if (requestId != that.deliveryRequestId) return
that.isLoadingPage = false;
that.loadStatus = 'nomore';
if (res.code == 200) {
that.syncDeliveryStatusCountFromPage(res.result && res.result.total)
if (this.searchForm.status == 1) {
if (that.searchForm.pageNum == 1) {
that.deliveryItem1 = res.result.records;
const result = res.result || {}
const records = Array.isArray(result.records) ? result.records : []
that.syncDeliveryStatusCountFromPage(result.total, requestStatus)
if (requestStatus == 1) {
if (requestPageNum == 1) {
that.deliveryItem1 = records;
} else {
that.deliveryItem1 = [...that.deliveryItem1, ...res.result.records]
that.deliveryItem1 = [...that.deliveryItem1, ...records]
}
that.pageData = that.deliveryItem1
} else {
if (that.searchForm.pageNum == 1) {
that.deliveryItem = res.result.records;
if (requestPageNum == 1) {
that.deliveryItem = records;
} else {
that.deliveryItem = [...that.deliveryItem, ...res.result.records]
that.deliveryItem = [...that.deliveryItem, ...records]
}
that.pageData = that.deliveryItem
}
that.totalPages = res.result.pages; // pages =
that.totalPages = Number(result.pages) || 1; // pages =
that.loadStatus = requestPageNum >= that.totalPages ? 'nomore' : 'more';
that.$forceUpdate();
} else {
that.loadStatus = 'more';
that.tui.toast(res.message);
}
if (that.refreshLoading) {
@ -930,7 +943,9 @@
uni.hideLoading();
}
}).catch((res) => {
if (requestId != that.deliveryRequestId) return
that.isLoadingPage = false;
that.loadStatus = 'more';
if (that.refreshLoading) {
that.finishRefreshLoading()
}

293
components/tab-bar/myCenter.vue

@ -5,11 +5,12 @@
<view class="my-content" :style="contentStyle">
<view class="top-zone fade-up" style="animation-delay: 0ms;">
<view class="profile-row">
<view class="avatar-card">
<image class="avatar-img" :src="getMyCenterImage('avatar')" mode="aspectFill"></image>
<view class="avatar-card" hover-class="tap-soft" hover-stay-time="80" @tap.stop="openProfileEditor">
<image class="avatar-img" :src="userAvatar" mode="aspectFill"></image>
<view class="avatar-edit"></view>
</view>
<view class="profile-main">
<view class="name-row">
<view class="name-row" hover-class="tap-soft" hover-stay-time="80" @tap.stop="openProfileEditor">
<text class="user-name">{{(userName == null || userName == undefined) ? '半径同学' : userName}}</text>
<text class="verify-dot"></text>
</view>
@ -158,6 +159,31 @@
</view>
</view>
</uni-popup>
<!-- 头像昵称编辑弹窗 -->
<uni-popup ref="profilePopup" background-color="transparent">
<view class="profile-popup" @tap.stop>
<view class="profile-popup-title">编辑头像昵称</view>
<view class="profile-popup-sub">使用微信头像昵称填写能力完善资料</view>
<button class="profile-avatar-picker" open-type="chooseAvatar" hover-class="tap-soft"
hover-stay-time="80" @chooseavatar="onChooseWechatAvatar">
<image class="profile-avatar-preview" :src="editAvatar || userAvatar" mode="aspectFill"></image>
<view class="profile-avatar-mask">{{profileUploading ? '上传中' : '选择微信头像'}}</view>
</button>
<form @submit="saveProfile">
<input class="profile-name-input" v-model="editNickname" type="nickname" name="nickname"
:adjust-position="false" maxlength="20" placeholder="点击选择微信昵称"
@input="onProfileNicknameInput"
placeholder-class="profile-name-placeholder" />
<view class="profile-popup-actions">
<view class="profile-popup-cancel" @tap="$refs.profilePopup.close()">取消</view>
<button class="profile-popup-save" :class="{ disabled: profileSaving || profileUploading || !editNickname || !editAvatar }"
:disabled="profileSaving || profileUploading || !editNickname || !editAvatar" form-type="submit">
{{profileSaving ? '保存中' : '一键保存'}}
</button>
</view>
</form>
</view>
</uni-popup>
<!-- 弹出区域选择 -->
<uni-popup ref="areaPopup" background-color="transparent">
<view class="campus-area-popup">
@ -230,6 +256,14 @@
worker: uni.getStorageSync('worker'),
balanceRefreshId: 0,
refreshing: false,
profileNickname: uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '半径同学',
profileAvatar: uni.getStorageSync('avatarUrl') || uni.getStorageSync('avatar') || uni.getStorageSync('headimgurl') || '',
editNickname: '',
editAvatar: '',
nicknameInputByKeyboard: false,
nicknameManualTipShown: false,
profileSaving: false,
profileUploading: false,
couponCount: Number(uni.getStorageSync('couponCount')) || 8,
blindBoxCount: Number(uni.getStorageSync('blindBoxCount')) || 8,
orderCounts: {
@ -277,7 +311,10 @@
})
},
userName() {
return uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '半径同学'
return this.profileNickname || '半径同学'
},
userAvatar() {
return this.profileAvatar || this.getMyCenterImage('avatar')
},
avatarText() {
return (this.userName || '张').slice(0, 1)
@ -402,6 +439,7 @@
},
mounted() {
this.setMenuButtonInfo()
this.syncCachedUserProfile()
uni.$on('myCenterCountsChange', this.setPendingCounts)
},
beforeDestroy() {
@ -411,6 +449,120 @@
getMyCenterImage(key) {
return this.myCenterImages[key] || MY_CENTER_IMAGE_URLS[key] || ''
},
openProfileEditor() {
this.editNickname = this.userName || ''
this.editAvatar = this.profileAvatar || ''
this.nicknameInputByKeyboard = false
this.nicknameManualTipShown = false
this.$refs.profilePopup.open()
},
onProfileNicknameInput(e) {
const detail = e && e.detail ? e.detail : {}
const keyCode = detail.keyCode
const isKeyboardInput = keyCode !== undefined && keyCode !== 0
this.nicknameInputByKeyboard = isKeyboardInput
if (isKeyboardInput && !this.nicknameManualTipShown) {
this.nicknameManualTipShown = true
this.tui.toast('只能选择微信昵称')
}
if (!isKeyboardInput) {
this.nicknameManualTipShown = false
}
},
onChooseWechatAvatar(e) {
if (this.profileUploading) return
const filePath = e && e.detail ? e.detail.avatarUrl : ''
if (!filePath) return
this.uploadProfileAvatar(filePath).then((url) => {
if (url) this.editAvatar = url
})
},
uploadProfileAvatar(path) {
if (!path) return Promise.resolve('')
this.profileUploading = true
return this.tui.uploadFile('/upload/file', path).then((fileObj) => {
const url = this.extractUploadUrl(fileObj)
if (!url) this.tui.toast('头像上传失败')
return url
}).catch(() => {
this.tui.toast('头像上传失败,请稍后重试')
return ''
}).finally(() => {
this.profileUploading = false
})
},
extractUploadUrl(fileObj) {
if (!fileObj) return ''
if (typeof fileObj == 'string') return fileObj
return fileObj.url || fileObj.fileUrl || fileObj.path || fileObj.fullPath || ''
},
saveProfile() {
if (this.profileSaving || this.profileUploading) return
const nickname = (this.editNickname || '').trim()
if (!nickname) {
this.tui.toast('请先获取微信昵称')
return
}
if (this.nicknameInputByKeyboard) {
this.tui.toast('只能选择微信昵称,不能手动输入')
return
}
if (!this.editAvatar) {
this.tui.toast('请先获取微信头像')
return
}
this.editNickname = nickname
this.submitProfile(nickname)
},
submitProfile(nickname) {
this.profileSaving = true
this.tui.request('/user/profile', 'POST', {
nickname,
avatar: this.editAvatar || ''
}, false, false).then((res) => {
if (res.code == 200) {
const user = Object.assign({
nickname,
avatar: this.editAvatar || this.profileAvatar
}, res.result || {})
this.cacheUserProfile(user)
this.$refs.profilePopup.close()
this.tui.toast('修改成功', 1500, true)
this.$forceUpdate()
} else {
this.tui.toast(res.message || '修改失败')
}
}).catch(() => {
this.tui.toast('修改失败,请稍后重试')
}).finally(() => {
this.profileSaving = false
})
},
cacheUserProfile(user) {
if (!user) return
if (user.nickname !== undefined) {
this.profileNickname = user.nickname || '半径同学'
uni.setStorageSync('nickName', this.profileNickname)
uni.setStorageSync('nickname', this.profileNickname)
uni.setStorageSync('userName', this.profileNickname)
}
if (user.avatar !== undefined) {
this.profileAvatar = user.avatar || ''
uni.setStorageSync('avatarUrl', this.profileAvatar)
uni.setStorageSync('avatar', this.profileAvatar)
uni.setStorageSync('headimgurl', this.profileAvatar)
}
if (user.id !== undefined) uni.setStorageSync('id', user.id)
if (user.unionid !== undefined) uni.setStorageSync('unionid', user.unionid)
if (user.miniProgramOpenid !== undefined) uni.setStorageSync('miniProgramOpenid', user.miniProgramOpenid)
if (user.officialAccountOpenid !== undefined) uni.setStorageSync('officialAccountOpenid', user.officialAccountOpenid)
},
syncCachedUserProfile() {
const nickname = uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || uni.getStorageSync('userName') || ''
const avatar = uni.getStorageSync('avatarUrl') || uni.getStorageSync('avatar') || uni.getStorageSync('headimgurl') || ''
if (nickname) this.profileNickname = nickname
if (avatar) this.profileAvatar = avatar
},
getMyCenterImageCache() {
const cache = uni.getStorageSync(MY_CENTER_IMAGE_CACHE_KEY)
return cache && typeof cache === 'object' ? cache : {}
@ -646,6 +798,7 @@
},
init() {
this.applyCachedMyCenterImages()
this.syncCachedUserProfile()
const area = this.getArea()
this.schoolName1 = area.title
this.schoolShop = this.normalizeShopList(uni.getStorageSync('schoolShop'))
@ -917,6 +1070,21 @@
display: block;
}
.avatar-edit {
position: absolute;
right: 0;
bottom: 0;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(7, 61, 51, .72);
color: #FFFFFF;
font-size: 18rpx;
font-weight: 900;
line-height: 32rpx;
text-align: center;
}
.level-chip,
.identity-pill,
.saved-badge,
@ -1698,6 +1866,123 @@
background: rgba(255, 255, 255, .92);
}
.profile-popup {
box-sizing: border-box;
width: 620rpx;
padding: 38rpx 34rpx 30rpx;
border: 1rpx solid rgba(255, 255, 255, .78);
border-radius: 42rpx;
background: rgba(255, 255, 255, .94);
box-shadow: 0 30rpx 86rpx rgba(7, 61, 51, .18);
text-align: center;
}
.profile-popup-title {
color: #073D33;
font-size: 36rpx;
font-weight: 900;
line-height: 44rpx;
}
.profile-popup-sub {
margin-top: 10rpx;
color: rgba(7, 61, 51, .45);
font-size: 23rpx;
line-height: 32rpx;
}
.profile-avatar-picker {
position: relative;
width: 132rpx;
height: 132rpx;
margin: 30rpx auto 26rpx;
padding: 0;
border: 0;
border-radius: 38rpx;
overflow: hidden;
background: linear-gradient(145deg, #EAFBF4, #B7F3D5);
box-shadow: 0 12rpx 34rpx rgba(7, 61, 51, .12);
line-height: normal;
}
.profile-avatar-picker::after {
border: 0;
}
.profile-avatar-preview {
width: 100%;
height: 100%;
display: block;
}
.profile-avatar-mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 42rpx;
background: rgba(7, 61, 51, .62);
color: #FFFFFF;
font-size: 20rpx;
font-weight: 800;
line-height: 42rpx;
}
.profile-name-input {
box-sizing: border-box;
width: 100%;
height: 88rpx;
padding: 0 26rpx;
border-radius: 28rpx;
background: #F8FCFA;
color: #073D33;
font-size: 28rpx;
font-weight: 800;
text-align: left;
}
.profile-name-placeholder {
color: rgba(7, 61, 51, .32);
}
.profile-popup-actions {
display: flex;
gap: 18rpx;
margin-top: 30rpx;
}
.profile-popup-cancel,
.profile-popup-save {
flex: 1;
height: 78rpx;
margin: 0;
padding: 0;
border: 0;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 900;
line-height: 78rpx;
}
.profile-popup-save::after {
border: 0;
}
.profile-popup-cancel {
background: #F2F6F4;
color: rgba(7, 61, 51, .58);
}
.profile-popup-save {
background: linear-gradient(90deg, #E3FF96, #A6FFEA);
color: #073D33;
box-shadow: 0 12rpx 28rpx rgba(13, 114, 82, .14);
}
.profile-popup-save.disabled {
opacity: .58;
}
.popup-pay-title,
.popup-area-title {
margin-bottom: 24rpx;

2
components/tab-bar/postList.vue

@ -111,7 +111,7 @@
</view>
<view class="seller-row">
<image class="seller-avatar" :src="item.icon" mode="aspectFill"></image>
<text class="seller-name">半径卖家</text>
<text class="seller-name">{{item.shopName}}</text>
<view class="want-count" @tap.stop="toggleLike(item)">
<uni-icons
:type="item.liked ? 'heart-filled' : 'heart'"

819
components/tab-bar/release.vue

@ -0,0 +1,819 @@
<template>
<view class="campus-page">
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="brand-line">
<text class="brand">校园热榜</text>
<text class="eating">热议中</text>
</view>
<view class="subtitle">每天都有新鲜事全校都在聊</view>
<view class="search-line">
<view class="search-box">
<uni-icons type="search" size="19" color="#8a9692"></uni-icons>
<input class="search-input" v-model.trim="keyword" placeholder="搜索校园热议、关键词" placeholder-class="placeholder" confirm-type="search" @confirm="search" />
<view class="search-button" @tap="search">搜索</view>
</view>
<view class="release-fab" @tap="goPublish">
<view class="fab-spark fab-spark--one"></view>
<view class="fab-spark fab-spark--two"></view>
<view class="fab-camera">
<view class="fab-camera-lens"></view>
</view>
<text style="margin-top: 6rpx;">发布</text>
</view>
</view>
<view class="category-row">
<view
class="category-card"
:class="{ active: activeCategory === item.tone }"
v-for="item in channels"
:key="item.tone"
@tap="selectCategory(item.tone)"
>
<view class="hot-category-icon">
<image class="hot-category-img" :src="item.icon" mode="aspectFit"></image>
</view>
<view class="category-badge" v-if="item.tone === 'mine' && myCircleUnread > 0">{{formatBadge(myCircleUnread)}}</view>
<view class="hot-category-name">{{item.name}}</view>
<view class="hot-category-desc">{{item.desc}}</view>
</view>
</view>
</view>
<scroll-view class="page-scroll" scroll-y :style="{ paddingTop: headerHeight + 'px' }" :lower-threshold="120" @scrolltolower="loadMore">
<view class="sub-category-scroll" v-if="activeCategory !== 'mine'">
<view
class="sub-category"
:class="{ active: !activeCategoryId && postSortType !== 'latest' }"
@tap="selectSecondCategory('')"
>全部</view>
<view
class="sub-category"
:class="{ active: postSortType === 'latest' }"
@tap="selectLatest"
>最新</view>
<view
class="sub-category"
:class="{ active: postSortType !== 'latest' && activeCategoryId === item.id }"
v-for="item in secondCategoryOptions"
:key="item.id"
@tap="selectSecondCategory(item.id)"
>{{item.name}}</view>
</view>
<view class="filter-panel" v-if="activeCategory !== 'wall' && activeCategory !== 'mine'">
<view class="filter-head">
<text>随机找搭子</text>
<view class="match-btn" :class="{ matching }" @tap="startMatch">{{matching ? '匹配中...' : '发起匹配'}}</view>
</view>
<view class="match-animation" v-if="matching">
<view></view><view></view><view></view>
</view>
<scroll-view scroll-x class="tag-scroll" v-if="tagOptions.length">
<view
class="tag"
:class="{ active: selectedTag === item.name }"
v-for="item in tagOptions"
:key="item.key"
@tap="toggleTag(item.name)"
>{{item.name}}</view>
</scroll-view>
</view>
<view class="section-head">
<view>
<view class="section-title">{{sectionTitle}}</view>
<view class="section-sub">{{sectionSubTitle}}</view>
</view>
<view class="live-dot"><text></text>{{postSortType === 'latest' ? '最新优先' : (activeCategory === 'wall' ? '实时更新' : '热度优先')}}</view>
</view>
<view class="post-list">
<view
class="post-card"
:class="{ 'post-card--image': getImages(item).length }"
v-for="item in posts"
:key="item.id"
@tap="goDetail(item)"
>
<view class="post-top">
<image class="avatar" :src="displayAvatar(item)" mode="aspectFill"></image>
<view class="author-main">
<view class="author">{{displayName(item)}}</view>
<view class="time">{{item.createTime || ''}} · {{item.categoryName || '本校'}}</view>
</view>
<view class="partner-action" v-if="canUsePartnerChat(item)" @tap.stop="goChat(item)">搭一下</view>
</view>
<view class="title">{{item.title}}</view>
<view class="content">{{item.contentText || stripHtml(item.content)}}</view>
<view v-if="getImages(item).length" class="image-grid" :class="'image-grid--' + Math.min(getImages(item).length, 3)">
<image
v-for="(pic, index) in getImages(item).slice(0, 3)"
:key="pic"
:src="pic"
:mode="getImages(item).length === 1 ? 'aspectFit' : 'aspectFill'"
:style="getPostImageStyle(pic, getImages(item).length)"
@load="onPostImageLoad(pic, $event)"
@tap.stop="previewImages(item, index)"
></image>
</view>
<view class="tag-row" v-if="getTags(item).length">
<text v-for="tag in getTags(item)" :key="tag">{{tag}}</text>
</view>
<view class="partner-info" v-if="item.postType === 'partner'">
<text>{{item.location || '地点待定'}}</text>
<text>{{item.activityTime || '时间可商量'}}</text>
<text>{{item.joinedCount || 0}}/{{item.targetCount || '不限'}} 人联系</text>
</view>
<view class="contact-row" v-if="item.contactWechat || item.contactPhone">
<text v-if="item.contactWechat">微信 {{item.contactWechat}}</text>
<text v-if="item.contactPhone">手机 {{item.contactPhone}}</text>
</view>
<view class="post-footer">
<view class="meta">{{item.commentCount || 0}} 评论 · 热度 {{item.hotScore || 0}}</view>
<view class="reaction-row">
<view
class="reaction"
:class="{ 'reaction--active': reactionEffects[item.id + ':' + emoji] }"
v-for="emoji in reactions"
:key="emoji"
@tap.stop="react(item, emoji)"
>
<view class="reaction-emoji">{{emoji}}</view>
<text class="reaction-count">{{getReactionCount(item, emoji)}}</text>
</view>
</view>
</view>
</view>
</view>
<view class="empty" v-if="!loading && !posts.length">暂无内容来发布第一条吧</view>
<view class="load-state">{{loading ? '加载中...' : (hasMore ? '上滑加载更多' : '已经到底了')}}</view>
<view class="bottom-space"></view>
</scroll-view>
<view class="page-reaction-effect" v-if="reactionBurstEffects.length">
<view
class="page-reaction-float"
:class="effect.className"
:style="effect.style"
v-for="effect in reactionBurstEffects"
:key="effect.id"
>{{effect.emoji}}</view>
</view>
<view class="match-popup-mask" v-if="showMatchPanel" @tap="closeMatchPanel">
<view class="match-popup" @tap.stop>
<view class="match-popup-head">
<view>
<view class="match-popup-title">模糊匹配找搭子</view>
<view class="match-popup-sub">按标题关键词和二级分类从当前搭子列表里挑一个最合适的</view>
</view>
<view class="match-close" @tap="closeMatchPanel">×</view>
</view>
<view class="match-field-title">想找什么搭子</view>
<input class="match-input" v-model.trim="matchKeyword" maxlength="30" placeholder="例如:王者、夜跑、饭搭、自习" />
<view class="match-field-title">选择二级分类</view>
<view class="match-tag-wrap">
<view class="match-tag" :class="{ active: !matchCategoryId }" @tap="setMatchCategory('')">不限</view>
<view
class="match-tag"
:class="{ active: matchCategoryId === item.id }"
v-for="item in secondCategoryOptions"
:key="item.key"
@tap="setMatchCategory(item.id)"
>{{item.name}}</view>
</view>
<view class="match-actions">
<view class="match-cancel" @tap="closeMatchPanel">取消</view>
<view class="match-start" @tap="confirmMatch">开始匹配</view>
</view>
</view>
</view>
<view class="creating-mask" v-if="matching">
<view class="creating-card">
<view class="creating-orbit">
<view class="creating-dot dot-a"></view>
<view class="creating-dot dot-b"></view>
<view class="creating-dot dot-c"></view>
<view class="creating-core"></view>
</view>
<view class="creating-title">正在漂流匹配</view>
<view class="creating-desc">根据关键词和标签寻找最合适的搭子...</view>
</view>
</view>
</view>
</template>
<script>
import {
listCampusCategories,
pageCampusPosts,
pageCampusPartnerPosts,
matchCampusPartnerPost,
pageMyCampusPosts,
toggleCampusLike,
createCampusChatSession,
getCampusUnreadSummary
} from '@/common/campusApi.js'
import { getIeProfile } from '@/common/ieApi.js'
import { ensureIeVerifiedBeforeAction } from '@/common/ieRealNameAuth.js'
const CHANNELS = [
{ tone: 'wall', name: '校园墙', desc: '新鲜事', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/f3b050e7f6d74bb9adca54de0bf3fc17.png' },
{ tone: 'game', name: '游戏搭子', desc: '开黑组队', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/f8d490dfec0a4d17b7c18d710502f6f2.png' },
{ tone: 'daily', name: '日常搭子', desc: '吃饭自习', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/e810837993204689bda2fc68d1d29448.png' },
{ tone: 'sport', name: '运动搭子', desc: '约球夜跑', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/01444d0812c544fe9cde7cbbb919b473.png' },
{ tone: 'mine', name: '我的圈子', desc: '消息动态', icon: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/430e4ae880574845b7d028bd96abde02.jpg' }
]
export default {
data() {
return {
statusBarHeight: 24,
channels: CHANNELS,
activeCategory: 'wall',
activeCategoryId: '',
keyword: '',
selectedTag: '',
postSortType: 'hot',
posts: [],
pageNum: 1,
pageSize: 10,
totalPages: 1,
loading: false,
inited: false,
matching: false,
showMatchPanel: false,
matchKeyword: '',
matchTag: '',
matchCategoryId: '',
myCircleUnread: 0,
categoryMap: {},
tagMap: {},
reactions: ['😂', '😭', '🤢', '👍'],
reactionLocks: {},
reactionEffects: {},
reactionBurstSeq: 0,
reactionBurstTimer: null,
reactionBurstEffects: [],
imageSizeMap: {},
defaultAvatar: '/static/images/img/songshu.png'
}
},
computed: {
headerHeight() {
return this.statusBarHeight + (uni.upx2px ? uni.upx2px(424) : 212)
},
hasMore() {
return this.pageNum < this.totalPages
},
tagOptions() {
return (this.tagMap[this.activeCategory] || []).map((item, index) => ({
...item,
key: item.id || item.code || item.name || String(index)
}))
},
secondCategoryOptions() {
return (this.categoryMap[this.activeCategory] || []).map((item, index) => ({
...item,
key: item.id || item.code || item.name || String(index)
}))
},
sectionTitle() {
if (this.activeCategory === 'mine') return '我的圈子'
const item = this.channels.find(row => row.tone === this.activeCategory)
return item ? item.name : '校园圈'
},
sectionSubTitle() {
if (this.postSortType === 'latest') return '按发布时间倒序排列'
if (this.activeCategory === 'wall') return '按点赞和评论综合热度排序'
if (this.activeCategory === 'mine') return '我的发布、评论和聊天动态'
return '支持关键词和热门标签模糊匹配'
}
},
mounted() {
if (uni.getMenuButtonBoundingClientRect) {
this.statusBarHeight = uni.getMenuButtonBoundingClientRect().top || this.statusBarHeight
}
uni.$on('campusUnreadChange', this.applyUnread)
uni.$on('campusPostPublished', this.refreshAfterCampusPublish)
},
beforeDestroy() {
uni.$off('campusUnreadChange', this.applyUnread)
uni.$off('campusPostPublished', this.refreshAfterCampusPublish)
if (this.reactionBurstTimer) clearTimeout(this.reactionBurstTimer)
},
methods: {
init() {
if (this.inited && this.loading) return
this.inited = true
this.loadUnread()
this.loadCategories().finally(() => {
this.reload()
})
},
refreshAfterCampusPublish() {
if (!this.inited) return
this.loadUnread()
this.reload()
},
getArea() {
const area = uni.getStorageSync('area')
if (!area) return {}
if (typeof area === 'string') {
try { return JSON.parse(area) } catch (e) { return {} }
}
return area
},
loadCategories() {
const area = this.getArea()
return Promise.all([
listCampusCategories({ regionId: area.id || '', categoryType: 'category' }),
listCampusCategories({ regionId: area.id || '', categoryType: 'tag' })
]).then(([categories, tags]) => {
const byParent = {}
;(Array.isArray(tags) ? tags : []).forEach(item => {
const key = item.parentCode
if (!byParent[key]) byParent[key] = []
byParent[key].push(item)
})
this.tagMap = byParent
const categoryList = Array.isArray(categories) ? categories : []
const categoryByParent = {}
categoryList.forEach(item => {
const key = item.parentCode
if (!categoryByParent[key]) categoryByParent[key] = []
categoryByParent[key].push(item)
})
this.categoryMap = categoryByParent
this.channels = CHANNELS
})
},
loadUnread() {
getCampusUnreadSummary().then(summary => this.applyUnread(summary)).catch(() => {})
},
applyUnread(summary = {}) {
this.myCircleUnread = Number(summary.myCircleUnread || summary.campusUnread) || 0
},
selectCategory(tone) {
if (this.activeCategory === tone) return
this.activeCategory = tone
this.activeCategoryId = ''
this.selectedTag = ''
this.postSortType = 'hot'
this.reload()
},
selectSecondCategory(id) {
if (this.activeCategoryId === id && this.postSortType !== 'latest') return
this.activeCategoryId = id
this.postSortType = 'hot'
this.reload()
},
selectLatest() {
if (this.postSortType === 'latest') return
this.activeCategoryId = ''
this.postSortType = 'latest'
this.reload()
},
search() {
this.reload()
},
toggleTag(tag) {
this.selectedTag = this.selectedTag === tag ? '' : tag
this.reload()
},
startMatch() {
if (this.matching) return
this.matchKeyword = this.keyword || ''
this.matchTag = this.selectedTag || ''
this.matchCategoryId = this.activeCategoryId || ''
this.showMatchPanel = true
},
closeMatchPanel() {
if (this.matching) return
this.showMatchPanel = false
},
setMatchTag(tag) {
this.matchTag = tag
},
setMatchCategory(id) {
this.matchCategoryId = id || ''
},
confirmMatch() {
if (this.matching) return
this.showMatchPanel = false
this.matching = true
const area = this.getArea()
const query = {
regionId: area.id || '',
partnerType: this.activeCategory,
categoryId: this.matchCategoryId,
keyword: this.matchKeyword,
tag: this.matchTag
}
const matchRequest = matchCampusPartnerPost(query).catch(() => null)
setTimeout(() => {
matchRequest.then(matched => {
this.matching = false
if (!matched) {
uni.showToast({ title: '暂时没有匹配到合适搭子', icon: 'none' })
return
}
this.goDetail(matched)
})
}, 1500)
},
reload() {
this.pageNum = 1
this.totalPages = 1
this.posts = []
this.loadPosts()
},
loadMore() {
if (this.loading || !this.hasMore) return
this.pageNum += 1
this.loadPosts()
},
loadPosts() {
if (this.loading) return
this.loading = true
const area = this.getArea()
const query = {
pageNum: this.pageNum,
pageSize: this.pageSize,
regionId: area.id || '',
keyword: this.keyword,
categoryId: this.activeCategoryId,
tag: this.selectedTag,
sortType: this.postSortType
}
let request
if (this.activeCategory === 'mine') {
request = pageMyCampusPosts(query)
} else if (this.activeCategory === 'wall') {
request = pageCampusPosts({ ...query, postType: 'wall' })
} else {
request = pageCampusPartnerPosts({ ...query, partnerType: this.activeCategory })
}
request.then(page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1
this.posts = this.pageNum === 1 ? records : this.posts.concat(records)
}).finally(() => {
this.loading = false
})
},
getImages(item) {
if (!item) return []
if (Array.isArray(item.imageList)) return item.imageList
if (Array.isArray(item.images)) return item.images
if (!item.images) return []
try { return JSON.parse(item.images) || [] } catch (e) { return [] }
},
onPostImageLoad(pic, e) {
if (!pic || !e || !e.detail) return
this.$set(this.imageSizeMap, this.getImageKey(pic), {
width: Number(e.detail.width) || 0,
height: Number(e.detail.height) || 0
})
},
getImageKey(pic) {
const value = String(pic || '')
let hash = 0
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash) + value.charCodeAt(i)
hash |= 0
}
return 'img_' + Math.abs(hash)
},
getPostImageStyle(pic, count) {
if (count > 1) return ''
const maxWidth = 560
const maxHeight = 360
const size = this.imageSizeMap[this.getImageKey(pic)]
if (!size || !size.width || !size.height) {
return 'width:' + maxWidth + 'rpx;height:320rpx;max-width:' + maxWidth + 'rpx;max-height:' + maxHeight + 'rpx;'
}
const ratio = size.width / size.height
let width = maxWidth
let height = Math.round(width / ratio)
if (height > maxHeight) {
height = maxHeight
width = Math.round(height * ratio)
}
return 'width:' + width + 'rpx;height:' + height + 'rpx;max-width:' + maxWidth + 'rpx;max-height:' + maxHeight + 'rpx;'
},
getTags(item) {
if (!item) return []
if (Array.isArray(item.tagList)) return item.tagList
if (Array.isArray(item.tags)) return item.tags
if (!item.tags) return []
try { return JSON.parse(item.tags) || [] } catch (e) { return [] }
},
displayName(item) {
return item && item.userName ? item.userName : '半径同学'
},
displayAvatar(item) {
return item && item.userAvatar ? item.userAvatar : this.defaultAvatar
},
canUsePartnerChat(item) {
return !!(item && item.postType === 'partner' && item.partnerType !== 'game')
},
getReactionCount(item, emoji) {
if (!item || !item.reactionCounts) return 0
return Number(item.reactionCounts[emoji] || 0)
},
stripHtml(html = '') {
return String(html).replace(/<[^>]+>/g, '')
},
previewImages(item, index) {
const urls = this.getImages(item)
if (!urls.length) return
uni.previewImage({ urls, current: urls[index] || urls[0] })
},
goDetail(item) {
uni.navigateTo({ url: '/package1/post/postDetail?id=' + item.id + '&source=campus' })
},
goPublish() {
uni.navigateTo({ url: '/package1/campus/campusRelease?type=' + (this.activeCategory === 'wall' || this.activeCategory === 'mine' ? 'wall' : this.activeCategory) })
},
async goChat(item) {
if (!this.canUsePartnerChat(item)) return
const ok = await this.ensureVerified('联系校园圈搭子')
if (!ok) return
createCampusChatSession(item.id).then(session => {
if (!session || !session.sessionId) return
uni.navigateTo({ url: '/package1/campus/campusChat?sessionId=' + session.sessionId })
})
},
async react(item, emoji) {
const key = item.id + ':' + emoji
const now = Date.now()
if (this.reactionLocks[key] && now - this.reactionLocks[key] < 1200) return
this.reactionLocks[key] = now
this.$set(this.reactionEffects, key, true)
this.playReactionBurst(emoji)
setTimeout(() => {
this.$delete(this.reactionEffects, key)
}, 680)
toggleCampusLike({ targetType: 'reaction', targetId: item.id, reaction: emoji }).then(res => {
const counts = item.reactionCounts || {}
if (res && res.reactionCount !== undefined && res.reactionCount !== null) {
this.$set(counts, emoji, Number(res.reactionCount) || 0)
this.$set(item, 'reactionCounts', counts)
} else if (res) {
const nextCount = Math.max(0, Number(counts[emoji] || 0) + (res.liked === false ? -1 : 1))
this.$set(counts, emoji, nextCount)
this.$set(item, 'reactionCounts', counts)
}
if (res && res.liked !== false) {
item.likeCount = Number(item.likeCount || 0) + 1
}
})
},
playReactionBurst(emoji) {
const burstId = Date.now() + '-' + (++this.reactionBurstSeq)
const tracks = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve']
const emojis = [emoji, emoji, emoji, emoji, ...this.reactions]
this.reactionBurstEffects = tracks.map((name, index) => ({
id: burstId + '-' + index,
emoji: emojis[index % emojis.length],
className: 'page-reaction-float--' + name,
style: `left:${4 + ((index * 47) % 92)}%; animation-delay:${index * 72}ms;`
}))
if (this.reactionBurstTimer) clearTimeout(this.reactionBurstTimer)
this.reactionBurstTimer = setTimeout(() => {
if (this.reactionBurstEffects.length && String(this.reactionBurstEffects[0].id).indexOf(burstId) === 0) {
this.reactionBurstEffects = []
}
this.reactionBurstTimer = null
}, 3100)
},
async ensureVerified(actionText) {
const profile = await getIeProfile().catch(() => null)
return ensureIeVerifiedBeforeAction({
profile: profile || {},
actionText,
authDialog: { badge: '校园圈', title: '实名认证', desc: actionText + '前需完成实名认证' },
reload: getIeProfile
})
},
formatBadge(count) {
const value = Number(count) || 0
return value > 99 ? '99+' : value
}
}
}
</script>
<style scoped>
.campus-page { height: 100vh; overflow: hidden; background: radial-gradient(circle at 90rpx 30rpx, rgba(198, 247, 231, .65), transparent 250rpx), linear-gradient(180deg, #f1fffb 0, #fff 720rpx, #f8fbf9 100%); position: relative; color: #161d1a; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; }
.page-scroll { width: 100%; height: 100%; box-sizing: border-box; }
.header { position: fixed; left: 0; right: 0; top: 0; z-index: 900; padding: 20rpx 28rpx 18rpx; background: linear-gradient(180deg, rgba(237, 255, 249, .99), rgba(249, 255, 252, .98)); box-shadow: 0 8rpx 24rpx rgba(33, 111, 87, .06); box-sizing: border-box; }
.brand-line { height: 58rpx; display: flex; align-items: center; }
.brand { color: #12362e; font-size: 39rpx; font-weight: 900; letter-spacing: -1rpx; }
.eating { margin-left: 13rpx; padding: 7rpx 17rpx; border-radius: 24rpx; background: #31c78a; color: #fff; font-size: 19rpx; font-weight: 800; }
.subtitle { margin-top: 2rpx; color: #69726f; font-size: 19rpx; letter-spacing: 1rpx; }
.search-line { display: flex; align-items: center; gap: 16rpx; margin-top: 20rpx; }
.search-box { position: relative; flex: 1; min-width: 0; height: 66rpx; display: flex; align-items: center; padding-left: 20rpx; border: 2rpx solid #57d4ac; border-radius: 36rpx; background: rgba(255,255,255,.92); box-shadow: 0 9rpx 22rpx rgba(35, 145, 111, .08); }
.search-input { flex: 1; height: 62rpx; margin-left: 9rpx; color: #26302c; font-size: 22rpx; line-height: 62rpx; }
.placeholder { color: #a0a6a4; }
.search-button { width: 94rpx; height: 56rpx; margin-right: 5rpx; border-radius: 30rpx; background: linear-gradient(135deg, #2bd398, #0db779); box-shadow: 0 7rpx 15rpx rgba(20, 182, 123, .24); color: #fff; font-size: 21rpx; font-weight: 800; line-height: 56rpx; text-align: center; }
.category-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14rpx; margin-top: 18rpx; padding: 10rpx 24rpx 20rpx; }
.category-card { position: relative; height: 170rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 24rpx; background: linear-gradient(145deg, #ffffff, #edf5ff); text-align: center; box-shadow: 0 10rpx 24rpx rgba(35, 74, 61, 0.075); border: 2rpx solid transparent; box-sizing: border-box; }
.category-card.active { border-color: rgba(34, 191, 134, 0.55); box-shadow: 0 14rpx 30rpx rgba(34, 191, 134, 0.14); transform: translateY(-3rpx); }
.category-badge { position: absolute; right: 8rpx; top: 8rpx; min-width: 32rpx; height: 32rpx; line-height: 32rpx; padding: 0 8rpx; border-radius: 999rpx; background: #ff5b5b; color: #fff; font-size: 19rpx; box-sizing: border-box; }
.hot-category-icon { width: 84rpx; height: 84rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 4rpx; border-radius: 26rpx; background: rgba(255, 255, 255, 0.72); box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 0.92), 0 9rpx 18rpx rgba(33, 111, 87, 0.08); }
.hot-category-img { width: 88rpx; height: 88rpx; border-radius: 22rpx; overflow: hidden; }
.hot-category-name { color: #173f36; font-size: 21rpx; font-weight: 900; line-height: 30rpx; }
.hot-category-desc { margin-top: 5rpx; color: #7d8e88; font-size: 17rpx; font-weight: 700; line-height: 23rpx; }
.sub-category-scroll { min-height: 62rpx; display: flex; align-items: center; gap: 18rpx; overflow-x: auto; padding: 12rpx 24rpx 4rpx; box-sizing: border-box; }
.sub-category { display: inline-block; margin-right: 18rpx; padding: 12rpx 4rpx; color: #6c7d78; font-size: 25rpx; border-bottom: 5rpx solid transparent; }
.sub-category.active { color: #18b892; font-weight: 800; border-bottom-color: #18d0a4; }
.filter-panel { margin: 18rpx 24rpx 0; padding: 20rpx; background: #fff; border-radius: 28rpx; box-shadow: 0 10rpx 30rpx rgba(41, 80, 73, .08); }
.filter-head { display: flex; justify-content: space-between; align-items: center; font-weight: 700; color: #183a32; }
.match-btn { padding: 10rpx 18rpx; border-radius: 999rpx; background: #232f3d; color: #fff; font-size: 22rpx; }
.match-btn.matching { background: #34c7a2; }
.match-animation { display: flex; gap: 12rpx; justify-content: center; padding: 18rpx 0; }
.match-animation view { width: 18rpx; height: 18rpx; border-radius: 50%; background: #34c7a2; animation: pulse 1s infinite alternate; }
.match-animation view:nth-child(2) { animation-delay: .18s; }
.match-animation view:nth-child(3) { animation-delay: .36s; }
.match-popup-mask { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; background: rgba(14, 28, 25, .38); padding: 40rpx 28rpx 180rpx; box-sizing: border-box; }
.match-popup { width: 100%; max-height: 78vh; padding: 30rpx; border-radius: 34rpx; background: linear-gradient(180deg, #ffffff, #f3fffb); box-shadow: 0 22rpx 58rpx rgba(22, 69, 58, .18); box-sizing: border-box; overflow-y: auto; }
.match-popup-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; }
.match-popup-title { color: #173a32; font-size: 32rpx; font-weight: 900; }
.match-popup-sub { margin-top: 8rpx; color: #788984; font-size: 23rpx; line-height: 1.45; }
.match-close { flex: 0 0 54rpx; width: 54rpx; height: 54rpx; line-height: 50rpx; text-align: center; border-radius: 50%; background: #eef6f3; color: #657772; font-size: 34rpx; }
.match-field-title { margin-top: 24rpx; color: #213d37; font-size: 25rpx; font-weight: 800; }
.match-input { margin-top: 12rpx; width: 100%; height: 76rpx; box-sizing: border-box; padding: 0 22rpx; border-radius: 22rpx; background: #f5fbf9; color: #263d38; font-size: 26rpx; border: 1rpx solid #e2efeb; }
.match-tag-wrap { margin-top: 14rpx; display: flex; flex-wrap: wrap; gap: 12rpx; max-height: 190rpx; overflow-y: auto; }
.match-tag { padding: 12rpx 20rpx; border-radius: 999rpx; background: #eef6f3; color: #526761; font-size: 23rpx; font-weight: 700; }
.match-tag.active { background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; box-shadow: 0 8rpx 18rpx rgba(38, 196, 169, .18); }
.match-actions { display: flex; gap: 18rpx; margin-top: 30rpx; }
.match-cancel, .match-start { flex: 1; height: 82rpx; line-height: 82rpx; text-align: center; border-radius: 999rpx; font-size: 27rpx; font-weight: 900; }
.match-cancel { background: #eef3f1; color: #526761; }
.match-start { background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; box-shadow: 0 12rpx 28rpx rgba(38, 196, 169, .26); }
.creating-mask { position: fixed; inset: 0; z-index: 90; display: flex; align-items: center; justify-content: center; background: rgba(12, 24, 22, .48); backdrop-filter: blur(6rpx); }
.creating-card { width: 500rpx; padding: 46rpx 34rpx; border-radius: 36rpx; background: radial-gradient(circle at 50% 24%, rgba(64, 225, 176, .22), transparent 150rpx), #fff; text-align: center; box-shadow: 0 28rpx 70rpx rgba(12, 50, 42, .18); }
.creating-orbit { position: relative; width: 190rpx; height: 190rpx; margin: 0 auto 24rpx; border-radius: 50%; border: 2rpx solid rgba(44, 202, 161, .26); animation: matchOrbitRotate 1.5s linear infinite; }
.creating-core { position: absolute; left: 50%; top: 50%; width: 92rpx; height: 92rpx; margin-left: -46rpx; margin-top: -46rpx; border-radius: 50%; background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; font-size: 34rpx; font-weight: 900; line-height: 92rpx; box-shadow: 0 12rpx 28rpx rgba(38, 196, 169, .28); animation: matchCorePulse .75s ease-in-out infinite alternate; }
.creating-dot { position: absolute; border-radius: 50%; background: #41d9b2; box-shadow: 0 0 18rpx rgba(65, 217, 178, .48); }
.dot-a { left: 18rpx; top: 38rpx; width: 22rpx; height: 22rpx; }
.dot-b { right: 14rpx; top: 84rpx; width: 16rpx; height: 16rpx; background: #74e6ff; }
.dot-c { left: 86rpx; bottom: 10rpx; width: 18rpx; height: 18rpx; background: #ffd36f; }
.creating-title { color: #173a32; font-size: 31rpx; font-weight: 900; }
.creating-desc { margin-top: 10rpx; color: #71837d; font-size: 24rpx; }
.tag-scroll { white-space: nowrap; margin-top: 16rpx; }
.tag { display: inline-block; padding: 10rpx 20rpx; border-radius: 999rpx; margin-right: 12rpx; background: #f0f6f4; color: #51645f; font-size: 23rpx; }
.tag.active { background: #34c7a2; color: #fff; }
.section-head { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 28rpx 14rpx; }
.section-title { font-size: 34rpx; font-weight: 800; color: #1d352f; }
.section-sub { margin-top: 6rpx; font-size: 23rpx; color: #83918d; }
.live-dot { display: flex; align-items: center; gap: 8rpx; font-size: 22rpx; color: #5a716c; }
.live-dot text { width: 12rpx; height: 12rpx; border-radius: 50%; background: #34c7a2; }
.post-list { padding: 0 24rpx; }
.post-card { margin-bottom: 20rpx; padding: 24rpx; background: #fff; border-radius: 30rpx; box-shadow: 0 12rpx 34rpx rgba(38, 76, 69, .09); }
.post-card--image { background: linear-gradient(180deg, #fff 0%, #fbfffd 100%); }
.post-top { display: flex; align-items: center; }
.avatar { width: 62rpx; height: 62rpx; border-radius: 50%; background: #dff8f0; color: #257c69; text-align: center; line-height: 62rpx; font-weight: 700; }
.author-main { flex: 1; margin-left: 14rpx; min-width: 0; }
.author { font-size: 26rpx; font-weight: 700; color: #263d38; }
.time { margin-top: 4rpx; font-size: 21rpx; color: #93a09c; }
.partner-action { padding: 10rpx 18rpx; border-radius: 999rpx; background: #34c7a2; color: #fff; font-size: 23rpx; }
.title { margin-top: 20rpx; font-size: 31rpx; font-weight: 800; color: #162d28; }
.content { margin-top: 12rpx; font-size: 26rpx; color: #51645f; line-height: 1.6; }
.image-grid { display: grid; gap: 8rpx; margin-top: 18rpx; }
.image-grid--1 { display: flex; align-items: center; justify-content: flex-start; max-width: 560rpx; max-height: 360rpx; overflow: hidden; border-radius: 20rpx; background: transparent; }
.image-grid--2 { grid-template-columns: repeat(2, 1fr); }
.image-grid--3 { grid-template-columns: repeat(3, 1fr); }
.image-grid image { width: 100%; height: 190rpx; max-width: 100%; border-radius: 20rpx; background: #eef5f3; }
.image-grid--1 image { max-width: 560rpx; max-height: 360rpx; background: transparent; }
.tag-row { margin-top: 16rpx; display: flex; flex-wrap: wrap; gap: 10rpx; }
.tag-row text { padding: 8rpx 16rpx; border-radius: 999rpx; background: #eefaf6; color: #2f9a81; font-size: 22rpx; }
.partner-info { margin-top: 14rpx; display: flex; flex-wrap: wrap; gap: 12rpx; color: #71807c; font-size: 22rpx; }
.contact-row { margin-top: 14rpx; display: flex; flex-wrap: wrap; gap: 10rpx; }
.contact-row text { padding: 8rpx 14rpx; border-radius: 999rpx; background: #f0fbf7; color: #2b8d76; font-size: 22rpx; }
.post-footer { margin-top: 18rpx; display: flex; justify-content: space-between; align-items: flex-end; gap: 12rpx; }
.meta { color: #889692; font-size: 22rpx; }
.reaction-row { display: flex; gap: 14rpx; overflow: visible; }
.reaction { position: relative; z-index: 1; width: 52rpx; min-height: 62rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; border-radius: 18rpx; background: transparent; transform-origin: center center; transition: background .16s ease, box-shadow .16s ease; overflow: visible; }
.reaction-emoji { font-size: 31rpx; line-height: 34rpx; }
.reaction-count { margin-top: 2rpx; color: #667671; font-size: 18rpx; line-height: 20rpx; font-weight: 700; }
.reaction--active { z-index: 5; background: #fff9dc; box-shadow: 0 12rpx 28rpx rgba(255, 186, 61, .30), 0 0 0 8rpx rgba(255, 207, 88, .16); animation: reactionBoom .68s cubic-bezier(.18, .88, .24, 1.22); }
.reaction--active::after { content: ''; position: absolute; left: 50%; top: 50%; width: 56rpx; height: 56rpx; margin-left: -28rpx; margin-top: -28rpx; border-radius: 50%; border: 4rpx solid rgba(255, 199, 63, .42); animation: reactionRing .68s ease-out; pointer-events: none; }
.empty, .load-state { text-align: center; color: #8a9692; font-size: 24rpx; padding: 30rpx 0; }
.bottom-space { height: 160rpx; }
.page-reaction-effect { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; pointer-events: none; z-index: 99999; }
.page-reaction-float { position: absolute; bottom: -160rpx; width: 86rpx; height: 86rpx; line-height: 86rpx; text-align: center; opacity: 0; transform-origin: center; font-size: 48rpx; filter: drop-shadow(0 18rpx 28rpx rgba(35, 74, 61, .18)); animation-duration: 2600ms; animation-timing-function: cubic-bezier(.14, .66, .22, 1); animation-fill-mode: forwards; }
.page-reaction-float--one { font-size: 66rpx; animation-name: reactionFloatOne; }
.page-reaction-float--two { font-size: 50rpx; animation-name: reactionFloatTwo; }
.page-reaction-float--three { font-size: 74rpx; animation-name: reactionFloatThree; }
.page-reaction-float--four { font-size: 44rpx; animation-name: reactionFloatFour; }
.page-reaction-float--five { font-size: 58rpx; animation-name: reactionFloatFive; }
.page-reaction-float--six { font-size: 42rpx; animation-name: reactionFloatSix; }
.page-reaction-float--seven { font-size: 68rpx; animation-name: reactionFloatSeven; }
.page-reaction-float--eight { font-size: 48rpx; animation-name: reactionFloatEight; }
.page-reaction-float--nine { font-size: 56rpx; animation-name: reactionFloatNine; }
.page-reaction-float--ten { font-size: 46rpx; animation-name: reactionFloatTen; }
.page-reaction-float--eleven { font-size: 62rpx; animation-name: reactionFloatEleven; }
.page-reaction-float--twelve { font-size: 40rpx; animation-name: reactionFloatTwelve; }
.release-fab { position: relative; flex: 0 0 82rpx; width: 82rpx; height: 82rpx; border-radius: 50%; background: radial-gradient(circle at 35% 28%, #8cf7a9 0%, #49dc7b 42%, #24ba5b 100%); color: #fff; box-shadow: 0 0 0 8rpx rgba(119, 232, 146, .18), 0 0 22rpx 8rpx rgba(80, 225, 118, .26), 0 10rpx 18rpx rgba(18, 151, 76, .20); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 3; animation: fabFloat 3s ease-in-out infinite; overflow: visible; border: 3rpx solid rgba(239, 255, 241, .78); }
.release-fab::before { content: ''; position: absolute; inset: -13rpx; border-radius: 50%; background: radial-gradient(circle, rgba(148, 255, 167, .34) 0%, rgba(115, 238, 144, .15) 46%, rgba(115, 238, 144, 0) 72%); animation: fabGlow 2s ease-in-out infinite; z-index: -1; }
.release-fab::after { content: ''; position: absolute; inset: 8rpx; border-radius: 50%; border: 2rpx solid rgba(255,255,255,.42); box-shadow: inset 0 6rpx 12rpx rgba(255,255,255,.18); pointer-events: none; }
.fab-camera { position: relative; width: 30rpx; height: 22rpx; border-radius: 7rpx; background: #167c3f; box-shadow: inset 0 -3rpx 6rpx rgba(0,0,0,.10); }
.fab-camera::before { content: ''; position: absolute; left: 9rpx; top: -6rpx; width: 13rpx; height: 8rpx; border-radius: 6rpx 6rpx 0 0; background: #167c3f; }
.fab-camera::after { content: ''; position: absolute; right: 4rpx; top: 5rpx; width: 5rpx; height: 5rpx; border-radius: 50%; background: rgba(255,255,255,.75); }
.fab-camera-lens { position: absolute; left: 9rpx; top: 6rpx; width: 12rpx; height: 12rpx; border-radius: 50%; background: #78ee99; box-shadow: 0 0 0 3rpx rgba(255,255,255,.24); }
.release-fab text { position: relative; margin-top: 3rpx; font-size: 18rpx; line-height: 22rpx; font-weight: 800; text-shadow: 0 2rpx 6rpx rgba(0, 101, 49, .22); }
.fab-spark { position: absolute; background: #96f4a6; transform: rotate(45deg); animation: sparkBlink 1.8s ease-in-out infinite; }
.fab-spark--one { right: -6rpx; top: -12rpx; width: 18rpx; height: 18rpx; }
.fab-spark--two { right: 22rpx; top: -30rpx; width: 10rpx; height: 10rpx; animation-delay: .45s; }
@keyframes pulse { from { opacity: .35; transform: scale(.8); } to { opacity: 1; transform: scale(1.15); } }
@keyframes matchOrbitRotate { to { transform: rotate(360deg); } }
@keyframes matchCorePulse { from { transform: scale(.92); } to { transform: scale(1.08); } }
@keyframes fabGlow { 0%, 100% { opacity: .55; transform: scale(.94); } 50% { opacity: 1; transform: scale(1.08); } }
@keyframes fabFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6rpx); } }
@keyframes sparkBlink { 0%, 100% { opacity: .45; transform: rotate(45deg) scale(.85); } 50% { opacity: 1; transform: rotate(45deg) scale(1.16); } }
@keyframes reactionBoom {
0% { transform: translateY(0) scale(1) rotate(0); }
22% { transform: translateY(-18rpx) scale(2.25) rotate(-8deg); }
48% { transform: translateY(-8rpx) scale(1.55) rotate(6deg); }
68% { transform: translateY(-12rpx) scale(1.82) rotate(-3deg); }
100% { transform: translateY(0) scale(1) rotate(0); }
}
@keyframes reactionRing {
0% { opacity: .95; transform: scale(.7); }
70% { opacity: .35; transform: scale(1.75); }
100% { opacity: 0; transform: scale(2.1); }
}
@keyframes reactionFloatOne {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.68) rotate(-8deg); }
14% { opacity: .92; transform: translate3d(-18rpx, -92rpx, 0) scale(1) rotate(5deg); }
62% { opacity: .56; transform: translate3d(-72rpx, -370rpx, 0) scale(.94) rotate(-12deg); }
100% { opacity: 0; transform: translate3d(-112rpx, -650rpx, 0) scale(.72) rotate(-22deg); }
}
@keyframes reactionFloatTwo {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.62) rotate(6deg); }
16% { opacity: .84; transform: translate3d(28rpx, -112rpx, 0) scale(.92) rotate(-6deg); }
64% { opacity: .48; transform: translate3d(96rpx, -350rpx, 0) scale(.82) rotate(12deg); }
100% { opacity: 0; transform: translate3d(146rpx, -600rpx, 0) scale(.62) rotate(22deg); }
}
@keyframes reactionFloatThree {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.64) rotate(-4deg); }
12% { opacity: .78; transform: translate3d(8rpx, -86rpx, 0) scale(.94) rotate(7deg); }
60% { opacity: .42; transform: translate3d(-36rpx, -420rpx, 0) scale(.78) rotate(-12deg); }
100% { opacity: 0; transform: translate3d(-68rpx, -710rpx, 0) scale(.58) rotate(-24deg); }
}
@keyframes reactionFloatFour {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.58) rotate(12deg); }
18% { opacity: .8; transform: translate3d(-26rpx, -124rpx, 0) scale(.86) rotate(-6deg); }
66% { opacity: .44; transform: translate3d(-112rpx, -320rpx, 0) scale(.72) rotate(14deg); }
100% { opacity: 0; transform: translate3d(-166rpx, -560rpx, 0) scale(.54) rotate(26deg); }
}
@keyframes reactionFloatFive {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.64) rotate(-10deg); }
14% { opacity: .86; transform: translate3d(34rpx, -96rpx, 0) scale(.96) rotate(6deg); }
62% { opacity: .48; transform: translate3d(68rpx, -380rpx, 0) scale(.78) rotate(-10deg); }
100% { opacity: 0; transform: translate3d(104rpx, -650rpx, 0) scale(.58) rotate(-20deg); }
}
@keyframes reactionFloatSix {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.52) rotate(8deg); }
20% { opacity: .76; transform: translate3d(6rpx, -132rpx, 0) scale(.8) rotate(-8deg); }
68% { opacity: .4; transform: translate3d(44rpx, -290rpx, 0) scale(.64) rotate(12deg); }
100% { opacity: 0; transform: translate3d(70rpx, -500rpx, 0) scale(.48) rotate(22deg); }
}
@keyframes reactionFloatSeven {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.62) rotate(-8deg); }
13% { opacity: .78; transform: translate3d(-28rpx, -82rpx, 0) scale(.88) rotate(6deg); }
58% { opacity: .42; transform: translate3d(-86rpx, -390rpx, 0) scale(.72) rotate(-12deg); }
100% { opacity: 0; transform: translate3d(-132rpx, -680rpx, 0) scale(.52) rotate(-22deg); }
}
@keyframes reactionFloatEight {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.56) rotate(10deg); }
18% { opacity: .8; transform: translate3d(38rpx, -118rpx, 0) scale(.84) rotate(-8deg); }
64% { opacity: .44; transform: translate3d(122rpx, -338rpx, 0) scale(.68) rotate(14deg); }
100% { opacity: 0; transform: translate3d(178rpx, -580rpx, 0) scale(.5) rotate(26deg); }
}
@keyframes reactionFloatNine {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.55) rotate(-14deg); }
16% { opacity: .82; transform: translate3d(-46rpx, -110rpx, 0) scale(.88) rotate(8deg); }
62% { opacity: .46; transform: translate3d(-150rpx, -360rpx, 0) scale(.68) rotate(-10deg); }
100% { opacity: 0; transform: translate3d(-210rpx, -620rpx, 0) scale(.5) rotate(-24deg); }
}
@keyframes reactionFloatTen {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.52) rotate(14deg); }
18% { opacity: .74; transform: translate3d(50rpx, -96rpx, 0) scale(.82) rotate(-8deg); }
64% { opacity: .38; transform: translate3d(170rpx, -300rpx, 0) scale(.62) rotate(12deg); }
100% { opacity: 0; transform: translate3d(230rpx, -540rpx, 0) scale(.46) rotate(24deg); }
}
@keyframes reactionFloatEleven {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.58) rotate(-6deg); }
14% { opacity: .82; transform: translate3d(4rpx, -136rpx, 0) scale(.9) rotate(6deg); }
62% { opacity: .42; transform: translate3d(-24rpx, -450rpx, 0) scale(.7) rotate(-10deg); }
100% { opacity: 0; transform: translate3d(-48rpx, -760rpx, 0) scale(.48) rotate(-20deg); }
}
@keyframes reactionFloatTwelve {
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(.48) rotate(8deg); }
18% { opacity: .72; transform: translate3d(22rpx, -150rpx, 0) scale(.72) rotate(-8deg); }
66% { opacity: .36; transform: translate3d(82rpx, -260rpx, 0) scale(.56) rotate(12deg); }
100% { opacity: 0; transform: translate3d(126rpx, -470rpx, 0) scale(.42) rotate(22deg); }
}
</style>

77
components/tab-bar/tab-bar.vue

@ -7,6 +7,9 @@
<view class="red-dot" v-if="index == 1 && indexWorkerCount > 0">
{{formatBadgeCount(indexWorkerCount)}}
</view>
<view class="red-dot" v-if="index == 2 && indexCampusCount > 0">
{{formatBadgeCount(indexCampusCount)}}
</view>
<view class="red-dot" v-if="index == 3 && indexFishCount > 0">
{{formatBadgeCount(indexFishCount)}}
</view>
@ -16,10 +19,10 @@
<view class="img-box" v-if="index != 2" :class="item.shaking?'shaking':''">
<img :src="item.isCheck?item.tabIconSelect:item.tabIcon" alt="" />
</view>
<view class="big-img-box" v-if="index == 2" :class="item.shaking?'shaking':''">
<img :src="item.isCheck?item.tabIconSelect:item.tabIcon" alt="" />
<view class="img-box uni-tab-icon" v-if="index == 2" :class="item.shaking?'shaking':''">
<uni-icons type="pyq" size="25" :color="item.isCheck?'#48D1CC':'#30363d'"></uni-icons>
</view>
<view class="text-box" v-if="index != 2" :style="{color:item.isCheck?'#48D1CC':'#000'}">
<view class="text-box" :style="{color:item.isCheck?'#48D1CC':'#000'}">
{{item.tabName}}
</view>
</view>
@ -43,9 +46,9 @@
isCheck: false,
shaking:false
}, {
tabName: '发布',
tabIcon: '/static/images/tabbar/fabu222.png',
tabIconSelect: '/static/images/tabbar/shouye.png',
tabName: '校园圈',
tabIcon: '/static/images/tabbar/xiaoyuan.png',
tabIconSelect: '',
isCheck: false,
shaking:false
}, {
@ -63,9 +66,13 @@
}],
indexZhipaiCount:0,
indexWorkerCount:0,
indexCampusCount:0,
indexFishCount:0,
indexMyCount:0,
countRefreshId:0
countRefreshId:0,
countLoading:false,
countPromise:null,
lastCountAt:0
}
},
props: {
@ -95,11 +102,13 @@
mounted() {
uni.$on('deliveryStatusCountsChange', this.setDeliveryStatusCounts)
uni.$on('fishUnreadChange', this.setFishUnreadCounts)
uni.$on('campusUnreadChange', this.setCampusUnreadCounts)
this.setDeliveryStatusCounts(uni.getStorageSync('deliveryStatusCounts') || {})
},
beforeDestroy() {
uni.$off('deliveryStatusCountsChange', this.setDeliveryStatusCounts)
uni.$off('fishUnreadChange', this.setFishUnreadCounts)
uni.$off('campusUnreadChange', this.setCampusUnreadCounts)
},
methods: {
hasLogin() {
@ -124,6 +133,17 @@
setFishUnreadCounts(summary) {
if (summary && summary.fishUnread !== undefined) {
this.indexFishCount = Number(summary.fishUnread) || 0
if (summary.campusUnread !== undefined || summary.myCircleUnread !== undefined) {
this.indexCampusCount = Number(summary.campusUnread || summary.myCircleUnread) || 0
}
this.$forceUpdate()
return
}
this.getWorkerCounts()
},
setCampusUnreadCounts(summary) {
if (summary && (summary.campusUnread !== undefined || summary.myCircleUnread !== undefined)) {
this.indexCampusCount = Number(summary.campusUnread || summary.myCircleUnread) || 0
this.$forceUpdate()
return
}
@ -139,7 +159,6 @@
for (let i = 0; i < this.tabList.length; i++) {
if (index == i) {
this.$emit('tab-index', i);
if(index == 2) return
this.getWorkerCounts()
this.tabList[i].isCheck = true
if (this.tabList[i].shaking) return
@ -156,12 +175,24 @@
}
},
getWorkerCounts(){
const now = Date.now()
if (this.countLoading && this.countPromise) {
return this.countPromise
}
if (this.countPromise && now - this.lastCountAt < 600) {
return this.countPromise
}
this.countLoading = true
this.lastCountAt = now
const refreshId = ++this.countRefreshId
this.indexZhipaiCount = 0
this.indexWorkerCount = 0
this.indexCampusCount = 0
this.indexFishCount = 0
this.indexMyCount = 0
if (!this.hasLogin()) {
this.countLoading = false
this.countPromise = null
return Promise.resolve()
}
let deliveryStatusCounts = {
@ -183,6 +214,8 @@
if (res && res.success !== false) {
const result = res.result || res
this.indexFishCount = Number(result.fishUnread) || 0
this.indexCampusCount = Number(result.campusUnread || result.myCircleUnread) || 0
uni.$emit('campusUnreadChange', result)
}
}).catch(() => {}))
if(worker){
@ -269,7 +302,7 @@
}else{
uni.setStorageSync('shopCounts', 0)
}
return Promise.all(tasks).then(() => {
const countPromise = Promise.all(tasks).then(() => {
if (refreshId != this.countRefreshId) return
this.indexZhipaiCount = workerResult.zhipaiCount
this.indexWorkerCount = workerResult.workerTabCount
@ -283,7 +316,16 @@
shopCounts: shopCounts
})
this.$forceUpdate()
}).finally(() => {
if (refreshId == this.countRefreshId) {
this.countLoading = false
setTimeout(() => {
if (this.countPromise === countPromise) this.countPromise = null
}, 600)
}
})
this.countPromise = countPromise
return countPromise
}
}
}
@ -355,14 +397,11 @@
pointer-events: none;
-webkit-tap-highlight-color: transparent;
}
.big-img-box img{
width: 80%;
height: 90%;
background-size: 100%;
margin: -20rpx 0 0 10%;
display: block;
pointer-events: none;
-webkit-tap-highlight-color: transparent;
.uni-tab-icon {
display: flex;
align-items: center;
justify-content: center;
}
@keyframes simple-scale-shake {
@ -411,8 +450,4 @@
font-family: 'PingFang HK';
font-weight: bold;
}
.big-img-box{
width: 100%;
height: 100%;
}
</style>
Loading…
Cancel
Save