Browse Source

chatbi: 集成api 1.0

qin_27
han2015 2 weeks ago
parent
commit
e937ac642d
  1. 65
      src/api/date/chatbi.ts
  2. 156
      src/views/date/chatbi.vue
  3. 145
      src/views/date/chatbi_setting_panel.vue
  4. 352
      src/views/date/chatbit_ask_panel.vue
  5. 119
      src/views/date/device_panel.vue
  6. 2
      src/views/doc/spacePermission.vue

65
src/api/date/chatbi.ts

@ -0,0 +1,65 @@
import request from '@/utils/request';
import axios from 'axios';
/**
*
*/
export function getDevicesTree() {
return request({
url: "/aibot/devices/tree",
method: "get",
});
}
export function getDevicesMonitors(data:{deviceCode:string,rows:number,page:number}) {
return axios.post( import.meta.env.VITE_APP_BASE_API+"/aibot/devices/ep_monitors",data);
}
// 获取ChatBI列表
export function getChatBIList() {
return request({
url: '/aibot/chatbi/list',
method: 'get',
});
}
// 创建ChatBI
export function newChatBI(data: any) {
return request({
url: '/aibot/chatbi/new',
method: 'post',
data: data
});
}
// 编辑ChatBI
export function editChatBI(data: any) {
return request({
url: '/aibot/chatbi/edit',
method: 'post',
data: data
});
}
// 删除ChatBI
export function delChatBI(data: any) {
return request({
url: '/aibot/chatbi/del',
method: 'post',
data: data
});
}
export interface askChatBIRequest {
user_query:string;
db_type?:string;
db_config?:Object;
}
// 调用chatbi
export function askChatBI(data: askChatBIRequest, mode:string) {
return request({
url: '/aibot/chatbi/api?mode='+mode,
method: 'post',
data: data
});
}

156
src/views/date/chatbi.vue

@ -0,0 +1,156 @@
<!--
@ 时间: 2025-12-30
@ 备注: chatbi 服务配置界面
-->
<script setup lang="ts">
import device_panel from './device_panel.vue'
import ask_panel from './chatbit_ask_panel.vue'
import {getChatBIList,newChatBI,editChatBI,delChatBI} from "@/api/date/chatbi"
import { h } from 'vue'
import { Delete,Edit } from '@element-plus/icons-vue'
import { ElSelect,ElOption, ElInput,ElMessage,ElMessageBox } from 'element-plus'
import Chatbi_setting_panel from './chatbi_setting_panel.vue';
const dataTable=ref([])
const dynamicVNode = ref<VNode | null>(null)
const currentHoverRow=ref("") //table
const onChatBISetting=()=>{
dynamicVNode.value=h(device_panel,{
checked:[],
confirmFunc:(data:string[])=>{
},
closeFunc:()=>dynamicVNode.value=null
})
}
const handleMouseEnter=(row:any)=>{
currentHoverRow.value=row.uuid
}
const onChatBIEditOrNew=(row:any|null)=>{
const isEdit=row!==null
const newName=ref(isEdit?row.name:'')
const newType=ref(isEdit?row.type:'')
dynamicVNode.value=h(Chatbi_setting_panel,{
title: isEdit?'编辑ChatBI实例':'新建ChatBI实例',
name:newName.value,
dbtype:newType.value,
checked:row?row.dataset:"",
confirmFunc:(_name:string,_type:string, data:string[])=>{
if(_name!=""){
const params={
name: _name,
type: _type,
dataset: data.join(",")
}
const apiCall=isEdit?editChatBI({...params,uuid:row.uuid}):newChatBI(params)
apiCall.then(()=>{
ElMessage.success(isEdit?'编辑成功':'新建成功')
getChatBIList().then((res:any)=>{
dataTable.value=res.data||[]
})
}).catch(()=>{
ElMessage.error(isEdit?'编辑失败':'新建失败')
})
}else{
ElMessage.warning('请填写完整信息')
}
dynamicVNode.value=null
},
closeFunc:()=>dynamicVNode.value=null
})
}
const onChatBINew=()=>onChatBIEditOrNew(null)
const onEditChatBI=(row:any)=>onChatBIEditOrNew(row)
const onDeleteChatBI=(row:any)=>{
ElMessageBox.confirm(`确认删除项目( ${row.name}) ?删除后不可恢复!取消则放弃删除操作。`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
delChatBI({
uuid: row.uuid
}).then(()=>{
ElMessage.success('删除成功')
getChatBIList().then((res:any)=>{
dataTable.value=res.data||[]
})
}).catch(()=>{
ElMessage.error('删除失败')
})
})
}
const askChatBI=(row:any)=>{
dynamicVNode.value = h(ask_panel, {
'agent': { model: true, name: row.name, uuid: row.uuid,dbm:row.type,dataset:row.dataset },
'closefunc': () => dynamicVNode.value = null
})
}
onMounted(()=>{
getChatBIList().then((res:any)=>{
dataTable.value=res.data||[]
})
})
</script>
<template>
<div>
<el-button @click="onChatBISetting">chatbi 设置</el-button>
<el-button @click="onChatBINew">chatbi 新建</el-button>
</div>
<div>
<el-table
stripe
:data="dataTable"
ref="multipleTableRef"
:header-cell-style="{ background: '#f5f8fd' }"
style="width: 100%"
row-key="uuid"
:row-style ="() => ({ lineHeight: '36px' })"
@cell-mouse-enter="handleMouseEnter"
@selection-change="">
<el-table-column width="450" property="name" label="项目名">
</el-table-column>
<el-table-column width="360" property="type" label="类型">
<template #default="scope">
<span v-if="scope.row.type=='tsd'">时序型</span>
<span v-else >关系型</span>
</template>
</el-table-column>
<el-table-column width="360" label="配置">
<template #default="scope">
<div v-show="currentHoverRow === scope.row.uuid">
<span>
<el-button size="small" :icon="Edit" circle @click="onEditChatBI(scope.row)"></el-button>
<el-button size="small" :icon="Delete" circle @click="onDeleteChatBI(scope.row)"></el-button>
<el-button size="small" circle @click="askChatBI(scope.row)"></el-button>
</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="dynamicVNode">
<component :is="dynamicVNode" />
</div>
</template>
<style lang="scss" scoped>
</style>
<style>
.chatbi-new-dialog {
width: 800px !important;
}
</style>

145
src/views/date/chatbi_setting_panel.vue

@ -0,0 +1,145 @@
<!--
@ 时间: 2025-12-30
@ 备注: chatbi_setting_panel
-->
<script setup lang="ts">
import { getOrPostDate } from "@/api/date/apidate";
import {
dataSourceTypes,
interfaceTypes,
} from "@/api/date/type";
const props = withDefaults(defineProps<{
title:string;
name:string;
dbtype:string;
checked:string, //id
confirmFunc:(name:string,type:string, data:string[])=>void, // dataid
closeFunc:()=>void, //
}>(),{})
const checkList = ref<string[]>([]) //id
const checkoptions=ref<{id:string,name:string,code:string}[]>([]) //check option
const newType=ref("")
const newName=ref("")
const opTotal=ref(0)
const dblist=ref<string[]>([]) //
watch(newType,(val)=>{
if(val=="rdb"){
onLoadData(1)
}
})
const onSaveChange=()=>{
if(newType.value=="tsd") checkList.value=[]
props.confirmFunc(newName.value,newType.value, checkList.value)
}
const onLoadData=(page:number)=>{
freshPage(page)
}
const freshPage=(page:number)=>{
//
let sendData = {
url: import.meta.env.VITE_APP_SJZT_URL + "/database/app/datasource/page",
methodType: "GET",
where: "pageNum="+page+"&pageSize=10&dataType=1"
};
getOrPostDate("POST", sendData).then((data: any) => {
checkoptions.value=[]
opTotal.value=data.data.total;
if (data.data.records && data.data.records.length > 0) {
data.data.records.forEach((item: any) => {
dataSourceTypes.forEach((itemsd: any) => {
if (itemsd.value == item.datasourceType) {
item.datasourceTypeName = itemsd.label;
}
});
interfaceTypes.forEach((interItem: any) => {
if (interItem.value == item.interfaceType) {
item.interfaceTypeName = interItem.label;
}
});
checkoptions.value.push({
id:item.databaseName,
name:item.databaseName+item.dataSourceDes,
code:item.databaseName})
});
}
});
}
onMounted(() => {
newName.value=props.name
newType.value=props.dbtype
if(props.checked!=""){
checkList.value=props.checked.split(",")
dblist.value=props.checked.split(",")
}
})
</script>
<template>
<el-dialog :model-value="true" :style="{'max-height': '880px'}" @close="closeFunc">
<template #header>
<span>{{props.title}}</span>
</template>
<div class="tablelist">
<el-input style="width:40%;margin:10px;" placeholder="请输入项目名称" v-model="newName"></el-input>
<el-select v-model="newType" style="width:40%;margin:10px;" placeholder="请选择数据库类型" valueKey="value">
<el-option key="rdb" value="rdb">关系型</el-option>
<el-option key="tsd" value="tsd">时序型</el-option>
</el-select>
<div v-if="newType=='rdb'" style="margin: 10px;">
<div style="width: 100%;" >当前关联数据库
<span class="dbtag" v-for="value in dblist">{{ value }}</span>
</div>
<el-checkbox-group v-model="checkList" >
<el-checkbox v-for="op in checkoptions" :id="op.id" :label="op.name" :value="op.code" style="width: 40%;"/>
</el-checkbox-group>
<div v-if="opTotal>10" style="margin-top: 16px;">
<el-pagination size="small" @current-change="onLoadData" background layout="prev, pager, next" :total="opTotal" :page-size="10"/>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeFunc()">取消</el-button>
<el-button type="primary" @click="onSaveChange">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.menus_tree{
display: block;
padding: 2px;
margin-right: 24px;
box-shadow: 0px 0px 12px rgb(0 0 0 / 18%);
}
.tablelist{
margin-bottom:20px;
display:flex;
flex-direction:column;
width: 90%;
}
.checkTitle{
font-weight: bold;
}
.dbtag{
background-color: #c3c1bd;
padding: 1px 9px;
border-radius: 7px;
margin: 0 4px;
}
</style>

352
src/views/date/chatbit_ask_panel.vue

@ -0,0 +1,352 @@
<!--
@ 作者: han2015
@ 时间: 2025-05-12 15:39:13
@ 备注: chatbi ask_panel
-->
<script lang="ts" setup>
import {ElText,ElInput} from "element-plus";
import {Promotion,Remove } from '@element-plus/icons-vue'
import { getOrPostDate } from "@/api/date/apidate";
import {askChatBI} from "@/api/date/chatbi"
import * as echarts from 'echarts/core';
import { nextTick } from 'vue'
const props = withDefaults(defineProps<{
closefunc:()=>void,
agent:{model:boolean,name:string,uuid:string,dbm:string,dataset:string}
}>(),{})
interface charData{
dataList:Object[];
dimCols:string[];
measureCols:string[];
}
const myquestion=ref('')
const currentAgent=ref<{name:string,uuid:string,model:boolean,dbm:string}>({})
const interact_msg=ref<{ask:boolean,think:string,content:string,chart?:charData}[]>([])
const inputState=ref(true)
let relationDB={}
//
interface message{
ask:boolean,
think:string,
content:string
}
async function onSendTextToAI(){
interact_msg.value.push({ask:true,think:"",content:myquestion.value})
const params={"user_query":myquestion.value}
if(props.agent.dbm=="rdb"){
switch(relationDB.datasourceType){
case "4":
params.db_type="sqlserver"
break;
case "8":
params.db_type="postgresql"
break;
default:
params.db_type="mysql"
}
params.db_config={
host:relationDB.ipAddress,
port:relationDB.port,
database:relationDB.databaseName,
username:relationDB.account,
password:relationDB.password,
}
}
myquestion.value=""
askChatBI(params,props.agent.dbm).then((res:any)=>{
interact_msg.value.push({ask:false,think:"",content:"",chart:res.data})
}).catch((err:any)=>{
ElMessage.error("请求异常:"+err.message)
})
}
const drawChart= async (name:string,data:Object)=>{
const option= buildSmoothLineOption(data.chart)
await nextTick()
const dom=document.getElementById(name)
if(dom){
const myChart = echarts.init(dom);
myChart.setOption(option);
}
}
//
const buildSmoothLineOption=(chart:any)=>{
const dimKey = chart.dimCols[0]; // ""
const measureKey = chart.measureCols[0]; // ""
const xAxisData = chart.dataList.map(item => item[dimKey]);
const seriesData = chart.dataList.map(item => Number(item[measureKey]));
const option= {
tooltip: {},
grid:{
left: 10,
right: 10,
bottom: 0,
top: 30,
containLabel: true
},
xAxis: {
type: "category",
boundaryGap: true,
axisLabel: {
rotate: 45, // 45
color: '#333',
fontSize: 12,
},
data: xAxisData
},
yAxis: {
type: "value",
},
series: [{
name: measureKey,
type: "line",
smooth: true,
data: seriesData,
lineStyle: { width: 2 },
itemStyle: { color: "#5470c6" }
}],
dataZoom:null,
};
if (seriesData.length>100){
option.dataZoom=[
{
type: 'slider', //
xAxisIndex: 0, // 0 x
show: true,
realtime: true, //
start: 0, //
end: 30, //
height: 5, //
bottom: 5, //
handleSize: '40%',
// handleStyle: { color: 'rgba(27,90,169,1)' },
// backgroundColor: 'rgba(37,46,100,.8)',
// fillerColor: 'rgba(27,90,169,1)',
borderColor: 'transparent'
}
// // /
// { type: 'inside', xAxisIndex: 0 }
]
}
return option;
}
//chatbi
const getRefDatabase=(name:string)=>{
//
let sendData = {
url: import.meta.env.VITE_APP_SJZT_URL + "/database/app/datasource/page",
methodType: "GET",
where: `pageNum=1&databaseName=${name}&pageSize=10&dataType=1`
};
getOrPostDate("POST", sendData).then((res: any) => {
if(res.data.records.length>0){
relationDB=res.data.records[0]
}
});
}
//
onMounted(() => {
currentAgent.value=props.agent
getRefDatabase(props.agent.dataset)
// interact_msg.value.push({ask:true,think:"",content:","})
// interact_msg.value.push({ask:false,think:"",content:"", chart:{
// chart: {
// "dataList": [
// {
// "create_time": "2026-01-09T08:43:47",
// "name": "202521.pdf",
// "size": 278271,
// "uuid": "fdae94f6-7fb8-491e-b075-b98595269580"
// },
// {
// "create_time": "2026-01-09T08:43:46",
// "name": "202518.pdf",
// "size": 287256,
// "uuid": "ce40f7b1-d84b-4709-adf8-4dfc83cf2882"
// },
// {
// "create_time": "2026-01-09T08:43:46",
// "name": "202520.pdf",
// "size": 246410,
// "uuid": "6e58feb4-f4e8-4eaf-9242-1ce0735e106b"
// },
// {
// "create_time": "2026-01-09T08:43:46",
// "name": "202519.pdf",
// "size": 306184,
// "uuid": "c8a14d7b-336f-4ccf-8fda-39ff9aafd7e0"
// },
// {
// "create_time": "2026-01-09T08:43:45",
// "name": "202515.pdf",
// "size": 212694,
// "uuid": "8ef700cc-a121-4cdc-87d7-ec9f090083e2"
// },
// {
// "create_time": "2026-01-09T08:43:45",
// "name": "202517.pdf",
// "size": 227187,
// "uuid": "0eb8c280-9f13-40d3-bef0-ab88946d38e5"
// },
// {
// "create_time": "2026-01-09T08:43:45",
// "name": "202516.pdf",
// "size": 236041,
// "uuid": "f97bd81b-82ad-48f0-8666-3f02f8c851de"
// },
// {
// "create_time": "2026-01-09T08:43:44",
// "name": "202512.pdf",
// "size": 219716,
// "uuid": "9b748cc9-4def-49d0-baa8-567b6bf50673"
// },
// {
// "create_time": "2026-01-09T08:43:44",
// "name": "202514.pdf",
// "size": 203773,
// "uuid": "70b045db-766c-45ca-a54a-be8df2adc396"
// },
// {
// "create_time": "2026-01-09T08:43:44",
// "name": "202513.pdf",
// "size": 218646,
// "uuid": "da728907-b787-4d36-b210-820bd2632dac"
// }
// ],
// "dimCols": [
// "create_time",
// "name",
// "uuid"
// ],
// "measureCols": [
// "size"
// ]
// },
// message: "matter10SQLuuid",
// sql: "SELECT DISTINCT create_time, name, uuid, size FROM tank31_matter ORDER BY create_time DESC LIMIT 10"
// }})
});
</script>
<template>
<el-drawer
:model-value="currentAgent.model"
:title="currentAgent.name+' : 问数'"
direction="rtl"
size="60%"
@close="props.closefunc()"
:style="{padding:'17px',backgroundColor:'#f3f3f3'}">
<div style="position: relative;background: white;height:85%;overflow-y: auto;">
<div class="reply_area" >
<template v-for="msg,index of interact_msg">
<el-text v-if="msg.ask" class="t_ask" >{{ msg.content }}</el-text>
<div v-else class="t_resp">
<el-text >{{ msg.chart?.message }}</el-text>
<div v-if="props.agent.dbm=='tsd'">
<div :id="'chart_'+index" style="width: 100%;height:360px;">
{{ drawChart('chart_'+index,msg.chart!) }}
</div>
</div>
<div v-else>
<el-text v-if="msg.chart?.sql" class="sql">{{ msg.chart?.sql }}</el-text>
<div :id="'chart_'+index" style="width: 100%;height:360px;overflow: scroll;">
<el-table :data="msg.chart?.chart.dataList">
<el-table-column v-for="value in msg.chart?.chart.dimCols" :prop="value" :label="value"></el-table-column>
<el-table-column v-for="value in msg.chart?.chart.measureCols" :prop="value" :label="value"></el-table-column>
</el-table>
</div>
</div>
</div>
</template>
</div>
<div class="question_com" :class="{newquestion:interact_msg.length>0}">
<h1 v-show="interact_msg.length==0" style="font-size: 33px;font-weight: bold;margin: 10px;">恒信高科ChatBI服务</h1>
<el-input placeholder="问..." v-model="myquestion" input-style="border-radius: 20px;"
resize="none" :autosize="{minRows: 4}" type="textarea" />
<el-button :style="{display :inputState ? '':'none'}" type="primary" :icon="Promotion" circle @click="onSendTextToAI"/>
<el-button :style="{display :inputState ? 'none':''}" type="primary" :icon="Remove" circle @click=""/>
<span>内容由 AI 生成请仔细甄别</span>
</div>
</div>
</el-drawer>
</template>
<style lang="scss" scoped>
.app_container {
padding: 10px 30px 0px 30px;
height: 100%;
width: 100%;
}
.newquestion{
bottom: 20px;
}
.question_com{
position: fixed;
width: 52%;
margin: 0 50px;
text-align: center;
display: block;
button{
position: absolute;
bottom: 25px;
right: 5px;
}
}
.reply_area{
display: flex;
min-height: 20%;
flex-direction: column;
margin: 15px 15px 110px 0px;
}
.t_ask{
align-self: end;
line-height: 34px;
background-color: rgb(188 211 241);
padding: 0 30px;
border-radius:10px;
margin: 15px;
}
.t_resp{
align-self: start;
line-height: 30px;
margin: 20px 33px;
font-size: 16px;
color: black;
width: 92%;
.sql{
display: block;
padding: 8px;
margin: 10px 0;
background: #e5e5e5;
color: #0a4de9;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-all;
}
table {
width: 100%;
display: table;
border: 0px solid;
overflow: scroll;
}
}
</style>

119
src/views/date/device_panel.vue

@ -0,0 +1,119 @@
<!--
@ 时间: 2025-12-30
@ 备注: chatbi 设备和设备测点选择组件
-->
<script setup lang="ts">
import {getDevicesTree,getDevicesMonitors} from "@/api/date/chatbi"
const props = withDefaults(defineProps<{
checked:string[],
confirmFunc:(data:string[])=>void, //
closeFunc:()=>void, //
}>(),{})
const firstLevelKeys = ref<string[]>(["HXGK","JCFC"]) //
const treeData=ref([])
const checkList = ref<string[]>([])
const checkoptions=ref<{id:string,name:string,code:string}[]>([])
const nodeName=ref("")
const opTotal=ref(0)
const currentNode=ref<{id:string}>({id:""})
const onSaveChange=()=>{
props.confirmFunc(checkList.value)
}
const onSelectDevice=(node)=>{
nodeName.value=node.label
currentNode.value=node
freshPage(1)
}
const onLoadData=(page:number)=>{
freshPage(page)
}
const freshPage=(page:number)=>{
getDevicesMonitors({
deviceCode:currentNode.value?.id,
rows:30,
page:page
}).then(res=>{
checkoptions.value=[]
opTotal.value=0
if(res.status==200){
opTotal.value=res.data.total
res.data.rows?.forEach(row => {
checkoptions.value.push({id:row.id,name:row.name,code:row.code})
});
}else{
ElMessage.error('测点获取失败!')
}
})
}
onMounted(() => {
checkList.value=props.checked
getDevicesTree().then((res)=>{
treeData.value = res.data
})
})
</script>
<template>
<el-dialog :model-value="true" :style="{'max-height': '880px'}" @close="closeFunc">
<template #header>
<span>请选择设备测点</span>
</template>
<div style="display: grid;width: 100%;grid-template-columns:1fr 2fr;">
<div class="menus_tree">
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
:check-on-click-leaf="false"
:style="{maxHeight:'500px','overflow-y': 'auto'}"
:props="{label: 'label',children:'children',isLeaf:'children'}"
:default-expanded-keys="firstLevelKeys"
@node-click="onSelectDevice"
/>
</div>
<div class="tablelist">
<div class="checkTitle" v-if="checkoptions.length==0&&nodeName!=''">{{nodeName}}: 没有测点!</div>
<div class="checkTitle" v-else>当前节点{{nodeName}} </div>
<el-checkbox-group v-model="checkList" >
<el-checkbox v-for="op in checkoptions" :id="op.id" :label="op.name" :value="op.code" />
</el-checkbox-group>
<div v-if="opTotal>30" style="margin-top: 16px;">
<el-pagination size="small" @current-change="onLoadData" background layout="prev, pager, next" :total="opTotal" :page-size="30"/>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeFunc()">取消</el-button>
<el-button type="primary" @click="onSaveChange">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.menus_tree{
display: block;
padding: 2px;
margin-right: 24px;
box-shadow: 0px 0px 12px rgb(0 0 0 / 18%);
}
.checkTitle{
font-weight: bold;
}
</style>

2
src/views/doc/spacePermission.vue

@ -256,7 +256,7 @@ function addNode(key:string,node:Tree){
}
function checkNode(key:string,node:Tree):boolean{
for (const ch of node.child||[]) {
for (const ch of node.child??[]) {
if(ch.id==key){
return true
}

Loading…
Cancel
Save