315 lines
6.7 KiB
Vue
315 lines
6.7 KiB
Vue
<template>
|
||
<div class="chat-input-container">
|
||
<div class="input-wrapper">
|
||
<div class="input-area">
|
||
<textarea
|
||
ref="textareaRef"
|
||
v-model="inputText"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
class="chat-textarea"
|
||
rows="1"
|
||
@keydown="handleKeydown"
|
||
@input="adjustHeight"
|
||
/>
|
||
<button
|
||
:disabled="!canSend"
|
||
class="send-button"
|
||
@click="handleSend"
|
||
>
|
||
<svg
|
||
v-if="!isLoading"
|
||
class="send-icon"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||
/>
|
||
</svg>
|
||
<div v-else class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 快捷操作按钮 -->
|
||
<div class="quick-actions" v-if="showQuickActions">
|
||
<button
|
||
v-for="action in quickActions"
|
||
:key="action.text"
|
||
class="quick-action-btn"
|
||
@click="selectQuickAction(action)"
|
||
>
|
||
{{ action.text }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输入提示 -->
|
||
<div class="input-hint" v-if="showHint">
|
||
<span class="hint-text">{{ hintText }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, nextTick, onMounted } from 'vue';
|
||
|
||
interface QuickAction {
|
||
text: string;
|
||
value?: string;
|
||
tag?: 'order' | 'product';
|
||
}
|
||
|
||
interface Props {
|
||
placeholder?: string;
|
||
disabled?: boolean;
|
||
isLoading?: boolean;
|
||
showQuickActions?: boolean;
|
||
quickActions?: QuickAction[];
|
||
maxLength?: number;
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'send', message: string, options?: { tag?: 'order' | 'product' }): void;
|
||
(e: 'input', value: string): void;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
placeholder: '请输入您的问题...',
|
||
disabled: false,
|
||
isLoading: false,
|
||
showQuickActions: true,
|
||
quickActions: () => [
|
||
{ text: '订单诊断', tag: 'order' },
|
||
{ text: '商品查询', tag: 'product' }
|
||
],
|
||
maxLength: 1000
|
||
});
|
||
|
||
const emit = defineEmits<Emits>();
|
||
|
||
// 响应式数据
|
||
const inputText = ref('');
|
||
const textareaRef = ref<HTMLTextAreaElement>();
|
||
|
||
// 计算属性
|
||
const canSend = computed(() => {
|
||
return inputText.value.trim().length > 0 && !props.disabled && !props.isLoading;
|
||
});
|
||
|
||
const showHint = computed(() => {
|
||
return inputText.value.length > props.maxLength * 0.8;
|
||
});
|
||
|
||
const hintText = computed(() => {
|
||
const remaining = props.maxLength - inputText.value.length;
|
||
if (remaining < 0) {
|
||
return `超出字符限制 ${Math.abs(remaining)} 个字符`;
|
||
}
|
||
return `还可输入 ${remaining} 个字符`;
|
||
});
|
||
|
||
// 自动调整高度
|
||
const adjustHeight = () => {
|
||
nextTick(() => {
|
||
if (textareaRef.value) {
|
||
textareaRef.value.style.height = 'auto';
|
||
const scrollHeight = textareaRef.value.scrollHeight;
|
||
const maxHeight = 120; // 最大高度约5行
|
||
textareaRef.value.style.height = Math.min(scrollHeight, maxHeight) + 'px';
|
||
}
|
||
});
|
||
|
||
// 触发输入事件
|
||
emit('input', inputText.value);
|
||
};
|
||
|
||
// 处理键盘事件
|
||
const handleKeydown = (event: KeyboardEvent) => {
|
||
// Ctrl/Cmd + Enter 发送消息
|
||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||
event.preventDefault();
|
||
handleSend();
|
||
return;
|
||
}
|
||
|
||
// Enter 发送消息(非 Shift + Enter)
|
||
if (event.key === 'Enter' && !event.shiftKey) {
|
||
event.preventDefault();
|
||
handleSend();
|
||
return;
|
||
}
|
||
|
||
// 字符限制检查
|
||
if (inputText.value.length >= props.maxLength &&
|
||
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||
event.preventDefault();
|
||
}
|
||
};
|
||
|
||
// 发送消息
|
||
const handleSend = () => {
|
||
if (!canSend.value) return;
|
||
|
||
const message = inputText.value.trim();
|
||
if (message) {
|
||
emit('send', message, currentTag.value ? { tag: currentTag.value } : undefined);
|
||
inputText.value = '';
|
||
currentTag.value = undefined;
|
||
adjustHeight();
|
||
}
|
||
};
|
||
|
||
// 选择快捷操作
|
||
const currentTag = ref<'order' | 'product' | undefined>(undefined);
|
||
|
||
const selectQuickAction = (action: QuickAction) => {
|
||
currentTag.value = action.tag;
|
||
};
|
||
|
||
// 聚焦输入框
|
||
const focus = () => {
|
||
nextTick(() => {
|
||
textareaRef.value?.focus();
|
||
});
|
||
};
|
||
|
||
// 清空输入
|
||
const clear = () => {
|
||
inputText.value = '';
|
||
adjustHeight();
|
||
};
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
focus,
|
||
clear
|
||
});
|
||
|
||
onMounted(() => {
|
||
adjustHeight();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.chat-input-container {
|
||
@apply w-full;
|
||
}
|
||
|
||
.input-wrapper {
|
||
@apply bg-white border border-gray-300 rounded-lg shadow-sm overflow-hidden;
|
||
}
|
||
|
||
.input-area {
|
||
@apply flex items-end p-3;
|
||
}
|
||
|
||
.chat-textarea {
|
||
@apply flex-1 resize-none border-0 outline-none bg-transparent text-gray-900 placeholder-gray-500 min-h-[24px] max-h-[120px];
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.chat-textarea:disabled {
|
||
@apply text-gray-400 cursor-not-allowed;
|
||
}
|
||
|
||
.send-button {
|
||
@apply ml-3 p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors duration-200 flex-shrink-0;
|
||
}
|
||
|
||
.send-icon {
|
||
@apply w-5 h-5;
|
||
}
|
||
|
||
.loading-spinner {
|
||
@apply w-5 h-5 flex items-center justify-center;
|
||
}
|
||
|
||
.spinner {
|
||
@apply w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin;
|
||
}
|
||
|
||
/* 快捷操作 */
|
||
.quick-actions {
|
||
@apply flex flex-wrap gap-2 p-3 pt-0;
|
||
}
|
||
|
||
.quick-action-btn {
|
||
@apply px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 transition-colors duration-200;
|
||
}
|
||
|
||
/* 输入提示 */
|
||
.input-hint {
|
||
@apply px-3 py-2 text-xs;
|
||
}
|
||
|
||
.hint-text {
|
||
@apply text-gray-500;
|
||
}
|
||
|
||
/* 字符超限时的提示样式 */
|
||
.input-wrapper:has(.chat-textarea:invalid) .hint-text,
|
||
.hint-text:has-text("超出") {
|
||
@apply text-red-500;
|
||
}
|
||
|
||
/* 暗色主题支持 */
|
||
.dark .input-wrapper {
|
||
@apply bg-gray-800 border-gray-600;
|
||
}
|
||
|
||
.dark .chat-textarea {
|
||
@apply text-gray-100 placeholder-gray-400;
|
||
}
|
||
|
||
.dark .chat-textarea:disabled {
|
||
@apply text-gray-500;
|
||
}
|
||
|
||
.dark .quick-action-btn {
|
||
@apply bg-gray-700 text-gray-300 hover:bg-gray-600;
|
||
}
|
||
|
||
.dark .hint-text {
|
||
@apply text-gray-400;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 640px) {
|
||
.input-area {
|
||
@apply p-2;
|
||
}
|
||
|
||
.send-button {
|
||
@apply ml-2 p-1.5;
|
||
}
|
||
|
||
.send-icon {
|
||
@apply w-4 h-4;
|
||
}
|
||
|
||
.quick-actions {
|
||
@apply p-2 pt-0;
|
||
}
|
||
|
||
.quick-action-btn {
|
||
@apply px-2 py-1 text-xs;
|
||
}
|
||
}
|
||
|
||
/* 焦点状态 */
|
||
.input-wrapper:focus-within {
|
||
@apply border-blue-500 ring-1 ring-blue-500;
|
||
}
|
||
|
||
.dark .input-wrapper:focus-within {
|
||
@apply border-blue-400 ring-blue-400;
|
||
}
|
||
</style>
|