You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fu-hsi-web/src/views/PoliceAi/index.vue

978 lines
32 KiB
Vue

<template>
<div class="PoliceAi">
<HistoricalRecords v-if="caseOptions.length > 0" ref="HistoricalRecordsRef" @getDetail="getDetail" @addNew="addNew" />
<div class="PoliceAi-main">
<div v-if="!conversationId" class="case-title">
<div class="case-item">
<span>案件名称</span>
<span>|</span>
<el-select v-model="caseId" filterable :disabled="conversationId !==''" style="width: 200px;" clearable placeholder="请选择案件" @change="selectCase">
<el-option v-for="item in caseOptions" :key="item.id" :label="item.caseName" :value="item.id" />
</el-select>
</div>
<el-input v-model="caseActorName" class="case-input" readonly placeholder="行为人" />
</div>
<div ref="scrollbar" :style="{marginTop:!conversationId?'110px':'0px'}" class="main-content" @scroll="handleScroll">
<div class="robot">
<div class="robot-top">
<img src="../../assets/police/robot.png" alt="">
<span>伏小羲</span>
</div>
<span class="robot-item">
你好,本数字民警可为您提供案件分析结果查询服务,您还能通过输入 @来选取特定罪与非罪指标结果,以便精准获取您所需信息。
</span>
</div>
<div v-if="navListFlag" class="nav-list">
<div v-for="(item, index) in intentTypeList" :key="index" class="nav-item" @click="selectCaseType(item)">
<div class="nav-item-top">
<img v-if="item.type ==='1'" src="../../assets/police/analysis.png" alt="">
<img v-if="item.type ==='2'" src="../../assets/police/case_info.png" alt="">
<img v-if="item.type ==='3'" src="../../assets/police/evidence.png" alt="">
<span>{{ item.name }}</span>
</div>
<span class="desc">{{ item.desc }}</span>
</div>
</div>
<div
v-for="(message, index) in conversationList"
:id="'message' + index"
:key="index"
class="conversation-item"
>
<div v-if="message.role === 'user'" class="user-message">
{{ message.text }}
</div>
<transition name="fade">
<div v-if="message.role === 'robot'" class="robot">
<div class="robot-top">
<img src="../../assets/police/robot.png" alt="">
<span>伏小羲</span>
</div>
<div v-if="index !== conversationList.length -1 || !loading">
<!-- 不询问 -->
<div v-if="message.dialogueCount !== 1 ">
<!-- 案件分析结果 -->
<div v-if="message.intentType === '1'" class="robot-item">
<span>您好,【<span>{{ message.caseName }}</span> 】的综合得分为<span :class="['score', message.totalScore >= 70 ? 'success' : 'warning']">{{ `${message.commonScore}+${message.specificCrimeScore}=${message.totalScore}` }}</span>
分, 认定<span :class="['score', message.totalScore >= 70 ? 'success' : 'warning']">{{ message.scoreDesc }}</span>
</span>
<span class="robot-message-item">
<span class="point" />
<span>{{ `${message.commonIndexCount}个共性证据指标占${message.commonIndexHitCount}个` }}</span>
</span>
<span class="robot-message-item">
<span class="point" />
<span>{{ `${message.specificCrimeIndexCount}个${message.specificCrime}占${message.specificCrimeIndexHitCount}个` }}</span>
</span>
</div>
<!-- 案件概述 案件证据指引 -->
<div v-if="message.intentType === '2' || message.intentType === '3'" class="robot-item">
<span class="robot-message-item">
{{ message.text }}
</span>
</div>
</div>
<!-- 指标回答 -->
<div v-if="message.intentType === '0'">
<div class="robot-item index-content">
<span class="index-item">
<span class="label">指标类型:</span>
<span :title="getIndexType(message.indexType)" class="value">{{ getIndexType(message.indexType) }}</span>
</span>
<span class="index-item">
<span class="label">指标名称:</span>
<span class="value">{{ message.indexName }}</span>
</span>
<span class="index-item">
<span class="label">分析结果:</span>
<span class="value">{{ message.result }}</span>
</span>
</div>
<div v-if="message.qaSplitList.length > 0 || message.evidenceNames.length > 0" class="collapse" @click="changeShowFlag(message)">
<span>相关笔录及证据</span>
<i v-if="!message.showFlag" class="el-icon-arrow-down" />
<i v-if="message.showFlag" class="el-icon-arrow-up" />
</div>
<div v-if="message.showFlag" class="collapse-content">
<div v-if="message.qaSplitList.length > 0" class="title">涉及笔录:</div>
<div v-for="(item, index) in message.qaSplitList" :key="index" class="record-item">
<span class="file-name" @click="downloadRecord(item)">{{ item.noteName }}</span>
<span>{{ item.answer }}</span>
<span>{{ item.question }}</span>
</div>
<div v-if="message.evidenceNames.length > 0" class="title">证据:</div>
<div class="evidence-list">
<div v-for="(item, index) in message.evidenceNames" :key="index" class="evidence-item">
<span class="file-name" @click="downloadEvidence(item)">{{ item.evidenceName }}</span>
</div>
</div>
</div>
</div>
<!-- dify回答 -->
<div v-if="message.type === '0' ">
<div v-if="message.answer" class="robot-item">
<span class="robot-message-item">
{{ message.answer }}
</span>
<div v-if="message.askFlag && message.intentType && message.intentType!=='0'" class="btns">
<div class="btn-item" @click="selectAskItem(message,'是')">是</div>
<div class="btn-item" @click="selectAskItem(message,'否')">否</div>
</div>
</div>
<div v-if="message.segmentList && message.segmentList.length > 0" class="collapse" @click="changeShowFlag(message)">
<span>相关知识内容</span>
<i v-if="!message.showFlag" class="el-icon-arrow-down" />
<i v-if="message.showFlag" class="el-icon-arrow-up" />
</div>
<div v-if="message.segmentList && message.segmentList.length > 0 && message.showFlag" class="collapse-content">
<div class="title">涉及文件:</div>
<div v-for="(item, index) in message.segmentList" :key="index" class="record-item">
<span style="color: #333333;cursor: unset;" class="file-name ">{{ item.name }}</span>
<span v-html="item.snippet" />
</div>
</div>
</div>
<!-- 指标询问选项 -->
<div v-if="message.adviceList && message.askFlag " class="option-list">
<span v-for="(item, index) in message.adviceList" :key="index" class="option-item" @click="selectAskIndex(message,item.indexName)">
<span class="name">{{ `${item.indexType} ${item.indexType?'|':''} ${item.indexName}` }}</span>
<i class="el-icon-arrow-right" />
</span>
</div>
</div>
<!-- <div v-if="!message.adviceList && message.askFlag ">
</div> -->
<div v-else class="loading">
<img src="../../assets/common/loading.gif" alt="">
<span>模型正在生成结果....</span>
</div>
</div>
</transition>
</div>
</div>
<div class="send-bottom">
<el-input
v-model="sendText"
:readonly="!caseId"
placeholder="可咨询案件内容、输入@可选择查询指标结果"
@keyup.enter.native="handleSend"
/>
<img v-if="!loading" class="input-img" src="@/assets/police/send.png" alt="" @click="handleSend">
<img v-if="loading" class="input-img" src="@/assets/police/pause.png" alt="">
<div v-if="indexFlag" class="select_index">
<div class="top">
<span v-for="(item, index) in indexTypeList" :key="index" :class="[item.type === activedIndex?'actived':'']" @click="changeType(item)">
{{ item.name }}
</span>
</div>
<el-input
ref="myInput"
v-model="searchName"
maxlength="50"
class="top-input"
placeholder="请输入内容"
>
<i slot="prefix" class="el-input__icon el-icon-search" />
</el-input>
<div class="index-list">
<span v-for="(item,index) in showIndexList" :key="index" class="index-list-item" @click="selectIndex(item.name)">
{{ item.name }}
</span>
</div>
</div>
<div v-if="isShowDownIcon" class="scroll-icon" @click="scrollToBottom">
<img src="../../assets/police/down.png" alt="">
</div>
</div>
</div>
</div>
</template>
<script>
import { queryCaseList, robotChat, conversationInfoList } from '@/api/caseManagement'
import { queryIndexData } from '@/api/indexRule'
import HistoricalRecords from './HistoricalRecords.vue'
import { baseURL } from '@/config'
import { downloadEvidence, commonDownloadFile } from '@/api/config/uploadApi'
export default {
name: 'PoliceAi',
components: {
HistoricalRecords
},
data() {
return {
caseId: '',
caseActorName: '',
// 是否第一次进入
firstEnter: true,
navListFlag: true,
sendText: '',
indexFlag: false,
loading: false,
conversationId: '',
conversationList: [],
indexTypeList: [
{
name: '共性指标',
type: '1'
},
{
name: '入罪指标',
type: '2'
},
{
name: '出罪指标',
type: '3'
}
],
activedIndex: '1',
// 是否显示down icon
isShowDownIcon: false,
indexList: [],
showIndexList: [], // 展示指标
intentTypeList: [
{
name: '案件分析结果',
type: '1',
desc: '案件分析预测入罪与出罪,提示司法判定风险'
},
{
name: '案件概况',
type: '2',
desc: '获取案件的基本情况概述'
},
{
name: '案件证据指引',
type: '3',
desc: '为补充证据材料信息提供导向'
}
],
searchName: '',
distanceToBottom: 0,
caseOptions: []
}
},
watch: {
sendText(val) {
if (val === '@') {
this.indexFlag = true
this.activedIndex = '1'
this.getIndexData('1')
this.$nextTick(() => {
const inputElement = this.$refs.myInput.$el.querySelector('input')
inputElement.focus()
})
} else {
this.indexFlag = false
}
},
searchName(val) {
if (val) {
const list = this.filterByProperty(this.indexList, 'name', val)
this.showIndexList = list
} else {
this.showIndexList = this.indexList
}
}
},
mounted() {
this.fetchData()
},
methods: {
// 获取详情
async getDetail(id, caseId) {
this.loading = false
this.caseId = caseId
this.selectCase(this.caseId)
this.conversationList = []
this.conversationId = id
const res = await conversationInfoList({
conversationId: this.conversationId,
pageNum: 1,
pageSize: 99999
})
if (res.code === 200) {
for (const item of res.data.records) {
this.conversationList.push({
role: 'user',
text: item.question
})
this.getAnswerContent(item)
this.navListFlag = false
}
}
},
// 开启新对话
addNew() {
this.caseId = ''
this.conversationId = ''
this.caseActorName = ''
this.conversationList = []
this.navListFlag = true
},
// 请求接口数据
fetchData() {
queryCaseList({}, 1, 99999).then(res => {
this.caseOptions = res.data.records
})
},
// 查询指标
getIndexData(val) {
queryIndexData({
indexType: val
}, 1, 9999).then(res => {
this.indexList = res.data.result
this.showIndexList = res.data.result
})
},
// 切换指标类型
changeType(item) {
this.activedIndex = item.type
this.getIndexData(item.type)
this.searchName = ''
},
selectCase(val) {
const obj = this.caseOptions.find(item => item.id === val)
this.caseActorName = obj.caseActorName
},
selectCaseType(item) {
if (this.loading) return
if (!this.caseId) {
this.$baseMessage.error('请选择案件!')
return
}
this.conversationList.push({
role: 'user',
text: item.name
})
this.navListFlag = false
this.chat({
caseId: this.caseId,
conversationId: this.conversationId,
query: item.name,
type: '1',
intentType: item.type
})
},
// 过滤数组
filterByProperty(arr, key, searchString) {
return arr.filter(obj =>
obj[key] && obj[key].toString().toLowerCase().includes(searchString.toLowerCase())
)
},
downloadFile(url, fileName) {
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
},
// 下载证据
downloadEvidence(item) {
console.log('baseURL', baseURL)
this.downloadFile(`${baseURL}${downloadEvidence}${item.evidenceId}`, item.evidenceName)
},
// 下载笔录
downloadRecord(item) {
// const fileId = item.fileIds.split(',')[0]
this.downloadFile(`${baseURL}${commonDownloadFile}${item.noteFileId}`, item.noteName)
},
// 选择询问指标
selectAskIndex(item, name) {
this.$set(item, 'askFlag', false)
this.selectIndex(name, name === '以上都不是' ? 'none' : '', item.dialogueCount)
},
// 选择是否
selectAskItem(item, val) {
this.$set(item, 'askFlag', false)
if (this.loading) return
this.indexFlag = false
this.searchName = ''
this.conversationList.push({
role: 'user',
text: val
})
this.sendText = ''
this.chat({
caseId: this.caseId,
query: val,
conversationId: this.conversationId,
dialogueCount: item.dialogueCount,
type: '0',
intentType: item.intentType
})
},
// 选择指标
selectIndex(name, val, dialogueCount) {
if (this.loading) return
this.indexFlag = false
this.searchName = ''
this.conversationList.push({
role: 'user',
text: name
})
this.sendText = ''
this.chat({
caseId: this.caseId,
query: name,
dialogueCount,
conversationId: this.conversationId,
type: val === 'none' ? '0' : '1',
intentType: val === 'none' ? '' : '0'
})
},
chat(obj) {
this.loading = true
this.conversationList.push({
role: 'robot'
})
this.scrollToBottom()
robotChat(obj).then(res => {
this.conversationList.splice(this.conversationList.length - 1, 1)
if (res.code === 200) {
if (this.conversationId !== res.data.conversationId) {
this.$refs.HistoricalRecordsRef.getList(res.data.conversationId)
}
this.conversationId = res.data.conversationId
this.getAnswerContent(res.data, 'add')
}
}).catch(() => {
// 处理错误
this.loading = false
this.conversationList.splice(this.conversationList.length - 1, 1)
this.conversationList.push({
role: 'robot',
intentType: '2',
text: '抱歉,我可能没理解清楚。如需进一步咨询,请明确您的问题。同时,输入“@”可选取特定指标结果,以便精准获取信息。'
})
})
},
// 识别回答内容
getAnswerContent(data, type) {
// 判断是否询问
if (data.dialogueCount === 1) {
this.conversationList.push({
role: 'robot',
answer: data.answer,
type: data.type,
...data.answwerMap,
intentType: data.intentType,
dialogueCount: data.dialogueCount,
askFlag: type === 'add'
})
} else {
if (data.intentType === '1') {
this.conversationList.push({
...data.answwerMap,
role: 'robot',
intentType: data.intentType
})
data.type = '1'
} else if (data.intentType === '2') {
this.conversationList.push({
role: 'robot',
intentType: data.intentType,
text: data.answwerMap.answerText
})
data.type = '1'
} else if (data.intentType === '3') {
this.conversationList.push({
role: 'robot',
intentType: data.intentType,
text: data.answwerMap.guideDesc
})
data.type = '1'
// 指标问答
} else if (data.intentType === '0' && data.answwerMap.indexName) {
this.conversationList.push({
role: 'robot',
intentType: data.intentType,
...data.answwerMap
})
data.type = '1'
}
// dify 问答
if (data.type === '0') {
this.conversationList.push({
role: 'robot',
answer: data.answer,
type: data.type,
segmentList: data.segmentList,
...data.answwerMap
})
}
}
setTimeout(() => {
this.loading = false
this.scrollToBottom()
}, 2000)
},
// 获取指标类型名称
getIndexType(val) {
for (const item of this.indexTypeList) {
if (val === item.type) {
return item.name
}
}
},
// 展开收缩具体内容
changeShowFlag(item) {
if (item.showFlag === true) {
this.$set(item, 'showFlag', false)
} else {
this.$set(item, 'showFlag', true)
}
},
scrollToBottom() {
// const scrollbar = this.$refs.scrollbar
// if (scrollbar) {
// scrollbar.wrap.scrollTo(0, scrollbar.wrap.scrollHeight)
// }
this.$nextTick(() => {
const div = this.$refs.scrollbar
div.scrollTop = div.scrollHeight
})
},
// 滚动
handleScroll(event) {
const scrollTop = event.target.scrollTop // 当前滚动位置
const clientHeight = event.target.clientHeight // 可视区域高度
const scrollHeight = this.$refs.scrollbar.scrollHeight // 内容总高度
// 计算滚动条底部的距离
this.distanceToBottom = scrollHeight - (scrollTop + clientHeight)
if (this.distanceToBottom > 2) {
this.isShowDownIcon = true
} else {
this.isShowDownIcon = false
}
},
handleSend() {
if (this.loading) return
if (!this.caseId) {
this.$baseMessage.error('请选择案件!')
return
}
if (!this.sendText) {
this.$baseMessage.error('请输入内容!')
return
}
this.conversationList.push({
role: 'user',
text: this.sendText
})
this.scrollToBottom()
this.chat({
caseId: this.caseId,
query: this.sendText,
conversationId: this.conversationId,
dialogueCount: this.conversationList.length > 2 ? this.conversationList[this.conversationList.length - 2].dialogueCount : '',
type: '0',
intentType: this.conversationList.length > 2 ? this.conversationList[this.conversationList.length - 2].intentType : ''
})
this.sendText = ''
}
}
}
</script>
<style lang="scss" scoped>
.PoliceAi {
background: #FFFFFF;
height: 100%;
position: relative;
// padding-top: 24px;
display: flex;
.PoliceAi-main {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
padding-top: 24px;
.case-title {
width: 632px;
height: 98px;
background: rgba(55,99,255,0.15);
border-radius: 21px 21px 21px 21px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 9;
top: 24px;
.case-item {
display: flex;background: #FFFFFF;
border-radius: 6px 6px 6px 6px;
border: 1px solid #D9D9D9;
padding: 8px 16px;
align-items: center;
justify-content: center;
span {
font-size: 16px;
color: #333333;
margin-right: 16px;
}
::v-deep {
.el-input__inner {
border: none !important;
}
}
}
.case-input {
width: 202px;
height: 50px;
background: #F4F8FF;
border-radius: 6px 6px 6px 6px;
margin-left: 16px;
::v-deep {
.el-input__inner {
height: 50px !important;
}
}
}
}
.main-content {
flex: 1;
overflow-y: auto;
width: 1154px;
position: relative;
// padding-right: 16px;
.robot {
.robot-top {
display: flex;
align-items: center;
margin-bottom: 10px;
img {
width: 45px;
height: 45px;
margin-right: 10px;
}
span {
font-size: 20px;
color: #3763FF;
}
}
.loading {
display: flex;
background: #F5F7FF;
border-radius: 16px 0px 16px 16px;
line-height: 28px;font-size: 16px;
color: #333333;
padding: 16px;
margin-bottom: 16px;
align-items: center;
img {
width: 38px;
height: 38px;
margin-right: 8px;
}
}
.robot-item {
background: #F5F7FF;
border-radius: 16px 0px 16px 16px;
line-height: 28px;font-size: 16px;
color: #333333;
padding: 16px;
// width: 90%;
margin-bottom: 16px;
display: flex;
flex-direction: column;
.btns {
display: flex;
margin-top: 16px;
.btn-item {
width: 80px;
height: 40px;
box-shadow: 0px 2px 0px 0px rgba(0,0,0,0.04);
border-radius: 6px 6px 6px 6px;
border: 1px solid #3763FF;
margin-right: 24px;
font-size: 16px;
color: #3763FF;
cursor: pointer;
text-align: center;
line-height: 40px;
}
}
.success {
color: #00975E;
font-weight: bold;
}
.warning {
color: rgba(255, 147, 17, 1);
font-weight: bold;
}
.robot-message-item {
display: flex;
align-items: center;
.point {
width: 5px;
height: 5px;
background-color: black;
border-radius: 50%;
margin: 0 10px;
}
}
}
.index-content {
display: flex;
flex-direction: column;
.index-item {
display: flex;
margin-bottom: 16px;
.label {
font-weight: bold;
font-size: 16px;
color: #333333;
margin-right: 10px;
}
.value {
font-size: 16px;
color: #333333;
width: 900px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.collapse {
width: 198px;
padding: 10px 16px;
background: #F6F7F9;
border-radius: 6px 6px 6px 6px;
font-size: 16px;
display: flex;
color: #666666;
justify-content: space-between;
margin-bottom: 8px;
cursor: pointer;
}
.collapse-content {
padding: 16px;
background: #F6F7F9;
border-radius: 6px 6px 6px 6px;
// width: 90%;
.title {
font-size: 16px;
color: #333333;
margin-bottom: 8px;
}
.record-item {
margin-bottom: 16px;
display: flex;
flex-direction: column;font-size: 16px;
color: #333333;
line-height: 28px;
.file-name {
font-size: 16px;
color: #3763FF;
cursor: pointer;
margin-bottom: 8px;
}
}
.evidence-list {
display: flex;
.evidence-item {
font-size: 16px;
color: #3763FF;
cursor: pointer;
margin-bottom: 8px;
margin-right: 8px;
}
}
}
.option-list {
display: flex;
flex-direction: column;
.option-item {
display: flex;
align-items: center;
position: relative;
font-size: 16px;
cursor: pointer;
color: #464546;
margin-bottom: 16px;
.name {
border-radius: 9px 9px 9px 9px;
border: 1px solid #D9D9D9;
padding: 8px 36px 8px 16px;
}
i {
margin-left: -32px;
}
}
.option-item:hover {
color: #3763FF;
.name{
border: 1px solid #3763FF;
}
}
}
}
.user-message {
margin: 24px 0;
text-align: right;font-size: 16px;
color: #333333;
padding-right: 16px;
}
.nav-list {
display: flex;
margin-top: 16px;
margin-bottom: 16px;
.nav-item {
background: #FFFFFF;
border-radius: 8px 8px 8px 8px;
border: 1px solid #D9D9D9;
padding: 24px;
margin-right: 24px;
cursor: pointer;
.desc {
font-size: 14px;
color: #999999;
}
.nav-item-top {
display: flex;
margin-bottom: 8px;
align-items: center;
img {
width: 30px;
height: 30px;
margin-right: 10px;
}
}
}
}
}
.main-content::-webkit-scrollbar {
// display: none; /* 对于 Chrome, Safari 和 Opera 隐藏滚动条 */
}
.send-bottom {
width: 1154px;
height: 75px;
position: relative;
background: #FFFFFF;
border-radius: 16px 16px 16px 16px;
border: 2px solid rgba(55, 99, 255, 1);
// border-image: linear-gradient(90deg, rgba(55, 99, 255, 1), rgba(25, 166, 254, 1)) 2 2;
margin-bottom: 56px;
display: flex;
align-items: center;
position: relative;
::v-deep {
.el-input__inner {
border: none !important;
font-size: 16px;
}
}
.input-img {
width: 60px;
height: 60px;
position: absolute;
right: 12px;
top: 12px;
cursor: pointer;
}
.select_index {
position: absolute;
bottom: 77px;
left: 12px;
width: 1132px;
height: 424px;
background: #FFFFFF;
box-shadow: 0px 0px 8px 0px rgba(0,0,0,0.15);
border-radius: 6px 6px 6px 6px;
.top {
height: 62px;
display: flex;
border-bottom: 1px solid #EAEAEA;
align-items: center;
padding-left: 24px;
span {
font-size: 16px;
height: 62px;
line-height: 62px;
color: #999999;
margin-right: 40px;
cursor: pointer;
}
.actived {
color: #3763FF;
border-bottom: 3px solid #3763FF;
}
}
.top-input {
width: 1000px;
border-radius: 6px 6px 6px 6px;
border: 1.4px solid #3763FF;
margin-top: 24px;
margin: 24px;
}
.index-list {
display: flex;
flex-direction: column;
overflow: auto;
padding-left: 24px;
height: 260px;
.index-list-item {
padding-left: 8px;
font-weight: 400;
font-size: 16px;
color: #333333;
cursor: pointer;
line-height: 45px;
}
.index-list-item:hover {
background: #F2F6FD;
}
}
}
}
.scroll-icon {
position: absolute;
bottom: 77px;
left: 50%;
display: flex;
justify-content: center;
align-items: center;
img {
width: 68px;
height: 68px;
cursor: pointer;
}
}
}
}
</style>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>