<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 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> </div> <!-- dify回答 --> <div v-if="message.type === '0' "> <div class="robot-item"> <span class="robot-message-item"> {{ message.answer }} </span> <div v-if="message.askFlag && message.intentType" 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;" 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)"> <span class="name">{{ item }}</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 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') } 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) { 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) }, // 选择是否 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) { 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, conversationId: this.conversationId, type: '1', intentType: '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 }) } } this.$nextTick(() => { setTimeout(() => { this.loading = false }, 2000) this.scrollToBottom() }) }, // 获取指标类型名称 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) // } 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 > 0) { 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, type: '0', 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; .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 { 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>