feat: 优化语音输入时的样式和输入框
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="audio-visualizer" ref="containerRef">
|
<div class="audio-visualizer" ref="containerRef">
|
||||||
<div class="visualizer-container" ref="visualizerRef">
|
<div class="visualizer-container" ref="visualizerRef">
|
||||||
<div v-for="(line, index) in lines" :key="index" :class="['line', `line-${line.type}`]"></div>
|
<template v-if="isInitialized">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in lines"
|
||||||
|
:key="index"
|
||||||
|
:class="['line', `line-${line.type}`]"
|
||||||
|
></div> </template
|
||||||
|
>>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,12 +23,17 @@ interface Line {
|
|||||||
const containerRef = ref<HTMLElement>()
|
const containerRef = ref<HTMLElement>()
|
||||||
const visualizerRef = ref<HTMLElement>()
|
const visualizerRef = ref<HTMLElement>()
|
||||||
const lines = ref<Line[]>([])
|
const lines = ref<Line[]>([])
|
||||||
|
const isInitialized = ref<boolean>(false)
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
// 计算需要的线条数量
|
// 计算需要的线条数量
|
||||||
const calculateLines = (): Line[] => {
|
const calculateLines = (): Line[] => {
|
||||||
if (!visualizerRef.value) return []
|
if (!visualizerRef.value) return []
|
||||||
|
|
||||||
const containerWidth = visualizerRef.value.offsetWidth
|
const containerWidth = visualizerRef.value.offsetWidth
|
||||||
|
|
||||||
|
// 如果容器宽度为0或很小,说明还没有正确渲染,返回空数组
|
||||||
|
if (containerWidth < 50) return []
|
||||||
const lineWidth = 0.96 // 每条线的宽度 (rem)
|
const lineWidth = 0.96 // 每条线的宽度 (rem)
|
||||||
const gap = 0.96 // 线条之间的间距 (rem)
|
const gap = 0.96 // 线条之间的间距 (rem)
|
||||||
|
|
||||||
@@ -91,9 +102,18 @@ const calculateLines = (): Line[] => {
|
|||||||
|
|
||||||
// 更新线条
|
// 更新线条
|
||||||
const updateLines = () => {
|
const updateLines = () => {
|
||||||
lines.value = calculateLines()
|
const newLines = calculateLines()
|
||||||
|
if (newLines.length > 0) {
|
||||||
|
lines.value = newLines
|
||||||
|
isInitialized.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
updateLines
|
||||||
|
})
|
||||||
|
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
updateLines()
|
updateLines()
|
||||||
@@ -101,12 +121,45 @@ const handleResize = () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
|
// 立即尝试第一次更新
|
||||||
updateLines()
|
updateLines()
|
||||||
|
|
||||||
|
// 如果第一次没有成功,快速重试
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isInitialized.value) {
|
||||||
|
updateLines()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
// 最后的备用尝试
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isInitialized.value) {
|
||||||
|
updateLines()
|
||||||
|
}
|
||||||
|
}, 150)
|
||||||
|
|
||||||
|
// 使用ResizeObserver监听容器大小变化,更精确地检测初始化时机
|
||||||
|
if (visualizerRef.value && window.ResizeObserver) {
|
||||||
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.contentRect.width > 50 && !isInitialized.value) {
|
||||||
|
updateLines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resizeObserver.observe(visualizerRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,20 @@
|
|||||||
|
|
||||||
<!-- 正常状态:显示输入框 -->
|
<!-- 正常状态:显示输入框 -->
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<input
|
<textarea
|
||||||
id="textarea"
|
id="textarea"
|
||||||
v-show="!isRecording"
|
v-show="!isRecording"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
type="text"
|
rows="1"
|
||||||
placeholder="Ask anything about your desired outfit"
|
placeholder="Ask anything about your desired outfit"
|
||||||
class="text-input"
|
class="text-input"
|
||||||
@keyup.enter="handleSend"
|
@keydown="handleKeyDown"
|
||||||
/>
|
@input="handleInput"
|
||||||
|
ref="textareaRef"
|
||||||
|
></textarea>
|
||||||
<div v-show="isRecording" class="recording-wrapper">
|
<div v-show="isRecording" class="recording-wrapper">
|
||||||
<!-- <div class="recording-animation"> -->
|
<!-- <div class="recording-animation"> -->
|
||||||
<AudioVisualizer />
|
<AudioVisualizer ref="audioVisualizerRef" />
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,13 +53,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onUnmounted } from 'vue'
|
import { ref, onUnmounted, nextTick } from 'vue'
|
||||||
import AudioVisualizer from './AudioVisualizer.vue'
|
import AudioVisualizer from './AudioVisualizer.vue'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
const emit = defineEmits(['send-message'])
|
const emit = defineEmits(['send-message'])
|
||||||
|
|
||||||
const inputValue = ref<string>('')
|
const inputValue = ref<string>('')
|
||||||
const isRecording = ref<boolean>(false)
|
const isRecording = ref<boolean>(false)
|
||||||
|
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
|
||||||
|
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||||
const shortcutList: string[] = [
|
const shortcutList: string[] = [
|
||||||
'Suggest shoe styles',
|
'Suggest shoe styles',
|
||||||
'Recommend evening bags',
|
'Recommend evening bags',
|
||||||
@@ -72,6 +77,34 @@ const handleSend = (): void => {
|
|||||||
console.log('input发送消息:', inputValue.value)
|
console.log('input发送消息:', inputValue.value)
|
||||||
emit('send-message', inputValue.value)
|
emit('send-message', inputValue.value)
|
||||||
inputValue.value = ''
|
inputValue.value = ''
|
||||||
|
// 重置textarea高度
|
||||||
|
if (textareaRef.value) {
|
||||||
|
textareaRef.value.style.height = 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输入事件,自动调整高度
|
||||||
|
const handleInput = (): void => {
|
||||||
|
if (textareaRef.value) {
|
||||||
|
textareaRef.value.style.height = 'auto'
|
||||||
|
|
||||||
|
// const lineHeight = 4.8
|
||||||
|
// const maxLines = 3
|
||||||
|
// const maxHeight = lineHeight * maxLines
|
||||||
|
|
||||||
|
const scrollHeight = textareaRef.value.scrollHeight
|
||||||
|
// const newHeight = Math.min(scrollHeight, maxHeight * 10)
|
||||||
|
|
||||||
|
textareaRef.value.style.height = `${scrollHeight}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +122,24 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClickAudio = (): void => {
|
const handleClickAudio = async (): Promise<void> => {
|
||||||
isRecording.value = !isRecording.value
|
isRecording.value = !isRecording.value
|
||||||
|
|
||||||
|
// 当开始录音时,等待DOM更新后触发AudioVisualizer重新计算
|
||||||
|
if (isRecording.value) {
|
||||||
|
await nextTick()
|
||||||
|
// 立即尝试更新
|
||||||
|
if (audioVisualizerRef.value) {
|
||||||
|
audioVisualizerRef.value.updateLines?.()
|
||||||
|
}
|
||||||
|
// 快速重试
|
||||||
|
setTimeout(() => {
|
||||||
|
if (audioVisualizerRef.value) {
|
||||||
|
audioVisualizerRef.value.updateLines?.()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
if (isRecording.value) {
|
if (isRecording.value) {
|
||||||
startRecording()
|
startRecording()
|
||||||
} else {
|
} else {
|
||||||
@@ -168,7 +216,8 @@ const startRecording = () => {
|
|||||||
speechRecognition.onerror = (event) => {
|
speechRecognition.onerror = (event) => {
|
||||||
console.error('语音识别错误:', event.error)
|
console.error('语音识别错误:', event.error)
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
alert('语音识别失败,请重试')
|
// alert('语音识别失败,请重试')
|
||||||
|
showToast(event.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始识别
|
// 开始识别
|
||||||
@@ -215,8 +264,8 @@ const stopRecording = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
padding: 0 4.85rem 0 5.2rem;
|
padding: 1.5rem 4.85rem 1.5rem 5.2rem;
|
||||||
height: 19.3rem;
|
min-height: 19.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wrapper {
|
.icon-wrapper {
|
||||||
@@ -236,6 +285,7 @@ const stopRecording = () => {
|
|||||||
margin-left: 5.59rem;
|
margin-left: 5.59rem;
|
||||||
margin-right: 3.5rem;
|
margin-right: 3.5rem;
|
||||||
background-color: #888;
|
background-color: #888;
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
@@ -243,26 +293,22 @@ const stopRecording = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input {
|
.text-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// height: 100%;
|
|
||||||
height: auto;
|
|
||||||
// min-height: 4.8rem;
|
// min-height: 4.8rem;
|
||||||
// max-height: 100%;
|
max-height: 14.4rem;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
font-family: 'robotoBold';
|
font-family: 'robotoBold';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 121%;
|
line-height: 4.8rem; /* 设置行高等于实际渲染高度,实现垂直居中 */
|
||||||
// padding-right: 2rem;
|
padding: 0;
|
||||||
margin-right: 4.21rem;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
// resize: none;
|
word-break: break-all;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #888;
|
color: #888;
|
||||||
@@ -288,6 +334,7 @@ const stopRecording = () => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-height: 4.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recording-animation {
|
.recording-animation {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="asistant-container flex flex-column">
|
<div class="asistant-container flex flex-column">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<HeaderTitle light hasSetting />
|
<HeaderTitle hasSetting styleType="2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="loading-container" v-if="isLoading">
|
<div class="loading-container" v-if="isLoading">
|
||||||
<ChatLoading />
|
<ChatLoading />
|
||||||
@@ -80,7 +80,6 @@ onMounted(() => {
|
|||||||
// handleSendMessage('123')
|
// handleSendMessage('123')
|
||||||
})
|
})
|
||||||
|
|
||||||
let loadingTimer: any = null
|
|
||||||
const handleSendMessage = (message: string): void => {
|
const handleSendMessage = (message: string): void => {
|
||||||
console.log('收到消息:', message)
|
console.log('收到消息:', message)
|
||||||
messageList.value.push({
|
messageList.value.push({
|
||||||
@@ -91,27 +90,6 @@ const handleSendMessage = (message: string): void => {
|
|||||||
thumb: ''
|
thumb: ''
|
||||||
})
|
})
|
||||||
//
|
//
|
||||||
|
|
||||||
// 模拟请求延迟
|
|
||||||
// setTimeout(() => {
|
|
||||||
// // 调用NoticeList的方法添加新消息
|
|
||||||
// if (noticeListRef.value) {
|
|
||||||
// isLoading.value = true
|
|
||||||
|
|
||||||
// loadingTimer = setTimeout(() => {
|
|
||||||
// const newMessage: ChatMessage = {
|
|
||||||
// id: Date.now().toString(),
|
|
||||||
// content:
|
|
||||||
// "Thanks for your message! I'm processing your request and will provide you with the best fashion advice.",
|
|
||||||
// userId: '2',
|
|
||||||
// time: new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
|
|
||||||
// thumb: 'https://files-dev.deercal.com/uploads/platforms/logo_code/669933e1b873e798.jpg'
|
|
||||||
// }
|
|
||||||
// messageList.value.push(newMessage)
|
|
||||||
// isLoading.value = false
|
|
||||||
// }, 3000)
|
|
||||||
// }
|
|
||||||
// }, 10000) // 3秒后完成
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
|
|||||||
@@ -11,15 +11,7 @@
|
|||||||
<div class="carousel-container">
|
<div class="carousel-container">
|
||||||
<!-- 左箭头 -->
|
<!-- 左箭头 -->
|
||||||
<div class="nav-arrow left" @click="handleChangeSwiper('prev')">
|
<div class="nav-arrow left" @click="handleChangeSwiper('prev')">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
<van-icon name="arrow-left" color="#fff" size="40" />
|
||||||
<path
|
|
||||||
d="M15 18L9 12L15 6"
|
|
||||||
stroke="white"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-swipe touchable ref="swiperRef">
|
<van-swipe touchable ref="swiperRef">
|
||||||
@@ -37,7 +29,8 @@
|
|||||||
</van-swipe>
|
</van-swipe>
|
||||||
|
|
||||||
<div class="nav-arrow right" @click="handleChangeSwiper('next')">
|
<div class="nav-arrow right" @click="handleChangeSwiper('next')">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
<van-icon name="arrow" color="#fff" size="40" />
|
||||||
|
<!-- <svg width="15" height="26" viewBox="0 0 24 24" fill="none">
|
||||||
<path
|
<path
|
||||||
d="M9 18L15 12L9 6"
|
d="M9 18L15 12L9 6"
|
||||||
stroke="white"
|
stroke="white"
|
||||||
@@ -45,7 +38,7 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,8 +175,8 @@ watch(showVideo, (newValue) => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 5rem;
|
width: 8.6rem;
|
||||||
height: 5rem;
|
height: 8.4rem;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -192,15 +185,9 @@ watch(showVideo, (newValue) => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
box-shadow: 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2), 0 0.2rem 0.4rem rgba(0, 0, 0, 0.1),
|
box-shadow: 0 2rem 2.5rem rgba(0, 0, 0, 0.25), 0 0 6rem rgba(0, 0, 0, 0.25);
|
||||||
inset 0 0.1rem 0.2rem rgba(255, 255, 255, 0.3);
|
|
||||||
border: 0.1rem solid rgba(255, 255, 255, 0.2);
|
border: 0.1rem solid rgba(255, 255, 255, 0.2);
|
||||||
|
filter: drop-shadow(2px 4px 6.6px rgba(0, 0, 0, 0.25));
|
||||||
&:active {
|
|
||||||
transform: translateY(-50%) scale(0.95);
|
|
||||||
box-shadow: 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2), inset 0 0.1rem 0.2rem rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
@@ -223,6 +210,15 @@ watch(showVideo, (newValue) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4.25rem 0 2.65rem;
|
padding: 4.25rem 0 2.65rem;
|
||||||
|
background: radial-gradient(
|
||||||
|
100% 100% at 0% 0%,
|
||||||
|
rgba(115, 115, 115, 0.4) 0%,
|
||||||
|
rgba(115, 115, 115, 0.2) 50%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(35px);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
img {
|
img {
|
||||||
width: 59rem;
|
width: 59rem;
|
||||||
height: 63rem;
|
height: 63rem;
|
||||||
@@ -299,25 +295,19 @@ watch(showVideo, (newValue) => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: blur(1rem);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
box-shadow:
|
box-shadow: 0 0.8rem 1.6rem rgba(0, 0, 0, 0.4), inset 0 0.2rem 0.4rem rgba(255, 255, 255, 0.1),
|
||||||
0 0.8rem 1.6rem rgba(0, 0, 0, 0.4),
|
|
||||||
inset 0 0.2rem 0.4rem rgba(255, 255, 255, 0.1),
|
|
||||||
inset 0 -0.2rem 0.4rem rgba(0, 0, 0, 0.3);
|
inset 0 -0.2rem 0.4rem rgba(0, 0, 0, 0.3);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-0.2rem);
|
transform: translateY(-0.2rem);
|
||||||
box-shadow:
|
box-shadow: 0 1.2rem 2.4rem rgba(0, 0, 0, 0.5),
|
||||||
0 1.2rem 2.4rem rgba(0, 0, 0, 0.5),
|
inset 0 0.2rem 0.4rem rgba(255, 255, 255, 0.15), inset 0 -0.2rem 0.4rem rgba(0, 0, 0, 0.4);
|
||||||
inset 0 0.2rem 0.4rem rgba(255, 255, 255, 0.15),
|
|
||||||
inset 0 -0.2rem 0.4rem rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(0.1rem);
|
transform: translateY(0.1rem);
|
||||||
box-shadow:
|
box-shadow: 0 0.4rem 0.8rem rgba(0, 0, 0, 0.3), inset 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2);
|
||||||
0 0.4rem 0.8rem rgba(0, 0, 0, 0.3),
|
|
||||||
inset 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-icon {
|
.close-icon {
|
||||||
|
|||||||
Reference in New Issue
Block a user