762 lines
20 KiB
Vue
762 lines
20 KiB
Vue
<template>
|
||
<div class="chat-input-container" :class="{ 'is-focused': isFocused, 'is-expanded': isExpanded }">
|
||
<!-- 输入区域 -->
|
||
<div class="input-area">
|
||
<!-- 左侧功能按钮 -->
|
||
<div class="input-actions left">
|
||
<StackedCards :cards="attachments" :supports-files="supports_files" :supports-vision="supports_vision"
|
||
@remove="removeAttachment" @add-upload="triggerUploadInput" />
|
||
|
||
<!-- 隐藏的文件输入框 -->
|
||
<input
|
||
ref="uploadInputRef"
|
||
type="file"
|
||
:accept="uploadAccept"
|
||
multiple
|
||
hidden
|
||
@change="handleUploadSelect"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 文本输入框 -->
|
||
<div class="textarea-wrapper">
|
||
<textarea ref="textareaRef" v-model="inputText" :placeholder="placeholder" :rows="1"
|
||
@beforeinput="handleBeforeInput" @input="autoResize" @focus="isFocused = true" @blur="isFocused = false"
|
||
@keydown="handleKeydown" @paste="handlePaste" />
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- 底部工具栏 -->
|
||
<div class="input-toolbar">
|
||
<div class="toolbar-left">
|
||
<!-- 展开/收起 -->
|
||
<!-- <button class="toolbar-btn" title="展开输入框" @click="toggleExpand">
|
||
<Maximize2 v-if="!isExpanded" :size="16" />
|
||
<Minimize2 v-else :size="16" />
|
||
</button> -->
|
||
<!-- 深度思考开关 -->
|
||
<button class="toolbar-btn"
|
||
:class="{ active: isDeepThinking, disabled: isForceDeepThinkingModel || !supports_thinking }"
|
||
:disabled="isForceDeepThinkingModel || !supports_thinking"
|
||
:title="isForceDeepThinkingModel ? '当前模型强制开启深度思考' : (supports_thinking ? '深度思考' : '当前模型不支持深度思考')"
|
||
@click="!isForceDeepThinkingModel && supports_thinking && toggleDeepThink()">
|
||
<DeepThinkingIcon :size="16" />
|
||
<span>深度思考</span>
|
||
</button>
|
||
|
||
<!-- 深度搜索开关 -->
|
||
<button class="toolbar-btn" :class="{ active: isDeepSearch, disabled: !supports_web_search }"
|
||
:disabled="!supports_web_search" :title="supports_web_search ? '深度搜索' : '当前模型不支持联网搜索'"
|
||
@click="supports_web_search && toggleDeepSearch()">
|
||
<DeepSearchIcon :size="16" />
|
||
<span>深度搜索</span>
|
||
</button>
|
||
|
||
<!-- 联网搜索开关 -->
|
||
<button class="toolbar-btn" :class="{ active: isWebSearch, disabled: !supports_web_search }"
|
||
:disabled="!supports_web_search" :title="supports_web_search ? '联网搜索' : '当前模型不支持联网搜索'"
|
||
@click="supports_web_search && toggleWebSearch()">
|
||
<WebSearchIcon :size="16" />
|
||
<span>联网搜索</span>
|
||
</button>
|
||
</div>
|
||
<!-- 右侧功能按钮 -->
|
||
<div class="input-actions right">
|
||
<!-- 发送/停止按钮 -->
|
||
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
||
<StopIcon />
|
||
</button>
|
||
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
||
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
||
<LoadingIcon v-if="isProcessingAttachments" class="animate-spin" />
|
||
<SendIcon v-else :size="20" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
||
import { generateId } from "@/utils/helpers";
|
||
import type { Attachment } from "@/types/chat";
|
||
import { chatApi } from "@/services/api";
|
||
import { useAuthStore } from "@/stores/auth";
|
||
import { useSettingsStore } from "@/stores/settings";
|
||
import StackedCards from "@/components/ui/StackedCards.vue";
|
||
import DeepThinkingIcon from "../icons/custom/DeepThinkingIcon.vue";
|
||
import DeepSearchIcon from "../icons/custom/DeepSearchIcon.vue";
|
||
import WebSearchIcon from "../icons/custom/WebSearchIcon.vue";
|
||
import SendIcon from "../icons/custom/SendIcon.vue";
|
||
import StopIcon from "../icons/custom/StopIcon.vue";
|
||
import LoadingIcon from "../icons/custom/LoadingIcon.vue";
|
||
|
||
interface AttachmentWithProgress extends Attachment {
|
||
uploading?: boolean;
|
||
progress?: number;
|
||
deleting?: boolean;
|
||
}
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
placeholder?: string;
|
||
isStreaming?: boolean;
|
||
sendOnEnter?: boolean;
|
||
maxChars?: number;
|
||
disabled?: boolean;
|
||
// 模型能力
|
||
supports_thinking?: boolean;
|
||
supports_web_search?: boolean;
|
||
supports_vision?: boolean;
|
||
supports_files?: boolean;
|
||
}>(),
|
||
{
|
||
placeholder: "输入你的问题...",
|
||
isStreaming: false,
|
||
sendOnEnter: false,
|
||
maxChars: 10000,
|
||
disabled: false,
|
||
// 默认全部支持
|
||
supports_thinking: true,
|
||
supports_web_search: true,
|
||
supports_vision: true,
|
||
supports_files: true,
|
||
},
|
||
);
|
||
|
||
// 强制深度思考的模型列表(这些模型默认开启深度思考且不可关闭)
|
||
const FORCE_DEEP_THINKING_MODELS = ["deepseek-reasoner", "glm-z1-flash"];
|
||
|
||
// 判断当前模型是否强制开启深度思考
|
||
const isForceDeepThinkingModel = computed(() => {
|
||
return FORCE_DEEP_THINKING_MODELS.includes(modelName.value.toLowerCase());
|
||
});
|
||
|
||
const emit = defineEmits<{
|
||
send: [
|
||
text: string,
|
||
attachments: Attachment[],
|
||
options: { deepSearch: boolean; webSearch: boolean; deepThinking: boolean },
|
||
];
|
||
stop: [];
|
||
}>();
|
||
|
||
// 响应式状态
|
||
const authStore = useAuthStore();
|
||
const settingsStore = useSettingsStore();
|
||
const modelName = computed(() => settingsStore.settings.defaultModel);
|
||
|
||
const inputText = ref("");
|
||
const attachments = ref<AttachmentWithProgress[]>([]);
|
||
const isFocused = ref(false);
|
||
const isExpanded = ref(false);
|
||
const isDeepSearch = ref(
|
||
JSON.parse(localStorage.getItem("isDeepSearch") || "false"),
|
||
);
|
||
const isDeepThinking = ref(
|
||
JSON.parse(localStorage.getItem("isDeepThinking") || "false"),
|
||
);
|
||
const isWebSearch = ref(
|
||
JSON.parse(localStorage.getItem("isWebSearch") || "false"),
|
||
);
|
||
// DOM引用
|
||
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||
const uploadInputRef = ref<HTMLInputElement | null>(null);
|
||
|
||
// toast 节流
|
||
let lastToastTime = 0;
|
||
const toastThrottleMs = 2000;
|
||
|
||
function showThrottledToast(message: string, type: "error" = "error") {
|
||
const now = Date.now();
|
||
if (now - lastToastTime >= toastThrottleMs) {
|
||
lastToastTime = now;
|
||
window.$toast?.(message, type);
|
||
}
|
||
}
|
||
|
||
// 计算属性
|
||
const charCount = computed(() => inputText.value.length);
|
||
const isProcessingAttachments = computed(() =>
|
||
attachments.value.some((a) => a.uploading || a.deleting),
|
||
);
|
||
const textFileAccept =
|
||
".txt,.md,.markdown,.pdf,.doc,.docx,.rtf,.csv,.tsv,.json,.xml,.html,.htm,.yaml,.yml,.log,.ini,.conf,.sql,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.hpp,.go,.rs,.sh";
|
||
const uploadAccept = computed(() => {
|
||
if (props.supports_vision && props.supports_files) {
|
||
return `image/*,${textFileAccept}`;
|
||
}
|
||
if (props.supports_vision) {
|
||
return "image/*";
|
||
}
|
||
if (props.supports_files) {
|
||
return textFileAccept;
|
||
}
|
||
return "";
|
||
});
|
||
|
||
function getFileExt(fileName: string) {
|
||
const idx = fileName.lastIndexOf(".");
|
||
if (idx === -1) return "";
|
||
return fileName.slice(idx).toLowerCase();
|
||
}
|
||
|
||
function getUploadTypeByModel(file: File): "image" | "file" | null {
|
||
const isImage =
|
||
file.type.startsWith("image/") ||
|
||
[".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".heic"].includes(
|
||
getFileExt(file.name),
|
||
);
|
||
|
||
if (isImage) {
|
||
return props.supports_vision ? "image" : null;
|
||
}
|
||
|
||
return props.supports_files ? "file" : null;
|
||
}
|
||
const canSend = computed(() => {
|
||
return (
|
||
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
|
||
!props.disabled &&
|
||
charCount.value <= props.maxChars &&
|
||
!isProcessingAttachments.value
|
||
);
|
||
});
|
||
|
||
// 自动调整高度
|
||
function autoResize() {
|
||
const textarea = textareaRef.value;
|
||
if (!textarea) return;
|
||
|
||
textarea.style.height = "auto";
|
||
const maxHeight = isExpanded.value ? 400 : 116;
|
||
// 增加1px是为了避免滚动条出现
|
||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight) + 1}px`;
|
||
}
|
||
|
||
// 处理输入前事件,限制字数
|
||
function handleBeforeInput(event: InputEvent) {
|
||
// 如果不是插入文本的操作(如删除、退格等),允许
|
||
if (!event.data) return;
|
||
|
||
// 检查输入后是否会超过限制
|
||
const currentLength = inputText.value.length;
|
||
const insertLength = event.data?.length || 0;
|
||
const selectionStart =
|
||
(event.target as HTMLTextAreaElement).selectionStart || 0;
|
||
const selectionEnd = (event.target as HTMLTextAreaElement).selectionEnd || 0;
|
||
const selectedLength = selectionEnd - selectionStart;
|
||
|
||
// 计算输入后的长度
|
||
const newLength = currentLength - selectedLength + insertLength;
|
||
|
||
if (newLength > props.maxChars) {
|
||
event.preventDefault();
|
||
showThrottledToast(`已超${props.maxChars}字上限,请删除部分内容`);
|
||
}
|
||
}
|
||
|
||
// 处理键盘事件
|
||
function handleKeydown(event: KeyboardEvent) {
|
||
// Ctrl+Enter 或 Cmd+Enter 发送
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||
event.preventDefault();
|
||
handleSend();
|
||
return;
|
||
}
|
||
|
||
// Enter 发送(如果开启了这个选项且没有按 Shift)
|
||
if (props.sendOnEnter && event.key === "Enter" && !event.shiftKey) {
|
||
event.preventDefault();
|
||
handleSend();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
function handleSend() {
|
||
if (!canSend.value) return;
|
||
|
||
emit("send", inputText.value.trim(), [...attachments.value], {
|
||
deepSearch: isDeepSearch.value,
|
||
webSearch: isWebSearch.value,
|
||
deepThinking: isDeepThinking.value,
|
||
});
|
||
|
||
// 重置状态
|
||
inputText.value = "";
|
||
attachments.value = [];
|
||
// isDeepSearch.value = false;
|
||
// isWebSearch.value = false;
|
||
// isDeepThinking.value = false;
|
||
|
||
nextTick(() => {
|
||
autoResize();
|
||
});
|
||
}
|
||
|
||
// 处理粘贴事件
|
||
async function handlePaste(event: ClipboardEvent) {
|
||
const items = event.clipboardData?.items;
|
||
if (!items) return;
|
||
|
||
// 检查粘贴文本是否会超过字数限制
|
||
const text = event.clipboardData?.getData("text");
|
||
if (text) {
|
||
const textarea = event.target as HTMLTextAreaElement;
|
||
const selectionStart = textarea.selectionStart || 0;
|
||
const selectionEnd = textarea.selectionEnd || 0;
|
||
const selectedLength = selectionEnd - selectionStart;
|
||
const newLength = inputText.value.length - selectedLength + text.length;
|
||
|
||
if (newLength > props.maxChars) {
|
||
event.preventDefault();
|
||
showThrottledToast(`已超${props.maxChars}字上限,请删除部分内容`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
for (const item of items) {
|
||
if (item.type.startsWith("image/")) {
|
||
const file = item.getAsFile();
|
||
if (file) {
|
||
const uploadType = getUploadTypeByModel(file);
|
||
if (uploadType === "image") {
|
||
event.preventDefault();
|
||
await addFileAsAttachment(file, uploadType);
|
||
} else {
|
||
event.preventDefault();
|
||
showThrottledToast("当前模型不支持上传图片");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function triggerUploadInput() {
|
||
if (!props.supports_vision && !props.supports_files) {
|
||
showThrottledToast("当前模型不支持上传附件");
|
||
return;
|
||
}
|
||
uploadInputRef.value?.click();
|
||
}
|
||
|
||
// 处理上传选择
|
||
async function handleUploadSelect(event: Event) {
|
||
const input = event.target as HTMLInputElement;
|
||
const files = input.files;
|
||
if (!files) return;
|
||
|
||
for (const file of files) {
|
||
const uploadType = getUploadTypeByModel(file);
|
||
if (!uploadType) {
|
||
showThrottledToast(
|
||
file.type.startsWith("image/")
|
||
? "当前模型不支持上传图片"
|
||
: "当前模型不支持上传附件文件",
|
||
);
|
||
continue;
|
||
}
|
||
await addFileAsAttachment(file, uploadType);
|
||
}
|
||
|
||
input.value = "";
|
||
}
|
||
|
||
// 添加文件作为附件
|
||
async function addFileAsAttachment(
|
||
file: File,
|
||
type: "image" | "file" | "video",
|
||
) {
|
||
// 检查认证状态
|
||
if (!authStore.isAuthenticated) {
|
||
window.$toast?.("请先登录", "error");
|
||
return;
|
||
}
|
||
|
||
const id = generateId();
|
||
|
||
// 创建本地预览URL
|
||
const url = URL.createObjectURL(file);
|
||
|
||
const attachment: AttachmentWithProgress = {
|
||
id,
|
||
name: file.name,
|
||
type,
|
||
url,
|
||
size: file.size,
|
||
mimeType: file.type,
|
||
uploading: true,
|
||
progress: 0,
|
||
};
|
||
|
||
attachments.value.push(attachment);
|
||
|
||
// 实际上传文件到服务器
|
||
await uploadFileToServer(id, file);
|
||
}
|
||
|
||
// 上传文件到服务器
|
||
async function uploadFileToServer(id: string, file: File) {
|
||
try {
|
||
const uploadResult = await chatApi.uploadFile(file);
|
||
|
||
const attachment = attachments.value.find((a) => a.id === id);
|
||
if (attachment) {
|
||
// 替换本地预览URL为服务器返回的真实URL
|
||
attachment.url = uploadResult.url;
|
||
attachment.uploading = false;
|
||
attachment.progress = 100;
|
||
}
|
||
|
||
// 显示上传成功提示
|
||
window.$toast && window.$toast("文件上传成功", "success");
|
||
} catch (error) {
|
||
console.error("文件上传失败:", error);
|
||
|
||
const attachment = attachments.value.find((a) => a.id === id);
|
||
if (attachment) {
|
||
attachment.uploading = false;
|
||
// 设置错误状态或者移除附件
|
||
removeAttachment(id);
|
||
}
|
||
|
||
// 显示错误提示
|
||
window.$toast && window.$toast("文件上传失败,请重试", "error");
|
||
}
|
||
}
|
||
|
||
// 移除附件
|
||
async function removeAttachment(id: string | number) {
|
||
const targetId = String(id);
|
||
const index = attachments.value.findIndex((a) => a.id === targetId);
|
||
if (index === -1) return;
|
||
|
||
const attachment = attachments.value[index];
|
||
let deletedFromOss = false;
|
||
|
||
// 如果已上传到 OSS(不是本地 blob URL),则从 OSS 删除
|
||
if (attachment.url && !attachment.url.startsWith("blob:")) {
|
||
try {
|
||
attachment.deleting = true;
|
||
await nextTick();
|
||
await chatApi.deleteAttachment(attachment.url);
|
||
deletedFromOss = true;
|
||
} catch (error) {
|
||
console.error("删除 OSS 文件失败:", error);
|
||
// 即使删除失败也继续移除本地引用
|
||
} finally {
|
||
attachment.deleting = false;
|
||
}
|
||
}
|
||
|
||
// 释放 blob URL(如果是本地的)
|
||
if (attachment.url.startsWith("blob:")) {
|
||
URL.revokeObjectURL(attachment.url);
|
||
}
|
||
|
||
attachments.value.splice(index, 1);
|
||
|
||
if (deletedFromOss) {
|
||
window.$toast?.("OSS 文件删除成功", "success");
|
||
}
|
||
}
|
||
|
||
// 切换功能(深度搜索与联网搜索互斥)
|
||
function toggleDeepSearch() {
|
||
isDeepSearch.value = !isDeepSearch.value;
|
||
if (isDeepSearch.value) {
|
||
isWebSearch.value = false;
|
||
localStorage.setItem("isWebSearch", "false");
|
||
}
|
||
localStorage.setItem("isDeepSearch", String(isDeepSearch.value));
|
||
}
|
||
|
||
function toggleDeepThink() {
|
||
isDeepThinking.value = !isDeepThinking.value;
|
||
localStorage.setItem("isDeepThinking", String(isDeepThinking.value));
|
||
}
|
||
|
||
function toggleWebSearch() {
|
||
isWebSearch.value = !isWebSearch.value;
|
||
if (isWebSearch.value) {
|
||
isDeepSearch.value = false;
|
||
localStorage.setItem("isDeepSearch", "false");
|
||
}
|
||
localStorage.setItem("isWebSearch", String(isWebSearch.value));
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
function focus() {
|
||
textareaRef.value?.focus();
|
||
}
|
||
|
||
function clear() {
|
||
inputText.value = "";
|
||
attachments.value = [];
|
||
nextTick(() => {
|
||
autoResize();
|
||
});
|
||
}
|
||
|
||
defineExpose({
|
||
focus,
|
||
clear,
|
||
attachments,
|
||
removeAttachment,
|
||
});
|
||
|
||
// 监听文本变化,自动调整高度
|
||
watch(inputText, () => {
|
||
nextTick(() => {
|
||
autoResize();
|
||
});
|
||
});
|
||
// 监听模型变化,重置选项功能
|
||
watch(
|
||
() => settingsStore.settings.defaultModel,
|
||
(newModel) => {
|
||
// 如果是强制深度思考的模型,自动开启深度思考
|
||
if (FORCE_DEEP_THINKING_MODELS.includes(newModel.toLowerCase())) {
|
||
isDeepThinking.value = true;
|
||
localStorage.setItem("isDeepThinking", "true");
|
||
} else {
|
||
isDeepThinking.value = false;
|
||
localStorage.setItem("isDeepThinking", "false");
|
||
}
|
||
// 重置搜索相关选项
|
||
isDeepSearch.value = false;
|
||
isWebSearch.value = false;
|
||
localStorage.setItem("isDeepSearch", "false");
|
||
localStorage.setItem("isWebSearch", "false");
|
||
},
|
||
);
|
||
|
||
onMounted(() => {
|
||
autoResize();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.chat-input-container {
|
||
background: #F8F9FA;
|
||
// border: 2px solid #e2e8f0;
|
||
height: 200px;
|
||
border-radius: 20px;
|
||
padding: 20px;
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto;
|
||
gap: 12px;
|
||
border-color: #374151;
|
||
|
||
// &.is-focused {
|
||
// border-color: #3b82f6;
|
||
// box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||
// }
|
||
|
||
&.is-expanded {
|
||
.textarea-wrapper textarea {
|
||
min-height: 200px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.input-area {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: flex-start;
|
||
min-height: 0;
|
||
gap: 8px;
|
||
}
|
||
|
||
.input-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding-bottom: 4px;
|
||
min-width: 0;
|
||
|
||
&.left {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&.right {
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.action-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
border: none;
|
||
border-radius: 50px;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: #9f9fa05c;
|
||
color: #374151;
|
||
|
||
.dark & {
|
||
background: #374151;
|
||
color: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
&.disabled,
|
||
&:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
|
||
// &:hover {
|
||
// background: transparent;
|
||
// color: #6b7280;
|
||
// }
|
||
}
|
||
|
||
&.send {
|
||
background: rgba(0, 15, 51, 0.20);
|
||
color: #9ca3af;
|
||
|
||
.dark & {
|
||
background: #374151;
|
||
}
|
||
|
||
&.active {
|
||
background: #000E32;
|
||
color: white;
|
||
|
||
&:hover {
|
||
transform: scale(1.05);
|
||
// box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||
}
|
||
}
|
||
|
||
&.loading {
|
||
background: #000F33;
|
||
color: white;
|
||
cursor: wait;
|
||
}
|
||
|
||
&:disabled {
|
||
background: rgba(0, 15, 51, 0.20);
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
&.stop {
|
||
background: #000F33;
|
||
color: white;
|
||
|
||
&:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
}
|
||
}
|
||
|
||
.textarea-wrapper {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 25px;
|
||
max-height: 100%;
|
||
overflow-y: auto;
|
||
scrollbar-gutter: stable;
|
||
padding: 8px 0;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
font-family: inherit;
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
color: #1f2937;
|
||
resize: none;
|
||
|
||
.dark & {
|
||
color: #f3f4f6;
|
||
}
|
||
|
||
&::placeholder {
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
}
|
||
|
||
.input-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
// background: #fafbfc;
|
||
|
||
.dark & {
|
||
border-top-color: #2d2d3d;
|
||
background: #181825;
|
||
}
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
height: 36px;
|
||
padding: 10px 15px;
|
||
border-radius: 50px;
|
||
background: var(---FFFFFF, #FFF);
|
||
border: 1px solid transparent;
|
||
color: var(--6-666666, #666);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: #9f9fa05c;
|
||
color: #374151;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
color: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background: #DFE2E6;
|
||
// border-color: rgba(59, 130, 246, 0.3);
|
||
color: #000F33;
|
||
|
||
svg {
|
||
color: #000F33;
|
||
}
|
||
}
|
||
|
||
&.disabled,
|
||
&:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
|
||
&:hover {
|
||
background: transparent;
|
||
color: #6b7280;
|
||
}
|
||
}
|
||
}
|
||
|
||
</style>
|