﻿<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>
		<scroll-view
			class="page-scroll"
			scroll-y
			refresher-enabled
			:refresher-triggered="refreshing"
			:style="{ paddingTop: headerHeight + 'px' }"
			:lower-threshold="120"
			@refresherrefresh="onPullRefresh"
			@scrolltolower="loadMore"
		>
			<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="getReleaseChannelIcon(item)" 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 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 || ''}} · <text class="time-category">{{item.categoryName || '本校'}}</text></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 === 1" class="image-grid image-grid--1">
						<image
							:src="getImages(item)[0]"
							mode="aspectFit"
							:style="getPostImageStyle(getImages(item)[0], 1)"
							@load="onPostImageLoad(getImages(item)[0], $event)"
							@tap.stop="previewImages(item, 0)"
						></image>
					</view>
					<scroll-view v-else-if="getImages(item).length > 1" scroll-x class="image-scroll" @tap.stop>
						<view class="image-scroll-inner">
							<image
								class="image-scroll-item"
								v-for="(pic, index) in getImages(item)"
								:key="pic"
								:src="pic"
								mode="aspectFill"
								@tap.stop="previewImages(item, index)"
							></image>
						</view>
					</scroll-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.targetCount ? 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' }
	]
	const RELEASE_ICON_CACHE_KEY = 'release_channel_icon_cache_v1'
	const RELEASE_ICON_CACHE_TTL = 30 * 24 * 60 * 60 * 1000
	const RELEASE_ICON_CACHE_LIMIT = 12
	const releaseIconDownloading = {}

	export default {
		data() {
			return {
				statusBarHeight: 24,
				channels: CHANNELS,
				releaseChannelIcons: {},
				activeCategory: 'wall',
				activeCategoryId: '',
				keyword: '',
				selectedTag: '',
				postSortType: 'hot',
				posts: [],
				pageNum: 1,
				pageSize: 10,
				totalPages: 1,
				loading: false,
				refreshing: 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(204) : 102)
			},
			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 '支持关键词和热门标签模糊匹配'
			}
		},
		created() {
			this.applyCachedReleaseChannelIcons()
		},
		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: {
			getReleaseChannelIcon(item) {
				if (!item) return ''
				return this.releaseChannelIcons[item.tone] || item.icon || ''
			},
			getReleaseIconCache() {
				const cache = uni.getStorageSync(RELEASE_ICON_CACHE_KEY)
				return cache && typeof cache === 'object' ? cache : {}
			},
			saveReleaseIconCache(cache) {
				uni.setStorageSync(RELEASE_ICON_CACHE_KEY, cache)
			},
			isSavedFileExists(filePath) {
				if (!filePath || filePath.indexOf('__tmp__') !== -1) return false
				try {
					const fs = (uni.getFileSystemManager && uni.getFileSystemManager())
						|| (typeof wx !== 'undefined' && wx.getFileSystemManager ? wx.getFileSystemManager() : null)
					if (fs && fs.accessSync) {
						fs.accessSync(filePath)
						return true
					}
				} catch (e) {
					return false
				}
				return true
			},
			isReleaseIconCacheValid(cacheItem) {
				return !!(cacheItem
					&& Date.now() - Number(cacheItem.savedAt || 0) < RELEASE_ICON_CACHE_TTL
					&& this.isSavedFileExists(cacheItem.path))
			},
			applyCachedReleaseChannelIcons() {
				const cache = this.getReleaseIconCache()
				CHANNELS.forEach((item) => {
					const imageUrl = item.icon
					const cacheItem = cache[imageUrl]
					if (this.isReleaseIconCacheValid(cacheItem)) {
						this.$set(this.releaseChannelIcons, item.tone, cacheItem.path)
						return
					}
					if (cacheItem && cacheItem.path) {
						delete cache[imageUrl]
					}
					this.$set(this.releaseChannelIcons, item.tone, imageUrl)
					this.cacheReleaseChannelIcon(item.tone, imageUrl)
				})
				this.pruneReleaseIconCache(cache, CHANNELS.map(item => item.icon))
			},
			cacheReleaseChannelIcon(tone, imageUrl) {
				if (releaseIconDownloading[imageUrl]) return
				releaseIconDownloading[imageUrl] = true
				uni.downloadFile({
					url: imageUrl,
					success: (downloadRes) => {
						if (downloadRes.statusCode !== 200 || !downloadRes.tempFilePath) {
							this.$set(this.releaseChannelIcons, tone, imageUrl)
							delete releaseIconDownloading[imageUrl]
							return
						}
						uni.saveFile({
							tempFilePath: downloadRes.tempFilePath,
							success: (saveRes) => {
								const cache = this.getReleaseIconCache()
								const oldCacheItem = cache[imageUrl]
								if (oldCacheItem && oldCacheItem.path && oldCacheItem.path !== saveRes.savedFilePath) {
									uni.removeSavedFile({
										filePath: oldCacheItem.path,
										fail: () => {}
									})
								}
								cache[imageUrl] = {
									path: saveRes.savedFilePath,
									savedAt: Date.now()
								}
								this.saveReleaseIconCache(cache)
								this.$set(this.releaseChannelIcons, tone, saveRes.savedFilePath)
							},
							fail: () => {
								this.$set(this.releaseChannelIcons, tone, imageUrl)
							},
							complete: () => {
								delete releaseIconDownloading[imageUrl]
							}
						})
					},
					fail: () => {
						this.$set(this.releaseChannelIcons, tone, imageUrl)
						delete releaseIconDownloading[imageUrl]
					},
					complete: (downloadRes) => {
						if (!downloadRes || downloadRes.statusCode !== 200) {
							this.$set(this.releaseChannelIcons, tone, imageUrl)
							delete releaseIconDownloading[imageUrl]
						}
					}
				})
			},
			pruneReleaseIconCache(cache, activeUrls) {
				const activeUrlMap = activeUrls.reduce((result, url) => {
					result[url] = true
					return result
				}, {})
				const now = Date.now()
				Object.keys(cache).forEach((url) => {
					const item = cache[url]
					const isExpired = !this.isReleaseIconCacheValid(item)
					if (isExpired && !activeUrlMap[url]) {
						item && item.path && uni.removeSavedFile({
							filePath: item.path,
							fail: () => {}
						})
						delete cache[url]
					}
				})
				const urls = Object.keys(cache).sort((a, b) => Number(cache[a].savedAt || now) - Number(cache[b].savedAt || now))
				while (urls.length > RELEASE_ICON_CACHE_LIMIT) {
					const url = urls.shift()
					if (activeUrlMap[url]) {
						urls.push(url)
						if (urls.every(itemUrl => activeUrlMap[itemUrl])) break
						continue
					}
					const item = cache[url]
					item && item.path && uni.removeSavedFile({
						filePath: item.path,
						fail: () => {}
					})
					delete cache[url]
				}
				this.saveReleaseIconCache(cache)
			},
			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 (tone === 'mine') {
					uni.navigateTo({ url: '/package1/campus/myCampusPosts' })
					return
				}
				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()
			},
			onPullRefresh() {
				if (this.refreshing) return
				if (this.loading) return
				this.refreshing = true
				this.loadUnread()
				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
					this.refreshing = 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 false
			},
			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; 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; }
	.time-category { color: #56cfa4; font-weight: 800; }
	.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; }
	.image-scroll { width: 100%; margin-top: 18rpx; white-space: nowrap; }
	.image-scroll-inner { display: inline-flex; gap: 8rpx; padding-right: 4rpx; }
	.image-scroll-item { flex: 0 0 208rpx; width: 208rpx; height: 190rpx; border-radius: 20rpx; background: #eef5f3; }
	.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>
