Merge branches 'master' and 'master' of https://gitee.com/lvYeJu/lane-crawford-3

This commit is contained in:
X1627315083
2025-10-20 15:34:14 +08:00
7 changed files with 249 additions and 193 deletions

View File

@@ -1,13 +1,19 @@
<template>
<div class="audio-visualizer" ref="containerRef">
<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>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, onMounted, nextTick } from 'vue'
// 定义线条类型
interface Line {
@@ -17,12 +23,16 @@ interface Line {
const containerRef = ref<HTMLElement>()
const visualizerRef = ref<HTMLElement>()
const lines = ref<Line[]>([])
const isInitialized = ref<boolean>(false)
// 计算需要的线条数量
const calculateLines = (): Line[] => {
if (!visualizerRef.value) return []
const containerWidth = visualizerRef.value.offsetWidth
// 如果容器宽度为0或很小说明还没有正确渲染返回空数组
if (containerWidth < 50) return []
const lineWidth = 0.96 // 每条线的宽度 (rem)
const gap = 0.96 // 线条之间的间距 (rem)
@@ -66,7 +76,7 @@ const calculateLines = (): Line[] => {
// 生成一个完整周期的内容
const oneCycle: Line[] = []
oneCycle.push(...basePattern)
// 添加重复模式直到填满一个周期
let currentIndex = basePattern.length
while (currentIndex < linesNeeded) {
@@ -91,22 +101,38 @@ const calculateLines = (): Line[] => {
// 更新线条
const updateLines = () => {
lines.value = calculateLines()
if (isInitialized.value) return
const newLines = calculateLines()
if (newLines.length > 0) {
lines.value = newLines
isInitialized.value = true
}
}
// 监听窗口大小变化
const handleResize = () => {
updateLines()
}
// 暴露方法给父组件
defineExpose({
updateLines
})
onMounted(async () => {
await nextTick()
updateLines()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
// 立即尝试第一次更新
updateLines()
// 如果第一次没有成功,快速重试
setTimeout(() => {
if (!isInitialized.value) {
updateLines()
}
}, 50)
// 最后的备用尝试
setTimeout(() => {
if (!isInitialized.value) {
updateLines()
}
}, 150)
})
</script>
@@ -117,7 +143,6 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
padding: 20px;
min-height: 200px;
border-radius: 10px;
}

View File

@@ -13,7 +13,8 @@
<div class="input-container">
<!-- 加号图标 -->
<div class="icon-wrapper">
<SvgIcon name="plus" size="40" />
<SvgIcon v-if="!isRecording" name="plus" size="40" />
<SvgIcon v-else name="pause" size="60" @click="stopRecording" />
</div>
<!-- 分隔线 -->
@@ -21,18 +22,20 @@
<!-- 正常状态显示输入框 -->
<div class="input-wrapper">
<input
<textarea
id="textarea"
v-show="!isRecording"
v-model="inputValue"
type="text"
rows="1"
placeholder="Ask anything about your desired outfit"
class="text-input"
@keyup.enter="handleSend"
/>
@keydown="handleKeyDown"
@input="handleInput"
ref="textareaRef"
></textarea>
<div v-show="isRecording" class="recording-wrapper">
<!-- <div class="recording-animation"> -->
<AudioVisualizer />
<AudioVisualizer ref="audioVisualizerRef" />
<!-- </div> -->
</div>
</div>
@@ -51,13 +54,16 @@
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { ref, onUnmounted, nextTick } from 'vue'
import AudioVisualizer from './AudioVisualizer.vue'
import { showToast } from 'vant'
const emit = defineEmits(['send-message'])
const inputValue = ref<string>('')
const isRecording = ref<boolean>(false)
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const shortcutList: string[] = [
'Suggest shoe styles',
'Recommend evening bags',
@@ -72,6 +78,34 @@ const handleSend = (): void => {
console.log('input发送消息:', inputValue.value)
emit('send-message', 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,10 +123,22 @@ onUnmounted(() => {
}
})
const handleClickAudio = (): void => {
const handleClickAudio = async (): Promise<void> => {
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)
startRecording()
} else {
stopRecording()
@@ -126,7 +172,7 @@ const startRecording = () => {
}
// 识别结果
speechRecognition.onresult = (event) => {
speechRecognition.onresult = (event: any) => {
let finalTranscript = ''
let interimTranscript = ''
@@ -165,10 +211,11 @@ const startRecording = () => {
}
// 识别错误
speechRecognition.onerror = (event) => {
speechRecognition.onerror = (event: any) => {
console.error('语音识别错误:', event.error)
isRecording.value = false
alert('语音识别失败,请重试')
// alert('语音识别失败,请重试')
showToast(event.error)
}
// 开始识别
@@ -215,8 +262,8 @@ const stopRecording = () => {
display: flex;
align-items: center;
background-color: #efefef;
padding: 0 4.85rem 0 5.2rem;
height: 19.3rem;
padding: 1.5rem 4.85rem 1.5rem 5.2rem;
min-height: 19.3rem;
}
.icon-wrapper {
@@ -224,6 +271,7 @@ const stopRecording = () => {
align-items: center;
justify-content: center;
cursor: pointer;
color: #6d6868;
&.send-icon {
margin-left: 4.38rem;
@@ -236,6 +284,7 @@ const stopRecording = () => {
margin-left: 5.59rem;
margin-right: 3.5rem;
background-color: #888;
align-self: center;
}
.input-wrapper {
@@ -243,26 +292,22 @@ const stopRecording = () => {
display: flex;
align-items: center;
overflow: hidden;
// height: 100%;
}
.text-input {
width: 100%;
// height: 100%;
height: auto;
// min-height: 4.8rem;
// max-height: 100%;
max-height: 14.4rem;
border: none;
outline: none;
background: transparent;
font-size: 4rem;
font-family: 'robotoBold';
font-weight: 400;
line-height: 121%;
// padding-right: 2rem;
margin-right: 4.21rem;
line-height: 4.8rem; /* 设置行高等于实际渲染高度,实现垂直居中 */
padding: 0;
color: #000;
// resize: none;
word-break: break-all;
&::placeholder {
color: #888;
@@ -288,6 +333,7 @@ const stopRecording = () => {
flex: 1;
display: flex;
align-items: center;
min-height: 4.8rem;
}
.recording-animation {