first commit

This commit is contained in:
2025-12-24 18:19:05 +08:00
commit 78407f1cbd
283 changed files with 170690 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
<template>
<div class="dialog">
<div class="file">
<el-image
v-for="(i, index) in answers.previewImages"
:key="index"
:src="i.url"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:scale="0.8"
:preview-src-list="answers.previewImages.map(i=>i.url)"
show-progress
:initial-index="4"
fit="object-fit"
/>
</div>
<div v-if="answers.query" class="user">
{{ answers.query }}
</div>
<div v-if="answers.answer" class="ai-message">
<img src="@/assets/images/largeModel/icon-robot.png" alt="">
<div class="content" v-html="formattedAnswer(answers.answer)"></div>
</div>
<div class="loading" v-else>
<span class="dot" v-for="i in 3" :key="i"></span>
</div>
</div>
<el-image-viewer
v-if="showPreview"
:url-list="srcList"
show-progress
:max-scale="7"
:min-scale="0.2"
:scale="0.8"
fit="object-fit"
@close="showPreview = false"
/>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { marked } from 'marked'
// import { useStore } from 'vuex'
import GlobalMap from '@/components/map/js/GlobalMap.js'
// const store = useStore()
const emit = defineEmits([ 'openVideo', 'openUav' ])
const videoArr = ref([])
const uavArr = ref([])
const boatArr = ref([])
const codes = ref([])
const visible = ref(false)
// 创建自定义渲染器
const renderer = new marked.Renderer()
const showPreview = ref(false)
const srcList = ref([])
// 自定义链接渲染
renderer.link = function(e) {
if(e.href) {
return e.href.split(' ').map(i => {
return renderMedia(i)
})
}
}
// 渲染媒体元素的通用函数
const renderMedia = (url, text) => {
if (!url) {
return ''
}
console.log(url, 'url')
// 监控
if (url.startsWith('{') && url.includes('rtsp_url')) {
const videoNameMatch = JSON.parse(url).video_name
const codeMatch = JSON.parse(url).video_code
const previewUrlMatch = JSON.parse(url).rtsp_url
const videoObj = {
videoName: videoNameMatch,
videoCode: codeMatch,
previewUrl: previewUrlMatch
}
if (videoObj.videoCode) {
const exists = videoArr.value.some(item =>
item.videoCode === videoObj.videoCode && item.previewUrl === videoObj.previewUrl
)
let index
if (!exists) {
// 只有不存在时才添加
index = videoArr.value.length
videoArr.value.push(videoObj)
} else {
// 如果已存在,找到对应的索引
index = videoArr.value.findIndex(item =>
item.videoCode === videoObj.videoCode && item.previewUrl === videoObj.previewUrl
)
}
// 创建全局事件来处理视频弹窗
return `<div style="color:#1D91FE ;cursor:pointer" onclick="openVideoHandler('${index}')">
${videoObj.videoName}
</div>`
}
}
// 无人机
if(url.startsWith('{') && url.includes('droneSn')) {
if(JSON.parse(url).droneSn) {
const droneSnMatch = JSON.parse(url).droneSn
const gatewaySnMatch = JSON.parse(url).gatewaySn
const videoNameMatch = JSON.parse(url).video_name
const uavObj = {
droneSn: droneSnMatch,
gatewaySn: gatewaySnMatch,
videoName: videoNameMatch
}
if (uavObj.videoName) {
const exists = uavArr.value.some(item =>
item.videoName === uavObj.videoName
)
let index
if (!exists) {
// 只有不存在时才添加
index = uavArr.value.length
uavArr.value.push(uavObj)
} else {
// 如果已存在,找到对应的索引
index = uavArr.value.findIndex(item =>
item.droneSn === uavObj.droneSn || item.gatewaySnMatch === uavObj.gatewaySnMatch
)
}
// 创建全局事件来处理视频弹窗
return `<div style="color:#1D91FE ;cursor:pointer" onclick="openUavHandler('${index}')">
${uavObj.videoName}
</div>`
}
}
}
// 船舶智能追踪
if(url.startsWith('{') && url.includes('mmsi')) {
if(JSON.parse(url).mmsi) {
const terminalCodeMatch = JSON.parse(url).mmsi
const boatNameMatch = JSON.parse(url).boatName
const sogMatch = JSON.parse(url).sog
const cogMatch = JSON.parse(url).cog
const longitudeMatch = JSON.parse(url).longitude
const latitudeMatch = JSON.parse(url).latitude
const boatObj = {
terminalCode: terminalCodeMatch,
boatName: boatNameMatch,
sog: Number(sogMatch),
cog: 360 - Number(cogMatch),
angle: Number(cogMatch),
longitude: longitudeMatch,
latitude: latitudeMatch
}
if (boatObj.terminalCode) {
const exists = boatArr.value.some(item =>
item.terminalCode === boatObj.terminalCode
)
let index
if (!exists) {
// 只有不存在时才添加
index = boatArr.value.length
boatArr.value.push(boatObj)
} else {
// 如果已存在,找到对应的索引
index = boatArr.value.findIndex(item =>
item.terminalCode === boatObj.terminalCode || item.boatName === boatObj.boatName
)
}
// 创建全局事件来处理视频弹窗
return `<div style="color:#1D91FE ;cursor:pointer" onclick="openTrawlerHandler('${index}')">
${boatObj.boatName}
</div>`
}
}
}
// 检查是否为视频链接
if (url.match(/\.(mp4|webm|ogg)(\?.*)?$/i)) {
return `<div style="margin: 10px 0;">
<video controls loop muted style="width: 260px; height: 150px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 10px 10px 0;">
<source src="${url}" type="video/${url.split('.').pop().split('?')[0]}">
您的浏览器不支持视频播放。
</video>
</div>`
}
// 检查是否为图片链接
if (url.match(/\.(jpg|jpeg|png|gif|bmp|webp)(\?.*)?$/i)) {
return `<img onclick="openImageHandle('${url}')" src="${url}" alt="${text || ''}" style="width: 260px; height: 150px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 10px 10px 0;" />`
}
}
// 监控视频查看
const openVideoHandler = (index) => {
const data = videoArr.value[index]
// 雷云一体机一个视频
if(data.videoName.indexOf('雷云') !== -1) {
const videos = videoArr.value.filter(i => i.videoName.indexOf('雷云') !== -1).sort((a, b) => {
const index1 = a.videoName[a.videoName.length - 1]
const index2 = b.videoName[b.videoName.length - 1]
return index1 - index2
})
codes.value = [ videos[0].videoCode ]
}else{
if(data.videoName.indexOf('球机') !== -1) {
const videos = videoArr.value.filter(i => i.videoName.indexOf('球机') !== -1).sort((a, b) => {
const index1 = a.videoName[a.videoName.length - 1]
const index2 = b.videoName[b.videoName.length - 1]
return index1 - index2
})
codes.value = videos.map((i, index) => {
return i.previewUrl
})
}else{
codes.value = [ data.videoCode ]
}
}
nextTick(() => {
setTimeout(() => {
visible.value = true
emit('openVideo', { codes: codes.value, visible: visible.value })
// 再次延迟后触发窗口大小调整事件模拟resize
setTimeout(() => {
window.dispatchEvent(new Event('resize'))
}, 100)
}, 50)
})
}
// 预览图片
const openImageHandle = (url) => {
srcList.value = [ url ]
showPreview.value = true
}
// 开启无人机控制
const openUavHandler = (index) => {
const data = uavArr.value[index]
// store.commit('mapStore/updateUavData', { ...data })
emit('openUav', { ...data })
}
// 渔船追踪
const openTrawlerHandler = (index) => {
const data = boatArr.value[index]
// store.commit('mapStore/updateWindowInfo', {
// visible: true,
// type: '_trawler_dynamic',
// data: { ...data }
// })
GlobalMap.instance.map.animateTo(
{
zoom: 20,
center: [ data.longitude, data.latitude ]
},
{
duration: 1000 * 0.5,
easing: 'out'
}
)
}
// 配置 marked 选项并应用自定义渲染器
marked.setOptions({
renderer: renderer,
gfm: true,
breaks: true,
smartypants: false
})
const props = defineProps({
answers: {
type: Object,
default: () => {
return {}
}
}
})
const formattedAnswer = (answer) => {
if (!answer) {
return ''
}
const processedText = answer.replace(/{[^}]+}/g, (match) => {
return renderMedia(match)
})
return marked.parse(processedText)
}
onMounted(() => {
videoArr.value = []
window.openVideoHandler = openVideoHandler
window.openImageHandle = openImageHandle
// 无人机控制方法
window.openUavHandler = openUavHandler
// 渔船追踪
window.openTrawlerHandler = openTrawlerHandler
})
onUnmounted(() => {
delete window.openVideoHandler
delete window.openImageHandle
delete window.openUavHandler
delete window.openTrawlerHandler
})
</script>
<style lang="scss" scoped>
.dialog {
height: calc(100% - 50px);
overflow: auto;
}
.user,.file{
color: #E1F1FA;
max-width: calc(100% - 88px);
padding: 6px;
font-size: 14px;
line-height: 24px;
width: fit-content;
margin-left: auto;
background: rgba(89,175,255,0.15);
border-radius: 3px 3px 3px 3px;
}
.file{
background: none;
padding-right: 0;
.el-image{
width:100px;
height: 100px;
margin-left: 10px;
cursor: pointer;
}
}
.ai-message {
color: #E1F1FA;
border-radius: 22px;
max-width: calc(100% - 88px);
padding: 10px 0;
font-size: 14px;
line-height: 24px;
width: fit-content;
margin-right: auto;
text-align: left;
display: flex;
img{
width: 16px;
height: 16px;
margin-right: 10px;
}
.content{
margin-bottom: 10px;
background: rgba(89,175,255,0.15);
border-radius: 3px 3px 3px 3px;
padding: 6px;
}
}
.loading {
color: #0C1533;
border-radius: 22px;
max-width: calc(100% - 88px);
padding: 10px 16px;
font-size: 16px;
line-height: 24px;
width: fit-content;
margin-right: auto;
text-align: left;
}
.dot {
display: inline-block;
width: 6px;
height: 6px;
background-color: #ccc;
border-radius: 50%;
margin: 0 2px;
animation: dot-animation 1.5s infinite ease-in-out;
}
@keyframes dot-animation {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
</style>