- DBManager DB创建管理器 - FormatTimeTool - ImageViewer工具 - InputArear工具 - OverlayPage工具 - 下拉刷新容器工具 - 视频查看器工具
742 lines
21 KiB
Vue
742 lines
21 KiB
Vue
<template>
|
||
<view class="input-wrapper" :style="{ '--main-color': themeColor , '--bar-height' :barHeight , 'padding-bottom': dynamicBottom }">
|
||
<!-- 引用条 + 输入框:纵向 flex,引用条在上、输入框在下 -->
|
||
<view class="input-bar-group">
|
||
<!-- 引用回复条 -->
|
||
<view v-if="isReplyValid" class="reply-bar" :style="{ '--dynamic-radius': dynamicRadius + 'rpx' }" @touchstart.prevent>
|
||
<view class="reply-bar-inner">
|
||
<view class="reply-text-wrap">
|
||
<text class="reply-name">{{ replyDisplayName }}</text>
|
||
<text class="reply-content">{{ replyDisplayContent }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 取消引用 -->
|
||
<view v-if="isReplyValid" class="reply-seam" @touchstart.prevent="clearReplyRef">
|
||
<view class="reply-seam-line" />
|
||
<text class="reply-seam-text">取消引用</text>
|
||
</view>
|
||
|
||
<view
|
||
class="input-container"
|
||
:class="{
|
||
'has-reply': isReplyValid,
|
||
'mode-longpress': isLongPressing,
|
||
'mode-cancelling': isLongPressing && isCancelling
|
||
}"
|
||
:style="{
|
||
'--dynamic-radius': dynamicRadius + 'rpx',
|
||
height: containerHeightRpx + 'rpx'
|
||
}"
|
||
@tap.stop
|
||
>
|
||
<!-- 左侧图标 -->
|
||
<view
|
||
class="toggle-wrapper"
|
||
:class="{ 'node-hidden': isLongPressing, 'btn-press': leftBtnPressed }"
|
||
v-if="showLeftBtn"
|
||
@touchstart="leftBtnPressed = true"
|
||
@touchend="leftBtnPressed = false"
|
||
@touchcancel="leftBtnPressed = false"
|
||
@tap="LeftButtonHandle"
|
||
>
|
||
<image class="toggle-icon" :src="LeftIcon" />
|
||
</view>
|
||
|
||
<!-- 中间触发区域 -->
|
||
<view
|
||
class="input-area"
|
||
@touchstart="handleTouchStart"
|
||
@touchmove="handleTouchMove"
|
||
@touchend="handleTouchEnd"
|
||
@touchcancel="handleTouchEnd"
|
||
>
|
||
<textarea
|
||
class="input-text"
|
||
:class="{ 'input-hidden': isLongPressing, 'is-scrollable': !shouldAutoHeight }"
|
||
v-model="localInputText"
|
||
:adjust-position="false"
|
||
:hold-keyboard="holdKeyboard"
|
||
:focus="inputFocus"
|
||
:confirm-type="confirmType"
|
||
:auto-height="shouldAutoHeight"
|
||
:fixed="true"
|
||
:maxlength="-1"
|
||
:style="{
|
||
fontSize: textareaFontSize + 'rpx',
|
||
...(!shouldAutoHeight ? { height: MAX_TEXTAREA_HEIGHT + 'rpx' } : {})
|
||
}"
|
||
@focus="onFocus"
|
||
@blur="onBlur"
|
||
@confirm="handleConfirm"
|
||
@linechange="onLineChange"
|
||
:placeholder="placeholder"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
|
||
<!-- 长按激活时的遮罩/提示 -->
|
||
<view v-if="isLongPressing" class="longpress-overlay">
|
||
<text class="longpress-tip">
|
||
{{ isCancelling ? cancelTip : longPressTip }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧按钮区域 -->
|
||
<view
|
||
class="attachment-btn"
|
||
:class="{ 'is-send': hasText, 'node-hidden': isLongPressing, 'btn-press': rightBtnPressed }"
|
||
@touchstart.prevent="rightBtnPressed = true; handleRightBtnTap()"
|
||
@touchend="rightBtnPressed = false"
|
||
@touchcancel="rightBtnPressed = false"
|
||
>
|
||
<image class="icon send-icon" :src="sendIcon"></image>
|
||
<image class="icon attachment-icon" :src="attachmentIcon"></image>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 悬浮面板 -->
|
||
<floatpanel
|
||
:visible="innerShowPanel"
|
||
:buttons="buttons"
|
||
:height="panelHeight"
|
||
:bottomOffset="panelBottomOffset"
|
||
:horizontalPadding="panelPadding"
|
||
@handleTap="onPanelSelect"
|
||
>
|
||
<template v-if="hasCustomPanel" #default>
|
||
<slot name="panel-content"></slot>
|
||
</template>
|
||
</floatpanel>
|
||
|
||
<!-- 遮罩层:点击空白处关闭面板 -->
|
||
<view
|
||
v-if="innerShowPanel"
|
||
class="panel-mask"
|
||
@tap="setPanelVisible(false)"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick, watch, useSlots , onMounted , onUnmounted } from 'vue'
|
||
import floatpanel from './floatPanel.vue'
|
||
|
||
const slots = useSlots()
|
||
|
||
const props = defineProps({
|
||
// --- 内容与状态 ---
|
||
modelValue: { type: String, default: '' }, // 支持 v-model 直接绑定文字内容
|
||
showPanel: { type: Boolean, default: false }, // 支持 v-model:showPanel
|
||
/** 引用回复:外部传入任意对象,内部仅要求包含 content(string) 与 Name/name;有效时才显示引用条,支持双向 */
|
||
replyRef: { type: [Object, null], default: null },
|
||
/** 外部控制的焦点状态 */
|
||
focus: { type: Boolean, default: false },
|
||
|
||
// --- 提示文字 ---
|
||
placeholder: { type: String, default: '点击输入或按住说话....' },
|
||
longPressTip: { type: String, default: '正在录音,上滑取消' },
|
||
cancelTip: { type: String, default: '松开手指,取消发送' },
|
||
|
||
// --- 样式配置 ---
|
||
themeColor: { type: String, default: '#a846e6' },
|
||
barHeight: { type: String, default: '80rpx' },
|
||
padBottom: { type: String, default: '4rpx' },
|
||
LeftIcon: { type: String, default: '/static/Mic.png' },
|
||
sendIcon: { type: String, default: '/static/send.png' },
|
||
attachmentIcon: { type: String, default: '/static/Attachment.png' },
|
||
|
||
// --- 功能开关 ---
|
||
showLeftBtn: { type: Boolean, default: true }, // 是否显示左侧图标
|
||
|
||
// --- 面板配置 ---
|
||
buttons: { type: Array },
|
||
panelHeight: { type: String, default: '100rpx' },
|
||
panelBottomOffset: { type: String, default: '10rpx' },
|
||
panelPadding: { type: String, default: '15rpx' },
|
||
|
||
// --- 交互参数 ---
|
||
longPressDelay: { type: Number, default: 200}, // 长按判定时间(ms) -- 这个数值不建议高于500 高于会触发长按聚焦文字框
|
||
cancelThreshold: { type: Number, default: 100 }, // 上滑取消判定距离(px)
|
||
|
||
// --- 原生 Input 属性 ---
|
||
adjustPosition: { type: Boolean, default: false },
|
||
holdKeyboard: { type: Boolean, default: true },
|
||
confirmType: { type: String, default: 'send' }
|
||
})
|
||
|
||
const emit = defineEmits([
|
||
'update:modelValue',
|
||
'update:showPanel',
|
||
'update:replyRef',
|
||
'longPressStart',
|
||
'longPressEnd',
|
||
'longPressCancel',
|
||
'send',
|
||
'panelClick',
|
||
'leftButtonClick',
|
||
'focus',
|
||
'blur',
|
||
'keyboardHeightChange'
|
||
])
|
||
|
||
// --- 内部变量 ---
|
||
const inputFocus = ref(false)
|
||
const isLongPressing = ref(false)
|
||
const isCancelling = ref(false)
|
||
const startY = ref(0)
|
||
const lastMoveY = ref(0) // 记录最后一次移动的 Y 坐标
|
||
const longPressTimer = ref(null)
|
||
const focusLock = ref(false)
|
||
const didTouchMove = ref(false) // 是否发生了滑动(用于区分点击聚焦 / 滑动松手)
|
||
const leftBtnPressed = ref(false)
|
||
const rightBtnPressed = ref(false)
|
||
const keyboardHeight = ref(0)
|
||
|
||
// 监听外部传入的 focus prop,立即同步以提高响应速度
|
||
watch(() => props.focus, (val) => {
|
||
if (val !== inputFocus.value) {
|
||
inputFocus.value = val
|
||
// 如果外部设置为 false,触发 blur 事件
|
||
if (!val) {
|
||
emit('blur')
|
||
}
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 双向绑定输入框内容
|
||
const localInputText = ref(props.modelValue)
|
||
watch(() => props.modelValue, (val) => localInputText.value = val)
|
||
watch(localInputText, (val) => emit('update:modelValue', val))
|
||
|
||
// 双向绑定面板显示状态
|
||
const innerShowPanel = ref(props.showPanel)
|
||
watch(() => props.showPanel, (val) => innerShowPanel.value = val)
|
||
const setPanelVisible = (visible) => {
|
||
innerShowPanel.value = visible
|
||
emit('update:showPanel', visible)
|
||
}
|
||
|
||
// 引用回复:仅当包含 content(string) 与 Name/name 时视为有效
|
||
const isReplyValid = computed(() => {
|
||
const r = props.replyRef
|
||
if (!r || typeof r !== 'object') return false
|
||
const hasContent = typeof r.content === 'string'
|
||
const hasName = typeof (r.Name ?? r.name) === 'string'
|
||
return hasContent && hasName
|
||
})
|
||
const replyDisplayName = computed(() => {
|
||
if (!isReplyValid.value) return ''
|
||
const r = props.replyRef
|
||
return (r.Name ?? r.name) ?? ''
|
||
})
|
||
const replyDisplayContent = computed(() => {
|
||
if (!isReplyValid.value) return ''
|
||
const raw = props.replyRef?.content ?? ''
|
||
const oneLine = String(raw).replace(/\s*[\r\n]+\s*/g, ' ').trim()
|
||
return oneLine.length > 40 ? oneLine.slice(0, 40) + '…' : oneLine
|
||
})
|
||
const clearReplyRef = () => {
|
||
emit('update:replyRef', null)
|
||
}
|
||
|
||
// 逻辑计算
|
||
const hasCustomPanel = computed(() => !!slots['panel-content'])
|
||
const hasText = computed(() => localInputText.value.trim().length > 0)
|
||
|
||
// 输入区高度(rpx)
|
||
const textareaHeightRpx = ref(68)
|
||
|
||
// 从 barHeight prop 解析出数值(去掉 'rpx' 后缀)
|
||
const PILL_HEIGHT = computed(() => {
|
||
const heightStr = props.barHeight.replace('rpx', '')
|
||
return parseInt(heightStr) || 80
|
||
})
|
||
|
||
// 字体大小跟随 barHeight 等比例缩放(默认 80rpx 对应 28rpx)
|
||
const textareaFontSize = computed(() => {
|
||
const baseHeight = 80
|
||
const baseFontSize = 28
|
||
const ratio = PILL_HEIGHT.value / baseHeight
|
||
return Math.round(baseFontSize * ratio)
|
||
})
|
||
|
||
const MAX_TEXTAREA_HEIGHT = 300 // textarea 超过此高度时变为可滚动
|
||
const MAX_HEIGHT = 16 + MAX_TEXTAREA_HEIGHT // 容器最大高度 = 内边距 + textarea高度
|
||
const PILL_RADIUS = 50
|
||
const RECT_RADIUS = 36
|
||
|
||
// 当高度超过阈值时 - 输入区域变成可滚动
|
||
const shouldAutoHeight = computed(() => textareaHeightRpx.value < MAX_TEXTAREA_HEIGHT)
|
||
|
||
// 容器高度(rpx)
|
||
const containerHeightRpx = computed(() => {
|
||
// 最小高度 = barHeight - padding (上下各6rpx = 12rpx)
|
||
const minInnerH = PILL_HEIGHT.value - 12
|
||
const innerH = Math.max(minInnerH, textareaHeightRpx.value)
|
||
const h = 12 + innerH // 上下 padding 各 6rpx
|
||
return Math.min(MAX_HEIGHT, Math.max(PILL_HEIGHT.value, h))
|
||
})
|
||
|
||
const dynamicRadius = computed(() => {
|
||
const containerH = containerHeightRpx.value
|
||
const pillH = PILL_HEIGHT.value
|
||
if (containerH <= pillH) return PILL_RADIUS
|
||
if (containerH >= MAX_HEIGHT) return RECT_RADIUS
|
||
return Math.round(PILL_RADIUS - (containerH - pillH) * (PILL_RADIUS - RECT_RADIUS) / (MAX_HEIGHT - pillH))
|
||
})
|
||
|
||
const dynamicBottom = computed(() => {
|
||
if (keyboardHeight.value > 0) {
|
||
// 键盘弹出时:直接使用键盘的物理像素高度 + 你自己的下边距
|
||
return `calc(${keyboardHeight.value}px + ${props.padBottom})`
|
||
} else {
|
||
// 键盘收起时:回归底部,兼容全面屏小黑条
|
||
return `calc(env(safe-area-inset-bottom) + ${props.padBottom})`
|
||
}
|
||
})
|
||
|
||
const onLineChange = (e) => {
|
||
let { height, heightRpx } = e.detail || {}
|
||
// 部分平台只有 height(px),需转换为 rpx
|
||
if ((!heightRpx || heightRpx <= 0) && height > 0) {
|
||
const winWidth = uni.getSystemInfoSync().windowWidth || 375
|
||
heightRpx = Math.round(height * 750 / winWidth)
|
||
}
|
||
// if (heightRpx > 0) textareaHeightRpx.value = heightRpx
|
||
if (heightRpx > 0) {
|
||
textareaHeightRpx.value = heightRpx;
|
||
// 触发高度变化事件,通知父组件滚动列表
|
||
nextTick(() => {
|
||
emit('heightChange');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 清空时重置高度
|
||
watch(localInputText, (val) => {
|
||
if (!val.trim()) {
|
||
// 重置为 barHeight - padding (上下各6rpx = 12rpx)
|
||
textareaHeightRpx.value = PILL_HEIGHT.value - 12
|
||
}
|
||
})
|
||
|
||
// --- 事件处理 ---
|
||
const handleTouchStart = (e) => {
|
||
didTouchMove.value = false
|
||
if (focusLock.value || hasText.value || inputFocus.value) return
|
||
const touchY = e.touches[0].clientY
|
||
startY.value = touchY
|
||
lastMoveY.value = touchY // 初始化为起始位置
|
||
longPressTimer.value = setTimeout(() => {
|
||
isLongPressing.value = true
|
||
isCancelling.value = false
|
||
uni.vibrateShort();
|
||
emit('longPressStart');
|
||
}, props.longPressDelay)
|
||
}
|
||
|
||
const handleTouchMove = (e) => {
|
||
if (!isLongPressing.value) {
|
||
didTouchMove.value = true // 发生滑动,touchend 时不要触发聚焦
|
||
return // 不拦截,让 textarea 能正常滚动
|
||
}
|
||
e.preventDefault()
|
||
const moveY = e.touches[0].clientY
|
||
lastMoveY.value = moveY
|
||
isCancelling.value = (startY.value - moveY > props.cancelThreshold)
|
||
}
|
||
|
||
const handleTouchEnd = () => {
|
||
if (longPressTimer.value) {
|
||
clearTimeout(longPressTimer.value)
|
||
longPressTimer.value = null
|
||
}
|
||
|
||
if (isLongPressing.value) {
|
||
const shouldCancel = (startY.value - lastMoveY.value > props.cancelThreshold)
|
||
|
||
emit(shouldCancel ? 'longPressCancel' : 'longPressEnd')
|
||
isLongPressing.value = false
|
||
isCancelling.value = false
|
||
focusLock.value = true
|
||
setTimeout(() => { focusLock.value = false }, 200) // 减少锁定时间,提高响应速度
|
||
} else {
|
||
// 只有真正的点击(无滑动)才触发聚焦,滑动松手不触发
|
||
// 优化:如果外部已经设置了 focus,或者没有滑动,则触发聚焦
|
||
if (!focusLock.value && !didTouchMove.value) {
|
||
// 如果外部传入的 focus 为 true,直接同步内部状态
|
||
if (props.focus) {
|
||
inputFocus.value = true
|
||
} else if (!inputFocus.value) {
|
||
// 只有在未聚焦时才触发聚焦
|
||
nextTick(() => {
|
||
inputFocus.value = true
|
||
emit('focus')
|
||
})
|
||
}
|
||
}
|
||
// 重置滑动标志
|
||
didTouchMove.value = false
|
||
}
|
||
}
|
||
|
||
const onFocus = () => {
|
||
inputFocus.value = true
|
||
emit('focus');
|
||
}
|
||
const onBlur = () => {
|
||
inputFocus.value = false
|
||
emit('blur'); // 触发 blur 事件,让父组件知道焦点已失去
|
||
}
|
||
|
||
const handleRightBtnTap = () => {
|
||
if (hasText.value) {
|
||
emit('send', { type: 'text', content: localInputText.value })
|
||
localInputText.value = ''
|
||
} else {
|
||
setPanelVisible(!innerShowPanel.value)
|
||
}
|
||
}
|
||
|
||
// 发送键
|
||
const handleConfirm = () => {
|
||
if (hasText.value) {
|
||
emit('send', { type: 'text', content: localInputText.value })
|
||
localInputText.value = ''
|
||
}
|
||
}
|
||
|
||
const onPanelSelect = (item) => {
|
||
emit('panelClick', item.button)//返回对应的button数据
|
||
setTimeout(() => setPanelVisible(false), 150)
|
||
}
|
||
|
||
const LeftButtonHandle = () => {
|
||
emit('leftButtonClick');
|
||
}
|
||
|
||
onMounted(() => {
|
||
uni.onKeyboardHeightChange(res => {
|
||
keyboardHeight.value = res.height;
|
||
emit('keyboardHeightChange', res.height);
|
||
});
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.offKeyboardHeightChange();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
$paddingBottom : var(--paddingBottom);
|
||
$MainColor: var(--main-color);
|
||
|
||
.input-wrapper {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
z-index: 999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 0 12rpx;
|
||
transition: padding-bottom 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
height 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
/* 引用条 + 输入框的纵向容器 */
|
||
.input-bar-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
align-items: stretch;
|
||
}
|
||
|
||
/* 引用回复条*/
|
||
.reply-bar {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background-color: rgba(38, 38, 38, 0.6);
|
||
backdrop-filter: blur(20rpx);
|
||
-webkit-backdrop-filter: blur(20rpx);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-bottom: none;
|
||
border-radius: var(--dynamic-radius, 50rpx) var(--dynamic-radius, 50rpx) 0 0;
|
||
margin-bottom: -1rpx;
|
||
padding: 8rpx 10rpx 10rpx;
|
||
}
|
||
.reply-bar-inner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2rpx;
|
||
}
|
||
.reply-text-wrap {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2rpx;
|
||
}
|
||
/* Name */
|
||
.reply-name {
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
color: rgba(255, 255, 255, 0.42);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
/* Content */
|
||
.reply-content {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
color: rgba(255, 255, 255, 0.88);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
width: 100%;
|
||
text-align: center;
|
||
max-width: 100%;
|
||
}
|
||
/* 引用内容渐变线 */
|
||
.reply-seam-line {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
height: 1px;
|
||
background: linear-gradient(
|
||
to right,
|
||
transparent 0%,
|
||
transparent 28%,
|
||
rgba(255, 255, 255, 0.2) 48%,
|
||
rgba(255, 255, 255, 0.2) 52%,
|
||
transparent 72%,
|
||
transparent 100%
|
||
);
|
||
}
|
||
/* 接缝 + 「取消引用」文字 */
|
||
.reply-seam {
|
||
position: relative;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background-color: rgba(38, 38, 38, 0.6);
|
||
backdrop-filter: blur(20rpx);
|
||
-webkit-backdrop-filter: blur(20rpx);
|
||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||
padding: 5rpx 10rpx 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.reply-seam-text {
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.input-container {
|
||
display: flex;
|
||
box-sizing: border-box;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-height: var(--bar-height);
|
||
max-height: 360rpx;
|
||
background-color: rgba(38, 38, 38, 0.6);
|
||
backdrop-filter: blur(20rpx);
|
||
-webkit-backdrop-filter: blur(20rpx);
|
||
border-radius: var(--dynamic-radius, 50rpx);
|
||
padding-top: 6rpx;
|
||
padding-bottom: 6rpx;
|
||
padding-left: 6rpx;
|
||
padding-right: 6rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
/* 高度、圆角、背景色 带缓动过渡 */
|
||
transition: height 0.28s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
border-radius 0.28s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
background-color 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
backdrop-filter 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
overflow: hidden;
|
||
|
||
&.has-reply {
|
||
border-radius: 0 0 var(--dynamic-radius, 36rpx) var(--dynamic-radius, 36rpx);
|
||
}
|
||
&.mode-longpress {
|
||
background-color: var(--main-color) !important;
|
||
backdrop-filter: blur(20rpx);
|
||
-webkit-backdrop-filter: blur(20rpx);
|
||
}
|
||
&.mode-cancelling {
|
||
background-color: rgba(255, 77, 79, 0.8) !important;
|
||
backdrop-filter: blur(20rpx);
|
||
-webkit-backdrop-filter: blur(20rpx);
|
||
}
|
||
}
|
||
|
||
/* 长按时两侧按钮的平滑缩小/隐藏(带过渡动效) */
|
||
.node-hidden {
|
||
width: 0 !important;
|
||
height: 0 !important;
|
||
opacity: 0 !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
transform: scale(0);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.toggle-wrapper {
|
||
height: calc(var(--bar-height) - 12rpx);
|
||
width: calc(var(--bar-height) - 12rpx);
|
||
aspect-ratio: 1;
|
||
border-radius: 999rpx;
|
||
background-color: $MainColor;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
/* 按下缩放 + 长按隐藏时的缩小动效 */
|
||
transition: width 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
height 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
opacity 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
margin 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
padding 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
transform 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
|
||
&.btn-press {
|
||
transform: scale(0.92);
|
||
}
|
||
|
||
.toggle-icon {
|
||
height: 70%;
|
||
width: 70%;
|
||
}
|
||
}
|
||
|
||
.input-area {
|
||
flex: 1;
|
||
margin: 0 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
position: relative;
|
||
overflow: visible;
|
||
min-width: 0;
|
||
min-height: calc(var(--bar-height) - 12rpx);
|
||
max-height: 300rpx;
|
||
|
||
.input-placeholder {
|
||
color: #999999;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.input-text {
|
||
width: 100%;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
transition: opacity 0.2s;
|
||
line-height: 1.3;
|
||
box-sizing: border-box;
|
||
padding: 0;
|
||
|
||
&.input-hidden {
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
}
|
||
|
||
/* 超过最大高度后变为可滚动 */
|
||
&.is-scrollable {
|
||
overflow-y: auto !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.longpress-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
pointer-events: none;
|
||
|
||
.longpress-tip {
|
||
font-size: 30rpx;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.attachment-btn {
|
||
--offset: 6%;
|
||
width: calc(var(--bar-height) - 12rpx);
|
||
height: calc(var(--bar-height) - 12rpx);
|
||
border-radius: 999rpx;
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
/* 按下缩放 + 长按隐藏时的缩小动效 + 主题色渐变 */
|
||
transition: width 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
height 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
opacity 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
margin 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
padding 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
transform 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
background-color 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||
filter 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
|
||
&.btn-press {
|
||
transform: scale(0.92);
|
||
}
|
||
|
||
.icon {
|
||
position: absolute;
|
||
width: 50%;
|
||
height: 50%;
|
||
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||
}
|
||
|
||
.attachment-icon {
|
||
opacity: 1;
|
||
transform: rotate(0deg) scale(1);
|
||
}
|
||
|
||
.send-icon {
|
||
opacity: 0;
|
||
transform: rotate(-90deg) scale(0) translateX(var(--offset));
|
||
}
|
||
|
||
&.is-send {
|
||
background-color: $MainColor;
|
||
filter: brightness(1.1); // 相对主要色变得亮一点
|
||
|
||
.attachment-icon {
|
||
opacity: 0;
|
||
transform: rotate(90deg) scale(0);
|
||
}
|
||
|
||
.send-icon {
|
||
opacity: 1;
|
||
transform: rotate(0deg) scale(1) translateX(var(--offset));
|
||
}
|
||
}
|
||
}
|
||
|
||
.panel-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 998;
|
||
}
|
||
</style> |