refactor: migrate styles to Tailwind and clean up components
- remove SCSS style files for ChatQuickAccess and ChatInputArea, replace with inline Tailwind utilities - uncomment ChatQuickAccess component in ChatMainList to re-enable the quick access bar - simplify speech recognition logic in ChatInputArea, update input placeholder text - replace direct uni.showToast calls with a shared helper function - remove unused keyboard height change listener and redundant keyboard hiding code
This commit is contained in:
@@ -1,58 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-area-wrapper" @touchend="handleVoiceTouchEndFromContainer"
|
<div class="input-area-wrapper" @touchend="handleVoiceTouchEndFromContainer"
|
||||||
@touchcancel="handleVoiceTouchEndFromContainer">
|
@touchcancel="handleVoiceTouchEndFromContainer">
|
||||||
<div v-if="!visibleWaveBtn || speechProvider !== 'wechat'" class="area-input">
|
<div v-if="!visibleWaveBtn || speechProvider !== 'wechat'"
|
||||||
|
class="mx-[8px] mb-[6px] flex min-h-[42px] items-end rounded-[21px] bg-white px-[8px] py-[6px] shadow-[0_2px_12px_rgba(18,39,75,0.06)]">
|
||||||
<!-- 语音/键盘切换 -->
|
<!-- 语音/键盘切换 -->
|
||||||
<div v-if="isSpeechRecognitionSupported" class="input-container-voice" @click="toggleVoiceMode">
|
<div v-if="isSpeechRecognitionSupported" class="flex h-[30px] w-[30px] shrink-0 items-center justify-center"
|
||||||
<img class="voice-icon" v-if="!isVoiceMode"
|
@click="toggleVoiceMode">
|
||||||
|
<img class="h-[22px] w-[22px]" v-if="!isVoiceMode"
|
||||||
src="https://oss.nianxx.cn/mp/static/version_101/home/input_voice_icon.png" />
|
src="https://oss.nianxx.cn/mp/static/version_101/home/input_voice_icon.png" />
|
||||||
<img class="voice-icon" v-else src="https://oss.nianxx.cn/mp/static/version_101/home/input_keyboard_icon.png" />
|
<img class="h-[22px] w-[22px]" v-else
|
||||||
|
src="https://oss.nianxx.cn/mp/static/version_101/home/input_keyboard_icon.png" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入框/语音按钮容器 -->
|
<!-- 输入框/语音按钮容器 -->
|
||||||
<div class="input-button-container"
|
<div class="min-w-0 flex-1" :class="{ 'pl-[4px]': !isSpeechRecognitionSupported }">
|
||||||
:class="{ 'input-button-container--no-voice': !isSpeechRecognitionSupported }">
|
<textarea ref="textareaRef" v-if="!isVoiceMode" v-model="inputMessage" rows="1" maxlength="300"
|
||||||
<textarea ref="textareaRef" v-if="!isVoiceMode" class="textarea" type="text" cursor-spacing="20"
|
:placeholder="placeholder"
|
||||||
confirm-type="send" v-model="inputMessage" auto-height :focus="isFocused" :confirm-hold="true"
|
class="block h-[22px] max-h-[92px] min-h-[22px] w-full resize-none overflow-x-hidden overflow-y-auto border-0 bg-transparent p-0 text-[14px] leading-[22px] text-[#333] outline-none appearance-none placeholder:text-[#A5AAB4] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
:placeholder="placeholder" :show-confirm-bar="false" :hold-keyboard="holdKeyboard" :adjust-position="true"
|
@input="adjustTextareaHeight" @focus="handleFocus" @blur="handleBlur" @touchstart="handleTouchStart"
|
||||||
:disable-default-padding="true" maxlength="300" @confirm="sendMessage" @focus="handleFocus" @blur="handleBlur"
|
@touchend="handleTouchEnd" />
|
||||||
@touchstart="handleTouchStart" @touchend="handleTouchEnd" />
|
|
||||||
|
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
|
||||||
<div v-if="isVoiceMode" class="hold-to-talk-button" @longpress="handleVoiceTouchStart"
|
<div v-if="isVoiceMode"
|
||||||
@touchend="handleVoiceTouchEnd" @touchcancel="handleVoiceTouchEnd">
|
class="flex h-[36px] w-full select-none items-center justify-center bg-transparent text-[14px] leading-[22px] text-[#333] active:bg-gray-100"
|
||||||
|
@longpress="handleVoiceTouchStart" @touchend="handleVoiceTouchEnd" @touchcancel="handleVoiceTouchEnd">
|
||||||
按住 说话
|
按住 说话
|
||||||
</div>
|
</div>
|
||||||
<!-- #endif -->
|
|
||||||
|
|
||||||
<!-- #ifdef APP-PLUS -->
|
|
||||||
<div v-if="isVoiceMode" class="hold-to-talk-button" @touchstart="handleVoiceTouchStart"
|
|
||||||
@touchend="handleVoiceTouchEnd" @touchcancel="handleVoiceTouchEnd">
|
|
||||||
<RecordingWaveBtn v-if="visibleWaveBtn" class="recording-wave-inline" ref="recordingWaveBtnRef" />
|
|
||||||
<text v-else>按住 说话</text>
|
|
||||||
</div>
|
|
||||||
<!-- #endif -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-container-send">
|
<div class="flex h-[30px] w-[34px] shrink-0 items-center justify-end">
|
||||||
<div class="input-container-send-btn" @click="sendMessage">
|
<div
|
||||||
<div v-if="isSessionActive" class="send-stop"> </div>
|
class="flex h-[30px] w-[30px] items-center justify-center rounded-full [background:radial-gradient(39%_39%_at_97%_81%,#79dffb_0%,rgba(138,227,252,0)_100%),radial-gradient(54%_54%_at_3%_70%,#8afcf8_0%,rgba(138,252,248,0)_100%),#0CCD58] shadow-[0_4px_10px_rgba(12,205,88,0.22)]"
|
||||||
<img v-else class="send-icon" src="https://oss.nianxx.cn/mp/static/version_101/home/input_send_icon.png" />
|
@click="sendMessage">
|
||||||
|
<div v-if="isSessionActive" class="h-[10px] w-[10px] rounded-[3px] bg-white"> </div>
|
||||||
|
<img v-else class="h-[20px] w-[20px]"
|
||||||
|
src="https://oss.nianxx.cn/mp/static/version_101/home/input_send_icon.png" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
|
||||||
<!-- 录音按钮 -->
|
<!-- 录音按钮 -->
|
||||||
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
||||||
<!-- #endif -->
|
|
||||||
|
|
||||||
<!-- #ifdef APP-PLUS -->
|
<div class="text-center text-[9px] leading-[12px] text-[#A5AAB4]">
|
||||||
<yao-asdRealSpeech v-if="isSpeechRecognitionSupported && appSpeechVisible" :key="appSpeechKey" ref="appSpeechRef"
|
|
||||||
:options="appSpeechOptions" @result="handleAppSpeechResult" @change="handleAppSpeechChange" />
|
|
||||||
<!-- #endif -->
|
|
||||||
|
|
||||||
<div class="color-99A0AE font-size-9 text-center text-gray-400">
|
|
||||||
内容由AI大模型生成,请仔细鉴别
|
内容由AI大模型生成,请仔细鉴别
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,24 +57,9 @@ import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue";
|
|||||||
let manager = null;
|
let manager = null;
|
||||||
let speechProvider = "";
|
let speechProvider = "";
|
||||||
const isSpeechRecognitionEnabled = ref(true);
|
const isSpeechRecognitionEnabled = ref(true);
|
||||||
const isSpeechRecognitionSupported = ref(false);
|
const isSpeechRecognitionSupported = ref(true);
|
||||||
let appSpeechOptions = {};
|
let appSpeechOptions = {};
|
||||||
|
|
||||||
|
|
||||||
// WechatSI 是微信小程序插件,App 原生基座没有 requirePlugin。
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
const plugin = requirePlugin("WechatSI");
|
|
||||||
manager = plugin.getRecordRecognitionManager();
|
|
||||||
isSpeechRecognitionSupported.value = isSpeechRecognitionEnabled.value && !!manager;
|
|
||||||
speechProvider = manager ? "wechat" : "";
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
// App 端使用 yao-asdRealSpeech;apikey 请在 src/constant/speech.js 中配置。
|
|
||||||
// #ifdef APP-PLUS
|
|
||||||
isSpeechRecognitionSupported.value = isSpeechRecognitionEnabled.value;
|
|
||||||
speechProvider = "app";
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: String,
|
modelValue: String,
|
||||||
holdKeyboard: Boolean,
|
holdKeyboard: Boolean,
|
||||||
@@ -104,8 +80,9 @@ const recordingWaveBtnRef = ref(null);
|
|||||||
const appSpeechRef = ref(null);
|
const appSpeechRef = ref(null);
|
||||||
const appSpeechKey = ref(0);
|
const appSpeechKey = ref(0);
|
||||||
const appSpeechVisible = ref(true);
|
const appSpeechVisible = ref(true);
|
||||||
const placeholder = ref('请输入');
|
const placeholder = ref("快告诉小七您在想什么~");
|
||||||
const inputMessage = ref(props.modelValue || "");
|
const inputMessage = ref(props.modelValue || "");
|
||||||
|
const maxTextareaHeight = 92;
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
const keyboardHeight = ref(0);
|
const keyboardHeight = ref(0);
|
||||||
const isVoiceMode = ref(false);
|
const isVoiceMode = ref(false);
|
||||||
@@ -148,6 +125,14 @@ const startWatchDog = (timeout = 10000) => {
|
|||||||
}, timeout);
|
}, timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const adjustTextareaHeight = () => {
|
||||||
|
const textarea = textareaRef.value;
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
textarea.style.height = "22px";
|
||||||
|
textarea.style.height = `${Math.min(textarea.scrollHeight, maxTextareaHeight)}px`;
|
||||||
|
};
|
||||||
|
|
||||||
const clearAppStopFallback = () => {
|
const clearAppStopFallback = () => {
|
||||||
if (appStopFallbackTimer) {
|
if (appStopFallbackTimer) {
|
||||||
clearTimeout(appStopFallbackTimer);
|
clearTimeout(appStopFallbackTimer);
|
||||||
@@ -202,6 +187,7 @@ watch(
|
|||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
inputMessage.value = val;
|
inputMessage.value = val;
|
||||||
|
nextTick(adjustTextareaHeight);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -211,6 +197,7 @@ watch(inputMessage, (val) => {
|
|||||||
if (val !== props.modelValue) {
|
if (val !== props.modelValue) {
|
||||||
emit("update:modelValue", val);
|
emit("update:modelValue", val);
|
||||||
}
|
}
|
||||||
|
nextTick(adjustTextareaHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换语音/文本模式
|
// 切换语音/文本模式
|
||||||
@@ -236,10 +223,7 @@ const handleVoiceTouchStart = () => {
|
|||||||
showRecordingUI();
|
showRecordingUI();
|
||||||
} else if (speechProvider === "app") {
|
} else if (speechProvider === "app") {
|
||||||
if (!appSpeechOptions.apikey) {
|
if (!appSpeechOptions.apikey) {
|
||||||
uni.showToast({
|
showToast("请先配置语音识别API Key");
|
||||||
title: "请先配置语音识别API Key",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +237,7 @@ const handleVoiceTouchStart = () => {
|
|||||||
const appSpeech = appSpeechRef.value;
|
const appSpeech = appSpeechRef.value;
|
||||||
if (!appSpeech || typeof appSpeech.start !== "function") {
|
if (!appSpeech || typeof appSpeech.start !== "function") {
|
||||||
isAppSpeechStarting.value = false;
|
isAppSpeechStarting.value = false;
|
||||||
uni.showToast({
|
showToast("语音组件未初始化");
|
||||||
title: "语音组件未初始化",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,17 +469,8 @@ const handleAppSpeechChange = (msg) => {
|
|||||||
|
|
||||||
// 监听键盘高度变化
|
// 监听键盘高度变化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 监听键盘弹起
|
|
||||||
uni.onKeyboardHeightChange((res) => {
|
|
||||||
keyboardHeight.value = res.height;
|
|
||||||
if (res.height) {
|
|
||||||
emit("keyboardShow", res.height);
|
|
||||||
} else {
|
|
||||||
emit("keyboardHide");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
initRecord();
|
initRecord();
|
||||||
|
nextTick(adjustTextareaHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -533,10 +505,6 @@ const hideKeyboardAfterSend = () => {
|
|||||||
if (textarea && typeof textarea.blur === "function") {
|
if (textarea && typeof textarea.blur === "function") {
|
||||||
textarea.blur();
|
textarea.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
uni.hideKeyboard();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
@@ -590,16 +558,8 @@ const blurInput = () => {
|
|||||||
if (textarea && typeof textarea.blur === "function") {
|
if (textarea && typeof textarea.blur === "function") {
|
||||||
textarea.blur();
|
textarea.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
uni.hideKeyboard();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({ focusInput, blurInput });
|
defineExpose({ focusInput, blurInput });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "./styles/index.scss";
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
.area-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 24px;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
|
|
||||||
margin: 0 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.input-container-voice {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-self: flex-end;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
.voice-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-button-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&--no-voice {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hold-to-talk-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #fff;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-wave-inline {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container-send {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-self: flex-end;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
.input-container-send-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background:
|
|
||||||
radial-gradient(
|
|
||||||
39% 39% at 97% 81%,
|
|
||||||
#79dffb 0%,
|
|
||||||
rgba(138, 227, 252, 0) 100%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
54% 54% at 3% 70%,
|
|
||||||
#8afcf8 0%,
|
|
||||||
rgba(138, 252, 248, 0) 100%
|
|
||||||
),
|
|
||||||
#0ccd58;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-stop {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea {
|
|
||||||
flex: 1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 92px;
|
|
||||||
min-height: 22px;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 22px;
|
|
||||||
margin: 6px 0;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #cccccc;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -119,12 +119,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入框区域 -->
|
<!-- 输入框区域 -->
|
||||||
<!-- <div>
|
<div>
|
||||||
<ChatQuickAccess />
|
<ChatQuickAccess />
|
||||||
<ChatInputArea ref="inputAreaRef" v-model="inputMessage" :holdKeyboard="holdKeyboard"
|
<ChatInputArea ref="inputAreaRef" v-model="inputMessage" :holdKeyboard="holdKeyboard"
|
||||||
:is-session-active="isSessionActive" :stop-request="stopRequest" @send="sendMessageAction"
|
:is-session-active="isSessionActive" :stop-request="stopRequest" @send="sendMessageAction"
|
||||||
@noHideKeyboard="handleNoHideKeyboard" @keyboardShow="handleKeyboardShow" @keyboardHide="handleKeyboardHide" />
|
@noHideKeyboard="handleNoHideKeyboard" @keyboardShow="handleKeyboardShow" @keyboardHide="handleKeyboardHide" />
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="quick-access flex flex-row ml-12 pt-8 pb-8 scroll-x whitespace-nowrap">
|
<div
|
||||||
<div class="item border-box rounded-50 flex flex-row items-center" v-for="(item, index) in itemList" :key="index"
|
class="flex flex-row gap-x-[9px] ml-[12px] py-[8px] overflow-x-auto whitespace-nowrap [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||||
@click="sendReply(item)">
|
<div class="border border-white bg-white/50 px-3 py-1 rounded-[50px] flex flex-row items-center"
|
||||||
|
v-for="(item, index) in itemList" :key="index" @click="sendReply(item)">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<img v-if="item.icon" class="icon" :src="item.icon" />
|
<img v-if="item.icon" class="w-5 h-5 mr-[2px]" :src="item.icon" />
|
||||||
<span class="font-size-14 theme-color-500 line-height-20">
|
<span class="text-[14px] text-[#2D91FF] leading-5">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +68,3 @@ const sendReply = (item) => {
|
|||||||
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, item);
|
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, item);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "./styles/index.scss";
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
.quick-access {
|
|
||||||
gap: 0 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
border: 1px solid #fff;
|
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
|
||||||
padding: 4px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user