560 行
14 KiB
Plaintext
560 行
14 KiB
Plaintext
<template>
|
|
<view class="chat-textarea-box">
|
|
<view class="chat-header">
|
|
<text class="chat-title">@ 提及演示</text>
|
|
<text class="chat-subtitle">在输入框中敲 @ 选择联系人</text>
|
|
</view>
|
|
|
|
<scroll-view class="message-area" direction="vertical" :show-scrollbar="false" @click="tapList">
|
|
<view class="hint-card">
|
|
<text class="hint-title">使用说明</text>
|
|
<text class="hint-item">· 在下方输入框输入 @ 唤起联系人列表</text>
|
|
<text class="hint-item">· 点击联系人即可插入 @提及</text>
|
|
<text class="hint-item">· 删除 @ 后列表会自动收起</text>
|
|
<view class="animation-switch-row">
|
|
<text class="animation-switch-text">foot-box 动画</text>
|
|
<switch :checked="enableFootBoxAnimation" color="#faad14" @change="onAnimationSwitchChange" />
|
|
</view>
|
|
</view>
|
|
<view class="preview-label">
|
|
<text class="preview-label-text"></text>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<view class="user-list-mask" v-if="showUserList" @click="closeUserList"></view>
|
|
<view class="user-list-panel" :class="{ 'user-list-panel-show': showUserList }">
|
|
<view class="user-list-header">
|
|
<text class="user-list-title">选择联系人</text>
|
|
<text class="user-list-count">{{ userList.length }} 人</text>
|
|
</view>
|
|
<scroll-view class="user-list-scroll" direction="vertical" :show-scrollbar="false">
|
|
<view class="user-list-item" v-for="item in userList" :key="item.id" @click="insertAt(item.id)">
|
|
<view class="user-avatar" :style="{ backgroundColor: getAvatarColor(item.name) }">
|
|
<text class="user-avatar-text">{{ item.name.substring(0, 1) }}</text>
|
|
</view>
|
|
<text class="user-name">{{ item.name }}</text>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<view class="foot-box">
|
|
<view class="foot-box-main">
|
|
<view class="input-wrapper" @click="onEditorTap">
|
|
<editor
|
|
class="preview-editor"
|
|
id="editor-props-demo"
|
|
:type="editorKeyboardType"
|
|
placeholder="输入消息,敲 @ 提及联系人"
|
|
@ready="onEditorReady"
|
|
@focus="onEditorFocus"
|
|
@click="onEditorTap"
|
|
@input="onEditorInput"
|
|
/>
|
|
</view>
|
|
<view class="emoji-toggle" @click="toggleEmojiList">
|
|
<KeyboardIcon v-if="showEmojiList" />
|
|
<EmojiIcon v-else />
|
|
</view>
|
|
</view>
|
|
<view class="emoji-panel"
|
|
:class="{ 'emoji-panel-animate': enableFootBoxAnimation, 'emoji-panel-animate-show': enableFootBoxAnimation && showEmojiList }"
|
|
:style="{ height: (showEmojiList ? emojiPanelDisplayHeight : 0) + 'px', marginTop: (showEmojiList ? emojiPanelMarginTop : 0) + 'px' }"
|
|
>
|
|
<scroll-view class="emoji-scroll" direction="vertical" :show-scrollbar="false" :style="{ height: emojiScrollHeight + 'px' }">
|
|
<view class="emoji-grid">
|
|
<view class="emoji-item" v-for="emoji in emojiList" :key="emoji" @click="insertEmoji(emoji)">
|
|
<text class="emoji-item-text">{{ emoji }}</text>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<view class="keyboard-placeholder" :class="{ 'keyboard-placeholder-animate': enableFootBoxAnimation }" :style="{ height: keyboardHeight + 'px' }"></view>
|
|
<view class="safe-area-inset-bottom" v-if="keyboardHeight == 0 && !showEmojiList"></view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import EmojiIcon from './emoji-icon.uvue'
|
|
import KeyboardIcon from './keyboard-icon.uvue'
|
|
|
|
const keyboardHeight = ref(0)
|
|
const keyboardMaxHeight = ref(0)
|
|
const emojiPanelMarginTop = 10
|
|
const enableFootBoxAnimation = ref(false)
|
|
const emojiPanelFallbackHeight = ref(0)
|
|
const showUserList = ref(false)
|
|
const showEmojiList = ref(false)
|
|
const editorKeyboardType = computed((): string | null => {
|
|
if (showEmojiList.value || showUserList.value) return 'none'
|
|
return null
|
|
})
|
|
const emojiPanelDisplayHeight = computed((): number => {
|
|
const targetHeight = keyboardMaxHeight.value > 0 ? keyboardMaxHeight.value : emojiPanelFallbackHeight.value
|
|
return targetHeight > emojiPanelMarginTop ? targetHeight - emojiPanelMarginTop : 0
|
|
})
|
|
const emojiScrollHeight = computed((): number => {
|
|
return emojiPanelDisplayHeight.value > 20 ? emojiPanelDisplayHeight.value - 20 : 0
|
|
})
|
|
|
|
let editorCtx: null | EditorContext = null
|
|
function onEditorReady() {
|
|
const options: CreateEditorContextAsyncOptions = {
|
|
id: 'editor-props-demo',
|
|
success: (context: EditorContext) => {
|
|
editorCtx = context
|
|
},
|
|
fail: (error: UniError) => {
|
|
console.error('error', `createEditorContextAsync fail: ${error.errMsg}`)
|
|
}
|
|
}
|
|
uni.createEditorContextAsync(options)
|
|
}
|
|
const emojiList = reactive<string[]>([
|
|
'😀', '😄', '😊', '😍', '😘', '😎', '😢', '😭',
|
|
'😡', '👍', '👏', '🙏', '💪', '🎉', '❤️', '🔥',
|
|
'😂', '🤔', '😴', '😋', '🤝', '👌', '🌹', '⭐',
|
|
'😆', '😉', '😇', '🥰', '😜', '🤪', '😬', '🙄',
|
|
'😮', '😱', '🥳', '😤', '🤗', '🤭', '🫡', '😵',
|
|
'💯', '✨', '💖', '💔', '💙', '💚', '💛', '🧡',
|
|
'🎁', '🍻', '☕', '🍰', '🍔', '🍟', '🍓', '🍉',
|
|
'🐶', '🐱', '🐼', '🐰', '🌈', '☀️', '🌙', '⚡'
|
|
])
|
|
|
|
type User = {
|
|
id: string
|
|
name: string
|
|
}
|
|
const userList = reactive<User[]>([
|
|
{ id: '000', name: '张三' },
|
|
{ id: '001', name: '李四' },
|
|
{ id: '002', name: '王五' },
|
|
{ id: '003', name: '赵六' },
|
|
{ id: '004', name: '孙七' },
|
|
{ id: '005', name: '周八' },
|
|
{ id: '006', name: '吴九' },
|
|
{ id: '007', name: '郑十' }
|
|
])
|
|
|
|
const avatarColors = ['#3c99ff', '#52c41a', '#fa8c16', '#722ed1', '#eb2f96', '#13c2c2', '#faad14', '#597ef7']
|
|
|
|
function updateEmojiPanelHeight() {
|
|
const windowInfo = uni.getWindowInfo()
|
|
emojiPanelFallbackHeight.value = Math.floor(windowInfo.windowHeight / 2)
|
|
}
|
|
|
|
function updateKeyboardMaxHeight(height: number) {
|
|
if (height > keyboardMaxHeight.value) {
|
|
keyboardMaxHeight.value = height
|
|
}
|
|
}
|
|
|
|
function getAvatarColor(name: string): string {
|
|
const code = name.charCodeAt(0)
|
|
return avatarColors[code % avatarColors.length]
|
|
}
|
|
|
|
function closeUserList() {
|
|
showUserList.value = false
|
|
}
|
|
|
|
function closeEmojiList() {
|
|
showEmojiList.value = false
|
|
}
|
|
|
|
function onEditorFocus() {
|
|
if (showEmojiList.value) {
|
|
showEmojiList.value = false
|
|
if (keyboardMaxHeight.value > 0) {
|
|
keyboardHeight.value = keyboardMaxHeight.value
|
|
}
|
|
return
|
|
}
|
|
if (!showUserList.value && keyboardMaxHeight.value > 0) {
|
|
keyboardHeight.value = keyboardMaxHeight.value
|
|
}
|
|
}
|
|
|
|
function onEditorTap() {
|
|
if (showEmojiList.value) {
|
|
showEmojiList.value = false
|
|
if (keyboardMaxHeight.value > 0) {
|
|
keyboardHeight.value = keyboardMaxHeight.value
|
|
}
|
|
return
|
|
}
|
|
if (!showUserList.value && keyboardMaxHeight.value > 0) {
|
|
keyboardHeight.value = keyboardMaxHeight.value
|
|
}
|
|
}
|
|
|
|
function onAnimationSwitchChange(event: UniSwitchChangeEvent) {
|
|
enableFootBoxAnimation.value = event.detail.value
|
|
}
|
|
|
|
function toggleEmojiList() {
|
|
showEmojiList.value = !showEmojiList.value
|
|
if (showEmojiList.value) {
|
|
closeUserList()
|
|
updateEmojiPanelHeight()
|
|
keyboardHeight.value = 0
|
|
uni.hideKeyboard()
|
|
}
|
|
}
|
|
|
|
function insertEmoji(emoji: string) {
|
|
editorCtx?.insertText({
|
|
text: emoji
|
|
})
|
|
}
|
|
|
|
function tapList() {
|
|
keyboardHeight.value = 0
|
|
closeEmojiList()
|
|
closeUserList()
|
|
uni.hideKeyboard()
|
|
}
|
|
|
|
function insertAt(id: string) {
|
|
const user = userList.find(item => item.id === id)!
|
|
closeUserList()
|
|
setTimeout(() => {
|
|
editorCtx?.undo()
|
|
editorCtx?.insertMention({
|
|
id: id,
|
|
name: user.name
|
|
})
|
|
}, 0)
|
|
}
|
|
|
|
let lastText = ''
|
|
function onEditorInput(event) {
|
|
if(event.detail.text.includes('@')) {
|
|
const lastTextAtCount = lastText.split('@').length - 1
|
|
const currentTextAtCount = event.detail.text.split('@').length - 1
|
|
if (lastTextAtCount === currentTextAtCount) {
|
|
// 没有新敲 @
|
|
} else if (lastTextAtCount > currentTextAtCount) {
|
|
closeUserList()
|
|
} else if (lastTextAtCount < currentTextAtCount) {
|
|
showUserList.value = true
|
|
showEmojiList.value = false
|
|
keyboardHeight.value = 0
|
|
uni.hideKeyboard()
|
|
}
|
|
} else {
|
|
closeUserList()
|
|
}
|
|
lastText = event.detail.text
|
|
}
|
|
|
|
onLoad(() => {
|
|
updateEmojiPanelHeight()
|
|
uni.onKeyboardHeightChange((res) => {
|
|
if (res.height > 0) {
|
|
updateKeyboardMaxHeight(res.height)
|
|
if (!showEmojiList.value && !showUserList.value) {
|
|
keyboardHeight.value = keyboardMaxHeight.value
|
|
}
|
|
} else if (!showEmojiList.value && !showUserList.value) {
|
|
keyboardHeight.value = 0
|
|
}
|
|
})
|
|
})
|
|
|
|
onResize(() => {
|
|
updateEmojiPanelHeight()
|
|
})
|
|
|
|
function jest_setShowUserList(val : boolean) {
|
|
showUserList.value = val
|
|
}
|
|
|
|
function jest_simulateAtInput(text : string) {
|
|
const lastAtCount = lastText.split('@').length - 1
|
|
const newAtCount = text.split('@').length - 1
|
|
if (text.includes('@')) {
|
|
if (newAtCount > lastAtCount) {
|
|
showUserList.value = true
|
|
showEmojiList.value = false
|
|
} else if (newAtCount < lastAtCount) {
|
|
showUserList.value = false
|
|
}
|
|
} else {
|
|
showUserList.value = false
|
|
}
|
|
lastText = text
|
|
}
|
|
|
|
defineExpose({ jest_setShowUserList, jest_simulateAtInput })
|
|
|
|
onUnload(() => {
|
|
keyboardHeight.value = 0
|
|
uni.offKeyboardHeightChange()
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.chat-textarea-box {
|
|
flex: 1;
|
|
flex-direction: column;
|
|
background-color: #ededed;
|
|
}
|
|
|
|
.chat-header {
|
|
padding: 16px 16px 12px;
|
|
background-color: #ffffff;
|
|
border-bottom-width: 1px;
|
|
border-bottom-style: solid;
|
|
border-bottom-color: #ebebeb;
|
|
}
|
|
|
|
.chat-title {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #1a1a1a;
|
|
line-height: 24px;
|
|
}
|
|
|
|
.chat-subtitle {
|
|
margin-top: 4px;
|
|
font-size: 13px;
|
|
color: #999999;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.message-area {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
background-color: #ededed;
|
|
}
|
|
|
|
.hint-card {
|
|
padding: 14px 16px;
|
|
background-color: #ffffff;
|
|
border-radius: 10px;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.hint-title {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #333333;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.hint-item {
|
|
font-size: 13px;
|
|
color: #666666;
|
|
line-height: 22px;
|
|
}
|
|
|
|
.animation-switch-row {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top-width: 1px;
|
|
border-top-style: solid;
|
|
border-top-color: #f0f0f0;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.animation-switch-text {
|
|
font-size: 13px;
|
|
color: #333333;
|
|
}
|
|
|
|
.preview-label {
|
|
margin-top: 16px;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.preview-label-text {
|
|
font-size: 12px;
|
|
color: #bbbbbb;
|
|
}
|
|
|
|
.user-list-mask {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.25);
|
|
z-index: 998;
|
|
}
|
|
|
|
.user-list-panel {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
max-height: 280px;
|
|
background-color: #ffffff;
|
|
border-top-left-radius: 16px;
|
|
border-top-right-radius: 16px;
|
|
flex-direction: column;
|
|
z-index: 999;
|
|
transform: translateY(100%);
|
|
transition-property: transform;
|
|
transition-duration: 0.25s;
|
|
transition-timing-function: ease-out;
|
|
}
|
|
|
|
.user-list-panel-show {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.user-list-header {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 16px 10px;
|
|
border-bottom-width: 1px;
|
|
border-bottom-style: solid;
|
|
border-bottom-color: #f0f0f0;
|
|
}
|
|
|
|
.user-list-title {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #333333;
|
|
}
|
|
|
|
.user-list-count {
|
|
font-size: 12px;
|
|
color: #bbbbbb;
|
|
}
|
|
|
|
.user-list-scroll {
|
|
flex: 1;
|
|
max-height: 220px;
|
|
}
|
|
|
|
.user-list-item {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 10px 16px;
|
|
border-bottom-width: 1px;
|
|
border-bottom-style: solid;
|
|
border-bottom-color: #f5f5f5;
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 18px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.user-avatar-text {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.user-name {
|
|
flex: 1;
|
|
margin-left: 12px;
|
|
font-size: 15px;
|
|
color: #333333;
|
|
}
|
|
|
|
.foot-box {
|
|
background-color: #f6f6f6;
|
|
}
|
|
|
|
.foot-box-main {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 15px;
|
|
}
|
|
|
|
.input-wrapper {
|
|
flex: 1;
|
|
min-height: 44px;
|
|
max-height: 120px;
|
|
padding: 8px 14px;
|
|
background-color: #ffffff;
|
|
border-radius: 8px;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.preview-editor {
|
|
flex: 1;
|
|
min-height: 28px;
|
|
font-size: 15px;
|
|
color: #333333;
|
|
}
|
|
|
|
.emoji-toggle {
|
|
width: 44px;
|
|
height: 44px;
|
|
margin-left: 10px;
|
|
border-radius: 22px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.emoji-panel {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.emoji-panel-animate {
|
|
transform: scaleY(0.92);
|
|
transform-origin: bottom center;
|
|
transition-property: height, margin-top, opacity, transform, border-top-color;
|
|
transition-duration: 0.24s;
|
|
transition-timing-function: ease-out;
|
|
}
|
|
|
|
.emoji-panel-animate-show {
|
|
transform: scaleY(1);
|
|
}
|
|
|
|
.emoji-scroll {
|
|
border-top-width: 1px;
|
|
border-top-style: solid;
|
|
border-top-color: #ebebeb;
|
|
}
|
|
|
|
.emoji-grid {
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
padding-top: 10px;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.emoji-item {
|
|
width: 12.5%;
|
|
height: 44px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.emoji-item-text {
|
|
font-size: 30px;
|
|
line-height: 44px;
|
|
}
|
|
|
|
.safe-area-inset-bottom {
|
|
height: env(safe-area-inset-bottom);
|
|
min-height: 8px;
|
|
}
|
|
|
|
.keyboard-placeholder {
|
|
height: 0;
|
|
}
|
|
|
|
.keyboard-placeholder-animate {
|
|
transition-property: height;
|
|
transition-duration: 0.24s;
|
|
transition-timing-function: ease-out;
|
|
}
|
|
</style>
|