聊天界面信息长按操作风格变更
This commit is contained in:
+106
-31
@@ -31,7 +31,7 @@
|
|||||||
class="row"
|
class="row"
|
||||||
:class="{ mine: m.mine, pick: selecting }"
|
:class="{ mine: m.mine, pick: selecting }"
|
||||||
@click="selecting ? togglePick(m) : tapMsg(m)"
|
@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="selecting" class="ck" :class="{ on: isPicked(m) }" />
|
||||||
<view v-if="!m.mine" class="av" @click.stop="openSender(m)">
|
<view v-if="!m.mine" class="av" @click.stop="openSender(m)">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="col" :class="{ mine: m.mine }">
|
<view class="col" :class="{ mine: m.mine }">
|
||||||
<text v-if="isGroup && !m.mine" class="who">{{ sender(m) }}</text>
|
<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>
|
<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" />
|
<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) }">
|
<view v-else-if="m.kind === 'voice'" class="voice" :style="{ width: voiceW(m) }">
|
||||||
@@ -81,20 +81,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="actionMsg && !selecting" class="mask" @click="closeAct">
|
<view v-if="actionMsg && !selecting" class="pop-mask" @click="closeAct" @touchmove.stop.prevent>
|
||||||
<view class="sheet" :style="{ paddingBottom: (safeBottom + 12) + 'px' }" @click.stop>
|
<view class="pop" :class="{ below: popBelow }" :style="popBoxStyle" @click.stop>
|
||||||
<text class="sheet-t">消息操作</text>
|
<view v-for="it in actItems" :key="it.k" class="pop-item" @click="runAct(it.k)">
|
||||||
<text class="sheet-s">{{ actPreview }}</text>
|
<wd-icon :name="it.icon" size="22px" color="#fff" />
|
||||||
<view class="sheet-grid">
|
<text class="pop-lab">{{ it.label }}</text>
|
||||||
<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>
|
</view>
|
||||||
<view class="sheet-cancel" @click="closeAct">取消</view>
|
<view class="pop-caret" :style="{ left: caretLeft + 'px' }" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -202,6 +195,12 @@ export default {
|
|||||||
recWillCancel: false,
|
recWillCancel: false,
|
||||||
recSec: 0,
|
recSec: 0,
|
||||||
actionMsg: null,
|
actionMsg: null,
|
||||||
|
popTop: 0,
|
||||||
|
popLeft: 8,
|
||||||
|
popW: 224,
|
||||||
|
caretLeft: 24,
|
||||||
|
popBelow: false,
|
||||||
|
popAt: 0,
|
||||||
quoting: null,
|
quoting: null,
|
||||||
selecting: false,
|
selecting: false,
|
||||||
picked: [],
|
picked: [],
|
||||||
@@ -232,8 +231,25 @@ export default {
|
|||||||
const m = this.actionMsg
|
const m = this.actionMsg
|
||||||
return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000)
|
return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000)
|
||||||
},
|
},
|
||||||
actPreview() {
|
actItems() {
|
||||||
return previewOf(this.actionMsg)
|
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) {
|
onLoad(q) {
|
||||||
@@ -592,14 +608,53 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
act(m) {
|
act(m, index) {
|
||||||
if (!m || m.recalled || this.selecting) return
|
if (!m || m.recalled || this.selecting) return
|
||||||
this.more = false
|
this.more = false
|
||||||
this.emoji = false
|
this.emoji = false
|
||||||
this.hideKb()
|
this.hideKb()
|
||||||
|
this.popAt = Date.now()
|
||||||
this.actionMsg = m
|
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() {
|
liveMsg() {
|
||||||
const c = store.findConv(this.sid)
|
const c = store.findConv(this.sid)
|
||||||
const m = this.actionMsg
|
const m = this.actionMsg
|
||||||
@@ -840,19 +895,39 @@ export default {
|
|||||||
.em { width: 12.5%; text-align: center; font-size: 44rpx; line-height: 88rpx; }
|
.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; }
|
.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; }
|
.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; }
|
.pop-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 40; }
|
||||||
.sheet { width: 100%; background: #fff; border-radius: 28rpx 28rpx 0 0; padding: 28rpx 28rpx 12rpx; }
|
.pop {
|
||||||
.sheet-t { display: block; text-align: center; font-size: 30rpx; font-weight: 650; }
|
position: fixed;
|
||||||
.sheet-s { display: block; text-align: center; font-size: 24rpx; color: #9CA3AF; margin: 8rpx 0 24rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
display: flex;
|
||||||
.sheet-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
flex-wrap: wrap;
|
||||||
.cell {
|
background: #4C4C4C;
|
||||||
width: calc(25% - 12rpx); height: 140rpx; background: #F5F7FA; border-radius: 16rpx;
|
border-radius: 8px;
|
||||||
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 22rpx; color: #374151;
|
padding: 4px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.ce { font-size: 32rpx; margin-bottom: 8rpx; color: #111827; font-weight: 650; }
|
.pop-item {
|
||||||
.sheet-cancel {
|
width: 56px;
|
||||||
margin: 20rpx 0 8rpx; height: 88rpx; border-radius: 16rpx; background: #F5F7FA;
|
height: 62px;
|
||||||
display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #111827;
|
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; }
|
.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; }
|
.selbtn { flex: 1; height: 96rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #1677FF; }
|
||||||
|
|||||||
Reference in New Issue
Block a user