You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
820 lines
42 KiB
820 lines
42 KiB
|
1 day ago
|
<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>
|