wangfukang 3 weeks ago
parent
commit
cd0e8b7c5d
  1. 334
      package1/ieBrowser/chat.vue
  2. 88
      package1/ieBrowser/profileSetup.vue
  3. 140
      package1/ieBrowser/settings.vue
  4. 42
      package1/ieBrowser/videoPreview.vue

334
package1/ieBrowser/chat.vue

@ -22,6 +22,10 @@
<view class="radio-title">电台陪伴中</view>
<view class="radio-sub">你们已经安静陪伴了 {{ silentMinutes }} 分钟</view>
</view>
<view class="safe-actions">
<view class="safe-btn warn" @tap="block">加入黑名单</view>
<view class="safe-btn" @tap="openReportPanel">举报</view>
</view>
</view>
<view class="timer-card">
@ -51,6 +55,16 @@
<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)">
@ -74,26 +88,42 @@
<view class="input-bar" :style="{ bottom: inputBarBottom }">
<view class="tool-btn" @tap="toggleEmoji"></view>
<view class="tool-btn" @tap="voiceMode = !voiceMode">{{ voiceMode ? '键' : '语' }}</view>
<input class="input" v-if="!voiceMode" v-model="draft" placeholder="轻轻说一句,或保持安静"
<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="chooseImage"></view>
<view class="send" v-if="!voiceMode" @tap="sendMessage">发送</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="report">举报不适内容</view>
<view class="safety-item" @tap="openReportPanel">举报不适内容</view>
<view class="safety-item" @tap="block">拉黑这个对象</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="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>
@ -141,6 +171,8 @@
pollTimer: null,
draft: '',
showSafety: false,
showReportPanel: false,
isBlocked: false,
showTargetProfile: false,
targetProfile: {},
showEmoji: false,
@ -169,6 +201,16 @@
sendingMessage: false,
emojis: ['🙂', '😄', '🥲', '😭', '😴', '🙌', '🌙', '☁️', '🍃', '✨', '💛', '🫶'],
presenceActions: ['我在', '听着呢', '慢慢说', '抱一下空气'],
reportReasons: [
{ key: 'harassment', label: '骚扰不适' },
{ key: 'abuse', label: '攻击辱骂' },
{ key: 'fraud', label: '疑似欺诈' },
{ key: 'other', label: '其他问题' }
],
reportForm: {
reasonType: 'other',
reasonText: ''
},
silentModes: ['一起听歌', '一起倒计时', '一起自习', '一起失眠'],
companions: {
i: {
@ -314,6 +356,7 @@
mediaDuration: item.mediaDuration,
mediaSize: item.mediaSize,
mediaFormat: item.mediaFormat,
poster: item.poster || item.thumbTempFilePath || '',
mine,
pending: false,
blocked: item.isBlocked === 1
@ -400,9 +443,20 @@
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
},
sendEmoji(emoji) {
this.showEmoji = false
this.sendChatContent(emoji, 3)
@ -425,6 +479,10 @@
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
@ -438,6 +496,7 @@
mediaDuration: media.mediaDuration,
mediaSize: media.mediaSize,
mediaFormat: media.mediaFormat,
poster: media.poster || '',
mine: true,
pending: true
})
@ -471,11 +530,13 @@
msg.content = '发送失败,请稍后再试'
}
} catch (e) {
const message = (e && e.message) || ''
if (message.indexOf('黑名单') >= 0) this.isBlocked = true
const msg = this.messages.find(item => item.clientMsgId === payload.clientMsgId)
if (msg) {
msg.pending = false
msg.blocked = true
msg.content = (e && e.message) || '发送失败,请稍后再试'
msg.content = message || '发送失败,请稍后再试'
}
}
},
@ -491,8 +552,33 @@
msg.blocked = ack.isBlocked === 1
msg.content = msg.blocked ? '这句话没有送出,请换一种更温柔的说法。' : (ack.content || msg.content)
},
chooseImage() {
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',
success: async (res) => {
const file = res.tempFiles && res.tempFiles[0]
if (!file || !file.tempFilePath) return
if (res.type === 'video' || file.fileType === 'video') {
await this.uploadAndSendVideo(file)
return
}
await this.uploadAndSendImage(file.tempFilePath, file.size)
}
})
},
chooseImageCompat() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
@ -504,7 +590,7 @@
}
})
},
async uploadAndSendImage(filePath) {
async uploadAndSendImage(filePath, fileSize) {
this.uploadingImage = true
try {
const result = await tui.uploadFile('/upload/file', filePath)
@ -514,7 +600,30 @@
return
}
this.showEmoji = false
this.sendChatContent(imageUrl, 2)
this.sendChatContent(imageUrl, 2, {
mediaSize: fileSize,
mediaFormat: 'image'
})
} finally {
this.uploadingImage = false
}
},
async uploadAndSendVideo(file) {
this.uploadingImage = true
try {
const result = await tui.uploadFile('/upload/file', file.tempFilePath)
const videoUrl = typeof result === 'string' ? result : (result.url || result.fileUrl || result.path || result.fullPath || result)
if (!videoUrl || typeof videoUrl !== 'string') {
uni.showToast({ title: '视频上传失败', icon: 'none' })
return
}
this.showEmoji = false
this.sendChatContent(videoUrl, 5, {
mediaDuration: file.duration ? Math.round(file.duration) : undefined,
mediaSize: file.size,
mediaFormat: 'video',
poster: file.thumbTempFilePath || ''
})
} finally {
this.uploadingImage = false
}
@ -523,6 +632,12 @@
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()
@ -564,6 +679,10 @@
return touch ? touch.clientY || touch.pageY || 0 : 0
},
startRecord(event) {
if (this.isBlocked) {
uni.showToast({ title: '已加入黑名单,无法继续发送', icon: 'none' })
return
}
if (!this.recorderManager || this.uploadingVoice) return
this.showEmoji = false
this.recording = true
@ -681,15 +800,45 @@
prompt: profile.intro || this.companion.prompt
}
},
async report() {
openReportPanel() {
this.showSafety = false
await reportIeRoom(this.roomId, { reportedUserId: this.targetUserId, reasonType: 'other', reasonText: '聊天内容不适' })
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' })
},
async block() {
this.showSafety = false
if (this.targetUserId) await blockIeUser(this.targetUserId, '聊天中拉黑')
uni.showToast({ title: '已拉黑', icon: 'none' })
if (!this.targetUserId) {
uni.showToast({ title: '拉黑对象缺失', icon: 'none' })
return
}
uni.showModal({
title: '加入黑名单?',
content: '加入后你们不能继续互相发送消息,也不会再次随机匹配到对方。',
confirmText: '加入',
confirmColor: '#e85d75',
success: async (res) => {
if (!res.confirm) return
await blockIeUser(this.targetUserId, '聊天中拉黑')
this.isBlocked = true
this.showEmoji = false
this.voiceMode = false
uni.showToast({ title: '已加入黑名单', icon: 'none' })
}
})
},
async finishRoom() {
if (this.finishing) return
@ -925,6 +1074,8 @@
.radio-copy {
position: relative;
z-index: 1;
flex: 1;
min-width: 0;
}
.radio-title {
@ -938,6 +1089,31 @@
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);
}
.silent-modes {
position: relative;
z-index: 1;
@ -1098,6 +1274,13 @@
background: rgba(255, 255, 255, 0.72);
}
.video-bubble {
padding: 0;
border: none;
background: transparent;
overflow: hidden;
}
.chat-image {
display: block;
width: 260rpx;
@ -1106,6 +1289,71 @@
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;
@ -1254,6 +1502,10 @@
font-weight: 800;
}
.send.disabled {
opacity: .46;
}
.safety-mask {
position: fixed;
left: 0;
@ -1295,6 +1547,62 @@
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;

88
package1/ieBrowser/profileSetup.vue

@ -4,10 +4,10 @@
<view class="nav">
<view class="back" @tap="back"></view>
<view class="title">{{ isEdit ? '编辑 i/e 资料' : '先认识一下你' }}</view>
<view class="step">{{ step }}/4</view>
<view class="step">{{ step }}/5</view>
</view>
<view class="progress"><view :style="{ width: step * 25 + '%' }"></view></view>
<view class="progress"><view :style="{ width: step * 20 + '%' }"></view></view>
<view class="card" v-if="step === 1">
<view class="eyebrow">Your Energy</view>
@ -52,6 +52,22 @@
</view>
<view class="card" v-if="step === 4">
<view class="eyebrow">Persona Cards</view>
<view class="headline">上传你的人格卡片</view>
<view class="card-desc">最多 5 像个人主页的形象展示别人点击头像时可以看到</view>
<view class="persona-grid">
<view class="persona-item" v-for="(img, index) in form.personaImages" :key="img">
<image :src="img" mode="aspectFill" @tap="previewPersona(index)"></image>
<view class="persona-delete" @tap.stop="removePersonaImage(index)">×</view>
</view>
<view class="persona-add" v-if="form.personaImages.length < 5" @tap="choosePersonaImages">
<view class="plus"></view>
<view>添加图片</view>
</view>
</view>
</view>
<view class="card" v-if="step === 5">
<view class="eyebrow">Match Target</view>
<view class="headline">你现在想遇见哪类人</view>
<view class="target-card" v-for="item in targetOptions" :key="item.key" :class="{ active: form.targetModePreference === item.key }" @tap="form.targetModePreference = item.key">
@ -68,7 +84,7 @@
<view class="actions">
<view class="ghost" @tap="prev" v-if="step > 1">上一步</view>
<view class="solid" @tap="next">{{ step === 4 ? '进入 i/e 此刻' : '继续' }}</view>
<view class="solid" @tap="next">{{ step === 5 ? '进入 i/e 此刻' : '继续' }}</view>
</view>
</view>
</template>
@ -94,7 +110,8 @@
avatarUrl: '',
gender: 'unknown',
intro: '',
interestTags: []
interestTags: [],
personaImages: []
},
tagOptions: ['慢热', '爱听歌', '夜跑', '自习', '想聊天', '想安静', '情绪低电量', '散步', '电影', '游戏', '摄影', '干饭'],
targetOptions: [
@ -142,6 +159,7 @@
this.form.gender = profile.gender || 'unknown'
this.form.intro = profile.intro || ''
this.form.interestTags = profile.interestTags || []
this.form.personaImages = profile.personaImages || []
},
toggleTag(tag) {
const index = this.form.interestTags.indexOf(tag)
@ -208,6 +226,59 @@
// uni.hideLoading();
}, 1000)
},
choosePersonaImages() {
const remain = 5 - this.form.personaImages.length
if (remain <= 0) {
uni.showToast({ title: '最多上传 5 张', icon: 'none' })
return
}
uni.chooseMedia({
count: remain,
mediaType: ['image'],
sourceType: ['camera', 'album'],
camera: 'back',
success: async (res) => {
const files = res.tempFiles || []
for (const file of files) {
if (this.form.personaImages.length >= 5) break
const url = await this.uploadPersonaFile(file.tempFilePath)
if (url) this.form.personaImages.push(url)
}
}
})
},
uploadPersonaFile(path) {
if (!path) return Promise.resolve('')
const hiverToken = uni.getStorageSync('hiver_token')
uni.showLoading({ title: '上传中...', mask: true })
return new Promise((resolve) => {
uni.uploadFile({
url: this.tui.interfaceUrl() + '/upload/file',
filePath: path,
name: 'file',
header: {
'content-type': 'multipart/form-data',
'accessToken': hiverToken
},
success: (uploadFileRes) => {
try {
const data = JSON.parse(uploadFileRes.data)
resolve(data.result || '')
} catch (e) {
resolve('')
}
},
fail: () => resolve(''),
complete: () => uni.hideLoading()
})
})
},
removePersonaImage(index) {
this.form.personaImages.splice(index, 1)
},
previewPersona(index) {
uni.previewImage({ urls: this.form.personaImages, current: this.form.personaImages[index] })
},
validateStep() {
if (this.step === 2 && !this.form.anonymousName.trim()) {
uni.showToast({ title: '先起一个昵称吧', icon: 'none' })
@ -223,7 +294,7 @@
async next() {
if (this.submitting) return
if (!this.validateStep()) return
if (this.step < 4) {
if (this.step < 5) {
this.step += 1
return
}
@ -264,6 +335,7 @@
.card { margin-top: 36rpx; padding: 38rpx 32rpx; border-radius: 46rpx; border: 1rpx solid rgba(255,255,255,.82); background: rgba(255,255,255,.64); box-shadow: 0 26rpx 80rpx rgba(96,112,160,.14); backdrop-filter: blur(26rpx); }
.eyebrow { color: rgba(21,26,45,.42); font-size: 22rpx; letter-spacing: 5rpx; text-transform: uppercase; }
.headline { margin-top: 16rpx; margin-bottom: 28rpx; font-size: 46rpx; line-height: 60rpx; font-weight: 800; }
.card-desc { margin: -10rpx 0 24rpx; color: rgba(21,26,45,.52); font-size: 24rpx; line-height: 38rpx; }
.mode-card, .target-card { display: flex; align-items: center; margin-top: 22rpx; padding: 28rpx; border-radius: 34rpx; background: rgba(238,244,255,.68); border: 2rpx solid transparent; }
.mode-card.active, .target-card.active { border-color: #a9ffe7; background: rgba(223,254,244,.7); transform: scale(1.01); }
.mark { width: 82rpx; height: 82rpx; margin-right: 22rpx; border-radius: 28rpx; text-align: center; line-height: 82rpx; font-size: 46rpx; font-weight: 800; }
@ -288,6 +360,12 @@
.tag-grid { display: flex; flex-wrap: wrap; }
.tag { margin: 0 14rpx 18rpx 0; padding: 16rpx 24rpx; border-radius: 999rpx; color: rgba(21,26,45,.62); background: rgba(238,244,255,.82); font-size: 24rpx; }
.tag.active { color: #11162a; background: #a9ffe7; font-weight: 800; }
.persona-grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
.persona-item, .persona-add { position: relative; width: 184rpx; height: 224rpx; border-radius: 30rpx; overflow: hidden; background: rgba(238,244,255,.82); }
.persona-item image { width: 100%; height: 100%; display: block; }
.persona-delete { position: absolute; right: 12rpx; top: 12rpx; width: 42rpx; height: 42rpx; border-radius: 50%; text-align: center; line-height: 38rpx; color: #fff; background: rgba(21,26,45,.56); font-size: 34rpx; }
.persona-add { display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(21,26,45,.46); border: 2rpx dashed rgba(108,105,216,.22); box-sizing: border-box; font-size: 23rpx; }
.persona-add .plus { margin-bottom: 10rpx; color: #6c69d8; font-size: 44rpx; font-weight: 800; }
.actions { position: fixed; left: 32rpx; right: 32rpx; bottom: 42rpx; display: flex; gap: 18rpx; }
.ghost, .solid { flex: 1; height: 96rpx; border-radius: 999rpx; text-align: center; line-height: 96rpx; font-size: 28rpx; font-weight: 800; }
.ghost { color: rgba(21,26,45,.58); background: rgba(255,255,255,.68); }

140
package1/ieBrowser/settings.vue

@ -3,64 +3,154 @@
<view class="top-safe" :style="{ height: menuButtonInfo.top + 'px' }"></view>
<view class="nav">
<view class="back" @tap="back"></view>
<view class="title">设置</view>
<view class="title">安全</view>
<view class="ghost"></view>
</view>
<view class="panel" v-for="group in groups" :key="group.title">
<view class="panel-title">{{ group.title }}</view>
<view class="row" v-for="item in group.items" :key="item.name" @tap="tapItem(item)">
<view>
<view class="name">{{ item.name }}</view>
<view class="desc">{{ item.desc }}</view>
<view class="hero">
<view class="hero-title">安全中心</view>
<view class="hero-sub">这里只有黑名单和举报记录方便你随时管理边界</view>
</view>
<view class="arrow"></view>
<view class="tabs">
<view class="tab" :class="{ active: activeTab === 'blocks' }" @tap="switchTab('blocks')">黑名单</view>
<view class="tab" :class="{ active: activeTab === 'reports' }" @tap="switchTab('reports')">举报记录</view>
</view>
<view class="panel" v-if="activeTab === 'blocks'">
<view class="panel-title">已拉黑对象</view>
<view class="row" v-for="item in blocks" :key="item.id">
<view class="row-main">
<view class="name">用户 {{ item.blockedUserId }}</view>
<view class="desc">{{ item.reason || '已加入黑名单,不会再次出现在推荐中。' }}</view>
<view class="time">{{ item.createTime || '' }}</view>
</view>
<view class="action danger" @tap="confirmUnblock(item)">解除</view>
</view>
<view class="empty" v-if="!loading && !blocks.length">还没有拉黑任何人</view>
<view class="empty" v-else>{{ loading ? '正在加载...' : (hasMore ? '上滑加载更多' : '没有更多了') }}</view>
</view>
<view class="panel" v-if="activeTab === 'reports'">
<view class="panel-title">我的举报记录</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="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>
</view>
<view class="empty" v-if="!loading && !reports.length">还没有举报记录</view>
<view class="empty" v-else>{{ loading ? '正在加载...' : (hasMore ? '上滑加载更多' : '没有更多了') }}</view>
</view>
</view>
</template>
<script>
import { pageIeBlocks, pageIeReports, unblockIeUser } from '@/common/ieApi.js'
export default {
data() {
return {
menuButtonInfo: { top: 44 },
groups: [
{ title: '隐私', items: [
{ name: '半匿名资料', desc: '不展示真实头像、学校、距离。' },
{ name: '聊天保留规则', desc: '匹配成功后保留聊天入口和历史消息。' }
] },
{ title: '安全', items: [
{ name: '黑名单', desc: '被拉黑对象不会再次出现。' },
{ name: '举报记录', desc: '查看已提交的安全反馈。' }
] },
{ title: '提醒', items: [
{ name: '深夜轻提醒', desc: '只在你允许的时间出现。' },
{ name: '今日次数', desc: '每天 3 次,不做无限刷。' }
] }
]
activeTab: 'blocks',
blocks: [],
reports: [],
pageNumber: 1,
pageSize: 10,
hasMore: true,
loading: false
}
},
onLoad() {
if (uni.getMenuButtonBoundingClientRect) this.menuButtonInfo = uni.getMenuButtonBoundingClientRect()
this.loadList()
},
onReachBottom() {
this.loadList()
},
methods: {
back() { uni.redirectTo({ url: '/package1/ieBrowser/index' }) },
tapItem(item) { uni.showToast({ title: item.name, icon: 'none' }) }
switchTab(tab) {
if (this.activeTab === tab) return
this.activeTab = tab
this.pageNumber = 1
this.hasMore = true
this.loading = false
if (tab === 'blocks') this.blocks = []
if (tab === 'reports') this.reports = []
this.loadList()
},
async loadList() {
if (this.loading || !this.hasMore) return
this.loading = true
try {
const page = this.activeTab === 'blocks'
? await pageIeBlocks(this.pageNumber, this.pageSize)
: await pageIeReports(this.pageNumber, this.pageSize)
const list = page && page.records ? page.records : []
if (this.activeTab === 'blocks') {
const exists = new Set(this.blocks.map(item => item.id))
this.blocks = this.blocks.concat(list.filter(item => !exists.has(item.id)))
} else {
const exists = new Set(this.reports.map(item => item.id))
this.reports = this.reports.concat(list.filter(item => !exists.has(item.id)))
}
this.hasMore = page ? (page.current || this.pageNumber) < (page.pages || this.pageNumber) : false
this.pageNumber += 1
} finally {
this.loading = false
}
},
confirmUnblock(item) {
if (!item || !item.blockedUserId) return
uni.showModal({
title: '解除拉黑?',
content: '解除后,对方可能再次出现在随机陪伴中。',
confirmText: '解除',
confirmColor: '#6c69d8',
success: async (res) => {
if (!res.confirm) return
await unblockIeUser(item.blockedUserId)
this.blocks = this.blocks.filter(block => block.id !== item.id)
uni.showToast({ title: '已解除', icon: 'none' })
}
})
},
reasonText(type) {
const map = { other: '其他问题', harassment: '骚扰不适', fraud: '疑似欺诈', abuse: '攻击辱骂' }
return map[type] || '安全反馈'
},
statusText(status) {
if (status === 1) return '已处理'
if (status === 2) return '已驳回'
return '待处理'
}
}
}
</script>
<style lang="scss" scoped>
page { background: #f7f9ff; }
.settings-page { min-height: 100vh; padding: 0 30rpx 60rpx; box-sizing: border-box; color: #151a2d; background: linear-gradient(180deg, #fbfdff, #eef4ff); }
.settings-page { min-height: 100vh; padding: 0 30rpx 60rpx; box-sizing: border-box; color: #151a2d; background: radial-gradient(circle at 82% 8%, rgba(169,255,231,.36), transparent 300rpx), linear-gradient(180deg, #fbfdff, #eef4ff); }
.nav { height: 90rpx; display: flex; align-items: center; }
.back, .ghost { width: 70rpx; font-size: 56rpx; color: rgba(21,26,45,.64); }
.title { flex: 1; text-align: center; font-size: 31rpx; font-weight: 800; }
.hero { margin-top: 20rpx; padding: 34rpx; border-radius: 40rpx; background: rgba(255,255,255,.62); border: 1rpx solid rgba(255,255,255,.86); box-shadow: 0 22rpx 60rpx rgba(96,112,160,.1); }
.hero-title { font-size: 44rpx; font-weight: 900; }
.hero-sub { margin-top: 12rpx; color: rgba(21,26,45,.5); font-size: 24rpx; line-height: 38rpx; }
.tabs { display: flex; gap: 16rpx; margin-top: 24rpx; padding: 10rpx; border-radius: 999rpx; background: rgba(255,255,255,.62); }
.tab { flex: 1; height: 70rpx; line-height: 70rpx; border-radius: 999rpx; text-align: center; color: rgba(21,26,45,.52); font-size: 25rpx; font-weight: 800; }
.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; }
.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; }
.desc { margin-top: 8rpx; color: rgba(21,26,45,.48); font-size: 22rpx; }
.arrow { flex: 1; text-align: right; color: rgba(21,26,45,.34); font-size: 42rpx; }
.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; }
.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; }
</style>

42
package1/ieBrowser/videoPreview.vue

@ -0,0 +1,42 @@
<template>
<view class="preview-page">
<view class="top-safe" :style="{ height: menuButtonInfo.top + 'px' }"></view>
<view class="nav">
<view class="back" @tap="back"></view>
<view class="title">视频</view>
<view class="ghost"></view>
</view>
<video class="video-player" v-if="url" :src="url" :controls="true" :autoplay="true" object-fit="contain"></video>
<view class="empty" v-else>视频地址为空</view>
</view>
</template>
<script>
export default {
data() {
return {
menuButtonInfo: { top: 44 },
url: ''
}
},
onLoad(options) {
if (uni.getMenuButtonBoundingClientRect) this.menuButtonInfo = uni.getMenuButtonBoundingClientRect()
this.url = decodeURIComponent((options && options.url) || '')
},
methods: {
back() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
page { background: #05070d; }
.preview-page { min-height: 100vh; box-sizing: border-box; color: #fff; background: #05070d; }
.nav { height: 88rpx; display: flex; align-items: center; padding: 0 24rpx; box-sizing: border-box; }
.back, .ghost { width: 72rpx; color: rgba(255,255,255,.78); font-size: 56rpx; }
.title { flex: 1; text-align: center; font-size: 30rpx; font-weight: 800; }
.video-player { width: 100vw; height: calc(100vh - 150rpx); background: #000; }
.empty { margin-top: 200rpx; text-align: center; color: rgba(255,255,255,.56); font-size: 26rpx; }
</style>
Loading…
Cancel
Save