mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
786 lines
19 KiB
Vue
786 lines
19 KiB
Vue
<template>
|
||
<div
|
||
ref="containerRef"
|
||
class="chat-input-wrapper"
|
||
:class="{
|
||
'is-focused': isFocused,
|
||
'is-disabled': disabled,
|
||
'is-loading': loading,
|
||
}"
|
||
>
|
||
<!-- 附件列表 -->
|
||
<div v-if="attachments.length" class="attachment-list">
|
||
<div
|
||
v-for="attachment in attachments"
|
||
:key="attachment.storedName || attachment.path"
|
||
class="attachment-chip"
|
||
:class="{
|
||
'attachment-chip--dir': attachment.contentType === 'inode/directory',
|
||
'attachment-chip--image': attachment.contentType?.startsWith('image/'),
|
||
}"
|
||
>
|
||
<!-- 图片缩略图预览(优先用本地 previewUrl,避免 JWT 认证问题) -->
|
||
<img
|
||
v-if="attachment.contentType?.startsWith('image/') && (attachment.previewUrl || attachment.url)"
|
||
:src="attachment.previewUrl || attachment.url"
|
||
:alt="attachment.name"
|
||
class="attachment-chip__thumbnail"
|
||
loading="lazy"
|
||
/>
|
||
<component
|
||
:is="attachment.url ? 'a' : 'span'"
|
||
:href="attachment.url || undefined"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
class="attachment-chip__label"
|
||
>
|
||
<span>{{ attachment.contentType === 'inode/directory' ? '📁 ' : '' }}{{ attachment.name }}</span>
|
||
<span v-if="attachment.size">{{ formatFileSize(attachment.size) }}</span>
|
||
<span v-else-if="attachment.contentType === 'inode/directory'" class="attachment-chip__path">{{ attachment.path }}</span>
|
||
</component>
|
||
<button
|
||
type="button"
|
||
class="attachment-chip__remove"
|
||
@click="removeAttachment(attachment.storedName || attachment.path)"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 审批栏:有待审批时替换输入区域 -->
|
||
<div v-if="pendingApproval?.status === 'pending_approval'" class="approval-bar">
|
||
<div class="approval-bar__info">
|
||
<span class="approval-bar__icon">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="approval-bar__label">{{ t('chat.approvalAllow') }}</span>
|
||
<span class="approval-bar__tool">{{ pendingApproval.toolName }}</span>
|
||
<span class="approval-bar__label">{{ t('chat.approvalExecute') }}</span>
|
||
</div>
|
||
<div class="approval-bar__actions">
|
||
<button
|
||
type="button"
|
||
class="approval-bar__btn approval-bar__btn--deny"
|
||
@click="emit('deny', pendingApproval.pendingId)"
|
||
>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
{{ t('chat.deny') }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="approval-bar__btn approval-bar__btn--approve"
|
||
@click="emit('approve', pendingApproval.pendingId)"
|
||
>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<polyline points="20 6 9 17 4 12"/>
|
||
</svg>
|
||
{{ t('chat.approve') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输入区域(运行中也可输入) -->
|
||
<div v-else class="input-area-container">
|
||
<!-- 排队消息指示器 -->
|
||
<div v-if="queuedMessage && queuedMessage.status !== 'cancelled'" class="queued-indicator">
|
||
<div class="queued-indicator__info">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
|
||
</svg>
|
||
<span class="queued-indicator__text">
|
||
{{ queuedMessage.status === 'sending' ? t('chat.queuedSending') : t('chat.queuedWillSend') }}
|
||
<span v-if="queueSize > 1" class="queued-indicator__count">({{ queueSize }})</span>
|
||
</span>
|
||
</div>
|
||
<button
|
||
v-if="queuedMessage.status === 'queued'"
|
||
type="button"
|
||
class="queued-indicator__cancel"
|
||
@click="emit('cancel-queued')"
|
||
>{{ t('chat.queuedCancel') }}</button>
|
||
</div>
|
||
|
||
<div class="input-area">
|
||
<textarea
|
||
ref="textareaRef"
|
||
v-model="inputValue"
|
||
class="chat-textarea"
|
||
:placeholder="inputPlaceholder"
|
||
:disabled="disabled"
|
||
:maxlength="maxLength"
|
||
rows="1"
|
||
@keydown.enter.exact.prevent="handleEnter"
|
||
@compositionstart="isComposing = true"
|
||
@compositionend="isComposing = false"
|
||
@focus="isFocused = true"
|
||
@blur="isFocused = false"
|
||
@input="autoResize"
|
||
@paste="handlePaste"
|
||
></textarea>
|
||
|
||
<div class="input-actions">
|
||
<!-- 附件按钮 -->
|
||
<button
|
||
v-if="enableAttachments"
|
||
type="button"
|
||
class="action-btn attach-btn"
|
||
:disabled="disabled || loading || uploading"
|
||
@click="openFilePicker"
|
||
>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21.44 11.05l-8.49 8.49a6 6 0 0 1-8.49-8.49l9.2-9.19a4 4 0 1 1 5.66 5.66l-9.19 9.2a2 2 0 1 1-2.83-2.83l8.49-8.48"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- 发送/停止/中断按钮 -->
|
||
<button
|
||
type="button"
|
||
class="action-btn send-btn"
|
||
:class="sendBtnClass"
|
||
:disabled="!canSend && !loading"
|
||
@click="handleSubmit"
|
||
>
|
||
<!-- 有输入时始终显示发送图标(运行中发送 = interrupt) -->
|
||
<svg v-if="canSend" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||
</svg>
|
||
<!-- 运行中无输入:停止图标 -->
|
||
<svg v-else-if="loading" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
</svg>
|
||
<!-- 空闲无输入 -->
|
||
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部信息 -->
|
||
<div class="input-footer">
|
||
<span class="input-hint">{{ hint }}</span>
|
||
<span v-if="maxLength" class="input-length">
|
||
{{ inputValue.length }}/{{ maxLength }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 隐藏的文件输入 -->
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
class="hidden-file-input"
|
||
multiple
|
||
:accept="acceptedFileTypes"
|
||
@change="handleFileChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, nextTick, watch } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import type { ChatAttachment, PendingApprovalMeta, StreamPhase, QueuedMessage } from '@/types'
|
||
|
||
interface Props {
|
||
/** 输入值 */
|
||
modelValue?: string
|
||
/** 占位符 */
|
||
placeholder?: string
|
||
/** 是否加载中(AI 正在运行) */
|
||
loading?: boolean
|
||
/** 是否禁用(无法输入) */
|
||
disabled?: boolean
|
||
/** 最大长度 */
|
||
maxLength?: number
|
||
/** 是否启用附件 */
|
||
enableAttachments?: boolean
|
||
/** 接受文件类型 */
|
||
acceptedFileTypes?: string
|
||
/** 提示文字 */
|
||
hint?: string
|
||
/** 附件列表 */
|
||
attachments?: ChatAttachment[]
|
||
/** 是否上传中 */
|
||
uploading?: boolean
|
||
/** 待审批数据:存在时将输入框替换为审批栏 */
|
||
pendingApproval?: PendingApprovalMeta | null
|
||
/** 当前流阶段 */
|
||
streamPhase?: StreamPhase
|
||
/** 排队的消息(队首) */
|
||
queuedMessage?: QueuedMessage | null
|
||
/** 排队消息总数 */
|
||
queueSize?: number
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
modelValue: '',
|
||
placeholder: '',
|
||
loading: false,
|
||
disabled: false,
|
||
enableAttachments: true,
|
||
acceptedFileTypes: '*/*',
|
||
hint: '',
|
||
attachments: () => [],
|
||
uploading: false,
|
||
pendingApproval: null,
|
||
streamPhase: 'idle',
|
||
queuedMessage: null,
|
||
queueSize: 0,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: string]
|
||
submit: [value: string]
|
||
stop: []
|
||
'cancel-queued': []
|
||
'file-select': [files: File[]]
|
||
'attachment-remove': [storedName: string]
|
||
approve: [pendingId: string]
|
||
deny: [pendingId: string]
|
||
}>()
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 内部状态
|
||
const containerRef = ref<HTMLElement | null>(null)
|
||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||
const isFocused = ref(false)
|
||
const isComposing = ref(false)
|
||
|
||
// 输入值处理
|
||
const inputValue = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value),
|
||
})
|
||
|
||
// 是否可以发送
|
||
const canSend = computed(() => {
|
||
return inputValue.value.trim().length > 0 || props.attachments.length > 0
|
||
})
|
||
|
||
// 运行中输入的占位符
|
||
const inputPlaceholder = computed(() => {
|
||
if (props.loading) {
|
||
if (props.queuedMessage) return t('chat.queuedReplace')
|
||
return props.placeholder + ' (Enter to send / interrupt)'
|
||
}
|
||
return props.placeholder
|
||
})
|
||
|
||
// 处理提交
|
||
const handleSubmit = () => {
|
||
// 有排队消息时,点击按钮取消排队
|
||
if (props.queuedMessage && props.queuedMessage.status === 'queued') {
|
||
// 如果输入框为空,取消排队;如果有新输入,替换排队消息
|
||
if (!inputValue.value.trim()) {
|
||
emit('cancel-queued')
|
||
return
|
||
}
|
||
}
|
||
|
||
// 运行中且输入为空时,停止生成
|
||
if (props.loading && !canSend.value) {
|
||
emit('stop')
|
||
return
|
||
}
|
||
|
||
if (!canSend.value || props.disabled) return
|
||
|
||
// 运行中有内容:发送(useChat 会走 interrupt/queue 逻辑)
|
||
// 非运行中有内容:正常发送
|
||
emit('submit', inputValue.value)
|
||
}
|
||
|
||
// 发送按钮样式
|
||
const sendBtnClass = computed(() => ({
|
||
'is-loading': props.loading && !canSend.value,
|
||
'is-empty': !canSend.value && !props.loading,
|
||
'is-interrupt': props.loading && canSend.value,
|
||
}))
|
||
|
||
// 处理回车键
|
||
const handleEnter = () => {
|
||
if (isComposing.value) return
|
||
handleSubmit()
|
||
}
|
||
|
||
// 自动调整高度
|
||
const autoResize = () => {
|
||
nextTick(() => {
|
||
const textarea = textareaRef.value
|
||
if (!textarea) return
|
||
|
||
textarea.style.height = 'auto'
|
||
const newHeight = Math.min(textarea.scrollHeight, 160)
|
||
textarea.style.height = newHeight + 'px'
|
||
})
|
||
}
|
||
|
||
// 监听输入值变化,调整高度
|
||
watch(inputValue, autoResize)
|
||
|
||
// 文件处理
|
||
const openFilePicker = () => {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
const handleFileChange = (event: Event) => {
|
||
const input = event.target as HTMLInputElement
|
||
const files = Array.from(input.files || [])
|
||
|
||
if (files.length) {
|
||
emit('file-select', files)
|
||
}
|
||
|
||
// 清空 input 以便重复选择同一文件
|
||
input.value = ''
|
||
}
|
||
|
||
const removeAttachment = (storedName: string) => {
|
||
emit('attachment-remove', storedName)
|
||
}
|
||
|
||
// 粘贴处理
|
||
const handlePaste = (event: ClipboardEvent) => {
|
||
if (!props.enableAttachments) return
|
||
|
||
const items = Array.from(event.clipboardData?.items || [])
|
||
const files = items
|
||
.filter(item => item.kind === 'file')
|
||
.map(item => item.getAsFile())
|
||
.filter((file): file is File => file !== null)
|
||
|
||
if (files.length > 0) {
|
||
emit('file-select', files)
|
||
event.preventDefault()
|
||
}
|
||
}
|
||
|
||
// 文件大小格式化
|
||
const formatFileSize = (size: number) => {
|
||
if (size < 1024) return `${size} B`
|
||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
focus: () => textareaRef.value?.focus(),
|
||
blur: () => textareaRef.value?.blur(),
|
||
clear: () => {
|
||
emit('update:modelValue', '')
|
||
nextTick(() => {
|
||
if (textareaRef.value) {
|
||
textareaRef.value.style.height = 'auto'
|
||
}
|
||
})
|
||
},
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.chat-input-wrapper {
|
||
padding: 18px 24px 22px;
|
||
background: var(--mc-bg-elevated, #f8fafc);
|
||
}
|
||
|
||
.chat-input-wrapper.is-focused {
|
||
/* focus state handled on .input-area */
|
||
}
|
||
|
||
/* 附件列表 */
|
||
.attachment-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.attachment-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
max-width: 100%;
|
||
background: var(--mc-attachment-bg, #f1f5f9);
|
||
border: 1px solid var(--mc-attachment-border, #e2e8f0);
|
||
border-radius: 999px;
|
||
padding: 6px 8px 6px 12px;
|
||
}
|
||
|
||
.attachment-chip--image {
|
||
padding: 4px 6px;
|
||
}
|
||
|
||
.attachment-chip__thumbnail {
|
||
width: 36px;
|
||
height: 36px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.attachment-chip__label {
|
||
display: inline-flex;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
color: var(--mc-attachment-color, #1e293b);
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.attachment-chip__label span:first-child {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 280px;
|
||
}
|
||
|
||
.attachment-chip__label span:last-child {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: var(--mc-primary, #D97757);
|
||
}
|
||
|
||
.attachment-chip__remove {
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: rgba(217, 119, 87, 0.16);
|
||
color: var(--mc-primary-hover, #C1572B);
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.attachment-chip__remove:hover {
|
||
background: rgba(217, 119, 87, 0.24);
|
||
}
|
||
|
||
.attachment-chip--dir {
|
||
border-style: dashed;
|
||
}
|
||
|
||
.attachment-chip__path {
|
||
font-size: 11px;
|
||
color: var(--mc-text-tertiary, #94a3b8);
|
||
max-width: 200px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 输入区域 */
|
||
.input-area {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-end;
|
||
background: var(--mc-input-bg, #ffffff);
|
||
border: none;
|
||
border-radius: 18px;
|
||
padding: 12px 12px 12px 16px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||
transition: box-shadow 0.15s;
|
||
}
|
||
|
||
.chat-input-wrapper.is-focused .input-area {
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(217, 119, 87, 0.25);
|
||
}
|
||
|
||
.chat-textarea {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
resize: none;
|
||
outline: none;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: var(--mc-input-text, #1e293b);
|
||
min-height: 44px;
|
||
max-height: 160px;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.chat-textarea::placeholder {
|
||
color: var(--mc-text-tertiary, #94a3b8);
|
||
}
|
||
|
||
.chat-textarea:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.input-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: none;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
background: transparent;
|
||
color: var(--mc-text-secondary, #64748b);
|
||
}
|
||
|
||
.action-btn:hover:not(:disabled) {
|
||
background: var(--mc-bg-sunken, #f1f5f9);
|
||
color: var(--mc-text-primary, #1e293b);
|
||
}
|
||
|
||
.action-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.send-btn {
|
||
background: var(--mc-primary, #D97757);
|
||
color: white;
|
||
}
|
||
|
||
.send-btn:hover:not(:disabled) {
|
||
background: var(--mc-primary-hover, #C1572B);
|
||
}
|
||
|
||
.send-btn.is-loading {
|
||
background: var(--mc-danger, #ef4444);
|
||
}
|
||
|
||
.send-btn.is-loading:hover:not(:disabled) {
|
||
background: var(--mc-danger-hover, #dc2626);
|
||
}
|
||
|
||
.send-btn.is-empty:not(.is-loading) {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 底部信息 */
|
||
.input-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 8px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.input-hint {
|
||
font-size: 12px;
|
||
color: var(--mc-text-tertiary, #94a3b8);
|
||
}
|
||
|
||
.input-length {
|
||
font-size: 12px;
|
||
color: var(--mc-text-tertiary, #94a3b8);
|
||
}
|
||
|
||
/* 隐藏的文件输入 */
|
||
.hidden-file-input {
|
||
position: absolute;
|
||
width: 0;
|
||
height: 0;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 审批栏 */
|
||
.approval-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
background: var(--mc-input-bg, #ffffff);
|
||
border-radius: 18px;
|
||
padding: 10px 10px 10px 16px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(217, 119, 87, 0.3);
|
||
min-height: 56px;
|
||
}
|
||
|
||
.approval-bar__info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 14px;
|
||
color: var(--mc-text-secondary, #64748b);
|
||
}
|
||
|
||
.approval-bar__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
color: var(--mc-primary, #D97757);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.approval-bar__label {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.approval-bar__tool {
|
||
font-weight: 600;
|
||
color: var(--mc-text-primary, #1e293b);
|
||
font-family: ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
||
font-size: 13px;
|
||
background: var(--mc-bg-sunken, #f1f5f9);
|
||
padding: 1px 7px;
|
||
border-radius: 5px;
|
||
max-width: 260px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
flex-shrink: 1;
|
||
}
|
||
|
||
.approval-bar__actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.approval-bar__btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 7px 14px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
line-height: 1;
|
||
}
|
||
|
||
.approval-bar__btn--approve {
|
||
background: var(--mc-primary, #D97757);
|
||
color: #fff;
|
||
}
|
||
|
||
.approval-bar__btn--approve:hover {
|
||
background: var(--mc-primary-hover, #C1572B);
|
||
}
|
||
|
||
.approval-bar__btn--deny {
|
||
background: var(--mc-bg-sunken, #f1f5f9);
|
||
color: var(--mc-text-secondary, #64748b);
|
||
border: 1px solid var(--mc-border, #e2e8f0);
|
||
}
|
||
|
||
.approval-bar__btn--deny:hover {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
border-color: #fca5a5;
|
||
}
|
||
|
||
/* 输入区域容器 */
|
||
.input-area-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
}
|
||
|
||
/* 排队指示器 */
|
||
.queued-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 6px 16px;
|
||
margin-bottom: 4px;
|
||
border-radius: 10px;
|
||
background: rgba(59, 130, 246, 0.06);
|
||
border: 1px solid rgba(59, 130, 246, 0.15);
|
||
}
|
||
|
||
.queued-indicator__info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.queued-indicator__text {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.queued-indicator__cancel {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
background: none;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.queued-indicator__cancel:hover {
|
||
color: #ef4444;
|
||
border-color: #fca5a5;
|
||
background: #fee2e2;
|
||
}
|
||
|
||
/* 中断发送按钮样式 */
|
||
.send-btn.is-interrupt {
|
||
background: #f59e0b;
|
||
color: white;
|
||
}
|
||
|
||
.send-btn.is-interrupt:hover:not(:disabled) {
|
||
background: #d97706;
|
||
}
|
||
|
||
/* ===== 移动端适配 ===== */
|
||
@media (max-width: 768px) {
|
||
.chat-input-wrapper {
|
||
padding: 10px 12px 14px;
|
||
}
|
||
|
||
.approval-bar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.approval-bar__info {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.approval-bar__actions {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.approval-bar__tool {
|
||
max-width: 180px;
|
||
}
|
||
|
||
.attachment-chip__label span:first-child {
|
||
max-width: 180px;
|
||
}
|
||
}
|
||
</style>
|