<template>
	<view class="room-page">
		<view class="glow glow-a"></view>
		<view class="glow glow-b"></view>
		<view class="nav" :style="{ paddingTop: menuButtonInfo.top + 'px' }">
			<view class="back" @tap="back">‹</view>
			<view class="nav-avatar-wrap" @tap="openTargetProfile">
				<image class="nav-avatar-img" v-if="companion.avatarUrl" :src="companion.avatarUrl" mode="aspectFill"></image>
				<view class="nav-avatar" v-else>{{ companion.avatar }}</view>
			</view>
			<view class="room-user" @tap="openTargetProfile">
				<view class="room-name">{{ companion.name }}</view>
				<view class="room-meta">{{ roomModeText }} · 可以随时回来继续聊</view>
			</view>
			<view class="more" @tap="showSafety = true">···</view>
		</view>

		<view class="radio-room">
			<view class="radio-orbit"></view>
			<view class="radio-core">FM</view>
			<view class="radio-copy">
				<view class="radio-title">电台陪伴中</view>
				<view class="radio-sub">你们正在安静陪伴中</view>
			</view>
			<view class="safe-actions">
				<view class="safe-btn warn" :class="{ active: blockedByMe }" @tap="toggleBlock">
					{{ blockedByMe ? '解除拉黑' : '加入黑名单' }}
				</view>
				<view class="safe-btn" @tap="openReportPanel">举报</view>
			</view>
		</view>

		<view class="timer-card">
			<view class="timer-info">
				<view class="timer-label">请勿发送涉黄涉暴反动侮辱等内容</view>
				<view class="timer-title">发送图片、音视频消息涉及内容审核，可能略有延迟</view>
			</view>
			<view class="timer-pill" :class="{ streak: streak.streakDays > 0, expiring: streak.expiring }" @tap="streak.streakDays > 0 && showStreakTip()">
				{{ streak.streakDays > 0 ? '🔥x' + streak.streakDays : '已连接' }}
			</view>
		</view>

		<!-- <view class="silent-modes">
			<view class="silent-chip" v-for="item in silentModes" :key="item" @tap="sendPresence(item)">
				{{ item }}
			</view>
		</view> -->

		<scroll-view class="messages" scroll-y :style="messagesStyle" :scroll-into-view="scrollIntoView" :scroll-with-animation="scrollAnimation"
			upper-threshold="80" @scrolltoupper="loadOlderMessages">
			<view class="load-more" v-if="loadingMessages">正在加载更早消息...</view>
			<view class="load-more" v-else-if="!hasMoreMessages && messages.length">没有更早的消息了</view>
			<view class="system-line">你们被放进同一个频道。不说话也算陪伴。</view>
			<view class="msg-row" v-for="item in messages" :key="item.domId"
				:id="item.domId"
				:class="item.mine ? 'mine' : 'other'">
				<image class="avatar-img" v-if="!item.mine && companion.avatarUrl" :src="companion.avatarUrl" mode="aspectFill" @tap="openTargetProfile"></image>
				<view class="avatar" :class="roomMode" v-else-if="!item.mine" @tap="openTargetProfile">{{ companion.avatar }}</view>
				<view class="msg-body" :class="{ mine: item.mine }">
					<view class="bubble image-bubble" v-if="item.messageType === 2" :class="{ muted: item.pending, blocked: item.blocked }">
						<image class="chat-image" :src="item.content" mode="aspectFill" @tap="previewImage(item.content)"></image>
					</view>
					<view class="bubble video-bubble" v-else-if="item.messageType === 5" :class="{ muted: item.pending, blocked: item.blocked }">
						<view class="video-card" @tap="previewVideo(item.content)">
							<image class="video-poster" v-if="item.poster" :src="item.poster" mode="aspectFill"></image>
							<view class="video-placeholder" v-else>
								<view class="video-play">▶</view>
								<view class="video-text">点击播放视频</view>
							</view>
							<view class="video-meta" v-if="item.mediaDuration">{{ item.mediaDuration }}s</view>
						</view>
					</view>
					<view class="bubble emoji-bubble" v-else-if="item.messageType === 3" :class="{ muted: item.pending, blocked: item.blocked }">{{ item.content }}</view>
					<view class="bubble voice-bubble" v-else-if="item.messageType === 4" :class="{ muted: item.pending, blocked: item.blocked, playing: playingVoiceUrl === item.content }"
						@tap="playVoice(item)">
						<text class="voice-wave">{{ playingVoiceUrl === item.content ? '▮▮▮' : '▮▯▮' }}</text>
						<text>{{ item.mediaDuration || 1 }}''</text>
					</view>
					<view class="bubble" v-else :class="{ muted: item.pending, blocked: item.blocked }">{{ item.content }}</view>
					<view class="blocked-tip" v-if="item.mine && item.blocked">{{ item.blockTip || '内容未通过审核，请换一个更合适的内容' }}</view>
				</view>
			</view>
			<view class="hesitate-line right" v-if="typingHint">{{ typingHint }}</view>
			<view id="message-bottom-anchor" class="message-bottom-spacer" :style="{ height: bottomSafePadding }"></view>
		</scroll-view>

		<view class="presence-row" :style="{ bottom: presenceBottom }">
			<view class="presence-chip quiz-chip" @tap="openQuiz">🎯 {{ quizButtonText }}</view>
			<view class="presence-chip" v-for="item in presenceActions" :key="item" @tap="sendPresence(item)">
				{{ item }}
			</view>
		</view>

		<view class="emoji-panel" v-if="showEmoji" :style="{ bottom: emojiPanelBottom }">
			<view class="emoji-item" v-for="item in emojis" :key="item" @tap="sendEmoji(item)">{{ item }}</view>
		</view>

		<view class="input-bar" :style="{ bottom: inputBarBottom }">
			<view class="tool-btn" @tap="toggleEmoji">☺</view>
			<view class="tool-btn" @tap="toggleVoiceMode">{{ voiceMode ? '键' : '语' }}</view>
			<input class="input" v-if="!voiceMode" v-model="draft" :disabled="isBlocked" :placeholder="isBlocked ? '已加入黑名单，无法继续发送' : '轻轻说一句，或保持安静'"
				:adjust-position="false" :cursor-spacing="20" @focus="handleInputFocus" @blur="handleInputBlur" />
			<view class="voice-hold" v-else :class="{ recording: recording, cancelling: voiceCancelling }"
				@touchstart.prevent="startRecord" @touchmove.prevent="moveRecord" @touchend="stopRecord" @touchcancel="cancelRecord">
				{{ voiceHoldText }}
			</view>
			<view class="tool-btn" @tap="chooseMedia">＋</view>
			<view class="send" v-if="!voiceMode" :class="{ disabled: isBlocked }" @tap="sendMessage">发送</view>
		</view>

		<view class="safety-mask" v-if="showSafety" @tap="showSafety = false">
			<view class="safety-panel" @tap.stop>
				<view class="safety-title">安全与退出</view>
				<view class="safety-item" @tap="openReportPanel">举报不适内容</view>
				<view class="safety-item" @tap="toggleBlock">{{ blockedByMe ? '解除拉黑' : '拉黑这个对象' }}</view>
				<view class="safety-item quiet" @tap="finishRoom">提前结束陪伴</view>
			</view>
		</view>

		<view class="safety-mask" v-if="showReportPanel" @tap="showReportPanel = false">
			<view class="safety-panel" @tap.stop>
				<view class="safety-title">举报这个对象</view>
				<view class="report-reason" v-for="item in reportReasons" :key="item.key"
					:class="{ active: reportForm.reasonType === item.key }" @tap="reportForm.reasonType = item.key">
					{{ item.label }}
				</view>
				<textarea class="report-textarea" v-model="reportForm.reasonText" maxlength="120"
					placeholder="可以补充说明发生了什么（选填）"></textarea>
				<view class="report-actions">
					<view class="report-cancel" @tap="showReportPanel = false">取消</view>
					<view class="report-submit" @tap="submitReport">提交举报</view>
				</view>
			</view>
		</view>

		<view class="safety-mask" v-if="showQuiz" @tap="closeQuizPanel">
			<view class="quiz-panel" @tap.stop>
				<view class="quiz-title">🎯 默契小测试</view>
				<view class="quiz-sub" v-if="!quiz.exists">正在出题...</view>
				<template v-else>
					<view class="quiz-result" v-if="quiz.bothDone">
						<view class="quiz-rate">{{ quiz.matchRate }}%</view>
						<view class="quiz-rate-label">{{ quizRateText }}</view>
					</view>
					<view class="quiz-sub" v-else-if="quiz.myDone">已提交，等 TA 答完就揭晓 ✨</view>
					<view class="quiz-sub" v-else>5 道二选一，凭直觉选，看看你们多同频</view>
					<scroll-view scroll-y class="quiz-scroll">
						<view class="quiz-q" v-for="(q, qi) in quiz.questions" :key="qi">
							<view class="quiz-q-text">{{ qi + 1 }}. {{ q.text }}</view>
							<view class="quiz-options">
								<view class="quiz-option"
									:class="{ picked: !quiz.bothDone && quizPicks[qi] === 'A', revealed: quiz.bothDone, same: quiz.bothDone && q.same && q.mine === 'A' }"
									@tap="pickQuizOption(qi, 'A')">
									{{ q.optionA }}
									<text class="quiz-marks" v-if="quiz.bothDone">{{ q.mine === 'A' ? ' 我' : '' }}{{ q.other === 'A' ? ' TA' : '' }}</text>
								</view>
								<view class="quiz-option"
									:class="{ picked: !quiz.bothDone && quizPicks[qi] === 'B', revealed: quiz.bothDone, same: quiz.bothDone && q.same && q.mine === 'B' }"
									@tap="pickQuizOption(qi, 'B')">
									{{ q.optionB }}
									<text class="quiz-marks" v-if="quiz.bothDone">{{ q.mine === 'B' ? ' 我' : '' }}{{ q.other === 'B' ? ' TA' : '' }}</text>
								</view>
							</view>
						</view>
					</scroll-view>
					<view class="quiz-submit" v-if="!quiz.myDone" :class="{ disabled: !quizComplete }" @tap="submitQuiz">提交答案</view>
					<view class="quiz-submit ghost" v-else @tap="closeQuizPanel">{{ quiz.bothDone ? '完成' : '先去聊天，揭晓了叫我' }}</view>
				</template>
			</view>
		</view>

		<view class="profile-mask" v-if="showTargetProfile" @tap="showTargetProfile = false">
			<view class="profile-panel" @tap.stop>
				<image class="profile-avatar-img" v-if="targetProfile.avatarUrl" :src="targetProfile.avatarUrl" mode="aspectFill"></image>
				<view class="profile-avatar" v-else>{{ targetProfile.avatarText || companion.avatar }}</view>
				<view class="profile-name">{{ targetProfile.anonymousName || companion.name }}</view>
				<view class="profile-mode">{{ targetProfile.currentMode === 'e' ? 'e 人 · 轻轻热闹' : 'i 人 · 安静陪伴' }}</view>
				<view class="profile-info-grid">
					<view class="profile-info">
						<text class="profile-info-label">性别</text>
						<text>{{ genderText(targetProfile.gender) }}</text>
					</view>
					<view class="profile-info">
						<text class="profile-info-label">当前状态</text>
						<text>{{ profileStateText(targetProfile) }}</text>
					</view>
				</view>
				<view class="profile-intro">{{ targetProfile.intro || companion.prompt || '这个人还没有写介绍。' }}</view>
				<view class="profile-tags" v-if="targetProfile.interestTags && targetProfile.interestTags.length">
					<text v-for="tag in targetProfile.interestTags" :key="tag">{{ tag }}</text>
				</view>
				<view class="profile-persona" v-if="targetProfile.personaImages && targetProfile.personaImages.length">
					<view class="profile-section-title">人格卡片</view>
					<scroll-view scroll-x class="profile-persona-scroll">
						<image class="profile-persona-image" v-for="(img, index) in targetProfile.personaImages" :key="img" :src="img" mode="aspectFill" @tap="previewTargetPersona(index)"></image>
					</scroll-view>
				</view>
				<view class="profile-space-entry" v-if="targetProfile.userId" @tap="goTargetSpace">
					<text class="profile-space-icon">🪐</text>
					<text>看TA的个人空间</text>
					<text class="profile-space-arrow">›</text>
				</view>
				<view class="profile-close" @tap="showTargetProfile = false">继续聊天</view>
			</view>
		</view>
	</view>
</template>

<script>
	import ieSocket from '@/common/ieSocket.js'
	import { finishIeRoom, reportIeRoom, blockIeRoomTarget, unblockIeRoomTarget, getIeRoomBlockStatus, pageIeMessages, sendIeMessage, getIeRoomTargetProfile, getIeRoomStreak, startIeQuiz, getIeQuizCurrent, answerIeQuiz } from '@/common/ieApi.js'
	import tui from '@/common/httpRequest.js'

	export default {
		data() {
			return {
				menuButtonInfo: {
					top: 24
				},
				roomMode: 'i',
				roomId: '',
				targetUserId: '',
				secondsLeft: 15 * 60,
				timer: null,
				pollTimer: null,
				draft: '',
				showSafety: false,
				showReportPanel: false,
				isBlocked: false,
				blockedByMe: false,
				blockedByOther: false,
				showTargetProfile: false,
				targetProfile: {},
				showEmoji: false,
				voiceMode: false,
				typingHint: '',
				finishing: false,
				uploadingImage: false,
				uploadingVoice: false,
				recording: false,
				voiceCancelling: false,
				// idle | starting | recording：解决“按得快松得快”时 stop 先于 start 到达导致的录音失败
				recordPhase: 'idle',
				pendingStop: false,
				recordStartY: 0,
				recordStartTime: 0,
				recorderManager: null,
				innerAudioContext: null,
				playingVoiceUrl: '',
				voiceSwitching: false,
				keyboardHeight: 0,
				keyboardListener: null,
				streak: { streakDays: 0, bothToday: false, expiring: false },
				lastStreakLoad: 0,
				showQuiz: false,
				quiz: { exists: false, questions: [] },
				quizPicks: ['', '', '', '', ''],
				quizPolling: null,
				quizNoticeId: '',
				messages: [],
				messagePage: 1,
				messagePageSize: 20,
				hasMoreMessages: true,
				loadingMessages: false,
				scrollIntoView: '',
				scrollAnimation: true,
				sendingMessage: false,
				emojis: ['🙂', '😄', '🥲', '😭', '😴', '🙌', '🌙', '☁️', '🍃', '✨', '💛', '🫶'],
				presenceActions: ['我在', '听着呢', '慢慢说', '抱一下空气'],
				reportReasons: [
					{ key: 'harassment', label: '骚扰不适' },
					{ key: 'abuse', label: '攻击辱骂' },
					{ key: 'fraud', label: '疑似欺诈' },
					{ key: 'other', label: '其他问题' }
				],
				reportForm: {
					reasonType: 'other',
					reasonText: ''
				},
				silentModes: ['一起听歌', '一起倒计时', '一起自习', '一起失眠'],
				companions: {
					i: {
						name: '月台旁的影子',
						avatar: '月',
						prompt: '如果今晚可以把一个念头放在操场边，你想放什么？',
						firstMessage: '我刚从图书馆出来，路上风有点凉。'
					},
					e: {
						name: '便利店灯光',
						avatar: '光',
						prompt: '今晚聊一个不重要的小事，越轻越好。',
						firstMessage: '我刚买了夜宵，突然很想知道大家今晚都在干嘛。'
					}
				}
			}
		},
		computed: {
			companion() {
				return this.companions[this.roomMode] || this.companions.i
			},
			roomModeText() {
				return this.roomMode === 'i' ? '安静陪伴' : '轻轻热闹'
			},
			silentMinutes() {
				return Math.max(1, 15 - Math.ceil(this.secondsLeft / 60))
			},
			countdownText() {
				const minutes = String(Math.floor(this.secondsLeft / 60)).padStart(2, '0')
				const seconds = String(this.secondsLeft % 60).padStart(2, '0')
				return `${minutes}:${seconds}`
			},
			voiceHoldText() {
				if (!this.recording) return '按住说话'
				return this.voiceCancelling ? '松开取消' : '松开发送，上滑取消'
			},
			keyboardBottomPx() {
				return this.keyboardHeight > 0 ? (this.keyboardHeight + 8) + 'px' : '30rpx'
			},
			bottomSafePadding() {
				return this.keyboardHeight > 0 ? (this.keyboardHeight + 188) + 'px' : '260rpx'
			},
			inputBarBottom() {
				return this.keyboardBottomPx
			},
			presenceBottom() {
				return this.keyboardHeight > 0 ? (this.keyboardHeight + 76) + 'px' : '132rpx'
			},
			emojiPanelBottom() {
				return this.keyboardHeight > 0 ? (this.keyboardHeight + 82) + 'px' : '128rpx'
			},
			messagesStyle() {
				return {}
			},
			quizComplete() {
				return this.quizPicks.every(pick => pick === 'A' || pick === 'B')
			},
			quizRateText() {
				const rate = this.quiz.matchRate
				if (rate === null || rate === undefined) return ''
				if (rate >= 100) return '灵魂同频！这默契没谁了'
				if (rate >= 80) return '默契惊人，你们很合拍'
				if (rate >= 60) return '相当同频，可以处'
				if (rate >= 40) return '互补型搭子，刚刚好'
				return '来自两个星球，但这才有趣'
			},
			quizButtonText() {
				if (!this.quiz || !this.quiz.exists) return '默契测试'
				if (this.quiz.bothDone) return '查看默契'
				if (this.quiz.myDone) return '等TA答题'
				const selfId = String(uni.getStorageSync('id') || '')
				if (this.quiz.starterId && String(this.quiz.starterId) !== selfId) return 'TA发起测试'
				return '继续测试'
			}
		},
		onLoad(options) {
			if (uni.getMenuButtonBoundingClientRect) {
				this.menuButtonInfo = uni.getMenuButtonBoundingClientRect()
			}
			if (options && options.mode) {
				this.roomMode = options.mode
			}
			this.roomId = options.roomId || ''
			this.targetUserId = options.targetUserId || ''
			if (options.name || options.avatar || options.avatarUrl || options.quote) {
				this.companions[this.roomMode] = {
					...this.companion,
					name: decodeURIComponent(options.name || this.companion.name),
					avatar: decodeURIComponent(options.avatar || this.companion.avatar),
					avatarUrl: decodeURIComponent(options.avatarUrl || ''),
					prompt: decodeURIComponent(options.quote || this.companion.prompt),
					firstMessage: ''
				}
			}
			this.initRecorder()
			this.initAudio()
			this.loadTargetProfile()
			this.loadBlockStatus()
			this.loadLatestMessages()
			this.initSocket()
			this.startMessagePolling()
			this.bindKeyboardListener()
			this.loadStreak()
			this.startQuizPolling()
		},
		onShow() {
			if (this.roomId) {
				this.startMessagePolling()
				this.startQuizPolling()
				this.pullNewMessages()
			}
		},
		onHide() {
			// 切后台停止轮询：避免无意义请求，也避免后台期间被动 markRead 导致未读数失真
			this.stopMessagePolling()
			this.stopQuizPolling()
		},
		onUnload() {
			this.clearTimer()
			this.stopMessagePolling()
			this.stopQuizPolling()
			this.unbindKeyboardListener()
			if (this.roomId) {
				ieSocket.unsubscribeRoom(this.roomId)
			}
			ieSocket.resetHandlers()
			ieSocket.close()
			if (this.innerAudioContext) {
				this.innerAudioContext.destroy()
				this.innerAudioContext = null
			}
		},
		methods: {
			bindKeyboardListener() {
				if (!uni.onKeyboardHeightChange) return
				this.keyboardListener = (res = {}) => {
					this.keyboardHeight = res.height || 0
					this.$nextTick(() => {
						this.scrollToBottom(false)
					})
				}
				uni.onKeyboardHeightChange(this.keyboardListener)
			},
			unbindKeyboardListener() {
				if (!this.keyboardListener) return
				if (uni.offKeyboardHeightChange) {
					uni.offKeyboardHeightChange(this.keyboardListener)
				}
				this.keyboardListener = null
			},
			handleInputFocus(e) {
				const height = e && e.detail ? e.detail.height : 0
				if (height) {
					this.keyboardHeight = height
				}
				this.$nextTick(() => {
					this.scrollToBottom(false)
				})
			},
			handleInputBlur() {
				setTimeout(() => {
					this.keyboardHeight = 0
				}, 120)
			},
			async loadBlockStatus() {
				if (!this.roomId) return
				const status = await getIeRoomBlockStatus(this.roomId)
				if (!status) return
				this.blockedByMe = !!status.blockedByMe
				this.blockedByOther = !!status.blockedByOther
				this.isBlocked = !!status.blocked
			},
			startTimer() {
				// 已匹配的陪伴关系可持续聊天，不再自动倒计时结束。
			},
			clearTimer() {
				if (this.timer) {
					clearInterval(this.timer)
					this.timer = null
				}
			},
			normalizeMessage(item) {
				const currentUserId = String(uni.getStorageSync('id') || '')
				const messageId = item.id || item.messageId
				const clientMsgId = item.clientMsgId || ''
				const mine = item.mine !== undefined && item.mine !== null ? item.mine : (currentUserId && String(item.senderId) === currentUserId)
				return {
					domId: 'msg-' + (messageId || clientMsgId || Date.now() + '-' + Math.random().toString(16).slice(2)),
					messageId,
					clientMsgId,
					content: item.filteredContent || item.content || item.rawContent || '',
					messageType: item.messageType || 1,
					mediaDuration: item.mediaDuration,
					mediaSize: item.mediaSize,
					mediaFormat: item.mediaFormat,
					poster: item.poster || item.thumbTempFilePath || '',
					mine,
					pending: false,
					blocked: item.isBlocked === 1,
					mediaAuditStatus: item.mediaAuditStatus || 0
				}
			},
			startMessagePolling() {
				this.stopMessagePolling()
				if (!this.roomId) return
				this.pollTimer = setInterval(() => {
					this.pullNewMessages()
				}, 2200)
			},
			stopMessagePolling() {
				if (this.pollTimer) {
					clearInterval(this.pollTimer)
					this.pollTimer = null
				}
			},
			loadStreak(throttled) {
				if (!this.roomId) return
				const now = Date.now()
				if (throttled && now - this.lastStreakLoad < 30000) return
				this.lastStreakLoad = now
				getIeRoomStreak(this.roomId).then(res => {
					if (res) this.streak = res
				}).catch(() => {})
			},
			showStreakTip() {
				const tip = this.streak.expiring
					? `火花 x${this.streak.streakDays}，今天还没续上，互发一句就不会熄灭`
					: `连续 ${this.streak.streakDays} 天互相陪伴，每满 ${this.streak.rewardInterval || 7} 天双方各得一次匹配机会`
				uni.showToast({ title: tip, icon: 'none', duration: 2600 })
			},
			applyQuizState(res, options = {}) {
				if (!res) return
				const previousId = this.quiz && this.quiz.quizId
				const previousBothDone = this.quiz && this.quiz.bothDone
				this.quiz = res
				if (options.resetPicks && !res.myDone) {
					this.quizPicks = ['', '', '', '', '']
				}
				const selfId = String(uni.getStorageSync('id') || '')
				if (res.exists && res.quizId && res.starterId && String(res.starterId) !== selfId && !res.myDone && this.quizNoticeId !== res.quizId) {
					this.quizNoticeId = res.quizId
					if (!this.showQuiz) {
						uni.showToast({ title: 'TA发起了默契测试', icon: 'none', duration: 2200 })
					}
				}
				if (res.exists && res.bothDone && !previousBothDone && previousId === res.quizId && !this.showQuiz) {
					uni.showToast({ title: `默契测试揭晓：${res.matchRate}%`, icon: 'none', duration: 2600 })
				}
				if (res.exists && res.bothDone) {
					this.stopQuizPolling()
				}
			},
			openQuiz() {
				if (!this.roomId) return
				this.showQuiz = true
				const handle = (res) => {
					this.applyQuizState(res, { resetPicks: true })
					if (res && !res.bothDone) {
						this.startQuizPolling()
					}
				}
				if (this.quiz.exists) {
					getIeQuizCurrent(this.roomId).then(handle).catch(() => {})
					return
				}
				startIeQuiz(this.roomId).then(handle).catch(() => {})
			},
			closeQuizPanel() {
				this.showQuiz = false
				// 未揭晓时保留后台轮询，用于提醒对方发起或测试揭晓。
				if (this.quiz.bothDone) {
					this.stopQuizPolling()
				}
			},
			pickQuizOption(index, option) {
				if (this.quiz.myDone) return
				this.$set(this.quizPicks, index, option)
			},
			submitQuiz() {
				if (!this.quizComplete) {
					uni.showToast({ title: '还有题没选哦', icon: 'none' })
					return
				}
				answerIeQuiz(this.roomId, this.quizPicks.join('')).then(res => {
					this.applyQuizState(res)
					if (res && !res.bothDone) {
						this.startQuizPolling()
					}
				}).catch(() => {})
			},
			startQuizPolling() {
				if (!this.roomId) return
				if (this.quiz && this.quiz.bothDone) return
				this.stopQuizPolling()
				this.quizPolling = setInterval(() => {
					getIeQuizCurrent(this.roomId).then(res => {
						this.applyQuizState(res)
					}).catch(() => {})
				}, 5000)
			},
			stopQuizPolling() {
				if (this.quizPolling) {
					clearInterval(this.quizPolling)
					this.quizPolling = null
				}
			},
			async pullNewMessages() {
				if (!this.roomId || this.loadingMessages) return
				const page = await pageIeMessages(this.roomId, 1, this.messagePageSize)
				const latest = this.normalizePage(page)
				if (!latest.length) return
				// 本地刚发送的消息在 ack 前没有 messageId，去重必须同时核对两种 id，避免心跳把自己的消息拉成重复
				const exists = new Set()
				this.messages.forEach(item => {
					if (item.messageId) exists.add(String(item.messageId))
					if (item.clientMsgId) exists.add(item.clientMsgId)
				})
				const appended = latest.filter(item => !(item.messageId && exists.has(String(item.messageId))) &&
					!(item.clientMsgId && exists.has(item.clientMsgId)))
				this.mergeMessageStatus(latest)
				if (!appended.length) return
				this.messages = this.messages.concat(appended)
				this.scrollToBottom()
			},
			mergeMessageStatus(latest) {
				latest.forEach(remote => {
					const local = this.messages.find(item => (remote.messageId && String(item.messageId || '') === String(remote.messageId)) ||
						(remote.clientMsgId && item.clientMsgId === remote.clientMsgId))
					if (!local) return
					const wasBlocked = !!local.blocked
					local.pending = false
					local.blocked = !!remote.blocked
					local.mediaAuditStatus = remote.mediaAuditStatus || local.mediaAuditStatus || 0
					local.messageId = remote.messageId || local.messageId
					if (!wasBlocked && local.blocked && local.mine) {
						uni.showToast({ title: '内容未通过审核，请换一个更合适的内容', icon: 'none' })
					}
				})
			},
			normalizePage(page) {
				if (!page) return []
				const records = Array.isArray(page) ? page : (page.records || [])
				return records.map(this.normalizeMessage).reverse()
			},
			async loadLatestMessages() {
				if (!this.roomId) return
				this.messagePage = 1
				this.loadingMessages = true
				try {
					const page = await pageIeMessages(this.roomId, this.messagePage, this.messagePageSize)
					const latest = this.normalizePage(page)
					this.messages = latest
					this.hasMoreMessages = page ? (page.current || 1) < (page.pages || 1) : false
					this.scrollToBottom(false)
				} finally {
					this.loadingMessages = false
				}
			},
			async loadOlderMessages() {
				if (!this.roomId || this.loadingMessages || !this.hasMoreMessages) return
				const anchor = this.messages.length ? this.messages[0].domId : ''
				this.loadingMessages = true
				const nextPage = this.messagePage + 1
				try {
					const page = await pageIeMessages(this.roomId, nextPage, this.messagePageSize)
					const older = this.normalizePage(page)
					const exists = new Set(this.messages.map(item => item.messageId || item.clientMsgId))
					const deduped = older.filter(item => !exists.has(item.messageId || item.clientMsgId))
					this.messages = deduped.concat(this.messages)
					this.messagePage = nextPage
					this.hasMoreMessages = page ? (page.current || nextPage) < (page.pages || nextPage) : false
					if (anchor) {
						this.scrollAnimation = false
						this.$nextTick(() => {
							this.scrollIntoView = anchor
							setTimeout(() => { this.scrollAnimation = true }, 120)
						})
					}
				} finally {
					this.loadingMessages = false
				}
			},
			scrollToBottom(animated = true) {
				this.$nextTick(() => {
					if (!this.messages.length) return
					this.scrollAnimation = animated
					this.scrollIntoView = 'message-bottom-anchor'
				})
			},
			sendPresence(text) {
				if (!this.roomId) return
				this.showEmoji = false
				this.sendChatContent(text, 1)
			},
			toggleEmoji() {
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return
				}
				this.voiceMode = false
				this.showEmoji = !this.showEmoji
			},
			toggleVoiceMode() {
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return
				}
				this.voiceMode = !this.voiceMode
				// 切到语音模式就先把麦克风权限要好，避免按住说话时才失败
				if (this.voiceMode) this.ensureRecordAuth()
			},
			ensureRecordAuth() {
				return new Promise((resolve) => {
					uni.getSetting({
						success: (res) => {
							const auth = res.authSetting || {}
							if (auth['scope.record']) {
								resolve(true)
								return
							}
							if (auth['scope.record'] === false) {
								// 之前拒绝过，authorize 不会再弹窗，只能引导去设置页打开
								uni.showModal({
									title: '需要麦克风权限',
									content: '发送语音需要使用麦克风，请在设置中打开「麦克风」权限',
									confirmText: '去设置',
									success: (modal) => {
										if (!modal.confirm) {
											resolve(false)
											return
										}
										uni.openSetting({
											success: (setting) => resolve(!!(setting.authSetting && setting.authSetting['scope.record'])),
											fail: () => resolve(false)
										})
									}
								})
								return
							}
							uni.authorize({
								scope: 'scope.record',
								success: () => resolve(true),
								fail: () => resolve(false)
							})
						},
						fail: () => resolve(true)
					})
				})
			},
			sendEmoji(emoji) {
				this.showEmoji = false
				this.sendChatContent(emoji, 3)
			},
			sendMessage() {
				if (!this.draft.trim()) {
					uni.showToast({
						title: '也可以先不说话',
						icon: 'none'
					})
					return
				}
				const content = this.draft.trim()
				this.draft = ''
				this.showEmoji = false
				this.sendChatContent(content, 1)
			},
			sendChatContent(content, messageType, media = {}) {
				if (!this.roomId) {
					uni.showToast({ title: '房间信息缺失', icon: 'none' })
					return
				}
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return
				}
				if (this.needWaitReply()) {
					uni.showToast({ title: '先等等对方回复吧，首次破冰最多发送 3 条', icon: 'none' })
					return
				}
				const clientMsgId = this.pushLocalMessage(content, messageType, media)
				this.persistMessage({
					roomId: Number(this.roomId),
					clientMsgId,
					messageType,
					content,
					mediaDuration: media.mediaDuration,
					mediaSize: media.mediaSize,
					mediaFormat: media.mediaFormat,
					mediaCheckUrl: media.mediaCheckUrl || '',
					mediaCheckType: media.mediaCheckType
				})
			},
			// 本地先上屏（媒体用本地临时路径秒显示），后台上传/送审，ack 回来再更新状态
			pushLocalMessage(content, messageType, media = {}) {
				// 同一毫秒内连续发送（如连点表情）必须保证 clientMsgId 唯一，否则 ACK 会串消息
				this.msgSeq = (this.msgSeq || 0) + 1
				const clientMsgId = 'm-' + Date.now() + '-' + this.msgSeq
				this.messages.push({
					domId: 'msg-' + clientMsgId,
					clientMsgId,
					content,
					messageType,
					mediaDuration: media.mediaDuration,
					mediaSize: media.mediaSize,
					mediaFormat: media.mediaFormat,
					poster: media.poster || '',
					mine: true,
					pending: true,
					blocked: false,
					blockTip: '',
					mediaAuditStatus: media.mediaAuditStatus || 0
				})
				this.scrollToBottom()
				return clientMsgId
			},
			markSendFailed(clientMsgId, tip) {
				const msg = this.messages.find(item => item.clientMsgId === clientMsgId)
				if (!msg) return
				msg.pending = false
				msg.blocked = true
				msg.blockTip = tip || '发送失败，请稍后再试'
			},
			canSendMedia() {
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return false
				}
				if (this.needWaitReply()) {
					uni.showToast({ title: '先等等对方回复吧，首次破冰最多发送 3 条', icon: 'none' })
					return false
				}
				return true
			},
			needWaitReply() {
				const delivered = this.messages.filter(item => item && !item.blocked && item.messageType)
				if (delivered.some(item => !item.mine)) return false
				return delivered.filter(item => item.mine).length >= 3
			},
			async persistMessage(payload) {
				try {
					const ack = await sendIeMessage(this.roomId, payload)
					if (ack) {
						this.applyAck(ack)
						return
					}
					this.markSendFailed(payload.clientMsgId)
				} catch (e) {
					const message = (e && e.message) || ''
					if (message.indexOf('黑名单') >= 0) this.isBlocked = true
					this.markSendFailed(payload.clientMsgId, message || '发送失败，请稍后再试')
				}
			},
			applyAck(ack) {
				const msg = this.messages.find(item => (ack.clientMsgId && item.clientMsgId === ack.clientMsgId) ||
					(ack.messageId && String(item.messageId || '') === String(ack.messageId)))
				if (!msg) return
				const wasBlocked = !!msg.blocked
				msg.pending = false
				msg.messageId = ack.messageId
				msg.messageType = ack.messageType || msg.messageType || 1
				msg.mediaDuration = ack.mediaDuration || msg.mediaDuration
				msg.mediaSize = ack.mediaSize || msg.mediaSize
				msg.mediaFormat = ack.mediaFormat || msg.mediaFormat
				msg.mediaAuditStatus = ack.mediaAuditStatus || msg.mediaAuditStatus || 0
				msg.blocked = ack.isBlocked === 1
				if (msg.blocked) {
					if (msg.messageType === 1) msg.content = '这句话没有送出，请换一种更温柔的说法。'
					msg.blockTip = '内容未通过审核，请换一个更合适的内容'
					if (!wasBlocked) {
						uni.showToast({ title: '内容未通过审核，请换一个更合适的内容', icon: 'none' })
					}
				} else {
					// 媒体消息保留本地临时路径展示，避免换远程地址重新加载闪一下
					if (msg.messageType === 1 || msg.messageType === 3) msg.content = ack.content || msg.content
					this.loadStreak(true)
				}
			},
			chooseMedia() {
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return
				}
				if (this.uploadingImage) return
				if (!uni.chooseMedia) {
					this.chooseImageCompat()
					return
				}
				uni.chooseMedia({
					count: 1,
					mediaType: ['image', 'video'],
					sourceType: ['album', 'camera'],
					camera: 'back',
					maxDuration: 60,
					success: async (res) => {
						const file = res.tempFiles && res.tempFiles[0]
						if (!file || !file.tempFilePath) return
						if (res.type === 'video' || file.fileType === 'video') {
							if (file.size && file.size > 100 * 1024 * 1024) {
								uni.showToast({ title: '视频不能超过100MB，请先剪辑', icon: 'none' })
								return
							}
							if (file.duration && file.duration > 61) {
								uni.showToast({ title: '视频时长不能超过60秒', icon: 'none' })
								return
							}
							await this.uploadAndSendVideo(file)
							return
						}
						if (file.size && file.size > 10 * 1024 * 1024) {
							uni.showToast({ title: '图片不能超过10MB', icon: 'none' })
							return
						}
						await this.uploadAndSendImage(file.tempFilePath, file.size)
					}
				})
			},
			chooseImageCompat() {
				uni.chooseImage({
					count: 1,
					sourceType: ['album', 'camera'],
					success: async (res) => {
						const filePath = res.tempFilePaths && res.tempFilePaths[0]
						if (!filePath) return
						await this.uploadAndSendImage(filePath)
					}
				})
			},
			extractUploadUrl(result) {
				const url = typeof result === 'string' ? result : (result && (result.url || result.fileUrl || result.path || result.fullPath))
				return url && typeof url === 'string' ? url : ''
			},
			async uploadAndSendImage(filePath, fileSize) {
				if (!this.canSendMedia()) return
				this.showEmoji = false
				// 本地临时路径先上屏，后台再上传+送审
				const clientMsgId = this.pushLocalMessage(filePath, 2, { mediaSize: fileSize, mediaFormat: 'image' })
				this.uploadingImage = true
				try {
					const imageUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', filePath))
					if (!imageUrl) {
						this.markSendFailed(clientMsgId, '图片上传失败，请稍后再试')
						return
					}
					this.persistMessage({
						roomId: Number(this.roomId),
						clientMsgId,
						messageType: 2,
						content: imageUrl,
						mediaSize: fileSize,
						mediaFormat: 'image',
						mediaCheckUrl: imageUrl,
						mediaCheckType: 2
					})
				} catch (e) {
					this.markSendFailed(clientMsgId, '图片上传失败，请稍后再试')
				} finally {
					this.uploadingImage = false
				}
			},
			async uploadAndSendVideo(file) {
				if (!this.canSendMedia()) return
				this.showEmoji = false
				const mediaDuration = file.duration ? Math.round(file.duration) : undefined
				const clientMsgId = this.pushLocalMessage(file.tempFilePath, 5, {
					mediaDuration,
					mediaSize: file.size,
					mediaFormat: 'video',
					poster: file.thumbTempFilePath || ''
				})
				this.uploadingImage = true
				try {
					const videoUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.tempFilePath))
					if (!videoUrl) {
						this.markSendFailed(clientMsgId, '视频上传失败，请稍后再试')
						return
					}
					let posterUrl = ''
					if (file.thumbTempFilePath) {
						try {
							posterUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.thumbTempFilePath))
						} catch (e) {
							posterUrl = ''
						}
					}
					this.persistMessage({
						roomId: Number(this.roomId),
						clientMsgId,
						messageType: 5,
						content: videoUrl,
						mediaDuration,
						mediaSize: file.size,
						mediaFormat: 'video',
						mediaCheckUrl: posterUrl,
						mediaCheckType: posterUrl ? 2 : undefined
					})
				} catch (e) {
					this.markSendFailed(clientMsgId, '视频上传失败，请稍后再试')
				} finally {
					this.uploadingImage = false
				}
			},
			previewImage(url) {
				if (!url) return
				uni.previewImage({ urls: [url], current: url })
			},
			previewVideo(url) {
				if (!url) return
				uni.navigateTo({
					url: '/package1/ieBrowser/videoPreview?url=' + encodeURIComponent(url)
				})
			},
			initRecorder() {
				if (!uni.getRecorderManager) return
				this.recorderManager = uni.getRecorderManager()
				this.recorderManager.onStart(() => {
					this.recordPhase = 'recording'
					// 录音器就绪前用户已松手：这里补上 stop
					if (this.pendingStop) {
						this.pendingStop = false
						this.recorderManager.stop()
					}
				})
				this.recorderManager.onStop(async (res) => {
					const cancelled = this.voiceCancelling
					this.recording = false
					this.voiceCancelling = false
					this.recordPhase = 'idle'
					this.pendingStop = false
					this.recordStartY = 0
					if (cancelled) {
						uni.showToast({ title: '已取消发送', icon: 'none' })
						return
					}
					if (!res.tempFilePath) return
					const duration = Math.max(1, Math.round((res.duration || (Date.now() - this.recordStartTime)) / 1000))
					if (duration < 1) {
						uni.showToast({ title: '说得太短啦', icon: 'none' })
						return
					}
					await this.uploadAndSendVoice(res.tempFilePath, duration, res.fileSize)
				})
				this.recorderManager.onError(() => {
					const active = this.recordPhase !== 'idle'
					this.recording = false
					this.voiceCancelling = false
					this.recordPhase = 'idle'
					this.pendingStop = false
					if (!active) return
					uni.showToast({ title: '录音失败，请检查麦克风权限', icon: 'none' })
					// 顺手检查授权状态，被拒绝过会引导去设置页
					this.ensureRecordAuth()
				})
			},
			initAudio() {
				if (!uni.createInnerAudioContext) return
				this.innerAudioContext = uni.createInnerAudioContext()
				this.innerAudioContext.onEnded(() => { this.playingVoiceUrl = '' })
				this.innerAudioContext.onStop(() => { this.playingVoiceUrl = '' })
				this.innerAudioContext.onError(() => {
					if (this.voiceSwitching) return
					this.playingVoiceUrl = ''
					uni.showToast({ title: '语音播放失败', icon: 'none' })
				})
			},
			recordTouchPoint(event) {
				const touch = (event.touches && event.touches[0]) || (event.changedTouches && event.changedTouches[0])
				return touch ? touch.clientY || touch.pageY || 0 : 0
			},
			async startRecord(event) {
				if (this.isBlocked) {
					uni.showToast({ title: '已加入黑名单，无法继续发送', icon: 'none' })
					return
				}
				if (!this.recorderManager || this.uploadingVoice || this.recordPhase !== 'idle') return
				this.showEmoji = false
				this.recording = true
				this.voiceCancelling = false
				this.recordPhase = 'starting'
				this.pendingStop = false
				this.recordStartY = this.recordTouchPoint(event)
				this.recordStartTime = Date.now()
				const authed = await this.ensureRecordAuth()
				// 等授权期间用户可能已松手，或授权被拒：直接复位，不启动录音器
				if (!authed || this.pendingStop) {
					this.recording = false
					this.voiceCancelling = false
					this.recordPhase = 'idle'
					this.pendingStop = false
					return
				}
				this.recorderManager.start({
					duration: 60000,
					sampleRate: 16000,
					numberOfChannels: 1,
					encodeBitRate: 48000,
					format: 'mp3'
				})
			},
			moveRecord(event) {
				if (!this.recording) return
				const y = this.recordTouchPoint(event)
				if (!y || !this.recordStartY) return
				this.voiceCancelling = this.recordStartY - y > 70
			},
			stopRecord() {
				if (!this.recorderManager || this.recordPhase === 'idle') return
				if (this.recordPhase === 'recording') {
					this.recorderManager.stop()
				} else {
					// 录音器还在启动中，标记一下，onStart 里补 stop
					this.pendingStop = true
				}
			},
			cancelRecord() {
				if (!this.recorderManager || this.recordPhase === 'idle') return
				this.voiceCancelling = true
				if (this.recordPhase === 'recording') {
					this.recorderManager.stop()
				} else {
					this.pendingStop = true
				}
			},
			async uploadAndSendVoice(filePath, duration, fileSize) {
				if (!filePath || this.uploadingVoice) return
				if (!this.canSendMedia()) return
				const mediaDuration = Math.min(duration, 60)
				const clientMsgId = this.pushLocalMessage(filePath, 4, {
					mediaDuration,
					mediaSize: fileSize,
					mediaFormat: 'mp3'
				})
				this.uploadingVoice = true
				try {
					const voiceUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', filePath))
					if (!voiceUrl) {
						this.markSendFailed(clientMsgId, '语音上传失败，请稍后再试')
						return
					}
					this.persistMessage({
						roomId: Number(this.roomId),
						clientMsgId,
						messageType: 4,
						content: voiceUrl,
						mediaDuration,
						mediaSize: fileSize,
						mediaFormat: 'mp3',
						mediaCheckUrl: voiceUrl,
						mediaCheckType: 1
					})
				} catch (e) {
					this.markSendFailed(clientMsgId, '语音上传失败，请稍后再试')
				} finally {
					this.uploadingVoice = false
				}
			},
			playVoice(item) {
				if (!item || !item.content || !this.innerAudioContext) return
				if (this.playingVoiceUrl === item.content) {
					this.voiceSwitching = true
					this.innerAudioContext.stop()
					this.playingVoiceUrl = ''
					setTimeout(() => { this.voiceSwitching = false }, 200)
					return
				}
				this.voiceSwitching = true
				this.innerAudioContext.stop()
				setTimeout(() => {
					this.innerAudioContext.src = item.content
					this.playingVoiceUrl = item.content
					this.voiceSwitching = false
					this.innerAudioContext.play()
				}, 80)
			},
			genderText(gender) {
				if (gender === 'male') return '男生'
				if (gender === 'female') return '女生'
				return '未设置'
			},
			profileStateText(profile) {
				const tags = profile && profile.interestTags && profile.interestTags.length ? profile.interestTags.slice(0, 2).join(' · ') : '此刻在线'
				return tags
			},
			previewTargetPersona(index) {
				const images = this.targetProfile.personaImages || []
				if (!images.length) return
				uni.previewImage({ urls: images, current: images[index] })
			},
			goTargetSpace() {
				const userId = this.targetProfile.userId || this.targetUserId
				if (!userId) return
				this.showTargetProfile = false
				const name = encodeURIComponent(this.targetProfile.anonymousName || this.companion.name || 'TA')
				const avatar = encodeURIComponent(this.targetProfile.avatarText || this.companion.avatar || 'TA')
				const avatarUrl = encodeURIComponent(this.targetProfile.avatarUrl || this.companion.avatarUrl || '')
				const mode = this.targetProfile.currentMode || this.roomMode || 'i'
				// roomId 一并带上：服务端凭房间成员关系放行，避免依赖前端回传的 userId
				uni.navigateTo({ url: `/package1/ieBrowser/space?userId=${userId}&roomId=${this.roomId || ''}&name=${name}&avatar=${avatar}&avatarUrl=${avatarUrl}&mode=${mode}` })
			},
			async openTargetProfile() {
				await this.loadTargetProfile()
				if (!this.targetProfile || !this.targetProfile.userId) {
					this.targetProfile = {
						anonymousName: this.companion.name,
						avatarText: this.companion.avatar,
						avatarUrl: this.companion.avatarUrl,
						intro: this.companion.prompt,
						currentMode: this.roomMode,
						interestTags: [],
						personaImages: []
					}
				}
				this.showTargetProfile = true
			},
			async loadTargetProfile() {
				if (!this.roomId) return
				const profile = await getIeRoomTargetProfile(this.roomId)
				if (!profile) return
				if (profile.userId) this.targetUserId = profile.userId
				const usableProfile = profile.exists !== false
				this.targetProfile = usableProfile ? {
					...profile,
					anonymousName: profile.anonymousName || this.companion.name,
					avatarText: profile.avatarText || this.companion.avatar,
					avatarUrl: profile.avatarUrl || this.companion.avatarUrl,
					intro: profile.intro || this.companion.prompt,
					currentMode: profile.currentMode || this.roomMode,
					interestTags: profile.interestTags || [],
					personaImages: profile.personaImages || []
				} : {
					...profile,
					anonymousName: this.companion.name,
					avatarText: this.companion.avatar,
					avatarUrl: this.companion.avatarUrl,
					intro: this.companion.prompt,
					currentMode: this.roomMode,
					interestTags: profile.interestTags || [],
					personaImages: profile.personaImages || []
				}
				if (!usableProfile) return
				this.companions[this.roomMode] = {
					...this.companion,
					name: profile.anonymousName || this.companion.name,
					avatar: profile.avatarText || this.companion.avatar,
					avatarUrl: profile.avatarUrl || this.companion.avatarUrl || '',
					prompt: profile.intro || this.companion.prompt
				}
			},
			openReportPanel() {
				this.showSafety = false
				this.reportForm.reasonType = 'other'
				this.reportForm.reasonText = ''
				this.showReportPanel = true
			},
			async submitReport() {
				if (!this.roomId || !this.targetUserId) {
					uni.showToast({ title: '举报对象缺失', icon: 'none' })
					return
				}
				await reportIeRoom(this.roomId, {
					reportedUserId: this.targetUserId,
					reasonType: this.reportForm.reasonType,
					reasonText: this.reportForm.reasonText || '聊天内容不适'
				})
				this.showReportPanel = false
				uni.showToast({ title: '已收到举报', icon: 'none' })
			},
			toggleBlock() {
				if (this.blockedByMe) {
					this.unblock()
					return
				}
				this.block()
			},
			async block() {
				this.showSafety = false
				if (!this.targetUserId) {
					uni.showToast({ title: '拉黑对象缺失', icon: 'none' })
					return
				}
				uni.showModal({
					title: '加入黑名单？',
					content: '加入后你们不能继续互相发送消息，也不会再次随机匹配到对方。',
					confirmText: '加入',
					confirmColor: '#e85d75',
					success: async (res) => {
						if (!res.confirm) return
						await blockIeRoomTarget(this.roomId, '聊天中拉黑')
						this.blockedByMe = true
						this.isBlocked = true
						this.showEmoji = false
						this.voiceMode = false
						uni.showToast({ title: '已加入黑名单', icon: 'none' })
					}
				})
			},
			async unblock() {
				this.showSafety = false
				if (!this.targetUserId) {
					uni.showToast({ title: '解除对象缺失', icon: 'none' })
					return
				}
				uni.showModal({
					title: '解除拉黑？',
					content: '解除后可以继续发送消息，也可能再次随机匹配到对方。',
					confirmText: '解除',
					confirmColor: '#6c69d8',
					success: async (res) => {
						if (!res.confirm) return
						await unblockIeRoomTarget(this.roomId)
						this.blockedByMe = false
						this.isBlocked = this.blockedByOther
						uni.showToast({ title: '已解除拉黑', icon: 'none' })
					}
				})
			},
			async finishRoom() {
				if (this.finishing) return
				this.finishing = true
				this.clearTimer()
				if (this.roomId) await finishIeRoom(this.roomId)
				uni.redirectTo({
					url: '/package1/ieBrowser/chatList'
				})
			},
			initSocket() {
				if (!this.roomId) return
				ieSocket.resetHandlers()
				ieSocket.onAck((ack) => {
					this.applyAck(ack)
				})
				ieSocket.onMessage((msg) => {
					if (String(msg.roomId) !== String(this.roomId)) return
					if (msg.type === 'blockStatus') {
						this.applyBlockStatusEvent(msg)
						return
					}
					if (this.messages.some(item => String(item.messageId) === String(msg.messageId))) return
					if (msg.clientMsgId && this.messages.some(item => item.clientMsgId === msg.clientMsgId)) {
						this.applyAck(msg)
						return
					}
					this.messages.push({
						domId: 'msg-' + (msg.messageId || Date.now()),
						messageId: msg.messageId,
						clientMsgId: msg.clientMsgId,
						messageType: msg.messageType || 1,
						mediaDuration: msg.mediaDuration,
						mediaSize: msg.mediaSize,
						mediaFormat: msg.mediaFormat,
						mediaAuditStatus: msg.mediaAuditStatus || 0,
						content: msg.content,
						mine: String(msg.senderId || '') === String(uni.getStorageSync('id') || '')
					})
					this.scrollToBottom()
				})
				ieSocket.onPresence((event) => {
					if (String(event.roomId) !== String(this.roomId)) return
					this.typingHint = event.eventText || '对方有一个轻轻回应'
					setTimeout(() => { this.typingHint = '' }, 1800)
				})
				ieSocket.onRoomEnd((room) => {
					if (String(room.id) !== String(this.roomId)) return
					uni.showToast({ title: '这段聊天已结束', icon: 'none' })
					this.clearTimer()
					this.finishing = true
					uni.redirectTo({ url: '/package1/ieBrowser/chatList' })
				})
				ieSocket.connect().then(() => {
					ieSocket.subscribeRoom(this.roomId)
				})
			},
			applyBlockStatusEvent(msg) {
				const selfId = String(uni.getStorageSync('id') || '')
				const targetId = String(this.targetUserId || '')
				const userId = String(msg.userId || '')
				const eventTargetId = String(msg.targetUserId || '')
				const status = userId === selfId ? msg.userStatus : (eventTargetId === selfId ? msg.targetStatus : null)
				if (status) {
					this.blockedByMe = !!status.blockedByMe
					this.blockedByOther = !!status.blockedByOther
					this.isBlocked = !!status.blocked
					if (this.isBlocked) {
						this.showEmoji = false
						this.voiceMode = false
					}
					return
				}
				if (userId === targetId || eventTargetId === targetId) {
					this.loadBlockStatus()
				}
			},
			back() {
				uni.redirectTo({ url: '/package1/ieBrowser/index' })
			}
		}
	}
</script>

<style lang="scss" scoped>
	page {
		background: #f7f9ff;
	}

	.room-page {
		height: 100vh;
		padding: 0 24rpx;
		box-sizing: border-box;
		color: #161b2e;
		background:
			radial-gradient(circle at 14% 10%, rgba(139, 124, 255, 0.2), rgba(139, 124, 255, 0) 280rpx),
			radial-gradient(circle at 82% 18%, rgba(169, 255, 231, .42), rgba(169, 255, 231, 0) 320rpx),
			linear-gradient(180deg, #fbfdff 0%, #eef4ff 64%, #fff4e8 100%);
		position: relative;
		overflow: hidden;
		display: flex;
		flex-direction: column;
	}

	.room-page::before {
		content: '';
		position: absolute;
		inset: 0;
		pointer-events: none;
		opacity: .1;
		background-image: radial-gradient(circle, rgba(22,27,46,.7) 0 1rpx, transparent 1rpx);
		background-size: 44rpx 44rpx;
		animation: noiseFloat 8s steps(5) infinite;
	}

	.glow {
		position: absolute;
		border-radius: 50%;
		filter: blur(10rpx);
	}

	.glow-a {
		right: -120rpx;
		top: 210rpx;
		width: 300rpx;
		height: 300rpx;
		background: rgba(169, 255, 231, 0.34);
	}

	.glow-b {
		left: -150rpx;
		bottom: 180rpx;
		width: 360rpx;
		height: 360rpx;
		background: rgba(255, 184, 209, 0.22);
	}

	.nav {
		flex-shrink: 0;
		position: relative;
		z-index: 12;
		min-height: 86rpx;
		padding-bottom: 12rpx;
		display: flex;
		align-items: center;
		box-sizing: content-box;
	}

	.back,
	.more {
		width: 62rpx;
		color: rgba(22, 27, 46, 0.62);
		font-size: 54rpx;
		line-height: 56rpx;
	}

	.more {
		text-align: right;
		font-size: 34rpx;
	}

	.nav-avatar-wrap {
		width: 54rpx;
		height: 54rpx;
		margin-right: 12rpx;
		border-radius: 50%;
	}

	.nav-avatar,
	.nav-avatar-img {
		width: 54rpx;
		height: 54rpx;
		border-radius: 50%;
	}

	.nav-avatar {
		text-align: center;
		line-height: 54rpx;
		color: #11162a;
		background: linear-gradient(145deg, #ffffff, #a9ffe7);
		font-size: 23rpx;
		font-weight: 800;
	}

	.room-user {
		flex: 1;
		text-align: left;
	}

	.room-name {
		font-size: 28rpx;
		font-weight: 800;
	}

	.room-meta {
		margin-top: 4rpx;
		color: rgba(22, 27, 46, 0.46);
		font-size: 20rpx;
	}

	.timer-card,
	.topic-card {
		position: relative;
		z-index: 2;
		border: 1rpx solid rgba(255, 255, 255, 0.78);
		background: rgba(255, 255, 255, 0.62);
		backdrop-filter: blur(24rpx);
		box-shadow: 0 20rpx 64rpx rgba(96, 112, 160, 0.12);
	}

	.timer-card {
		flex-shrink: 0;
		display: flex;
		align-items: center;
		justify-content: space-between;
		margin-top: 14rpx;
		padding: 22rpx 24rpx;
		border-radius: 30rpx;
	}

	.radio-room {
		flex-shrink: 0;
		position: relative;
		z-index: 2;
		display: flex;
		align-items: center;
		margin-top: 24rpx;
		padding: 22rpx 24rpx;
		border-radius: 34rpx;
		background: rgba(255,255,255,.58);
		border: 1rpx solid rgba(255,255,255,.78);
		backdrop-filter: blur(24rpx);
		box-shadow: 0 24rpx 72rpx rgba(96,112,160,.13);
		overflow: hidden;
	}

	.radio-orbit {
		position: absolute;
		right: -40rpx;
		top: -60rpx;
		width: 190rpx;
		height: 190rpx;
		border-radius: 50%;
		border: 1rpx solid rgba(139,124,255,.16);
		animation: radioSpin 16s linear infinite;
	}

	.radio-core {
		width: 78rpx;
		height: 78rpx;
		margin-right: 20rpx;
		border-radius: 50%;
		text-align: center;
		line-height: 78rpx;
		color: #11162a;
		background: linear-gradient(145deg, #ffffff, #a9ffe7);
		font-size: 26rpx;
		font-weight: 800;
		box-shadow: 0 18rpx 48rpx rgba(169,255,231,.3);
		animation: breathe 3s ease-in-out infinite;
	}

	.radio-copy {
		position: relative;
		z-index: 1;
		flex: 1;
		min-width: 0;
	}

	.radio-title {
		font-size: 28rpx;
		font-weight: 800;
	}

	.radio-sub {
		margin-top: 6rpx;
		color: rgba(22,27,46,.52);
		font-size: 22rpx;
	}

	.safe-actions {
		position: relative;
		z-index: 1;
		display: flex;
		gap: 12rpx;
		margin-left: 18rpx;
	}

	.safe-btn {
		height: 52rpx;
		line-height: 52rpx;
		padding: 0 18rpx;
		border-radius: 999rpx;
		color: #6c69d8;
		background: rgba(139,124,255,.1);
		font-size: 21rpx;
		font-weight: 800;
		white-space: nowrap;
	}

	.safe-btn.warn {
		color: #e85d75;
		background: rgba(232,93,117,.1);
	}

	.safe-btn.warn.active {
		color: #6c69d8;
		background: rgba(139,124,255,.1);
	}

	.silent-modes {
		position: relative;
		z-index: 1;
		display: flex;
		overflow-x: auto;
		white-space: nowrap;
		margin-top: 22rpx;
	}

	.silent-chip {
		flex-shrink: 0;
		margin-right: 14rpx;
		padding: 16rpx 24rpx;
		border-radius: 999rpx;
		color: rgba(22,27,46,.62);
		background: rgba(255,255,255,.62);
		border: 1rpx solid rgba(255,255,255,.72);
		backdrop-filter: blur(18rpx);
		font-size: 23rpx;
	}

	.timer-label,
	.topic-label {
		color: #6c69d8;
		font-size: 22rpx;
	}

	.timer-info {
		flex: 1;
		min-width: 0;
		margin-right: 16rpx;
	}

	.timer-title {
		margin-top: 8rpx;
		font-size: 18rpx;
		font-weight: 600;
		line-height: 36rpx;
	}

	.timer-pill {
		flex-shrink: 0;
		width: 112rpx;
		height: 60rpx;
		border-radius: 999rpx;
		text-align: center;
		line-height: 60rpx;
		color: #09101f;
		background: #dffef4;
		font-size: 25rpx;
		font-weight: 800;
	}

	.timer-pill.streak {
		color: #ff6a3c;
		background: rgba(255, 138, 84, 0.14);
		border: 1rpx solid rgba(255, 138, 84, 0.3);
	}

	.timer-pill.expiring {
		animation: streakBlink 1.2s ease-in-out infinite;
	}

	.topic-card {
		margin-top: 24rpx;
		padding: 26rpx;
		border-radius: 30rpx;
	}

	.topic-text {
		margin-top: 12rpx;
		color: rgba(22, 27, 46, 0.72);
		font-size: 27rpx;
		line-height: 42rpx;
	}

	.messages {
		flex: 1;
		min-height: 0;
		position: relative;
		z-index: 1;
		padding-top: 22rpx;
		padding-bottom: 24rpx;
		box-sizing: border-box;
	}

	.message-bottom-spacer {
		width: 100%;
		flex-shrink: 0;
	}

	.load-more {
		width: 360rpx;
		margin: 0 auto 24rpx;
		text-align: center;
		color: rgba(22, 27, 46, 0.34);
		font-size: 21rpx;
		line-height: 34rpx;
	}

	.system-line {
		width: 560rpx;
		margin: 0 auto 22rpx;
		text-align: center;
		color: rgba(22, 27, 46, 0.36);
		font-size: 20rpx;
		line-height: 32rpx;
	}

	.hesitate-line {
		width: 260rpx;
		margin: 0 auto 24rpx;
		padding: 8rpx 18rpx;
		border-radius: 999rpx;
		color: rgba(22,27,46,.36);
		background: rgba(255,255,255,.46);
		font-size: 20rpx;
		animation: fadeHint 2.8s ease-in-out infinite;
	}

	.hesitate-line.right {
		margin-right: 18rpx;
	}

	.msg-row {
		display: flex;
		align-items: flex-start;
		margin-bottom: 24rpx;
	}

	.msg-row.mine {
		justify-content: flex-end;
	}

	.avatar,
	.avatar-img {
		width: 72rpx;
		height: 72rpx;
		margin-right: 16rpx;
		border-radius: 50%;
	}

	.avatar {
		text-align: center;
		line-height: 72rpx;
		font-size: 27rpx;
		font-weight: 800;
	}

	.avatar.i {
		color: #f7f4ff;
		background: linear-gradient(145deg, #8b7cff, #dde7ff);
	}

	.avatar.e {
		color: #07101e;
		background: linear-gradient(145deg, #a9ffe7, #fff0b8);
	}

	.bubble {
		max-width: 500rpx;
		padding: 22rpx 26rpx;
		border: 1rpx solid rgba(255, 255, 255, 0.78);
		border-radius: 30rpx;
		background: rgba(255, 255, 255, 0.62);
		color: rgba(22, 27, 46, 0.72);
		font-size: 27rpx;
		line-height: 42rpx;
	}

	.mine .bubble {
		color: #09101f;
		background: #dffef4;
	}

	.msg-body {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		max-width: 520rpx;
	}

	.msg-body.mine {
		align-items: flex-end;
	}

	.bubble.muted {
		background: rgba(223, 254, 244, 0.88);
		opacity: .62;
	}

	.bubble.blocked {
		opacity: .45;
		filter: grayscale(1);
	}

	.blocked-tip {
		margin-top: 10rpx;
		padding: 8rpx 18rpx;
		border-radius: 999rpx;
		color: #e85d75;
		background: rgba(255, 228, 236, .82);
		font-size: 20rpx;
		line-height: 30rpx;
		max-width: 460rpx;
	}

	.image-bubble {
		padding: 8rpx;
		background: rgba(255, 255, 255, 0.72);
	}

	.video-bubble {
		padding: 0;
		border: none;
		background: transparent;
		overflow: hidden;
	}

	.chat-image {
		display: block;
		width: 260rpx;
		height: 260rpx;
		border-radius: 24rpx;
		background: rgba(22, 27, 46, 0.06);
	}

	.video-card {
		position: relative;
		width: 300rpx;
		height: 390rpx;
		border-radius: 28rpx;
		overflow: hidden;
		background:
			radial-gradient(circle at 22% 16%, rgba(169,255,231,.36), transparent 130rpx),
			radial-gradient(circle at 78% 80%, rgba(139,124,255,.32), transparent 150rpx),
			linear-gradient(145deg, #151a2d, #2c3350);
		box-shadow: 0 18rpx 44rpx rgba(96,112,160,.18);
	}

	.mine .video-card {
		box-shadow: 0 18rpx 44rpx rgba(169,255,231,.24);
	}

	.video-poster,
	.video-placeholder {
		width: 100%;
		height: 100%;
	}

	.video-placeholder {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		color: rgba(255,255,255,.92);
	}

	.video-play {
		width: 86rpx;
		height: 86rpx;
		border-radius: 50%;
		text-align: center;
		line-height: 86rpx;
		color: #151a2d;
		background: rgba(255,255,255,.9);
		box-shadow: 0 12rpx 32rpx rgba(0,0,0,.18);
		font-size: 34rpx;
		font-weight: 900;
	}

	.video-text {
		margin-top: 18rpx;
		color: rgba(255,255,255,.78);
		font-size: 24rpx;
		font-weight: 700;
	}

	.video-meta {
		position: absolute;
		right: 16rpx;
		bottom: 16rpx;
		height: 42rpx;
		line-height: 42rpx;
		padding: 0 16rpx;
		border-radius: 999rpx;
		color: #fff;
		background: rgba(0,0,0,.38);
		font-size: 21rpx;
		font-weight: 800;
	}

	.emoji-bubble {
		min-width: 72rpx;
		text-align: center;
		font-size: 48rpx;
		line-height: 62rpx;
	}

	.voice-bubble {
		min-width: 180rpx;
		display: flex;
		align-items: center;
		justify-content: space-between;
		font-weight: 800;
	}

	.voice-bubble.playing {
		box-shadow: 0 0 0 4rpx rgba(169, 255, 231, 0.32);
	}

	.voice-wave {
		margin-right: 24rpx;
		color: #6c69d8;
		letter-spacing: 4rpx;
	}

	.presence-row {
		position: fixed;
		left: 28rpx;
		right: 28rpx;
		bottom: 132rpx;
		z-index: 13;
		display: flex;
		overflow-x: auto;
		white-space: nowrap;
	}

	.presence-chip {
		flex-shrink: 0;
		height: 58rpx;
		line-height: 58rpx;
		padding: 0 22rpx;
		margin-right: 14rpx;
		border-radius: 999rpx;
		color: rgba(22, 27, 46, 0.58);
		background: rgba(255, 255, 255, 0.62);
		box-shadow: 0 12rpx 30rpx rgba(96, 112, 160, .1);
		font-size: 23rpx;
	}

	.quiz-chip {
		color: #7a4dff;
		font-weight: 800;
		background: linear-gradient(135deg, rgba(167, 139, 250, 0.22), rgba(255, 184, 209, 0.26));
		border: 1rpx solid rgba(122, 77, 255, 0.22);
	}

	@keyframes streakBlink {
		0%, 100% { opacity: 1; }
		50% { opacity: 0.45; }
	}

	.quiz-panel {
		width: 100%;
		max-height: 76vh;
		padding: 32rpx 30rpx 30rpx;
		border-radius: 38rpx;
		background: linear-gradient(180deg, #ffffff 0%, #f6f8ff 100%);
		box-sizing: border-box;
		display: flex;
		flex-direction: column;
	}

	.quiz-title {
		font-size: 32rpx;
		font-weight: 800;
		text-align: center;
	}

	.quiz-sub {
		margin-top: 10rpx;
		text-align: center;
		font-size: 24rpx;
		color: rgba(22, 27, 46, 0.55);
	}

	.quiz-result {
		margin-top: 12rpx;
		text-align: center;
	}

	.quiz-rate {
		font-size: 72rpx;
		font-weight: 900;
		background: linear-gradient(135deg, #7a4dff, #ff7eb3);
		-webkit-background-clip: text;
		color: transparent;
	}

	.quiz-rate-label {
		margin-top: 4rpx;
		font-size: 26rpx;
		font-weight: 700;
		color: rgba(22, 27, 46, 0.72);
	}

	.quiz-scroll {
		margin-top: 20rpx;
		max-height: 46vh;
	}

	.quiz-q {
		margin-bottom: 24rpx;
	}

	.quiz-q-text {
		font-size: 27rpx;
		font-weight: 700;
		margin-bottom: 12rpx;
	}

	.quiz-options {
		display: flex;
		gap: 14rpx;
	}

	.quiz-option {
		flex: 1;
		padding: 18rpx 16rpx;
		border-radius: 20rpx;
		font-size: 24rpx;
		text-align: center;
		color: rgba(22, 27, 46, 0.66);
		background: rgba(255, 255, 255, 0.9);
		border: 2rpx solid rgba(122, 77, 255, 0.12);
	}

	.quiz-option.picked {
		color: #7a4dff;
		font-weight: 800;
		background: rgba(122, 77, 255, 0.1);
		border-color: rgba(122, 77, 255, 0.5);
	}

	.quiz-option.revealed {
		border-color: rgba(22, 27, 46, 0.08);
	}

	.quiz-option.same {
		color: #ff6a3c;
		font-weight: 800;
		background: rgba(255, 138, 84, 0.12);
		border-color: rgba(255, 138, 84, 0.45);
	}

	.quiz-marks {
		margin-left: 8rpx;
		font-size: 20rpx;
		color: #7a4dff;
		font-weight: 800;
	}

	.quiz-submit {
		margin-top: 18rpx;
		height: 86rpx;
		line-height: 86rpx;
		text-align: center;
		border-radius: 999rpx;
		font-size: 29rpx;
		font-weight: 800;
		color: #fff;
		background: linear-gradient(135deg, #7a4dff, #ff7eb3);
		box-shadow: 0 16rpx 34rpx rgba(122, 77, 255, 0.3);
	}

	.quiz-submit.disabled {
		opacity: 0.5;
		box-shadow: none;
	}

	.quiz-submit.ghost {
		color: rgba(22, 27, 46, 0.62);
		background: rgba(22, 27, 46, 0.06);
		box-shadow: none;
	}

	.input-bar {
		position: fixed;
		left: 24rpx;
		right: 24rpx;
		bottom: 30rpx;
		z-index: 14;
		height: 88rpx;
		padding: 10rpx;
		box-sizing: border-box;
		border: 1rpx solid rgba(255, 255, 255, 0.78);
		border-radius: 999rpx;
		background: rgba(255, 255, 255, 0.72);
		backdrop-filter: blur(18rpx);
		display: flex;
		align-items: center;
	}

	.emoji-panel {
		position: fixed;
		left: 24rpx;
		right: 24rpx;
		bottom: 128rpx;
		z-index: 11;
		display: flex;
		flex-wrap: wrap;
		padding: 22rpx 20rpx 12rpx;
		border: 1rpx solid rgba(255, 255, 255, 0.78);
		border-radius: 34rpx;
		background: rgba(255, 255, 255, 0.88);
		backdrop-filter: blur(20rpx);
		box-shadow: 0 18rpx 58rpx rgba(96, 112, 160, 0.16);
		box-sizing: border-box;
	}

	.emoji-item {
		width: 16.66%;
		height: 66rpx;
		text-align: center;
		line-height: 66rpx;
		font-size: 42rpx;
	}

	.input {
		flex: 1;
		height: 66rpx;
		padding: 0 24rpx;
		border-radius: 999rpx;
		color: #161b2e;
		background: rgba(238, 244, 255, 0.72);
		font-size: 25rpx;
	}

	.voice-hold {
		flex: 1;
		height: 62rpx;
		line-height: 62rpx;
		padding: 0 18rpx;
		border-radius: 999rpx;
		text-align: center;
		color: rgba(22, 27, 46, 0.62);
		background: rgba(238, 244, 255, 0.86);
		font-size: 25rpx;
		font-weight: 800;
	}

	.voice-hold.recording {
		color: #09101f;
		background: #ffe4ec;
	}

	.voice-hold.cancelling {
		color: #fff;
		background: #ff6b8a;
	}

	.tool-btn {
		width: 62rpx;
		height: 62rpx;
		border-radius: 50%;
		text-align: center;
		line-height: 62rpx;
		color: rgba(22, 27, 46, 0.58);
		background: rgba(238, 244, 255, 0.72);
		font-size: 34rpx;
		font-weight: 800;
	}

	.send {
		height: 66rpx;
		line-height: 66rpx;
		margin-left: 10rpx;
		padding: 0 26rpx;
		border-radius: 999rpx;
		color: #09101f;
		background: #a9ffe7;
		font-size: 24rpx;
		font-weight: 800;
	}

	.send.disabled {
		opacity: .46;
	}

	.safety-mask {
		position: fixed;
		left: 0;
		right: 0;
		top: 0;
		bottom: 0;
		z-index: 20;
		display: flex;
		align-items: flex-end;
		padding: 28rpx;
		background: rgba(22, 27, 46, 0.18);
		backdrop-filter: blur(18rpx);
		box-sizing: border-box;
	}

	.safety-panel {
		width: 100%;
		padding: 30rpx;
		border-radius: 38rpx;
		background: rgba(255, 255, 255, 0.86);
		box-sizing: border-box;
	}

	.safety-title {
		margin-bottom: 18rpx;
		font-size: 30rpx;
		font-weight: 800;
	}

	.safety-item {
		height: 86rpx;
		line-height: 86rpx;
		border-top: 1rpx solid rgba(22, 27, 46, 0.08);
		color: rgba(22, 27, 46, 0.72);
		font-size: 26rpx;
	}

	.safety-item.quiet {
		color: #6c69d8;
	}

	.report-reason {
		height: 76rpx;
		line-height: 76rpx;
		margin-top: 12rpx;
		padding: 0 24rpx;
		border-radius: 24rpx;
		color: rgba(22,27,46,.66);
		background: rgba(238,244,255,.72);
		font-size: 25rpx;
		font-weight: 800;
	}

	.report-reason.active {
		color: #151a2d;
		background: #a9ffe7;
	}

	.report-textarea {
		width: 100%;
		height: 150rpx;
		margin-top: 18rpx;
		padding: 22rpx;
		border-radius: 24rpx;
		box-sizing: border-box;
		color: #151a2d;
		background: rgba(238,244,255,.72);
		font-size: 24rpx;
	}

	.report-actions {
		display: flex;
		gap: 16rpx;
		margin-top: 20rpx;
	}

	.report-cancel,
	.report-submit {
		flex: 1;
		height: 78rpx;
		line-height: 78rpx;
		border-radius: 999rpx;
		text-align: center;
		font-size: 25rpx;
		font-weight: 800;
	}

	.report-cancel {
		color: rgba(22,27,46,.62);
		background: rgba(22,27,46,.06);
	}

	.report-submit {
		color: #11162a;
		background: #a9ffe7;
	}

	.profile-mask {
		position: fixed;
		left: 0;
		right: 0;
		top: 0;
		bottom: 0;
		z-index: 32;
		display: flex;
		align-items: flex-end;
		padding: 34rpx;
		background: rgba(22, 27, 46, 0.16);
		backdrop-filter: blur(16rpx);
		box-sizing: border-box;
	}

	.profile-panel {
		width: 100%;
		padding: 42rpx 34rpx 34rpx;
		border-radius: 44rpx;
		background: rgba(255, 255, 255, 0.88);
		border: 1rpx solid rgba(255, 255, 255, 0.92);
		box-shadow: 0 30rpx 90rpx rgba(96, 112, 160, 0.18);
		text-align: center;
		box-sizing: border-box;
	}

	.profile-avatar,
	.profile-avatar-img {
		width: 132rpx;
		height: 132rpx;
		margin: 0 auto;
		border-radius: 50%;
	}

	.profile-avatar {
		line-height: 132rpx;
		color: #11162a;
		background: linear-gradient(145deg, #ffffff, #a9ffe7);
		font-size: 44rpx;
		font-weight: 800;
	}

	.profile-name {
		margin-top: 24rpx;
		font-size: 36rpx;
		font-weight: 800;
	}

	.profile-mode {
		margin-top: 10rpx;
		color: rgba(22, 27, 46, 0.48);
		font-size: 23rpx;
	}

	.profile-info-grid {
		display: flex;
		gap: 18rpx;
		margin-top: 26rpx;
		justify-content: center;
	}

	.profile-info {
		min-width: 220rpx;
		padding: 18rpx 14rpx;
		border-radius: 24rpx;
		color: rgba(22, 27, 46, 0.72);
		background: rgba(238, 244, 255, 0.72);
		font-size: 24rpx;
		font-weight: 800;
	}

	.profile-info-label {
		display: block;
		margin-bottom: 8rpx;
		color: rgba(22, 27, 46, 0.38);
		font-size: 20rpx;
		font-weight: 600;
	}

	.profile-intro {
		margin-top: 26rpx;
		padding: 24rpx;
		border-radius: 28rpx;
		color: rgba(22, 27, 46, 0.68);
		background: rgba(238, 244, 255, 0.72);
		font-size: 25rpx;
		line-height: 40rpx;
	}

	.profile-tags {
		margin-top: 22rpx;
	}

	.profile-tags text {
		display: inline-block;
		height: 44rpx;
		line-height: 44rpx;
		padding: 0 16rpx;
		margin: 0 10rpx 10rpx 0;
		border-radius: 999rpx;
		color: #6c69d8;
		background: rgba(139, 124, 255, 0.1);
		font-size: 21rpx;
	}

	.profile-persona {
		margin-top: 24rpx;
		text-align: left;
	}

	.profile-section-title {
		margin-bottom: 16rpx;
		color: rgba(22, 27, 46, 0.72);
		font-size: 26rpx;
		font-weight: 800;
	}

	.profile-persona-scroll {
		white-space: nowrap;
	}

	.profile-persona-image {
		display: inline-block;
		width: 198rpx;
		height: 242rpx;
		margin-right: 16rpx;
		border-radius: 28rpx;
		background: rgba(22, 27, 46, 0.06);
		box-shadow: 0 16rpx 42rpx rgba(96, 112, 160, 0.12);
	}

	.profile-space-entry {
		display: flex;
		align-items: center;
		margin-top: 22rpx;
		padding: 20rpx 24rpx;
		border-radius: 22rpx;
		background: rgba(139, 124, 255, 0.08);
		border: 1rpx solid rgba(139, 124, 255, 0.14);
		color: #5a55c8;
		font-size: 25rpx;
		font-weight: 700;
	}

	.profile-space-entry:active {
		transform: scale(.98);
		background: rgba(139, 124, 255, 0.14);
	}

	.profile-space-icon {
		margin-right: 12rpx;
		font-size: 30rpx;
	}

	.profile-space-arrow {
		flex: 1;
		text-align: right;
		font-size: 36rpx;
		color: rgba(90, 85, 200, 0.5);
	}

	.profile-close {
		height: 82rpx;
		line-height: 82rpx;
		margin-top: 24rpx;
		border-radius: 999rpx;
		color: #11162a;
		background: #a9ffe7;
		font-size: 26rpx;
		font-weight: 800;
	}

	@keyframes noiseFloat {
		0%, 100% { transform: translate3d(0,0,0); }
		50% { transform: translate3d(12rpx,-8rpx,0); }
	}

	@keyframes radioSpin {
		from { transform: rotate(0); }
		to { transform: rotate(360deg); }
	}

	@keyframes breathe {
		0%, 100% { transform: scale(.98); }
		50% { transform: scale(1.05); }
	}

	@keyframes fadeHint {
		0%, 100% { opacity: .38; transform: translateY(0); }
		50% { opacity: .82; transform: translateY(-4rpx); }
	}
</style>
