页面树结构

版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

...


URL参数名

URL参数值

说明

输入参数

className

AIChatRemoteService


methodName

loginIfSmartbiLogged


params

[]

样例:[]

返回值

retCode

0

非0表示错误

result

{\"token\":\"CE3CE70AC12B70052A507D4B560E5374\"}

NLA 的token信息,这个非常重要,后续NLA所有方法需要用到这个token;

非0时是错误信息

备注:登陆NLA前需要登陆成功Smartbi(通过账户密码/单点/其他登陆方式)

4.3 查询数据模型清单、推荐问句


URL参数名

URL参数值

说明

输入参数

className

AIChatRemoteService


methodName

getBuildThemes


params

token

loginIfSmartbiLogged方法获取的token

返回值

retCode

0

参考query方法

result


参考query方法

...

<script>


window.aiChatbotConfig = {


// 配置AIChat的版本号


version: 1,


// 配置嵌入的域名


iframeSrc: "http://localhost:3000",


// btn: {


// 按钮图片可以自定义配置


//  backgroundImg: 'http://xxx.png'


// }


};


</script>


<script type="text/javascript" src="http://localhost:3000/smartbi/vision/external.js"></script>

嵌入后页面展示:

6. Demo代码编写

6.1 demo使用开发框架说明

本次demo,为了更好的说明第三方集成情况。采用完全模拟第三方平台的开发模式。demo应用使用的开发框架如下:

  • Java框架 -- Springboot 2
  • 前端框架 -- Vue2 、elementui、 jquery、Echarts
  • SmartbiV11 -- 非常重要,本次开发对接的API是基于V11版本提供的

6.2 代码结构说明

Image Removed

【代码结构】

@startuml
  前端HTML -> JS_API: 调用前端接口
  JS_API --> APIController: 调用服务端接口
  
  APIController --> SmartbiAPI: 调用Java远程接口
  SmartbiAPI --> Smartbi_Server: 调用Smartbi接口
  @enduml
  

【接口调用时序】

6.2.1 主要代码文件说明

...

文件

...

说明

...

后端Java代码

...

SmartbiAPI.java

...

Smartbi api 接口

...

APIController.java

...

演示程序,提供给前端的接口,实现内容与SmartbiAPI.java对应

...

前端JS

...

api.js

...

前端API接口,与APIController.java 对应

...

components/result-view.js

...

查询结果展示路由组件

...

components/query-tip.js

...

查询条件展示组件

...

components/select-entity.js

...

选择实体展示组件

...

components/result/chart-view.js

...

图形显示组件

...

components/result/table-view.js

...

表格显示组件

...

components/result/text-view.js

...

文字显示组件

...

components/result/select-view.js

...

模糊实体选择组件

...

前端HTML

...

index.html

...

页面文件

6.3 登录

Image Removed

【登录部分】

<el-form :inline="true" :model="form1" class="demo-form-inline">
    <el-form-item label="账号:">
        <el-input v-model="form1.user" placeholder="账号"></el-input>
    </el-form-item>
    <el-form-item label="密码:">
        <el-input v-model="form1.password" placeholder="密码" show-password></el-input>
    </el-form-item>
    <el-form-item label="Smartbi地址:">
        <el-input v-model="form1.server" placeholder="Smartbi地址" style="width:350px"></el-input>
    </el-form-item>
    <el-form-item>
        <el-button type="primary" @click="login" :disabled="loginOk">登录</el-button>
    </el-form-item>
</el-form>

login: function(server, userName, password, callback) {
    $.ajax({
        type: "POST",
        url: 'smartbi/login',
        data: {server: server, userName: userName, password: password},
        success: function (data) {
            callback(data);
        },
        error: function (data) {
            alert(data.responseJSON.message);
        }
    });
},

/**
 * 登录Smartbi 和 nla
 * @param server
 * @param userName
 * @param password
 * @return
 */
@RequestMapping("login")
public boolean login(String server, String userName, String password, HttpSession session) throws RemoteException {
    SmartbiAPI smartbi = new SmartbiAPI(server);
    try {
        boolean rs = smartbi.login(userName, password);
        if (rs) {
            rs = smartbi.loginNLA();
            if (rs) {
                saveSmartbi(session, smartbi);
                return true;
            }
        }
    } catch (RemoteException e) {
        logger.error(e.getMessage(), e);
        throw e;
    }
    throw new RemoteException("登录失败");
}

...

6.4 执行查询

6.4.1 执行查询

...
<result-view ref="resultView" v-if="status==4"></result-view>
...
query() {
    this.status = 3;
    API.query(this.input1, (data) => {
        if (data) {
            this.status = 4;
            this.$nextTick(() => {
                this.$refs.resultView.setResult(data);
            })
        }
    });
}

/**
 * 查询
 */
query: function(question, callback) {
    $.ajax({
        type: "POST",
        url: 'smartbi/query',
        data: {question: question, datasetId: ""},
        success: function (data) {
            callback(data);
        },
        error: function (data) {
            alert(data.responseJSON.message);
        }
    });
},

@RequestMapping("query")
public String query(String question, String datasetId, HttpSession session) throws RemoteException {
    SmartbiAPI smartbi = getSmartbi(session);
    if (smartbi == null) {
        throw new RemoteException("用户未登录!");
    } else {
        try {
            return smartbi.query(question, datasetId);
        } catch (RemoteException e) {
            logger.error(e.getMessage(), e);
            throw e;
        }
    }
}

/**
 * 执行自然语言查询
 *
 * @param question  问题
 * @param datasetId 模型Id
 * @return
 */
public String query(String question, String datasetId) throws RemoteException {
    List<Object> params = new ArrayList<>();
    params.add(question);
    params.add(this.nlaToken);
    //isMultiRound
    params.add(false);
    params.add(datasetId);
    //reportId
    params.add("");
    //uuid
    params.add("");
    //userChoseThemeId -- 交互式问答选择的Id,或者用户强制的模型Id
    params.add("");
    //isMobileCall
    params.add(false);
    //getPageBO -- 集成仪表盘使用,是否返回仪表盘定义
    params.add(false);
    return remoteInvokeEx("AIChatRemoteService", "query", params);
}

6.4.2 结果展示路由

执行 query 方法后,将返回结果设置给 result-view 组件

setResult(result) {
    this.result = JSON.parse(result);
    this.routerResult();
    this.show = true;
},
/**
 * 路由,显示结果展示组件
 */
routerResult() {
    if (this.result.code == 0) {
        // 正常展示结果
        if (this.isChartsResult()) {
            this.resultComponent = Vue.component('chart-view');
            this.resultContent = this.result.result;
        } else if (this.isTableResult()) {
            this.resultComponent = Vue.component('table-view');
            this.resultContent = this.result.result.html;
        } else {
            this.resultComponent = Vue.component('text-view');
            this.resultContent = "异常展示组件 -- 还没有实现!";
        }
    } else if (this.result.code == 50) {
        // 反问,选择实体
        this.resultComponent = Vue.component('select-view');
        this.resultContent = this.result;
    } else if (this.result.code == 10) {
        // 元数据
        this.resultComponent = Vue.component('text-view');
        this.resultContent = "此处是元数据查询,还没有实现!";
    } else if (this.result.code == 20) {
        // 闲聊
        this.resultComponent = Vue.component('text-view');
        this.resultContent = "此处是聊天内容,还没有实现!";
    } else if (this.result.code == 30) {
        // 打开报表
        this.resultComponent = Vue.component('text-view');
        this.resultContent = "此处是打开报表,还没有实现!";
    } else if (this.result.code == 100) {
        // 空结果
        this.resultComponent = Vue.component('text-view');
        this.resultContent = "查询无结果!";
    } else if (this.result.code == -10) {
        // 如果nl2sql有输出,但smartbi查询数据报错
        this.resultComponent = Vue.component('text-view');
        this.resultContent = "查询报错了!" + this.result.message;
    }
},

6.5 结果展示

6.5.1 表格展示

Vue.component('table-view', {
    name: 'TableView',
    template: '\
<div class="container">\
    <div class="scrollbar" v-html="resultContent" style="max-height: 330px;"></div>\
</div>\
    ',
    props: ["resultContent"]
});

6.5.2 图形展示

var tooltipValueFormatter = function(value) {
    let valueStr = String(value);
    let isFloat = valueStr.indexOf(".") + 1 > 0;
    if (isFloat && typeof(value) == "number") {
        return value.toFixed(2);
    } else if (isFloat && typeof(value) == "string") {
        return Number(value).toFixed(2);
    }
    return value;
}
Vue.component('chart-view', {
    name: 'ChartView',
    template: '\
<div class="container">\
    <div style="height: 270px; width: 100%; background-color: white;" ref="echartContainerId"></div>\
</div>\
    ',
    props: ["resultContent"],
    data() {
        return {
            myChart: null,
            chartType: null,
            option: null,
            onceShowMaxItem: 7
        }
    },
    watch: {
        //queryResult
        resultContent: {
            handler: function (newValue, oldValue) {
                let that = this;
                setTimeout(function () {
                    that.showResult(that.resultContent.portletType, that.resultContent.html);
                }, 100);
            },
            immediate: true
        }
    },
    computed: {
        isEmptyData() {
            //option不存在不是空数据
            if (!this.option) {
                return false;
            }
            let series = this.option.series;
            return !series || (series && series.length === 0) || (!series[0].data || (series[0].data && series[0].data.length === 0))
        }
    },
    methods: {
        showResult(chartType, echartsOption) {
            this.chartType = chartType;
            this.option = JSON.parse(echartsOption);
            if (this.isEmptyData) {
                this.$emit("showEmptyQueryResultView");
                return;
            }
            //该方法可以不需要,主要是对后端返回的option进行加工
            this.processOption();
            this.$nextTick(() => {
                this.myChart = echarts.init(this.$refs.echartContainerId);
                this.myChart.setOption(this.option);
            });
        },
        processOption() {
            let grid = {
                left: '2%',
                right: '2%',
                bottom: '2%',
                top: '7%',
                containLabel: true,
                show: 'true',
                borderWidth: '0'
            };
            if (this.chartType === "ECHARTS_BAR" || this.chartType === "ECHARTS_LINE" || this.chartType === "ECHARTS_COMBINATION__DUAL") {
                let onceShowMaxItem = this.onceShowMaxItem; //一次最多显示多少根柱子,后面运算时用到, 3-7有效,太多没试过,小于3时不太准确
                let dataNum = this.option.xAxis.data.length;
                let processedEnd = 100;
                if (dataNum > onceShowMaxItem) {
                    processedEnd = 100 * onceShowMaxItem / dataNum;
                }
                let dataZoom = [
                    {
                        // start, end是控制echarts图形的缩放比例,%,通过计算end, 控制最多显示多少根柱子(多少单位的X轴长度)
                        type: 'inside',
                        xAxisIndex: [0],
                        start: 0, end: processedEnd,
                        //end: 100  / this.option.xAxis.data.length * 6,
                        zoomOnMouseWheel: false,
                        zoomLock: true,
                    },
                ];
                //设置图形可以左右滚动
                this.option["dataZoom"] = dataZoom;
                //给第一个柱子或线设置显示数值,所有柱子都设置会很挤
                //this.option["series"][0]["label"] = label;
                //解决Y轴刻度被遮挡
                this.option["grid"] = grid;
            }
            if (this.chartType === "ECHARTS_BAR__HORIZONTAL") {
                //解决Y轴刻度被遮挡
                this.option["grid"] = grid;
            }
            if (this.chartType === "ECHARTS_PIE") {
                let label = {
                    normal: {
                        position: "inside"//此处将展示的文字在内部展示
                    }
                };
                //解决饼图标签显示不全
                this.option["series"][0]["label"] = label;
            }
            if (!!this.option.tooltip) {
                this.option.tooltip["confine"] = true;
                this.option.tooltip["valueFormatter"] = tooltipValueFormatter;
            } else {
                let tooltip = {
                    confine: true,
                    valueFormatter: tooltipValueFormatter
                };
                this.option["tooltip"] = tooltip;
            }
        }
    },
    destroyed() {
        if (!!this.myChart) {
            this.myChart.dispose();
        }
        this.myChart = null;
    }
});