|
|
@@ -1,11 +1,25 @@
|
|
|
<script setup lang="ts">
|
|
|
import AutoResizeTextarea from './components/AutoResizeTextarea.vue'
|
|
|
+import LoadingDots from './components/LoadingDots.vue'
|
|
|
import AIQuestions from './components/AIQuestions.vue'
|
|
|
import userBubbleLight from './image/userBubbleLight.png'
|
|
|
import userBubbleDark from './image/userBubbleDark.png'
|
|
|
import robotBubbleLight from './image/robotBubbleLight.png'
|
|
|
import robotBubbleDark from './image/robotBubbleDark.png'
|
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
|
+import MarkdownIt from 'markdown-it'
|
|
|
+import 'github-markdown-css/github-markdown.css'
|
|
|
+
|
|
|
+const md = new MarkdownIt({
|
|
|
+ html: true,
|
|
|
+ linkify: true,
|
|
|
+ typographer: true,
|
|
|
+ breaks: true
|
|
|
+})
|
|
|
+
|
|
|
+const renderedMessage = (content) => {
|
|
|
+ return md.render(content)
|
|
|
+}
|
|
|
|
|
|
const themeStore = useThemeStore()
|
|
|
const modalSize = ref('large')
|
|
|
@@ -20,16 +34,25 @@ const robotBubbleImg = computed(() => {
|
|
|
return themeStore.theme === 'light' ? robotBubbleLight : robotBubbleDark
|
|
|
})
|
|
|
|
|
|
+// 是否显示footer的顶部阴影
|
|
|
+const isShowFooterShadow = ref(false)
|
|
|
+// 是否显示header的底部阴影
|
|
|
+const isShowHeaderShadow = ref(false)
|
|
|
+
|
|
|
+const loadingAnswer = ref(false) // 是否正在加载答案
|
|
|
interface MessageItem {
|
|
|
type: 'robot' | 'user'
|
|
|
content: string
|
|
|
feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
+ isError?: boolean // 是否为错误消息
|
|
|
+ isCancel?: boolean // 是否为取消消息
|
|
|
}
|
|
|
const messages = ref<MessageItem[]>([
|
|
|
{
|
|
|
type: 'robot',
|
|
|
+ isShowFeedback: false,
|
|
|
content: 'You can click on Frequently Asked Questions above or type your own question'
|
|
|
},
|
|
|
{
|
|
|
@@ -48,6 +71,19 @@ const messages = ref<MessageItem[]>([
|
|
|
content: 'Of course! Please provide me with the details of your shipment.'
|
|
|
}
|
|
|
])
|
|
|
+messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
|
+onMounted(() => {
|
|
|
+ scrollToBottom()
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => messages.value,
|
|
|
+ (newVal) => {
|
|
|
+ // 将消息存储到 sessionStorage 中
|
|
|
+ sessionStorage.setItem('AIChat', JSON.stringify(newVal))
|
|
|
+ },
|
|
|
+ { immediate: true, deep: true }
|
|
|
+)
|
|
|
|
|
|
// 用户问题请求时间
|
|
|
const queryTime = ref(-1)
|
|
|
@@ -62,59 +98,128 @@ const progressStatus = {
|
|
|
cancel: 'You have stopped this answer'
|
|
|
}
|
|
|
|
|
|
+const isShowTips = ref(false) // 是否展示提示信息
|
|
|
+
|
|
|
const progressInterval = ref()
|
|
|
-const handleSend = () => {
|
|
|
- if (!userQuestion.value) return
|
|
|
+const handleSend = (question, isPresetQuestion = true) => {
|
|
|
+ if (!question) return
|
|
|
+
|
|
|
+ if (loadingAnswer.value) {
|
|
|
+ isShowTips.value = true
|
|
|
+ setTimeout(() => {
|
|
|
+ isShowTips.value = false
|
|
|
+ }, 2000)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loadingAnswer.value = true
|
|
|
|
|
|
messages.value.push({
|
|
|
type: 'user',
|
|
|
- content: userQuestion.value
|
|
|
+ content: question
|
|
|
})
|
|
|
- userQuestion.value = ''
|
|
|
+ !isPresetQuestion ? (userQuestion.value = '') : ''
|
|
|
queryTime.value = 0
|
|
|
messages.value.push({
|
|
|
type: 'robot',
|
|
|
content: progressStatus[0]
|
|
|
})
|
|
|
+ autoScroll.value = true
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
progressInterval.value = setInterval(() => {
|
|
|
queryTime.value++
|
|
|
+ // 定义时间点与对应状态的映射
|
|
|
+ const timeToStatusMap = {
|
|
|
+ 15: 15,
|
|
|
+ 30: 30,
|
|
|
+ 60: 60
|
|
|
+ }
|
|
|
+ // 获取最后一个消息对象
|
|
|
+ const lastMessage = messages.value[messages.value.length - 1]
|
|
|
+ if (queryTime.value === 120) {
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
+ lastMessage.content = progressStatus[120]
|
|
|
+ lastMessage.isError = true
|
|
|
+ queryTime.value = -1
|
|
|
+ loadingAnswer.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (timeToStatusMap[queryTime.value] !== undefined) {
|
|
|
+ lastMessage.content = progressStatus[queryTime.value]
|
|
|
+ }
|
|
|
}, 1000)
|
|
|
}
|
|
|
+const showScrollButton = ref(false) // 控制按钮显示
|
|
|
+const messagesRef = ref()
|
|
|
+const autoScroll = ref(true)
|
|
|
|
|
|
-// 根据时间更新消息内容
|
|
|
-const updateMessageContent = (time) => {
|
|
|
- const lastMessageIndex = messages.value.length - 1
|
|
|
+function handleScroll() {
|
|
|
+ const el = messagesRef.value
|
|
|
+ const threshold = 50 // 到底部的距离阈值
|
|
|
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold
|
|
|
+ autoScroll.value = atBottom
|
|
|
|
|
|
- // 确保消息数组不为空
|
|
|
- if (lastMessageIndex >= 0) {
|
|
|
- messages.value[lastMessageIndex].content = progressStatus[time]
|
|
|
- if (time === 120) {
|
|
|
- clearInterval(progressInterval.value)
|
|
|
- queryTime.value = -3
|
|
|
- }
|
|
|
- }
|
|
|
+ showScrollButton.value = el.scrollHeight > el.clientHeight && !autoScroll.value
|
|
|
+
|
|
|
+ isShowFooterShadow.value =
|
|
|
+ messagesRef.value?.scrollTop <
|
|
|
+ messagesRef.value?.scrollHeight - messagesRef.value?.clientHeight - 1
|
|
|
+
|
|
|
+ isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
|
|
|
}
|
|
|
-watch(
|
|
|
- () => queryTime.value,
|
|
|
- (newVal) => {
|
|
|
- // 定义时间点与对应状态的映射
|
|
|
- const timeToStatusMap = {
|
|
|
- 15: 15,
|
|
|
- 30: 30,
|
|
|
- 60: 60,
|
|
|
- 120: 120
|
|
|
+function scrollToBottom() {
|
|
|
+ nextTick(() => {
|
|
|
+ if (messagesRef.value) {
|
|
|
+ messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
|
|
}
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const simulateStreamingMarkdown = () => {
|
|
|
+ loadingAnswer.value = true
|
|
|
+ const chunks = [
|
|
|
+ '# 欢迎使用 Markdown\n\n',
|
|
|
+ '这是一个用于测试的 **Markdown** 文本。\n\n',
|
|
|
+ '你可以使用 *斜体* 来强调重点。\n\n',
|
|
|
+ '也可以使用 [链接](https://example.com) 引导用户跳转。\n\n',
|
|
|
+ '- 支持无序列表\n',
|
|
|
+ '- 非常适合逐行流式显示\n\n',
|
|
|
+ '这是一段测试!\n'
|
|
|
+ ]
|
|
|
+
|
|
|
+ messages.value.push({
|
|
|
+ type: 'robot',
|
|
|
+ content: '',
|
|
|
+ isAnswer: true,
|
|
|
+ feedback: '',
|
|
|
+ isShowFeedback: false
|
|
|
+ })
|
|
|
+
|
|
|
+ let index = 0
|
|
|
|
|
|
- // 如果当前时间点在映射中,更新消息内容
|
|
|
- if (timeToStatusMap[newVal] !== undefined) {
|
|
|
- updateMessageContent(timeToStatusMap[newVal])
|
|
|
+ autoScroll.value = true
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
+ const interval = setInterval(() => {
|
|
|
+ index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
|
|
|
+ if (index < chunks.length) {
|
|
|
+ messages.value[messages.value.length - 1].content += chunks[index]
|
|
|
+ index++
|
|
|
+
|
|
|
+ if (autoScroll.value) {
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ clearInterval(interval)
|
|
|
}
|
|
|
- }
|
|
|
-)
|
|
|
+ }, 200) // 每500ms追加一段
|
|
|
+}
|
|
|
+
|
|
|
// 暂停回答
|
|
|
const handlePause = () => {
|
|
|
clearInterval(progressInterval.value)
|
|
|
- queryTime.value = -2
|
|
|
+ queryTime.value = -1
|
|
|
+ messages.value[messages.value.length - 1].isCancel = true
|
|
|
messages.value[messages.value.length - 1].content = progressStatus.cancel
|
|
|
}
|
|
|
|
|
|
@@ -128,7 +233,7 @@ const handleClose = () => {
|
|
|
|
|
|
<template>
|
|
|
<div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
|
|
|
- <div class="top-section">
|
|
|
+ <div class="top-section" :class="[isShowHeaderShadow ? 'box-shadow' : '']">
|
|
|
<div class="header">
|
|
|
<span class="welcome">Hi! I'm your Freight Assistant</span>
|
|
|
<div class="option-icon">
|
|
|
@@ -145,14 +250,20 @@ const handleClose = () => {
|
|
|
<span @click="handleClose" class="font_family icon-icon_collapsed__to_widget_b"></span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <AIQuestions :modalSize="modalSize"></AIQuestions>
|
|
|
+ <AIQuestions :modalSize="modalSize" @question="handleSend"></AIQuestions>
|
|
|
+ <div class="warning-tips" v-if="isShowTips">
|
|
|
+ <div class="warning-bg">
|
|
|
+ <span class="warning-icon font_family icon-icon_warning_fill_b"></span>
|
|
|
+ </div>
|
|
|
+ <span>Answer in progress, please wait.</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="chat-messages" ref="messagesRef" :style="{ marginTop: modalSize === 'large' ? '81px' : '151px' }">
|
|
|
+ <div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
|
|
|
<div
|
|
|
class="message-item"
|
|
|
:class="[
|
|
|
msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
|
|
|
- ((queryTime > -1 && queryTime < 120) || queryTime === -2) && index === messages.length - 1
|
|
|
+ (queryTime > -1 && queryTime < 120 && index === messages.length - 1) || msg.isCancel
|
|
|
? 'query-style'
|
|
|
: ''
|
|
|
]"
|
|
|
@@ -161,12 +272,10 @@ const handleClose = () => {
|
|
|
@mouseenter="msg.isShowFeedback = true"
|
|
|
@mouseleave="msg.isShowFeedback = false"
|
|
|
>
|
|
|
- <!-- 请求失败后的提示icon -->
|
|
|
- <span
|
|
|
- v-if="queryTime === -3 && index === messages.length - 1"
|
|
|
- class="font_family icon-icon_warning_fill_b"
|
|
|
- style="margin-top: 1px; color: #c9353f"
|
|
|
- ></span>
|
|
|
+ <!-- 请求失败后的提示icon -->
|
|
|
+ <div class="pause-bg" v-if="msg.isError">
|
|
|
+ <span class="pause-icon font_family icon-icon_warning_fill_b"></span>
|
|
|
+ </div>
|
|
|
<!-- loading icon -->
|
|
|
<img
|
|
|
class="loading-img"
|
|
|
@@ -174,7 +283,15 @@ const handleClose = () => {
|
|
|
src="./image/icon_loading.png"
|
|
|
alt=""
|
|
|
/>
|
|
|
- {{ msg.content }}
|
|
|
+ <span v-if="!msg.isAnswer">{{ msg.content }}</span>
|
|
|
+ <div v-else>
|
|
|
+ <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
+ <LoadingDots
|
|
|
+ v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
|
|
|
+ ></LoadingDots>
|
|
|
+ <div></div>
|
|
|
+ </div>
|
|
|
+ <!-- 评价 -->
|
|
|
<div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
|
|
|
<el-button
|
|
|
v-if="msg.feedback !== 'good'"
|
|
|
@@ -209,27 +326,38 @@ const handleClose = () => {
|
|
|
<!-- 暂停回答 icon -->
|
|
|
<div
|
|
|
class="pause-btn"
|
|
|
- v-if="index === messages.length - 1 && queryTime > 30 && queryTime < 120"
|
|
|
+ v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
|
|
|
@click="handlePause"
|
|
|
>
|
|
|
<div class="dot"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <!-- 滚动到底部icon -->
|
|
|
+ <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom">
|
|
|
+ <span class="font_family icon-icon_movedown_b"></span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="footer-input" :class="[isFooterInputFocus ? 'focus-style' : '']">
|
|
|
- <AutoResizeTextarea
|
|
|
- v-model="userQuestion"
|
|
|
- :placeholder="'Type your question here...'"
|
|
|
- @focus="isFooterInputFocus = true"
|
|
|
- @blur="isFooterInputFocus = false"
|
|
|
- />
|
|
|
- <div
|
|
|
- class="input-icon"
|
|
|
- :class="[userQuestion ? 'input-style' : 'disable']"
|
|
|
- @click="handleSend"
|
|
|
- >
|
|
|
- <span class="font_family icon-icon_send_b"></span>
|
|
|
+ <div
|
|
|
+ class="footer"
|
|
|
+ :class="[isFooterInputFocus ? 'focus-style' : '', isShowFooterShadow ? 'box-shadow' : '']"
|
|
|
+ >
|
|
|
+ <div class="footer-input">
|
|
|
+ <AutoResizeTextarea
|
|
|
+ style="flex: 1"
|
|
|
+ v-model="userQuestion"
|
|
|
+ :placeholder="'Type your question here...'"
|
|
|
+ @focus="isFooterInputFocus = true"
|
|
|
+ @blur="isFooterInputFocus = false"
|
|
|
+ />
|
|
|
+ <el-button @click="simulateStreamingMarkdown">测试</el-button>
|
|
|
+ <div
|
|
|
+ class="input-icon"
|
|
|
+ :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|
|
|
+ @click="handleSend(userQuestion, false)"
|
|
|
+ >
|
|
|
+ <span class="font_family icon-icon_send_b"></span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -250,18 +378,23 @@ const handleClose = () => {
|
|
|
background-color: var(--color-dialog-body-bg);
|
|
|
overflow: hidden;
|
|
|
.top-section {
|
|
|
- height: 150px;
|
|
|
+ position: relative;
|
|
|
+ padding-bottom: 16px;
|
|
|
background: linear-gradient(
|
|
|
to bottom,
|
|
|
var(--color-ai-chat-header-bg-gradient-begin) 10%,
|
|
|
var(--color-ai-chat-header-bg-gradient-begin) 10%,
|
|
|
var(--color-ai-chat-header-bg-gradient-end) 100%
|
|
|
);
|
|
|
+ &.box-shadow {
|
|
|
+ box-shadow: -2px 0px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
.header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
height: 64px;
|
|
|
+ margin-bottom: 6px;
|
|
|
padding: 0 16px;
|
|
|
.welcome {
|
|
|
font-size: 18px;
|
|
|
@@ -280,13 +413,45 @@ const handleClose = () => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ .warning-tips {
|
|
|
+ position: absolute;
|
|
|
+ top: 60px;
|
|
|
+ left: 17px;
|
|
|
+ width: calc(100% - 32px);
|
|
|
+ height: 40px;
|
|
|
+ padding-top: 14px;
|
|
|
+ background-color: var(--color-warning-tips-bg);
|
|
|
+ border-radius: 6px;
|
|
|
+ text-align: center;
|
|
|
+ .warning-bg {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 12px;
|
|
|
+ width: 12px;
|
|
|
+ margin-right: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+ .warning-icon {
|
|
|
+ position: absolute;
|
|
|
+ top: -3px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ span {
|
|
|
+ color: #edb82f;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
.chat-messages {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 16px;
|
|
|
- padding: 0 16px;
|
|
|
+ padding: 0 16px 18px;
|
|
|
overflow: auto;
|
|
|
.message-item {
|
|
|
position: relative;
|
|
|
@@ -306,20 +471,30 @@ const handleClose = () => {
|
|
|
height: 30px;
|
|
|
margin-top: 10px;
|
|
|
padding-left: 30px;
|
|
|
- padding-top: 10px;
|
|
|
+ padding-top: 5px;
|
|
|
|
|
|
button.el-button + .el-button {
|
|
|
margin-left: 0px;
|
|
|
}
|
|
|
}
|
|
|
- .review-input-card {
|
|
|
- margin-top: 6px;
|
|
|
- padding: 8px;
|
|
|
- text-align: right;
|
|
|
- box-shadow: 1px 1px 12px 0px rgba(0, 0, 0, 0.05);
|
|
|
- border-radius: 6px;
|
|
|
+ .pause-bg {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 12px;
|
|
|
+ width: 12px;
|
|
|
+ margin-right: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+ .pause-icon {
|
|
|
+ position: absolute;
|
|
|
+ top: -2px;
|
|
|
+ color: #c9353f;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: none;
|
|
|
}
|
|
|
-
|
|
|
.el-button--text {
|
|
|
height: 16px;
|
|
|
width: 16px;
|
|
|
@@ -338,10 +513,12 @@ const handleClose = () => {
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
margin-top: -1px;
|
|
|
- margin-right: 2px;
|
|
|
+ margin-right: 4px;
|
|
|
animation: loading-rotate 2s linear infinite;
|
|
|
}
|
|
|
-
|
|
|
+ .markdown-body {
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
.pause-btn {
|
|
|
position: absolute;
|
|
|
right: -22px;
|
|
|
@@ -362,7 +539,9 @@ const handleClose = () => {
|
|
|
}
|
|
|
}
|
|
|
.query-style {
|
|
|
- color: #b5b9bf;
|
|
|
+ span {
|
|
|
+ color: #b5b9bf;
|
|
|
+ }
|
|
|
}
|
|
|
.robot-bubble {
|
|
|
background: var(--scoring-bg-color);
|
|
|
@@ -386,6 +565,50 @@ const handleClose = () => {
|
|
|
bottom: -7px;
|
|
|
}
|
|
|
}
|
|
|
+ .scroll-to-bottom-btn {
|
|
|
+ position: absolute;
|
|
|
+ right: 50%;
|
|
|
+ transform: translateX(50%);
|
|
|
+ bottom: 58px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 32px;
|
|
|
+ width: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #f5f4f4;
|
|
|
+ box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.1);
|
|
|
+ span {
|
|
|
+ color: #2b2f36;
|
|
|
+ }
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--color-theme);
|
|
|
+ span {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .footer {
|
|
|
+ padding: 12px 16px;
|
|
|
+ &.box-shadow {
|
|
|
+ box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+ &.focus-style {
|
|
|
+ .footer-input {
|
|
|
+ border: 1px solid var(--color-theme);
|
|
|
+ }
|
|
|
+ .input-icon {
|
|
|
+ background-color: var(--color-theme);
|
|
|
+ span {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ background-color: #d56200;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
.footer-input {
|
|
|
display: flex;
|
|
|
@@ -393,10 +616,10 @@ const handleClose = () => {
|
|
|
gap: 12px;
|
|
|
padding: 4px 12px;
|
|
|
padding-right: 4px;
|
|
|
- margin: 12px 16px;
|
|
|
border: 1px solid var(--input-border);
|
|
|
border-radius: 20px;
|
|
|
box-sizing: border-box;
|
|
|
+
|
|
|
.input-icon {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
@@ -410,39 +633,8 @@ const handleClose = () => {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
}
|
|
|
- .input-style {
|
|
|
- background-color: var(--color-theme);
|
|
|
- span {
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
- &:hover {
|
|
|
- background-color: #d56200;
|
|
|
- }
|
|
|
- }
|
|
|
- &.focus-style {
|
|
|
- border: 1px solid var(--color-theme);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
- // .input-area {
|
|
|
- // width: 100%;
|
|
|
- // font-size: 14px;
|
|
|
- // line-height: 21px;
|
|
|
- // padding: 4px;
|
|
|
- // resize: none;
|
|
|
- // overflow-y: hidden; // 默认不显示滚动条
|
|
|
- // height: 40px; // 初始高度(1 行)
|
|
|
- // max-height: 100px; // 最多 4 行
|
|
|
- // box-sizing: border-box;
|
|
|
- // border: none;
|
|
|
- // outline-color: #fff;
|
|
|
- // border-radius: 8px;
|
|
|
- // transition: height 0.1s ease;
|
|
|
- // &::placeholder {
|
|
|
- // color: #b5b9bf;
|
|
|
- // opacity: 1;
|
|
|
- // }
|
|
|
- // }
|
|
|
@keyframes loading-rotate {
|
|
|
0% {
|
|
|
transform: rotate(0deg);
|