wangfukang 5 days ago
parent
commit
29dc3509a1
  1. 338
      package1/ieBrowser/chat.vue
  2. 345
      package1/ieBrowser/dailyQuestion.vue
  3. 296
      package1/ieBrowser/index.vue
  4. 23
      package1/ieBrowser/messages.vue
  5. 45
      package1/ieBrowser/mySpace.vue
  6. 19
      package1/ieBrowser/settings.vue

338
package1/ieBrowser/chat.vue

@ -35,8 +35,8 @@
<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 class="timer-pill" :class="{ streak: streak.streakDays > 0, zero: !streak.streakDays, expiring: streak.expiring }" @tap="streak.streakDays > 0 && showStreakTip()">
🔥x{{ streak.streakDays || 0 }}
</view>
</view>
@ -71,13 +71,15 @@
</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 }"
<view class="bubble voice-bubble" v-else-if="item.messageType === 4" :style="{ width: voiceBubbleWidth(item) }" :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 class="message-status" v-if="item.mine && messageStatusText(item)" :class="{ blocked: item.blocked }">
{{ messageStatusText(item) }}
</view>
</view>
</view>
<view class="hesitate-line right" v-if="typingHint">{{ typingHint }}</view>
@ -92,20 +94,25 @@
</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>
<scroll-view scroll-y class="emoji-scroll">
<view class="emoji-grid">
<view class="emoji-item" v-for="item in emojis" :key="item" @tap="sendEmoji(item)">{{ item }}</view>
</view>
</scroll-view>
</view>
<view class="emoji-close-mask" v-if="showEmoji" @tap="closeEmoji"></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="tool-btn voice-toggle" @tap="toggleVoiceMode">{{ voiceMode ? '' : '🎙' }}</view>
<input class="input" v-if="!voiceMode" v-model="draft" :focus="inputFocus" hold-keyboard="true" :confirm-hold="true" confirm-type="send" :disabled="isBlocked" :placeholder="isBlocked ? '已加入黑名单,无法继续发送' : '轻轻说一句,或保持安静'"
:adjust-position="false" :cursor-spacing="20" @focus="handleInputFocus" @blur="handleInputBlur" @confirm="sendMessage" />
<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 class="send" v-if="!voiceMode && !keyboardVisible" :class="{ disabled: isBlocked }" @tap="sendMessage">发送</view>
</view>
<view class="safety-mask" v-if="showSafety" @tap="showSafety = false">
@ -235,6 +242,7 @@
voiceMode: false,
typingHint: '',
finishing: false,
reporting: false,
uploadingImage: false,
uploadingVoice: false,
recording: false,
@ -250,6 +258,7 @@
voiceSwitching: false,
keyboardHeight: 0,
keyboardListener: null,
inputFocus: false,
streak: { streakDays: 0, bothToday: false, expiring: false },
lastStreakLoad: 0,
showQuiz: false,
@ -265,7 +274,7 @@
scrollIntoView: '',
scrollAnimation: true,
sendingMessage: false,
emojis: ['🙂', '😄', '🥲', '😭', '😴', '🙌', '🌙', '☁️', '🍃', '✨', '💛', '🫶'],
emojis: ['🙂', '😄', '😁', '😂', '🤣', '😊', '🥰', '😍', '😘', '😎', '🤔', '😳', '🥺', '😭', '😤', '😴', '🤯', '😇', '🙌', '👏', '👍', '👌', '🙏', '💪', '👀', '🤝', '🌙', '☁️', '🍃', '✨', '💛', '🫶', '🌸', '🌈', '🔥', '🎉', '🎧', '📚', '🍚', '🧋', '🚶', '🐱'],
presenceActions: ['我在', '听着呢', '慢慢说', '抱一下空气'],
reportReasons: [
{ key: 'harassment', label: '骚扰不适' },
@ -317,7 +326,7 @@
return this.keyboardHeight > 0 ? (this.keyboardHeight + 8) + 'px' : '30rpx'
},
bottomSafePadding() {
return this.keyboardHeight > 0 ? (this.keyboardHeight + 188) + 'px' : '260rpx'
return this.keyboardHeight > 0 ? (this.keyboardHeight + 138) + 'px' : '230rpx'
},
inputBarBottom() {
return this.keyboardBottomPx
@ -326,7 +335,10 @@
return this.keyboardHeight > 0 ? (this.keyboardHeight + 76) + 'px' : '132rpx'
},
emojiPanelBottom() {
return this.keyboardHeight > 0 ? (this.keyboardHeight + 82) + 'px' : '128rpx'
return this.keyboardHeight > 0 ? (this.keyboardHeight + 108) + 'px' : '138rpx'
},
keyboardVisible() {
return this.keyboardHeight > 0
},
messagesStyle() {
return {}
@ -384,6 +396,10 @@
},
onShow() {
if (this.roomId) {
if (!this.messages.length) {
const localFallback = this.mergeLocalMessages([])
if (localFallback.length) this.messages = localFallback
}
this.startMessagePolling()
this.startQuizPolling()
this.pullNewMessages()
@ -410,6 +426,10 @@
}
},
methods: {
voiceBubbleWidth(item) {
const duration = Math.max(1, Math.min(60, Number(item && item.mediaDuration) || 1))
return (170 + Math.min(230, duration * 5)) + 'rpx'
},
bindKeyboardListener() {
if (!uni.onKeyboardHeightChange) return
this.keyboardListener = (res = {}) => {
@ -417,6 +437,9 @@
this.$nextTick(() => {
this.scrollToBottom(false)
})
setTimeout(() => {
this.scrollToBottom(false)
}, 180)
}
uni.onKeyboardHeightChange(this.keyboardListener)
},
@ -428,6 +451,8 @@
this.keyboardListener = null
},
handleInputFocus(e) {
this.closeEmoji()
this.inputFocus = true
const height = e && e.detail ? e.detail.height : 0
if (height) {
this.keyboardHeight = height
@ -437,10 +462,63 @@
})
},
handleInputBlur() {
this.inputFocus = false
setTimeout(() => {
this.keyboardHeight = 0
this.scrollToBottom(false)
}, 120)
},
localMessageKey() {
return this.roomId ? 'ie_chat_local_messages_' + this.roomId : ''
},
saveLocalMessages() {
const key = this.localMessageKey()
if (!key) return
const locals = this.messages.filter(item => item && item.localOnly && item.createAt && Date.now() - item.createAt < 30 * 60 * 1000)
if (locals.length) {
// ACK
uni.setStorageSync(key, locals.slice(-20))
} else {
uni.removeStorageSync(key)
}
},
loadLocalMessages() {
const key = this.localMessageKey()
if (!key) return []
const list = uni.getStorageSync(key)
return Array.isArray(list) ? list : []
},
removeLocalMessage(clientMsgId) {
if (!clientMsgId) return
this.messages = this.messages.filter(item => item.clientMsgId !== clientMsgId)
this.saveLocalMessages()
},
mergeLocalMessages(remoteMessages) {
const remote = remoteMessages || []
const exists = new Set()
remote.forEach(item => {
if (item.messageId) exists.add('m:' + String(item.messageId))
if (item.clientMsgId) exists.add('c:' + item.clientMsgId)
})
const locals = this.loadLocalMessages().filter(item => {
if (item.messageId && exists.has('m:' + String(item.messageId))) return false
if (item.clientMsgId && exists.has('c:' + item.clientMsgId)) return false
if (item.localState === 'uploading' && item.createAt && Date.now() - item.createAt > 8 * 60 * 1000) {
item.localState = 'failed'
item.blocked = true
item.pending = false
item.blockTip = '上传中断,请重新发送'
}
return item.localOnly
})
return remote.concat(locals)
},
messageStatusText(item) {
if (!item || !item.mine) return ''
if (item.blocked) return item.blockTip || '内容未通过审核,请换一个更合适的内容'
if (item.localState === 'failed') return item.blockTip || '发送失败,请重新发送'
return ''
},
async loadBlockStatus() {
if (!this.roomId) return
const status = await getIeRoomBlockStatus(this.roomId)
@ -474,8 +552,11 @@
mediaFormat: item.mediaFormat,
poster: item.poster || item.thumbTempFilePath || '',
mine,
pending: false,
pending: item.mediaAuditStatus === 1,
blocked: item.isBlocked === 1,
blockTip: item.auditTip || (item.isBlocked === 1 ? '内容未通过审核,请换一个更合适的内容' : ''),
localState: '',
localOnly: false,
mediaAuditStatus: item.mediaAuditStatus || 0
}
},
@ -585,8 +666,13 @@
},
async pullNewMessages() {
if (!this.roomId || this.loadingMessages) return
const page = await pageIeMessages(this.roomId, 1, this.messagePageSize)
const latest = this.normalizePage(page)
let latest = []
try {
const page = await pageIeMessages(this.roomId, 1, this.messagePageSize)
latest = this.normalizePage(page)
} catch (e) {
return
}
if (!latest.length) return
// ack messageId id
const exists = new Set()
@ -607,10 +693,16 @@
(remote.clientMsgId && item.clientMsgId === remote.clientMsgId))
if (!local) return
const wasBlocked = !!local.blocked
local.pending = false
local.pending = remote.mediaAuditStatus === 1
local.blocked = !!remote.blocked
local.blockTip = remote.blockTip || (local.blocked ? '内容未通过审核,请换一个更合适的内容' : '')
local.mediaAuditStatus = remote.mediaAuditStatus || local.mediaAuditStatus || 0
local.messageId = remote.messageId || local.messageId
local.poster = remote.poster || local.poster || ''
local.localOnly = false
local.localState = ''
if (local.messageType === 1 || local.messageType === 3) local.content = remote.content || local.content
this.saveLocalMessages()
if (!wasBlocked && local.blocked && local.mine) {
uni.showToast({ title: '内容未通过审核,请换一个更合适的内容', icon: 'none' })
}
@ -626,9 +718,18 @@
this.messagePage = 1
this.loadingMessages = true
try {
const localFallback = this.mergeLocalMessages([])
if (!this.messages.length && localFallback.length) {
this.messages = localFallback
this.scrollToBottom(false)
}
const page = await pageIeMessages(this.roomId, this.messagePage, this.messagePageSize)
const latest = this.normalizePage(page)
this.messages = latest
if (!latest.length && this.messages.length) {
this.hasMoreMessages = false
return
}
this.messages = this.mergeLocalMessages(latest)
this.hasMoreMessages = page ? (page.current || 1) < (page.pages || 1) : false
this.scrollToBottom(false)
} finally {
@ -664,6 +765,12 @@
if (!this.messages.length) return
this.scrollAnimation = animated
this.scrollIntoView = 'message-bottom-anchor'
setTimeout(() => {
this.scrollIntoView = ''
this.$nextTick(() => {
this.scrollIntoView = 'message-bottom-anchor'
})
}, 80)
})
},
sendPresence(text) {
@ -679,11 +786,15 @@
this.voiceMode = false
this.showEmoji = !this.showEmoji
},
closeEmoji() {
this.showEmoji = false
},
toggleVoiceMode() {
if (this.isBlocked) {
uni.showToast({ title: '已加入黑名单,无法继续发送', icon: 'none' })
return
}
this.closeEmoji()
this.voiceMode = !this.voiceMode
//
if (this.voiceMode) this.ensureRecordAuth()
@ -787,8 +898,12 @@
pending: true,
blocked: false,
blockTip: '',
localState: media.localState || 'sending',
localOnly: true,
createAt: Date.now(),
mediaAuditStatus: media.mediaAuditStatus || 0
})
this.saveLocalMessages()
this.scrollToBottom()
return clientMsgId
},
@ -797,7 +912,9 @@
if (!msg) return
msg.pending = false
msg.blocked = true
msg.localState = 'failed'
msg.blockTip = tip || '发送失败,请稍后再试'
this.saveLocalMessages()
},
canSendMedia() {
if (this.isBlocked) {
@ -834,17 +951,20 @@
(ack.messageId && String(item.messageId || '') === String(ack.messageId)))
if (!msg) return
const wasBlocked = !!msg.blocked
msg.pending = false
msg.pending = ack.mediaAuditStatus === 1
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.poster = ack.poster || msg.poster || ''
msg.mediaAuditStatus = ack.mediaAuditStatus || msg.mediaAuditStatus || 0
msg.blocked = ack.isBlocked === 1
msg.localOnly = false
msg.localState = ''
if (msg.blocked) {
if (msg.messageType === 1) msg.content = '这句话没有送出,请换一种更温柔的说法。'
msg.blockTip = '内容未通过审核,请换一个更合适的内容'
msg.blockTip = ack.auditTip || '内容未通过审核,请换一个更合适的内容'
if (!wasBlocked) {
uni.showToast({ title: '内容未通过审核,请换一个更合适的内容', icon: 'none' })
}
@ -853,12 +973,14 @@
if (msg.messageType === 1 || msg.messageType === 3) msg.content = ack.content || msg.content
this.loadStreak(true)
}
this.saveLocalMessages()
},
chooseMedia() {
if (this.isBlocked) {
uni.showToast({ title: '已加入黑名单,无法继续发送', icon: 'none' })
return
}
this.closeEmoji()
if (this.uploadingImage) return
if (!uni.chooseMedia) {
this.chooseImageCompat()
@ -908,18 +1030,49 @@
const url = typeof result === 'string' ? result : (result && (result.url || result.fileUrl || result.path || result.fullPath))
return url && typeof url === 'string' ? url : ''
},
silentUpload(filePath) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: tui.interfaceUrl() + '/upload/file',
filePath,
name: 'file',
timeout: 300000,
header: { accessToken: uni.getStorageSync('hiver_token') },
success: (res) => {
try {
const d = JSON.parse(String(res.data || '{}').replace(/\ufeff/g, ''))
const fileObj = d.result !== undefined ? d.result : d.data
const url = this.extractUploadUrl(fileObj)
if (d.code % 100 === 0 && url) {
resolve(url)
return
}
reject(new Error(d.message || '上传失败'))
} catch (e) {
reject(e)
}
},
fail: (err) => reject(new Error((err && err.errMsg) || '上传失败,请检查网络'))
})
})
},
async uploadAndSendImage(filePath, fileSize) {
if (!this.canSendMedia()) return
this.showEmoji = false
// +
const clientMsgId = this.pushLocalMessage(filePath, 2, { mediaSize: fileSize, mediaFormat: 'image' })
const clientMsgId = this.pushLocalMessage(filePath, 2, { mediaSize: fileSize, mediaFormat: 'image', localState: 'uploading' })
this.uploadingImage = true
try {
const imageUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', filePath))
const imageUrl = await this.silentUpload(filePath)
if (!imageUrl) {
this.markSendFailed(clientMsgId, '图片上传失败,请稍后再试')
return
}
const local = this.messages.find(item => item.clientMsgId === clientMsgId)
if (local) {
local.localState = 'sending'
this.saveLocalMessages()
}
this.persistMessage({
roomId: Number(this.roomId),
clientMsgId,
@ -944,11 +1097,12 @@
mediaDuration,
mediaSize: file.size,
mediaFormat: 'video',
poster: file.thumbTempFilePath || ''
poster: file.thumbTempFilePath || '',
localState: 'uploading'
})
this.uploadingImage = true
try {
const videoUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.tempFilePath))
const videoUrl = await this.silentUpload(file.tempFilePath)
if (!videoUrl) {
this.markSendFailed(clientMsgId, '视频上传失败,请稍后再试')
return
@ -956,11 +1110,16 @@
let posterUrl = ''
if (file.thumbTempFilePath) {
try {
posterUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.thumbTempFilePath))
posterUrl = await this.silentUpload(file.thumbTempFilePath)
} catch (e) {
posterUrl = ''
}
}
const local = this.messages.find(item => item.clientMsgId === clientMsgId)
if (local) {
local.localState = 'sending'
this.saveLocalMessages()
}
this.persistMessage({
roomId: Number(this.roomId),
clientMsgId,
@ -1030,6 +1189,13 @@
this.ensureRecordAuth()
})
},
vibrateOnRecordStart() {
try {
if (uni.vibrateShort) {
uni.vibrateShort({ type: 'light' })
}
} catch (e) {}
},
initAudio() {
if (!uni.createInnerAudioContext) return
this.innerAudioContext = uni.createInnerAudioContext()
@ -1067,6 +1233,7 @@
this.pendingStop = false
return
}
this.vibrateOnRecordStart()
this.recorderManager.start({
duration: 60000,
sampleRate: 16000,
@ -1194,10 +1361,11 @@
if (!this.roomId) return
const profile = await getIeRoomTargetProfile(this.roomId)
if (!profile) return
if (profile.userId) this.targetUserId = profile.userId
const roomTargetUserId = this.targetUserId || profile.userId || ''
const usableProfile = profile.exists !== false
this.targetProfile = usableProfile ? {
...profile,
userId: roomTargetUserId,
anonymousName: profile.anonymousName || this.companion.name,
avatarText: profile.avatarText || this.companion.avatar,
avatarUrl: profile.avatarUrl || this.companion.avatarUrl,
@ -1207,6 +1375,7 @@
personaImages: profile.personaImages || []
} : {
...profile,
userId: roomTargetUserId,
anonymousName: this.companion.name,
avatarText: this.companion.avatar,
avatarUrl: this.companion.avatarUrl,
@ -1231,17 +1400,31 @@
this.showReportPanel = true
},
async submitReport() {
if (!this.roomId || !this.targetUserId) {
uni.showToast({ title: '举报对象缺失', icon: 'none' })
if (this.reporting) return
if (!this.roomId) {
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' })
this.reporting = true
try {
await reportIeRoom(this.roomId, {
reasonType: this.reportForm.reasonType,
reasonText: this.reportForm.reasonText || '聊天内容不适',
...this.getAreaInfo()
})
this.showReportPanel = false
uni.showToast({ title: '已提交举报,平台会尽快处理', icon: 'none' })
} finally {
this.reporting = false
}
},
getAreaInfo() {
try {
const area = JSON.parse(uni.getStorageSync('area') || '{}')
return { regionId: area.id || '', regionName: area.title || '' }
} catch (e) {
return { regionId: '', regionName: '' }
}
},
toggleBlock() {
if (this.blockedByMe) {
@ -1326,6 +1509,7 @@
mediaDuration: msg.mediaDuration,
mediaSize: msg.mediaSize,
mediaFormat: msg.mediaFormat,
poster: msg.poster || '',
mediaAuditStatus: msg.mediaAuditStatus || 0,
content: msg.content,
mine: String(msg.senderId || '') === String(uni.getStorageSync('id') || '')
@ -1660,6 +1844,12 @@
border: 1rpx solid rgba(255, 138, 84, 0.3);
}
.timer-pill.zero {
color: rgba(22, 27, 46, .38);
background: rgba(22, 27, 46, .05);
border: 1rpx solid rgba(22, 27, 46, .06);
}
.timer-pill.expiring {
animation: streakBlink 1.2s ease-in-out infinite;
}
@ -1687,11 +1877,6 @@
box-sizing: border-box;
}
.message-bottom-spacer {
width: 100%;
flex-shrink: 0;
}
.load-more {
width: 360rpx;
margin: 0 auto 24rpx;
@ -1797,17 +1982,22 @@
filter: grayscale(1);
}
.blocked-tip {
.message-status {
margin-top: 10rpx;
padding: 8rpx 18rpx;
border-radius: 999rpx;
color: #e85d75;
background: rgba(255, 228, 236, .82);
color: #9a6b18;
background: rgba(255, 196, 87, 0.16);
font-size: 20rpx;
line-height: 30rpx;
max-width: 460rpx;
}
.message-status.blocked {
color: #e85d75;
background: rgba(255, 228, 236, .82);
}
.image-bubble {
padding: 8rpx;
background: rgba(255, 255, 255, 0.72);
@ -1901,7 +2091,8 @@
}
.voice-bubble {
min-width: 180rpx;
min-width: 170rpx;
max-width: 400rpx;
display: flex;
align-items: center;
justify-content: space-between;
@ -1923,7 +2114,7 @@
left: 28rpx;
right: 28rpx;
bottom: 132rpx;
z-index: 13;
z-index: 12;
display: flex;
overflow-x: auto;
white-space: nowrap;
@ -1957,7 +2148,7 @@
.quiz-panel {
width: 100%;
max-height: 76vh;
padding: 32rpx 30rpx 30rpx;
padding: 32rpx 30rpx 40rpx;
border-radius: 38rpx;
background: linear-gradient(180deg, #ffffff 0%, #f6f8ff 100%);
box-sizing: border-box;
@ -2000,7 +2191,7 @@
.quiz-scroll {
margin-top: 20rpx;
max-height: 46vh;
max-height: 42vh;
}
.quiz-q {
@ -2083,7 +2274,7 @@
left: 24rpx;
right: 24rpx;
bottom: 30rpx;
z-index: 14;
z-index: 30;
height: 88rpx;
padding: 10rpx;
box-sizing: border-box;
@ -2100,24 +2291,48 @@
left: 24rpx;
right: 24rpx;
bottom: 128rpx;
z-index: 11;
display: flex;
flex-wrap: wrap;
padding: 22rpx 20rpx 12rpx;
z-index: 31;
max-height: 286rpx;
padding: 18rpx 18rpx 14rpx;
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;
overflow: hidden;
}
.emoji-close-mask {
position: fixed;
inset: 0;
z-index: 29;
background: transparent;
}
.emoji-scroll {
max-height: 254rpx;
}
.emoji-grid {
display: flex;
flex-wrap: wrap;
padding-bottom: 4rpx;
}
.emoji-item {
width: 16.66%;
height: 66rpx;
width: 14.28%;
height: 64rpx;
margin-bottom: 8rpx;
border-radius: 22rpx;
text-align: center;
line-height: 66rpx;
font-size: 42rpx;
line-height: 64rpx;
font-size: 40rpx;
}
.emoji-item:active {
background: rgba(169, 255, 231, .34);
transform: scale(.92);
}
.input {
@ -2154,7 +2369,8 @@
}
.tool-btn {
width: 62rpx;
flex-shrink: 0;
width: 66rpx;
height: 62rpx;
border-radius: 50%;
text-align: center;
@ -2165,6 +2381,14 @@
font-weight: 800;
}
.voice-toggle {
width: 78rpx;
margin: 0 8rpx;
border-radius: 999rpx;
font-size: 34rpx;
background: rgba(169, 255, 231, .5);
}
.send {
height: 66rpx;
line-height: 66rpx;
@ -2187,7 +2411,7 @@
right: 0;
top: 0;
bottom: 0;
z-index: 20;
z-index: 40;
display: flex;
align-items: flex-end;
padding: 28rpx;

345
package1/ieBrowser/dailyQuestion.vue

@ -19,7 +19,7 @@
<view class="answer-box" v-if="question.exists && !question.answered">
<textarea class="answer-input" v-model="draft" maxlength="200"
placeholder="写下你的回答,答完就能看到大家的答案(200字以内)"></textarea>
<view class="answer-submit" :class="{ disabled: !draft.trim() || submitting }" @tap="submit">
<view class="answer-submit" :class="{ disabled: submitDisabled }" @tap="submit">
提交回答解锁大家的答案
</view>
</view>
@ -33,17 +33,16 @@
<view class="feed-title">大家的回答</view>
<view class="answer-card" v-for="item in answers" :key="item.id">
<view class="card-head">
<image class="card-avatar-img" v-if="item.avatarUrl" :src="item.avatarUrl" mode="aspectFill"></image>
<view class="card-avatar" v-else>{{ item.avatarText || '' }}</view>
<view class="card-avatar" :class="item.displayMode">{{ item.displayMode }}</view>
<view class="card-user">
<view class="card-name">
{{ item.anonymousName || '半匿名漂流者' }}
{{ item.displayTitle }}
<text class="school-badge" v-if="item.sameSchool">同校</text>
<text class="mine-badge" v-if="item.mine"></text>
</view>
<view class="card-mode">{{ item.currentMode === 'e' ? 'e 人 · 轻轻热闹' : 'i 人 · 安静陪伴' }}</view>
<view class="card-mode">{{ item.displaySubTitle }}</view>
</view>
<view class="card-chat" v-if="!item.mine" @tap="chatWith(item)">和TA聊聊</view>
<view class="card-chat" v-if="!item.mine" :class="{ disabled: matching }" @tap="chatWith(item)">和TA聊聊</view>
</view>
<view class="card-content">{{ item.content }}</view>
</view>
@ -53,6 +52,19 @@
<view class="locked-icon">🔒</view>
<view class="locked-text">回答之后才能看到大家的答案</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-core">{{ matchingMode }}</view>
</view>
<view class="creating-title">感受到Ta的同频</view>
<view class="creating-sub">正在轻轻靠近请稍等一下</view>
<view class="creating-progress"><view></view></view>
</view>
</view>
</view>
</template>
@ -71,7 +83,14 @@
pageNumber: 1,
hasMore: true,
loadingAnswers: false,
matchingAnswerId: null
matching: false,
matchingAnswerId: null,
matchingMode: 'i'
}
},
computed: {
submitDisabled() {
return !this.draft.trim() || this.submitting
}
},
onLoad() {
@ -122,7 +141,7 @@
try {
const res = await pageIeDailyAnswers(this.pageNumber, 10)
if (!res) return
const records = res.records || []
const records = (res.records || []).map(item => this.normalizeAnswer(item))
this.answers = this.pageNumber === 1 ? records : this.answers.concat(records)
this.hasMore = this.answers.length < (res.total || 0)
this.pageNumber += 1
@ -130,22 +149,52 @@
this.loadingAnswers = false
}
},
normalizeAnswer(item) {
const mode = item && item.currentMode === 'e' ? 'e' : 'i'
const modeText = mode === 'e' ? '会主动递一句话' : '愿意慢慢听你说'
const scopeText = item && item.sameSchool ? '同校频率' : '此刻同频'
return {
...item,
displayMode: mode,
displayTitle: mode === 'e' ? '轻轻热闹的 e 人' : '安静靠近的 i 人',
displaySubTitle: `${scopeText} · ${modeText}`
}
},
answerMode(item) {
return item && item.displayMode ? item.displayMode : (item && item.currentMode === 'e' ? 'e' : 'i')
},
answerTitle(item) {
return item && item.displayTitle ? item.displayTitle : (this.answerMode(item) === 'e' ? '轻轻热闹的 e 人' : '安静靠近的 i 人')
},
answerSubTitle(item) {
const modeText = this.answerMode(item) === 'e' ? '会主动递一句话' : '愿意慢慢听你说'
const scopeText = item && item.sameSchool ? '同校频率' : '此刻同频'
return `${scopeText} · ${modeText}`
},
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
async chatWith(item) {
if (this.matchingAnswerId) return
if (!item || item.mine || this.matching) return
this.matching = true
this.matchingAnswerId = item.id
this.matchingMode = this.answerMode(item)
try {
const matchAnimation = this.wait(2000)
const match = await matchIeByAnswer(item.id)
await matchAnimation
if (!match || !match.roomId) return
uni.navigateTo({
url: '/package1/ieBrowser/chat?mode=' + (match.mode || 'i') +
'&roomId=' + match.roomId +
'&targetUserId=' + match.targetUserId +
'&name=' + encodeURIComponent(match.anonymousName || item.anonymousName || '') +
'&avatar=' + encodeURIComponent(match.avatarText || item.avatarText || '') +
'&name=' + encodeURIComponent(match.anonymousName || this.answerTitle(item) || '') +
'&avatar=' + encodeURIComponent(match.avatarText || this.answerMode(item) || '') +
'&avatarUrl=' + encodeURIComponent(match.avatarUrl || item.avatarUrl || '') +
'&quote=' + encodeURIComponent(match.quoteText || '')
})
} finally {
this.matching = false
this.matchingAnswerId = null
}
}
@ -155,16 +204,32 @@
<style scoped>
.daily-page {
position: relative;
min-height: 100vh;
padding: 0 32rpx 60rpx;
box-sizing: border-box;
color: #161b2e;
background: radial-gradient(circle at 18% 10%, rgba(167, 139, 250, .26), transparent 320rpx),
radial-gradient(circle at 86% 24%, rgba(255, 184, 209, .26), transparent 320rpx),
overflow: hidden;
background:
radial-gradient(circle at 18% 8%, rgba(169, 255, 231, .42), transparent 330rpx),
radial-gradient(circle at 88% 18%, rgba(167, 139, 250, .3), transparent 340rpx),
radial-gradient(circle at 22% 72%, rgba(255, 214, 165, .3), transparent 360rpx),
linear-gradient(180deg, #fbfdff, #eef4ff 62%, #fff4e8);
}
.daily-page::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
opacity: .1;
background-image: radial-gradient(circle, rgba(22, 27, 46, .42) 0 1rpx, transparent 1rpx);
background-size: 52rpx 52rpx;
}
.nav {
position: relative;
z-index: 2;
display: flex;
align-items: center;
height: 88rpx;
@ -188,13 +253,18 @@
}
.question-card {
position: relative;
z-index: 2;
margin-top: 18rpx;
padding: 36rpx 30rpx;
border-radius: 34rpx;
background: rgba(255, 255, 255, .8);
padding: 40rpx 32rpx 36rpx;
border-radius: 38rpx;
background:
linear-gradient(180deg, rgba(255, 255, 255, .92), rgba(255, 255, 255, .72)),
radial-gradient(circle at 82% 0%, rgba(169, 255, 231, .36), transparent 240rpx);
border: 1rpx solid rgba(255, 255, 255, .9);
box-shadow: 0 18rpx 44rpx rgba(96, 112, 160, .14);
box-shadow: 0 24rpx 70rpx rgba(96, 112, 160, .16), inset 0 1rpx 0 rgba(255, 255, 255, .95);
text-align: center;
backdrop-filter: blur(20rpx);
}
.q-deco {
@ -218,6 +288,8 @@
}
.answer-box {
position: relative;
z-index: 2;
margin-top: 24rpx;
}
@ -251,10 +323,12 @@
}
.my-answer {
position: relative;
z-index: 2;
margin-top: 24rpx;
padding: 24rpx 26rpx;
border-radius: 26rpx;
background: rgba(122, 77, 255, .08);
border-radius: 30rpx;
background: linear-gradient(135deg, rgba(139, 124, 255, .1), rgba(169, 255, 231, .16));
border: 1rpx solid rgba(122, 77, 255, .16);
}
@ -271,22 +345,50 @@
}
.feed {
position: relative;
z-index: 2;
margin-top: 28rpx;
}
.feed-title {
display: flex;
align-items: center;
font-size: 28rpx;
font-weight: 800;
margin-bottom: 16rpx;
}
.feed-title::before {
content: '';
width: 12rpx;
height: 12rpx;
margin-right: 12rpx;
border-radius: 50%;
background: #a9ffe7;
box-shadow: 0 0 18rpx rgba(43, 217, 164, .58);
}
.answer-card {
position: relative;
margin-bottom: 18rpx;
padding: 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, .82);
border: 1rpx solid rgba(255, 255, 255, .9);
box-shadow: 0 12rpx 30rpx rgba(96, 112, 160, .1);
padding: 26rpx;
border-radius: 34rpx;
overflow: hidden;
background:
linear-gradient(135deg, rgba(255, 255, 255, .94), rgba(255, 255, 255, .76)),
radial-gradient(circle at 10% 0%, rgba(169, 255, 231, .24), transparent 190rpx);
border: 1rpx solid rgba(255, 255, 255, .94);
box-shadow: 0 16rpx 42rpx rgba(96, 112, 160, .12), inset 0 1rpx 0 rgba(255, 255, 255, .95);
backdrop-filter: blur(18rpx);
}
.answer-card::after {
content: '✦';
position: absolute;
right: 26rpx;
bottom: 18rpx;
color: rgba(139, 124, 255, .16);
font-size: 42rpx;
}
.card-head {
@ -294,21 +396,26 @@
align-items: center;
}
.card-avatar,
.card-avatar-img {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
.card-avatar {
width: 82rpx;
height: 82rpx;
border-radius: 28rpx;
flex-shrink: 0;
line-height: 82rpx;
text-align: center;
font-size: 42rpx;
font-weight: 900;
box-shadow: 0 14rpx 32rpx rgba(96, 112, 160, .16), inset 0 1rpx 0 rgba(255, 255, 255, .8);
}
.card-avatar {
line-height: 76rpx;
text-align: center;
font-size: 30rpx;
font-weight: 800;
color: #5a4632;
background: linear-gradient(135deg, #ffe9c9, #ffd6a5);
.card-avatar.i {
color: #fff;
background: linear-gradient(145deg, #7771d8, #a29bfe);
}
.card-avatar.e {
color: #11162a;
background: linear-gradient(145deg, #a9ffe7, #ffd6a5);
}
.card-user {
@ -318,8 +425,9 @@
}
.card-name {
font-size: 27rpx;
font-weight: 800;
font-size: 28rpx;
font-weight: 900;
color: #151a2d;
}
.school-badge,
@ -343,25 +451,35 @@
}
.card-mode {
margin-top: 4rpx;
font-size: 21rpx;
color: rgba(22, 27, 46, .48);
margin-top: 8rpx;
font-size: 22rpx;
color: rgba(22, 27, 46, .46);
}
.card-chat {
flex-shrink: 0;
padding: 12rpx 24rpx;
padding: 13rpx 24rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, #7a4dff, #ff7eb3);
color: #11162a;
background: linear-gradient(135deg, #effffb, #a9ffe7 62%, #ffd6a5);
box-shadow: 0 12rpx 28rpx rgba(169, 255, 231, .28);
}
.card-chat.disabled {
opacity: .68;
}
.card-content {
margin-top: 16rpx;
font-size: 26rpx;
line-height: 1.55;
position: relative;
z-index: 1;
margin-top: 20rpx;
padding: 18rpx 20rpx;
border-radius: 22rpx;
background: rgba(238, 244, 255, .58);
font-size: 27rpx;
line-height: 1.6;
color: rgba(22, 27, 46, .82);
}
@ -373,6 +491,8 @@
}
.locked-tip {
position: relative;
z-index: 2;
margin-top: 60rpx;
text-align: center;
}
@ -386,4 +506,135 @@
font-size: 25rpx;
color: rgba(22, 27, 46, .5);
}
.creating-mask {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
background: rgba(20, 26, 46, .24);
backdrop-filter: blur(18rpx);
}
.creating-card {
width: 100%;
padding: 52rpx 40rpx 42rpx;
border-radius: 44rpx;
text-align: center;
background: linear-gradient(180deg, rgba(255, 255, 255, .96), rgba(247, 249, 255, .9));
border: 1rpx solid rgba(255, 255, 255, .92);
box-shadow: 0 34rpx 100rpx rgba(42, 50, 86, .22);
box-sizing: border-box;
}
.creating-orbit {
position: relative;
width: 132rpx;
height: 132rpx;
margin: 0 auto;
border-radius: 50%;
background: radial-gradient(circle, #effffb 0%, #a9ffe7 58%, rgba(169, 255, 231, .2) 100%);
box-shadow: 0 20rpx 54rpx rgba(96, 200, 170, .26), inset 0 2rpx 0 rgba(255, 255, 255, .9);
animation: creatingPulse 1.8s ease-in-out infinite;
}
.creating-orbit::before {
content: '';
position: absolute;
inset: -18rpx;
border-radius: 50%;
border: 3rpx dashed rgba(108, 105, 216, .24);
animation: creatingSpin 5s linear infinite;
}
.creating-core {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #151a2d;
font-size: 48rpx;
font-weight: 900;
}
.creating-dot {
position: absolute;
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #8b7cff;
box-shadow: 0 0 24rpx rgba(139, 124, 255, .45);
}
.dot-a {
right: 2rpx;
top: 22rpx;
animation: dotFloatA 1.7s ease-in-out infinite;
}
.dot-b {
left: 8rpx;
bottom: 26rpx;
background: #ff9fc1;
animation: dotFloatB 1.9s ease-in-out infinite;
}
.creating-title {
margin-top: 34rpx;
font-size: 36rpx;
font-weight: 900;
color: #151a2d;
}
.creating-sub {
margin-top: 12rpx;
color: rgba(21, 26, 45, .52);
font-size: 24rpx;
line-height: 38rpx;
}
.creating-progress {
height: 10rpx;
margin-top: 34rpx;
border-radius: 999rpx;
overflow: hidden;
background: rgba(21, 26, 45, .06);
}
.creating-progress view {
width: 44%;
height: 100%;
border-radius: 999rpx;
background: linear-gradient(90deg, #a9ffe7, #8b7cff, #ff9fc1);
animation: loadingSlide 1.2s ease-in-out infinite;
}
@keyframes creatingSpin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
@keyframes creatingPulse {
0%, 100% { transform: scale(.98); }
50% { transform: scale(1.04); }
}
@keyframes dotFloatA {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-10rpx, 8rpx); }
}
@keyframes dotFloatB {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(12rpx, -8rpx); }
}
@keyframes loadingSlide {
0% { transform: translateX(-120%); }
100% { transform: translateX(230%); }
}
</style>

296
package1/ieBrowser/index.vue

@ -1,5 +1,5 @@
<template>
<view class="ie-page" :class="{ 'low-performance': lowPowerMode }">
<view class="ie-page" :class="{ 'low-performance': lowPowerMode, 'android-home': isAndroid }">
<view class="aurora aurora-a"></view>
<view class="aurora aurora-b"></view>
<view class="aurora aurora-c"></view>
@ -40,6 +40,7 @@
</view>
</view>
<view class="lower-stack">
<view class="drift-field">
<view class="spark spark-a"></view>
<view class="spark spark-b"></view>
@ -132,6 +133,7 @@
</view>
</view>
</view>
</view>
<view class="target-mask" v-if="showTargetPanel" @tap="closeTargetPanel">
<view class="target-popup" @tap.stop>
@ -152,7 +154,7 @@
<view class="target-title gender-title">今天想遇见谁</view>
<view class="target-row">
<view class="target-chip" v-for="item in targetModes" :key="item.key"
:class="{ active: targetMode === item.key }" @tap="targetMode = item.key">
:class="{ active: targetMode === item.key }" @tap="setTargetMode(item.key)">
{{ item.label }}
</view>
</view>
@ -166,7 +168,7 @@
<view class="target-title gender-title">想匹配的性别</view>
<view class="target-row">
<view class="target-chip gender-chip" v-for="item in targetGenders" :key="item.key"
:class="{ active: targetGender === item.key }" @tap="targetGender = item.key">
:class="{ active: targetGender === item.key }" @tap="setTargetGender(item.key)">
{{ item.label }}
</view>
</view>
@ -186,7 +188,7 @@
</view>
<view class="match-body">
<view class="match-avatar-wrap">
<image class="companion-orb-img" v-if="matchedPerson.avatarUrl" :src="matchedPerson.avatarUrl" mode="aspectFill" @tap="previewMatchedPersona(0)"></image>
<image class="companion-orb-img" v-if="matchedPerson.avatarUrl" :src="matchedPerson.avatarUrl" mode="aspectFill"></image>
<view class="companion-orb" v-else :class="matchedPerson.mode || currentMode">{{ matchedPerson.avatar }}</view>
</view>
<view class="match-name">{{ matchedPerson.name }}</view>
@ -197,6 +199,10 @@
<view class="match-intent" v-if="currentMatch && currentMatch.intentMatched">
TA 也在找{{ intentLabel(currentMatch.intent) }}
</view>
<view class="match-mood" v-if="matchedPerson.moodText">
<text class="match-mood-icon">{{ matchedPerson.moodIcon }}</text>
<text>{{ matchedPerson.moodText }}</text>
</view>
<view class="match-state">{{ modeText(matchedPerson.mode) }} · {{ genderText(matchedPerson.gender) }}</view>
<view class="match-region" v-if="matchedPerson.regionName">📍 {{ matchedPerson.regionName }}</view>
<view class="match-tags" v-if="matchedPerson.tags && matchedPerson.tags.length">
@ -231,11 +237,24 @@
<view class="mini-action" @tap="goUniverse">进入我的宇宙</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-core">{{ currentMode }}</view>
</view>
<view class="creating-title">寻找同频中</view>
<view class="creating-sub">正在捕捉附近的轻轻回应请稍等一下</view>
<view class="creating-progress"><view></view></view>
</view>
</view>
</view>
</template>
<script>
import { ieHome, getIeUnreadCount, updateIeStatus, startIeMatch, getIeUserProfile, getIeDailyQuestion } from '@/common/ieApi.js'
import { ieHome, getIeUnreadCount, updateIeStatus, startIeMatch, getIeDailyQuestion } from '@/common/ieApi.js'
import IeBottomTab from '@/components/ie-bottom-tab/ie-bottom-tab.vue'
export default {
@ -246,9 +265,11 @@
hasShownOnce: false,
currentMood: 'quiet',
currentMode: 'i',
chancesLeft: 3,
chancesLeft: 20,
unreadCount: 0,
showMatch: false,
isAndroid: false,
ieBannedRedirecting: false,
now: new Date(),
awakeCount: 127,
onlineCountBase: 0,
@ -382,6 +403,7 @@
this.menuButtonInfo = uni.getMenuButtonBoundingClientRect()
}
this.lowPowerMode = uni.getStorageSync('ieLowPowerMode') === '1'
this.detectPlatform()
this.pickCompanion()
this.loadHome()
this.loadDailyQuestion()
@ -434,6 +456,14 @@
}, 15000)
}
},
detectPlatform() {
try {
const info = uni.getSystemInfoSync ? uni.getSystemInfoSync() : {}
this.isAndroid = String(info.platform || '').toLowerCase() === 'android'
} catch (e) {
this.isAndroid = false
}
},
stopPageTimers() {
if (this.liveTimer) {
clearInterval(this.liveTimer)
@ -464,6 +494,9 @@
randomBetween(min, max) {
return min + Math.floor(Math.random() * (max - min + 1))
},
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
randomAwakeCount(onlineCount) {
const count = Number(onlineCount) || 0
if (count > 1000) {
@ -516,6 +549,14 @@
toggleIntent(key) {
this.matchIntent = this.matchIntent === key ? '' : key
},
setTargetMode(key) {
this.targetMode = key
this.syncStatus()
},
setTargetGender(key) {
this.targetGender = key
this.syncStatus()
},
intentLabel(key) {
const item = this.intents.find(intent => intent.key === key)
return item ? item.label : '搭子'
@ -526,6 +567,10 @@
},
async loadHome() {
const home = await ieHome()
if (home && home.banned) {
this.redirectBannedIe(home.message)
return
}
if (!home) return
if (home.profileCompleted !== 1) {
uni.redirectTo({ url: '/package1/ieBrowser/profileSetup' })
@ -536,7 +581,7 @@
this.onlineCountBase = Number(home.onlineCount) || 0
this.awakeCount = this.randomAwakeCount(this.onlineCountBase)
this.waitingCount = home.waitingCount || this.waitingCount
this.chancesLeft = Math.max((home.dailyQuota || 3) - (home.usedQuota || 0), 0)
this.chancesLeft = Math.max((home.dailyQuota || 20) - (home.usedQuota || 0), 0)
this.unreadCount = home.unreadCount || 0
this.currentMode = home.currentMode || this.currentMode
this.currentMood = home.currentMood || this.currentMood
@ -552,6 +597,26 @@
this.animateAwakeCount(this.awakeCount)
this.syncStatus()
},
redirectBannedIe(message) {
if (this.ieBannedRedirecting) return
this.ieBannedRedirecting = true
this.stopPageTimers()
this.matching = false
this.showMatch = false
uni.showToast({
title: message || '你的 i/e 身份已被封禁',
icon: 'none',
duration: 1800
})
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index',
fail: () => {
uni.reLaunch({ url: '/pages/index/index' })
}
})
}, 1800)
},
syncStatus() {
if (!this.profileReady) return
const areaInfo = this.getAreaInfo()
@ -590,6 +655,7 @@
}
this.matching = true
try {
const matchAnimation = this.wait(2000)
const match = await startIeMatch({
mode: this.currentMode,
targetMode: this.targetMode,
@ -600,6 +666,7 @@
...areaInfo,
interestTags: [this.activeMood.label]
})
await matchAnimation
if (!match || !match.roomId) {
uni.showToast({ title: (match && match.failReason) || '暂时没有同频的人', icon: 'none' })
return
@ -610,6 +677,9 @@
avatarUrl: match.avatarUrl || '',
name: match.anonymousName || '半匿名 漂流者',
mode: match.mode || this.currentMode,
mood: match.mood || 'quiet',
moodIcon: this.moodIcon(match.mood || 'quiet'),
moodText: this.moodText(match.mood || 'quiet'),
gender: match.gender || '',
regionName: match.regionName || '',
tags: match.interestTags || [],
@ -618,7 +688,6 @@
quote: match.quoteText || '可以先安静待一会,不用急着找话题。',
lastActiveText: match.lastActiveText || ''
}
await this.loadMatchedProfile(match.targetUserId)
// usedQuota
this.chancesLeft = Math.max(this.chancesLeft - 1, 0)
this.showMatch = true
@ -626,33 +695,17 @@
this.matching = false
}
},
async loadMatchedProfile(targetUserId) {
if (!targetUserId) return
// match
const profile = await getIeUserProfile(targetUserId).catch(() => null)
if (!profile || profile.exists === false) return
this.matchedPerson = {
...this.matchedPerson,
avatar: profile.avatarText || this.matchedPerson.avatar,
avatarUrl: profile.avatarUrl || this.matchedPerson.avatarUrl || '',
name: profile.anonymousName || this.matchedPerson.name,
mode: profile.currentMode || this.matchedPerson.mode,
gender: profile.gender || this.matchedPerson.gender,
regionName: profile.regionName || this.matchedPerson.regionName || '',
tags: profile.interestTags || [],
personaImages: profile.personaImages || [],
state: this.profileStateText(profile),
quote: profile.intro || this.matchedPerson.quote
}
},
profileStateText(profile) {
const mode = profile.currentMode === 'e' ? '轻轻热闹' : '安静靠近'
const tags = profile.interestTags && profile.interestTags.length ? profile.interestTags.slice(0, 2).join(' · ') : '此刻在线'
return `${mode} · ${tags}`
},
modeText(mode) {
return mode === 'e' ? 'e 人' : 'i 人'
},
moodIcon(key) {
const item = this.moods.find(mood => mood.key === key)
return item ? item.icon : '🌙'
},
moodText(key) {
const item = this.moods.find(mood => mood.key === key)
return item ? `此刻${item.label}` : '此刻想安静'
},
moodEmoji(item) {
const map = { quiet: '🌙', talk: '💭', listen: '🎧', drift: '🫧' }
return map[item.mood] || (item.type === 'e' ? '⚡' : '🌱')
@ -669,7 +722,7 @@
},
closeMatch() { this.showMatch = false },
showTicketTip() {
uni.showToast({ title: '平台下单,订单完成可获得3张漂流票哦', icon: 'none' })
uni.showToast({ title: '平台下单,订单完成可获得更多漂流票哦', icon: 'none' })
},
toggleMode() {
this.currentMode = this.currentMode === 'i' ? 'e' : 'i'
@ -804,6 +857,8 @@
box-sizing: border-box;
color: #161b2e;
overflow: hidden;
display: flex;
flex-direction: column;
background:
radial-gradient(circle at 12% 8%, rgba(221, 231, 255, .9), transparent 320rpx),
radial-gradient(circle at 90% 16%, rgba(169, 255, 231, .5), transparent 340rpx),
@ -812,6 +867,11 @@
linear-gradient(180deg, #fbfdff 0%, #eef4ff 56%, #fff5ea 100%);
}
.android-home {
height: 100vh;
padding-bottom: 232rpx;
}
/* 星点纹理 */
.ie-page::before {
content: '';
@ -1077,6 +1137,15 @@
}
/* ============ 漂流星域:弹幕 + 星球 ============ */
.lower-stack {
position: relative;
z-index: 3;
}
.android-home .lower-stack {
transform: translateY(28rpx);
}
.drift-field {
position: relative;
z-index: 3;
@ -1372,6 +1441,14 @@
box-shadow: 0 22rpx 70rpx rgba(96, 112, 160, .14), inset 0 1rpx 0 rgba(255, 255, 255, .92);
}
.android-home .drift-field {
margin-top: 22rpx;
}
.android-home .home-action-panel {
margin-top: 18rpx;
}
.play-deck {
display: flex;
align-items: stretch;
@ -1934,6 +2011,28 @@
border: 1rpx solid rgba(255, 138, 84, .32);
}
.match-mood {
margin: 12rpx auto 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
width: fit-content;
max-width: 520rpx;
padding: 8rpx 22rpx;
border-radius: 999rpx;
color: #5a55c8;
background: rgba(139, 124, 255, .1);
border: 1rpx solid rgba(139, 124, 255, .18);
font-size: 22rpx;
font-weight: 800;
}
.match-mood-icon {
font-size: 24rpx;
line-height: 1;
}
.match-groupbuy {
margin: -10rpx 34rpx 34rpx;
padding: 18rpx;
@ -2142,6 +2241,112 @@
font-weight: 700;
}
.creating-mask {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
background: rgba(20, 26, 46, .24);
backdrop-filter: blur(18rpx);
}
.creating-card {
width: 100%;
padding: 52rpx 40rpx 42rpx;
border-radius: 44rpx;
text-align: center;
background: linear-gradient(180deg, rgba(255, 255, 255, .96), rgba(247, 249, 255, .9));
border: 1rpx solid rgba(255, 255, 255, .92);
box-shadow: 0 34rpx 100rpx rgba(42, 50, 86, .22);
box-sizing: border-box;
}
.creating-orbit {
position: relative;
width: 132rpx;
height: 132rpx;
margin: 0 auto;
border-radius: 50%;
background: radial-gradient(circle, #effffb 0%, #a9ffe7 58%, rgba(169, 255, 231, .2) 100%);
box-shadow: 0 20rpx 54rpx rgba(96, 200, 170, .26), inset 0 2rpx 0 rgba(255, 255, 255, .9);
animation: creatingPulse 1.8s ease-in-out infinite;
}
.creating-orbit::before {
content: '';
position: absolute;
inset: -18rpx;
border-radius: 50%;
border: 3rpx dashed rgba(108, 105, 216, .24);
animation: creatingSpin 5s linear infinite;
}
.creating-core {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #151a2d;
font-size: 48rpx;
font-weight: 900;
}
.creating-dot {
position: absolute;
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #8b7cff;
box-shadow: 0 0 24rpx rgba(139, 124, 255, .45);
}
.dot-a {
right: 2rpx;
top: 22rpx;
animation: dotFloatA 1.7s ease-in-out infinite;
}
.dot-b {
left: 8rpx;
bottom: 26rpx;
background: #ff9fc1;
animation: dotFloatB 1.9s ease-in-out infinite;
}
.creating-title {
margin-top: 34rpx;
font-size: 36rpx;
font-weight: 900;
color: #151a2d;
}
.creating-sub {
margin-top: 12rpx;
color: rgba(21, 26, 45, .52);
font-size: 24rpx;
line-height: 38rpx;
}
.creating-progress {
height: 10rpx;
margin-top: 34rpx;
border-radius: 999rpx;
overflow: hidden;
background: rgba(21, 26, 45, .06);
}
.creating-progress view {
width: 44%;
height: 100%;
border-radius: 999rpx;
background: linear-gradient(90deg, #a9ffe7, #8b7cff, #ff9fc1);
animation: loadingSlide 1.2s ease-in-out infinite;
}
/* ============ 动画 ============ */
@keyframes riseIn {
from { opacity: 0; transform: translateY(20rpx); }
@ -2243,6 +2448,31 @@
to { transform: translateY(0); opacity: 1; }
}
@keyframes creatingSpin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
@keyframes creatingPulse {
0%, 100% { transform: scale(.98); }
50% { transform: scale(1.04); }
}
@keyframes dotFloatA {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-10rpx, 8rpx); }
}
@keyframes dotFloatB {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(12rpx, -8rpx); }
}
@keyframes loadingSlide {
0% { transform: translateX(-120%); }
100% { transform: translateX(230%); }
}
/* ============ 流畅模式降级 ============ */
.low-performance::before,
.low-performance .aurora,

23
package1/ieBrowser/messages.vue

@ -64,6 +64,7 @@
pageSize: 10,
hasMore: true,
loading: false,
loadedOnce: false,
deleteDialogVisible: false,
deleteTarget: null,
deleting: false
@ -79,6 +80,7 @@
},
onShow() {
this.startUnreadTimer()
this.refreshOnShow()
},
onHide() {
this.stopUnreadTimer()
@ -104,6 +106,12 @@
if (count === null || count === undefined) return
this.unreadCount = Number(count) || 0
},
refreshOnShow() {
this.refreshUnreadCount()
if (this.loadedOnce) {
this.refreshRecords(true)
}
},
normalizeRecord(item) {
const remeet = item.remeetAvailable === 1
return {
@ -123,14 +131,22 @@
}
},
// /
async refreshRecords() {
if (this.loading || this.deleting) return
async refreshRecords(force = false) {
if ((this.loading && !force) || this.deleting) return
try {
const page = await pageIeRecords(1, this.pageSize)
const list = page && page.records ? page.records.map(this.normalizeRecord) : []
if (!list.length) return
if (!list.length) {
if (force) {
this.requests = []
this.pageNumber = 1
this.hasMore = false
}
return
}
const freshIds = new Set(list.map(item => item.id))
this.requests = list.concat(this.requests.filter(item => !freshIds.has(item.id)))
this.hasMore = page ? (page.current || 1) < (page.pages || 1) || this.requests.length < (page.total || this.requests.length) : false
} catch (e) {
//
}
@ -145,6 +161,7 @@
this.requests = this.requests.concat(list.filter(item => !exists.has(item.id)))
this.hasMore = page ? (page.current || this.pageNumber) < (page.pages || this.pageNumber) : false
this.pageNumber += 1
this.loadedOnce = true
} finally {
this.loading = false
}

45
package1/ieBrowser/mySpace.vue

@ -25,7 +25,6 @@
<view class="moment-top">
<text class="moment-time">{{ momentTime(item.createTime) }}</text>
<text class="moment-state blocked" v-if="item.localState === 'failed'">{{ item.failTip || '发布失败' }}</text>
<text class="moment-state auditing" v-else-if="item.status === 0">审核中</text>
<text class="moment-state blocked" v-else-if="item.status === 2">未通过审核</text>
<text class="moment-del" @tap="removeMoment(item)">删除</text>
</view>
@ -131,13 +130,39 @@
const page = reset ? 1 : this.momentPage
const result = await pageIeMoments(null, page, 10)
const records = (result && result.records) || []
this.moments = reset ? records : this.moments.concat(records)
this.moments = reset ? this.mergeLocalMoments(records) : this.moments.concat(records)
this.hasMoreMoments = records.length >= 10
this.momentPage = page + 1
} finally {
this.loadingMoments = false
}
},
localMomentKey() {
const userId = uni.getStorageSync('id') || this.profile.userId || 'me'
return 'ie_my_space_local_moments_' + userId
},
saveLocalMoments() {
const locals = this.moments.filter(item => String(item.id || '').indexOf('local-') === 0 &&
item.createAt && Date.now() - item.createAt < 30 * 60 * 1000)
if (locals.length) uni.setStorageSync(this.localMomentKey(), locals.slice(0, 4))
else uni.removeStorageSync(this.localMomentKey())
},
loadLocalMoments() {
const list = uni.getStorageSync(this.localMomentKey())
return Array.isArray(list) ? list : []
},
mergeLocalMoments(records) {
const remote = records || []
const locals = this.loadLocalMoments().map(item => {
if ((item.localState === 'uploading' || item.localState === 'publishing') &&
item.createAt && Date.now() - item.createAt > 10 * 60 * 1000) {
item.localState = 'failed'
item.failTip = '发布中断,请删除后重发'
}
return item
})
return locals.length ? locals.concat(remote) : remote
},
momentTime(timeStr) {
if (!timeStr) return ''
const time = new Date(String(timeStr).replace(/-/g, '/')).getTime()
@ -163,6 +188,7 @@
removeMoment(item) {
if (String(item.id).indexOf('local-') === 0) {
this.moments = this.moments.filter(m => m.id !== item.id)
this.saveLocalMoments()
return
}
uni.showModal({
@ -172,6 +198,7 @@
if (!res.confirm) return
await deleteIeMoment(item.id)
this.moments = this.moments.filter(m => m.id !== item.id)
this.saveLocalMoments()
}
})
},
@ -323,19 +350,23 @@
videoUrl: video ? video.local : '',
videoPoster: video ? video.poster : '',
status: 1,
localState: 'publishing',
localState: (images.some(item => item.status === 'uploading') || (video && video.status === 'uploading')) ? 'uploading' : 'publishing',
failTip: '',
createTime: this.formatNow()
createTime: this.formatNow(),
createAt: Date.now()
}
this.moments.unshift(localCard)
this.saveLocalMoments()
this.showComposer = false
this.composeText = ''
this.composeImages = []
this.composeVideo = null
this.publishing = false
uni.showToast({ title: '发布', icon: 'none' })
uni.showToast({ title: '发布成功', icon: 'none' })
try {
await this.waitComposeUploads(images, video)
localCard.localState = 'publishing'
this.saveLocalMoments()
const payload = { content, images: images.map(item => item.url) }
if (video) {
payload.videoUrl = video.url
@ -348,10 +379,12 @@
moment.imageList = localCard.imageList.length ? localCard.imageList : moment.imageList
moment.videoPoster = localCard.videoPoster || moment.videoPoster
this.moments.splice(index, 1, moment)
this.saveLocalMoments()
}
} catch (e) {
localCard.localState = 'failed'
localCard.failTip = (e && e.message) || '发布失败'
localCard.failTip = (e && e.message) || '发布失败,请删除后重发'
this.saveLocalMoments()
uni.showToast({ title: localCard.failTip, icon: 'none' })
}
},

19
package1/ieBrowser/settings.vue

@ -31,12 +31,19 @@
<view class="panel" v-if="activeTab === 'reports'">
<view class="panel-title">我的举报记录</view>
<view class="panel-sub">平台处理结果会同步展示在这里记录按提交时间倒序排列</view>
<view class="row report-row" v-for="item in reports" :key="item.id">
<view class="row-main">
<view class="name">{{ reasonText(item.reasonType) }}</view>
<view class="report-head">
<view class="name">{{ reasonText(item.reasonType) }}</view>
<view class="status-badge" :class="'s' + (item.status || 0)">{{ statusText(item.status) }}</view>
</view>
<view class="desc">{{ item.reasonText || '已提交安全反馈。' }}</view>
<view class="time">对象 {{ item.reportedUserId || '-' }} · {{ statusText(item.status) }} · {{ item.createTime || '' }}</view>
<view class="handle" v-if="item.handleResult">{{ item.handleResult }}</view>
<view class="time">对象 {{ item.reportedUserId || '-' }} · {{ item.regionName || '未知校区' }} · {{ item.createTime || '' }}</view>
<view class="handle" v-if="item.handleResult">
<text>{{ item.handleResult }}</text>
<text v-if="item.blockedIe === 1" class="blocked-tip">已限制对方 i/e 身份</text>
</view>
</view>
</view>
<view class="empty" v-if="!loading && !reports.length">还没有举报记录</view>
@ -143,13 +150,19 @@
.tab.active { color: #11162a; background: #a9ffe7; box-shadow: 0 12rpx 30rpx rgba(169,255,231,.28); }
.panel { margin-top: 26rpx; padding: 30rpx; border-radius: 36rpx; background: rgba(255,255,255,.68); border: 1rpx solid rgba(255,255,255,.88); box-shadow: 0 22rpx 60rpx rgba(96,112,160,.1); }
.panel-title { margin-bottom: 12rpx; font-size: 30rpx; font-weight: 800; }
.panel-sub { margin-bottom: 10rpx; color: rgba(21,26,45,.42); font-size: 22rpx; line-height: 34rpx; }
.row { display: flex; align-items: center; padding: 24rpx 0; border-top: 1rpx solid rgba(21,26,45,.06); }
.row:first-of-type { border-top: 0; }
.row-main { flex: 1; min-width: 0; }
.name { font-size: 27rpx; font-weight: 800; }
.report-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.status-badge { flex-shrink: 0; height: 42rpx; line-height: 42rpx; padding: 0 18rpx; border-radius: 999rpx; font-size: 20rpx; font-weight: 800; color: #a06a13; background: rgba(255,187,92,.16); }
.status-badge.s1 { color: #1f9b6b; background: rgba(69,214,161,.16); }
.status-badge.s2 { color: rgba(21,26,45,.48); background: rgba(21,26,45,.06); }
.desc { margin-top: 8rpx; color: rgba(21,26,45,.48); font-size: 22rpx; }
.time { margin-top: 8rpx; color: rgba(21,26,45,.34); font-size: 20rpx; line-height: 32rpx; }
.handle { margin-top: 12rpx; padding: 14rpx 18rpx; border-radius: 22rpx; color: #6c69d8; background: rgba(139,124,255,.1); font-size: 22rpx; line-height: 34rpx; }
.blocked-tip { display: block; margin-top: 6rpx; color: #e85d75; font-weight: 800; }
.action { flex-shrink: 0; height: 58rpx; line-height: 58rpx; padding: 0 22rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
.action.danger { color: #e85d75; background: rgba(232,93,117,.1); }
.empty { margin-top: 26rpx; text-align: center; color: rgba(21,26,45,.36); font-size: 23rpx; }

Loading…
Cancel
Save