聊天界面信息长按操作风格变更

This commit is contained in:
2026-09-24 10:04:34 +08:00
parent 2078923deb
commit bf6396089d
+106 -31
View File
@@ -31,7 +31,7 @@
class="row"
:class="{ mine: m.mine, pick: selecting }"
@click="selecting ? togglePick(m) : tapMsg(m)"
@longpress="act(m)"
@longpress="act(m, i)"
>
<view v-if="selecting" class="ck" :class="{ on: isPicked(m) }" />
<view v-if="!m.mine" class="av" @click.stop="openSender(m)">
@@ -41,7 +41,7 @@
</view>
<view class="col" :class="{ mine: m.mine }">
<text v-if="isGroup && !m.mine" class="who">{{ sender(m) }}</text>
<view class="ball" :class="ballClass(m)">
<view :id="'bubble-' + i" class="ball" :class="ballClass(m)">
<view v-if="m.quote" class="quote">{{ quoteLine(m.quote) }}</view>
<image v-if="m.kind === 'image' && m.fileUrl" class="pic" :src="m.fileUrl" mode="widthFix" />
<view v-else-if="m.kind === 'voice'" class="voice" :style="{ width: voiceW(m) }">
@@ -81,20 +81,13 @@
</view>
</view>
<view v-if="actionMsg && !selecting" class="mask" @click="closeAct">
<view class="sheet" :style="{ paddingBottom: (safeBottom + 12) + 'px' }" @click.stop>
<text class="sheet-t">消息操作</text>
<text class="sheet-s">{{ actPreview }}</text>
<view class="sheet-grid">
<view v-if="actionMsg.kind === 'voice'" class="cell" @click="doPlay"><text class="ce"></text><text>播放</text></view>
<view v-if="actionMsg.kind !== 'voice'" class="cell" @click="doCopy"><text class="ce"></text><text>复制</text></view>
<view class="cell" @click="doForward"><text class="ce"></text><text>转发</text></view>
<view class="cell" @click="doQuote"><text class="ce"></text><text>引用</text></view>
<view class="cell" @click="enterSelect"><text class="ce"></text><text>多选</text></view>
<view v-if="canRecall" class="cell" @click="doRecall"><text class="ce"></text><text>撤回</text></view>
<view class="cell" @click="doDelete"><text class="ce"></text><text>删除</text></view>
<view v-if="actionMsg && !selecting" class="pop-mask" @click="closeAct" @touchmove.stop.prevent>
<view class="pop" :class="{ below: popBelow }" :style="popBoxStyle" @click.stop>
<view v-for="it in actItems" :key="it.k" class="pop-item" @click="runAct(it.k)">
<wd-icon :name="it.icon" size="22px" color="#fff" />
<text class="pop-lab">{{ it.label }}</text>
</view>
<view class="sheet-cancel" @click="closeAct">取消</view>
<view class="pop-caret" :style="{ left: caretLeft + 'px' }" />
</view>
</view>
@@ -202,6 +195,12 @@ export default {
recWillCancel: false,
recSec: 0,
actionMsg: null,
popTop: 0,
popLeft: 8,
popW: 224,
caretLeft: 24,
popBelow: false,
popAt: 0,
quoting: null,
selecting: false,
picked: [],
@@ -232,8 +231,25 @@ export default {
const m = this.actionMsg
return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000)
},
actPreview() {
return previewOf(this.actionMsg)
actItems() {
const m = this.actionMsg
if (!m) return []
const items = []
if (m.kind === 'voice') items.push({ k: 'play', icon: 'play', label: '播放' })
else items.push({ k: 'copy', icon: 'copy', label: '复制' })
items.push({ k: 'forward', icon: 'share', label: '转发' })
items.push({ k: 'quote', icon: 'chat', label: '引用' })
items.push({ k: 'select', icon: 'view-list', label: '多选' })
if (this.canRecall) items.push({ k: 'recall', icon: 'rollback', label: '撤回' })
items.push({ k: 'delete', icon: 'delete', label: '删除' })
return items
},
popBoxStyle() {
return {
top: this.popTop + 'px',
left: this.popLeft + 'px',
width: this.popW + 'px'
}
}
},
onLoad(q) {
@@ -592,14 +608,53 @@ export default {
}
})
},
act(m) {
act(m, index) {
if (!m || m.recalled || this.selecting) return
this.more = false
this.emoji = false
this.hideKb()
this.popAt = Date.now()
this.actionMsg = m
this.$nextTick(() => this.placePop(index))
},
placePop(index) {
const q = uni.createSelectorQuery().in(this)
q.select('#bubble-' + index).boundingClientRect()
q.exec((res) => {
const r = res && res[0]
if (!r || !r.width) return
const sys = uni.getSystemInfoSync()
const n = this.actItems.length
const cols = Math.min(n, 5)
const itemW = 56
const w = cols * itemW
const rows = Math.ceil(n / cols)
const h = rows * 62 + 8
let left = r.left + r.width / 2 - w / 2
left = Math.max(8, Math.min(left, sys.windowWidth - w - 8))
const below = r.top < h + 16
const top = below ? r.bottom + 10 : r.top - h - 10
const caret = r.left + r.width / 2 - left - 6
this.popW = w
this.popLeft = left
this.popTop = Math.max(8, top)
this.popBelow = below
this.caretLeft = Math.max(14, Math.min(caret, w - 26))
})
},
runAct(k) {
if (k === 'copy') return this.doCopy()
if (k === 'play') return this.doPlay()
if (k === 'forward') return this.doForward()
if (k === 'quote') return this.doQuote()
if (k === 'select') return this.enterSelect()
if (k === 'recall') return this.doRecall()
if (k === 'delete') return this.doDelete()
},
closeAct() {
if (Date.now() - this.popAt < 400) return
this.actionMsg = null
},
closeAct() { this.actionMsg = null },
liveMsg() {
const c = store.findConv(this.sid)
const m = this.actionMsg
@@ -840,19 +895,39 @@ export default {
.em { width: 12.5%; text-align: center; font-size: 44rpx; line-height: 88rpx; }
.hint { text-align: center; font-size: 24rpx; color: #9CA3AF; padding: 24rpx; background: #fff; }
.ico { padding: 8rpx 8rpx; font-size: 40rpx; color: #111827; letter-spacing: 2rpx; }
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(17, 24, 39, 0.45); z-index: 40; display: flex; align-items: flex-end; }
.sheet { width: 100%; background: #fff; border-radius: 28rpx 28rpx 0 0; padding: 28rpx 28rpx 12rpx; }
.sheet-t { display: block; text-align: center; font-size: 30rpx; font-weight: 650; }
.sheet-s { display: block; text-align: center; font-size: 24rpx; color: #9CA3AF; margin: 8rpx 0 24rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
.cell {
width: calc(25% - 12rpx); height: 140rpx; background: #F5F7FA; border-radius: 16rpx;
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 22rpx; color: #374151;
.pop-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 40; }
.pop {
position: fixed;
display: flex;
flex-wrap: wrap;
background: #4C4C4C;
border-radius: 8px;
padding: 4px 0;
box-sizing: border-box;
}
.ce { font-size: 32rpx; margin-bottom: 8rpx; color: #111827; font-weight: 650; }
.sheet-cancel {
margin: 20rpx 0 8rpx; height: 88rpx; border-radius: 16rpx; background: #F5F7FA;
display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #111827;
.pop-item {
width: 56px;
height: 62px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
}
.pop-lab { margin-top: 4px; font-size: 12px; line-height: 1; color: #fff; }
.pop-caret {
position: absolute;
width: 0;
height: 0;
border: 6px solid transparent;
border-top-color: #4C4C4C;
bottom: -12px;
}
.pop.below .pop-caret {
top: -12px;
bottom: auto;
border-top-color: transparent;
border-bottom-color: #4C4C4C;
}
.selbar { display: flex; background: #fff; border-top: 1rpx solid #E8ECF2; }
.selbtn { flex: 1; height: 96rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #1677FF; }