633 lines
18 KiB
Vue
633 lines
18 KiB
Vue
|
|
<template>
|
|||
|
|
<div :class="['FAQ-container',!historyShow ? 'FAQ-width' : '']">
|
|||
|
|
<div :class="['container']">
|
|||
|
|
<div class="title">
|
|||
|
|
<span class="front">海知无涯,智慧伴航,千帆智瞰,</span>
|
|||
|
|
<span class="behind">引领你探索未知的海域</span>
|
|||
|
|
</div>
|
|||
|
|
<!-- 对话框内容 -->
|
|||
|
|
<DialogRealCom @openVideo="openVideo" @openUav="openUav" v-if="dialog && type === 'real'" :answers="answers"/>
|
|||
|
|
<DialogHistoryCom @openVideo="openVideo" v-if="dialog && type === 'history'" :answers="answers"/>
|
|||
|
|
<div class="introduceRecommend" v-if="!dialog">
|
|||
|
|
<div class="introduce">
|
|||
|
|
<img src="@/assets/images/largeModel/icon-robot.png" alt="">
|
|||
|
|
<div class="content">
|
|||
|
|
<div v-for="(i,index) in describe" :key="index">{{i}}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="recommend">
|
|||
|
|
<div class="header">
|
|||
|
|
<div class="title">
|
|||
|
|
<img src="@/assets/images/largeModel/icon-recommend.png" alt="">
|
|||
|
|
推荐
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="content">
|
|||
|
|
<div class="row" v-for="(item, index) in recommends" :key="index" @click="command(item)">
|
|||
|
|
<div class="desc">{{index+1}}. {{ item }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="input-container">
|
|||
|
|
<!-- 图片预览区域 - 放在输入框上方 -->
|
|||
|
|
<div v-if="previewImages.length > 0" class="image-preview-inline">
|
|||
|
|
<div class="image-item" v-for="(image, index) in previewImages" :key="index">
|
|||
|
|
<img :src="image.url" alt="预览图片" />
|
|||
|
|
<el-icon class="image-actions" @click="removeImage(index)"><Close /></el-icon>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<el-input
|
|||
|
|
v-model="message"
|
|||
|
|
placeholder="点这里,尽管问"
|
|||
|
|
:autosize="{ minRows: 2, maxRows: 10 }"
|
|||
|
|
type="textarea"
|
|||
|
|
@keyup.enter="send"
|
|||
|
|
/>
|
|||
|
|
<div class="input-actions">
|
|||
|
|
<!-- <div class="button" @click="startSpeechRecognition" title="语音输入">
|
|||
|
|
<img src="@/assets/images/largeModel/icon-voice.png" alt="">
|
|||
|
|
</div> -->
|
|||
|
|
<!-- <div class="button" title="上传图片">
|
|||
|
|
<img src="@/assets/images/largeModel/icon-upload.png" alt="" @click="triggerFileInput">
|
|||
|
|
<input
|
|||
|
|
ref="fileInput"
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
multiple
|
|||
|
|
@change="handleFileUpload"
|
|||
|
|
style="display: none;"
|
|||
|
|
/>
|
|||
|
|
</div> -->
|
|||
|
|
<div class="button" @click="handleClick" :style="loading ? {background: 'linear-gradient(90deg, #1d91fe85 0%, #536ce8ab 100%)'} : {}">
|
|||
|
|
<el-icon class="loading" v-if="loading"><Loading /></el-icon>
|
|||
|
|
<img v-else src="@/assets/images/largeModel/icon-send.png" alt="">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|||
|
|
import DialogRealCom from './realDialog.vue'
|
|||
|
|
import DialogHistoryCom from './historyDialog.vue'
|
|||
|
|
import { dayjs } from 'element-plus'
|
|||
|
|
import { Close, Loading } from '@element-plus/icons-vue'
|
|||
|
|
import { getAnswers, restart } from '@/api/model.js'
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
const emit = defineEmits([ 'initList', 'openVideo', 'openUav' ])
|
|||
|
|
const props = defineProps({
|
|||
|
|
historyShow: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: true
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// 添加流式输出相关变量
|
|||
|
|
const answersBuffer = ref('')
|
|||
|
|
const isStreaming = ref(false)
|
|||
|
|
// 添加文件上传相关的响应式变量
|
|||
|
|
const fileInput = ref(null)
|
|||
|
|
const previewImages = ref([])
|
|||
|
|
const selectedFiles = ref([])
|
|||
|
|
|
|||
|
|
const getPeriod = computed(() => {
|
|||
|
|
const hour = dayjs().hour()
|
|||
|
|
if (hour >= 0 && hour < 6) {
|
|||
|
|
return '凌晨'
|
|||
|
|
}
|
|||
|
|
if (hour >= 6 && hour < 12) {
|
|||
|
|
return '上午'
|
|||
|
|
}
|
|||
|
|
if (hour >= 12 && hour < 18) {
|
|||
|
|
return '下午'
|
|||
|
|
}
|
|||
|
|
return '晚上'
|
|||
|
|
})
|
|||
|
|
const message = ref('')
|
|||
|
|
const describe = ref([])
|
|||
|
|
const recommends = ref()
|
|||
|
|
const loading = ref(false)
|
|||
|
|
// 对话框
|
|||
|
|
const dialog = ref(false)
|
|||
|
|
const type = ref('real')
|
|||
|
|
const answers = reactive({})
|
|||
|
|
// 语音录入
|
|||
|
|
let speechRecognition = null
|
|||
|
|
const isListening = ref(false)
|
|||
|
|
let resizeObserver = null
|
|||
|
|
|
|||
|
|
const openVideo = (data) => {
|
|||
|
|
emit('openVideo', data)
|
|||
|
|
}
|
|||
|
|
const openUav = (data) => {
|
|||
|
|
emit('openUav', data)
|
|||
|
|
}
|
|||
|
|
// 触发文件选择
|
|||
|
|
const triggerFileInput = () => {
|
|||
|
|
fileInput.value.click()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理文件上传
|
|||
|
|
const handleFileUpload = (event) => {
|
|||
|
|
const files = event.target.files
|
|||
|
|
if (files.length === 0) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证文件类型
|
|||
|
|
for (let i = 0; i < files.length; i++) {
|
|||
|
|
const file = files[i]
|
|||
|
|
if (file.type.startsWith('image/')) {
|
|||
|
|
// 创建预览
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
previewImages.value.push({
|
|||
|
|
url: e.target.result,
|
|||
|
|
name: file.name,
|
|||
|
|
size: file.size
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
reader.readAsDataURL(file)
|
|||
|
|
|
|||
|
|
// 存储原始文件
|
|||
|
|
selectedFiles.value.push(file)
|
|||
|
|
} else {
|
|||
|
|
alert(`请选择有效的图片文件: ${file.name}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 移除单个图片
|
|||
|
|
const removeImage = (index) => {
|
|||
|
|
previewImages.value.splice(index, 1)
|
|||
|
|
selectedFiles.value.splice(index, 1)
|
|||
|
|
if (fileInput.value) {
|
|||
|
|
fileInput.value.value = ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 清除所有图片
|
|||
|
|
const clearAllImages = () => {
|
|||
|
|
previewImages.value = []
|
|||
|
|
selectedFiles.value = []
|
|||
|
|
if (fileInput.value) {
|
|||
|
|
fileInput.value.value = ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 点击推荐
|
|||
|
|
const command = (item) => {
|
|||
|
|
message.value = item
|
|||
|
|
nextTick(() => {
|
|||
|
|
send()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 终止回答
|
|||
|
|
const stop = () => {
|
|||
|
|
axios.post(restart, {}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
}
|
|||
|
|
}).then(() => {
|
|||
|
|
}).finally(() => {
|
|||
|
|
loading.value = false
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
const send = () => {
|
|||
|
|
if(!loading.value && (message.value !== '' || previewImages.value.length > 0)) {
|
|||
|
|
loading.value = true
|
|||
|
|
answersBuffer.value = ''
|
|||
|
|
isStreaming.value = true
|
|||
|
|
// 先清空
|
|||
|
|
Object.keys(answers).forEach(key => {
|
|||
|
|
answers[key] = ''
|
|||
|
|
})
|
|||
|
|
dialog.value = true
|
|||
|
|
type.value = 'real'
|
|||
|
|
// 请求问答
|
|||
|
|
let params = { query: message.value }
|
|||
|
|
answers.query = message.value
|
|||
|
|
answers.previewImages = previewImages.value
|
|||
|
|
if(previewImages.value.length > 0) {
|
|||
|
|
params = { query: '查看这艘船的告警信息' }
|
|||
|
|
}
|
|||
|
|
fetch(getAnswers, {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(params),
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json' // 根据实际情况设置请求头
|
|||
|
|
},
|
|||
|
|
responseType: 'stream'
|
|||
|
|
}).then(async(res) => {
|
|||
|
|
// 判断响应是否正常
|
|||
|
|
if (!res.ok) {
|
|||
|
|
throw new Error(`HTTP error! status: ${res.status}`)
|
|||
|
|
}
|
|||
|
|
const reader = res.body.getReader()
|
|||
|
|
const decoder = new TextDecoder()
|
|||
|
|
let result = true
|
|||
|
|
try {
|
|||
|
|
while (result) {
|
|||
|
|
const { done, value } = await reader.read()
|
|||
|
|
|
|||
|
|
if (done) {
|
|||
|
|
result = false
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
// 解码并累加到缓冲区
|
|||
|
|
decoder.decode(value).split('\n')
|
|||
|
|
.forEach((val) => {
|
|||
|
|
if (!val) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
// 后端返回的流式数据一般都是以data:开头的字符,排除掉data:后就是需要的数据
|
|||
|
|
// 具体返回结构可以跟后端约定
|
|||
|
|
let txt = val?.replace('data:', '') || ''
|
|||
|
|
let data = JSON.parse(txt)
|
|||
|
|
if(data?.content) {
|
|||
|
|
answersBuffer.value += data.content
|
|||
|
|
}else if(data?.mp4_url || data?.image_url || data?.rtsp_url || data?.rtsp_data) {
|
|||
|
|
let additionalContent = ''
|
|||
|
|
if(data.image_url && Array.isArray(data.image_url)) {
|
|||
|
|
additionalContent += '\n' + data.image_url.join(' ') + '\n'
|
|||
|
|
}
|
|||
|
|
if(data.mp4_url && Array.isArray(data.mp4_url)) {
|
|||
|
|
additionalContent += '\n' + data.mp4_url.join(' ') + '\n'
|
|||
|
|
}
|
|||
|
|
if(data.rtsp_url && Array.isArray(data.rtsp_url)) {
|
|||
|
|
console.log('data.rtsp_url', data.rtsp_url)
|
|||
|
|
data.rtsp_url.forEach(item => {
|
|||
|
|
additionalContent += '\n' + JSON.stringify(item) + '\n'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
if(data.rtsp_data && Array.isArray(data.rtsp_data)) {
|
|||
|
|
console.log('data.rtsp_data', data.rtsp_data)
|
|||
|
|
data.rtsp_data.forEach(item => {
|
|||
|
|
additionalContent += '\n' + JSON.stringify(item) + '\n'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
answersBuffer.value += additionalContent
|
|||
|
|
}else{
|
|||
|
|
answersBuffer.value += ''
|
|||
|
|
}
|
|||
|
|
if(data.clear_previous) {
|
|||
|
|
answersBuffer.value = ''
|
|||
|
|
}
|
|||
|
|
answers.answer = answersBuffer.value
|
|||
|
|
nextTick(() => {
|
|||
|
|
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
} catch (err) { }
|
|||
|
|
})
|
|||
|
|
// 更新答案内容
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
answers.answer = '抱歉,获取回答时出现错误。'
|
|||
|
|
loading.value = false
|
|||
|
|
} finally {
|
|||
|
|
reader.releaseLock() // 显式释放 reader 锁
|
|||
|
|
loading.value = false
|
|||
|
|
isStreaming.value = false
|
|||
|
|
}
|
|||
|
|
}).catch(error => {
|
|||
|
|
console.error('请求错误:', error)
|
|||
|
|
loading.value = false
|
|||
|
|
isStreaming.value = false
|
|||
|
|
answers.answer = '抱歉,获取回答时出现错误。'
|
|||
|
|
|
|||
|
|
}).finally(() => {
|
|||
|
|
loading.value = false
|
|||
|
|
message.value = ''
|
|||
|
|
previewImages.value = []
|
|||
|
|
selectedFiles.value = []
|
|||
|
|
clearAllImages()
|
|||
|
|
emit('initList')
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const handleClick = () => {
|
|||
|
|
if(loading.value) {
|
|||
|
|
stop()
|
|||
|
|
}else {
|
|||
|
|
send()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 初始化语音识别
|
|||
|
|
const initSpeechRecognition = () => {
|
|||
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
|||
|
|
if (!SpeechRecognition) {
|
|||
|
|
console.warn('当前浏览器不支持语音识别功能')
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
const recognition = new SpeechRecognition()
|
|||
|
|
recognition.lang = 'zh-CN' // 设置语言为中文
|
|||
|
|
recognition.continuous = false // 设置为单次识别
|
|||
|
|
recognition.interimResults = true // 获取中间结果
|
|||
|
|
recognition.onstart = () => {
|
|||
|
|
isListening.value = true
|
|||
|
|
console.log('语音识别已启动')
|
|||
|
|
}
|
|||
|
|
recognition.onresult = (event) => {
|
|||
|
|
const transcript = Array.from(event.results)
|
|||
|
|
.map(result => result[0])
|
|||
|
|
.map(result => result.transcript)
|
|||
|
|
.join('')
|
|||
|
|
message.value = transcript
|
|||
|
|
console.log('识别结果:', transcript)
|
|||
|
|
}
|
|||
|
|
recognition.onerror = (event) => {
|
|||
|
|
console.error('语音识别出错:', event.error)
|
|||
|
|
isListening.value = false
|
|||
|
|
}
|
|||
|
|
recognition.onend = () => {
|
|||
|
|
isListening.value = false
|
|||
|
|
console.log('语音识别已结束')
|
|||
|
|
}
|
|||
|
|
return recognition
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始语音识别
|
|||
|
|
const startSpeechRecognition = () => {
|
|||
|
|
// if (!checkSpeechRecognitionSupport()) {
|
|||
|
|
// alert('当前浏览器不支持语音识别功能,请使用Chrome或其他支持Web Speech API的浏览器')
|
|||
|
|
// return
|
|||
|
|
// }
|
|||
|
|
if (!speechRecognition) {
|
|||
|
|
speechRecognition = initSpeechRecognition()
|
|||
|
|
}
|
|||
|
|
if (isListening.value) {
|
|||
|
|
// 如果正在监听,则停止
|
|||
|
|
speechRecognition.stop()
|
|||
|
|
isListening.value = false
|
|||
|
|
} else {
|
|||
|
|
// 开始语音识别
|
|||
|
|
try {
|
|||
|
|
speechRecognition.start()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('启动语音识别失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const changeDialog = (item) => {
|
|||
|
|
dialog.value = item.flag
|
|||
|
|
type.value = 'history'
|
|||
|
|
if(item.data) {
|
|||
|
|
Object.keys(JSON.parse(item.data)).forEach(key => {
|
|||
|
|
answers[key] = JSON.parse(item.data)[key]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
const changeRecommend = (data) => {
|
|||
|
|
describe.value = data.desc
|
|||
|
|
recommends.value = data.recommends
|
|||
|
|
}
|
|||
|
|
const observeContainerResize = () => {
|
|||
|
|
// 查找目标容器元素
|
|||
|
|
const targetElement = document.querySelector('.large-model-container')
|
|||
|
|
if (targetElement) {
|
|||
|
|
// 创建 ResizeObserver 实例
|
|||
|
|
resizeObserver = new ResizeObserver((entries) => {
|
|||
|
|
for (let entry of entries) {
|
|||
|
|
const height = entry.contentRect.height
|
|||
|
|
if (height < 585) {
|
|||
|
|
document.querySelector('.introduce').style.display = 'none'
|
|||
|
|
}else{
|
|||
|
|
document.querySelector('.introduce').style.display = 'flex'
|
|||
|
|
}
|
|||
|
|
if (height < 443) {
|
|||
|
|
document.querySelector('.recommend').style.display = 'none'
|
|||
|
|
}else{
|
|||
|
|
document.querySelector('.recommend').style.display = 'block'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// 开始观察目标元素
|
|||
|
|
resizeObserver.observe(targetElement)
|
|||
|
|
} else {
|
|||
|
|
console.log('未找到 .large-model-container 元素')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
onMounted(() => {
|
|||
|
|
nextTick(() => {
|
|||
|
|
observeContainerResize()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
if (resizeObserver) {
|
|||
|
|
resizeObserver.disconnect()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
defineExpose({
|
|||
|
|
changeDialog,
|
|||
|
|
changeRecommend
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.FAQ-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: calc(100% - 200px);
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
font-family: 'SHSCNR';
|
|||
|
|
&.FAQ-width{
|
|||
|
|
width: 100%
|
|||
|
|
}
|
|||
|
|
.title{
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
display: inline;
|
|||
|
|
.front{
|
|||
|
|
color: #FFFFFF;
|
|||
|
|
}
|
|||
|
|
.behind{
|
|||
|
|
color: #42D9FF;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.container{
|
|||
|
|
width: 100%;
|
|||
|
|
margin: 10% auto;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 20px;
|
|||
|
|
height: calc(100% - 220px);
|
|||
|
|
margin: 0 auto;
|
|||
|
|
|
|||
|
|
background: linear-gradient( 180deg, rgba(2,25,66,0.9) 1%, rgba(2,25,65,0.5) 99%);
|
|||
|
|
border-radius: 5px 5px 5px 5px;
|
|||
|
|
border: 1px solid;
|
|||
|
|
border-image: linear-gradient(180deg, rgba(0, 228, 236, 0.5), rgba(79, 139, 152, 0.1)) 1 1;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
padding: 20px;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
.introduceRecommend{
|
|||
|
|
height: calc(100% - 200px);
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 20px;
|
|||
|
|
}
|
|||
|
|
.introduce{
|
|||
|
|
display: flex;
|
|||
|
|
}
|
|||
|
|
.introduce img{
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
margin-right: 10px;
|
|||
|
|
}
|
|||
|
|
.introduce .content{
|
|||
|
|
font-weight: 400;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
// text-indent: 2em;
|
|||
|
|
padding: 6px;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
background: rgba(89,175,255,0.15);
|
|||
|
|
border-radius: 3px;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
.introduce.content:hover {
|
|||
|
|
transform: scale(1.02);
|
|||
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recommend,.describe{
|
|||
|
|
margin-top: 40px;
|
|||
|
|
.header{
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
.title{
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #42D9FF;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 5px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.content{
|
|||
|
|
display: flex;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
width: 100%;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
.row{
|
|||
|
|
width: 23%;
|
|||
|
|
img{
|
|||
|
|
width: 100%;
|
|||
|
|
height: 120px;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
border: 2px solid #3B80FF;
|
|||
|
|
}
|
|||
|
|
.title{
|
|||
|
|
font-weight: 500;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #fff;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}
|
|||
|
|
.desc{
|
|||
|
|
font-weight: 400;
|
|||
|
|
text-align: left;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: rgba(255, 255, 255, 0.7);
|
|||
|
|
line-height: 24px;
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.recommend{
|
|||
|
|
margin-top: 10px;
|
|||
|
|
.content{
|
|||
|
|
display: block;
|
|||
|
|
.row{
|
|||
|
|
width: 100%;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 图片预览样式 - 内联显示
|
|||
|
|
.image-preview-inline {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 10px;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
.image-item {
|
|||
|
|
position: relative;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
width: 80px;
|
|||
|
|
height: 80px;
|
|||
|
|
img {
|
|||
|
|
width: 80px;
|
|||
|
|
height: 80px;
|
|||
|
|
object-fit: cover;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
.image-actions {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 0;
|
|||
|
|
background: #bbb6b6;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 10px;
|
|||
|
|
padding: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 输入框
|
|||
|
|
.input-container{
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 200px;
|
|||
|
|
background: linear-gradient( 180deg, rgba(2,25,66,0.9) 1%, rgba(2,25,65,0.5) 99%);
|
|||
|
|
border-radius: 5px 5px 5px 5px;
|
|||
|
|
border: 1px solid;
|
|||
|
|
border-image: linear-gradient(180deg, rgba(0, 228, 236, 0.5), rgba(79, 139, 152, 0.1)) 1 1;
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 0;
|
|||
|
|
:deep(.el-textarea__inner){
|
|||
|
|
box-shadow: none;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
background: transparent;
|
|||
|
|
}
|
|||
|
|
.input-actions{
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
position: absolute;
|
|||
|
|
right: 10px;
|
|||
|
|
bottom: 0;
|
|||
|
|
.button{
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
line-height: 36px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.button .loading{
|
|||
|
|
color: #fff;
|
|||
|
|
animation: rotate 2s linear infinite;
|
|||
|
|
}
|
|||
|
|
.button:last-child{
|
|||
|
|
&:hover {
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 定义旋转动画
|
|||
|
|
@keyframes rotate {
|
|||
|
|
from {
|
|||
|
|
transform: rotate(0deg);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|