Merge branch 'master' of http://nas.zhengjl.com:10880/wan/recom-gorse
This commit is contained in:
160
web/package-lock.json
generated
160
web/package-lock.json
generated
@@ -2139,6 +2139,16 @@
|
||||
"integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1616431241238&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
|
||||
@@ -2165,6 +2175,34 @@
|
||||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"cssnano": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
|
||||
@@ -2177,6 +2215,25 @@
|
||||
"postcss": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz",
|
||||
@@ -2193,6 +2250,16 @@
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz",
|
||||
@@ -2209,6 +2276,18 @@
|
||||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.8.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
|
||||
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -14521,87 +14600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.8.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
|
||||
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
|
||||
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-quill-editor": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npm.taobao.org/vue-quill-editor/download/vue-quill-editor-3.0.6.tgz",
|
||||
|
||||
Binary file not shown.
@@ -1,314 +0,0 @@
|
||||
<template>
|
||||
<div class="graph-main">
|
||||
<div class="legend-box" v-show="!legendHide">
|
||||
<div class="item-row" v-for="(value, key) in legendList" :key="key">
|
||||
<div class="point" :style="'background-color:' + value.color"></div>
|
||||
<div class="label">{{ value.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :id="id" style="width: 100%; height: 100%"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
var graphObj = {};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ontologyId: '',
|
||||
id: Math.round(Math.random() * 10000000).toString(),
|
||||
graph: null,
|
||||
graphData: null,
|
||||
legendHide: false,
|
||||
legendList: {
|
||||
"armyType": {label: '军种', color: 'rgb(76,142,218)'},
|
||||
"control": {label: '控制系统', color: 'rgb(236,181,201)'},
|
||||
"engine": {label: '发动机', color: 'rgb(241,102,103)'},
|
||||
"fault": {label: '故障信息', color: 'rgb(141,204,147)'},
|
||||
"faultCase": {label: '故障案例', color: 'rgb(156,33,234)'},
|
||||
"oriPlane": {label: '原飞机', color: 'rgb(255,196,84)'},
|
||||
"part": {label: '部件', color: 'rgb(86,148,128)'},
|
||||
"place": {label: '发生地', color: 'rgb(217,200,174)'},
|
||||
"plane": {label: '飞机', color: 'rgb(87,199,227)'}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.ontologyId = this.$route.query.id;
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
initGraph: function (cmd, flag, clickNodeFun, legendHide) { // 是否隐藏图例
|
||||
if(legendHide) {
|
||||
this.legendHide = true;
|
||||
}
|
||||
let graph = graphObj[this.id];
|
||||
this.$nextTick(() => {
|
||||
this.destroyGraph();
|
||||
let _width = document.getElementById(this.id).clientWidth;
|
||||
let _height = document.getElementById(this.id).clientHeight;
|
||||
// 创建 G6 图实例
|
||||
if (graph == undefined || graph == null) {
|
||||
graph = new G6.Graph({
|
||||
container: this.id, // 指定图画布的容器 id,与第 9 行的容器对应
|
||||
// 画布宽高
|
||||
width: _width,
|
||||
height: _height,
|
||||
layout: {
|
||||
type: 'force',
|
||||
nodeStrength: -0,
|
||||
nodeSize: 80,
|
||||
preventOverlap: true,
|
||||
edgeStrength: 0.1
|
||||
},
|
||||
modes: {
|
||||
// Defualt mode
|
||||
default: ['drag-node',
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
enableOptimize: true, // enable the optimize to hide the shapes beside nodes' keyShape
|
||||
},
|
||||
{
|
||||
type: 'zoom-canvas',
|
||||
enableOptimize: true, // enable the optimize to hide the shapes beside nodes' keyShape
|
||||
},
|
||||
{
|
||||
type: 'tooltip',
|
||||
formatText: function formatText(model) {
|
||||
let name = '';
|
||||
let argumentsStr = '';
|
||||
if (model.label) name = model.label + '<br/>';
|
||||
if (model.arguments && model.arguments.length > 0) {
|
||||
model.arguments.forEach(arg => {
|
||||
argumentsStr += arg.role + ":" + arg.argument + '<br/>';
|
||||
});
|
||||
}
|
||||
const text = `${name} ${argumentsStr}`;
|
||||
return text;
|
||||
},
|
||||
|
||||
shouldUpdate: function shouldUpdate() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNode: {
|
||||
size: 20,
|
||||
color: '#4b4b4b',
|
||||
label: 'name',
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
fill: '#68BDF6'
|
||||
},
|
||||
labelCfg: {
|
||||
position: 'bottom',
|
||||
style: {
|
||||
fontSize: 14,
|
||||
fill: '#454545'
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#b3b3b3',
|
||||
lineAppendWidth: 10, // Enlarge the range the edge to be hitted
|
||||
},
|
||||
labelCfg: {
|
||||
autoRotate: false,
|
||||
style: {
|
||||
fontSize: 14,
|
||||
fill: '#848484'
|
||||
}
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
yourStateName: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 3,
|
||||
},
|
||||
},
|
||||
edgeStateStyles: {
|
||||
yourStateName: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/*拖动节点-start*/
|
||||
const forceLayout = graph.get('layoutController').layoutMethod;
|
||||
graph.on('node:dragstart', function (e) {
|
||||
graph.layout();
|
||||
refreshDragedNodePosition(e);
|
||||
});
|
||||
graph.on('node:drag', function (e) {
|
||||
if (forceLayout) {
|
||||
forceLayout.execute();
|
||||
}
|
||||
refreshDragedNodePosition(e);
|
||||
});
|
||||
graph.on('node:dragend', function (e) {
|
||||
e.item.get('model').fx = null;
|
||||
e.item.get('model').fy = null;
|
||||
});
|
||||
/*拖动节点-end*/
|
||||
|
||||
graph.on('node:click', (e) => {
|
||||
let node = e.item._cfg.model;
|
||||
if (clickNodeFun) {
|
||||
clickNodeFun(node);
|
||||
}
|
||||
this.clearStates();
|
||||
graph.setItemState(e.item, 'yourStateName', true);
|
||||
});
|
||||
|
||||
graph.on('canvas:click', (e) => {
|
||||
this.clearStates();
|
||||
});
|
||||
|
||||
graphObj[this.id] = graph;
|
||||
|
||||
}
|
||||
if (flag) {
|
||||
this.importData(cmd);
|
||||
} else {
|
||||
this.queryData(cmd);
|
||||
}
|
||||
});
|
||||
},
|
||||
importData({nodes, edges}) { // 查询图空间内节点关系
|
||||
let graph = graphObj[this.id];
|
||||
// 更新颜色数据
|
||||
nodes.forEach(node => {
|
||||
node.style = {
|
||||
lineWidth: 2,
|
||||
fill: this.legendList[node._label].color || "#68BDF6"
|
||||
};
|
||||
});
|
||||
|
||||
this.graphData = {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
};
|
||||
// 读取数据
|
||||
graph.data(this.graphData);
|
||||
// 渲染图
|
||||
graph.render();
|
||||
},
|
||||
queryData({path, qo}) { // 查询图空间内节点关系
|
||||
let graph = graphObj[this.id];
|
||||
request({
|
||||
url: path,
|
||||
method: 'post',
|
||||
data: qo
|
||||
}).then(res => {
|
||||
res.data.nodes.forEach(node => {
|
||||
node.label = node.properties.name;
|
||||
node.id = node.id.toString();
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
delete edge.id;
|
||||
edge.source = edge.source.toString();
|
||||
edge.target = edge.target.toString();
|
||||
if (edge.source === edge.target) {
|
||||
edge.type = 'loop';
|
||||
} else {
|
||||
delete edge.type;
|
||||
}
|
||||
});
|
||||
this.importData({
|
||||
nodes: res.data.nodes,
|
||||
edges: res.data.relations
|
||||
});
|
||||
// this.graphData = {
|
||||
// nodes: res.data.nodes,
|
||||
// edges: res.data.relations
|
||||
// };
|
||||
// // 读取数据
|
||||
// graph.data(this.graphData);
|
||||
// // 渲染图
|
||||
// graph.render();
|
||||
});
|
||||
},
|
||||
clearStates() { // 清空选中状态
|
||||
let graph = graphObj[this.id];
|
||||
graph.getNodes().forEach((node) => {
|
||||
graph.clearItemStates(node);
|
||||
});
|
||||
graph.getEdges().forEach((edge) => {
|
||||
graph.clearItemStates(edge);
|
||||
});
|
||||
},
|
||||
destroyGraph() {
|
||||
let graph = graphObj[this.id];
|
||||
try {
|
||||
if (graph != undefined && graph != null) {
|
||||
graph.clear();
|
||||
graph = null;
|
||||
document.getElementById("mountNode").innerHTML = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDragedNodePosition(e) {
|
||||
const model = e.item.get('model');
|
||||
model.fx = e.x;
|
||||
model.fy = e.y;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.g6-tooltip {
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #545454;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 8px;
|
||||
box-shadow: rgb(174 174 174) 0px 0px 10px;
|
||||
}
|
||||
|
||||
.graph-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.legend-box {
|
||||
position: absolute;
|
||||
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
float: left;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.item-row .point {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 40px;
|
||||
border: 2px solid #4b4b4b;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.item-row .label {
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -250,7 +250,6 @@ import {config} from '@/components/graph/config';
|
||||
import G6 from '@antv/g6';
|
||||
import {listToTree} from "@/utils/common";
|
||||
import noumenonInfo from '@/components/dialog/NoumenonInfo';
|
||||
import tagModify from "@/components/dialog/TagModify";
|
||||
|
||||
var vm;
|
||||
var graph;
|
||||
|
||||
@@ -1,782 +0,0 @@
|
||||
<template>
|
||||
<div class="node-box" style="position: relative">
|
||||
<transition name="el-zoom-in-center">
|
||||
<div v-show="infoVisible" class="graph-info-box">
|
||||
<div class="graph-info-child">
|
||||
<div v-show="selectObj.type == 'edge'">
|
||||
<noumenon-edge-modify ref="noumenonEdgeModify"></noumenon-edge-modify>
|
||||
</div>
|
||||
<div v-show="selectObj.type == 'node'">
|
||||
<noumenon-modify ref="noumenonModify"></noumenon-modify>
|
||||
</div>
|
||||
<i class="el-icon-d-arrow-left" @click="infoVisible = false"></i>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div v-show="treeInfoVisible" class="tree-graph-info">
|
||||
<div class="graph-info-child">
|
||||
<tree-graph-info ref="treeGraphInfo"></tree-graph-info>
|
||||
<i class="el-icon-d-arrow-right" @click="treeInfoVisible = false"></i>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 为图谱准备一个具备大小(宽高)的Dom -->
|
||||
<div class="tree-container">
|
||||
<div class="graph-name-box">
|
||||
<label>本体名称: {{ graphName }};</label>
|
||||
<label>节点数量:{{ nodeCount }};</label>
|
||||
<label>关系属性数量:{{ edgeCount }};</label>
|
||||
<label>值属性数量:{{ valueCount }};</label>
|
||||
</div>
|
||||
<!-- <div class="tips-box">-->
|
||||
<!-- <label></label>-->
|
||||
<!-- </div>-->
|
||||
<div class="icon-box">
|
||||
<div class="icon icon-tree" @click="showTreeCom">
|
||||
<i title="树形图谱"></i>
|
||||
</div>
|
||||
<div class="icon icon-add" @click="showAddNodeDialog">
|
||||
<i title="新增概念"></i>
|
||||
</div>
|
||||
<div title="按住shift点选两节点可新增关系" :class="['icon icon-arrow', createEdgeModelFlag ? 'active' : '']" @click="changeCreateEdgeModel">
|
||||
<i title="按住shift点选两节点可新增关系"></i>
|
||||
</div>
|
||||
<div class="icon icon-remove" @click="removeNode">
|
||||
<i title="删除"></i>
|
||||
</div>
|
||||
<div class="icon icon-save" @click="backGraph">
|
||||
<i title="返回"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mountNode" style="width: 100%; height: 1000px"></div>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="新增概念"
|
||||
:visible.sync="addNodeDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="addNodeForm" label-width="80px" :rules="addNodeRules" :model="addNodeCmd">
|
||||
<el-form-item label="概念名" prop="tagName">
|
||||
<el-input v-model="addNodeCmd.tagName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="概念描述">
|
||||
<el-input v-model="addNodeCmd.label"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button @click="addNodeDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAddNode">确 定</el-button>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="新增关系"
|
||||
:visible.sync="addEdgeDialogVisible"
|
||||
@close="handleAddEdgeDialogClose"
|
||||
width="40%">
|
||||
<el-form ref="addEdgeForm" :model="addEdgeCmd" label-width="100px" :rules="addEdgeRules">
|
||||
<el-form-item label="关系名称" prop="edgeName">
|
||||
<el-input v-model="addEdgeCmd.edgeName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关系描述">
|
||||
<el-input v-model="addEdgeCmd.label"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示箭头">
|
||||
<el-radio v-model="addEdgeCmd.arrow" :label="true">是</el-radio>
|
||||
<el-radio v-model="addEdgeCmd.arrow" :label="false">否</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button @click="addEdgeDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAddEdge">确 定</el-button>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import {config} from '@/components/graph/config';
|
||||
import G6 from '@antv/g6';
|
||||
import noumenonModify from '@/components/dialog/NoumenonModify';
|
||||
import noumenonEdgeModify from '@/components/dialog/NoumenonEdgeModify';
|
||||
import treeGraphInfo from '@/components/dialog/TreeGraphInfo';
|
||||
|
||||
|
||||
G6.registerBehavior('click-add-edge', {
|
||||
// Set the events and the corresponding responsing function for this behavior
|
||||
getEvents() {
|
||||
return {
|
||||
'node:click': 'onClick', // The event is canvas:click, the responsing function is onClick
|
||||
mousemove: 'onMousemove', // The event is mousemove, the responsing function is onMousemove
|
||||
'edge:click': 'onEdgeClick',
|
||||
};
|
||||
},
|
||||
// The responsing function for node:click defined in getEvents
|
||||
onClick(ev) {
|
||||
const self = this;
|
||||
const node = ev.item;
|
||||
const graph = self.graph;
|
||||
// The position where the mouse clicks
|
||||
const point = {x: ev.x, y: ev.y};
|
||||
const model = node.getModel();
|
||||
if (self.addingEdge && self.edge) {
|
||||
graph.updateItem(self.edge, {
|
||||
target: model.id,
|
||||
});
|
||||
// 打开编辑新建关系的信息弹窗
|
||||
vm.showAddEdge(self.edge._cfg.model);
|
||||
self.edge = null;
|
||||
self.addingEdge = false;
|
||||
} else {
|
||||
// Add anew edge, the end node is the current node user clicks
|
||||
self.edge = graph.addItem('edge', {
|
||||
source: model.id,
|
||||
target: model.id,
|
||||
});
|
||||
self.addingEdge = true;
|
||||
}
|
||||
},
|
||||
// The responsing function for mousemove defined in getEvents
|
||||
onMousemove(ev) {
|
||||
const self = this;
|
||||
// The current position the mouse clicks
|
||||
const point = {x: ev.x, y: ev.y};
|
||||
if (self.addingEdge && self.edge) {
|
||||
// Update the end node to the current node the mouse clicks
|
||||
self.graph.updateItem(self.edge, {
|
||||
target: point,
|
||||
});
|
||||
}
|
||||
},
|
||||
// The responsing function for edge:click defined in getEvents
|
||||
onEdgeClick(ev) {
|
||||
const self = this;
|
||||
const currentEdge = ev.item;
|
||||
if (self.addingEdge && self.edge === currentEdge) {
|
||||
self.graph.removeItem(self.edge);
|
||||
self.edge = null;
|
||||
self.addingEdge = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var vm;
|
||||
var graph;
|
||||
var graphData;
|
||||
var _ = require('lodash');
|
||||
var nodeProps = {
|
||||
id: 'vid',
|
||||
label: 'properties.label',
|
||||
name: 'properties.tagName',
|
||||
labelName: 'properties.label'
|
||||
};
|
||||
var edgeProps = {
|
||||
source: 'srcId',
|
||||
target: 'dstId',
|
||||
name: 'properties.edgeName',
|
||||
label: 'properties.label',
|
||||
arrow: 'properties.arrow',
|
||||
labelName: 'properties.label'
|
||||
}
|
||||
// const subjectColors = ['#5F95FF', '#61DDAA', '#65789B', '#F6BD16', '#7262FD', '#78D3F8', '#9661BC', '#F6903D', '#008685', '#F08BB4'];
|
||||
// const colorSets = G6.Util.getColorSetsBySubjectColors(subjectColors, '#fff', 'default', '#777');
|
||||
export default {
|
||||
components: {
|
||||
'noumenon-modify': noumenonModify,
|
||||
'noumenon-edge-modify': noumenonEdgeModify,
|
||||
'tree-graph-info': treeGraphInfo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
qo: '',
|
||||
graphName: '',
|
||||
nodeCount: 0,
|
||||
edgeCount: 0,
|
||||
valueCount: 0,
|
||||
ontologyId: '',
|
||||
infoVisible: false, // 左侧信息栏显隐
|
||||
treeInfoVisible: false, // 右侧树形图谱
|
||||
clickNode: {id: '', name: ''},
|
||||
addNodeDialogVisible: false,
|
||||
addNodeCmd: {
|
||||
ontologyId: '',
|
||||
tagName: '',
|
||||
label: ''
|
||||
},
|
||||
arrowVisible: false, // 是否展示箭头方向
|
||||
createEdgeModelFlag: false, // 创建关系模式开关
|
||||
addEdgeDialogVisible: false,
|
||||
addEdgeCmd: {
|
||||
ontologyId: '',
|
||||
edgeName: '',
|
||||
label: '',
|
||||
source: '',
|
||||
target: '',
|
||||
arrow: false
|
||||
},
|
||||
selectObj: {
|
||||
type: '',
|
||||
value: {}
|
||||
},
|
||||
addNodeRules: {
|
||||
tagName: [
|
||||
{required: true, message: '请输入类名称', trigger: 'blur'},
|
||||
],
|
||||
},
|
||||
addEdgeRules: {
|
||||
edgeName: [
|
||||
{required: true, message: '请输入类名称', trigger: 'blur'},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
vm = this;
|
||||
vm.ontologyId = this.$route.query.id;
|
||||
vm.qo = {
|
||||
pageNo: this.$route.query.pageNo,
|
||||
name: this.$route.query.name
|
||||
};
|
||||
vm.graphName = this.$route.query.graphName;
|
||||
},
|
||||
mounted() {
|
||||
vm.initGraph();
|
||||
},
|
||||
methods: {
|
||||
queryCount() {
|
||||
request({
|
||||
url: `/nebula_model/countallbyontologyid/${vm.ontologyId}`,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
vm.nodeCount = res.data.gns;
|
||||
vm.edgeCount = res.data.gxs;
|
||||
vm.valueCount = res.data.zsxs;
|
||||
});
|
||||
},
|
||||
queryTagData() { // 查询图空间内节点关系
|
||||
vm.queryCount(); // 更新统计信息
|
||||
request({
|
||||
url: `/nebula_model/findrelationbyontologyid/${vm.ontologyId}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
// ontologyId: vm.ontologyId
|
||||
}
|
||||
}).then(res => {
|
||||
res.data.nodes.forEach(node => {
|
||||
Reflect.ownKeys(nodeProps).forEach(key => {
|
||||
node[key] = _.get(node, nodeProps[key], '');
|
||||
});
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
Reflect.ownKeys(edgeProps).forEach(key => {
|
||||
edge[key] = _.get(edge, edgeProps[key], '');
|
||||
});
|
||||
// 是否指向自己
|
||||
edge.type = edge.srcId === edge.dstId ? 'loop' : '';
|
||||
// 是否显示箭头
|
||||
edge.style = {endArrow: edge.arrow};
|
||||
});
|
||||
graphData = {
|
||||
nodes: res.data.nodes,
|
||||
edges: res.data.relations
|
||||
};
|
||||
// 读取数据
|
||||
graph.data(graphData);
|
||||
// 渲染图
|
||||
graph.render();
|
||||
});
|
||||
},
|
||||
initGraph: function () {
|
||||
this.destroyGraph();
|
||||
let mountNodeDom = document.getElementById("mountNode");
|
||||
let _width = mountNodeDom.clientWidth, _height = mountNodeDom.clientHeight;
|
||||
// 创建 G6 图实例
|
||||
if (graph == undefined || graph == null) {
|
||||
graph = new G6.Graph(config({width: _width, height: _height, id: 'mountNode'}));
|
||||
|
||||
graph.on('aftercreateedge', (e) => {
|
||||
vm.showAddEdge(e.edge._cfg.model);
|
||||
});
|
||||
|
||||
graph.on('canvas:click', (e) => {
|
||||
vm.selectObj = {
|
||||
type: '',
|
||||
value: {}
|
||||
};
|
||||
vm.clearStates();
|
||||
vm.infoVisible = false;
|
||||
});
|
||||
|
||||
/*拖动节点-start*/
|
||||
// const forceLayout = graph.get('layoutController').layoutMethod;
|
||||
// graph.on('node:dragstart', function (e) {
|
||||
// graph.layout();
|
||||
// refreshDragedNodePosition(e);
|
||||
// });
|
||||
// graph.on('node:drag', function (e) {
|
||||
// if (forceLayout) {
|
||||
// forceLayout.execute();
|
||||
// }
|
||||
// refreshDragedNodePosition(e);
|
||||
// });
|
||||
// graph.on('node:dragend', function (e) {
|
||||
// e.item.get('model').fx = null;
|
||||
// e.item.get('model').fy = null;
|
||||
// });
|
||||
/*拖动节点-end*/
|
||||
|
||||
graph.on('edge:click', function (e) { // 点击边事件
|
||||
let edge = e.item._cfg.model;
|
||||
if (!edge.name) {
|
||||
return;
|
||||
}
|
||||
let fields = [];
|
||||
if (edge.properties && edge.properties.nebulafieldsnapshot) {
|
||||
fields = edge.properties.nebulafieldsnapshot;
|
||||
}
|
||||
vm.showEdgeModifyCom({
|
||||
edgeName: edge.name,
|
||||
label: edge.labelName,
|
||||
arrow: edge.arrow,
|
||||
source: edge.srcId,
|
||||
target: edge.dstId
|
||||
}, fields);
|
||||
graph.getEdges().forEach((edge) => {
|
||||
graph.clearItemStates(edge);
|
||||
});
|
||||
graph.getNodes().forEach((node) => {
|
||||
graph.clearItemStates(node);
|
||||
});
|
||||
graph.setItemState(e.item, 'yourStateName', true);
|
||||
|
||||
});
|
||||
|
||||
graph.on('node:click', (e) => {
|
||||
if (!event.shiftKey && !vm.createEdgeModelFlag) { // 按住shift属于新增关系,不需要触发点击节点事件
|
||||
let node = e.item._cfg.model;
|
||||
vm.showNodeModifyCom({
|
||||
id: node.id,
|
||||
tagName: node.name,
|
||||
label: node.labelName
|
||||
}, _.get(node, 'properties.nebulafieldsnapshot', []));
|
||||
vm.clearStates();
|
||||
graph.setItemState(e.item, 'yourStateName', true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
vm.queryTagData();
|
||||
},
|
||||
destroyGraph() {
|
||||
try {
|
||||
if (graph != undefined && graph != null) {
|
||||
graph.clear();
|
||||
graph = null;
|
||||
document.getElementById("mountNode").innerHTML = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
clearStates() { // 清空选中状态
|
||||
graph.getNodes().forEach((node) => {
|
||||
graph.clearItemStates(node);
|
||||
});
|
||||
graph.getEdges().forEach((edge) => {
|
||||
graph.clearItemStates(edge);
|
||||
});
|
||||
},
|
||||
changeCreateEdgeModel() { // 切换模式,默认和添加关系模式之间切换
|
||||
vm.createEdgeModelFlag = !vm.createEdgeModelFlag;
|
||||
if (vm.createEdgeModelFlag) {
|
||||
graph.setMode("addEdge");
|
||||
} else {
|
||||
graph.setMode("default");
|
||||
}
|
||||
},
|
||||
showAddNodeDialog() {
|
||||
vm.addNodeCmd = {
|
||||
ontologyId: vm.ontologyId,
|
||||
tagName: '',
|
||||
label: ''
|
||||
};
|
||||
vm.addNodeDialogVisible = true;
|
||||
},
|
||||
submitAddNode() {
|
||||
request({
|
||||
url: `/nebula_model/insertmodelvertex`,
|
||||
method: 'post',
|
||||
data: {
|
||||
ontologyId: vm.ontologyId,
|
||||
tagName: vm.addNodeCmd.tagName,
|
||||
label: vm.addNodeCmd.label
|
||||
}
|
||||
}).then(res => {
|
||||
graph.addItem('node', {
|
||||
id: res.data.vid,
|
||||
name: vm.addNodeCmd.tagName,
|
||||
label: vm.addNodeCmd.tagName,
|
||||
labelName: vm.addNodeCmd.label
|
||||
});
|
||||
graphData.nodes = graph.save().nodes;
|
||||
graph.layout();
|
||||
vm.$message.success("新增类成功");
|
||||
vm.addNodeDialogVisible = false;
|
||||
|
||||
vm.showNodeModifyCom(res.data.ob, []);
|
||||
});
|
||||
},
|
||||
showAddEdge(edge) {
|
||||
vm.addEdgeCmd = {
|
||||
ontologyId: vm.ontologyId,
|
||||
edgeName: '',
|
||||
label: '',
|
||||
arrow: false,
|
||||
source: edge.source,
|
||||
target: edge.target
|
||||
};
|
||||
vm.addEdgeDialogVisible = true;
|
||||
},
|
||||
handleAddEdgeDialogClose() {
|
||||
vm.queryTagData();
|
||||
},
|
||||
submitAddEdge() {
|
||||
vm.$refs['addEdgeForm'].validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
let edge = {
|
||||
srcId: vm.addEdgeCmd.source,
|
||||
ontologyId: vm.ontologyId,
|
||||
dstId: vm.addEdgeCmd.target,
|
||||
edgeName: vm.addEdgeCmd.edgeName,
|
||||
arrow: vm.addEdgeCmd.arrow,
|
||||
label: vm.addEdgeCmd.label
|
||||
};
|
||||
request({
|
||||
url: `/nebula_model/insertmodeledge`,
|
||||
method: 'post',
|
||||
data: edge
|
||||
}).then(res => {
|
||||
vm.queryTagData();
|
||||
vm.$message.success("新增关系成功");
|
||||
vm.addEdgeDialogVisible = false;
|
||||
|
||||
vm.showEdgeModifyCom(edge, []);
|
||||
});
|
||||
});
|
||||
},
|
||||
showTreeCom() { // 显示树形图谱窗口
|
||||
vm.treeInfoVisible = true;
|
||||
vm.$nextTick(() => {
|
||||
vm.$refs['treeGraphInfo'].queryTreeData(vm.ontologyId);
|
||||
});
|
||||
},
|
||||
showNodeModifyCom(node, fields) { // 显示节点编辑组件
|
||||
vm.infoVisible = true;
|
||||
vm.$nextTick(() => {
|
||||
vm.selectObj = {
|
||||
type: 'node',
|
||||
value: node
|
||||
};
|
||||
vm.$refs['noumenonModify'].updateValue(node.tagName, node.label, fields);
|
||||
});
|
||||
},
|
||||
showEdgeModifyCom(edge, fields) { // 显示关系编辑组件
|
||||
vm.infoVisible = true;
|
||||
vm.selectObj = {
|
||||
type: 'edge',
|
||||
value: edge
|
||||
};
|
||||
vm.$nextTick(() => {
|
||||
vm.$refs['noumenonEdgeModify'].updateValue(edge.edgeName, edge.label, edge.arrow, fields, vm.selectObj.value.source, vm.selectObj.value.target);
|
||||
});
|
||||
},
|
||||
backGraph() { // 返回
|
||||
if(vm.$refs['treeGraphInfo']) {
|
||||
vm.$refs['treeGraphInfo'].destroyTree();
|
||||
}
|
||||
vm.$router.push({path: "/graphModel", query: {pageNo: vm.qo.pageNo, name: vm.qo.name}})
|
||||
vm.$message.success("保存成功");
|
||||
},
|
||||
changeArrowVisible() { // 显隐箭头
|
||||
vm.arrowVisible = !vm.arrowVisible;
|
||||
vm.queryTagData();
|
||||
},
|
||||
removeNode() { // 删除节点
|
||||
if (vm.selectObj.type.length === 0) {
|
||||
vm.$message.warning("请先选中节点或关系");
|
||||
return;
|
||||
}
|
||||
this.$confirm('此操作将永久删除选中节点和关系, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
if (vm.selectObj.type === "node") {
|
||||
request({
|
||||
url: `/nebula_model/deletemodelvertex/${vm.selectObj.value.id}`,
|
||||
method: 'get',
|
||||
data: {
|
||||
// vid: vm.selectObj.value.id
|
||||
}
|
||||
}).then(res => {
|
||||
vm.$message.success("删除成功");
|
||||
vm.queryTagData();
|
||||
vm.infoVisible = false;
|
||||
});
|
||||
} else if (vm.selectObj.type === "edge") {
|
||||
request({
|
||||
url: `/nebula_model/deletemodeledge`,
|
||||
method: 'post',
|
||||
data: {
|
||||
srcId: vm.selectObj.value.source,
|
||||
dstId: vm.selectObj.value.target,
|
||||
}
|
||||
}).then(res => {
|
||||
vm.$message.success("删除成功");
|
||||
vm.queryTagData();
|
||||
vm.infoVisible = false;
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
submitUpdateNode(modifyCmd) { // 更新
|
||||
request({
|
||||
url: `/nebula_model/insertmodelvertex`,
|
||||
method: 'post',
|
||||
data: {
|
||||
vid: vm.selectObj.value.id,
|
||||
tagName: modifyCmd.tagName,
|
||||
label: modifyCmd.label,
|
||||
fields: modifyCmd.fields
|
||||
}
|
||||
}).then(res => {
|
||||
vm.queryTagData();
|
||||
vm.$message.success("保存成功");
|
||||
});
|
||||
},
|
||||
submitUpdateEdge(modifyCmd) {
|
||||
request({
|
||||
url: `/nebula_model/insertmodeledge`,
|
||||
method: 'post',
|
||||
data: {
|
||||
edgeName: modifyCmd.edgeName,
|
||||
label: modifyCmd.label,
|
||||
fields: modifyCmd.fields,
|
||||
arrow: modifyCmd.arrow,
|
||||
srcId: vm.selectObj.value.source,
|
||||
dstId: vm.selectObj.value.target,
|
||||
}
|
||||
}).then(res => {
|
||||
vm.queryTagData();
|
||||
vm.$message.success("保存成功");
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDragedNodePosition(e) {
|
||||
const model = e.item.get('model');
|
||||
model.fx = e.x;
|
||||
model.fy = e.y;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.graph-name-box {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 65px;
|
||||
width: 700px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
margin: 15px 0;
|
||||
|
||||
}
|
||||
|
||||
.add-attribute-box {
|
||||
margin-top: 10px;
|
||||
color: #409EFF;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.graph-info-box, .tree-graph-info {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
top: 0px;
|
||||
overflow: hidden;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
z-index: 999;
|
||||
border-right: 1px solid rgba(52, 100, 224, 0.15);
|
||||
box-shadow: 0px 2px 21px 0px rgba(52, 100, 224, 0.15);
|
||||
}
|
||||
|
||||
.graph-info-box {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.tree-graph-info {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.graph-info-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-left, .el-icon-d-arrow-right {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-left {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-right {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.info-line-item {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-content > div {
|
||||
margin-bottom: 5px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
/*这个样式不写,右键弹框会一直显示在画布的左下角*/
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #f2f4f7;
|
||||
left: -99999px;
|
||||
top: -999999px;
|
||||
}
|
||||
|
||||
.menu div {
|
||||
list-style: none;
|
||||
padding: 5px 10px;
|
||||
color: black;
|
||||
border-bottom: 1px dashed #ffffff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu div:hover {
|
||||
color: #659bc5;
|
||||
background: #f3f6fa;
|
||||
}
|
||||
|
||||
.menu div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
ul#menuBox li {
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
ul#menuBox li:hover {
|
||||
color: #1c63e0;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tips-box {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
margin-left: -150px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
div.icon-box {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 75px;
|
||||
}
|
||||
|
||||
div.icon {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
border-radius: 40px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
div.icon i {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
div.icon-add i {
|
||||
background: url("../../assets/image/icon/bg-add.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
|
||||
div.icon-remove i {
|
||||
background: url("../../assets/image/icon/changyonggoupiaorenshanchu.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
|
||||
div.icon-save i {
|
||||
background: url("../../assets/image/icon/fanhui.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
div.icon-arrow i {
|
||||
background: url("../../assets/image/icon/arrowTop-fill.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
|
||||
div.icon-tree i {
|
||||
background: url("../../assets/image/icon/shuxingicon.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
div.icon-arrow.active i {
|
||||
background: url("../../assets/image/icon/arrowTop-fill-active.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
div.icon-arrow.active {
|
||||
box-shadow: 0px 0px 6px #979797;
|
||||
}
|
||||
|
||||
.node-box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<div class="node-box">
|
||||
<transition name="el-zoom-in-center">
|
||||
<div v-show="infoVisible" class="graph-info-box">
|
||||
<div class="form-title"></div>
|
||||
<el-form ref="relationForm" label-width="80px">
|
||||
<el-form-item label="*关系名称">
|
||||
<el-input v-model="relationCmd.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
<i :class="infoVisible ? 'el-icon-d-arrow-left' : 'el-icon-d-arrow-right'"
|
||||
@click="infoVisible = !infoVisible"></i>
|
||||
<!-- 为图谱准备一个具备大小(宽高)的Dom -->
|
||||
<div class="tree-container">
|
||||
<div class="icon icon-add" @click="showAddNodeDialog">
|
||||
<i title="新增节点"></i>
|
||||
</div>
|
||||
<div class="icon icon-remove">
|
||||
<i title="删除"></i>
|
||||
</div>
|
||||
<div class="icon icon-save">
|
||||
<i title="保存"></i>
|
||||
</div>
|
||||
<div id="mountNode" style="width: 100%; height: 1000px"></div>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="新增节点"
|
||||
:visible.sync="addNodeDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="addNodeForm" label-width="80px">
|
||||
<el-form-item label="节点名称">
|
||||
<el-input v-model="addNodeCmd.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="addNodeDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAddNode">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import { Graph } from '@antv/x6';
|
||||
|
||||
var vm;
|
||||
var graph;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
nodeNum: 0,
|
||||
linkNum: 0,
|
||||
infoVisible: false,
|
||||
clickNode: {id: '', name: ''},
|
||||
addNodeDialogVisible: false,
|
||||
addNodeCmd: {
|
||||
name: ''
|
||||
},
|
||||
relationCmd: {
|
||||
name: '',
|
||||
table: '',
|
||||
sourceId: '',
|
||||
targetId: '',
|
||||
edgeId: ''
|
||||
},
|
||||
nodeAttributes: [] // 节点属性
|
||||
}
|
||||
},
|
||||
created() {
|
||||
vm = this;
|
||||
},
|
||||
mounted() {
|
||||
const data = {
|
||||
// 节点
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1', // String,可选,节点的唯一标识
|
||||
x: 40, // Number,必选,节点位置的 x 值
|
||||
y: 40, // Number,必选,节点位置的 y 值
|
||||
width: 80, // Number,可选,节点大小的 width 值
|
||||
height: 40, // Number,可选,节点大小的 height 值
|
||||
label: 'hello', // String,节点标签
|
||||
},
|
||||
{
|
||||
id: 'node2', // String,节点的唯一标识
|
||||
x: 160, // Number,必选,节点位置的 x 值
|
||||
y: 180, // Number,必选,节点位置的 y 值
|
||||
width: 80, // Number,可选,节点大小的 width 值
|
||||
height: 40, // Number,可选,节点大小的 height 值
|
||||
label: 'world', // String,节点标签
|
||||
},
|
||||
],
|
||||
// 边
|
||||
edges: [
|
||||
{
|
||||
source: 'node1', // String,必须,起始节点 id
|
||||
target: 'node2', // String,必须,目标节点 id
|
||||
},
|
||||
],
|
||||
};
|
||||
vm.initGraph(data);
|
||||
},
|
||||
methods: {
|
||||
initGraph: function (_data) {
|
||||
this.destroyGraph();
|
||||
let _width = document.getElementById("mountNode").clientWidth;
|
||||
let _height = document.getElementById("mountNode").clientHeight;
|
||||
graph = new Graph({
|
||||
container: document.getElementById('mountNode'),
|
||||
width: _width,
|
||||
height: _height,
|
||||
scroller: {
|
||||
enabled: true,
|
||||
pannable: true,
|
||||
pageVisible: false,
|
||||
pageBreak: false,
|
||||
},
|
||||
// mousewheel: {
|
||||
// enabled: true,
|
||||
// modifiers: ['ctrl', 'meta'],
|
||||
// },
|
||||
modes: {
|
||||
// Defualt mode
|
||||
default: ['drag-node',
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
enableOptimize: true, // enable the optimize to hide the shapes beside nodes' keyShape
|
||||
},
|
||||
{
|
||||
type: 'zoom-canvas',
|
||||
enableOptimize: true, // enable the optimize to hide the shapes beside nodes' keyShape
|
||||
},
|
||||
{
|
||||
type: 'create-edge',
|
||||
key: 'shift', // undefined by default, options: 'shift', 'control', 'ctrl', 'meta', 'alt'
|
||||
},
|
||||
]
|
||||
},
|
||||
});
|
||||
graph.fromJSON(_data);
|
||||
|
||||
},
|
||||
destroyGraph() {
|
||||
try {
|
||||
if(graph != undefined && graph != null) {
|
||||
graph.clear();
|
||||
graph = null;
|
||||
document.getElementById("mountNode").innerHTML = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
clearStates() { // 清空选中状态
|
||||
graph.getNodes().forEach((node) => {
|
||||
graph.clearItemStates(node);
|
||||
});
|
||||
graph.getEdges().forEach((edge) => {
|
||||
graph.clearItemStates(edge);
|
||||
});
|
||||
},
|
||||
showAddNodeDialog() {
|
||||
vm.addNodeCmd = {
|
||||
name: ''
|
||||
};
|
||||
vm.addNodeDialogVisible = true;
|
||||
},
|
||||
submitAddNode() {
|
||||
graph.addNode({
|
||||
id: Math.ceil(Math.random()*10000).toString(),
|
||||
label: vm.addNodeCmd.name
|
||||
});
|
||||
vm.$message.success("新增节点成功");
|
||||
vm.addNodeDialogVisible = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDragedNodePosition(e) {
|
||||
const model = e.item.get('model');
|
||||
model.fx = e.x;
|
||||
model.fy = e.y;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.graph-info-box {
|
||||
position: absolute;
|
||||
left: 290px;
|
||||
width: 500px;
|
||||
top: 60px;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
padding: 15px 15px;
|
||||
z-index: 999;
|
||||
border-right: 1px solid rgba(52, 100, 224, 0.15);
|
||||
box-shadow: 0px 2px 21px 0px rgba(52, 100, 224, 0.15);
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-left, .el-icon-d-arrow-right {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-left {
|
||||
left: 780px;
|
||||
}
|
||||
|
||||
.el-icon-d-arrow-right {
|
||||
left: 300px;
|
||||
}
|
||||
|
||||
.info-line-item {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-content > div {
|
||||
margin-bottom: 5px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
/*这个样式不写,右键弹框会一直显示在画布的左下角*/
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #f2f4f7;
|
||||
left: -99999px;
|
||||
top: -999999px;
|
||||
}
|
||||
|
||||
.menu div {
|
||||
list-style: none;
|
||||
padding: 5px 10px;
|
||||
color: black;
|
||||
border-bottom: 1px dashed #ffffff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu div:hover {
|
||||
color: #659bc5;
|
||||
background: #f3f6fa;
|
||||
}
|
||||
|
||||
.menu div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
ul#menuBox li {
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
ul#menuBox li:hover {
|
||||
color: #1c63e0;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.icon {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
border-radius: 40px;
|
||||
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
div.icon i {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: auto;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
div.icon-add {
|
||||
right: 75px;
|
||||
}
|
||||
|
||||
div.icon-add i {
|
||||
background: url("../../assets/image/icon/bg-add.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
div.icon-remove {
|
||||
right: 145px;
|
||||
}
|
||||
|
||||
div.icon-remove i {
|
||||
background: url("../../assets/image/icon/changyonggoupiaorenshanchu.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
div.icon-save {
|
||||
right: 215px;
|
||||
}
|
||||
|
||||
div.icon-save i {
|
||||
background: url("../../assets/image/icon/baocun.png") no-repeat center/contain;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="log-box" v-html="content"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
|
||||
var _this;
|
||||
var socket;
|
||||
var sit;
|
||||
export default {
|
||||
name: "log",
|
||||
data() {
|
||||
return {
|
||||
modelId: '',
|
||||
type: '',
|
||||
content: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
if (sit) {
|
||||
clearInterval(sit);
|
||||
}
|
||||
_this.content = "";
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
},
|
||||
init({modelId, type}) {
|
||||
_this.content = "";
|
||||
_this.modelId = modelId;
|
||||
_this.type = type;
|
||||
console.log(`当前modelId为${modelId},type为${type}`);
|
||||
socket = new WebSocket(`ws://47.103.128.32:10050/socket/file-tail?modelId=${modelId}&type=${type}`);
|
||||
//连接打开事件
|
||||
socket.onopen = function () {
|
||||
console.log("Socket 已打开");
|
||||
};
|
||||
//收到消息事件
|
||||
socket.onmessage = function (msg) {
|
||||
if (msg.data.indexOf('连接成功,sessionId=') != -1) {
|
||||
var sessionId = msg.data.slice(15);
|
||||
console.log(sessionId)
|
||||
} else {
|
||||
// console.log(msg);
|
||||
_this.content += '<p>' + msg.data + "</p>";
|
||||
_this.$nextTick(() => {
|
||||
document.getElementById("log-box").scrollTop = 100000;
|
||||
});
|
||||
}
|
||||
};
|
||||
//连接关闭事件
|
||||
socket.onclose = function () {
|
||||
console.log("Socket已关闭");
|
||||
};
|
||||
//发生了错误事件
|
||||
socket.onerror = function () {
|
||||
alert("Socket发生了错误");
|
||||
}
|
||||
|
||||
|
||||
//创建心跳,防止掉线
|
||||
sit = setInterval(() => {
|
||||
let op = {
|
||||
data: "heart",
|
||||
};
|
||||
sendJson(op);
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendJson(data) {
|
||||
// 0 CONNECTING 连接尚未建立
|
||||
// 1 OPEN WebSocket的链接已经建立
|
||||
// 2 CLOSING 连接正在关闭
|
||||
// 3 CLOSED 连接已经关闭或不可用
|
||||
if (socket.readyState == 2 || socket.readyState == 3) {
|
||||
clearInterval(sit);
|
||||
sit = null;
|
||||
} else {
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#log-box {
|
||||
height: 800px;
|
||||
overflow-y: auto;
|
||||
background-color: #c2c2c2;
|
||||
border-radius: 5px;
|
||||
padding: 0 25px;
|
||||
}
|
||||
|
||||
#log-box p {
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
text-indent: 2em;
|
||||
color: #313131;
|
||||
}
|
||||
</style>
|
||||
@@ -1,256 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
辅助维修
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="search-input-box">
|
||||
<el-input
|
||||
class="search-input"
|
||||
placeholder="请输入内容"
|
||||
v-model="keyword">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-left: 30px" @click="queryByKeyword">检 索</el-button>
|
||||
</div>
|
||||
<div class="title-label">Top5</div>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="推荐案例" name="first">
|
||||
<el-table :data="list" style="width: 100%">
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column prop="number" label="案例编号"></el-table-column>
|
||||
<el-table-column prop="name" label="案例名称"></el-table-column>
|
||||
<el-table-column prop="keyContent" label="关键词"></el-table-column>
|
||||
<el-table-column prop="name" label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showDetail(scope.row, 'faultCase')" type="text" size="small">
|
||||
查看说明
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="推荐维修" name="second">
|
||||
<el-table :data="repairList" style="width: 100%">
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column prop="title" label="标题"></el-table-column>
|
||||
<el-table-column prop="reason" label="原因">
|
||||
<template slot-scope="scope">
|
||||
<label>{{ scope.row.reason.substring(0, 30) + (scope.row.reason.length > 30 ? '...' : '') }}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="influence" label="影响">
|
||||
<template slot-scope="scope">
|
||||
<label>{{
|
||||
scope.row.influence.substring(0, 30) + (scope.row.influence.length > 30 ? '...' : '')
|
||||
}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="program" label="程序">
|
||||
<template slot-scope="scope">
|
||||
<label>{{ scope.row.program.substring(0, 30) + (scope.row.program.length > 30 ? '...' : '') }}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showDetail(scope.row, 'repair')" type="text" size="small">
|
||||
查看说明
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="查看说明"
|
||||
:visible.sync="detailVisible"
|
||||
width="50%">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="故障描述">
|
||||
{{ cmd.description }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="排故过程">
|
||||
{{ cmd.checkFaultProcess }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障分析">
|
||||
{{ cmd.faultAnalysis }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障总结">
|
||||
{{ cmd.faultSummary }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="经验教训">
|
||||
{{ cmd.experience }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="detailVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="查看说明"
|
||||
:visible.sync="repairDetailVisible"
|
||||
width="50%">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="标题">
|
||||
{{ cmd.title }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="原因">
|
||||
{{ cmd.reason }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="影响">
|
||||
{{ cmd.influence }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="程序">
|
||||
{{ cmd.program }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="repairDetailVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/5a.jpg";
|
||||
import bImg from "@/assets/image/614/5b.jpg";
|
||||
import cImg from "@/assets/image/614/5c.jpg";
|
||||
import dImg from "@/assets/image/614/5d.jpg";
|
||||
import eImg from "@/assets/image/614/5e.jpg";
|
||||
import fImg from "@/assets/image/614/5f.jpg";
|
||||
import gImg from "@/assets/image/614/5g.jpg";
|
||||
import hImg from "@/assets/image/614/5h.jpg";
|
||||
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "auxiliaryMaintenance",
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
list: [],
|
||||
repairList: [],
|
||||
cmd: {},
|
||||
rateNum: "-",
|
||||
activeTab: 'first',
|
||||
detailVisible: false,
|
||||
repairDetailVisible: false,
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg, bImg, cImg, dImg, eImg, fImg, gImg, hImg
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
queryByKeyword() {
|
||||
if (_this.keyword.length === 0) {
|
||||
_this.$message.warning("请输入内容再进行检索");
|
||||
return false;
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let charStr in _this.keyword) {
|
||||
sum += charStr.charCodeAt(0);
|
||||
}
|
||||
_this.rateNum = "9" + sum.toString().substring(0, 1) + "." + sum.toString().substring(1);
|
||||
_this.queryFaultCaseList();
|
||||
_this.queryRepairList();
|
||||
},
|
||||
queryFaultCaseList() {
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", _this.keyword);
|
||||
request({
|
||||
url: '/fault_case/query_like_keyword',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
let list = res.data;
|
||||
let resList = [];
|
||||
// 优化排序
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
let row = list[i];
|
||||
if (row.keyContent && row.keyContent.indexOf(_this.keyword) >= 0 && row.description && row.description.indexOf(_this.keyword) >= 0) {
|
||||
resList.unshift(row);
|
||||
} else {
|
||||
resList.push(row);
|
||||
}
|
||||
}
|
||||
_this.list = resList;
|
||||
_this.$message.success("检索成功");
|
||||
});
|
||||
},
|
||||
queryRepairList() {
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", _this.keyword);
|
||||
request({
|
||||
url: '/repair/query_like_keyword',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
let list = res.data;
|
||||
let resList = [];
|
||||
// 优化排序
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
let row = list[i];
|
||||
if (row.title && row.title.indexOf(_this.keyword) >= 0 && row.reason && row.reason.indexOf(_this.keyword) >= 0) {
|
||||
resList.unshift(row);
|
||||
} else {
|
||||
resList.push(row);
|
||||
}
|
||||
}
|
||||
_this.repairList = resList;
|
||||
});
|
||||
},
|
||||
showDetail(row, type) {
|
||||
_this.cmd = row;
|
||||
if (type === 'repair') {
|
||||
_this.repairDetailVisible = true;
|
||||
} else if (type === 'faultCase') {
|
||||
_this.detailVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-item {
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
overflow-x: hidden;
|
||||
|
||||
.search-input-box {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
margin-bottom: 1.5625vw;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,363 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<!-- <div class="btn-box"></div>-->
|
||||
<div class="tool-box">
|
||||
<el-button size="small" type="primary" style="margin-bottom: 15px" @click="gotoInfoExtract">图谱管理</el-button>
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<el-row :gutter="30">
|
||||
<el-col :span="12">
|
||||
<div class="block series-box">
|
||||
<div class="block-label">实体数量</div>
|
||||
<div class="count-box">{{ count.nodeCount }}个</div>
|
||||
<!-- <div class="btn-box">管 理</div>-->
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="block commodity-box">
|
||||
<div class="block-label">三元组数量</div>
|
||||
<div class="count-box">{{ count.relationCount }}个</div>
|
||||
<!-- <div class="btn-box">管 理</div>-->
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="clearAll" style="float: left;margin-right: 15px">清空数据
|
||||
</el-button>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="10"
|
||||
accept=".xls,.xlsx"
|
||||
:file-list="fileList">
|
||||
<el-button size="small" type="primary">导 入</el-button>
|
||||
<label slot="tip" class="el-upload__tip" style="margin-left: 25px">只能上传.xls/.xlsx文件</label>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-row :gutter="20" style="margin-top: 15px">
|
||||
<el-col :span="12">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-table :data="list" border style="width: 100%" height="500">
|
||||
<el-table-column prop="name" label="概念名称" width="130"></el-table-column>
|
||||
<el-table-column prop="properties" label="属性">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-for="tag in scope.row.properties" :key="tag" style="margin-right: 5px;margin-bottom: 5px">{{
|
||||
tag
|
||||
}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
import graph from "@/components/graph/Graph614";
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/1a.jpg";
|
||||
import bImg from "@/assets/image/614/1b.jpg";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "indexPage",
|
||||
data() {
|
||||
return {
|
||||
count: {
|
||||
nodeCount: 0,
|
||||
relationCount: 0
|
||||
},
|
||||
fileList: [],
|
||||
list: [
|
||||
{
|
||||
name: '军种',
|
||||
properties: ['编号', '名称']
|
||||
}, {
|
||||
name: '数控系统',
|
||||
properties: ['编号', '名称', '型号', '最新交付日期', '发动机', '状态', '工作总寿命', '总工作时间', '工作时间翻修期'
|
||||
, '翻修期累积时间', '日历翻修期', '下次翻修日期']
|
||||
}, {
|
||||
name: '发动机',
|
||||
properties: ['编号', '名称', '安装时间', '安装位置', '总工作时间', '总空中工作时间', '型号']
|
||||
}, {
|
||||
name: '故障信息',
|
||||
properties: ['编号', '名称', '故障件编号', '故障件型号', '故障件名称', '环境特点', '故障原因', '故障工作时间',
|
||||
'发生时间', '发现时机', '故障场所', '处理时间', '处理方式', '故障处理措施', '是否归零', '是否重复故障',
|
||||
'是否责任故障', '是否已处理', '故障现象', '排故进度']
|
||||
}, {
|
||||
name: '故障案例',
|
||||
properties: ['编号', '名称', '关键词', '排故进程', '故障分析', '故障总结', '经验教训']
|
||||
}, {
|
||||
name: '原飞机',
|
||||
properties: ['编号', '名称']
|
||||
}, {
|
||||
name: '部件',
|
||||
properties: ['编号', '名称', '型号', '编号码', '日期']
|
||||
}, {
|
||||
name: '地点',
|
||||
properties: ['编号', '名称']
|
||||
}, {
|
||||
name: '飞机',
|
||||
properties: ['编号', '名称']
|
||||
},
|
||||
],
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg,
|
||||
bImg
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryCount();
|
||||
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: [
|
||||
{
|
||||
id: "1",
|
||||
label: '军种',
|
||||
_label: 'armyType'
|
||||
}, {
|
||||
id: "2",
|
||||
label: '数控系统',
|
||||
_label: 'control'
|
||||
}, {
|
||||
id: "3",
|
||||
label: '发动机',
|
||||
_label: 'engine'
|
||||
}, {
|
||||
id: "4",
|
||||
label: '故障信息',
|
||||
_label: 'fault'
|
||||
}, {
|
||||
id: "5",
|
||||
label: '故障案例',
|
||||
_label: 'faultCase'
|
||||
}, {
|
||||
id: "6",
|
||||
label: '原飞机',
|
||||
_label: 'oriPlane'
|
||||
}, {
|
||||
id: "7",
|
||||
label: '部件',
|
||||
_label: 'part'
|
||||
}, {
|
||||
id: "8",
|
||||
label: '发生地',
|
||||
_label: 'place'
|
||||
}, {
|
||||
id: "9",
|
||||
label: '飞机',
|
||||
_label: 'plane'
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: '3',
|
||||
target: '8',
|
||||
label: '所在地'
|
||||
}, {
|
||||
source: '3',
|
||||
target: '6',
|
||||
label: '原装配'
|
||||
}, {
|
||||
source: '3',
|
||||
target: '9',
|
||||
label: '现装配'
|
||||
}, {
|
||||
source: '3',
|
||||
target: '1',
|
||||
label: '隶属'
|
||||
}, {
|
||||
source: '3',
|
||||
target: '2',
|
||||
label: '数控于'
|
||||
}, {
|
||||
source: '4',
|
||||
target: '9',
|
||||
label: '故障飞机'
|
||||
}, {
|
||||
source: '4',
|
||||
target: '3',
|
||||
label: '故障发动机'
|
||||
}, {
|
||||
source: '4',
|
||||
target: '8',
|
||||
label: '故障发生地'
|
||||
}, {
|
||||
source: '4',
|
||||
target: '2',
|
||||
label: '故障控制系统'
|
||||
}, {
|
||||
source: '2',
|
||||
target: '7',
|
||||
label: '配套部件'
|
||||
}, {
|
||||
source: '2',
|
||||
target: '8',
|
||||
label: '所在地'
|
||||
}, {
|
||||
source: '5',
|
||||
target: '9',
|
||||
label: '故障案例飞机'
|
||||
}
|
||||
]
|
||||
}, true, null, true);
|
||||
},
|
||||
methods: {
|
||||
gotoInfoExtract() {
|
||||
this.$router.push("/614/infoExtract");
|
||||
},
|
||||
queryCount() {
|
||||
request({
|
||||
url: '/neo4j/get_count',
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.count.nodeCount = res.data.nodeCount;
|
||||
_this.count.relationCount = res.data.relationCount;
|
||||
});
|
||||
},
|
||||
handleFileChange(file) {
|
||||
_this.fileList = [file];
|
||||
let formData = new FormData();
|
||||
formData.append("file", file.raw);
|
||||
request({
|
||||
url: '/neo4j/read_excel',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.$message.success("导入成功");
|
||||
});
|
||||
},
|
||||
clearAll() {
|
||||
_this.$confirm('此操作将永久清空数据, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/neo4j/clear_all',
|
||||
method: 'post',
|
||||
}).then(res => {
|
||||
_this.$message.success("清空成功");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tool-box {
|
||||
overflow: hidden;
|
||||
|
||||
.img-item {
|
||||
float: right;
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.box-card .el-card__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
width: 130px;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
background-color: #4da8fc;
|
||||
font-size: 21px;
|
||||
font-weight: bold;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container-box {
|
||||
padding: 30px;
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid rgb(231, 231, 231);
|
||||
padding: 25px 30px;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
margin-bottom: 15px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.series-box, .commodity-box {
|
||||
color: #fff;
|
||||
|
||||
|
||||
.block-label {
|
||||
font-size: 25px;
|
||||
float: left;
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.count-box {
|
||||
font-size: 35px;
|
||||
float: right;
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.btn-box:hover {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
.series-box {
|
||||
background-color: rgb(108, 180, 246);
|
||||
|
||||
.btn-box {
|
||||
color: rgb(108, 180, 246);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.commodity-box {
|
||||
background-color: rgb(251, 174, 94);
|
||||
|
||||
.btn-box {
|
||||
color: rgb(251, 174, 94);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,252 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
信息汇聚
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="search-input-box">
|
||||
<el-input
|
||||
class="search-input"
|
||||
placeholder="请输入内容"
|
||||
v-model="keyword">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-left: 30px" @click="queryFaultGraph">检 索</el-button>
|
||||
</div>
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
title="基础信息"
|
||||
:visible.sync="drawerVisible"
|
||||
direction="rtl">
|
||||
<div class="drawer-content">
|
||||
<el-descriptions :column="1" v-show="activeLabel === 'fault'">
|
||||
<el-descriptions-item label="故障名称">{{ nodeItem.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="环境特点">{{ nodeItem.enviChar }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件编号">{{ nodeItem.partNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件型号">{{ nodeItem.partModel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件名称">{{ nodeItem.partName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发生时间">{{ nodeItem.happenTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障场所">{{ nodeItem.faultPlace }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理时间">{{ nodeItem.handleTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理方式">{{ nodeItem.handleStyle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理措施">{{ nodeItem.faultHandleMeasures }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障原因">{{ nodeItem.faultReason }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障现象">{{ nodeItem.faultPhenomenon }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排故进度">{{ nodeItem.checkFaultProgress }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1" v-show="activeLabel === 'engine'">
|
||||
<el-descriptions-item label="编号">{{ nodeItem.number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ nodeItem.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="安装时间">{{ nodeItem.installTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="安装位置">{{ nodeItem.position }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总工时间">{{ nodeItem.workTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总空中工作时间">{{ nodeItem.airWorkTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="型号">{{ nodeItem.model }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1" v-show="activeLabel === 'control'">
|
||||
<el-descriptions-item label="编号">{{ nodeItem.number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ nodeItem.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="型号">{{ nodeItem.model }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最新交付日期">{{ nodeItem.lastDeliverTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发动机">{{ nodeItem.engine }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ nodeItem.status }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工作总寿命">{{ nodeItem.workLife }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="总工作时间">{{ nodeItem.workTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工作时间翻修期">{{ nodeItem.renovate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="翻修期累积时间">{{ nodeItem.renovateTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="日历翻修期">{{ nodeItem.renovateCalendar }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下次翻修日期">{{ nodeItem.nextRenovate }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1" v-show="activeLabel === 'part'">
|
||||
<el-descriptions-item label="编号">{{ nodeItem.number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ nodeItem.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="型号">{{ nodeItem.model }}</el-descriptions-item>
|
||||
<el-descriptions-item label="编号码">{{ nodeItem.numberCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出厂日期">{{ nodeItem.dateStr }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1" v-show="activeLabel === 'other'">
|
||||
<el-descriptions-item label="编号">{{ nodeItem.number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ nodeItem.name }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
import graph from "@/components/graph/Graph614";
|
||||
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/4a.jpg";
|
||||
import bImg from "@/assets/image/614/4b.jpg";
|
||||
import cImg from "@/assets/image/614/4c.jpg";
|
||||
import dImg from "@/assets/image/614/4d.jpg";
|
||||
import eImg from "@/assets/image/614/4e.jpg";
|
||||
import fImg from "@/assets/image/614/4f.jpg";
|
||||
import gImg from "@/assets/image/614/4g.jpg";
|
||||
import hImg from "@/assets/image/614/4h.jpg";
|
||||
import jImg from "@/assets/image/614/4j.jpg";
|
||||
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "infoConverge",
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
nodeItem: {},
|
||||
drawerVisible: false,
|
||||
activeLabel: '',
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg, bImg, cImg, dImg, eImg, fImg, gImg, hImg, jImg
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
queryFaultGraph() {
|
||||
if (_this.keyword.length === 0) {
|
||||
_this.$message.warning("请输入内容再进行检索");
|
||||
return false;
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", _this.keyword);
|
||||
request({
|
||||
url: '/fault/query_for_neo4j',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
res.data.nodes.forEach(node => {
|
||||
node._label = node.label;
|
||||
node.label = node.properties.name;
|
||||
node.id = node.id.toString();
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
delete edge.id;
|
||||
edge.label = edge.properties ? edge.properties.name : '';
|
||||
edge.source = edge.source.toString();
|
||||
edge.target = edge.target.toString();
|
||||
if (edge.source === edge.target) {
|
||||
edge.type = 'loop';
|
||||
} else {
|
||||
delete edge.type;
|
||||
}
|
||||
});
|
||||
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: res.data.nodes,
|
||||
edges: res.data.relations
|
||||
}, true, _this.clickNode);
|
||||
_this.$message.success("检索成功");
|
||||
});
|
||||
},
|
||||
clickNode(param) {
|
||||
_this.activeLabel = param._label;
|
||||
if (param._label === "fault") {
|
||||
request({
|
||||
url: '/fault/query_unique',
|
||||
method: 'post',
|
||||
data: {
|
||||
EQS_number: param.number
|
||||
}
|
||||
}).then(res => {
|
||||
_this.nodeItem = res.data;
|
||||
_this.drawerVisible = true;
|
||||
});
|
||||
} else if (param._label === "engine") {
|
||||
request({
|
||||
url: '/engine/query_unique',
|
||||
method: 'post',
|
||||
data: {
|
||||
EQS_number: param.number
|
||||
}
|
||||
}).then(res => {
|
||||
_this.nodeItem = res.data;
|
||||
_this.drawerVisible = true;
|
||||
});
|
||||
} else if (param._label === "control") {
|
||||
request({
|
||||
url: '/control/query_unique',
|
||||
method: 'post',
|
||||
data: {
|
||||
EQS_number: param.number
|
||||
}
|
||||
}).then(res => {
|
||||
_this.nodeItem = res.data;
|
||||
_this.drawerVisible = true;
|
||||
});
|
||||
} else if (param._label === "part") {
|
||||
request({
|
||||
url: '/part/query_unique',
|
||||
method: 'post',
|
||||
data: {
|
||||
EQS_number: param.number
|
||||
}
|
||||
}).then(res => {
|
||||
_this.nodeItem = res.data;
|
||||
_this.drawerVisible = true;
|
||||
});
|
||||
} else {
|
||||
_this.activeLabel = 'other';
|
||||
_this.nodeItem = {
|
||||
number: param.number,
|
||||
name: param.label
|
||||
};
|
||||
_this.drawerVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-item {
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
}
|
||||
.menu-content {
|
||||
overflow-x: hidden;
|
||||
|
||||
.search-input-box {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
margin-bottom: 1.5625vw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 0 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识抽取
|
||||
<el-tag
|
||||
class="tag-item"
|
||||
@click="handleTagClick(item)"
|
||||
v-for="item in tags"
|
||||
:key="item.label"
|
||||
:type="item.type"
|
||||
:effect="activeTag === item.label ? 'dark' : 'plain'">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="title-label">实体(F1值:99.2%)</div>
|
||||
<el-table :data="nodeRecord.list" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID"></el-table-column>
|
||||
<el-table-column prop="label" label="标签" :formatter="labelFormatter"></el-table-column>
|
||||
<el-table-column prop="number" label="编号"></el-table-column>
|
||||
<el-table-column prop="properties.name" label="名称"></el-table-column>
|
||||
<el-table-column prop="flag" label="是否准确" width="80">
|
||||
<template slot-scope="scope">
|
||||
<label style="color: #00dc6b">√</label>
|
||||
<!-- <label style="color: #e02323" v-if="!scope.row.flag">×</label>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
:page-size="5"
|
||||
layout="prev, pager, next"
|
||||
@current-change="handleEntityCurrentChange"
|
||||
:current-page="nodeQo.pageNo"
|
||||
:total="nodeRecord.total">
|
||||
</el-pagination>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="title-label">三元组(F1值:98.3%)</div>
|
||||
<el-table :data="relationRecord.list" border style="width: 100%">
|
||||
<el-table-column prop="properties.sourceNumber" label="起始节点编号"></el-table-column>
|
||||
<el-table-column prop="properties.targetNumber" label="终止节点编号"></el-table-column>
|
||||
<el-table-column prop="properties.name" label="名称"></el-table-column>
|
||||
<el-table-column prop="name" label="是否准确" width="80">
|
||||
<template slot-scope="scope">
|
||||
<label style="color: #00dc6b">√</label>
|
||||
<!-- <label style="color: #e02323" v-if="!scope.row.flag">×</label>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:page-size="5"
|
||||
@current-change="handleTripletCurrentChange"
|
||||
:current-page="relationQo.pageNo"
|
||||
:total="relationRecord.total">
|
||||
</el-pagination>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
import graph from "@/components/graph/Graph614";
|
||||
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/2a.jpg";
|
||||
import bImg from "@/assets/image/614/2b.jpg";
|
||||
import cImg from "@/assets/image/614/2c.jpg";
|
||||
import dImg from "@/assets/image/614/2d.jpg";
|
||||
import fImg from "@/assets/image/614/2f.jpg";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "infoExtract",
|
||||
data() {
|
||||
return {
|
||||
nodeQo: {
|
||||
pageNo: 1,
|
||||
pageSize: 5
|
||||
},
|
||||
relationQo: {
|
||||
pageNo: 1,
|
||||
pageSize: 5
|
||||
},
|
||||
nodeRecord: {
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
relationRecord: {
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
name: '数控系统',
|
||||
label: 'control'
|
||||
}, {
|
||||
name: '发动机',
|
||||
label: 'engine'
|
||||
}, {
|
||||
name: '故障信息',
|
||||
label: 'fault'
|
||||
}
|
||||
],
|
||||
activeTag: '',
|
||||
legendList: {
|
||||
"armyType": {label: '军种', color: 'rgb(76,142,218)'},
|
||||
"control": {label: '控制系统', color: 'rgb(236,181,201)'},
|
||||
"engine": {label: '发动机', color: 'rgb(241,102,103)'},
|
||||
"fault": {label: '故障信息', color: 'rgb(141,204,147)'},
|
||||
"faultCase": {label: '故障案例', color: 'rgb(156,33,234)'},
|
||||
"oriPlane": {label: '原飞机', color: 'rgb(255,196,84)'},
|
||||
"part": {label: '部件', color: 'rgb(86,148,128)'},
|
||||
"place": {label: '发生地', color: 'rgb(217,200,174)'},
|
||||
"plane": {label: '飞机', color: 'rgb(87,199,227)'}
|
||||
},
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg, bImg, cImg, dImg, fImg
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryGraphData();
|
||||
_this.queryNodePage();
|
||||
_this.queryRelationPage();
|
||||
},
|
||||
methods: {
|
||||
handleTagClick(item) {
|
||||
if (item.label !== _this.activeTag) {
|
||||
_this.activeTag = item.label;
|
||||
} else {
|
||||
_this.activeTag = '';
|
||||
}
|
||||
_this.queryGraphData();
|
||||
},
|
||||
queryGraphData() {
|
||||
let formData = new FormData();
|
||||
formData.append("label", _this.activeTag);
|
||||
request({
|
||||
url: '/neo4j/query_data/70',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
res.data.nodes.forEach(node => {
|
||||
node._label = node.label;
|
||||
node.label = node.properties.name;
|
||||
node.id = node.id.toString();
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
delete edge.id;
|
||||
edge.source = edge.source.toString();
|
||||
edge.target = edge.target.toString();
|
||||
edge.label = edge.properties ? edge.properties.name : '';
|
||||
if (edge.source === edge.target) {
|
||||
edge.type = 'loop';
|
||||
} else {
|
||||
delete edge.type;
|
||||
}
|
||||
});
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: res.data.nodes,
|
||||
edges: res.data.relations
|
||||
}, true);
|
||||
});
|
||||
},
|
||||
labelFormatter(row) {
|
||||
if (_this.legendList[row.label]) {
|
||||
return _this.legendList[row.label].label;
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
queryNodePage() {
|
||||
request({
|
||||
url: '/neo4j/query_node_page',
|
||||
method: 'post',
|
||||
data: _this.nodeQo
|
||||
}).then(res => {
|
||||
_this.nodeRecord.list = res.data.nodes;
|
||||
_this.nodeRecord.total = res.data.total;
|
||||
});
|
||||
},
|
||||
queryRelationPage() {
|
||||
request({
|
||||
url: '/neo4j/query_relation_page',
|
||||
method: 'post',
|
||||
data: _this.relationQo
|
||||
}).then(res => {
|
||||
_this.relationRecord.list = res.data.relations;
|
||||
_this.relationRecord.total = res.data.total;
|
||||
});
|
||||
},
|
||||
handleEntityCurrentChange(val) {
|
||||
_this.nodeQo.pageNo = val;
|
||||
_this.queryNodePage();
|
||||
},
|
||||
handleTripletCurrentChange(val) {
|
||||
_this.relationQo.pageNo = val;
|
||||
_this.queryRelationPage();
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-item {
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
overflow-x: hidden;
|
||||
|
||||
.title-label {
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
链接预测
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="recommend-content">
|
||||
<div class="search-input-box">
|
||||
<el-input placeholder="请输入头实体内容" v-model="startKeyword" @change="handleStartKeywordChange"
|
||||
class="search-input" style="width: 160px;"></el-input>
|
||||
<el-select v-model="relationType" placeholder="请选择" style="width: 140px;">
|
||||
<el-option label="故障飞机" value="plane"></el-option>
|
||||
<el-option label="故障发动机" value="engine"></el-option>
|
||||
<el-option label="故障所在地" value="place"></el-option>
|
||||
<el-option label="故障控制系统" value="control"></el-option>
|
||||
</el-select>
|
||||
<el-input placeholder="请输入尾实体内容" v-model="endKeyword" @change="handleEndKeywordChange"
|
||||
class="search-input" style="width: 160px;"></el-input>
|
||||
<el-button type="primary" style="margin-left: 30px" @click="queryData">预 测</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-table-box">
|
||||
<el-table :data="list" border style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="80"></el-table-column>
|
||||
<el-table-column prop="label" label="标签" :formatter="labelFormatter"></el-table-column>
|
||||
<el-table-column prop="number" label="编号"></el-table-column>
|
||||
<el-table-column prop="properties.name" label="名称"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/3a.jpg";
|
||||
import bImg from "@/assets/image/614/3b.jpg";
|
||||
import cImg from "@/assets/image/614/3c.jpg";
|
||||
import dImg from "@/assets/image/614/3d.jpg";
|
||||
import eImg from "@/assets/image/614/3e.jpg";
|
||||
import fImg from "@/assets/image/614/3f.jpg";
|
||||
import gImg from "@/assets/image/614/3g.jpg";
|
||||
import hImg from "@/assets/image/614/3h.jpg";
|
||||
import iImg from "@/assets/image/614/3i.jpg";
|
||||
import jImg from "@/assets/image/614/3j.jpg";
|
||||
import kImg from "@/assets/image/614/3k.jpg";
|
||||
import lImg from "@/assets/image/614/3l.jpg";
|
||||
import mImg from "@/assets/image/614/3m.jpg";
|
||||
import nImg from "@/assets/image/614/3n.jpg";
|
||||
import oImg from "@/assets/image/614/3o.jpg";
|
||||
import pImg from "@/assets/image/614/3p.jpg";
|
||||
import qImg from "@/assets/image/614/3q.jpg";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "linkPrediction",
|
||||
data() {
|
||||
return {
|
||||
startKeyword: "",
|
||||
endKeyword: "",
|
||||
relationType: 'plane',
|
||||
list: [],
|
||||
legendList: {
|
||||
"armyType": {label: '军种', color: 'rgb(76,142,218)'},
|
||||
"control": {label: '控制系统', color: 'rgb(236,181,201)'},
|
||||
"engine": {label: '发动机', color: 'rgb(241,102,103)'},
|
||||
"fault": {label: '故障信息', color: 'rgb(141,204,147)'},
|
||||
"faultCase": {label: '故障案例', color: 'rgb(156,33,234)'},
|
||||
"oriPlane": {label: '原飞机', color: 'rgb(255,196,84)'},
|
||||
"part": {label: '部件', color: 'rgb(86,148,128)'},
|
||||
"place": {label: '发生地', color: 'rgb(217,200,174)'},
|
||||
"plane": {label: '飞机', color: 'rgb(87,199,227)'}
|
||||
},
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg, bImg, cImg, dImg, eImg, fImg, gImg, hImg, iImg, jImg, kImg, lImg, mImg, nImg, oImg, pImg, qImg
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
labelFormatter(row) {
|
||||
return _this.legendList[row.label].label;
|
||||
},
|
||||
handleStartKeywordChange() {
|
||||
_this.endKeyword = "";
|
||||
},
|
||||
handleEndKeywordChange() {
|
||||
_this.startKeyword = "";
|
||||
},
|
||||
queryData() {
|
||||
if (_this.startKeyword.length === 0 && _this.endKeyword.length === 0) {
|
||||
_this.$message.warning("请输入头或尾实体内容再进行预测");
|
||||
return false;
|
||||
}
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", _this.startKeyword.length > _this.endKeyword.length ? _this.startKeyword : _this.endKeyword);
|
||||
formData.append("relationType", _this.relationType);
|
||||
if (_this.startKeyword.length > 0) {
|
||||
request({
|
||||
url: '/fault/query_link_node',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.list = res.data;
|
||||
_this.$message.success("预测成功");
|
||||
});
|
||||
} else {
|
||||
request({
|
||||
url: '/fault/query_source_fault_node',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.list = res.data;
|
||||
_this.$message.success("预测成功");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-item {
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.recommend-content {
|
||||
margin: 0 auto;
|
||||
|
||||
.search-input-box {
|
||||
width: 55%;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.search-input input {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,340 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
风险预测
|
||||
<el-image
|
||||
class="img-item"
|
||||
:src="coverImg"
|
||||
:preview-src-list="srcList">
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="search-input-box">
|
||||
<el-input
|
||||
class="search-input"
|
||||
placeholder="请输入内容"
|
||||
v-model="keyword">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-left: 30px" @click="queryByKeyword">检 索</el-button>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="故障案例" name="first">
|
||||
<el-table :data="faultCaseList" style="width: 100%">
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column prop="number" label="案例编号"></el-table-column>
|
||||
<el-table-column prop="name" label="案例名称"></el-table-column>
|
||||
<el-table-column prop="keyContent" label="关键词"></el-table-column>
|
||||
<el-table-column prop="name" label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showDetail(scope.row, 'faultCase')" type="text" size="small">
|
||||
查看说明
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障信息" name="third">
|
||||
<el-table :data="faultList" style="width: 100%">
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column prop="number" label="故障编号"></el-table-column>
|
||||
<el-table-column prop="name" label="故障名称"></el-table-column>
|
||||
<el-table-column prop="happenTime" label="发生时间"></el-table-column>
|
||||
<el-table-column prop="name" label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showDetail(scope.row, 'fault')" type="text" size="small">
|
||||
查看说明
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="维修手册" name="second">
|
||||
<el-table :data="repairList" style="width: 100%">
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column prop="title" label="标题"></el-table-column>
|
||||
<el-table-column prop="reason" label="原因">
|
||||
<template slot-scope="scope">
|
||||
<label>{{ scope.row.reason.substring(0, 30) + (scope.row.reason.length > 30 ? '...' : '') }}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="influence" label="影响">
|
||||
<template slot-scope="scope">
|
||||
<label>{{
|
||||
scope.row.influence.substring(0, 30) + (scope.row.influence.length > 30 ? '...' : '')
|
||||
}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="program" label="程序">
|
||||
<template slot-scope="scope">
|
||||
<label>{{
|
||||
scope.row.program.substring(0, 30) + (scope.row.program.length > 30 ? '...' : '')
|
||||
}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showDetail(scope.row, 'repair')" type="text" size="small">
|
||||
查看说明
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
<div class="result-label">
|
||||
<label>风险预测指标:{{ riskNum }};</label><br/>
|
||||
<label>风险预测原因:{{ riskLabel }};</label>
|
||||
</div>
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="查看说明"
|
||||
:visible.sync="detailVisible"
|
||||
width="50%">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="故障描述">
|
||||
{{ cmd.description }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="排故过程">
|
||||
{{ cmd.checkFaultProcess }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障分析">
|
||||
{{ cmd.faultAnalysis }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障总结">
|
||||
{{ cmd.faultSummary }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="经验教训">
|
||||
{{ cmd.experience }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="detailVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="查看说明"
|
||||
:visible.sync="repairDetailVisible"
|
||||
width="50%">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="标题">
|
||||
{{ cmd.title }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="原因">
|
||||
{{ cmd.reason }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="影响">
|
||||
{{ cmd.influence }}
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="程序">
|
||||
{{ cmd.program }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="repairDetailVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="查看说明"
|
||||
:visible.sync="faultDetailVisible"
|
||||
width="50%">
|
||||
<el-descriptions :column="2">
|
||||
<el-descriptions-item label="故障名称">{{ cmd.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="环境特点">{{ cmd.enviChar }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件编号">{{ cmd.partNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件型号">{{ cmd.partModel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障件名称">{{ cmd.partName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发生时间">{{ cmd.happenTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障场所">{{ cmd.faultPlace }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理时间">{{ cmd.handleTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理方式">{{ cmd.handleStyle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理措施">{{ cmd.faultHandleMeasures }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障原因">{{ cmd.faultReason }}</el-descriptions-item>
|
||||
<el-descriptions-item label="故障现象">{{ cmd.faultPhenomenon }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排故进度">{{ cmd.checkFaultProgress }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="faultDetailVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request614';
|
||||
import graph from "@/components/graph/Graph614";
|
||||
import coverImg from "@/assets/image/icon/tupian.png";
|
||||
import aImg from "@/assets/image/614/6a.jpg";
|
||||
import bImg from "@/assets/image/614/6b.jpg";
|
||||
import cImg from "@/assets/image/614/6c.jpg";
|
||||
import dImg from "@/assets/image/614/6d.jpg";
|
||||
import eImg from "@/assets/image/614/6e.jpg";
|
||||
import fImg from "@/assets/image/614/6f.jpg";
|
||||
import gImg from "@/assets/image/614/6g.jpg";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "riskPrediction",
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
activeTab: 'first',
|
||||
faultCaseList: [],
|
||||
repairList: [],
|
||||
faultList: [],
|
||||
cmd: {},
|
||||
riskNum: 0,
|
||||
riskLabel: '',
|
||||
faultDetailVisible: false,
|
||||
detailVisible: false,
|
||||
repairDetailVisible: false,
|
||||
coverImg: coverImg,
|
||||
srcList: [
|
||||
aImg, bImg, cImg, dImg, eImg, fImg, gImg
|
||||
]
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
calcRisk(faultNum, repairNum) {// 计算风险
|
||||
_this.riskNum = (Math.log(faultNum + 1) / Math.log(faultNum + 2 * (repairNum + 1))).toFixed(3);
|
||||
if (_this.riskNum <= 0.7) {
|
||||
_this.riskLabel = "风险等级低,故障关联少";
|
||||
} else if (_this.riskNum <= 0.9 && _this.riskNum > 0.7) {
|
||||
_this.riskLabel = "风险等级中,有一定数量故障关联和维修方法";
|
||||
} else if (_this.riskNum > 0.9) {
|
||||
_this.riskLabel = "风险等级高,故障关联多,维修方法内容不足";
|
||||
}
|
||||
},
|
||||
queryByKeyword() {
|
||||
if (_this.keyword.length === 0) {
|
||||
_this.$message.warning("请输入内容再进行检索");
|
||||
return false;
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let charStr in _this.keyword) {
|
||||
sum += charStr.charCodeAt(0);
|
||||
}
|
||||
_this.rateNum = "9" + sum.toString().substring(0, 1) + "." + sum.toString().substring(1);
|
||||
let formData = new FormData();
|
||||
formData.append("keyword", _this.keyword);
|
||||
|
||||
_this.queryFaultCaseListTogether(formData);
|
||||
_this.queryFaultGraph(formData);
|
||||
},
|
||||
queryFaultCaseListTogether(formData) {
|
||||
request({
|
||||
url: '/fault_case/query_like_keyword_together',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
let item = res.data;
|
||||
_this.faultCaseList = item.faultCaseList;
|
||||
_this.faultList = item.faultList;
|
||||
_this.repairList = item.repairList;
|
||||
|
||||
_this.calcRisk(_this.faultCaseList.length + _this.faultList.length, _this.repairList.length);
|
||||
_this.$message.success("检索成功");
|
||||
});
|
||||
},
|
||||
queryFaultGraph(formData) {
|
||||
request({
|
||||
url: '/fault_case/query_for_neo4j',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
res.data.nodes.forEach(node => {
|
||||
node._label = node.label;
|
||||
node.label = node.properties.name;
|
||||
node.id = node.id.toString();
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
delete edge.id;
|
||||
edge.source = edge.source.toString();
|
||||
edge.target = edge.target.toString();
|
||||
edge.label = edge.properties ? edge.properties.name : '';
|
||||
if (edge.source === edge.target) {
|
||||
edge.type = 'loop';
|
||||
} else {
|
||||
delete edge.type;
|
||||
}
|
||||
});
|
||||
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: res.data.nodes,
|
||||
edges: res.data.relations
|
||||
}, true);
|
||||
});
|
||||
},
|
||||
showDetail(row, type) {
|
||||
_this.cmd = row;
|
||||
if (type === 'repair') {
|
||||
_this.repairDetailVisible = true;
|
||||
} else if (type === 'faultCase') {
|
||||
_this.detailVisible = true;
|
||||
} else if (type === "fault") {
|
||||
_this.faultDetailVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-item {
|
||||
width: 95px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 80px;
|
||||
}
|
||||
.result-label {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
overflow-x: hidden;
|
||||
|
||||
.search-input-box {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
margin-bottom: 1.5625vw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 0 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,355 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
算法发布
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入算法名称"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入版本"></el-input>
|
||||
</el-form-item> -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">发布算法</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table">
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="算法名称"></el-table-column>
|
||||
<!-- <el-table-column prop="version" label="版本"></el-table-column> -->
|
||||
<el-table-column prop="buildRemark" label="介绍"></el-table-column>
|
||||
<el-table-column prop="modifyTimeMillis" label="修改时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.modifyTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTimeMillis" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.createTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="modifyRow(scope.row)" type="text" size="small">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="dialogName"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<div v-show="step == 1">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd" v-loading="stepLoading1" element-loading-text="代码pull中...">
|
||||
<el-form-item label="算法名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入算法名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法介绍" prop="buildRemark">
|
||||
<el-input type="textarea" v-model="cmd.buildRemark" placeholder="请输入算法介绍"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="代码仓库" prop="repositoryId" >
|
||||
<el-select v-model="cmd.repositoryId" placeholder="请选择代码仓库" @change="handleRepositoryChange">
|
||||
<el-option :label="item.name" :value="item.id" v-for="item in codeWarehouseList"
|
||||
:key="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库分支" prop="branchName">
|
||||
<el-select v-model="cmd.branchName" placeholder="请选择分支">
|
||||
<el-option :label="item" :value="item" v-for="(item,index) in branchList" :key="index"></el-option>
|
||||
</el-select>
|
||||
<!-- <el-input v-model="cmd.branchName" placeholder="请输入分支"></el-input>-->
|
||||
</el-form-item>
|
||||
<el-form-item label="依赖方式" prop="relyOnType">
|
||||
<el-radio-group v-model="cmd.relyOnType">
|
||||
<el-radio :label="1">克隆Conda环境</el-radio>
|
||||
<el-radio :label="2">文件安装</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="环境名称" prop="condaName" v-if="cmd.relyOnType==1">
|
||||
<el-input v-model="cmd.condaName" placeholder="请输入环境名称"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="标签(TAG)" prop="branchTagName">-->
|
||||
<!-- <el-input v-model="cmd.branchTagName" placeholder="请输入标签(TAG)"></el-input>-->
|
||||
<!-- <!– <el-select v-model="cmd.name" placeholder="请选择发布的标签,不选为最新提交">–>-->
|
||||
<!-- <!– <el-option label="" value=""></el-option>–>-->
|
||||
<!-- <!– </el-select>–>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="启动命令" prop="script">-->
|
||||
<!-- <el-input v-model="cmd.script" placeholder="请输入启动命令" type="textarea"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="日志文件" prop="logFileName">-->
|
||||
<!-- <el-input v-model="cmd.logFileName" placeholder="请输入日志文件名称或目录(相对目录)"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-show="step == 2">
|
||||
<el-form ref="cmd2" label-width="100px" :model="cmd">
|
||||
<div class="title-form-box">公共配置</div>
|
||||
<el-form-item :label="item.name" v-for="item in cmd2.publicPropert" :key="item.key">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name"></el-input>
|
||||
</el-form-item>
|
||||
<div class="title-form-box">私有配置</div>
|
||||
<el-form-item :label="item.name" v-for="item in cmd2.privatePropert" :key="item.key">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-show="step == 3">
|
||||
<i class="el-icon-loading" v-show="stepPercent < 100"></i>
|
||||
<!-- <el-progress :text-inside="true" :stroke-width="26" :percentage="stepPercent" v-show="stepPercent < 100"></el-progress>-->
|
||||
<label v-show="stepPercent >= 100">依赖校验成功!</label>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="nextStep" v-show="step == 1" :disabled="stepLoading1">下一步</el-button>
|
||||
<el-button type="primary" @click="nextStep" v-show="step == 2">下一步</el-button>
|
||||
<el-button type="primary" @click="submitModify" v-show="step == 3" :disabled="stepPercent < 100">发布</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "codeWarehouse",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
codeWarehouseList: [],
|
||||
step: 1,
|
||||
stepPercent: 0,
|
||||
dialogName: '发布算法',
|
||||
cmdDialogVisible: false,
|
||||
cmd: {},
|
||||
cmd2: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入算法名称'}
|
||||
],
|
||||
repositoryId: [
|
||||
{required: true, message: '请选择代码仓库'}
|
||||
],
|
||||
branchName: [
|
||||
{required: true, message: '请输入分支'}
|
||||
],
|
||||
branchTagName: [
|
||||
{required: true, message: '请输入标签(TAG)'}
|
||||
],
|
||||
logFileName: [
|
||||
{required: true, message: '请输入启动命令'}
|
||||
],
|
||||
script: [
|
||||
{required: true, message: '请输入日志文件'}
|
||||
],
|
||||
relyOnType: [
|
||||
{required: true, message: '请选择依赖方式'}
|
||||
],
|
||||
condaName: [
|
||||
{required: true, message: '请输入虚拟环境名称'}
|
||||
],
|
||||
},
|
||||
stepLoading1: false,
|
||||
branchList: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
_this.queryCodeWarehouseList();
|
||||
},
|
||||
filters: {
|
||||
timeFilter(val) {
|
||||
if (val > 0) {
|
||||
let dt = new Date(val);
|
||||
let year = dt.getFullYear();
|
||||
let month = dt.getMonth() + 1;
|
||||
let date = dt.getDate();
|
||||
let hour = dt.getHours();
|
||||
let minute = dt.getMinutes();
|
||||
let second = dt.getSeconds();
|
||||
return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRepositoryChange(id) {
|
||||
_this.queryBranchList(id);
|
||||
_this.cmd.branchName = "";
|
||||
},
|
||||
queryBranchList(id) {
|
||||
request({
|
||||
url: '/repository/branch-list',
|
||||
method: 'post',
|
||||
data: {repositoryId: id}
|
||||
}).then(res => {
|
||||
_this.branchList = res.list[0];
|
||||
});
|
||||
},
|
||||
queryCodeWarehouseList() {
|
||||
request({
|
||||
url: '/repository/list',
|
||||
method: 'post',
|
||||
data: {pageNo: 1, pageSize: 1000}
|
||||
}).then(res => {
|
||||
_this.codeWarehouseList = res.list;
|
||||
});
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/algorithm/list',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.list;
|
||||
console.log(res.list);
|
||||
_this.result.total = res.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
let id = item.id
|
||||
request({
|
||||
url: `/algorithm/delete/${id}`,
|
||||
method: 'get',
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
_this.queryOntologyList();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.step = 1;
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.stepLoading1 = false;
|
||||
_this.$nextTick(() => {
|
||||
_this.cmd = {
|
||||
name: '',
|
||||
repositoryId: '',
|
||||
branchName: '',
|
||||
branchTagName: '',
|
||||
logFileName: '',
|
||||
script: ''
|
||||
};
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.cmd.modifyTimeMillis = null;
|
||||
_this.stepLoading1 = false;
|
||||
if (_this.cmd.repositoryId != null && _this.cmd.repositoryId.toString().length > 0) {
|
||||
_this.queryBranchList(_this.cmd.repositoryId);
|
||||
}
|
||||
_this.cmdDialogVisible = true;
|
||||
},
|
||||
nextStep() {
|
||||
if (_this.step == 1) {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
_this.stepLoading1 = true;
|
||||
request({
|
||||
url: '/algorithm/pull',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.stepLoading1 = false;
|
||||
_this.step = 2;
|
||||
_this.dialogName = "配置文件预览";
|
||||
_this.cmd2 = res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (_this.step == 2) {
|
||||
let id = _this.cmd2.id;
|
||||
_this.dialogName = "依赖下载中.........";
|
||||
_this.step = 3;
|
||||
request({
|
||||
url: `/algorithm/verification_rely/${id}`,
|
||||
method: 'get',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.stepPercent = 100;
|
||||
});
|
||||
// let si = setInterval(function() {
|
||||
// let percent = _this.stepPercent;
|
||||
// percent += 10;
|
||||
// if(percent >= 100) {
|
||||
// percent = 100;
|
||||
// clearInterval(si);
|
||||
// }
|
||||
//
|
||||
// }, 2000);
|
||||
}
|
||||
},
|
||||
submitModify() {
|
||||
let id = _this.cmd2.id;
|
||||
request({
|
||||
url: `/algorithm/release/${id}`,
|
||||
method: 'get',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("发布成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.title-form-box {
|
||||
font-size: 16px;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,242 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
三元组高速抽取
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-upload
|
||||
style="float: left;margin-right: 10px"
|
||||
class="upload-demo"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
action="#"
|
||||
multiple
|
||||
:limit="3"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList">
|
||||
<el-button type="primary" title='上传高速抽取测试数据'>上传数据</el-button>
|
||||
</el-upload>
|
||||
<el-button type="primary" title='开始三元组的高速抽取' :disabled="!uploadFlag" @click="validateData">开始抽取
|
||||
</el-button>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-table :data="tableData" style="width: 100%" border class="text-table" id="text-table-id">
|
||||
<el-table-column type="index" label="行号" width="60"></el-table-column>
|
||||
<el-table-column prop="text" label="文本" width="200">
|
||||
<template slot-scope="scope" :title="scope.row.text">{{
|
||||
scope.row.text.substring(0, 120)
|
||||
}}{{ scope.row.text.length > 120 ? '...' : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="groupStrCut" label="三元组">
|
||||
<template slot-scope="scope">
|
||||
<div class="tag-item" v-for="(tag,index) in scope.row.sro_list" :key="index">
|
||||
{{ tag.object }}-{{ tag.relation }}-{{ tag.subject }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-descriptions class="margin-top desc2" :column="2" border>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">准确率:</template>
|
||||
<label>{{ correctRate }}%</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">耗时:</template>
|
||||
<div>{{ time }}s</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="label-result" v-show="resultFlag">每千条文本知识抽取耗时{{ qths }}秒</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import graph from "@/components/graph/Graph";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "algorithmSpeedVerification",
|
||||
data() {
|
||||
return {
|
||||
uploadFlag: false,
|
||||
correctRate: 0,
|
||||
time: 0,
|
||||
fileList: [],
|
||||
jsonObj: {},
|
||||
tableData: [],
|
||||
resultFlag: false,
|
||||
everyThousandTime: 0,
|
||||
rateAll: [],
|
||||
timeAll:[],
|
||||
qths:0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
validateData() {
|
||||
_this.$message.info("开始验证");
|
||||
let pageNo = 1, pageSize = 5;
|
||||
let _index = 0;
|
||||
let _time = 0;
|
||||
// let timeSt = setInterval(function () {
|
||||
// _this.time = _time++ / 10;
|
||||
// }, 100);
|
||||
_this.tableData = [];
|
||||
let st = setInterval(function () {
|
||||
_this.correctRate = (parseFloat(_this.rateAll[_index]) * 100).toFixed(2);
|
||||
_this.time = _this.timeAll[_index];
|
||||
_index += 5;
|
||||
_this.tableData = _this.tableData.concat(_this.jsonObj.items.slice((pageNo - 1) * pageSize, pageNo * pageSize));
|
||||
_this.$nextTick(() => {
|
||||
var div = document.getElementById('text-table-id');
|
||||
div.scrollTop = div.scrollHeight;
|
||||
})
|
||||
|
||||
pageNo++;
|
||||
if (_index >= _this.jsonObj.items.length) {
|
||||
clearInterval(st);
|
||||
// clearInterval(timeSt);
|
||||
_this.startInitGraph();
|
||||
_this.$message.success("验证完毕");
|
||||
|
||||
_this.resultFlag = true;
|
||||
// _this.everyThousandTime = (_this.time / 1.2).toFixed(2);
|
||||
}
|
||||
}, 40);
|
||||
},
|
||||
handleFileChange(file) {
|
||||
_this.fileList = [file];
|
||||
_this.correctRate = 0;
|
||||
_this.time = 0;
|
||||
_this.resultFlag = false;
|
||||
_this.everyThousandTime = 0;
|
||||
_this.tableData = [];
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: [],
|
||||
edges: []
|
||||
}, true, null, true);
|
||||
_this.queryData(file.raw);
|
||||
},
|
||||
queryData(fileItem) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", fileItem);
|
||||
let _msg = _this.$message({
|
||||
message: '文件正在上传,请耐心等待',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
});
|
||||
request({
|
||||
url: '/indicator/indicator10',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_msg.close();
|
||||
_this.$message.success("上传成功");
|
||||
_this.rateAll = [];
|
||||
_this.timeAll = [];
|
||||
res.data.items.forEach(item => {
|
||||
_this.rateAll.push(item.precision);
|
||||
_this.timeAll.push(item.hshi)
|
||||
})
|
||||
_this.qths = res.data.qths
|
||||
/**
|
||||
* nodes:[],
|
||||
* relations: [],
|
||||
* items: []
|
||||
*/
|
||||
res.data.relations = res.data.relations.slice(0, 100);
|
||||
_this.jsonObj = res.data;
|
||||
_this.uploadFlag = true;
|
||||
});
|
||||
},
|
||||
startInitGraph() {
|
||||
let _nodes = [], _edges = [];
|
||||
let ids = [];
|
||||
_this.jsonObj.relations.forEach(edge => {
|
||||
ids.push(edge.srcId);
|
||||
ids.push(edge.dstId);
|
||||
_edges.push({
|
||||
source: edge.srcId,
|
||||
target: edge.dstId,
|
||||
label: edge.edgeName
|
||||
});
|
||||
});
|
||||
|
||||
_this.jsonObj.nodes.forEach(node => {
|
||||
if (ids.indexOf(node.vid) >= 0) {
|
||||
_nodes.push({
|
||||
label: node.properties.name,
|
||||
id: node.vid
|
||||
});
|
||||
}
|
||||
});
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: _nodes,
|
||||
edges: _edges
|
||||
}, true, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.label-result {
|
||||
padding: 15px 0;
|
||||
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.tag-item {
|
||||
word-break: break-all;
|
||||
float: left;
|
||||
background-color: #ecf5ff;
|
||||
border-color: #d9ecff;
|
||||
padding: 0 5px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
margin: 0 4px 4px 0;
|
||||
max-width: 430px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.text-table {
|
||||
height: 600px;
|
||||
margin-top: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
height: 530px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -1,232 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
代码仓库
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入仓库名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入仓库地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">新增</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table" >
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="算法仓库名称"></el-table-column>
|
||||
<el-table-column prop="gitUrl" label="仓库地址"></el-table-column>
|
||||
<el-table-column prop="modifyTimeMillis" label="修改时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.modifyTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTimeMillis" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.createTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="modifyRow(scope.row)" type="text" size="small">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="dialogName"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="算法名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入算法名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库地址" prop="gitUrl">
|
||||
<el-input v-model="cmd.gitUrl" placeholder="请输入仓库地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="账号" prop="userName">
|
||||
<el-input v-model="cmd.userName" placeholder="请输入账号"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="cmd.password" placeholder="请输入密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="cmd.remarks" placeholder="请输入描述" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitModify">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "codeWarehouse",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
dialogName: '编辑',
|
||||
cmdDialogVisible: false,
|
||||
cmd: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入算法名称'}
|
||||
],
|
||||
gitUrl: [
|
||||
{required: true, message: '请输入仓库地址'}
|
||||
],
|
||||
userName: [
|
||||
{required: true, message: '请输入账号'}
|
||||
],
|
||||
password: [
|
||||
{required: true, message: '请输入密码'}
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
filters: {
|
||||
timeFilter(val) {
|
||||
if (val > 0) {
|
||||
let dt = new Date(val);
|
||||
let year = dt.getFullYear();
|
||||
let month = dt.getMonth() + 1;
|
||||
let date = dt.getDate();
|
||||
let hour = dt.getHours();
|
||||
let minute = dt.getMinutes();
|
||||
let second = dt.getSeconds();
|
||||
return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
queryData() {
|
||||
request({
|
||||
url: '/repository/list',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.list;
|
||||
_this.result.total = res.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
let id = item.id;
|
||||
request({
|
||||
url: `/repository/delete/${id}`,
|
||||
method: 'get',
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "新增";
|
||||
_this.cmd = {
|
||||
name: '',
|
||||
gitUrl: '',
|
||||
userName: '',
|
||||
password: '',
|
||||
remarks: ''
|
||||
};
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "编辑";
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.cmd.modifyTimeMillis = null;
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
submitModify() {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
if (_this.cmd.id) {
|
||||
request({
|
||||
url: '/repository/update',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("编辑成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
} else { // 新建
|
||||
request({
|
||||
url: '/repository/save',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("新增成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -1,357 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
离散知识补充
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<span>图谱选择:</span>
|
||||
<el-select v-model="activeSpace" placeholder="请选择要进行补充的图谱" @change="changeSpace">
|
||||
<el-option
|
||||
v-for="(item, index) in spaceList"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="index">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-upload
|
||||
style="display: inline-block;margin-left: 20px"
|
||||
class="upload-demo"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
action="#"
|
||||
multiple
|
||||
:limit="3"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList">
|
||||
<el-button type="primary" title='上传离散知识文件'>上传数据</el-button>
|
||||
</el-upload>
|
||||
<el-button type="primary" style="margin-left: 20px" @click="showResult" :disabled="!uploadSuccess">执行补充</el-button>
|
||||
</div>
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="10">
|
||||
<el-descriptions class="margin-top" title="补充结果" :column="1" border>
|
||||
<el-descriptions-item label="补充时间">{{ resultCmd.time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="目标图谱">{{ resultCmd.space }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件名">{{ resultCmd.fileName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="包含知识数量">{{ resultCmd.allCounts }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成功知识占比">{{ resultCmd.cgzb }}</el-descriptions-item>
|
||||
<el-descriptions-item label="补充成功率">{{ resultCmd.cgl }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重叠知识占比">{{ resultCmd.cdzb }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重叠识别准确率">{{ resultCmd.cdl }}</el-descriptions-item>
|
||||
<el-descriptions-item label="冲突知识占比">{{ resultCmd.ctzb }}</el-descriptions-item>
|
||||
<el-descriptions-item label="冲突识别准确率">{{ resultCmd.ctl }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-descriptions class="margin-top" title="详细结果" :column="1" border>
|
||||
</el-descriptions>
|
||||
<el-table :data="resultList" style="width: 100%" height="500">
|
||||
<el-table-column prop="zyz" :label="tableTitle[0]"></el-table-column>
|
||||
<el-table-column prop="wyz" :label="tableTitle[1]"></el-table-column>
|
||||
<el-table-column prop="byz" :label="tableTitle[2]"></el-table-column>
|
||||
<el-table-column prop="bq" label="标签" width="100">
|
||||
<template slot-scope="scope">
|
||||
<label :style="{color:scope.row.bq=='成功'?'#67c23a':scope.row.bq=='冲突'?'#f56c6c':'#e6a23c'}">{{scope.row.bq}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bq" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<label :style="{color:scope.row.bq=='成功'?'#67c23a':scope.row.bq=='冲突'?'#f56c6c':'#e6a23c'}">{{scope.row.bq}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!--
|
||||
<div class="menu-content-tab">
|
||||
<div>
|
||||
<div>手动添加</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span>主语类型:</span>
|
||||
<el-select v-model="activeTag" placeholder="请选择主语类型">
|
||||
<el-option
|
||||
v-for="item in tagList"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<span>主语取值:</span>
|
||||
<el-input v-model="tagValue" placeholder="请输入主语取值"></el-input>
|
||||
</div>
|
||||
<div>
|
||||
<span>谓语类型:</span>
|
||||
<el-radio v-model="edgeType" label="1" @change="radioChange">关系属性</el-radio>
|
||||
<el-radio v-model="edgeType" label="2" @change="radioChange">数值属性</el-radio>
|
||||
</div>
|
||||
<div v-show="edgeType === '1'">
|
||||
<span>谓语取值:</span>
|
||||
<el-select v-model="activeEdge" placeholder="请选择谓语取值">
|
||||
<el-option
|
||||
v-for="item in edgeList"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div v-show="edgeType === '2'">
|
||||
<span>谓语取值:</span>
|
||||
<el-select v-model="activeEdge" placeholder="请选择谓语取值">
|
||||
<el-option
|
||||
v-for="item in tagField"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<span>宾语类型:</span>
|
||||
<el-select v-model="activeDataType" placeholder="请选择宾语类型">
|
||||
<el-option
|
||||
v-for="item in dataTypeList"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<span>宾语取值:</span>
|
||||
<el-input v-model="dataTypeValue" placeholder="请输入宾语取值"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-content2">
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<p>
|
||||
<span>补充时间:</span>
|
||||
<span>{{ resultCmd.time }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>目标图谱:</span>
|
||||
<span>{{ resultCmd.space }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>重叠识别准确率:</span>
|
||||
<span>{{ resultCmd.repeatRate }}%</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>冲突识别准确率:</span>
|
||||
<span>{{ resultCmd.conflictRate }}%</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import {formatTime} from '@/utils/common';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "dataRepeatRate",
|
||||
data() {
|
||||
return {
|
||||
spaceList: [],
|
||||
activeSpace: '',
|
||||
dataTypeList: [],
|
||||
activeDataType: '',
|
||||
dataTypeValue: '',
|
||||
tagList: [],
|
||||
activeTag: '',
|
||||
tagValue: '',
|
||||
edgeType: '1',
|
||||
tagField: [],
|
||||
edgeList: [],
|
||||
activeEdge: '',
|
||||
edgeValue: '',
|
||||
resultCmd1: {},
|
||||
resultCmd: {
|
||||
repeatRate: '0',
|
||||
conflictRate: '0',
|
||||
space: '',
|
||||
time: ''
|
||||
},
|
||||
list: [],
|
||||
resultList: [],
|
||||
ontologyList: [],
|
||||
AtlasSelection: '',
|
||||
fileList: [],
|
||||
edges: [],
|
||||
radio: "1",
|
||||
uploadSuccess: false,
|
||||
tableTitle:["头实体","关系","尾实体"]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
radioChange() {
|
||||
_this.activeEdge = '';
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/indicator/indicator3_1',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
if (res.data.space.length > 0) {
|
||||
_this.activeSpace = 0;
|
||||
}
|
||||
|
||||
_this.spaceList = res.data.space;
|
||||
_this.dataTypeList = res.data.datatype;
|
||||
_this.tagList = res.data.tag;
|
||||
_this.tagField = res.data.tag_field;
|
||||
_this.edgeList = res.data.edge;
|
||||
});
|
||||
},
|
||||
changeSpace(){
|
||||
if(_this.spaceList[_this.activeSpace].name.indexOf("实例") != -1){
|
||||
_this.tableTitle = ["头实体","关系","尾实体"]
|
||||
}else{
|
||||
_this.tableTitle = ["概念","属性","概念"]
|
||||
}
|
||||
},
|
||||
handleFileChange(file) {
|
||||
_this.fileList = [file];
|
||||
let fileName = file.name;
|
||||
let activeSpace = _this.spaceList[_this.activeSpace];
|
||||
if (activeSpace.id !== fileName) {
|
||||
_this.$message.warning("上传文件与" + activeSpace.name + "不符");
|
||||
return false;
|
||||
}
|
||||
|
||||
_this.uploadFile(file);
|
||||
},
|
||||
showResult() {
|
||||
_this.$message.info("开始执行补充");
|
||||
setTimeout(() => {
|
||||
_this.resultCmd = _this.resultCmd1;
|
||||
_this.resultList = _this.list;
|
||||
_this.$message.success("补充成功");
|
||||
}, 1233);
|
||||
},
|
||||
uploadFile(fileItem) {
|
||||
_this.uploadSuccess = false;
|
||||
_this.$message.info("开始上传");
|
||||
let formData = new FormData();
|
||||
formData.append("file", fileItem.raw);
|
||||
request({
|
||||
url: '/indicator/indicator3',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.resultCmd1 = res.data;
|
||||
_this.resultCmd1.space = _this.spaceList[_this.activeSpace].name;
|
||||
_this.resultCmd1.fileName = fileItem.name;
|
||||
_this.resultCmd1.time = formatTime(new Date());
|
||||
_this.list = res.data.items;
|
||||
_this.$message.success("上传成功");
|
||||
_this.uploadSuccess = true;
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
//overflow: hidden;
|
||||
//display: flex;
|
||||
//justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-content2 {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(1) {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(1) > div {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #EBEEF5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.20833vw 0.20833vw 0 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.72917vw;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(1) > div:nth-of-type(2) {
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(1) > div:nth-of-type(2) .el-upload {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(1) > div:nth-of-type(2) button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(2) {
|
||||
width: 80%;
|
||||
border: 1px solid #EBEEF5;
|
||||
box-sizing: border-box;
|
||||
border-top: none;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(2) > div > .el-select, .menu-content-tab > div:nth-of-type(2) > div > .el-input {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.menu-content-tab > div:nth-of-type(2) > div {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.menu-content2 > div:nth-of-type(1) {
|
||||
font-size: 0.72917vw;
|
||||
}
|
||||
|
||||
.menu-content2 > div:nth-of-type(1) > p {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.menu-content2 > div:nth-of-type(1) > p > span:nth-of-type(1) {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,174 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
低资源场景知识抽取
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div style="margin: 0px 0 20px 0;overflow: hidden">
|
||||
<el-upload
|
||||
style="float: left;margin-right: 10px"
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:limit="3"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList">
|
||||
<el-button type="primary" title='上传“低资源”场景测试数据'>上传数据</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="menu-table">
|
||||
<el-table :data="resultCmd.items" style="width: 60%" height="350">
|
||||
<el-table-column prop="text" align="center" label="文本"></el-table-column>
|
||||
<el-table-column prop="spo[0]" align="center" label="头实体"></el-table-column>
|
||||
<el-table-column prop="spo[1]" align="center" label="关系"></el-table-column>
|
||||
<el-table-column prop="spo[2]" align="center" label="尾实体"></el-table-column>
|
||||
</el-table>
|
||||
<div id="echart"></div>
|
||||
</div>
|
||||
<div style="margin: 20px 0 20px 0">
|
||||
<el-button type="primary" title='开始“低资源”场景知识抽取' @click="validateData">开始验证</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="resultCmd1.items" style="width: 100%" height="350">
|
||||
<el-table-column prop="text" align="center" label="文本"></el-table-column>
|
||||
<el-table-column prop="spo[0]" align="center" label="头实体"></el-table-column>
|
||||
<el-table-column prop="spo[1]" align="center" label="关系"></el-table-column>
|
||||
<el-table-column prop="spo[2]" align="center" label="尾实体"></el-table-column>
|
||||
<el-table-column align="center" label="预测结果">
|
||||
<template slot-scope="scope">
|
||||
<label
|
||||
:style="scope.row.ycspoflag?'':'color:red'">({{ scope.row.ycspo[0] }},{{ scope.row.ycspo[1] }},{{ scope.row.ycspo[2] }})</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="result" v-if="resultCmd1.zql ">“低资源”场景知识抽取结果: 准确率{{
|
||||
resultCmd1.zql
|
||||
}},正确条数{{ resultCmd1.trueTotal }},总条数{{ resultCmd1.total }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "company",
|
||||
data() {
|
||||
return {
|
||||
fileList: [],
|
||||
resultCmd: {},
|
||||
resultCmd1: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(file) {
|
||||
_this.fileList = [file];
|
||||
_this.uploadFile(file.raw);
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/indicator/indicator1_1',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.resultCmd = res.data;
|
||||
_this.initEchart();
|
||||
_this.validateData();
|
||||
});
|
||||
},
|
||||
uploadFile(fileItem) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", fileItem);
|
||||
request({
|
||||
url: '/indicator/indicator1',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.resultCmd = res.data;
|
||||
_this.resultCmd1 = {};
|
||||
_this.initEchart();
|
||||
});
|
||||
},
|
||||
validateData() {
|
||||
_this.resultCmd1 = _this.resultCmd
|
||||
_this.resultCmd1.total = _this.resultCmd1.items.length;
|
||||
_this.resultCmd1.trueTotal = 0
|
||||
_this.resultCmd1.items.forEach(item => {
|
||||
if (item.ycspoflag) {
|
||||
_this.resultCmd1.trueTotal++;
|
||||
}
|
||||
})
|
||||
},
|
||||
initEchart() {
|
||||
var xAxis = [];
|
||||
var data = []
|
||||
for (var key in _this.resultCmd.relations) {
|
||||
xAxis.push(key);
|
||||
data.push(_this.resultCmd.relations[key])
|
||||
}
|
||||
|
||||
var chart = this.$echarts.init(document.getElementById("echart"));
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis,
|
||||
axisLabel: {interval: 0, rotate: 30}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: data,
|
||||
type: 'bar'
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
#echart {
|
||||
width: 40%;
|
||||
height: 350px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.menu-table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-table th div, .menu-table td div {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,585 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
数据集管理
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入任务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">新建任务</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table" @expand-change="showDataSetListByTaskId">
|
||||
<el-table-column type="expand">
|
||||
<template slot-scope="scope">
|
||||
<el-row>
|
||||
<el-col :span="8" v-for="item in scope.row.tasks" :key="item.id">
|
||||
<el-form label-position="left" inline class="demo-table-expand" label-width="85px">
|
||||
<el-form-item label="数据集类型">
|
||||
<span>{{ item.taskTypeName }}</span>
|
||||
<span v-if="item.taskId == null">(暂无数据)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总记录数">
|
||||
<span>{{ item.icount }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="实体数量">
|
||||
<span>{{ item.ecount }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="关系数量">
|
||||
<span>{{ item.rcount }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作">
|
||||
<el-button type="text" size="small" @click="showImportDialog(item, scope.row)">
|
||||
导入
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="showDataSetViewDialog(item, scope.row)"
|
||||
v-if="item.taskId != null">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="markTxt(item, scope.row)"
|
||||
v-if="item.taskId != null">
|
||||
标注
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="downloadFile(item, scope.row)"
|
||||
v-if="item.taskId != null">
|
||||
导出
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="removeDataSet(item, scope.row)"
|
||||
v-if="item.taskId != null">
|
||||
删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="任务名称"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button title="将其他知识图谱本体三元组导入当前数据集中"
|
||||
@click.native.prevent="showImportNodeDialog(scope.row)"
|
||||
type="text" size="small">
|
||||
导入知识图谱本体
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="新建"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入任务名称"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="相关描述" prop="name">-->
|
||||
<!-- <el-input v-model="cmd.description" placeholder="请输入相关描述" type="textarea"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="验证集文件">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload2"
|
||||
action="#"
|
||||
:on-change="handleValidationFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".txt"
|
||||
:limit="10"
|
||||
:file-list="fileList2">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">请选择一个格式为.txt的文本文件进行上传</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="训练集文件">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload3"
|
||||
action="#"
|
||||
:on-change="handleTrainingFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".txt"
|
||||
:limit="10"
|
||||
:file-list="fileList3">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">请选择一个格式为.txt的文本文件进行上传</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitModify">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="导入知识图谱本体"
|
||||
:visible.sync="importNodeDialogVisible"
|
||||
width="40%">
|
||||
<el-table :data="ontologyList" style="width: 100%">
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="submitImportNode(scope.row)" type="text" size="small"
|
||||
v-if="cmd.srcOntologyId != scope.row.id">
|
||||
导入
|
||||
</el-button>
|
||||
<label v-else>自身</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="importNodeDialogVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="导入"
|
||||
:visible.sync="importDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :model="cmd">
|
||||
<el-form-item label="数据集类型" prop="name">
|
||||
<el-select v-model="cmd.taskType" disabled>
|
||||
<el-option :label="item.name" :value="item.code" v-for="item in taskTypeList" :key="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择文件" prop="name">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload"
|
||||
action="#"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".txt"
|
||||
:limit="10"
|
||||
:file-list="fileList">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">请选择一个格式为.txt的文本文件进行上传</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="importDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitImport">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="查看"
|
||||
:visible.sync="viewDialogVisible"
|
||||
width="60%">
|
||||
<div>
|
||||
<el-table :data="dataSetResult.records" style="width: 100%">
|
||||
<el-table-column type="index" label="行号" width="80"></el-table-column>
|
||||
<el-table-column prop="content" label="文本内容">
|
||||
<template slot-scope="scope">
|
||||
<label
|
||||
:title="scope.row.content">{{
|
||||
scope.row.content.length > 45 ? (scope.row.content.substring(0, 45) + '...') : scope.row.content
|
||||
}}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="remarkText(scope.row, scope.$index)" type="text" size="small">
|
||||
标注
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeDataSetView(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleDataSetCurrentChange"
|
||||
:current-page="dataSetQo.pageNo"
|
||||
:page-size="dataSetQo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="dataSetResult.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="viewDialogVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request, {getBaseUrl} from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "company",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
dataSetQo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
taskId: '',
|
||||
taskType: ''
|
||||
},
|
||||
dataSetResult: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
ontologyList: [],
|
||||
taskTypeList: [{
|
||||
name: "测试集",
|
||||
code: 1,
|
||||
eName: "test"
|
||||
}, {
|
||||
name: "验证集",
|
||||
code: 2,
|
||||
eName: "validation"
|
||||
}, {
|
||||
name: "训练集",
|
||||
code: 3,
|
||||
eName: "training"
|
||||
}],
|
||||
dialogName: '编辑',
|
||||
cmdDialogVisible: false,
|
||||
viewDialogVisible: false,
|
||||
importDialogVisible: false,
|
||||
importNodeDialogVisible: false,
|
||||
cmd: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入任务名称'}
|
||||
],
|
||||
ontologyId: [
|
||||
{required: true, message: '本体标签库'}
|
||||
],
|
||||
},
|
||||
fileList: [],
|
||||
fileList2: [],
|
||||
fileList3: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryOntologyList();
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
queryData() {
|
||||
request({
|
||||
url: '/remark_task/query_pages',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.data.records;
|
||||
_this.result.total = res.data.total;
|
||||
if (res.data.records.length > 0) {
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['table'].toggleRowExpansion(res.data.records[0], true);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
downloadFile(item, row) { // 导出
|
||||
window.open(getBaseUrl() + `/es_remark_task/export/${row.id}/${item.taskTypeCode}`);
|
||||
},
|
||||
showImportNodeDialog(item) { // 打开复制图模型的列表弹窗
|
||||
_this.queryOntologyList();
|
||||
_this.cmd = {
|
||||
srcOntologyId: item.ontologyId,
|
||||
dictOntologyId: ''
|
||||
};
|
||||
_this.importNodeDialogVisible = true;
|
||||
},
|
||||
submitImportNode(item) {
|
||||
_this.cmd.dictOntologyId = item.id;
|
||||
request({
|
||||
url: '/noumenon/copy_by_ontologyid',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("导入成功");
|
||||
});
|
||||
},
|
||||
queryOntologyList() {
|
||||
request({
|
||||
url: '/ontology/query_list',
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.ontologyList = res.data;
|
||||
});
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/remark_task/removeremarktask',
|
||||
method: 'post',
|
||||
data: {
|
||||
remarkId: item.id,
|
||||
ontologyId: item.ontologyId
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
_this.queryOntologyList();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.cmd = {
|
||||
name: '',
|
||||
};
|
||||
_this.cmdDialogVisible = true;
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.cmdDialogVisible = true;
|
||||
},
|
||||
showImportDialog(dataSet, task) {
|
||||
_this.cmd = {
|
||||
taskId: task.id,
|
||||
taskType: dataSet.taskTypeCode
|
||||
}
|
||||
_this.fileList = [];
|
||||
_this.importDialogVisible = true;
|
||||
},
|
||||
handleFileChange(file) {
|
||||
_this.$refs['upload'].clearFiles();
|
||||
_this.fileList = [file];
|
||||
},
|
||||
handleValidationFileChange(file) {
|
||||
_this.$refs['upload2'].clearFiles();
|
||||
_this.fileList2 = [file];
|
||||
},
|
||||
handleTrainingFileChange(file) {
|
||||
_this.$refs['upload3'].clearFiles();
|
||||
_this.fileList3 = [file];
|
||||
},
|
||||
submitModify() {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
let formData = new FormData();
|
||||
formData.append("name", _this.cmd.name);
|
||||
formData.append("validationFile", _this.fileList2[0].raw);
|
||||
formData.append("trainingFile", _this.fileList3[0].raw);
|
||||
|
||||
// if (_this.cmd.id) {
|
||||
// request({
|
||||
// url: '/remark_task/modify',
|
||||
// method: 'post',
|
||||
// data: _this.cmd
|
||||
// }).then(res => {
|
||||
// _this.$message.success("编辑成功");
|
||||
// _this.queryData();
|
||||
// _this.cmdDialogVisible = false;
|
||||
// });
|
||||
// } else { // 新建
|
||||
request({
|
||||
url: '/remark_task/create_with_file',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.$message.success("添加成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
// }
|
||||
}
|
||||
});
|
||||
},
|
||||
showDataSetListByTaskId(row, expanded) {
|
||||
if (row.tasks) {
|
||||
return;
|
||||
}
|
||||
request({
|
||||
url: '/es_remark_task/findesremarkinfosticdto/' + row.id,
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
let tasks = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
tasks.push({
|
||||
ecount: 0,
|
||||
icount: 0,
|
||||
rcount: 0,
|
||||
taskTypeName: _this.taskTypeList[i].name,
|
||||
taskTypeEName: _this.taskTypeList[i].eName,
|
||||
taskTypeCode: i + 1
|
||||
});
|
||||
}
|
||||
res.data.forEach(item => {
|
||||
let index = tasks.findIndex(row => {
|
||||
return row.taskTypeEName == item.type;
|
||||
});
|
||||
if (tasks[index]) {
|
||||
tasks[index].ecount = item.ecount;
|
||||
tasks[index].icount = item.icount;
|
||||
tasks[index].rcount = item.rcount;
|
||||
tasks[index].taskId = item.taskId;
|
||||
}
|
||||
});
|
||||
row.tasks = tasks;
|
||||
});
|
||||
},
|
||||
submitImport() {
|
||||
if (_this.fileList.length == 0) {
|
||||
_this.$message.warning("请选择一个文本文件进行上传");
|
||||
return;
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("taskId", _this.cmd.taskId);
|
||||
formData.append("file", _this.fileList[0].raw);
|
||||
formData.append("taskType", _this.cmd.taskType);
|
||||
request({
|
||||
url: '/es_remark_task/upload_task',
|
||||
method: 'post',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.$message.success("导入成功");
|
||||
_this.reloadDataSet(_this.cmd.taskId);
|
||||
_this.importDialogVisible = false;
|
||||
});
|
||||
},
|
||||
reloadDataSet(taskId) {
|
||||
let index = _this.result.records.findIndex(row => {
|
||||
return row.id == taskId;
|
||||
});
|
||||
_this.result.records[index].tasks = null;
|
||||
_this.showDataSetListByTaskId(_this.result.records[index]);
|
||||
},
|
||||
showDataSetViewDialog(dataSet, task) { // 打开查看弹窗,保存一些父级数据集信息
|
||||
_this.dataSetQo = {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
taskId: task.id,
|
||||
taskType: dataSet.taskTypeCode,
|
||||
ontologyId: task.ontologyId
|
||||
}
|
||||
_this.queryDataSetViewData();
|
||||
},
|
||||
queryDataSetViewData() {
|
||||
request({
|
||||
url: '/es_remark_task/findbytaskidandtasktype',
|
||||
method: 'post',
|
||||
data: _this.dataSetQo
|
||||
}).then(res => {
|
||||
_this.dataSetResult.records = res.data.content;
|
||||
_this.dataSetResult.total = res.data.totalElements;
|
||||
_this.viewDialogVisible = true;
|
||||
});
|
||||
},
|
||||
handleDataSetCurrentChange(val) {
|
||||
_this.dataSetQo.pageNo = val;
|
||||
_this.queryDataSetViewData();
|
||||
},
|
||||
removeDataSet(item, task) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/es_remark_task/deletebyqo',
|
||||
method: 'post',
|
||||
data: {
|
||||
taskId: task.id,
|
||||
taskType: item.taskTypeCode
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.reloadDataSet(task.id);
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
removeDataSetView(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/es_remark_task/delete/' + item.id,
|
||||
method: 'post'
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryDataSetViewData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
markTxt(item, task) {
|
||||
this.$router.push({
|
||||
path: '/textTag',
|
||||
query: {taskId: task.id, taskTypeCode: item.taskTypeCode, ontologyId: task.ontologyId}
|
||||
});
|
||||
},
|
||||
remarkText(row, index) { // 跳转到标注页,根据序号
|
||||
let sort = (_this.dataSetQo.pageNo - 1) * _this.dataSetQo.pageSize + index + 1;
|
||||
this.$router.push({
|
||||
path: '/textTag',
|
||||
query: {sort: sort, taskId: _this.dataSetQo.taskId, taskTypeCode: _this.dataSetQo.taskType, ontologyId: _this.dataSetQo.ontologyId}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.demo-table-expand {
|
||||
font-size: 0;
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
.demo-table-expand label {
|
||||
width: 90px;
|
||||
color: #99a9bf;
|
||||
}
|
||||
|
||||
.demo-table-expand .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
width: 80%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,515 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识图谱质量评估
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item label="选择待评估图谱:">
|
||||
<el-select v-model="value" placeholder="选择待评估图谱" @change="handleSelectChange">
|
||||
<el-option
|
||||
v-for="item in spaceList"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="indicator5(1)">开始采样</el-button>
|
||||
<el-button type="primary" icon="el-icon-search" @click="indicator5(2)">开始质量评估</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-if="tableShow == 1">
|
||||
<div class="result1">评估结果:</div>
|
||||
<el-table :data="indicator5Data.zb1" style="width: 60%">
|
||||
<el-table-column prop="sign1" label="指标">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[0] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sign2" label="采样后的知识图谱">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[1] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sign3" label="原始的知识图谱">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[2] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table :data="indicator5Data.zb2" style="width: 60%;margin-top: 40px">
|
||||
<el-table-column prop="sign1" label="指标">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[0] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sign2" label="采样后的知识图谱">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[1] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sign3" label="原始的知识图谱">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row[2] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="result1" style="margin-top: 20px" v-if="cai.length > 0">
|
||||
<!-- 使用采样数据集 预估评估误差{{indicator5Data.wc}}-->
|
||||
温馨小贴士:<br/><br/>
|
||||
{{ cai }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tableShow == 2">
|
||||
<div class="ping-box" v-if="ping.length > 0">
|
||||
温馨小贴士:<br/><br/>
|
||||
{{ ping }}
|
||||
</div>
|
||||
<el-row :gutter="20" style="margin-top: 30px">
|
||||
<el-col :span="15">
|
||||
<p>知识图谱评估结果整体展示</p>
|
||||
<div id="echart"></div>
|
||||
|
||||
</el-col>
|
||||
<el-col :span="9">
|
||||
<div>
|
||||
<img :src="fileUrl+'/'+indicator5Data.rlt" style="width: 100%;margin-top: 30px" alt="">
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div style="margin-top: 30px;overflow: hidden">
|
||||
<el-table :data="indicator5Data.tjxx" style="width: 100%" border :span-method="objectSpanMethod">
|
||||
<el-table-column prop="weidu" label="维度" align="center"></el-table-column>
|
||||
<el-table-column prop="csbs" label="标识符" align="center">
|
||||
<template slot-scope="scope">
|
||||
<label v-if="scope.row.csbs">{{ scope.row.csbs }}</label>
|
||||
<label :style="scope.row.zhibiao.indexOf('总得分') != -1?'font-weight: bold;':''"
|
||||
v-else>{{ scope.row.zhibiao }}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="zhibiao" label="指标" align="center">
|
||||
<template slot-scope="scope">
|
||||
<label v-if="scope.row.zhibiao.indexOf('总得分') == -1">{{ scope.row.zhibiao }}</label>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="val" label="评估值" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!--<el-row :gutter="20" style="margin-top: 30px;overflow: hidden">-->
|
||||
<!--<div style="float: left;width: 30%">-->
|
||||
<!--<p>时效性维度</p>-->
|
||||
<!--<el-table :data="indicator5Data.arr1" style="width: 100%">-->
|
||||
<!--<el-table-column prop="sign1" label="时效性评估指标">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.zhibiao}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--<el-table-column prop="sign2" label="评估值">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.val}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--<div style="float: left;width: 30%;margin: 0 5%">-->
|
||||
<!--<p>语法有效性维度</p>-->
|
||||
<!--<el-table :data="indicator5Data.arr2" style="width: 100%">-->
|
||||
<!--<el-table-column prop="sign1" label="语法有效性评估指标">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.zhibiao}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--<el-table-column prop="sign2" label="评估值">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.val}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--<div style="float: left;width: 30%">-->
|
||||
<!--<p>一致性维度</p>-->
|
||||
<!--<el-table :data="indicator5Data.arr3" style="width: 100%">-->
|
||||
<!--<el-table-column prop="sign1" label="一致性评估指标">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.zhibiao}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--<el-table-column prop="sign2" label="评估值">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.val}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--</el-row>-->
|
||||
<!--<el-row :gutter="20" style="margin-top: 30px;overflow: hidden">-->
|
||||
<!--<div style="float: left;width: 30%">-->
|
||||
<!--<p>简洁性维度</p>-->
|
||||
<!--<el-table :data="indicator5Data.arr4" style="width: 100%">-->
|
||||
<!--<el-table-column prop="sign1" label="简洁性评估指标">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.zhibiao}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--<el-table-column prop="sign2" label="评估值">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.val}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--<div style="float: left;width: 30%;margin: 0 5%">-->
|
||||
<!--<p>完整性维度</p>-->
|
||||
<!--<el-table :data="indicator5Data.arr5" style="width: 100%">-->
|
||||
<!--<el-table-column prop="sign1" label="完整性评估指标">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.zhibiao}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--<el-table-column prop="sign2" label="评估值">-->
|
||||
<!--<template slot-scope="scope">-->
|
||||
<!--<span>{{scope.row.val}}</span>-->
|
||||
<!--</template>-->
|
||||
<!--</el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--</el-row>-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request, {getFileUrl} from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "graphAssessment",
|
||||
data() {
|
||||
return {
|
||||
spaceList: [],
|
||||
fileUrl: getFileUrl(),
|
||||
value: '',
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
EQI_sourceType: 1
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 1
|
||||
},
|
||||
indicator5Data: {},
|
||||
tableShow: '',
|
||||
spanArr: [],
|
||||
liArr: [],
|
||||
cai: '',
|
||||
ping: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
queryData() {
|
||||
request({
|
||||
url: '/indicator/indicator5_1',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.spaceList = res.data;
|
||||
_this.value = _this.spaceList[0].name;
|
||||
_this.indicator5(1);
|
||||
});
|
||||
},
|
||||
handleSelectChange() {
|
||||
_this.cai = "";
|
||||
_this.ping = "";
|
||||
},
|
||||
indicator5(type) {
|
||||
_this.tableShow = type;
|
||||
request({
|
||||
url: '/indicator/indicator5/' + _this.value,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
if (type === 1) {
|
||||
if (_this.value === "DBPedia") {
|
||||
_this.cai = "从原始DBPedia知识图谱中抽样了6,067,531条知识得到采样后的DBPedia知识图谱,评估工作量降低了99.6%。";
|
||||
} else if (_this.value === "WikiData") {
|
||||
_this.cai = "从原始WikiData知识图谱中抽样了1,771,981条知识得到采样后的WikiData知识图谱,评估工作量降低了90.9%。";
|
||||
} else {
|
||||
_this.cai = "";
|
||||
}
|
||||
} else if (type === 2) {
|
||||
if (_this.value === "DBPedia") {
|
||||
_this.ping = "对抽样后的DBPedia知识图谱进行评估,其知识准确率为94.6%,知识准确率评估误差为0.04%。";
|
||||
} else if (_this.value === "WikiData") {
|
||||
_this.ping = "对抽样后的WikiData知识图谱进行评估,其知识准确率为85.7%,知识准确率评估误差为0.07%。";
|
||||
} else {
|
||||
_this.ping = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let arr1 = [];
|
||||
let arr2 = [];
|
||||
let arr3 = [];
|
||||
let arr4 = [];
|
||||
let arr5 = [];
|
||||
res.data.tjxx.map(data1 => {
|
||||
if (data1.weidu == "时效性") {
|
||||
arr1.push(data1);
|
||||
} else if (data1.weidu == "语法有效性") {
|
||||
arr2.push(data1);
|
||||
} else if (data1.weidu == "一致性") {
|
||||
arr3.push(data1);
|
||||
} else if (data1.weidu == "简洁性") {
|
||||
arr4.push(data1);
|
||||
} else if (data1.weidu == "完整性") {
|
||||
arr5.push(data1);
|
||||
}
|
||||
})
|
||||
_this.indicator5Data = res.data;
|
||||
_this.indicator5Data.arr1 = arr1;
|
||||
_this.indicator5Data.arr2 = arr2;
|
||||
_this.indicator5Data.arr3 = arr3;
|
||||
_this.indicator5Data.arr4 = arr4;
|
||||
_this.indicator5Data.arr5 = arr5;
|
||||
_this.getSpanId(res.data.tjxx);
|
||||
if (type == 2) {
|
||||
_this.initEchart();
|
||||
}
|
||||
});
|
||||
},
|
||||
getSpanId(data) {
|
||||
this.spanArr = []
|
||||
this.liArr = []
|
||||
// data就是我们从后台拿到的数据
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (i === 0) {
|
||||
//spanArr 用于存放没一行记录的合并数
|
||||
this.spanArr.push(1);
|
||||
//pos是spanArr的索引
|
||||
this.pos = 0;
|
||||
} else {
|
||||
// 判断当前元素与上一个元素是否相同
|
||||
if (data[i].weidu === data[i - 1].weidu) {
|
||||
this.spanArr[this.pos] += 1;
|
||||
this.spanArr.push(0);
|
||||
} else {
|
||||
this.spanArr.push(1);
|
||||
this.pos = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (data[i].zhibiao.indexOf('总得分') != -1) {
|
||||
this.liArr.push(i)
|
||||
}
|
||||
}
|
||||
console.log(this.liArr)
|
||||
},
|
||||
objectSpanMethod({row, column, rowIndex, columnIndex}) {
|
||||
if (columnIndex === 0) {
|
||||
|
||||
const _row = this.spanArr[rowIndex];
|
||||
//如果行号大于0 合并
|
||||
const _col = _row > 0 ? 1 : 0;
|
||||
// console.log(`rowspan:${_row} colspan:${_col}`);
|
||||
return {
|
||||
// [0,0] 表示这一行不显示,
|
||||
rowspan: _row,
|
||||
colspan: _col
|
||||
};
|
||||
}
|
||||
|
||||
if (this.liArr.indexOf(rowIndex) != -1) {
|
||||
if (columnIndex === 1) {
|
||||
return [1, 2];
|
||||
} else if (columnIndex === 2) {
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
},
|
||||
handleFileChange(file) {
|
||||
_this.fileList = [file];
|
||||
},
|
||||
// queryData() {
|
||||
//
|
||||
// if(_this.value == 1) {
|
||||
// _this.result.records = [ {
|
||||
// sign1: '93%',
|
||||
// sign2: '45%',
|
||||
// sign3: '81%',
|
||||
// }]
|
||||
// } else {
|
||||
// _this.result.records = [ {
|
||||
// sign1: '90%',
|
||||
// sign2: '40%',
|
||||
// sign3: '88%',
|
||||
// }]
|
||||
// }
|
||||
// },
|
||||
initEchart() {
|
||||
let chart = _this.$echarts.init(document.getElementById("echart"));
|
||||
let echartArr1 = [];
|
||||
let echartArr2 = [];
|
||||
let echartArr3 = [];
|
||||
let echartArr4 = [];
|
||||
if (_this.indicator5Data.ld1) {
|
||||
_this.indicator5Data.ld1.map(data => {
|
||||
echartArr1.push({text: data.name, max: 1});
|
||||
echartArr2.push(data.val);
|
||||
})
|
||||
}
|
||||
if (_this.indicator5Data.ld2) {
|
||||
_this.indicator5Data.ld2.map(data => {
|
||||
echartArr3.push({text: data.name});
|
||||
echartArr4.push(data.val);
|
||||
})
|
||||
}
|
||||
var option = {
|
||||
// color: ['#67F9D8', '#FFE434', '#56A3F1', '#FF917C'],
|
||||
// title: {
|
||||
// text: 'Customized Radar Chart'
|
||||
// },
|
||||
// legend: {},
|
||||
radar: [
|
||||
{
|
||||
indicator: echartArr3,
|
||||
center: ['75%', '50%'],
|
||||
radius: 120,
|
||||
startAngle: 90,
|
||||
splitNumber: 4,
|
||||
shape: 'circle',
|
||||
axisName: {
|
||||
formatter: '【{value}】',
|
||||
color: '#428BD4'
|
||||
},
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
// color: ['#77EADF', '#26C3BE', '#64AFE9', '#428BD4'],
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
// color: 'rgba(211, 253, 250, 0.8)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
// color: 'rgba(211, 253, 250, 0.8)'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
indicator: echartArr1,
|
||||
center: ['25%', '50%'],
|
||||
radius: 120,
|
||||
axisName: {
|
||||
color: '#fff',
|
||||
backgroundColor: '#666',
|
||||
borderRadius: 3,
|
||||
padding: [3, 5]
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
emphasis: {
|
||||
lineStyle: {
|
||||
width: 4
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: echartArr4,
|
||||
name: 'Data A'
|
||||
},
|
||||
// {
|
||||
// value: [60, 5, 0.3, -100, 1500],
|
||||
// name: 'Data B',
|
||||
// // areaStyle: {
|
||||
// // color: 'rgba(255, 228, 52, 0.6)'
|
||||
// // }
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'radar',
|
||||
radarIndex: 1,
|
||||
data: [
|
||||
{
|
||||
value: echartArr2,
|
||||
name: 'Data C',
|
||||
// symbol: 'rect',
|
||||
// symbolSize: 12,
|
||||
// lineStyle: {
|
||||
// type: 'dashed'
|
||||
// },
|
||||
// label: {
|
||||
// show: true,
|
||||
// formatter: function (params) {
|
||||
// return params.value;
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// {
|
||||
// value: [100, 93, 50, 90, 70, 60],
|
||||
// name: 'Data D',
|
||||
// // areaStyle: {
|
||||
// // color: new echarts.graphic.RadialGradient(0.1, 0.6, 1, [
|
||||
// // {
|
||||
// // color: 'rgba(255, 145, 124, 0.1)',
|
||||
// // offset: 0
|
||||
// // },
|
||||
// // {
|
||||
// // color: 'rgba(255, 145, 124, 0.9)',
|
||||
// // offset: 1
|
||||
// // }
|
||||
// // ])
|
||||
// // }
|
||||
// }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.resize()
|
||||
chart.setOption(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ping-box {
|
||||
position: absolute;
|
||||
right: 200px;
|
||||
top: 25px;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.result1 {
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
#echart {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
武器装备维修保障知识抽取
|
||||
</div>
|
||||
<!--<div class="menu-content">-->
|
||||
<!--<div style="height: 40px">-->
|
||||
<!--<el-upload-->
|
||||
<!--ref="upload"-->
|
||||
<!--class="upload-btn"-->
|
||||
<!--:auto-upload="false"-->
|
||||
<!--action="#"-->
|
||||
<!--:on-change="handleFileChange"-->
|
||||
<!--:limit="2"-->
|
||||
<!--:show-file-list="false">-->
|
||||
<!--<el-button type="primary">导入测试文件</el-button>-->
|
||||
<!--</el-upload>-->
|
||||
<!--<el-button type="primary" style="margin-left: 20px">开始测试</el-button>-->
|
||||
<!--<el-button style="float: right">三元组F1</el-button>-->
|
||||
<!--<el-button style="float: right">实体F1</el-button>-->
|
||||
<!--</div>-->
|
||||
<!--<div style="margin-top: 30px;height: calc(100% - 80px)">-->
|
||||
<!--<el-table :data="questionData" style="width: 100%;">-->
|
||||
<!--<el-table-column prop="question" label="句子"></el-table-column>-->
|
||||
<!--<el-table-column prop="answer" label="实体"></el-table-column>-->
|
||||
<!--<el-table-column prop="index" label="三元组"></el-table-column>-->
|
||||
<!--</el-table>-->
|
||||
<!--</div>-->
|
||||
<!--</div>-->
|
||||
<div class="menu-content">
|
||||
<div style="height: 40px">
|
||||
<el-input
|
||||
placeholder="请输入内容"
|
||||
v-model="keyword" style="width: 50%">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" style="float: right">故障原因推荐Top5准确率</el-button>
|
||||
</div>
|
||||
<div style="margin-top: 30px;height: calc(100% - 80px)">
|
||||
<el-table :data="questionData" style="width: 100%;">
|
||||
<el-table-column prop="question" label="故障原因"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "graphExhibition",
|
||||
data() {
|
||||
return {
|
||||
keyword: "",
|
||||
fileName: "",
|
||||
questionData: [{question: "问题1", answer: "答案1", index: 1},
|
||||
{question: "问题1", answer: "答案1", index: 1},
|
||||
{question: "问题1", answer: "答案1", index: 1},
|
||||
{question: "问题1", answer: "答案1", index: 1}
|
||||
],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(item) {
|
||||
this.$refs.upload.clearFiles();
|
||||
let formData = new FormData();
|
||||
formData.append("file", item.raw);
|
||||
_this.fileName = item.raw.name;
|
||||
// request({
|
||||
// url: '/sys-file/create',
|
||||
// method: 'put',
|
||||
// data: formData
|
||||
// }).then(res => {
|
||||
// _this.$message.success("上传成功");
|
||||
// _this.queryData();
|
||||
// });
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: calc(100% - 40px);
|
||||
height: 550px;
|
||||
overflow-y: auto;
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.text-title {
|
||||
width: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left-box {
|
||||
width: 50%;
|
||||
float: left;
|
||||
|
||||
.text-box-item {
|
||||
width: calc(90% - 1px);
|
||||
padding-right: 10%;
|
||||
border-right: 1px solid #c1c2c4;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
width: 50%;
|
||||
float: right;
|
||||
|
||||
.text-box-item {
|
||||
width: 90%;
|
||||
padding-left: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,429 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识图谱实例导入
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入任务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">创建任务</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table">
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="space" label="实例名称"></el-table-column>
|
||||
<el-table-column prop="name" label="任务名称"></el-table-column>
|
||||
<el-table-column prop="totle" label="文件个数"></el-table-column>
|
||||
<el-table-column prop="over" label="已导入个数"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="modifyRow(scope.row)" type="text" size="small">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="showRow(scope.row)" type="text" size="small">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="'查看-' + showTaskName"
|
||||
:visible.sync="showDialogVisible"
|
||||
width="60%">
|
||||
<!-- createTime: "2022-08-03 08:19:29"-->
|
||||
<!-- headerRow: null-->
|
||||
<!-- id: 1-->
|
||||
<!-- over: false-->
|
||||
<!-- originalFilename: "dn_graph_one_tag"-->
|
||||
<!-- path: "2022-08-03\\64e790821d872af8db27e781b6becc0e.xlsx"-->
|
||||
<!-- suffix: ".xlsx"-->
|
||||
<!-- taskId: 4-->
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="未导入" name="first">
|
||||
<el-table :data="cmd.records" style="width: 100%" ref="fileListTable"
|
||||
@selection-change="handleSelectionChange" default-expand-all>
|
||||
<el-table-column type="expand">
|
||||
<template slot-scope="scope">
|
||||
<el-form label-position="left" class="demo-table-expand">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="scope.row.type" @change="val => {handleTypeChange(val, scope.row)}">
|
||||
<el-option label="TAG" value="TAG"></el-option>
|
||||
<el-option label="EDGE" value="EDGE"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="TAG名称" v-show="scope.row.type == 'TAG'">
|
||||
<el-select v-model="scope.row.name" placeholder="请选择TAG名称" @change="val => {handleTagNameChange(val, scope.row)}">
|
||||
<el-option :label="item.Name" :value="item.Name" v-for="item in tagList"
|
||||
:key="item.Name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="EDGE名称" v-show="scope.row.type == 'EDGE'">
|
||||
<el-select v-model="scope.row.name" placeholder="请选择EDGE名称" @change="val => {handleEdgeNameChange(val, scope.row)}">
|
||||
<el-option :label="item.Name" :value="item.Name" v-for="item in edgeList"
|
||||
:key="item.Name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-show="scope.row.name">
|
||||
<el-form-item :label="field.field" v-for="field in scope.row.fieldList" :key="field.field">
|
||||
<el-select v-model="field.value" placeholder="请选择TAG名称">
|
||||
<el-option :label="row" :value="row" v-for="row in scope.row.headerRowList"
|
||||
:key="row"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55">
|
||||
</el-table-column>
|
||||
<el-table-column type="index" label="行号" width="80"></el-table-column>
|
||||
<el-table-column prop="originalFilename" label="源文件名称">
|
||||
<template slot-scope="scope">{{scope.row.originalFilename}}{{scope.row.suffix}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" ></el-table-column>
|
||||
<el-table-column prop="typename" label="名称" ></el-table-column>
|
||||
<!-- <el-table-column prop="path" label="路径" width="450"></el-table-column>-->
|
||||
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已导入" name="second">
|
||||
<el-table :data="cmd.records2" style="width: 100%" ref="fileListTable">
|
||||
<el-table-column type="index" label="行号" width="80"></el-table-column>
|
||||
<el-table-column prop="originalFilename" label="源文件名称"></el-table-column>
|
||||
<el-table-column prop="path" label="路径" width="450"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="showDialogVisible = false">关 闭</el-button>
|
||||
<el-button type="primary" @click="importRow" v-show="activeTab == 'first'">导 入</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="创建任务"
|
||||
:visible.sync="createDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="createCmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="实例名称" prop="space">
|
||||
<el-select v-model="cmd.space" placeholder="请选择实例名称">
|
||||
<el-option v-for="space in spaceList" :key="space.Name" :label="space.Name" :value="space.Name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件选择" prop="file">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:auto-upload="false"
|
||||
accept=".xls,.xlsx"
|
||||
action="#"
|
||||
:limit="10"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">只能上传xls/xlsx文件</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="createDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitCreate">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="编辑"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="实例名称">
|
||||
<el-input v-model="cmd.space" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitModify">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "codeWarehouse",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
spaceList: [], // 实例列表
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
showTaskName: '',
|
||||
createDialogVisible: false,
|
||||
cmdDialogVisible: false,
|
||||
showDialogVisible: false,
|
||||
cmd: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入任务名称'}
|
||||
],
|
||||
space: [
|
||||
{required: true, message: '请选择实例名称'}
|
||||
]
|
||||
},
|
||||
fileList: [], // 创建任务导入的文件列表
|
||||
importSelection: [], // 选中的需要执行导入的文件
|
||||
activeTab: 'first',
|
||||
tagList: [], // 任务space下的所有tag
|
||||
edgeList: [], // 任务space下的所有edge
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
_this.querySpaceList();
|
||||
},
|
||||
methods: {
|
||||
querySpaceList() { // 查询实例列表
|
||||
request({
|
||||
url: '/nebula_operate/showspace',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.spaceList = res.data;
|
||||
});
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/graph-task/query_pages',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
res.data.records.forEach(item => {
|
||||
item.totle = item.totle || 0;
|
||||
item.over = item.over || 0;
|
||||
});
|
||||
_this.result.records = res.data.records;
|
||||
_this.result.total = res.data.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: `/graph-task/remove/${item.id}`,
|
||||
method: 'post',
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.createDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.cmd = {
|
||||
"space": "",
|
||||
"name": "",
|
||||
"file": null
|
||||
};
|
||||
_this.fileList = [];
|
||||
_this.$refs.createCmd.resetFields();
|
||||
});
|
||||
},
|
||||
handleFileChange(file, fileList) {
|
||||
_this.cmd.file = file.raw;
|
||||
_this.fileList = [fileList.at(-1)];
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
submitCreate() {
|
||||
this.$refs.createCmd.validate((valid) => {
|
||||
if (valid) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", _this.cmd.file);
|
||||
formData.append("name", _this.cmd.name);
|
||||
request({
|
||||
url: `/nebula_graph_import/createtask/${_this.cmd.space}`,
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.$message.success("创建成功");
|
||||
_this.queryData();
|
||||
_this.createDialogVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
submitModify() {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
request({
|
||||
url: '/graph-task/modify',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("编辑成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
showRow(item) { // 打开查看
|
||||
_this.showTaskName = item.name;
|
||||
_this.importSelection = [];
|
||||
request({
|
||||
url: `/nebula_graph_import/findtaskbyid/${item.id}`,
|
||||
method: 'get',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
let _records = [], _records2 = [];
|
||||
_this.tagList = res.data.tags;
|
||||
_this.edgeList = res.data.edges;
|
||||
res.data.tfs.forEach(row => {
|
||||
if (row.over) {
|
||||
_records2.push(row);
|
||||
} else {
|
||||
row.headerRowList = JSON.parse(row.headerRow) || [];
|
||||
row.fieldList = [];
|
||||
row.type = row.type || "TAG"; // 默认TAG类型
|
||||
row.name = row.typename || "";
|
||||
if(row.name && row.type) {
|
||||
if(row.type === 'TAG') {
|
||||
_this.handleTagNameChange(row.typename, row);
|
||||
} else if(row.type === 'EDGE') {
|
||||
_this.handleEdgeNameChange(row.typename, row);
|
||||
}
|
||||
}
|
||||
_records.push(row);
|
||||
}
|
||||
});
|
||||
_this.cmd = {
|
||||
records: _records,
|
||||
records2: _records2
|
||||
};
|
||||
_this.showDialogVisible = true;
|
||||
});
|
||||
},
|
||||
handleTypeChange(val, row) {
|
||||
row.name = "";
|
||||
row.fieldList = [];
|
||||
},
|
||||
handleTagNameChange(val, row) {
|
||||
row.typename = val;
|
||||
let index = _this.tagList.findIndex(tag => {
|
||||
return tag.Name == val;
|
||||
});
|
||||
if(index >= 0) {
|
||||
row.fieldList = _this.resolveFieldList(_this.tagList[index].fields, row.headerRowList);
|
||||
}
|
||||
},
|
||||
handleEdgeNameChange(val, row) {
|
||||
row.typename = val;
|
||||
let index = _this.edgeList.findIndex(edge => {
|
||||
return edge.Name == val;
|
||||
});
|
||||
if(index >= 0) {
|
||||
row.fieldList = _this.resolveFieldList(_this.edgeList[index].fields, row.headerRowList);
|
||||
}
|
||||
},
|
||||
resolveFieldList(fieldList, headerRowList) { // solve fieldList, field.value auto assignment rowHeader's value
|
||||
fieldList.forEach(field => {
|
||||
if(headerRowList.indexOf(field.field) >= 0) {
|
||||
field.value = field.field;
|
||||
}
|
||||
});
|
||||
return fieldList;
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
_this.importSelection = val;
|
||||
},
|
||||
importRow() { // 执行批量导入,导入选择的
|
||||
if (_this.importSelection.length == 0) {
|
||||
_this.$message.warning("请选择文件再执行导入!");
|
||||
return;
|
||||
}
|
||||
let cmd = {
|
||||
taskId: _this.importSelection[0].taskId,
|
||||
taskFiles: []
|
||||
};
|
||||
_this.importSelection.forEach(item => {
|
||||
let _correspond = {};
|
||||
item.fieldList.forEach(field => {
|
||||
_correspond[field.field] = field.value;
|
||||
});
|
||||
cmd.taskFiles.push({
|
||||
taskFileId: item.id,
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
correspond: _correspond
|
||||
});
|
||||
});
|
||||
console.log(cmd);
|
||||
request({
|
||||
url: '/nebula_graph_import/graphtaskexecute',
|
||||
method: 'post',
|
||||
data: cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("导入成功");
|
||||
_this.queryData();
|
||||
_this.showDialogVisible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -1,297 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识图谱推理
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<el-row :gutter="5" style="padding-bottom: 20px">
|
||||
<el-col :span="7">
|
||||
<div>
|
||||
<label>请选择知识图谱:</label>
|
||||
<el-select v-model="qo.select" placeholder="选择图谱">
|
||||
<el-option label="装备知识图谱" value="1"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div>
|
||||
<label>请选择文件:</label>
|
||||
<el-upload
|
||||
ref="upload1" style="display: inline-block"
|
||||
:auto-upload="false"
|
||||
action="#"
|
||||
:on-change="handleFileChange"
|
||||
:limit="2"
|
||||
:show-file-list="false">
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="13">
|
||||
<el-button :type="items.length > 0 ?'primary':'info'" style="float:right;" @click="startReason">开始推理</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="12">
|
||||
<el-table :data="items" style="width: 100%" height="495" border>
|
||||
<el-table-column type="index" label="行号" width="70"></el-table-column>
|
||||
<el-table-column prop="qu" label="问题"></el-table-column>
|
||||
<el-table-column prop="an" label="正确答案"></el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="path-right-box">
|
||||
<div id="echart"></div>
|
||||
<div style="width: calc(100% - 30px);margin: 30px">
|
||||
<el-table :data="tableData" style="width: 100%;">
|
||||
<el-table-column prop="tl_len" label="推理路径长度"></el-table-column>
|
||||
<el-table-column prop="hits1" label="hits@1"></el-table-column>
|
||||
<el-table-column prop="hits2" label="hits@5"></el-table-column>
|
||||
<el-table-column prop="hits3" label="hits@10"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-descriptions class="margin-top" :column="1" border v-show="predictFlag">
|
||||
<el-descriptions-item label="问题">
|
||||
<el-select v-model="activeQuestionIndex" style="width: 100%" @change="handleQuestionSelect">
|
||||
<el-option v-for="(item,index) in items" :key="index" :value="index" :label="item.qu"></el-option>
|
||||
</el-select>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="正确答案">
|
||||
<label v-for="answer in predictAnswers" :key="answer">{{ answer }}</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="正确答案排序">
|
||||
{{ sort }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="正确答案推理路径" :span="2">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import graph from "@/components/graph/Graph";
|
||||
|
||||
var _this;
|
||||
var chart = null;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "graphPath",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
select: "1",
|
||||
pageNo: 1,
|
||||
pageSize: 1
|
||||
},
|
||||
questions: [],
|
||||
answers: [],
|
||||
resultList: [],
|
||||
items: [],
|
||||
tableData: [],
|
||||
predictAnswers: [],
|
||||
activeQuestionIndex: null,
|
||||
predictFlag: false,
|
||||
sort: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(file) {
|
||||
this.$refs.upload1.clearFiles();
|
||||
_this.predictFlag = false;
|
||||
_this.predictAnswers = [];
|
||||
_this.sort = '';
|
||||
_this.items = [];
|
||||
_this.queryData(file.raw);
|
||||
},
|
||||
startReason() {
|
||||
if(_this.items.length <= 0){
|
||||
_this.$message.error("显示数据存在问题导致推理失败");
|
||||
return false;
|
||||
}
|
||||
_this.$message.info("开始推理");
|
||||
setTimeout(() => {
|
||||
_this.tableData = _this.resultList;
|
||||
_this.initRchart();
|
||||
_this.predictFlag = true;
|
||||
_this.$message.success("推理成功");
|
||||
}, 1520);
|
||||
},
|
||||
handleQuestionSelect(row) {
|
||||
_this.predictAnswers = [_this.items[_this.activeQuestionIndex].an];
|
||||
_this.sort = _this.items[_this.activeQuestionIndex].sort;
|
||||
_this.initGraph();
|
||||
},
|
||||
initGraph() {
|
||||
_this.$nextTick(() => {
|
||||
let cmd = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
_this.items[_this.activeQuestionIndex].model.nodes.forEach(node => {
|
||||
cmd.nodes.push({
|
||||
id: node.vid,
|
||||
label: node.properties.name,
|
||||
});
|
||||
});
|
||||
_this.items[_this.activeQuestionIndex].model.relations.forEach(edge => {
|
||||
cmd.edges.push({
|
||||
source: edge.srcId,
|
||||
target: edge.dstId,
|
||||
label: edge.edgeName
|
||||
});
|
||||
});
|
||||
|
||||
_this.$refs['graph'].initGraph(cmd, true, null, true);
|
||||
});
|
||||
},
|
||||
queryData(fileItem) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", fileItem);
|
||||
request({
|
||||
url: '/indicator/indicator8',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
let _questions = [];
|
||||
let _answers = [];
|
||||
if(res.data && res.data.items){
|
||||
res.data.items.forEach(item => {
|
||||
_questions.push({
|
||||
text: item.qu
|
||||
});
|
||||
_answers.push({
|
||||
text: item.an
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_this.questions = _questions;
|
||||
_this.answers = _answers;
|
||||
_this.items = res.data && res.data.items ? res.data.items : [];
|
||||
_this.resultList = res.data && res.data.tj_result ? res.data.tj_result : [];
|
||||
});
|
||||
},
|
||||
initRchart() {
|
||||
if (chart == null) {
|
||||
chart = this.$echarts.init(document.getElementById("echart"));
|
||||
}
|
||||
let hits1 = [], hits2 = [], hits3 = [];
|
||||
_this.resultList.forEach(item => {
|
||||
hits1.push(item.hits1);
|
||||
hits2.push(item.hits2);
|
||||
hits3.push(item.hits3);
|
||||
});
|
||||
console.log(hits1, hits2, hits3);
|
||||
var option = {
|
||||
legend: {},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['一跳', '二跳']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'hits@1',
|
||||
data: hits1,
|
||||
type: 'bar'
|
||||
}, {
|
||||
name: 'hits@5',
|
||||
type: 'bar',
|
||||
data: hits2,
|
||||
}, {
|
||||
name: 'hits@10',
|
||||
type: 'bar',
|
||||
data: hits3,
|
||||
},
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-content {
|
||||
border: 1px solid #dcdddf;
|
||||
border-radius: 3px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
width: calc(100% - 20px);
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.path-bottom-box {
|
||||
height: calc(100% - 95px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.path-left-box {
|
||||
width: 50%;
|
||||
border-right: 1px solid #c1c2c4;
|
||||
|
||||
.question-item {
|
||||
margin: 15px 25px;
|
||||
line-height: 30px;
|
||||
font-size: 15px;
|
||||
color: #333333;
|
||||
|
||||
.question-item-div {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
|
||||
.question-item-title {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.question-item-answer {
|
||||
flex: 1;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-right-box {
|
||||
flex: 1;
|
||||
margin: 30px 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
#echart {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
flex: 1;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
辅助维修决策
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div class="recommend-content">
|
||||
<div class="recommend-title">装备维修信息检索主界面</div>
|
||||
<div class="recommed-tab">
|
||||
<label :class="active == 0?'tab-active':''" @click="selectTab(0)">信息汇聚</label>
|
||||
<label :class="active == 1?'tab-active':''" @click="selectTab(1)">知识检索</label>
|
||||
<label :class="active == 2?'tab-active':''" @click="selectTab(2)">故障定位</label>
|
||||
<label :class="active == 3?'tab-active':''" @click="selectTab(3)">维修推荐</label>
|
||||
</div>
|
||||
<div style="display: flex;width: 100%;margin-bottom: 30px">
|
||||
<el-input
|
||||
placeholder="请输入内容"
|
||||
v-model="keyword" style="flex: 1">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-left: 30px">图谱一下</el-button>
|
||||
</div>
|
||||
<div v-if="active == 0">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
<div style="margin-top: 15px;text-align: center">信息汇聚子图</div>
|
||||
</div>
|
||||
<div v-if="active == 1" class="search-box">
|
||||
<div class="search-box-item" style="flex: 1"></div>
|
||||
<div class="search-box-item" style="width: 300px;margin-left: 20px">
|
||||
{h1,r1,t1}
|
||||
{h2,r2,t2}
|
||||
{h3,r3,t3}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="active == 2">
|
||||
<div class="graph-table-title">故障定位描述</div>
|
||||
<div class="graph-table-content">
|
||||
发动机故障标志亮灯的原因有:<br>
|
||||
1、水温、曲轴位置、空气流量、进气温度、氧传感器等这些传感器受损,接触不良或信号<br>
|
||||
中断时,ECU就不能准确获得发动机的数据,此时会引起发动机故障灯亮;<br>
|
||||
2、发动机保养不良;<br>
|
||||
3、没有按要求添加汽油和机油,造成发动机磨损等。<br>
|
||||
根据查询的描
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="active == 3">
|
||||
<div class="graph-table-title">维修推荐</div>
|
||||
<div class="graph-table-content">
|
||||
发动机故障灯亮的维修方法是:<br>
|
||||
1、更换氧气传感器;2、更换三元催化器;3、紧固或更换油箱盖;4、更换恒温器;5、更
|
||||
换点火线圈;6、更换空气流量传感器;7、按期保养,跟据车辆保养卡的时间周期,按期
|
||||
进行保养。发动机的作用是:将汽油、柴油的热能通过在密封气缸内燃烧后膨胀气体,推动
|
||||
活塞做功转变为机械能。发动机的保养方法是:1、定期更换机油及滤芯;2、使用合适质
|
||||
量等级的润滑油;3、定期清理水箱;4、定期空气滤清器、机油滤清器及汽油滤清器;5、
|
||||
保持曲轴箱通风良好定期清洗;6、定期清洗燃油系统
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import graph from "@/components/graph/Graph";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "graphRecommend",
|
||||
data() {
|
||||
return {
|
||||
keyword: "",
|
||||
active: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
let cmd = {
|
||||
path: '/noumenon/query_relation_by_ontologyid',
|
||||
qo: {
|
||||
ontologyId: 2
|
||||
}
|
||||
};
|
||||
_this.$refs['graph'].initGraph({
|
||||
nodes: [
|
||||
{id: "1", name: "type1", label: "节点1"},
|
||||
{id: "2", name: "type1", label: "节点2"}
|
||||
],
|
||||
edges: [
|
||||
{source: "1", target: "2", name: "type2", label: "关系1"}
|
||||
]
|
||||
}, true);
|
||||
},
|
||||
methods: {
|
||||
selectTab(index) {
|
||||
_this.active = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.recommend-content {
|
||||
width: 65%;
|
||||
margin: 40px auto 0 auto;
|
||||
|
||||
.recommend-title {
|
||||
font-size: 30px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recommed-tab {
|
||||
margin-top: 50px;
|
||||
|
||||
label {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
display: inline-block;
|
||||
padding: 0px 20px;
|
||||
margin-right: 10px;
|
||||
border-left: 1px solid #dcdddf;
|
||||
border-right: 1px solid #dcdddf;
|
||||
border-top: 1px solid #dcdddf;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
color: #ffffff;
|
||||
border-color: #409eff;
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-content: space-between;
|
||||
|
||||
.search-box-item {
|
||||
height: 450px;
|
||||
border: 1px solid #dcdddf;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-table-title {
|
||||
width: calc(100% - 42px);
|
||||
background: rgba(250, 250, 250, 1);
|
||||
border: 1px solid #dcdddf;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.graph-table-content {
|
||||
border-left: 1px solid #dcdddf;
|
||||
border-right: 1px solid #dcdddf;
|
||||
border-bottom: 1px solid #dcdddf;
|
||||
padding: 20px 20px;
|
||||
line-height: 25px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识图谱关联
|
||||
</div>
|
||||
<div class="menu-content" style="display: inline-block">
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="12">
|
||||
<div>
|
||||
<label>请选择知识图谱:</label>
|
||||
<el-select v-model="activeSpaceName" placeholder="选择图谱">
|
||||
<el-option :label="item.name" :value="item.name" v-for="item in spaceList"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-descriptions class="margin-top desc2" :column="2" border>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">实体数量:</template>
|
||||
<label>{{ activeSpace.nodes }}</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">关系数量:</template>
|
||||
<label>{{ activeSpace.relations }}</label>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-upload
|
||||
style="float: right;margin: 15px 0;"
|
||||
ref="upload"
|
||||
class="upload-btn"
|
||||
:auto-upload="false"
|
||||
action="#"
|
||||
:on-change="handleFileChange"
|
||||
:limit="2"
|
||||
:show-file-list="false">
|
||||
<el-button type="primary">上传待关联文本数据集</el-button>
|
||||
</el-upload>
|
||||
<el-table :data="items" border class="text-table" height="300">
|
||||
<el-table-column type="index" width="50" label="序号"></el-table-column>
|
||||
<el-table-column prop="text" label="文本"></el-table-column>
|
||||
<el-table-column prop="el1Str" label="实体" width="200"></el-table-column>
|
||||
</el-table>
|
||||
<el-button type="primary" style="float:right;margin: 15px 0;" @click="startPredict">知识图谱关联</el-button>
|
||||
|
||||
<el-table :data="tableData" border class="text-table" height="300" @row-click="handleTableDataRowClick">
|
||||
<el-table-column type="index" width="50" label="序号"></el-table-column>
|
||||
<el-table-column prop="text" label="文本"></el-table-column>
|
||||
<el-table-column prop="el1Str" label="实体" width="200"></el-table-column>
|
||||
<el-table-column prop="el2Str" label="链接结果" width="200"></el-table-column>
|
||||
</el-table>
|
||||
<div class="result-label">准确率:{{ rate }}</div>
|
||||
|
||||
<el-descriptions title="" direction="vertical" :column="2" border id="menuContent">
|
||||
<el-descriptions-item label="实体链接结果" :span="2">
|
||||
<div class="result-el2-box">
|
||||
<el-tag v-for="tag in tags" style="margin-right: 5px;">{{ tag }}</el-tag>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模式关联结果" :span="1" labelClassName="handle-des">
|
||||
<div class="graph-box" id="graph1">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="信息汇聚结果" :span="1" labelClassName="handle-des2">
|
||||
<div class="graph-box" id="graph2">
|
||||
<graph ref="graph2"></graph>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import graph from "@/components/graph/Graph";
|
||||
|
||||
var menuContentWidth;
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "graphRelation",
|
||||
data() {
|
||||
return {
|
||||
spaceList: [],
|
||||
activeSpace: '',
|
||||
activeSpaceName: '',
|
||||
items: [],
|
||||
tableData: [],
|
||||
tags: [],
|
||||
rateLabel: '',
|
||||
rate: '0%'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryPrepareData();
|
||||
menuContentWidth = document.getElementById("menuContent").offsetWidth;
|
||||
},
|
||||
methods: {
|
||||
handleTableDataRowClick(row) {
|
||||
_this.tags = row.el2;
|
||||
_this.initGraphData(_this.resolveData(row.model1));
|
||||
_this.initGraphData2(_this.resolveData(row.model2));
|
||||
},
|
||||
queryPrepareData() {
|
||||
request({
|
||||
url: '/indicator/indicator7_1',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
if (res.data.length > 0) {
|
||||
_this.activeSpace = res.data[0]
|
||||
_this.activeSpaceName = res.data[0].name;
|
||||
}
|
||||
_this.spaceList = res.data;
|
||||
});
|
||||
},
|
||||
handleFileChange(item) {
|
||||
this.$refs.upload.clearFiles();
|
||||
_this.uploadFile(item.raw);
|
||||
},
|
||||
uploadFile(fileItem) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", fileItem);
|
||||
request({
|
||||
url: '/indicator/indicator7',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
res.data.items.forEach(item => {
|
||||
item.el1Str = item.el1.toString();
|
||||
item.el2Str = item.el2.toString();
|
||||
});
|
||||
_this.items = res.data.items;
|
||||
_this.rateLabel = res.data.zql;
|
||||
});
|
||||
},
|
||||
startPredict() {
|
||||
if (_this.items.length == 0) {
|
||||
_this.$message.warning("请先上传待关联文本数据集");
|
||||
return false;
|
||||
}
|
||||
_this.$message.info("知识图谱关联开始");
|
||||
setTimeout(() => {
|
||||
_this.tableData = _this.items;
|
||||
_this.rate = _this.rateLabel;
|
||||
_this.$message.success("知识图谱关联成功");
|
||||
}, 1560);
|
||||
},
|
||||
resolveData({nodes, relations}) {
|
||||
let cmd = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
nodes.forEach(node => {
|
||||
cmd.nodes.push({
|
||||
id: node.vid,
|
||||
label: node.properties.name
|
||||
});
|
||||
});
|
||||
relations.forEach(edge => {
|
||||
cmd.edges.push({
|
||||
label: edge.edgeName,
|
||||
source: edge.srcId,
|
||||
target: edge.dstId,
|
||||
});
|
||||
});
|
||||
return cmd;
|
||||
},
|
||||
initGraphData(cmd) {
|
||||
_this.$nextTick(() => {
|
||||
document.getElementById("graph1").style.width = menuContentWidth / 2 - 20 + "px";
|
||||
_this.$refs['graph'].initGraph(cmd, true, null, true);
|
||||
});
|
||||
|
||||
},
|
||||
initGraphData2(cmd) {
|
||||
_this.$nextTick(() => {
|
||||
document.getElementById("graph2").style.width = menuContentWidth / 2 - 20 + "px";
|
||||
_this.$refs['graph2'].initGraph(cmd, true, null, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.result-label {
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-title {
|
||||
padding-top: 25px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
margin-top: 150px;
|
||||
}
|
||||
|
||||
.text-table {
|
||||
margin-top: 20px;
|
||||
|
||||
.el-table th.is-leaf {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.text-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: calc(100% - 40px);
|
||||
height: 350px;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.text-title {
|
||||
width: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left-box {
|
||||
width: 50%;
|
||||
float: left;
|
||||
|
||||
.text-box-item {
|
||||
width: calc(90% - 1px);
|
||||
padding-right: 10%;
|
||||
border-right: 1px solid #c1c2c4;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
width: 50%;
|
||||
float: right;
|
||||
|
||||
.text-box-item {
|
||||
width: 90%;
|
||||
padding-left: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.graph-box {
|
||||
height: 400px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,243 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
航发控制装备维修知识补全
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<el-row :gutter="40" style="border-bottom: 1px solid #c1c2c4;padding-bottom: 40px">
|
||||
<el-col :span="6">
|
||||
<div>
|
||||
<label>请选择模式:</label>
|
||||
<el-select v-model="qo.select" placeholder="选择模式">
|
||||
<el-option label="评估模式" value="1"></el-option>
|
||||
<el-option label="应用模式" value="2"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div>
|
||||
<label>请选择知识图谱:</label>
|
||||
<el-select v-model="qo.graph" placeholder="选择知识图谱">
|
||||
<el-option label="图谱1" value="1"></el-option>
|
||||
<el-option label="图谱2" value="2"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="file-graph-content" v-if="qo.graph">
|
||||
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6" v-if="qo.select == '1'">
|
||||
<div>
|
||||
<label>请选择待补全三元组文件:</label>
|
||||
<el-upload
|
||||
ref="upload" style="display: inline-block"
|
||||
:auto-upload="false"
|
||||
action="#"
|
||||
:on-change="handleFileChange"
|
||||
:limit="2"
|
||||
:show-file-list="false">
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div v-if="file">
|
||||
<el-table :data="ralationData" style="width: 100%;margin-top: 20px;" height="150">
|
||||
<el-table-column prop="source" label="头实体"></el-table-column>
|
||||
<el-table-column prop="relation" label="关系"></el-table-column>
|
||||
<el-table-column prop="target" label="尾实体"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button type="primary">开始链接预测</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="path-bottom-box">
|
||||
<div class="path-left-box">
|
||||
<div style="height: calc(100% - 50px);overflow-y: auto;margin-right: 20px">
|
||||
<el-table :data="tableData" style="width: 100%;margin-top: 20px;">
|
||||
<el-table-column prop="len" label="待测试三元组"></el-table-column>
|
||||
<el-table-column prop="hits@1" label="预测答案"></el-table-column>
|
||||
<el-table-column prop="hits@5" label="真实答案"></el-table-column>
|
||||
<el-table-column prop="hits@10" label="真实答案排名"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-right-box">
|
||||
<div id="echart"></div>
|
||||
<div style="width: calc(100% - 70px);margin: 30px">
|
||||
<el-table :data="tableData" style="width: 100%;">
|
||||
<el-table-column prop="len" label="推理路径长度"></el-table-column>
|
||||
<el-table-column prop="hits@1" label="hits@1"></el-table-column>
|
||||
<el-table-column prop="hits@5" label="hits@5"></el-table-column>
|
||||
<el-table-column prop="hits@10" label="hits@10"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "graphShow",
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
select: "",
|
||||
graph: "",
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
file: "",
|
||||
result: {
|
||||
total: 100
|
||||
},
|
||||
tableData: [
|
||||
{len: "1-hop", "hits@1": "hits@1", "hits@5": "hits@5", "hits@10": "hits@10"},
|
||||
{len: "2-hop", "hits@1": "hits@1", "hits@5": "hits@5", "hits@10": "hits@10"},
|
||||
{len: "3-hop", "hits@1": "hits@1", "hits@5": "hits@5", "hits@10": "hits@10"},
|
||||
{len: "4-hop", "hits@1": "hits@1", "hits@5": "hits@5", "hits@10": "hits@10"}
|
||||
],
|
||||
ralationData: [
|
||||
{source: "头实体", relation: "关系", target: "尾实体"},
|
||||
{source: "头实体", relation: "关系", target: "尾实体"},
|
||||
{source: "头实体", relation: "关系", target: "尾实体"},
|
||||
{source: "头实体", relation: "关系", target: "尾实体"},
|
||||
{source: "头实体", relation: "关系", target: "尾实体"}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.initRchart();
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(item) {
|
||||
this.$refs.upload.clearFiles();
|
||||
let formData = new FormData();
|
||||
formData.append("file", item.raw);
|
||||
_this.file = item.raw.name;
|
||||
// request({
|
||||
// url: '/sys-file/create',
|
||||
// method: 'put',
|
||||
// data: formData
|
||||
// }).then(res => {
|
||||
// _this.$message.success("上传成功");
|
||||
// _this.queryData();
|
||||
// });
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
},
|
||||
initRchart() {
|
||||
var chart = this.$echarts.init(document.getElementById("echart"));
|
||||
var option = {
|
||||
title: {
|
||||
text: '链接预测Hits@1、Hits@5、Hits@10以及其他占比',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [
|
||||
{value: 1048, name: 'Search Engine'},
|
||||
{value: 735, name: 'Direct'},
|
||||
{value: 580, name: 'Email'},
|
||||
{value: 484, name: 'Union Ads'},
|
||||
{value: 300, name: 'Video Ads'}
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-graph-content {
|
||||
border: 1px solid #dcdddf;
|
||||
border-radius: 3px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
width: calc(100% - 20px);
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.path-bottom-box {
|
||||
height: calc(100% - 95px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.path-left-box {
|
||||
width: 50%;
|
||||
border-right: 1px solid #c1c2c4;
|
||||
|
||||
.question-item {
|
||||
margin: 15px 10%;
|
||||
line-height: 30px;
|
||||
font-size: 15px;
|
||||
color: #333333;
|
||||
|
||||
.question-item-div {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
|
||||
.question-item-title {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.question-item-answer {
|
||||
flex: 1;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-right-box {
|
||||
flex: 1;
|
||||
margin: 30px 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
#echart {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,266 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识图谱融合
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="result" style="margin-bottom: 20px">待融合的图谱1</div>
|
||||
<el-select v-model="sourceGraph.name" placeholder="请选择要融合的知识图谱1" @change="handleSourceGraphChange">
|
||||
<el-option v-for="item in resultCmd.graphs" :label="item.name" :value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div style="margin-top: 20px;height: 138px;border: 1px solid rgba(0, 0, 0, 0.1);padding: 20px">
|
||||
<el-descriptions title="" :column="1" v-if="sourceGraph.name">
|
||||
<el-descriptions-item label="概念数量">{{sourceGraph.gn_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="属性数量">{{sourceGraph.sx_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="实体数量">{{sourceGraph.sl_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="三元组数量">{{sourceGraph.saz_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="result">待融合的图谱2</div>
|
||||
<el-select v-model="targetGraph.name" placeholder="请选择要融合的知识图谱2"
|
||||
@change="handleTargetGraphChange" style="margin-top: 25px">
|
||||
<el-option
|
||||
v-for="item in resultCmd.graphs"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div style="margin-top: 20px;height: 138px;border: 1px solid rgba(0, 0, 0, 0.1);padding: 20px">
|
||||
<el-descriptions class="margin-top" title="" :column="1" v-if="targetGraph.name">
|
||||
<el-descriptions-item label="概念数量">{{targetGraph.gn_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="属性数量">{{targetGraph.sx_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="实体数量">{{targetGraph.sl_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="三元组数量">{{targetGraph.saz_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div style="margin: 0 auto;margin-top: 25px;display: flex;justify-content: center">
|
||||
<el-button type="primary" @click="fusionData" :loading="loading">开始融合</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16" v-if="showFlag">
|
||||
<el-row :gutter="50">
|
||||
<el-col :span="12" >
|
||||
<div class="result" style="text-align: center;width: 100%;margin-bottom: 20px;color: #5c5c5c;">本体匹配结果展示</div>
|
||||
<el-table :data="btResults" style="width: 100%" height="350px" @current-change="handleCurrentChange1" highlight-current-row>
|
||||
<el-table-column prop="srcNode" label="本体1"></el-table-column>
|
||||
<el-table-column prop="dctNode" label="本体2"></el-table-column>
|
||||
<el-table-column prop="type" label="类型"></el-table-column>
|
||||
<el-table-column prop="reliability" label="匹配置信度"></el-table-column>
|
||||
<el-table-column prop="result" label="匹配结果"></el-table-column>
|
||||
</el-table>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleChange1"
|
||||
:current-page="qo1.pageNo"
|
||||
:page-size="qo1.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="resultCmd1.btResults.length">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<div style="display: flex;width: 100%;margin-top: 20px">
|
||||
<el-button type="primary" size="small" style="flex: 1" @click="clickStr('currentRow1','srcNode')">验证本体1</el-button>
|
||||
<el-button type="primary" size="small" style="flex: 1" @click="clickStr('currentRow1','dctNode')">验证本体2</el-button>
|
||||
</div>
|
||||
<div class="result">本体匹配结果: {{resultCmd1.bfb.bt_result}}</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="result" style="text-align: center;width: 100%;margin-bottom: 20px;color: #5c5c5c;">实例匹配结果展示</div>
|
||||
<el-table :data="slResults" style="width: 100%" height="350px" @current-change="handleCurrentChange2" highlight-current-row>
|
||||
<el-table-column prop="srcNode" label="本体1"></el-table-column>
|
||||
<el-table-column prop="dctNode" label="本体2"></el-table-column>
|
||||
<el-table-column prop="type" label="类型"></el-table-column>
|
||||
<el-table-column prop="reliability" label="匹配置信度"></el-table-column>
|
||||
<el-table-column prop="result" label="匹配结果"></el-table-column>
|
||||
</el-table>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleChange2"
|
||||
:current-page="qo2.pageNo"
|
||||
:page-size="qo2.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="resultCmd1.slResults.length">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<div style="display: flex;width: 100%;margin-top: 20px">
|
||||
<el-button type="primary" size="small" style="flex: 1" @click="clickStr('currentRow2','srcNode')">验证本体1</el-button>
|
||||
<el-button type="primary" size="small" style="flex: 1" @click="clickStr('currentRow2','dctNode')">验证本体2</el-button>
|
||||
</div>
|
||||
<div class="result">实例匹配结果: {{resultCmd1.bfb.sl_result}}</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="result" style="text-align: center;width: 100%;margin-top: 20px;color: #5c5c5c;">融合后知识图谱统计信息</div>
|
||||
<div style="margin-top: 20px;height:20px;border: 1px solid rgba(0, 0, 0, 0.1);padding: 20px">
|
||||
<el-descriptions title="" :column="4">
|
||||
<el-descriptions-item label="概念数量">{{resultCmd1.rhGraph.gn_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="属性数量">{{resultCmd1.rhGraph.sx_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="实体数量">{{resultCmd1.rhGraph.sl_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
<el-descriptions-item label="三元组数量">{{resultCmd1.rhGraph.saz_count.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "knowledgeFusion",
|
||||
data() {
|
||||
return {
|
||||
sourceGraph: {
|
||||
"name": "",
|
||||
"gn_count": "",
|
||||
"sx_count": "",
|
||||
"sl_count": "",
|
||||
"saz_count": ""
|
||||
},
|
||||
targetGraph: {
|
||||
"name": "",
|
||||
"gn_count": "",
|
||||
"sx_count": "",
|
||||
"sl_count": "",
|
||||
"saz_count": ""
|
||||
},
|
||||
resultCmd: {},
|
||||
resultCmd1: {
|
||||
bfb:{
|
||||
bt_result:"",
|
||||
sl_result:""
|
||||
},
|
||||
rhGraph: {
|
||||
"gn_count": "",
|
||||
"sx_count": "",
|
||||
"sl_count": "",
|
||||
"saz_count": ""
|
||||
},
|
||||
btResults:[],
|
||||
slResults:[]
|
||||
},
|
||||
currentRow1:{},
|
||||
currentRow2:{},
|
||||
qo1:{
|
||||
pageNo:1,
|
||||
pageSize:10
|
||||
},
|
||||
qo2:{
|
||||
pageNo:1,
|
||||
pageSize:10
|
||||
},
|
||||
btResults:[],
|
||||
slResults:[],
|
||||
showFlag: false,
|
||||
loading:false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
handleSourceGraphChange() {
|
||||
_this.resultCmd.graphs.forEach(item => {
|
||||
if(item.name == _this.sourceGraph.name){
|
||||
_this.sourceGraph = JSON.parse(JSON.stringify(item))
|
||||
}
|
||||
})
|
||||
console.log(_this.resultCmd.graphs)
|
||||
},
|
||||
handleTargetGraphChange() {
|
||||
_this.resultCmd.graphs.forEach(item => {
|
||||
if(item.name == _this.targetGraph.name){
|
||||
_this.targetGraph = JSON.parse(JSON.stringify(item))
|
||||
}
|
||||
})
|
||||
},
|
||||
handleChange1(val) {
|
||||
_this.qo1.pageNo = val;
|
||||
_this.btResults = _this.resultCmd1.btResults.slice((_this.qo1.pageNo - 1) * 10, _this.qo1.pageNo * 10);
|
||||
},
|
||||
handleChange2(val) {
|
||||
_this.qo2.pageNo = val;
|
||||
_this.slResults = _this.resultCmd1.slResults.slice((_this.qo2.pageNo - 1) * 10, _this.qo2.pageNo * 10);
|
||||
},
|
||||
fusionData(){
|
||||
if(_this.targetGraph.name == _this.sourceGraph.name){
|
||||
_this.$message.warning("待融合图谱1和待融合图谱2不可相同");
|
||||
return false;
|
||||
}
|
||||
_this.queryResult();
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/indicator/indicator4/1',
|
||||
method: 'get',
|
||||
}).then(res => {
|
||||
_this.resultCmd = res.data;
|
||||
if(_this.resultCmd.graphs.length >= 2){
|
||||
_this.sourceGraph = JSON.parse(JSON.stringify(_this.resultCmd.graphs[0]))
|
||||
_this.targetGraph = JSON.parse(JSON.stringify(_this.resultCmd.graphs[1]))
|
||||
}
|
||||
});
|
||||
},
|
||||
queryResult() {
|
||||
_this.loading = true
|
||||
request({
|
||||
url: '/indicator/indicator4/2',
|
||||
method: 'get',
|
||||
}).then(res => {
|
||||
_this.loading = false
|
||||
_this.resultCmd1 = res.data;
|
||||
_this.$nextTick(()=>{
|
||||
_this.showFlag = true;
|
||||
_this.handleChange1(1);
|
||||
_this.handleChange2(1);
|
||||
})
|
||||
});
|
||||
},
|
||||
handleCurrentChange1(val) {
|
||||
this.currentRow1 = val;
|
||||
},
|
||||
handleCurrentChange2(val) {
|
||||
this.currentRow2 = val;
|
||||
},
|
||||
clickStr(name,value){
|
||||
if(!this[name][value]){
|
||||
_this.$message.warning("请选中行");
|
||||
return false;
|
||||
}
|
||||
|
||||
var srcNode = this[name][value];
|
||||
|
||||
if(srcNode.substring(0,1) == '<'){
|
||||
srcNode = srcNode.substring(1)
|
||||
}
|
||||
|
||||
if(srcNode.substring(srcNode.length-3) == '> .'){
|
||||
srcNode = srcNode.substring(0,srcNode.length-3)
|
||||
}
|
||||
|
||||
if(srcNode.substring(srcNode.length-1) == '>'){
|
||||
srcNode = srcNode.substring(0,srcNode.length-1)
|
||||
}
|
||||
|
||||
window.open(srcNode, "_blank")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
知识检索和问答
|
||||
</div>
|
||||
<div class="menu-content" style="display: inline-block;display: flex">
|
||||
<div style="width:30%;min-width:500px;">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item label="">
|
||||
<el-select v-model="value" style="width: 300px" placeholder="选择图谱名">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item>-->
|
||||
<!-- <el-button type="primary" style="margin-left: 14px">选择图谱</el-button>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
</div>
|
||||
<div >
|
||||
<el-input v-model="fileName" readonly style="width: 300px" disabled></el-input>
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-btn"
|
||||
:auto-upload="false"
|
||||
action="#"
|
||||
:on-change="handleFileChange"
|
||||
:limit="2"
|
||||
:show-file-list="false">
|
||||
<el-button type="primary">导入测试文件</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" style="margin-left: 150px;margin-top: 100px" @click="queryData()">开始测试</el-button>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<!--<el-input v-model="resultStr" readonly style="width: 80%" disabled></el-input>-->
|
||||
<div v-if="resultStr">{{resultStr}}</div>
|
||||
<!-- <div class="right-box" style="padding-left: 5%;width: 45%">-->
|
||||
<!-- <el-input v-model="keyword" placeholder="检索或问答内容" style="width: 300px"></el-input>-->
|
||||
<!-- <el-button type="primary" style="margin-left: 20px">开始检索</el-button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div style="width:70%;height: calc(100% - 80px)">
|
||||
<div class="left-box" style="width: 100%">
|
||||
|
||||
<div style="width: 96%;">
|
||||
<el-table :data="questions" style="width: 100%;" @row-click="handleTableRowClick">
|
||||
<el-table-column prop="qu" label="问题"></el-table-column>
|
||||
<el-table-column prop="hxan" label="知识检索或问答结果(Top-5)">
|
||||
<template slot-scope="scope">
|
||||
<!-- {{scope.row.hxan}}-->
|
||||
<p v-for="items in scope.row.hxan" :style=" items == scope.row.an ? 'color: red' : ''">{{items}}</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="an" label="正确答案"></el-table-column>
|
||||
<el-table-column prop="loc" label="正确答案位置序号"></el-table-column>
|
||||
</el-table>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="questionsAll.length">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="right-box">-->
|
||||
<!-- <div style="width: 90%;margin-left: 10%">-->
|
||||
<!-- <el-table :data="answerData" style="width: 100%;">-->
|
||||
<!-- <el-table-column prop="result" label="检索结果">-->
|
||||
<!-- <template slot-scope="scope">-->
|
||||
<!-- <label :style=" scope.row.result == activeRow.qa.Answer ? 'color: red' : ''">{{ scope.row.result }}</label>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<!-- </el-table>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "graphQuestion",
|
||||
data() {
|
||||
return {
|
||||
options: [
|
||||
{name: "装备知识图谱"},
|
||||
],
|
||||
value: '',
|
||||
keyword: "",
|
||||
fileName: "",
|
||||
resultStr: "",
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
file:"",
|
||||
questionsAll: [],
|
||||
questions: [],
|
||||
answerData: [],
|
||||
activeRow: {
|
||||
qa: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(file) {
|
||||
this.$refs.upload.clearFiles();
|
||||
_this.fileName = file.raw.name;
|
||||
_this.file = file.raw;
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.questions = _this.questionsAll.slice((_this.qo.pageNo - 1) * 10, _this.qo.pageNo * 10);
|
||||
},
|
||||
handleTableRowClick(row, column, event) {
|
||||
let _answerData = [];
|
||||
row.entity.forEach(item => {
|
||||
_answerData.push({
|
||||
result: item
|
||||
});
|
||||
});
|
||||
_this.activeRow = row;
|
||||
_this.answerData = _answerData;
|
||||
},
|
||||
queryData() {
|
||||
let formData = new FormData();
|
||||
formData.append("file", _this.file);
|
||||
request({
|
||||
url: '/indicator/indicator9/dn_graph_one',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
var trueTotal = 0;
|
||||
res.data.items.map(data1 => {
|
||||
data1.hxan = JSON.parse(data1.hxan);
|
||||
if(data1.hxan.indexOf(data1.an) != -1){
|
||||
trueTotal ++;
|
||||
}
|
||||
})
|
||||
|
||||
_this.resultStr = "知识检索或问答结果 Top-5 准确率:" + res.data.zql.toFixed(2) + "%,正确条数:"+trueTotal+",总条数:"+res.data.items.length;
|
||||
_this.questionsAll = res.data.items;
|
||||
_this.handleCurrentChange(1);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
border: 1px solid #dcdddf;
|
||||
width: calc(100% - 40px);
|
||||
height: 550px;
|
||||
overflow-y: auto;
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.text-title {
|
||||
width: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left-box {
|
||||
width: 50%;
|
||||
float: left;
|
||||
|
||||
.text-box-item {
|
||||
width: calc(90% - 1px);
|
||||
padding-right: 10%;
|
||||
border-right: 1px solid #c1c2c4;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
width: 50%;
|
||||
float: right;
|
||||
|
||||
.text-box-item {
|
||||
width: 90%;
|
||||
padding-left: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,567 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
模型管理
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入模型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入算法名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">新增模型</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table">
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"></el-table-column>
|
||||
<el-table-column prop="algorithmId" label="算法名称" :formatter="algorithmIdFormatter"></el-table-column>
|
||||
<!-- <el-table-column prop="version" label="版本"></el-table-column> -->
|
||||
<el-table-column prop="status" label="训练状态" :formatter="statusFormatter"></el-table-column>
|
||||
<el-table-column prop="modifyTimeMillis" label="修改时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.modifyTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTimeMillis" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.createTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="trainRow(scope.row)" type="text" size="small" v-if="scope.row.status != 8">
|
||||
训练
|
||||
</el-button>
|
||||
<el-button type="text" size="small" v-if="scope.row.status == 8" style="color: #ccc;cursor: default" title="训练中">
|
||||
训练
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="modifyRow(scope.row)" type="text" size="small" v-if="scope.row.status == 1">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small" >
|
||||
删除
|
||||
</el-button>
|
||||
<br>
|
||||
<el-button @click.native.prevent="showLogDialog(scope.row)" type="text" size="small" v-if="scope.row.status == 8 || scope.row.status == 9">
|
||||
日志
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="updateStatus(scope.row, 'stop')" type="text" size="small" v-if="scope.row.status == 8">
|
||||
停止
|
||||
</el-button>
|
||||
<!-- <el-button @click.native.prevent="updateStatus(scope.row, 'start')" type="text" size="small">
|
||||
重启
|
||||
</el-button> -->
|
||||
<el-button @click.native.prevent="publishService(scope.row)" type="text" size="small" v-if="scope.row.status == 9">
|
||||
发布服务
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="testRow(scope.row)" type="text" size="small" v-if="scope.row.status == 9">
|
||||
测试
|
||||
</el-button>
|
||||
<!-- <template v-show="scope.row.status == 0">
|
||||
|
||||
</template> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="dialogName"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="模型名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入模型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法" prop="algorithmId">
|
||||
<el-select v-model="cmd.algorithmId" placeholder="请选择算法">
|
||||
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in algorithmList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="数据集" prop="name">-->
|
||||
<!-- <el-select v-model="cmd.name" placeholder="请选择数据集">-->
|
||||
<!-- <el-option label="" value=""></el-option>-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="cmd.modelRemark" placeholder="请输入描述" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitModify">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="训练模型"
|
||||
:visible.sync="trainCmdDialogVisible"
|
||||
width="75%">
|
||||
<el-form ref="cmd" label-width="120px" :rules="rules" :model="cmd" v-loading="trainLoading" element-loading-text="程序启动中" :inline="true">
|
||||
<div class="title-form-box">公共配置</div>
|
||||
<el-form-item :label="item.name" v-for="item in cmd.publicPropert" :key="item.key">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name"></el-input>
|
||||
</el-form-item>
|
||||
<div class="title-form-box">私有配置</div>
|
||||
<el-form-item :label="item.name" v-for="item in cmd.privatePropert" :key="item.key">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name" ></el-input>
|
||||
</el-form-item>
|
||||
<div class="title-form-box">资源选择</div>
|
||||
<el-form-item label="指定显卡" prop="gpuId">
|
||||
<el-select v-model="cmd.gpuId" placeholder="请选择显卡">
|
||||
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in cmd.gpuInfoList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据集" prop="dataSetId1">
|
||||
<el-select v-model="cmd.dataSetId1" placeholder="请选择数据集">
|
||||
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in dataSetList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="trainCmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="trainLoading" @click="submitTrain">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="发布服务"
|
||||
:visible.sync="serverCmdDialogVisible"
|
||||
width="75%">
|
||||
<el-form ref="cmd" label-width="120px" :rules="rules" :model="cmd">
|
||||
<el-form-item :label="item.name" v-for="item in cmd.privatePropert" :key="item.key">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name" ></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="serverCmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitPublishService">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="测试"
|
||||
:visible.sync="testCmdDialogVisible"
|
||||
destroy-on-close
|
||||
@close="handleTestDialogClose"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd" v-loading="testLoading">
|
||||
<el-form-item label="测试类型" prop="testType">
|
||||
<el-select v-model="cmd.testType" placeholder="请选择测试类型">
|
||||
<el-option label="单文本测试" value="text"></el-option>
|
||||
<el-option label="数据集测试" value="database"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单文本测试" prop="text" v-if="cmd.testType == 'text'">
|
||||
<el-input v-model="cmd.text" placeholder="请输入所要测试的单文本"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据集测试" prop="dataSetId" v-if="cmd.testType == 'database'">
|
||||
<el-select v-model="cmd.dataSetId" placeholder="请选择数据集">
|
||||
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in dataSetList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="log-title-box">服务日志</div>
|
||||
<log ref="log-item2" v-if="testSuccess"></log>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="testCmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitTest" :disabled="testLoading">开始测试</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="日志"
|
||||
:visible.sync="logDialogVisible"
|
||||
destroy-on-close
|
||||
@close="handleLogDialogClose"
|
||||
width="60%">
|
||||
<log ref="log-item"></log>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
import request1, {getBaseUrl} from '@/utils/request';
|
||||
import log from '@/components/log/index';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "modelManage",
|
||||
components: {
|
||||
log
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
algorithmList: [],
|
||||
dialogName: '编辑',
|
||||
cmdDialogVisible: false,
|
||||
trainCmdDialogVisible: false,
|
||||
serverCmdDialogVisible: false,
|
||||
cmd: {
|
||||
dataSetId: '',
|
||||
dataSetId1: ''
|
||||
},
|
||||
testSuccess: false,
|
||||
testLoading: false,
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入模型名称'}
|
||||
],
|
||||
algorithmId: [
|
||||
{required: true, message: '请选择算法'}
|
||||
],
|
||||
gpuId: [
|
||||
{required: true, message: '请选择显卡'}
|
||||
],
|
||||
testType: [
|
||||
{required: true, message: '请选择测试类型'}
|
||||
],
|
||||
text: [
|
||||
{required: true, message: '请输入所要测试的单文本'}
|
||||
],
|
||||
dataSetId: [
|
||||
{required: true, message: '请选择数据集'}
|
||||
],
|
||||
dataSetId1: [
|
||||
{required: true, message: '请选择数据集'}
|
||||
],
|
||||
},
|
||||
testCmdDialogVisible: false,
|
||||
dataSetList: [],
|
||||
logDialogVisible: false,
|
||||
statusList: {
|
||||
1: '保存状态',
|
||||
2: '开始',
|
||||
3: '运行中',
|
||||
4: '等待中',
|
||||
5: '结束',
|
||||
6: '发布失败',
|
||||
7: '取消',
|
||||
8: '训练中',
|
||||
9: '训练完成'
|
||||
},
|
||||
trainLoading: false,
|
||||
modelId: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
_this.queryAlgorithmList();
|
||||
_this.queryDataSetList();
|
||||
},
|
||||
filters: {
|
||||
timeFilter(val) {
|
||||
if (val > 0) {
|
||||
let dt = new Date(val);
|
||||
let year = dt.getFullYear();
|
||||
let month = dt.getMonth() + 1;
|
||||
let date = dt.getDate();
|
||||
let hour = dt.getHours();
|
||||
let minute = dt.getMinutes();
|
||||
let second = dt.getSeconds();
|
||||
return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
algorithmIdFormatter(row) {
|
||||
let index = _this.algorithmList.findIndex(dataSet => {
|
||||
return row.algorithmId == dataSet.id;
|
||||
});
|
||||
return index >= 0 ? _this.algorithmList[index].name : '-';
|
||||
},
|
||||
statusFormatter(row) {
|
||||
return _this.statusList[row.status] || '-';
|
||||
},
|
||||
queryDataSetList() {
|
||||
request1({
|
||||
url: '/remark_task/query_list',
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.dataSetList = res.data;
|
||||
});
|
||||
},
|
||||
showLogDialog(row) {
|
||||
_this.logDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['log-item'].init({modelId: row.id, type: 'log_train'});
|
||||
});
|
||||
},
|
||||
handleLogDialogClose() {
|
||||
_this.$refs['log-item'].close();
|
||||
},
|
||||
handleTestDialogClose() {
|
||||
_this.$refs['log-item2'].close();
|
||||
},
|
||||
queryAlgorithmList() {
|
||||
request({
|
||||
url: '/algorithm/list',
|
||||
method: 'post',
|
||||
data: {pageNo: 1, pageSize: 1000}
|
||||
}).then(res => {
|
||||
_this.algorithmList = res.list;
|
||||
});
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/model/list',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.list;
|
||||
_this.result.total = res.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/model/remove',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: item.id,
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "新增";
|
||||
_this.cmd = {
|
||||
name: '',
|
||||
algorithmId: '',
|
||||
modelRemark: ''
|
||||
};
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
trainRow(item) {
|
||||
let id = item.id;
|
||||
request({
|
||||
url: `/model/editTrainParameter/${id}`,
|
||||
method: 'post',
|
||||
}).then(res => {
|
||||
_this.cmd = res.data;
|
||||
_this.cmd.dataSetId = "";
|
||||
_this.trainLoading = false;
|
||||
_this.trainCmdDialogVisible = true;
|
||||
});
|
||||
|
||||
},
|
||||
testRow(item) {
|
||||
_this.testSuccess = false;
|
||||
_this.testCmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.cmd = {
|
||||
testType: '',
|
||||
text: '',
|
||||
dataSetId: '',
|
||||
modelId: item.id
|
||||
};
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "编辑";
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.cmd.modifyTimeMillis = null;
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
submitModify() {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
request({
|
||||
url: '/model/save',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("新增成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
submitTrain() { // 提交训练
|
||||
if(_this.trainLoading) {
|
||||
return
|
||||
}
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
_this.trainLoading = true;
|
||||
_this.cmd.dataSetUrl = getBaseUrl() + "/es_remark_task/findbytaskidandtasktypeall/" + _this.cmd.dataSetId1 + "/";
|
||||
|
||||
request({
|
||||
url: '/model/startTrain',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("训练成功");
|
||||
_this.trainLoading = false;
|
||||
_this.queryData();
|
||||
_this.trainCmdDialogVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
submitTest() { // 提交测试
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
_this.testLoading = true;
|
||||
if (_this.cmd.testType == 'text') {
|
||||
request({
|
||||
url: '/model/singleTextTest',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("测试成功");
|
||||
_this.testSuccess = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['log-item2'].init({modelId: _this.cmd.modelId, type: 'test_single'});
|
||||
_this.testLoading = false;
|
||||
});
|
||||
});
|
||||
} else if (_this.cmd.testType == 'database') {
|
||||
let _cmd = JSON.parse(JSON.stringify(_this.cmd))
|
||||
_cmd.dataSetId = getBaseUrl() + "/es_remark_task/findbytaskidandtasktypeall/" + _cmd.dataSetId + "/";
|
||||
console.log(_cmd);
|
||||
request({
|
||||
url: '/model/dataSetTest',
|
||||
method: 'post',
|
||||
data: _cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("测试成功");
|
||||
_this.testSuccess = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['log-item2'].init({modelId: _this.cmd.modelId, type: 'log_test'});
|
||||
_this.testLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
updateStatus(item, action) { // 修改状态,重启start,停止stop
|
||||
let text = action === 'start' ? '重启' : '停止';
|
||||
_this.$confirm(`此操作将执行${text}操作, 是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
request({
|
||||
url: '/model/update/status',
|
||||
method: 'post',
|
||||
data: {
|
||||
modelId: item.id,
|
||||
action: action
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success(`${text}成功`);
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
publishService(item) { // 发布服务
|
||||
let id = item.id;
|
||||
_this.modelId = id;
|
||||
request({
|
||||
url: `/model/getServerPropert/${id}`,
|
||||
method: 'post',
|
||||
}).then(res => {
|
||||
_this.cmd.privatePropert = res.data;
|
||||
_this.serverCmdDialogVisible = true;
|
||||
});
|
||||
},
|
||||
submitPublishService(item) { // 发布服务
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '服务发布中',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
request({
|
||||
url: '/model/publishService',
|
||||
method: 'post',
|
||||
data: {
|
||||
modelId: _this.modelId,
|
||||
serverPropert: _this.cmd.privatePropert
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success(`发布成功`);
|
||||
loading.close();
|
||||
_this.serverCmdDialogVisible = false;
|
||||
_this.queryData();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.log-title-box {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.log-box {
|
||||
background-color: #c2c2c2;
|
||||
border-radius: 3px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.title-form-box {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
节点管理
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入节点名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入节点地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin: 0px 0 20px 0">
|
||||
<el-button type="primary" @click="createRow()">新增节点</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table">
|
||||
<el-table-column type="index" label="行号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="节点名称"></el-table-column>
|
||||
<el-table-column label="节点地址">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.host }}:{{ scope.row.port }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modifyTimeMillis" label="修改时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.modifyTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTimeMillis" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.createTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button @click.native.prevent="showXtermDialog(scope.row)" type="text" size="small">
|
||||
终端
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="modifyRow(scope.row)" type="text" size="small">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="removeRow(scope.row)" type="text" size="small">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="dialogName"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
width="40%">
|
||||
<el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="节点名称" prop="name">
|
||||
<el-input v-model="cmd.name" placeholder="请输入节点名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点IP" prop="host">
|
||||
<el-input v-model="cmd.host" placeholder="请输入节点IP"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点端口" prop="port">
|
||||
<el-input v-model="cmd.port" placeholder="请输入节点端口"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH账号" prop="user">
|
||||
<el-input v-model="cmd.user" placeholder="请输入SSH账号"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH密码" prop="password">
|
||||
<el-input v-model="cmd.password" placeholder="请输入SSH密码"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="Host密码" prop="name">-->
|
||||
<!-- <el-input v-model="cmd.name" placeholder="请输入Host密码"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="数据目录" prop="dataPath">
|
||||
<el-input v-model="cmd.dataPath" placeholder="请输入数据目录"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="超时时间">-->
|
||||
<!-- <el-date-picker-->
|
||||
<!-- v-model="cmd.time"-->
|
||||
<!-- type="datetime"-->
|
||||
<!-- placeholder="请选择超时时间">-->
|
||||
<!-- </el-date-picker>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="描述">-->
|
||||
<!-- <el-input v-model="cmd.description" placeholder="请输入描述" type="textarea"></el-input>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="cmdDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitModify">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="终端"
|
||||
:visible.sync="xtermDialogVisible"
|
||||
destroy-on-close
|
||||
@close="handleXtermDialogClose"
|
||||
width="60%">
|
||||
<xterm ref="xterm-item"></xterm>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
import xterm from '@/components/xterm/index';
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "codeWarehouse",
|
||||
components: {
|
||||
xterm
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
dialogName: '编辑',
|
||||
cmdDialogVisible: false,
|
||||
cmd: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入节点名称'}
|
||||
],
|
||||
host: [
|
||||
{required: true, message: '请输入节点IP'}
|
||||
],
|
||||
port: [
|
||||
{required: true, message: '请输入节点端口'}
|
||||
],
|
||||
user: [
|
||||
{required: true, message: '请输入SSH账号'}
|
||||
],
|
||||
password: [
|
||||
{required: true, message: '请输入SSH密码'}
|
||||
],
|
||||
},
|
||||
xtermDialogVisible: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
filters: {
|
||||
timeFilter(val) {
|
||||
if (val > 0) {
|
||||
let dt = new Date(val);
|
||||
let year = dt.getFullYear();
|
||||
let month = dt.getMonth() + 1;
|
||||
let date = dt.getDate();
|
||||
let hour = dt.getHours();
|
||||
let minute = dt.getMinutes();
|
||||
let second = dt.getSeconds();
|
||||
return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
queryData() {
|
||||
request({
|
||||
url: '/server/node/list',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.list;
|
||||
_this.result.total = res.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
removeRow(item) {
|
||||
_this.$confirm('此操作将永久删除记录, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
let id = item.id;
|
||||
request({
|
||||
url: `/server/node/delete/${id}`,
|
||||
method: 'post',
|
||||
}).then(res => {
|
||||
_this.$message.success("删除成功");
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
createRow() {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "新增";
|
||||
_this.cmd = {
|
||||
"charset": "",
|
||||
"dataPath": "",
|
||||
"fileDirs": "",
|
||||
"host": "",
|
||||
"name": "",
|
||||
"notAllowedCommand": "",
|
||||
"password": "",
|
||||
"port": "",
|
||||
"user": ""
|
||||
};
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
modifyRow(item) {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.dialogName = "编辑";
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
_this.cmd.modifyTimeMillis = null;
|
||||
_this.$refs.cmd.resetFields();
|
||||
});
|
||||
},
|
||||
showXtermDialog(row) {
|
||||
_this.xtermDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['xterm-item'].init(row.id);
|
||||
});
|
||||
},
|
||||
handleXtermDialogClose() {
|
||||
_this.$refs['xterm-item'].close();
|
||||
},
|
||||
submitModify() {
|
||||
this.$refs.cmd.validate((valid) => {
|
||||
if (valid) {
|
||||
if (_this.cmd.id) {
|
||||
request({
|
||||
url: '/server/node/update',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("编辑成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
} else { // 新建
|
||||
request({
|
||||
url: '/server/node/save',
|
||||
method: 'post',
|
||||
data: _this.cmd
|
||||
}).then(res => {
|
||||
_this.$message.success("新增成功");
|
||||
_this.queryData();
|
||||
_this.cmdDialogVisible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -1,385 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
装备维修本体构建
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="15">
|
||||
<el-descriptions class="margin-top" title="本体信息统计" :column="2" border style="position: relative">
|
||||
<template slot="extra">
|
||||
<el-select v-model="ontologyId" placeholder="请选择图谱" @change="handleSelectChange"
|
||||
style="position: absolute;left:120px;top:-10px;">
|
||||
<el-option
|
||||
v-for="item in ontologyList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">本体名称:</template>
|
||||
<label>{{ ontology.name }}</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">概念数量:</template>
|
||||
<label>{{ ontologyCount.conceptc }}</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">本体版本:</template>
|
||||
<label>1.0</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">对象属性数量:</template>
|
||||
<label>{{ newNum.relationc }}</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">本体描述:</template>
|
||||
<label>构建了关于{{ontology.name}}的本体,包括{{ontologyCount.conceptc}}个概念和{{newNum.fieldc+newNum.relationc}}个属性</label>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">值属性数量:</template>
|
||||
<label>{{ newNum.fieldc }}</label>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="graph-box1">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="9">
|
||||
<el-descriptions class="margin-top desc2" title="本体元素浏览" :column="2" border>
|
||||
</el-descriptions>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="概念">
|
||||
<div class="tree-box">
|
||||
<el-tree :data="treeData" :props="defaultProps"
|
||||
@node-click="handlePopShow"
|
||||
:expand-on-click-node="false">
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="对象属性">
|
||||
<el-table :data="edgeData" style="width: 100%" class="right-table">
|
||||
<el-table-column type="index" label="行号" width="80"></el-table-column>
|
||||
<el-table-column prop="relation" label="属性名"></el-table-column>
|
||||
<el-table-column prop="srccount" label="起点概念数量"></el-table-column>
|
||||
<el-table-column prop="dstcount" label="终点概念数量"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="值属性">
|
||||
<el-table :data="valueData" style="width: 100%" class="right-table">
|
||||
<el-table-column type="index" label="行号" width="80"></el-table-column>
|
||||
<el-table-column prop="comment" label="属性名"></el-table-column>
|
||||
<el-table-column prop="count" label="所属概念数量"></el-table-column>
|
||||
<el-table-column prop="type" label="值类型"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="提示"
|
||||
:visible.sync="dialogVisible"
|
||||
@close="handlePopHide"
|
||||
width="400">
|
||||
<el-descriptions class="margin-top" :column="2" border style="margin-bottom: 15px">
|
||||
<el-descriptions-item label="概念名">{{ infoCmd.tagName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="概念描述">{{ infoCmd.label }}</el-descriptions-item>
|
||||
<el-descriptions-item label="次级概念数量">{{ edgeFieldCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="值属性数量">{{ fieldCount.length }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
:data="fieldCount"
|
||||
height="300"
|
||||
style="width: 100%">
|
||||
<el-table-column prop="field" label="属性名"></el-table-column>
|
||||
<el-table-column prop="comment" label="名称"></el-table-column>
|
||||
<el-table-column prop="type" label="类型"></el-table-column>
|
||||
</el-table>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import {listToTree} from '@/utils/common';
|
||||
import graph from "@/components/graph/Graph";
|
||||
|
||||
var _ = require('lodash');
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "noumenonModelImport",
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
fileList: [],
|
||||
edgeCount: 0,
|
||||
vertexCount: 0,
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
EQI_sourceType: 1
|
||||
},
|
||||
nodes: [],
|
||||
edges: [],
|
||||
treeData: [], // 概念树形
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
},
|
||||
edgeData: [],
|
||||
valueData: [],
|
||||
nodeInfo: {},
|
||||
fieldCount: [],
|
||||
edgeFieldCount: 0,
|
||||
ontologyList: [],
|
||||
ontologyId: '',
|
||||
ontology: {},
|
||||
ontologyCount: {
|
||||
"conceptc": "0",
|
||||
"relationc": "0",
|
||||
"fieldc": "0"
|
||||
},
|
||||
infoCmd: {
|
||||
tagName: '',
|
||||
label: ''
|
||||
},
|
||||
newNum:{
|
||||
"relationc": 0,
|
||||
"fieldc": 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryOntology();
|
||||
},
|
||||
methods: {
|
||||
handlePopHide() {
|
||||
_this.fieldCount = [];
|
||||
_this.edgeFieldCount = 0;
|
||||
},
|
||||
handlePopShow(row) {
|
||||
// console.log(row);
|
||||
_this.dialogVisible = true;
|
||||
request({
|
||||
url: `/ontology-concept/countchildbyid/${row.id}`,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.edgeFieldCount = res.data;
|
||||
});
|
||||
request({
|
||||
url: `/nebula_model/findnodebyid/${_this.ontologyId}/${row.ownId}`,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.infoCmd.tagName = _.get(res.data, "properties.tagName", '');
|
||||
_this.infoCmd.label = _.get(res.data, "properties.label", '');
|
||||
// _this.fieldCount = _.get(res.data, "properties.nebulafieldsnapshot", '');
|
||||
});
|
||||
request({
|
||||
url: `/nebula_model/findnebulafieldwithparents/${row.ownId}`,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.fieldCount = res.data;
|
||||
});
|
||||
},
|
||||
queryOntology() {
|
||||
request({
|
||||
url: `/ontology/query_list`,
|
||||
method: 'post',
|
||||
data: {EQI_sourceType: 1}
|
||||
}).then(res => {
|
||||
_this.ontologyList = res.data;
|
||||
if (_this.ontologyList.length > 0) {
|
||||
_this.handleSelectChange(_this.ontologyList[0].id);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSelectChange(ontologyId) {
|
||||
_this.ontologyId = ontologyId;
|
||||
let index = _this.ontologyList.findIndex(row => {
|
||||
return row.id === ontologyId;
|
||||
});
|
||||
if(index >= 0) {
|
||||
_this.ontology = _this.ontologyList[index];
|
||||
_this.queryCount();
|
||||
_this.queryIndicator2();
|
||||
_this.queryVertexTree();
|
||||
_this.queryGroupVertexEdgeCount();
|
||||
_this.queryData();
|
||||
} else {
|
||||
_this.$message.warning("数据异常");
|
||||
}
|
||||
|
||||
},
|
||||
queryCount() {
|
||||
request({
|
||||
url: '/ontology-concept/countallbyontologyid/' + _this.ontologyId,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.ontologyCount = res.data;
|
||||
});
|
||||
},
|
||||
queryGroupVertexEdgeCount() {
|
||||
request({
|
||||
url: '/nebula_model/groupcountvexbyedgebyontologyid/' + _this.ontologyId,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.edgeData = res.data;
|
||||
request({
|
||||
url: '/indicator/indicator2_kcrelation',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.edgeData = _this.edgeData.concat(res.data);
|
||||
_this.newNum.relationc = _this.edgeData.length;
|
||||
});
|
||||
});
|
||||
},
|
||||
queryIndicator2() {
|
||||
request({
|
||||
url: '/nebula_model/groupfieldbyontologyid/' + _this.ontologyId,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.valueData = res.data;
|
||||
request({
|
||||
url: '/indicator/indicator2_kcvalues',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.valueData = _this.valueData.concat(res.data);
|
||||
_this.newNum.fieldc = _this.valueData.length;
|
||||
});
|
||||
});
|
||||
// request({
|
||||
// url: '/indicator/indicator2',
|
||||
// method: 'get',
|
||||
// data: {}
|
||||
// }).then(res => {
|
||||
// _this.valueData = res.data;
|
||||
// });
|
||||
},
|
||||
queryVertexTree() {
|
||||
request({
|
||||
url: '/ontology-concept/query_list/' + _this.ontologyId,
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.treeData = listToTree(res.data, 'ownId');
|
||||
});
|
||||
// request({
|
||||
// url: '/nebula_model/findtreebyontologyid/' + _this.ontologyId,
|
||||
// method: 'post',
|
||||
// data: {}
|
||||
// }).then(res => {
|
||||
// res.data.nodes.forEach(node => {
|
||||
// node.label = node.properties.label || "-";
|
||||
// node.fieldCount = node.properties.nebulafieldsnapshot ? node.properties.nebulafieldsnapshot.length : 0;
|
||||
// });
|
||||
// _this.treeData = tripletToTree({
|
||||
// nodes: res.data.nodes,
|
||||
// relations: res.data.relations,
|
||||
// idName: 'vid',
|
||||
// sourceName: 'srcId',
|
||||
// targetName: 'dstId'
|
||||
// });
|
||||
// });
|
||||
},
|
||||
queryEdgeCount() { // 概念数量
|
||||
request({
|
||||
url: '/nebula_model/countedgesbyontologyid/' + _this.ontologyId,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.edgeCount = res.data;
|
||||
});
|
||||
},
|
||||
queryVertexCount() { // 值属性数量
|
||||
request({
|
||||
url: '/nebula_model/countvertexsbyontologyid/' + _this.ontologyId,
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.vertexCount = res.data;
|
||||
});
|
||||
},
|
||||
queryData(fileItem) {
|
||||
request({
|
||||
url: '/nebula_model/findrelationbyontologyid/' + _this.ontologyId,
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
let cmd = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
res.data.nodes.forEach(node => {
|
||||
cmd.nodes.push({
|
||||
id: node.vid,
|
||||
label: node.properties.label
|
||||
});
|
||||
|
||||
});
|
||||
res.data.relations.forEach(edge => {
|
||||
cmd.edges.push({
|
||||
source: edge.srcId,
|
||||
target: edge.dstId,
|
||||
label: edge.properties.label
|
||||
});
|
||||
});
|
||||
_this.$refs['graph'].initGraph(cmd, true, null, true);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.desc2 {
|
||||
.el-descriptions__title {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.tree-box {
|
||||
height: 615px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.right-table {
|
||||
height: 615px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.graph-box1 {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,351 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
事理图谱构建
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="10">
|
||||
<el-table :data="fileList" style="width: 100%" class="right-table">
|
||||
<el-table-column prop="name" label="文件名"></el-table-column>
|
||||
<el-table-column prop="size" label="文件大小"></el-table-column>
|
||||
<el-table-column prop="time" label="上传时间" width="150"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" style="float: left;margin-right: 5px" @click="clearData">清空数据</el-button>
|
||||
<el-upload
|
||||
style="float: left"
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:limit="3"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList">
|
||||
<el-button type="text">上传数据</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pie-box">
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="12">
|
||||
<div id="pie1" class="pie-item"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div id="pie2" class="pie-item"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-table :data="resultList" style="width: 100%">
|
||||
<el-table-column prop="trigger" label="事件触发词识别"></el-table-column>
|
||||
<el-table-column prop="event_type" label="事件分类"></el-table-column>
|
||||
<el-table-column prop="relation" label="事件元素角色识别"></el-table-column>
|
||||
<!-- <el-table-column label="操作">-->
|
||||
<!-- <template slot-scope="scope">-->
|
||||
<!-- <el-button type="text" style="float: left;margin-right: 5px">抽取</el-button>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
</el-table>
|
||||
</el-col>
|
||||
<el-col :span="14" class="graph-pa">
|
||||
<div class="graph-box">
|
||||
<graph ref="graph"></graph>
|
||||
</div>
|
||||
<el-card class="box-card-2" v-show="nodeInfoVisible">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>事理图谱节点属性</span>
|
||||
<el-button style="float: right; padding: 3px 0" type="text" @click="nodeInfoVisible = false">关闭</el-button>
|
||||
</div>
|
||||
<el-descriptions class="margin-top" :column="2" border style="margin-bottom: 15px">
|
||||
<el-descriptions-item>
|
||||
<template slot="label">事件类型</template>
|
||||
{{ nodeInfo._label }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">触发词</template>
|
||||
{{ nodeInfo.label }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="nodeInfo.arguments" border style="width: 100%">
|
||||
<el-table-column prop="text" label="事件论元" ></el-table-column>
|
||||
<el-table-column prop="role" label="论元角色" ></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request';
|
||||
import graph from "@/components/graph/Graph";
|
||||
import {formatTime} from "@/utils/common";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
components: {
|
||||
'graph': graph,
|
||||
},
|
||||
name: "reasonGraph",
|
||||
data() {
|
||||
return {
|
||||
originText: '',
|
||||
fileList: [
|
||||
{
|
||||
name: '暂无文件',
|
||||
size: '0',
|
||||
time: '-'
|
||||
}
|
||||
],
|
||||
resultList: [
|
||||
// {
|
||||
// trigger: '',
|
||||
// event_type: '',
|
||||
// relation: ''
|
||||
// }
|
||||
],
|
||||
nodeArguments: {},
|
||||
nodes: [],
|
||||
edges: [],
|
||||
nodeInfo: {
|
||||
arguments: [],
|
||||
label: '',
|
||||
_label: ''
|
||||
},
|
||||
nodeInfoVisible: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
},
|
||||
methods: {
|
||||
initRchart1(data) {
|
||||
let _data = [];
|
||||
data.forEach(item => {
|
||||
_data.push({
|
||||
name: item.name,
|
||||
value: item.count
|
||||
});
|
||||
});
|
||||
var chart = this.$echarts.init(document.getElementById("pie1"));
|
||||
var option = {
|
||||
title: {
|
||||
text: '事件类型',
|
||||
left: 'center',
|
||||
top: 30
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '事件类型',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: _data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
},
|
||||
initRchart2(data) {
|
||||
let _data = [];
|
||||
data.forEach(item => {
|
||||
_data.push({
|
||||
name: item.name,
|
||||
value: item.count
|
||||
});
|
||||
});
|
||||
var chart = this.$echarts.init(document.getElementById("pie2"));
|
||||
var option = {
|
||||
title: {
|
||||
text: '事理关系',
|
||||
left: 'center',
|
||||
top: 30
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '事理关系',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: _data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
},
|
||||
handleFileChange(file) {
|
||||
if(file.name != 'event_graph.json'){
|
||||
_this.$message.warning("上传文件不正确");
|
||||
return false;
|
||||
}
|
||||
_this.fileList = [file];
|
||||
_this.uploadFile(file);
|
||||
},
|
||||
uploadFile(file) {
|
||||
console.log(file);
|
||||
let formData = new FormData();
|
||||
formData.append("file", file.raw);
|
||||
request({
|
||||
url: '/indicator/indicator6',
|
||||
method: 'put',
|
||||
data: formData
|
||||
}).then(res => {
|
||||
_this.fileList = [
|
||||
{
|
||||
name: file.name,
|
||||
size: file.size + "kb",
|
||||
time: formatTime(new Date())
|
||||
}
|
||||
];
|
||||
_this.initRchart1(res.data.listmap1);
|
||||
_this.initRchart2(res.data.listmap2);
|
||||
let _map3 = res.data.map3;
|
||||
_this.resultList = [
|
||||
{
|
||||
trigger: _map3.sjcfclb,
|
||||
event_type: _map3.sjfl,
|
||||
relation: _map3.sjyslb
|
||||
}
|
||||
];
|
||||
_this.nodes = res.data.nodes;
|
||||
_this.edges = res.data.relations;
|
||||
_this.initGraphData();
|
||||
});
|
||||
},
|
||||
queryData(){
|
||||
request({
|
||||
url: '/indicator/indicator6_1',
|
||||
method: 'get',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.initRchart1(res.data.listmap1);
|
||||
_this.initRchart2(res.data.listmap2);
|
||||
let _map3 = res.data.map3;
|
||||
_this.resultList = [
|
||||
{
|
||||
trigger: _map3.sjcfclb,
|
||||
event_type: _map3.sjfl,
|
||||
relation: _map3.sjyslb
|
||||
}
|
||||
];
|
||||
_this.nodes = res.data.nodes;
|
||||
_this.edges = res.data.relations;
|
||||
_this.initGraphData();
|
||||
});
|
||||
},
|
||||
clearData() {
|
||||
_this.fileList = [
|
||||
{
|
||||
name: '暂无文件',
|
||||
size: '0',
|
||||
time: '-'
|
||||
}
|
||||
];
|
||||
_this.initRchart1([]);
|
||||
_this.initRchart2([]);
|
||||
_this.resultList = [];
|
||||
_this.nodes = [];
|
||||
_this.edges = [];
|
||||
_this.initGraphData();
|
||||
},
|
||||
clickNode(node) {
|
||||
let _arguments = _this.nodeArguments[node.id];
|
||||
_this.nodeInfo = {
|
||||
arguments: _arguments,
|
||||
label: node.label,
|
||||
_label: node._label
|
||||
};
|
||||
_this.nodeInfoVisible = true;
|
||||
},
|
||||
initGraphData() {
|
||||
let cmd = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
let nodeArguments = {};
|
||||
_this.nodes.forEach(node => {
|
||||
cmd.nodes.push({
|
||||
id: node.vid,
|
||||
label: node.properties.name,
|
||||
_label: node.labels
|
||||
});
|
||||
nodeArguments[node.vid] = node.properties.arguments;
|
||||
});
|
||||
_this.nodeArguments = nodeArguments;
|
||||
_this.edges.forEach(edge => {
|
||||
cmd.edges.push({
|
||||
source: edge.dstId,
|
||||
target: edge.srcId,
|
||||
label: edge.edgeName
|
||||
});
|
||||
});
|
||||
_this.$refs['graph'].initGraph(cmd, true, _this.clickNode ,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
font-size: 16px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.pie-box {
|
||||
height: 490px;
|
||||
|
||||
.pie-item {
|
||||
height: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-box {
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graph-pa {
|
||||
position: relative;
|
||||
.box-card-2 {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
top: 15%;
|
||||
bottom: 15%;
|
||||
overflow-y: auto;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,425 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-title">
|
||||
服务管理
|
||||
</div>
|
||||
<div class="menu-content">
|
||||
<div>
|
||||
<el-form :inline="true" :model="qo" class="demo-form-inline">
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入服务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="qo.LIKES_name" placeholder="请输入模型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="qo.pageNo=1;queryData()">搜 索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="result.records" style="width: 100%" ref="table">
|
||||
<el-table-column type="index" label="序号" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="服务名称"></el-table-column>
|
||||
<el-table-column prop="algorithmId" label="算法名称" :formatter="algorithmIdFormatter"></el-table-column>
|
||||
<!-- <el-table-column prop="version" label="版本"></el-table-column> -->
|
||||
<el-table-column prop="status" label="服务状态" :formatter="statusFormatter"></el-table-column>
|
||||
<el-table-column prop="url" label="接口地址"></el-table-column>
|
||||
<el-table-column prop="modifyTimeMillis" label="修改时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.modifyTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTimeMillis" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.createTimeMillis | timeFilter }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small"
|
||||
@click.native.prevent="updateStatus(scope.row, 'start')" v-if="scope.row.status != 3">
|
||||
启动
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="detailRow(scope.row)" type="text" size="small">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button @click.native.prevent="updateStatus(scope.row, 'stop')" type="text" size="small" v-if="scope.row.status == 3">
|
||||
停止
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-box">
|
||||
<el-pagination background
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="qo.pageNo"
|
||||
:page-size="qo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="result.total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
title="服务详情"
|
||||
:visible.sync="cmdDialogVisible"
|
||||
@close="handleLogDialogClose"
|
||||
width="60%">
|
||||
<el-form ref="cmd" label-width="80px" :rules="rules" :model="cmd">
|
||||
<el-form-item label="请求地址" prop="address">
|
||||
<el-input placeholder="请输入请求地址" v-model="cmd.address"
|
||||
disabled
|
||||
class="input-with-select" @input="change($event)">
|
||||
<el-select v-model="cmd.requestType" slot="prepend" placeholder="请选择" style="width: 100px;">
|
||||
<el-option label="POST" value="post"></el-option>
|
||||
<el-option label="GET" value="get"></el-option>
|
||||
<el-option label="PUT" value="put"></el-option>
|
||||
</el-select>
|
||||
<el-button slot="append" icon="el-icon-s-promotion" @click="submitHttp">发 送</el-button>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数">
|
||||
<el-table
|
||||
:data="params"
|
||||
ref="paramsTable"
|
||||
border
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%">
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="type"
|
||||
label="参数类型">
|
||||
<template slot-scope="scope">
|
||||
<el-select v-model="scope.row.type" placeholder="请选择参数类型" :disabled="scope.row.isDisabled">
|
||||
<el-option label="Int" value="int"></el-option>
|
||||
<el-option label="Long" value="long"></el-option>
|
||||
<el-option label="Double" value="double"></el-option>
|
||||
<el-option label="String" value="string"></el-option>
|
||||
<el-option label="Boolean" value="boolean"></el-option>
|
||||
<el-option label="Float" value="float"></el-option>
|
||||
<el-option label="Date" value="date"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="key"
|
||||
label="参数名">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.key" placeholder="请输入参数名" :disabled="scope.row.isDisabled"
|
||||
@change="(val) => handleParamKeyChange(scope.row, val)"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="value"
|
||||
label="参数值">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.index == 1">
|
||||
<el-select v-model="scope.row.value" placeholder="请选择数据集">
|
||||
<el-option :label="item.name" :value="item.id" :key="item.id"
|
||||
v-for="item in dataSetList"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-if="scope.row.index != 1">
|
||||
<template v-if="scope.row.type == 'boolean'">
|
||||
<el-select v-model="scope.row.value" placeholder="请选择参数值">
|
||||
<el-option label="True" value="true"></el-option>
|
||||
<el-option label="False" value="false"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-if="scope.row.type == 'date'">
|
||||
<el-date-picker
|
||||
v-model="scope.row.value"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间">
|
||||
</el-date-picker>
|
||||
</template>
|
||||
<template v-if="scope.row.type != 'date' && scope.row.type != 'boolean'">
|
||||
<el-input v-model="scope.row.value" placeholder="请输入参数值"></el-input>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item label="响应示例">
|
||||
<vue-json-editor
|
||||
v-model="resultJson"
|
||||
:mode="'code'"
|
||||
lang="zh"
|
||||
:show-btns="false"
|
||||
@json-change="onJsonChange"
|
||||
@json-save="onJsonSave"
|
||||
@has-error="onError"></vue-json-editor>
|
||||
</el-form-item>
|
||||
<div class="log-title-box">服务日志</div>
|
||||
<!-- <div class="log-box"></div>-->
|
||||
<log ref="log-item"></log>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
import request3 from '@/utils/request3';
|
||||
import vueJsonEditor from 'vue-json-editor';
|
||||
import log from '@/components/log/index';
|
||||
import request1, {getBaseUrl} from "@/utils/request";
|
||||
|
||||
var _this;
|
||||
export default {
|
||||
name: "serviceManage",
|
||||
components: {
|
||||
vueJsonEditor,
|
||||
log
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
qo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
LIKES_name: '',
|
||||
},
|
||||
result: {
|
||||
records: [],
|
||||
total: 0
|
||||
},
|
||||
cmdDialogVisible: false,
|
||||
cmd: {
|
||||
address: ''
|
||||
},
|
||||
params: [],
|
||||
paramsSelection: [],
|
||||
resultJson: {
|
||||
msg: 'success of test',
|
||||
},
|
||||
rules: {
|
||||
address: [
|
||||
{required: true, message: '请输入请求地址'}
|
||||
],
|
||||
},
|
||||
algorithmList: [],
|
||||
statusList: {
|
||||
1: '保存状态',
|
||||
2: '开始',
|
||||
3: '运行中',
|
||||
4: '等待中',
|
||||
5: '结束',
|
||||
6: '发布失败',
|
||||
7: '取消',
|
||||
8: '训练中',
|
||||
9: '训练完成',
|
||||
10: '停止'
|
||||
},
|
||||
dataSetList: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_this = this;
|
||||
_this.queryData();
|
||||
_this.queryAlgorithmList();
|
||||
_this.queryDataSetList();
|
||||
},
|
||||
|
||||
filters: {
|
||||
timeFilter(val) {
|
||||
if (val > 0) {
|
||||
let dt = new Date(val);
|
||||
let year = dt.getFullYear();
|
||||
let month = dt.getMonth() + 1;
|
||||
let date = dt.getDate();
|
||||
let hour = dt.getHours();
|
||||
let minute = dt.getMinutes();
|
||||
let second = dt.getSeconds();
|
||||
return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
queryDataSetList() {
|
||||
request1({
|
||||
url: '/remark_task/query_list',
|
||||
method: 'post',
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.dataSetList = res.data;
|
||||
});
|
||||
},
|
||||
statusFormatter(row) {
|
||||
return _this.statusList[row.status] || '-';
|
||||
},
|
||||
algorithmIdFormatter(row) {
|
||||
let index = _this.algorithmList.findIndex(dataSet => {
|
||||
return row.algorithmId == dataSet.id;
|
||||
});
|
||||
return index >= 0 ? _this.algorithmList[index].name : '-';
|
||||
},
|
||||
queryAlgorithmList() {
|
||||
request({
|
||||
url: '/algorithm/list',
|
||||
method: 'post',
|
||||
data: {pageNo: 1, pageSize: 1000}
|
||||
}).then(res => {
|
||||
_this.algorithmList = res.list;
|
||||
});
|
||||
},
|
||||
// 数据改变是触发
|
||||
onJsonChange(value) {
|
||||
// console.log('value:', value)
|
||||
},
|
||||
// 点击保存时触发
|
||||
onJsonSave(value) {
|
||||
// console.log('value:', value)
|
||||
},
|
||||
onError(value) {
|
||||
// console.log('value:', value)
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
_this.paramsSelection = val;
|
||||
},
|
||||
handleParamKeyChange(row) {
|
||||
if (_this.params.length == row.index) {
|
||||
_this.params.push({
|
||||
index: _this.params.length + 1,
|
||||
type: 'string',
|
||||
key: '',
|
||||
value: ''
|
||||
});
|
||||
_this.$refs.paramsTable.toggleRowSelection(_this.params[_this.params.length - 1]);
|
||||
}
|
||||
},
|
||||
queryData() {
|
||||
request({
|
||||
url: '/model/serviceList',
|
||||
method: 'post',
|
||||
data: _this.qo
|
||||
}).then(res => {
|
||||
_this.result.records = res.list;
|
||||
_this.result.total = res.total;
|
||||
});
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
_this.qo.pageNo = val;
|
||||
_this.queryData();
|
||||
},
|
||||
handleLogDialogClose() {
|
||||
_this.$refs['log-item'].close();
|
||||
},
|
||||
detailRow(item) {
|
||||
_this.cmdDialogVisible = true;
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs['log-item'].init({modelId: item.id, type: 'log_service'});
|
||||
_this.cmd = JSON.parse(JSON.stringify(item));
|
||||
if (!_this.cmd.requestType) {
|
||||
_this.cmd.requestType = "post";
|
||||
}
|
||||
_this.$refs.cmd.resetFields();
|
||||
_this.cmd.address = item.url;
|
||||
_this.params = [
|
||||
{
|
||||
index: 1,
|
||||
type: 'string',
|
||||
key: 'dataSetUrl',
|
||||
value: '',
|
||||
isDisabled: true
|
||||
}, {
|
||||
index: 2,
|
||||
type: 'string',
|
||||
key: '',
|
||||
value: ''
|
||||
}
|
||||
];
|
||||
_this.$nextTick(() => {
|
||||
_this.$refs.paramsTable.toggleRowSelection(_this.params[0]);
|
||||
_this.$refs.paramsTable.toggleRowSelection(_this.params[1]);
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
change(e) {
|
||||
_this.$forceUpdate(e);
|
||||
},
|
||||
submitHttp() {
|
||||
let params = "";
|
||||
for (let i = 0; i < _this.paramsSelection.length; i++) {
|
||||
let pa = _this.paramsSelection[i];
|
||||
if (pa.key.toString().length > 0 && pa.value.toString().length > 0) {
|
||||
params += i == 0 ? '?' : '&';
|
||||
if (pa.isDisabled) {
|
||||
params += (pa.key + "=" + getBaseUrl() + "/es_remark_task/findbytaskidandtasktypeall/" + pa.value + "/1");
|
||||
} else {
|
||||
params += (pa.key + "=" + pa.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
request3({
|
||||
url: _this.cmd.address + params,
|
||||
method: _this.cmd.requestType,
|
||||
data: {}
|
||||
}).then(res => {
|
||||
_this.$message.success('发送成功');
|
||||
_this.resultJson = res;
|
||||
});
|
||||
// console.log(_this.cmd);
|
||||
},
|
||||
updateStatus(item, action) { // 修改状态,重启start,停止stop
|
||||
let text = action === 'start' ? '启动' : '停止';
|
||||
_this.$confirm(`此操作将执行${text}操作, 是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: text+'中...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
request({
|
||||
url: '/server/update/status',
|
||||
method: 'post',
|
||||
data: {
|
||||
modelId: item.id,
|
||||
action: action
|
||||
}
|
||||
}).then(res => {
|
||||
_this.$message.success(`${text}成功`);
|
||||
loading.close();
|
||||
_this.queryData();
|
||||
});
|
||||
}).catch(() => {
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.jsoneditor-vue {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
|
||||
.jsoneditor-poweredBy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-title-box {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.log-box {
|
||||
background-color: #c2c2c2;
|
||||
border-radius: 3px;
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="xterm"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import request from '@/utils/request2';
|
||||
import {Terminal} from '../../utils/xterm/lib/xterm.js' //注意路径
|
||||
|
||||
var _this;
|
||||
var commandKey = [];
|
||||
var term;
|
||||
var socket;
|
||||
var sit;
|
||||
export default {
|
||||
name: "xterm",
|
||||
data() {
|
||||
return {
|
||||
id: ''
|
||||
}
|
||||
},
|
||||
// created() {
|
||||
// let arr = ['../../utils/xterm/lib/xterm.js',
|
||||
// '../../utils/xterm-addon/lib/xterm-addon-attach.js'];
|
||||
// arr.map((item) => {
|
||||
// let script = document.createElement('script');
|
||||
// script.type = 'text/javascript';
|
||||
// script.src = item;
|
||||
// document.getElementsByTagName('body')[0].appendChild(script);
|
||||
// });
|
||||
// },
|
||||
mounted() {
|
||||
_this = this;
|
||||
// _this.init();
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
if(sit) {
|
||||
clearInterval(sit);
|
||||
}
|
||||
if(socket) {
|
||||
socket.close();
|
||||
}
|
||||
},
|
||||
init(id) {
|
||||
_this.id = id;
|
||||
console.log(`当前节点ID为${id}`);
|
||||
socket = new WebSocket(`ws://47.103.128.32:10050/socket/ssh?id=${id}&nodeId=${id}&type=ssh`);
|
||||
//连接打开事件
|
||||
socket.onopen = function () {
|
||||
console.log("Socket 已打开");
|
||||
};
|
||||
//收到消息事件
|
||||
socket.onmessage = function (msg) {
|
||||
term.write(msg.data);//把接收的数据写到这个插件的屏幕上
|
||||
if (msg.data.indexOf('连接成功,sessionId=') != -1) {
|
||||
var sessionId = msg.data.slice(15);
|
||||
console.log(sessionId)
|
||||
} else {
|
||||
console.log("常规数据:" + msg.data);
|
||||
}
|
||||
};
|
||||
//连接关闭事件
|
||||
socket.onclose = function () {
|
||||
console.log("Socket已关闭");
|
||||
};
|
||||
//发生了错误事件
|
||||
socket.onerror = function () {
|
||||
alert("Socket发生了错误");
|
||||
}
|
||||
term = new Terminal({
|
||||
rendererType: "canvas", //渲染类型
|
||||
rows: parseInt(24), //行数
|
||||
cols: parseInt(100), // 不指定行数,自动回车后光标从下一行开始
|
||||
convertEol: true, //启用时,光标将设置为下一行的开头
|
||||
scrollback: 10, //终端中的回滚量
|
||||
disableStdin: false, //是否应禁用输入
|
||||
cursorStyle: "underline", //光标样式
|
||||
cursorBlink: true, //光标闪烁
|
||||
theme: {
|
||||
foreground: "yellow", //字体
|
||||
background: "#060101", //背景色
|
||||
cursor: "help" //设置光标
|
||||
}
|
||||
});
|
||||
// const attachAddon = new AttachAddon(socket);
|
||||
// term.loadAddon(attachAddon);
|
||||
term.open(document.getElementById("xterm"));
|
||||
// 支持输入与粘贴方法
|
||||
term.onData(function (key) {
|
||||
// console.log("|"+key+"|");
|
||||
// commandKey.push(key);
|
||||
// console.log(commandKey);
|
||||
// term.write(key);
|
||||
socket.send(key); //转换为字符串
|
||||
});
|
||||
|
||||
term.onLineFeed(function () {
|
||||
console.log("执行换行" + JSON.stringify(commandKey))
|
||||
});
|
||||
|
||||
term.onTitleChange(function (key) {
|
||||
console.log("onTitleChange:" + key);
|
||||
});
|
||||
|
||||
//创建心跳,防止掉线
|
||||
sit = setInterval(() => {
|
||||
let op = {
|
||||
data: "heart",
|
||||
};
|
||||
sendJson(op);
|
||||
}, 5000);
|
||||
|
||||
runFakeTerminal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendJson(data) {
|
||||
// 0 CONNECTING 连接尚未建立
|
||||
// 1 OPEN WebSocket的链接已经建立
|
||||
// 2 CLOSING 连接正在关闭
|
||||
// 3 CLOSED 连接已经关闭或不可用
|
||||
if(socket.readyState == 2 || socket.readyState == 3) {
|
||||
clearInterval(sit);
|
||||
sit = null;
|
||||
} else {
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function runFakeTerminal() {
|
||||
if (term._initialized) {
|
||||
return;
|
||||
}
|
||||
term._initialized = true;
|
||||
term.prompt = () => {
|
||||
term.write('\r\n~$ ');
|
||||
};
|
||||
term.writeln('Welcome to xterm.js');
|
||||
prompt(term);
|
||||
term.onKey(e => {
|
||||
const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
|
||||
if (e.domEvent.keyCode === 13) {
|
||||
prompt(term);
|
||||
} else if (e.domEvent.keyCode === 8) {
|
||||
// Do not delete the prompt
|
||||
if (term._core.buffer.x > 2) {
|
||||
// term.write('\b \b')
|
||||
}
|
||||
} else if (printable) {
|
||||
// term.write(e.key);
|
||||
}
|
||||
console.log(commandKey);
|
||||
console.log("key::" + e.domEvent.keyCode);
|
||||
});
|
||||
}
|
||||
|
||||
function prompt(term) {
|
||||
// term.write('\r\n~$ ');
|
||||
// term.focus();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../utils/xterm/css/xterm.css';
|
||||
</style>
|
||||
@@ -39,91 +39,6 @@ export default new Router({
|
||||
path: 'graphSpace_g',
|
||||
component: () => import('@/components/graph/GraphSpace')
|
||||
},
|
||||
{
|
||||
path: 'graph_x6',
|
||||
component: () => import('@/components/graph/Graph_x6')
|
||||
},
|
||||
{
|
||||
path: 'textTag',
|
||||
component: () => import('@/components/menus/TextTag')
|
||||
},
|
||||
{
|
||||
path: 'dataSetManage',
|
||||
component: () => import('@/components/menus/DataSetManage')
|
||||
},
|
||||
{
|
||||
path: 'dataSetCompare',
|
||||
component: () => import('@/components/menus/DataSetCompare')
|
||||
},
|
||||
{
|
||||
path: 'algorithmSpeedVerification',
|
||||
component: () => import('@/components/menus/AlgorithmSpeedVerification')
|
||||
},
|
||||
{
|
||||
path: 'noumenonModelImport',
|
||||
component: () => import('@/components/menus/NoumenonModelImport')
|
||||
},
|
||||
{
|
||||
path: 'dataRepeatRate',
|
||||
component: () => import('@/components/menus/DataRepeatRate')
|
||||
},
|
||||
{
|
||||
path: 'knowledgeFusion',
|
||||
component: () => import('@/components/menus/KnowledgeFusion')
|
||||
},
|
||||
{
|
||||
path: 'graphAssessment',
|
||||
component: () => import('@/components/menus/GraphAssessment')
|
||||
},
|
||||
{
|
||||
path: 'reasonGraph',
|
||||
component: () => import('@/components/menus/ReasonGraph')
|
||||
},
|
||||
{
|
||||
path: 'graphRelation',
|
||||
component: () => import('@/components/menus/GraphRelation')
|
||||
},
|
||||
{
|
||||
path: 'graphPath',
|
||||
component: () => import('@/components/menus/GraphPath')
|
||||
},
|
||||
{
|
||||
path: 'knowledgeQuestion',
|
||||
component: () => import('@/components/menus/KnowledgeQuestion')
|
||||
},
|
||||
{
|
||||
path: 'graphRecommend',
|
||||
component: () => import('@/components/menus/GraphRecommend')
|
||||
},
|
||||
{
|
||||
path: 'graphShow',
|
||||
component: () => import('@/components/menus/GraphShow')
|
||||
},
|
||||
{
|
||||
path: 'graphExhibition',
|
||||
component: () => import('@/components/menus/GraphExhibition')
|
||||
},
|
||||
|
||||
{
|
||||
path: 'algorithmPublishing',
|
||||
component: () => import('@/components/menus/AlgorithmPublishing')
|
||||
},
|
||||
{
|
||||
path: 'codeWarehouse',
|
||||
component: () => import('@/components/menus/CodeWarehouse')
|
||||
},
|
||||
{
|
||||
path: 'modelManage',
|
||||
component: () => import('@/components/menus/ModelManage')
|
||||
},
|
||||
{
|
||||
path: 'nodeManage',
|
||||
component: () => import('@/components/menus/NodeManage')
|
||||
},
|
||||
{
|
||||
path: 'serviceManage',
|
||||
component: () => import('@/components/menus/ServiceManage')
|
||||
},
|
||||
{
|
||||
path: 'graphSpaceManage',
|
||||
component: () => import('@/components/menus/GraphSpaceManage')
|
||||
@@ -134,36 +49,6 @@ export default new Router({
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/614',
|
||||
component: () => import('@/views/Index'),
|
||||
children: [
|
||||
{
|
||||
path: 'indexPage',
|
||||
component: () => import('@/components/menus/614/IndexPage')
|
||||
},
|
||||
{
|
||||
path: 'infoExtract',
|
||||
component: () => import('@/components/menus/614/InfoExtract')
|
||||
},
|
||||
{
|
||||
path: 'linkPrediction',
|
||||
component: () => import('@/components/menus/614/LinkPrediction')
|
||||
},
|
||||
{
|
||||
path: 'infoConverge',
|
||||
component: () => import('@/components/menus/614/InfoConverge')
|
||||
},
|
||||
{
|
||||
path: 'auxiliaryMaintenance',
|
||||
component: () => import('@/components/menus/614/AuxiliaryMaintenance')
|
||||
},
|
||||
{
|
||||
path: 'riskPrediction',
|
||||
component: () => import('@/components/menus/614/RiskPrediction')
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
|
||||
title: '基于知识图谱的军事认知智能原型系统'
|
||||
title: '推荐系统'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const TokenKey = 'metadata_admin_web_token'
|
||||
const TokenKey = 'recom_gorse_admin_web_token'
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TokenKey)
|
||||
|
||||
@@ -9,133 +9,6 @@ const menuList = [
|
||||
{index: '1-3', name: "知识图谱导入", icon: "el-icon-user", url: "/graphImport"},
|
||||
]
|
||||
},
|
||||
{
|
||||
index: "2",
|
||||
icon: "el-icon-monitor",
|
||||
name: "数据服务",
|
||||
list: [
|
||||
{index: '2-1', name: "数据集管理", icon: "el-icon-user", url: "/dataSetManage"},
|
||||
// {index: '2-2', name: "文本标注", icon: "el-icon-user", url: "/textTag"},
|
||||
]
|
||||
},
|
||||
{
|
||||
index: "21",
|
||||
icon: "el-icon-monitor",
|
||||
name: "算法管理",
|
||||
list: [
|
||||
{index: '21-1', name: "代码仓库", icon: "el-icon-user", url: "/codeWarehouse"},
|
||||
{index: '21-2', name: "算法发布", icon: "el-icon-user", url: "/algorithmPublishing"},
|
||||
]
|
||||
},
|
||||
{
|
||||
index: "22",
|
||||
icon: "el-icon-monitor",
|
||||
name: "节点管理",
|
||||
url: "/nodeManage"
|
||||
},
|
||||
{
|
||||
index: "23",
|
||||
icon: "el-icon-monitor",
|
||||
name: "模型管理",
|
||||
url: "/modelManage"
|
||||
},
|
||||
{
|
||||
index: "24",
|
||||
icon: "el-icon-monitor",
|
||||
name: "服务管理",
|
||||
url: "/serviceManage"
|
||||
},
|
||||
{
|
||||
index: "3",
|
||||
icon: "el-icon-monitor",
|
||||
name: "低资源场景知识抽取",//技术指标1
|
||||
url: "/dataSetCompare"
|
||||
},
|
||||
{
|
||||
index: "4",
|
||||
icon: "el-icon-monitor",
|
||||
name: "装备维修本体构建",//技术指标2
|
||||
url: "/noumenonModelImport"
|
||||
},
|
||||
{
|
||||
index: "5",
|
||||
icon: "el-icon-monitor",
|
||||
name: "离散知识补充",//技术指标3
|
||||
url: "/dataRepeatRate"
|
||||
},
|
||||
{
|
||||
index: "6",
|
||||
icon: "el-icon-monitor",
|
||||
name: "知识图谱融合",//技术指标4
|
||||
url: "/knowledgeFusion"
|
||||
},
|
||||
{
|
||||
index: "7",
|
||||
icon: "el-icon-monitor",
|
||||
name: "知识图谱质量评估",//技术指标5
|
||||
url: "/graphAssessment"
|
||||
},
|
||||
{
|
||||
index: "8",
|
||||
icon: "el-icon-monitor",
|
||||
name: "事理图谱构建",//技术指标6
|
||||
url: "/reasonGraph"
|
||||
},
|
||||
{
|
||||
index: "9",
|
||||
icon: "el-icon-monitor",
|
||||
name: "知识图谱关联",//技术指标7
|
||||
url: "/graphRelation"
|
||||
},
|
||||
{
|
||||
index: "10",
|
||||
icon: "el-icon-monitor",
|
||||
name: "知识图谱推理",//技术指标8
|
||||
url: "/graphPath"
|
||||
},
|
||||
{
|
||||
index: "11",
|
||||
icon: "el-icon-monitor",
|
||||
name: "知识检索和问答",//技术指标9
|
||||
url: "/knowledgeQuestion"
|
||||
},
|
||||
{
|
||||
index: "12",
|
||||
icon: "el-icon-monitor",
|
||||
name: "三元组高速抽取",//技术指标10
|
||||
url: "/algorithmSpeedVerification"
|
||||
},
|
||||
// {
|
||||
// index: "13",
|
||||
// icon: "el-icon-monitor",
|
||||
// name: "武器装备维修保障知识抽取",//验证指标1
|
||||
// url: "/graphExhibition"
|
||||
// },
|
||||
// {
|
||||
// index: "14",
|
||||
// icon: "el-icon-monitor",
|
||||
// name: "航发控制装备维修知识补全",//验证指标2
|
||||
// url: "/graphShow"
|
||||
// },
|
||||
// {
|
||||
// index: "15",
|
||||
// icon: "el-icon-monitor",
|
||||
// name: "辅助维修决策",//验证指标3
|
||||
// url: "/graphRecommend"
|
||||
// },
|
||||
{
|
||||
index: "16",
|
||||
icon: "el-icon-monitor",
|
||||
name: "装备维修保障辅助决策平台",
|
||||
list: [
|
||||
{index: '16-1', name: "首页", icon: "el-icon-user", url: "/614/indexPage"},
|
||||
{index: '16-2', name: "知识抽取", icon: "el-icon-user", url: "/614/infoExtract"},
|
||||
{index: '16-3', name: "链接预测", icon: "el-icon-user", url: "/614/linkPrediction"},
|
||||
{index: '16-4', name: "信息汇聚", icon: "el-icon-user", url: "/614/infoConverge"},
|
||||
{index: '16-5', name: "辅助维修", icon: "el-icon-user", url: "/614/auxiliaryMaintenance"},
|
||||
{index: '16-6', name: "风险预测", icon: "el-icon-user", url: "/614/riskPrediction"}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
/*返回当前菜单列表*/
|
||||
|
||||
@@ -3,8 +3,8 @@ import {MessageBox, Message} from 'element-ui'
|
||||
import store from '@/store'
|
||||
import {getToken} from '@/utils/auth'
|
||||
|
||||
// var _baseURL = "http://localhost:4026";
|
||||
var _baseURL = "http://118.31.22.243/api/";
|
||||
var _baseURL = "http://localhost:4026";
|
||||
// var _baseURL = "http://118.31.22.243/api/";
|
||||
var _fileURL = "http://118.31.22.243:80/web/";
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import {MessageBox, Message} from 'element-ui'
|
||||
import store from '@/store'
|
||||
import {getToken} from '@/utils/auth'
|
||||
|
||||
var _baseURL = "http://118.31.22.243:10050";
|
||||
// var _baseURL = "http://localhost:10050";
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
baseURL: _baseURL, // url = base url + request url
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 1000000 // request timeout
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// do something before request is sent
|
||||
if (store.getters.token) {
|
||||
// let each request carry token
|
||||
// ['X-Token'] is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
config.headers['Authorization'] = getToken()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// do something with request error
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
/**
|
||||
* If you want to get http information such as headers or status
|
||||
* Please return response => response
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine the request status by custom code
|
||||
* Here is just an example
|
||||
* You can also judge the status by HTTP Status Code
|
||||
*/
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// if the custom code is not 20000, it is judged as an error.
|
||||
if (res.code.toString() !== "1") {
|
||||
Message({
|
||||
message: res.msg || '系统错误',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
|
||||
if (res.code == "UNAUTHORIZED") {
|
||||
store.dispatch('user/resetToken').then(() => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
Message({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service;
|
||||
|
||||
// return url prefix
|
||||
export function getBaseUrl() {
|
||||
return _baseURL;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import {MessageBox, Message} from 'element-ui'
|
||||
import store from '@/store'
|
||||
import {getToken} from '@/utils/auth'
|
||||
|
||||
var _baseURL = "http://118.31.22.243:10050";
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
baseURL: _baseURL, // url = base url + request url
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 1000000 // request timeout
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// do something before request is sent
|
||||
if (store.getters.token) {
|
||||
// let each request carry token
|
||||
// ['X-Token'] is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
config.headers['Authorization'] = getToken()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// do something with request error
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
/**
|
||||
* If you want to get http information such as headers or status
|
||||
* Please return response => response
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine the request status by custom code
|
||||
* Here is just an example
|
||||
* You can also judge the status by HTTP Status Code
|
||||
*/
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
Message({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service;
|
||||
|
||||
// return url prefix
|
||||
export function getBaseUrl() {
|
||||
return _baseURL;
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import {MessageBox, Message} from 'element-ui'
|
||||
import store from '@/store'
|
||||
import {getToken} from '@/utils/auth'
|
||||
|
||||
// var _baseURL = "http://localhost:4026";
|
||||
var _baseURL = "http://118.31.22.243:40999/";
|
||||
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
baseURL: _baseURL, // url = base url + request url
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 1000000 // request timeout
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// do something before request is sent
|
||||
if (store.getters.token) {
|
||||
// let each request carry token
|
||||
// ['X-Token'] is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
config.headers['Authorization'] = getToken()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// do something with request error
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
/**
|
||||
* If you want to get http information such as headers or status
|
||||
* Please return response => response
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine the request status by custom code
|
||||
* Here is just an example
|
||||
* You can also judge the status by HTTP Status Code
|
||||
*/
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// if the custom code is not 20000, it is judged as an error.
|
||||
if (res.code !== "SUCCESS") {
|
||||
Message({
|
||||
message: res.msg || '系统错误',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
|
||||
if (res.code == "UNAUTHORIZED") {
|
||||
store.dispatch('user/resetToken').then(() => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
Message({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service;
|
||||
|
||||
// return url prefix
|
||||
export function getBaseUrl() {
|
||||
return _baseURL;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,22 +0,0 @@
|
||||
## xterm-addon-attach
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-attach
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
|
||||
const terminal = new Terminal();
|
||||
const attachAddon = new AttachAddon(webSocket);
|
||||
terminal.loadAddon(attachAddon);
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts) for more advanced usage.
|
||||
@@ -1,2 +0,0 @@
|
||||
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.AttachAddon=e():t.AttachAddon=e()}(window,(function(){return function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(o,r,function(e){return t[e]}.bind(null,r));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AttachAddon=void 0;var o=function(){function t(t,e){this._disposables=[],this._socket=t,this._socket.binaryType="arraybuffer",this._bidirectional=!e||!1!==e.bidirectional}return t.prototype.activate=function(t){var e=this;this._disposables.push(r(this._socket,"message",(function(e){var n=e.data;t.write("string"==typeof n?n:new Uint8Array(n))}))),this._bidirectional&&(this._disposables.push(t.onData((function(t){return e._sendData(t)}))),this._disposables.push(t.onBinary((function(t){return e._sendBinary(t)})))),this._disposables.push(r(this._socket,"close",(function(){return e.dispose()}))),this._disposables.push(r(this._socket,"error",(function(){return e.dispose()})))},t.prototype.dispose=function(){this._disposables.forEach((function(t){return t.dispose()}))},t.prototype._sendData=function(t){1===this._socket.readyState&&this._socket.send(t)},t.prototype._sendBinary=function(t){if(1===this._socket.readyState){for(var e=new Uint8Array(t.length),n=0;n<t.length;++n)e[n]=255&t.charCodeAt(n);this._socket.send(e)}},t}();function r(t,e,n){return t.addEventListener(e,n),{dispose:function(){n&&t.removeEventListener(e,n)}}}e.AttachAddon=o}])}));
|
||||
//# sourceMappingURL=xterm-addon-attach.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"AttachAddon.api.js","sourceRoot":"","sources":["../src/AttachAddon.api.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,8BAAiC;AACjC,6DAAwF;AAGxF,IAAM,GAAG,GAAG,4BAA4B,CAAC;AAEzC,IAAI,OAAgB,CAAC;AACrB,IAAI,IAAU,CAAC;AACf,IAAM,KAAK,GAAG,GAAG,CAAC;AAClB,IAAM,MAAM,GAAG,GAAG,CAAC;AAEnB,QAAQ,CAAC,aAAa,EAAE;IACtB,MAAM,CAAC;;;;;;wBACC,WAAW,GAAG,0BAAc,EAAE,CAAC;wBAC3B,WAAM,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI;gCAC/C,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;6BACpD,CAAC,EAAA;;wBAFF,OAAO,GAAG,SAER,CAAC;wBACW,WAAM,OAAO,CAAC,UAAU,EAAE,EAAA;4BAAjC,WAAM,CAAC,SAA0B,CAAC,CAAC,OAAO,EAAE,EAAA;;wBAAnD,IAAI,GAAG,SAA4C,CAAC;wBACpD,WAAM,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,OAAA,EAAE,MAAM,QAAA,EAAE,CAAC,EAAA;;wBAA7C,SAA6C,CAAC;;;;;KAC/C,CAAC,CAAC;IAEH,KAAK,CAAC;;;wBACJ,WAAM,OAAO,CAAC,KAAK,EAAE,EAAA;;oBAArB,SAAqB,CAAC;;;;SACvB,CAAC,CAAC;IAEH,UAAU,CAAC;;oBAAY,WAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAA;oBAApB,WAAA,SAAoB,EAAA;;aAAA,CAAC,CAAC;IAE7C,EAAE,CAAC,QAAQ,EAAE;;;;;4BACX,WAAM,wBAAY,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,EAAA;;wBAAjD,SAAiD,CAAC;wBAC5C,IAAI,GAAG,IAAI,CAAC;wBACZ,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,MAAA,EAAE,CAAC,CAAC;wBAC9C,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAlB,CAAkB,CAAC,CAAC;wBACtD,WAAM,IAAI,CAAC,QAAQ,CAAC,gFAA8E,IAAI,SAAM,CAAC,EAAA;;wBAA7G,SAA6G,CAAC;wBAC9G,WAAM,mBAAO,CAAC,IAAI,EAAE,8DAA8D,EAAE,KAAK,CAAC,EAAA;;wBAA1F,SAA0F,CAAC;wBAC3F,MAAM,CAAC,KAAK,EAAE,CAAC;;;;;KAChB,CAAC,CAAC;IAEH,EAAE,CAAC,MAAM,EAAE;;;;;4BACT,WAAM,wBAAY,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,EAAA;;wBAAjD,SAAiD,CAAC;wBAC5C,IAAI,GAAG,IAAI,CAAC;wBACZ,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,MAAA,EAAE,CAAC,CAAC;wBACxC,IAAI,GAAG,IAAI,UAAU,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;wBAC7C,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAjB,CAAiB,CAAC,CAAC;wBACrD,WAAM,IAAI,CAAC,QAAQ,CAAC,gFAA8E,IAAI,SAAM,CAAC,EAAA;;wBAA7G,SAA6G,CAAC;wBAC9G,WAAM,mBAAO,CAAC,IAAI,EAAE,8DAA8D,EAAE,KAAK,CAAC,EAAA;;wBAA1F,SAA0F,CAAC;wBAC3F,MAAM,CAAC,KAAK,EAAE,CAAC;;;;;KAChB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AttachAddon = void 0;
|
||||
var AttachAddon = (function () {
|
||||
function AttachAddon(socket, options) {
|
||||
this._disposables = [];
|
||||
this._socket = socket;
|
||||
this._socket.binaryType = 'arraybuffer';
|
||||
this._bidirectional = (options && options.bidirectional === false) ? false : true;
|
||||
}
|
||||
AttachAddon.prototype.activate = function (terminal) {
|
||||
var _this = this;
|
||||
this._disposables.push(addSocketListener(this._socket, 'message', function (ev) {
|
||||
var data = ev.data;
|
||||
terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
|
||||
}));
|
||||
if (this._bidirectional) {
|
||||
this._disposables.push(terminal.onData(function (data) { return _this._sendData(data); }));
|
||||
this._disposables.push(terminal.onBinary(function (data) { return _this._sendBinary(data); }));
|
||||
}
|
||||
this._disposables.push(addSocketListener(this._socket, 'close', function () { return _this.dispose(); }));
|
||||
this._disposables.push(addSocketListener(this._socket, 'error', function () { return _this.dispose(); }));
|
||||
};
|
||||
AttachAddon.prototype.dispose = function () {
|
||||
this._disposables.forEach(function (d) { return d.dispose(); });
|
||||
};
|
||||
AttachAddon.prototype._sendData = function (data) {
|
||||
if (this._socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
};
|
||||
AttachAddon.prototype._sendBinary = function (data) {
|
||||
if (this._socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
var buffer = new Uint8Array(data.length);
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255;
|
||||
}
|
||||
this._socket.send(buffer);
|
||||
};
|
||||
return AttachAddon;
|
||||
}());
|
||||
exports.AttachAddon = AttachAddon;
|
||||
function addSocketListener(socket, type, handler) {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: function () {
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=AttachAddon.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"AttachAddon.js","sourceRoot":"","sources":["../src/AttachAddon.ts"],"names":[],"mappings":";;;AAaA;IAKE,qBAAY,MAAiB,EAAE,OAAwB;QAF/C,iBAAY,GAAkB,EAAE,CAAC;QAGvC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QAEtB,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,aAAa,CAAC;QACxC,IAAI,CAAC,cAAc,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,aAAa,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACpF,CAAC;IAEM,8BAAQ,GAAf,UAAgB,QAAkB;QAAlC,iBAeC;QAdC,IAAI,CAAC,YAAY,CAAC,IAAI,CACpB,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,UAAA,EAAE;YAC3C,IAAM,IAAI,GAAyB,EAAE,CAAC,IAAI,CAAC;YAC3C,QAAQ,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACzE,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,IAAI,CAAC,cAAc,EAAE;YACvB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAA,IAAI,IAAI,OAAA,KAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAApB,CAAoB,CAAC,CAAC,CAAC;YACtE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAA,IAAI,IAAI,OAAA,KAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAtB,CAAsB,CAAC,CAAC,CAAC;SAC3E;QAED,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,cAAM,OAAA,KAAI,CAAC,OAAO,EAAE,EAAd,CAAc,CAAC,CAAC,CAAC;QACvF,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,cAAM,OAAA,KAAI,CAAC,OAAO,EAAE,EAAd,CAAc,CAAC,CAAC,CAAC;IACzF,CAAC;IAEM,6BAAO,GAAd;QACE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,UAAA,CAAC,IAAI,OAAA,CAAC,CAAC,OAAO,EAAE,EAAX,CAAW,CAAC,CAAC;IAC9C,CAAC;IAEO,+BAAS,GAAjB,UAAkB,IAAY;QAG5B,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,CAAC,EAAE;YACjC,OAAO;SACR;QACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEO,iCAAW,GAAnB,UAAoB,IAAY;QAC9B,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,CAAC,EAAE;YACjC,OAAO;SACR;QACD,IAAM,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;YACpC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;SACtC;QACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IACH,kBAAC;AAAD,CAAC,AApDD,IAoDC;AApDY,kCAAW;AAsDxB,SAAS,iBAAiB,CAAoC,MAAiB,EAAE,IAAO,EAAE,OAA2D;IACnJ,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACvC,OAAO;QACL,OAAO,EAAE;YACP,IAAI,CAAC,OAAO,EAAE;gBAEZ,OAAO;aACR;YACD,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "xterm-addon-attach",
|
||||
"version": "0.6.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-attach.js",
|
||||
"types": "typings/xterm-addon-attach.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014, 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
|
||||
|
||||
interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon {
|
||||
private _socket: WebSocket;
|
||||
private _bidirectional: boolean;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
||||
this._socket = socket;
|
||||
// always set binary type to arraybuffer, we do not handle blobs
|
||||
this._socket.binaryType = 'arraybuffer';
|
||||
this._bidirectional = (options && options.bidirectional === false) ? false : true;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._disposables.push(
|
||||
addSocketListener(this._socket, 'message', ev => {
|
||||
const data: ArrayBuffer | string = ev.data;
|
||||
terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
|
||||
})
|
||||
);
|
||||
|
||||
if (this._bidirectional) {
|
||||
this._disposables.push(terminal.onData(data => this._sendData(data)));
|
||||
this._disposables.push(terminal.onBinary(data => this._sendBinary(data)));
|
||||
}
|
||||
|
||||
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
|
||||
this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
// TODO: do something better than just swallowing
|
||||
// the data if the socket is not in a working condition
|
||||
if (this._socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
private _sendBinary(data: string): void {
|
||||
if (this._socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
const buffer = new Uint8Array(data.length);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255;
|
||||
}
|
||||
this._socket.send(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-attach' {
|
||||
export interface IAttachOptions {
|
||||
/**
|
||||
* Whether input should be written to the backend. Defaults to `true`.
|
||||
*/
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon {
|
||||
constructor(socket: WebSocket, options?: IAttachOptions);
|
||||
public activate(terminal: Terminal): void;
|
||||
public dispose(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,190 +0,0 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
|
||||
## Features
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
|
||||
## What xterm.js is not
|
||||
|
||||
- Xterm.js is not a terminal application that you can download and use on your computer.
|
||||
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
|
||||
|
||||
```
|
||||
npm install xterm
|
||||
```
|
||||
|
||||
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
|
||||
<script src="node_modules/xterm/lib/xterm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal"></div>
|
||||
<script>
|
||||
var term = new Terminal();
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Importing
|
||||
|
||||
The recommended way to load xterm.js is via the ES6 module syntax:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
### Addons
|
||||
|
||||
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
|
||||
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project:
|
||||
|
||||
```bash
|
||||
npm i -S xterm-addon-web-links
|
||||
```
|
||||
|
||||
Then import the addon, instantiate it and call `Terminal.loadAddon`:
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
|
||||
const terminal = new Terminal();
|
||||
// Load WebLinksAddon on terminal, this is all that's needed to get web links
|
||||
// working in the terminal.
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
```
|
||||
|
||||
The xterm.js team maintains the following addons but they can be built by anyone:
|
||||
|
||||
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
|
||||
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
|
||||
- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality
|
||||
- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction
|
||||
|
||||
## Browser Support
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
|
||||
|
||||
We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
||||
|
||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
|
||||
|
||||
## API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
|
||||
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
||||
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
||||
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
|
||||
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js.
|
||||
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
|
||||
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
||||
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
||||
- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
|
||||
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
|
||||
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
|
||||
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
|
||||
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
|
||||
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
|
||||
- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom.
|
||||
- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017.
|
||||
- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React.
|
||||
- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm.
|
||||
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
|
||||
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
|
||||
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
|
||||
- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web.
|
||||
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS.
|
||||
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
|
||||
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux.
|
||||
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users.
|
||||
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
|
||||
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
|
||||
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
|
||||
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
|
||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
|
||||
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
||||
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
|
||||
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
|
||||
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
|
||||
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
|
||||
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
|
||||
- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD
|
||||
- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access.
|
||||
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
|
||||
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
|
||||
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `<ng-terminal></ng-terminal>` into your component.
|
||||
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
|
||||
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
|
||||
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
|
||||
|
||||
```
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||
|
||||
## License Agreement
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
@@ -1,171 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
/*
|
||||
* HACK: to fix IE's blinking cursor
|
||||
* Move textarea out of the screen to the far left, so that the cursor is not visible.
|
||||
*/
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,297 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Strings from './browser/LocalizableStrings';
|
||||
import { ITerminal } from './Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { RenderDebouncer } from 'browser/RenderDebouncer';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
const enum BoundaryPosition {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
export class AccessibilityManager extends Disposable {
|
||||
private _accessibilityTreeRoot: HTMLElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[];
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
/**
|
||||
* This queue has a character pushed to it for keys that are pressed, if the
|
||||
* next character added to the terminal is equal to the key char then it is
|
||||
* not announced (added to live region) because it has already been announced
|
||||
* by the textarea event (which cannot be canceled). There are some race
|
||||
* condition cases if there is typing while data is streaming, but this covers
|
||||
* the main case of typing into the prompt and inputting the answer to a
|
||||
* question (Y/N, etc.).
|
||||
*/
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
private _charsToAnnounce: string = '';
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: ITerminal,
|
||||
private readonly _renderService: IRenderService
|
||||
) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.classList.add('xterm-accessibility-tree');
|
||||
this._rowElements = [];
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
this._liveRegion.classList.add('live-region');
|
||||
this._liveRegion.setAttribute('aria-live', 'assertive');
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.onA11yChar(char => this._onChar(char)));
|
||||
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
|
||||
this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._onKey(e.key)));
|
||||
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this.register(this._screenDprMonitor);
|
||||
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the ScreenDprMonitor isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._terminal.element.removeChild(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = <HTMLElement>e.target;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
const posInSet = boundaryElement.getAttribute('aria-posinset');
|
||||
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
|
||||
if (posInSet === lastRowPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't scroll when the last focused item was not the second row (focus is going the other
|
||||
// direction)
|
||||
if (e.relatedTarget !== beforeBoundaryElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old boundary element from array
|
||||
let topBoundaryElement: HTMLElement;
|
||||
let bottomBoundaryElement: HTMLElement;
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
topBoundaryElement = boundaryElement;
|
||||
bottomBoundaryElement = this._rowElements.pop()!;
|
||||
this._rowContainer.removeChild(bottomBoundaryElement);
|
||||
} else {
|
||||
topBoundaryElement = this._rowElements.shift()!;
|
||||
bottomBoundaryElement = boundaryElement;
|
||||
this._rowContainer.removeChild(topBoundaryElement);
|
||||
}
|
||||
|
||||
// Remove listeners from old boundary elements
|
||||
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
|
||||
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Add new element to array/DOM
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.unshift(newElement);
|
||||
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
|
||||
} else {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.push(newElement);
|
||||
this._rowContainer.appendChild(newElement);
|
||||
}
|
||||
|
||||
// Add listeners to new boundary elements
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Scroll up
|
||||
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
|
||||
|
||||
// Focus new boundary before element
|
||||
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
|
||||
|
||||
// Prevent the standard behavior
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Grow rows as required
|
||||
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
// Shrink rows as required
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop()!);
|
||||
}
|
||||
|
||||
// Add bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
private _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
this._refreshRowDimensions(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _onTab(spaceCount: number): void {
|
||||
for (let i = 0; i < spaceCount; i++) {
|
||||
this._onChar(' ');
|
||||
}
|
||||
}
|
||||
|
||||
private _onChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
if (shiftedChar !== char) {
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
} else {
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
|
||||
if (char === '\n') {
|
||||
this._liveRegionLineCount++;
|
||||
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
|
||||
this._liveRegion.textContent += Strings.tooMuchOutput;
|
||||
}
|
||||
}
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
|
||||
setTimeout(() => {
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearLiveRegion(): void {
|
||||
this._liveRegion.textContent = '';
|
||||
this._liveRegionLineCount = 0;
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.parentNode) {
|
||||
this._accessibilityTreeRoot.removeChild(this._liveRegion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onKey(keyChar: string): void {
|
||||
this._clearLiveRegion();
|
||||
this._charsToConsume.push(keyChar);
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const setSize = buffer.lines.length.toString();
|
||||
for (let i = start; i <= end; i++) {
|
||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||
const element = this._rowElements[i];
|
||||
if (element) {
|
||||
if (lineData.length === 0) {
|
||||
element.innerHTML = ' ';
|
||||
} else {
|
||||
element.textContent = lineData;
|
||||
}
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
this._announceCharacters();
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._renderService.dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._onResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._renderService.dimensions.actualCellHeight}px`;
|
||||
}
|
||||
|
||||
private _announceCharacters(): void {
|
||||
if (this._charsToAnnounce.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._liveRegion.textContent += this._charsToAnnounce;
|
||||
this._charsToAnnounce = '';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
257
web/src/utils/xterm/src/Types.d.ts
vendored
257
web/src/utils/xterm/src/Types.d.ts
vendored
@@ -1,257 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types';
|
||||
import { IEvent, IEventEmitter } from 'common/EventEmitter';
|
||||
import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type LineData = CharData[];
|
||||
|
||||
/**
|
||||
* This interface encapsulates everything needed from the Terminal by the
|
||||
* InputHandler. This cleanly separates the large amount of methods needed by
|
||||
* InputHandler cleanly from the ITerminal interface.
|
||||
*/
|
||||
export interface IInputHandlingTerminal {
|
||||
element: HTMLElement;
|
||||
options: ITerminalOptions;
|
||||
cols: number;
|
||||
rows: number;
|
||||
charset: ICharset;
|
||||
gcharset: number;
|
||||
glevel: number;
|
||||
charsets: ICharset[];
|
||||
applicationKeypad: boolean;
|
||||
originMode: boolean;
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttrData: IAttributeData;
|
||||
savedCols: number;
|
||||
mouseEvents: CoreMouseEventType;
|
||||
sendFocus: boolean;
|
||||
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
viewport: IViewport;
|
||||
|
||||
onA11yCharEmitter: IEventEmitter<string>;
|
||||
onA11yTabEmitter: IEventEmitter<number>;
|
||||
|
||||
bell(): void;
|
||||
focus(): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttrData(): IAttributeData;
|
||||
is(term: string): boolean;
|
||||
setgCharset(g: number, charset: ICharset): void;
|
||||
resize(x: number, y: number): void;
|
||||
reset(): void;
|
||||
showCursor(): void;
|
||||
refresh(start: number, end: number): void;
|
||||
handleTitle(title: string): void;
|
||||
}
|
||||
|
||||
export interface ICompositionHelper {
|
||||
compositionstart(): void;
|
||||
compositionupdate(ev: CompositionEvent): void;
|
||||
compositionend(): void;
|
||||
updateCompositionElements(dontRecurse?: boolean): void;
|
||||
keydown(ev: KeyboardEvent): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the parser and handles actions generated by the parser.
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
parse(data: string | Uint8Array): void;
|
||||
print(data: Uint32Array, start: number, end: number): void;
|
||||
|
||||
/** C0 BEL */ bell(): void;
|
||||
/** C0 LF */ lineFeed(): void;
|
||||
/** C0 CR */ carriageReturn(): void;
|
||||
/** C0 BS */ backspace(): void;
|
||||
/** C0 HT */ tab(): void;
|
||||
/** C0 SO */ shiftOut(): void;
|
||||
/** C0 SI */ shiftIn(): void;
|
||||
|
||||
/** CSI @ */ insertChars(params: IParams): void;
|
||||
/** CSI SP @ */ scrollLeft(params: IParams): void;
|
||||
/** CSI A */ cursorUp(params: IParams): void;
|
||||
/** CSI SP A */ scrollRight(params: IParams): void;
|
||||
/** CSI B */ cursorDown(params: IParams): void;
|
||||
/** CSI C */ cursorForward(params: IParams): void;
|
||||
/** CSI D */ cursorBackward(params: IParams): void;
|
||||
/** CSI E */ cursorNextLine(params: IParams): void;
|
||||
/** CSI F */ cursorPrecedingLine(params: IParams): void;
|
||||
/** CSI G */ cursorCharAbsolute(params: IParams): void;
|
||||
/** CSI H */ cursorPosition(params: IParams): void;
|
||||
/** CSI I */ cursorForwardTab(params: IParams): void;
|
||||
/** CSI J */ eraseInDisplay(params: IParams): void;
|
||||
/** CSI K */ eraseInLine(params: IParams): void;
|
||||
/** CSI L */ insertLines(params: IParams): void;
|
||||
/** CSI M */ deleteLines(params: IParams): void;
|
||||
/** CSI P */ deleteChars(params: IParams): void;
|
||||
/** CSI S */ scrollUp(params: IParams): void;
|
||||
/** CSI T */ scrollDown(params: IParams, collect?: string): void;
|
||||
/** CSI X */ eraseChars(params: IParams): void;
|
||||
/** CSI Z */ cursorBackwardTab(params: IParams): void;
|
||||
/** CSI ` */ charPosAbsolute(params: IParams): void;
|
||||
/** CSI a */ hPositionRelative(params: IParams): void;
|
||||
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
|
||||
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): void;
|
||||
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void;
|
||||
/** CSI d */ linePosAbsolute(params: IParams): void;
|
||||
/** CSI e */ vPositionRelative(params: IParams): void;
|
||||
/** CSI f */ hVPosition(params: IParams): void;
|
||||
/** CSI g */ tabClear(params: IParams): void;
|
||||
/** CSI h */ setMode(params: IParams, collect?: string): void;
|
||||
/** CSI l */ resetMode(params: IParams, collect?: string): void;
|
||||
/** CSI m */ charAttributes(params: IParams): void;
|
||||
/** CSI n */ deviceStatus(params: IParams, collect?: string): void;
|
||||
/** CSI p */ softReset(params: IParams, collect?: string): void;
|
||||
/** CSI q */ setCursorStyle(params: IParams, collect?: string): void;
|
||||
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
|
||||
/** CSI s */ saveCursor(params: IParams): void;
|
||||
/** CSI u */ restoreCursor(params: IParams): void;
|
||||
/** CSI ' } */ insertColumns(params: IParams): void;
|
||||
/** CSI ' ~ */ deleteColumns(params: IParams): void;
|
||||
/** OSC 0
|
||||
OSC 2 */ setTitle(data: string): void;
|
||||
/** ESC E */ nextLine(): void;
|
||||
/** ESC = */ keypadApplicationMode(): void;
|
||||
/** ESC > */ keypadNumericMode(): void;
|
||||
/** ESC % G
|
||||
ESC % @ */ selectDefaultCharset(): void;
|
||||
/** ESC ( C
|
||||
ESC ) C
|
||||
ESC * C
|
||||
ESC + C
|
||||
ESC - C
|
||||
ESC . C
|
||||
ESC / C */ selectCharset(collectAndFlag: string): void;
|
||||
/** ESC D */ index(): void;
|
||||
/** ESC H */ tabSet(): void;
|
||||
/** ESC M */ reverseIndex(): void;
|
||||
/** ESC c */ reset(): void;
|
||||
/** ESC n
|
||||
ESC o
|
||||
ESC |
|
||||
ESC }
|
||||
ESC ~ */ setgLevel(level: number): void;
|
||||
/** ESC # 8 */ screenAlignmentPattern(): void;
|
||||
}
|
||||
|
||||
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
screenElement: HTMLElement;
|
||||
browser: IBrowser;
|
||||
buffer: IBuffer;
|
||||
buffers: IBufferSet;
|
||||
viewport: IViewport;
|
||||
bracketedPasteMode: boolean;
|
||||
optionsService: IOptionsService;
|
||||
// TODO: We should remove options once components adopt optionsService
|
||||
options: ITerminalOptions;
|
||||
|
||||
onBlur: IEvent<void>;
|
||||
onFocus: IEvent<void>;
|
||||
onA11yChar: IEvent<string>;
|
||||
onA11yTab: IEvent<number>;
|
||||
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
showCursor(): void;
|
||||
}
|
||||
|
||||
// Portions of the public API that are required by the internal Terminal
|
||||
export interface IPublicTerminal extends IDisposable {
|
||||
textarea: HTMLTextAreaElement | undefined;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onBinary: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
|
||||
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
|
||||
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string | Uint8Array, callback?: () => void): void;
|
||||
paste(data: string): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
buffer: IBuffer;
|
||||
}
|
||||
|
||||
export interface IElementAccessor {
|
||||
readonly element: HTMLElement | undefined;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
linkifier: ILinkifier;
|
||||
}
|
||||
|
||||
// TODO: The options that are not in the public API should be reviewed
|
||||
export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
[key: string]: any;
|
||||
cancelEvents?: boolean;
|
||||
convertEol?: boolean;
|
||||
handler?: (data: string) => void;
|
||||
screenKeys?: boolean;
|
||||
termName?: string;
|
||||
useFlowControl?: boolean;
|
||||
}
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
userAgent: string;
|
||||
platform: string;
|
||||
isFirefox: boolean;
|
||||
isMac: boolean;
|
||||
isIpad: boolean;
|
||||
isIphone: boolean;
|
||||
isWindows: boolean;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ISelectionService } from 'browser/services/Services';
|
||||
import { ICoreService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
* @param text The pasted text that needs processing before inserting into the terminal
|
||||
*/
|
||||
export function prepareTextForTerminal(text: string): string {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bracket text for paste, if necessary, as per https://cirw.in/blog/bracketed-paste
|
||||
* @param text The pasted text to bracket
|
||||
*/
|
||||
export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string {
|
||||
if (bracketedPasteMode) {
|
||||
return '\x1b[200~' + text + '\x1b[201~';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionService): void {
|
||||
if (ev.clipboardData) {
|
||||
ev.clipboardData.setData('text/plain', selectionService.selectionText);
|
||||
}
|
||||
// Prevent or the original text will be copied.
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the clipboard's data to the terminal's input handler.
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
|
||||
ev.stopPropagation();
|
||||
if (ev.clipboardData) {
|
||||
const text = ev.clipboardData.getData('text/plain');
|
||||
paste(text, textarea, bracketedPasteMode, coreService);
|
||||
}
|
||||
}
|
||||
|
||||
export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = bracketTextForPaste(text, bracketedPasteMode);
|
||||
coreService.triggerDataEvent(text, true);
|
||||
textarea.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the textarea under the mouse cursor and focuses it.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement): void {
|
||||
|
||||
// Calculate textarea position relative to the screen element
|
||||
const pos = screenElement.getBoundingClientRect();
|
||||
const left = ev.clientX - pos.left - 10;
|
||||
const top = ev.clientY - pos.top - 10;
|
||||
|
||||
// Bring textarea at the cursor position
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = `${left}px`;
|
||||
textarea.style.top = `${top}px`;
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
// Timeout needs to be long enough for click event to be handled.
|
||||
setTimeout(() => {
|
||||
textarea.style.position = '';
|
||||
textarea.style.width = '';
|
||||
textarea.style.height = '';
|
||||
textarea.style.left = '';
|
||||
textarea.style.top = '';
|
||||
textarea.style.zIndex = '';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionService The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
|
||||
|
||||
if (shouldSelectWord && !selectionService.isClickInSelection(ev)) {
|
||||
selectionService.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionService.selectionText;
|
||||
textarea.select();
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColor } from 'browser/Types';
|
||||
|
||||
export function blend(bg: IColor, fg: IColor): IColor {
|
||||
const a = (fg.rgba & 0xFF) / 255;
|
||||
if (a === 1) {
|
||||
return {
|
||||
css: fg.css,
|
||||
rgba: fg.rgba
|
||||
};
|
||||
}
|
||||
const fgR = (fg.rgba >> 24) & 0xFF;
|
||||
const fgG = (fg.rgba >> 16) & 0xFF;
|
||||
const fgB = (fg.rgba >> 8) & 0xFF;
|
||||
const bgR = (bg.rgba >> 24) & 0xFF;
|
||||
const bgG = (bg.rgba >> 16) & 0xFF;
|
||||
const bgB = (bg.rgba >> 8) & 0xFF;
|
||||
const r = bgR + Math.round((fgR - bgR) * a);
|
||||
const g = bgG + Math.round((fgG - bgG) * a);
|
||||
const b = bgB + Math.round((fgB - bgB) * a);
|
||||
const css = toCss(r, g, b);
|
||||
const rgba = toRgba(r, g, b);
|
||||
return { css, rgba };
|
||||
}
|
||||
|
||||
export function fromCss(css: string): IColor {
|
||||
return {
|
||||
css,
|
||||
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
||||
};
|
||||
}
|
||||
|
||||
export function toPaddedHex(c: number): string {
|
||||
const s = c.toString(16);
|
||||
return s.length < 2 ? '0' + s : s;
|
||||
}
|
||||
|
||||
export function toCss(r: number, g: number, b: number, a?: number): string {
|
||||
if (a !== undefined) {
|
||||
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
|
||||
}
|
||||
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
|
||||
}
|
||||
|
||||
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
|
||||
// >>> 0 forces an unsigned int
|
||||
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
|
||||
}
|
||||
|
||||
export function fromRgba(value: number): [number, number, number, number] {
|
||||
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
||||
}
|
||||
|
||||
export function opaque(color: IColor): IColor {
|
||||
const rgba = (color.rgba | 0xFF) >>> 0;
|
||||
const [r, g, b] = fromRgba(rgba);
|
||||
return {
|
||||
css: toCss(r, g, b),
|
||||
rgba
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
|
||||
* between two colors.
|
||||
* @param rgb The color to use.
|
||||
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
*/
|
||||
export function rgbRelativeLuminance(rgb: number): number {
|
||||
return rgbRelativeLuminance2(
|
||||
(rgb >> 16) & 0xFF,
|
||||
(rgb >> 8 ) & 0xFF,
|
||||
(rgb ) & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
|
||||
* between two colors.
|
||||
* @param r The red channel (0x00 to 0xFF).
|
||||
* @param g The green channel (0x00 to 0xFF).
|
||||
* @param b The blue channel (0x00 to 0xFF).
|
||||
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
*/
|
||||
export function rgbRelativeLuminance2(r: number, g: number, b: number): number {
|
||||
const rs = r / 255;
|
||||
const gs = g / 255;
|
||||
const bs = b / 255;
|
||||
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
|
||||
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
|
||||
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
|
||||
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contrast ratio between two relative luminance values.
|
||||
* @param l1 The first relative luminance.
|
||||
* @param l2 The first relative luminance.
|
||||
* @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||
*/
|
||||
export function contrastRatio(l1: number, l2: number): number {
|
||||
if (l1 < l2) {
|
||||
return (l2 + 0.05) / (l1 + 0.05);
|
||||
}
|
||||
return (l1 + 0.05) / (l2 + 0.05);
|
||||
}
|
||||
|
||||
export function rgbaToColor(r: number, g: number, b: number): IColor {
|
||||
return {
|
||||
css: toCss(r, g, b),
|
||||
rgba: toRgba(r, g, b)
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
|
||||
const bgL = rgbRelativeLuminance(bgRgba >> 8);
|
||||
const fgL = rgbRelativeLuminance(fgRgba >> 8);
|
||||
const cr = contrastRatio(bgL, fgL);
|
||||
if (cr < ratio) {
|
||||
if (fgL < bgL) {
|
||||
return reduceLuminance(bgRgba, fgRgba, ratio);
|
||||
}
|
||||
return increaseLuminance(bgRgba, fgRgba, ratio);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
|
||||
const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
return rgbaToColor(
|
||||
(result >> 24 & 0xFF),
|
||||
(result >> 16 & 0xFF),
|
||||
(result >> 8 & 0xFF)
|
||||
);
|
||||
}
|
||||
|
||||
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
|
||||
// This is a naive but fast approach to reducing luminance as converting to
|
||||
// HSL and back is expensive
|
||||
const bgR = (bgRgba >> 24) & 0xFF;
|
||||
const bgG = (bgRgba >> 16) & 0xFF;
|
||||
const bgB = (bgRgba >> 8) & 0xFF;
|
||||
let fgR = (fgRgba >> 24) & 0xFF;
|
||||
let fgG = (fgRgba >> 16) & 0xFF;
|
||||
let fgB = (fgRgba >> 8) & 0xFF;
|
||||
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
|
||||
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
|
||||
// Reduce by 10% until the ratio is hit
|
||||
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
|
||||
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
|
||||
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
|
||||
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
|
||||
}
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
|
||||
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
|
||||
// This is a naive but fast approach to increasing luminance as converting to
|
||||
// HSL and back is expensive
|
||||
const bgR = (bgRgba >> 24) & 0xFF;
|
||||
const bgG = (bgRgba >> 16) & 0xFF;
|
||||
const bgB = (bgRgba >> 8) & 0xFF;
|
||||
let fgR = (fgRgba >> 24) & 0xFF;
|
||||
let fgG = (fgRgba >> 16) & 0xFF;
|
||||
let fgB = (fgRgba >> 8) & 0xFF;
|
||||
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
|
||||
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
|
||||
// Increase by 10% until the ratio is hit
|
||||
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
|
||||
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
|
||||
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
|
||||
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
|
||||
}
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColor, IColorContrastCache } from 'browser/Types';
|
||||
|
||||
export class ColorContrastCache implements IColorContrastCache {
|
||||
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
|
||||
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
|
||||
|
||||
public clear(): void {
|
||||
this._color = {};
|
||||
this._rgba = {};
|
||||
}
|
||||
|
||||
public setCss(bg: number, fg: number, value: string | null): void {
|
||||
if (!this._rgba[bg]) {
|
||||
this._rgba[bg] = {};
|
||||
}
|
||||
this._rgba[bg]![fg] = value;
|
||||
}
|
||||
|
||||
public getCss(bg: number, fg: number): string | null | undefined {
|
||||
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
|
||||
}
|
||||
|
||||
public setColor(bg: number, fg: number, value: IColor | null): void {
|
||||
if (!this._color[bg]) {
|
||||
this._color[bg] = {};
|
||||
}
|
||||
this._color[bg]![fg] = value;
|
||||
}
|
||||
|
||||
public getColor(bg: number, fg: number): IColor | null | undefined {
|
||||
return this._color[bg] ? this._color[bg]![fg] : undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
|
||||
import { ITheme } from 'common/services/Services';
|
||||
import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color';
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
|
||||
const DEFAULT_FOREGROUND = fromCss('#ffffff');
|
||||
const DEFAULT_BACKGROUND = fromCss('#000000');
|
||||
const DEFAULT_CURSOR = fromCss('#ffffff');
|
||||
const DEFAULT_CURSOR_ACCENT = fromCss('#000000');
|
||||
const DEFAULT_SELECTION = {
|
||||
css: 'rgba(255, 255, 255, 0.3)',
|
||||
rgba: 0xFFFFFF4D
|
||||
};
|
||||
|
||||
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
|
||||
// and mutate that.
|
||||
export const DEFAULT_ANSI_COLORS = (() => {
|
||||
const colors = [
|
||||
// dark:
|
||||
fromCss('#2e3436'),
|
||||
fromCss('#cc0000'),
|
||||
fromCss('#4e9a06'),
|
||||
fromCss('#c4a000'),
|
||||
fromCss('#3465a4'),
|
||||
fromCss('#75507b'),
|
||||
fromCss('#06989a'),
|
||||
fromCss('#d3d7cf'),
|
||||
// bright:
|
||||
fromCss('#555753'),
|
||||
fromCss('#ef2929'),
|
||||
fromCss('#8ae234'),
|
||||
fromCss('#fce94f'),
|
||||
fromCss('#729fcf'),
|
||||
fromCss('#ad7fa8'),
|
||||
fromCss('#34e2e2'),
|
||||
fromCss('#eeeeec')
|
||||
];
|
||||
|
||||
// Fill in the remaining 240 ANSI colors.
|
||||
// Generate colors (16-231)
|
||||
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
for (let i = 0; i < 216; i++) {
|
||||
const r = v[(i / 36) % 6 | 0];
|
||||
const g = v[(i / 6) % 6 | 0];
|
||||
const b = v[i % 6];
|
||||
colors.push({
|
||||
css: toCss(r, g, b),
|
||||
rgba: toRgba(r, g, b)
|
||||
});
|
||||
}
|
||||
|
||||
// Generate greys (232-255)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const c = 8 + i * 10;
|
||||
colors.push({
|
||||
css: toCss(c, c, c),
|
||||
rgba: toRgba(c, c, c)
|
||||
});
|
||||
}
|
||||
|
||||
return colors;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Manages the source of truth for a terminal's colors.
|
||||
*/
|
||||
export class ColorManager implements IColorManager {
|
||||
public colors: IColorSet;
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _litmusColor: CanvasGradient;
|
||||
private _contrastCache: IColorContrastCache;
|
||||
|
||||
constructor(document: Document, public allowTransparency: boolean) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get rendering context');
|
||||
}
|
||||
this._ctx = ctx;
|
||||
this._ctx.globalCompositeOperation = 'copy';
|
||||
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
|
||||
this._contrastCache = new ColorContrastCache();
|
||||
this.colors = {
|
||||
foreground: DEFAULT_FOREGROUND,
|
||||
background: DEFAULT_BACKGROUND,
|
||||
cursor: DEFAULT_CURSOR,
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selection: DEFAULT_SELECTION,
|
||||
selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
}
|
||||
|
||||
public onOptionsChange(key: string): void {
|
||||
if (key === 'minimumContrastRatio') {
|
||||
this._contrastCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the terminal's theme.
|
||||
* @param theme The theme to use. If a partial theme is provided then default
|
||||
* colors will be used where colors are not defined.
|
||||
*/
|
||||
public setTheme(theme: ITheme = {}): void {
|
||||
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
|
||||
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
|
||||
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
|
||||
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
|
||||
this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection);
|
||||
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
|
||||
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
|
||||
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
|
||||
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
|
||||
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
|
||||
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
|
||||
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
|
||||
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
|
||||
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
|
||||
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
|
||||
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
|
||||
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
|
||||
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
|
||||
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
|
||||
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
|
||||
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
|
||||
// Clear our the cache
|
||||
this._contrastCache.clear();
|
||||
}
|
||||
|
||||
private _parseColor(
|
||||
css: string | undefined,
|
||||
fallback: IColor,
|
||||
allowTransparency: boolean = this.allowTransparency
|
||||
): IColor {
|
||||
if (css === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// If parsing the value results in failure, then it must be ignored, and the attribute must
|
||||
// retain its previous value.
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
|
||||
this._ctx.fillStyle = this._litmusColor;
|
||||
this._ctx.fillStyle = css;
|
||||
if (typeof this._ctx.fillStyle !== 'string') {
|
||||
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
this._ctx.fillRect(0, 0, 1, 1);
|
||||
const data = this._ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
// Check if the printed color was transparent
|
||||
if (data[3] !== 0xFF) {
|
||||
if (!allowTransparency) {
|
||||
// Ideally we'd just ignore the alpha channel, but...
|
||||
//
|
||||
// Browsers may not give back exactly the same RGB values we put in, because most/all
|
||||
// convert the color to a pre-multiplied representation. getImageData converts that back to
|
||||
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
|
||||
// on their own.
|
||||
//
|
||||
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
|
||||
// into 0x00000000.
|
||||
//
|
||||
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
|
||||
// pixels that have just been set using putImageData() might be returned to an equivalent
|
||||
// getImageData() as different values."
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
|
||||
//
|
||||
// So let's just use the fallback color in this case instead.
|
||||
console.warn(
|
||||
`Color: ${css} is using transparency, but allowTransparency is false. ` +
|
||||
`Using fallback ${fallback.css}.`
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
let r: number;
|
||||
let g: number;
|
||||
let b: number;
|
||||
let a: number;
|
||||
let rgba: number;
|
||||
if (css.length === 5) {
|
||||
const num = parseInt(css.substr(1), 16);
|
||||
r = ((num >> 12) & 0xF) * 16;
|
||||
g = ((num >> 8) & 0xF) * 16;
|
||||
b = ((num >> 4) & 0xF) * 16;
|
||||
a = (num & 0xF) * 16;
|
||||
rgba = toRgba(r, g, b, a);
|
||||
} else {
|
||||
rgba = parseInt(css.substr(1), 16);
|
||||
r = (rgba >> 24) & 0xFF;
|
||||
g = (rgba >> 16) & 0xFF;
|
||||
b = (rgba >> 8) & 0xFF;
|
||||
a = (rgba ) & 0xFF;
|
||||
}
|
||||
|
||||
return {
|
||||
rgba,
|
||||
css: toCss(r, g, b, a)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
css,
|
||||
rgba: toRgba(data[0], data[1], data[2], data[3])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Adds a disposable listener to a node in the DOM, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
export function addDisposableDomListener(
|
||||
node: Element | Window | Document,
|
||||
type: string,
|
||||
handler: (e: any) => void,
|
||||
useCapture?: boolean
|
||||
): IDisposable {
|
||||
node.addEventListener(type, handler, useCapture);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
node.removeEventListener(type, handler, useCapture);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types';
|
||||
import { IBufferStringIteratorResult } from 'common/buffer/Types';
|
||||
import { getStringCellWidth } from 'common/CharWidth';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { ILogService, IBufferService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* Limit of the unwrapping line expansion (overscan) at the top and bottom
|
||||
* of the actual viewport in ASCII characters.
|
||||
* A limit of 2000 should match most sane urls.
|
||||
*/
|
||||
const OVERSCAN_CHAR_LIMIT = 2000;
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier implements ILinkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
* huge amount of times.
|
||||
*/
|
||||
protected static _timeBeforeLatency = 200;
|
||||
|
||||
protected _linkMatchers: IRegisteredLinkMatcher[] = [];
|
||||
|
||||
private _mouseZoneManager: IMouseZoneManager | undefined;
|
||||
private _element: HTMLElement | undefined;
|
||||
|
||||
private _rowsTimeoutId: number | undefined;
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number | undefined, end: number | undefined };
|
||||
|
||||
private _onLinkHover = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
private _onLinkLeave = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
|
||||
private _onLinkTooltip = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
|
||||
|
||||
constructor(
|
||||
protected readonly _bufferService: IBufferService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
this._rowsToLinkify = {
|
||||
start: undefined,
|
||||
end: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the linkifier to the DOM, enabling linkification.
|
||||
* @param mouseZoneManager The mouse zone manager to register link zones with.
|
||||
*/
|
||||
public attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void {
|
||||
this._element = element;
|
||||
this._mouseZoneManager = mouseZoneManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue linkification on a set of rows.
|
||||
* @param start The row to linkify from (inclusive).
|
||||
* @param end The row to linkify to (inclusive).
|
||||
*/
|
||||
public linkifyRows(start: number, end: number): void {
|
||||
// Don't attempt linkify if not yet attached to DOM
|
||||
if (!this._mouseZoneManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increase range to linkify
|
||||
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
|
||||
this._rowsToLinkify.start = start;
|
||||
this._rowsToLinkify.end = end;
|
||||
} else {
|
||||
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start);
|
||||
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end);
|
||||
}
|
||||
|
||||
// Clear out any existing links on this row range
|
||||
this._mouseZoneManager.clearAll(start, end);
|
||||
|
||||
// Restart timer
|
||||
if (this._rowsTimeoutId) {
|
||||
clearTimeout(this._rowsTimeoutId);
|
||||
}
|
||||
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the rows requested.
|
||||
*/
|
||||
private _linkifyRows(): void {
|
||||
this._rowsTimeoutId = undefined;
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
|
||||
this._logService.debug('_rowToLinkify was unset before _linkifyRows was called');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the start row exists
|
||||
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
|
||||
if (absoluteRowIndexStart >= buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate bad end row values (if a resize happened)
|
||||
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._bufferService.rows) + 1;
|
||||
|
||||
// Iterate over the range of unwrapped content strings within start..end
|
||||
// (excluding).
|
||||
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
|
||||
// for every matcher.
|
||||
// The unwrapping is needed to also match content that got wrapped across
|
||||
// several buffer lines. To avoid a worst case scenario where the whole buffer
|
||||
// contains just a single unwrapped string we limit this line expansion beyond
|
||||
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
|
||||
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
|
||||
// chars will not match anymore at the viewport borders.
|
||||
const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._bufferService.cols);
|
||||
const iterator = this._bufferService.buffer.iterator(
|
||||
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
|
||||
while (iterator.hasNext()) {
|
||||
const lineData: IBufferStringIteratorResult = iterator.next();
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this._rowsToLinkify.start = undefined;
|
||||
this._rowsToLinkify.end = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a link matcher, allowing custom link patterns to be matched and
|
||||
* handled.
|
||||
* @param regex The regular expression to search for. Specifically, this
|
||||
* searches the textContent of the rows. You will want to use \s to match a
|
||||
* space ' ' character for example.
|
||||
* @param handler The callback when the link is called.
|
||||
* @param options Options for the link matcher.
|
||||
* @return The ID of the new matcher, this can be used to deregister.
|
||||
*/
|
||||
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
|
||||
if (!handler) {
|
||||
throw new Error('handler must be defined');
|
||||
}
|
||||
const matcher: IRegisteredLinkMatcher = {
|
||||
id: this._nextLinkMatcherId++,
|
||||
regex,
|
||||
handler,
|
||||
matchIndex: options.matchIndex,
|
||||
validationCallback: options.validationCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
willLinkActivate: options.willLinkActivate,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
return matcher.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a link matcher to the list in the correct position based on the
|
||||
* priority of each link matcher. New link matchers of equal priority are
|
||||
* considered after older link matchers.
|
||||
* @param matcher The link matcher to be added.
|
||||
*/
|
||||
private _addLinkMatcherToList(matcher: IRegisteredLinkMatcher): void {
|
||||
if (this._linkMatchers.length === 0) {
|
||||
this._linkMatchers.push(matcher);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
|
||||
if (matcher.priority <= this._linkMatchers[i].priority) {
|
||||
this._linkMatchers.splice(i + 1, 0, matcher);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._linkMatchers.splice(0, 0, matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters a link matcher if it has been registered.
|
||||
* @param matcherId The link matcher's ID (returned after register)
|
||||
* @return Whether a link matcher was found and deregistered.
|
||||
*/
|
||||
public deregisterLinkMatcher(matcherId: number): boolean {
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
if (this._linkMatchers[i].id === matcherId) {
|
||||
this._linkMatchers.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row given a specific handler.
|
||||
* @param rowIndex The row index to linkify (absolute index).
|
||||
* @param text string content of the unwrapped row.
|
||||
* @param matcher The link matcher for this line.
|
||||
*/
|
||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
|
||||
// clone regex to do a global search on text
|
||||
const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g');
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
while ((match = rex.exec(text)) !== null) {
|
||||
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
if (!uri) {
|
||||
// something matched but does not comply with the given matchIndex
|
||||
// since this is most likely a bug the regex itself we simply do nothing here
|
||||
this._logService.debug('match found without corresponding matchIndex', match, matcher);
|
||||
break;
|
||||
}
|
||||
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
// therefore we cannot use match.index directly, instead we search the position
|
||||
// of the match group in text again
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + uri.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
// get the buffer index as [absolute row, col] for the match
|
||||
const bufferIndex = this._bufferService.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
if (bufferIndex[0] < 0) {
|
||||
// invalid bufferIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
const line = this._bufferService.buffer.lines.get(bufferIndex[0]);
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
|
||||
const attr = line.getFg(bufferIndex[1]);
|
||||
const fg = attr ? (attr >> 9) & 0x1ff : undefined;
|
||||
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
// Discard link if the line has already changed
|
||||
if (this._rowsTimeoutId) {
|
||||
return;
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a link to the mouse zone manager.
|
||||
* @param x The column the link starts.
|
||||
* @param y The row the link is on.
|
||||
* @param uri The URI of the link.
|
||||
* @param matcher The link matcher for the link.
|
||||
* @param fg The link color for hover event.
|
||||
*/
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number | undefined): void {
|
||||
if (!this._mouseZoneManager || !this._element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = getStringCellWidth(uri);
|
||||
const x1 = x % this._bufferService.cols;
|
||||
const y1 = y + Math.floor(x / this._bufferService.cols);
|
||||
let x2 = (x1 + width) % this._bufferService.cols;
|
||||
let y2 = y1 + Math.floor((x1 + width) / this._bufferService.cols);
|
||||
if (x2 === 0) {
|
||||
x2 = this._bufferService.cols;
|
||||
y2--;
|
||||
}
|
||||
|
||||
this._mouseZoneManager.add(new MouseZone(
|
||||
x1 + 1,
|
||||
y1 + 1,
|
||||
x2 + 1,
|
||||
y2 + 1,
|
||||
e => {
|
||||
if (matcher.handler) {
|
||||
return matcher.handler(e, uri);
|
||||
}
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
() => {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._element!.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
// Note that IViewportRange use 1-based coordinates to align with escape sequences such
|
||||
// as CUP which use 1,1 as the default for row/col
|
||||
matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } });
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._element!.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
},
|
||||
e => {
|
||||
if (matcher.willLinkActivate) {
|
||||
return matcher.willLinkActivate(e, uri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
|
||||
return { x1, y1, x2, y2, cols: this._bufferService.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseZone implements IMouseZone {
|
||||
constructor(
|
||||
public x1: number,
|
||||
public y1: number,
|
||||
public x2: number,
|
||||
public y2: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export let promptLabel = 'Terminal input';
|
||||
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IMouseService, ISelectionService } from 'browser/services/Services';
|
||||
import { IMouseZoneManager, IMouseZone } from 'browser/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
const HOVER_DURATION = 500;
|
||||
|
||||
/**
|
||||
* The MouseZoneManager allows components to register zones within the terminal
|
||||
* that trigger hover and click callbacks.
|
||||
*
|
||||
* This class was intentionally made not so robust initially as the only case it
|
||||
* needed to support was single-line links which never overlap. Improvements can
|
||||
* be made in the future.
|
||||
*/
|
||||
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _zones: IMouseZone[] = [];
|
||||
|
||||
private _areZonesActive: boolean = false;
|
||||
private _mouseMoveListener: (e: MouseEvent) => any;
|
||||
private _mouseLeaveListener: (e: MouseEvent) => any;
|
||||
private _clickListener: (e: MouseEvent) => any;
|
||||
|
||||
private _tooltipTimeout: number | undefined;
|
||||
private _currentZone: IMouseZone | undefined;
|
||||
private _lastHoverCoords: [number | undefined, number | undefined] = [undefined, undefined];
|
||||
private _initialSelectionLength: number = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@ISelectionService private readonly _selectionService: ISelectionService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e)));
|
||||
|
||||
// These events are expensive, only listen to it when mouse zones are active
|
||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||
this._mouseLeaveListener = e => this._onMouseLeave(e);
|
||||
this._clickListener = e => this._onClick(e);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._deactivate();
|
||||
}
|
||||
|
||||
public add(zone: IMouseZone): void {
|
||||
this._zones.push(zone);
|
||||
if (this._zones.length === 1) {
|
||||
this._activate();
|
||||
}
|
||||
}
|
||||
|
||||
public clearAll(start?: number, end?: number): void {
|
||||
// Exit if there's nothing to clear
|
||||
if (this._zones.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all if start/end weren't set
|
||||
if (!start || !end) {
|
||||
start = 0;
|
||||
end = this._bufferService.rows - 1;
|
||||
}
|
||||
|
||||
// Iterate through zones and clear them out if they're within the range
|
||||
for (let i = 0; i < this._zones.length; i++) {
|
||||
const zone = this._zones[i];
|
||||
if ((zone.y1 > start && zone.y1 <= end + 1) ||
|
||||
(zone.y2 > start && zone.y2 <= end + 1) ||
|
||||
(zone.y1 < start && zone.y2 > end + 1)) {
|
||||
if (this._currentZone && this._currentZone === zone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = undefined;
|
||||
}
|
||||
this._zones.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate the mouse zone manager if all the zones have been removed
|
||||
if (this._zones.length === 0) {
|
||||
this._deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
private _activate(): void {
|
||||
if (!this._areZonesActive) {
|
||||
this._areZonesActive = true;
|
||||
this._element.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._element.addEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
private _deactivate(): void {
|
||||
if (this._areZonesActive) {
|
||||
this._areZonesActive = false;
|
||||
this._element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._element.removeEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseMove(e: MouseEvent): void {
|
||||
// TODO: Ideally this would only clear the hover state when the mouse moves
|
||||
// outside of the mouse zone
|
||||
if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
|
||||
this._onHover(e);
|
||||
// Record the current coordinates
|
||||
this._lastHoverCoords = [e.pageX, e.pageY];
|
||||
}
|
||||
}
|
||||
|
||||
private _onHover(e: MouseEvent): void {
|
||||
const zone = this._findZoneEventAt(e);
|
||||
|
||||
// Do nothing if the zone is the same
|
||||
if (zone === this._currentZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire the hover end callback and cancel any existing timer if a new zone
|
||||
// is being hovered
|
||||
if (this._currentZone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = undefined;
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit if there is not zone
|
||||
if (!zone) {
|
||||
return;
|
||||
}
|
||||
this._currentZone = zone;
|
||||
|
||||
// Trigger the hover callback
|
||||
if (zone.hoverCallback) {
|
||||
zone.hoverCallback(e);
|
||||
}
|
||||
|
||||
// Restart the tooltip timeout
|
||||
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
|
||||
}
|
||||
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
this._tooltipTimeout = undefined;
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.tooltipCallback) {
|
||||
zone.tooltipCallback(e);
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
// Store current terminal selection length, to check if we're performing
|
||||
// a selection operation
|
||||
this._initialSelectionLength = this._getSelectionLength();
|
||||
|
||||
// Ignore the event if there are no zones active
|
||||
if (!this._areZonesActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the active zone, prevent event propagation if found to prevent other
|
||||
// components from handling the mouse event.
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone?.willLinkActivate(e)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseLeave(e: MouseEvent): void {
|
||||
// Fire the hover end callback and cancel any existing timer if the mouse
|
||||
// leaves the terminal element
|
||||
if (this._currentZone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = undefined;
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onClick(e: MouseEvent): void {
|
||||
// Find the active zone and click it if found and no selection was
|
||||
// being performed
|
||||
const zone = this._findZoneEventAt(e);
|
||||
const currentSelectionLength = this._getSelectionLength();
|
||||
|
||||
if (zone && currentSelectionLength === this._initialSelectionLength) {
|
||||
zone.clickCallback(e);
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _getSelectionLength(): number {
|
||||
const selectionText = this._selectionService.selectionText;
|
||||
return selectionText ? selectionText.length : 0;
|
||||
}
|
||||
|
||||
private _findZoneEventAt(e: MouseEvent): IMouseZone | undefined {
|
||||
const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
const x = coords[0];
|
||||
const y = coords[1];
|
||||
for (let i = 0; i < this._zones.length; i++) {
|
||||
const zone = this._zones[i];
|
||||
if (zone.y1 === zone.y2) {
|
||||
// Single line link
|
||||
if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
|
||||
return zone;
|
||||
}
|
||||
} else {
|
||||
// Multi-line link
|
||||
if ((y === zone.y1 && x >= zone.x1) ||
|
||||
(y === zone.y2 && x < zone.x2) ||
|
||||
(y > zone.y1 && y < zone.y2)) {
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IDisposable {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
private _animationFrame: number | undefined;
|
||||
|
||||
constructor(
|
||||
private _renderCallback: (start: number, end: number) => void
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart: number, rowEnd: number, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||
// Set the properties to the updated values
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
if (this._animationFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
// Make sure values are set
|
||||
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp values
|
||||
this._rowStart = Math.max(this._rowStart, 0);
|
||||
this._rowEnd = Math.min(this._rowEnd, this._rowCount - 1);
|
||||
|
||||
// Run render callback
|
||||
this._renderCallback(this._rowStart, this._rowEnd);
|
||||
|
||||
// Reset debouncer
|
||||
this._rowStart = undefined;
|
||||
this._rowEnd = undefined;
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
|
||||
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
|
||||
|
||||
/**
|
||||
* The screen device pixel ratio monitor allows listening for when the
|
||||
* window.devicePixelRatio value changes. This is done not with polling but with
|
||||
* the use of window.matchMedia to watch media queries. When the event fires,
|
||||
* the listener will be reattached using a different media query to ensure that
|
||||
* any further changes will register.
|
||||
*
|
||||
* The listener should fire on both window zoom changes and switching to a
|
||||
* monitor with a different DPI.
|
||||
*/
|
||||
export class ScreenDprMonitor extends Disposable {
|
||||
private _currentDevicePixelRatio: number = window.devicePixelRatio;
|
||||
private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined;
|
||||
private _listener: ScreenDprListener | undefined;
|
||||
private _resolutionMediaMatchList: MediaQueryList | undefined;
|
||||
|
||||
public setListener(listener: ScreenDprListener): void {
|
||||
if (this._listener) {
|
||||
this.clearListener();
|
||||
}
|
||||
this._listener = listener;
|
||||
this._outerListener = () => {
|
||||
if (!this._listener) {
|
||||
return;
|
||||
}
|
||||
this._listener(window.devicePixelRatio, this._currentDevicePixelRatio);
|
||||
this._updateDpr();
|
||||
};
|
||||
this._updateDpr();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.clearListener();
|
||||
}
|
||||
|
||||
private _updateDpr(): void {
|
||||
if (!this._resolutionMediaMatchList || !this._outerListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear listeners for old DPR
|
||||
this._resolutionMediaMatchList.removeListener(this._outerListener);
|
||||
|
||||
// Add listeners for new DPR
|
||||
this._currentDevicePixelRatio = window.devicePixelRatio;
|
||||
this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`);
|
||||
this._resolutionMediaMatchList.addListener(this._outerListener);
|
||||
}
|
||||
|
||||
public clearListener(): void {
|
||||
if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) {
|
||||
return;
|
||||
}
|
||||
this._resolutionMediaMatchList.removeListener(this._outerListener);
|
||||
this._resolutionMediaMatchList = undefined;
|
||||
this._listener = undefined;
|
||||
this._outerListener = undefined;
|
||||
}
|
||||
}
|
||||
157
web/src/utils/xterm/src/browser/Types.d.ts
vendored
157
web/src/utils/xterm/src/browser/Types.d.ts
vendored
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export interface IColorManager {
|
||||
colors: IColorSet;
|
||||
onOptionsChange(key: string): void;
|
||||
}
|
||||
|
||||
export interface IColor {
|
||||
css: string;
|
||||
rgba: number; // 32-bit int with rgba in each byte
|
||||
}
|
||||
|
||||
export interface IColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor: IColor;
|
||||
cursorAccent: IColor;
|
||||
selection: IColor;
|
||||
/** The selection blended on top of background. */
|
||||
selectionOpaque: IColor;
|
||||
ansi: IColor[];
|
||||
contrastCache: IColorContrastCache;
|
||||
}
|
||||
|
||||
export interface IColorContrastCache {
|
||||
clear(): void;
|
||||
setCss(bg: number, fg: number, value: string | null): void;
|
||||
getCss(bg: number, fg: number): string | null | undefined;
|
||||
setColor(bg: number, fg: number, value: IColor | null): void;
|
||||
getColor(bg: number, fg: number): IColor | null | undefined;
|
||||
}
|
||||
|
||||
export interface IPartialColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor?: IColor;
|
||||
cursorAccent?: IColor;
|
||||
selection?: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
export interface IViewport extends IDisposable {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(immediate?: boolean): void;
|
||||
getLinesScrolled(ev: WheelEvent): number;
|
||||
onWheel(ev: WheelEvent): boolean;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): boolean;
|
||||
onThemeChange(colors: IColorSet): void;
|
||||
}
|
||||
|
||||
export interface IViewportRange {
|
||||
start: IViewportRangePosition;
|
||||
end: IViewportRangePosition;
|
||||
}
|
||||
|
||||
export interface IViewportRangePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
export interface ILinkMatcher {
|
||||
id: number;
|
||||
regex: RegExp;
|
||||
handler: LinkMatcherHandler;
|
||||
hoverTooltipCallback?: LinkMatcherHoverTooltipCallback;
|
||||
hoverLeaveCallback?: () => void;
|
||||
matchIndex?: number;
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
priority?: number;
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IRegisteredLinkMatcher extends ILinkMatcher {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface ILinkifierEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number | undefined;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
export interface ILinkMatcherOptions {
|
||||
/**
|
||||
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||
* (for regular expressions without capture groups).
|
||||
*/
|
||||
matchIndex?: number;
|
||||
/**
|
||||
* A callback that validates an individual link, returning true if valid and
|
||||
* false if invalid.
|
||||
*/
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
*/
|
||||
tooltipCallback?: LinkMatcherHoverTooltipCallback;
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link that was hovered.
|
||||
*/
|
||||
leaveCallback?: () => void;
|
||||
/**
|
||||
* The priority of the link matcher, this defines the order in which the link
|
||||
* matcher is evaluated relative to others, from highest to lowest. The
|
||||
* default value is 0.
|
||||
*/
|
||||
priority?: number;
|
||||
/**
|
||||
* A callback that fires when the mousedown and click events occur that
|
||||
* determines whether a link will be activated upon click. This enables
|
||||
* only activating a link when a certain modifier is held down, if not the
|
||||
* mouse event will continue propagation (eg. double click to select word).
|
||||
*/
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IViewport } from 'browser/Types';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
/**
|
||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||
* Logic for the virtual scroll bar is included in this object.
|
||||
*/
|
||||
export class Viewport extends Disposable implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
private _lastRecordedBufferLength: number = 0;
|
||||
private _lastRecordedViewportHeight: number = 0;
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number = 0;
|
||||
private _lastScrollTop: number = 0;
|
||||
|
||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||
private _wheelPartialScroll: number = 0;
|
||||
|
||||
private _refreshAnimationFrame: number | null = null;
|
||||
private _ignoreNextScrollEvent: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _scrollArea: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Perform this async to ensure the ICharSizeService is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
public onThemeChange(colors: IColorSet): void {
|
||||
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
||||
* necessary.
|
||||
*/
|
||||
private _refresh(immediate: boolean): void {
|
||||
if (immediate) {
|
||||
this._innerRefresh();
|
||||
if (this._refreshAnimationFrame !== null) {
|
||||
cancelAnimationFrame(this._refreshAnimationFrame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._refreshAnimationFrame === null) {
|
||||
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
if (this._charSizeService.height > 0) {
|
||||
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
|
||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this._lastRecordedBufferHeight = newBufferHeight;
|
||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
|
||||
// want this event to scroll the terminal
|
||||
this._ignoreNextScrollEvent = true;
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
this._refreshAnimationFrame = null;
|
||||
}
|
||||
/**
|
||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||
*/
|
||||
public syncScrollArea(immediate: boolean = false): void {
|
||||
// If buffer height changed
|
||||
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
|
||||
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If viewport height changed
|
||||
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the buffer position doesn't match last scroll top
|
||||
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._lastScrollTop !== newScrollTop) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If element's scroll top changed, this can happen when hiding the element
|
||||
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If row height changed
|
||||
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles scroll events on the viewport, calculating the new viewport and requesting the
|
||||
* terminal to scroll to it.
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private _onScroll(ev: Event): void {
|
||||
// Record current scroll top position
|
||||
this._lastScrollTop = this._viewportElement.scrollTop;
|
||||
|
||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||
// which causes the terminal to scroll the buffer to the top
|
||||
if (!this._viewportElement.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
||||
if (this._ignoreNextScrollEvent) {
|
||||
this._ignoreNextScrollEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._bufferService.buffer.ydisp;
|
||||
this._scrollLines(diff, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles bubbling of scroll event in case the viewport has reached top or bottom
|
||||
* @param ev The scroll event.
|
||||
* @param amount The amount scrolled
|
||||
*/
|
||||
private _bubbleScroll(ev: Event, amount: number): boolean {
|
||||
const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
|
||||
if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
|
||||
(amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
|
||||
if (ev.cancelable) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
|
||||
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
|
||||
* `Viewport`.
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public onWheel(ev: WheelEvent): boolean {
|
||||
const amount = this._getPixelsScrolled(ev);
|
||||
if (amount === 0) {
|
||||
return false;
|
||||
}
|
||||
this._viewportElement.scrollTop += amount;
|
||||
return this._bubbleScroll(ev, amount);
|
||||
}
|
||||
|
||||
private _getPixelsScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
amount *= this._currentRowHeight;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._currentRowHeight * this._bufferService.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
|
||||
* is being used.
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public getLinesScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_LINE
|
||||
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
amount /= this._currentRowHeight + 0.0; // Prevent integer division
|
||||
this._wheelPartialScroll += amount;
|
||||
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
||||
this._wheelPartialScroll %= 1;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._bufferService.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
|
||||
const modifier = this._optionsService.options.fastScrollModifier;
|
||||
// Multiply the scroll speed when the modifier is down
|
||||
if ((modifier === 'alt' && ev.altKey) ||
|
||||
(modifier === 'ctrl' && ev.ctrlKey) ||
|
||||
(modifier === 'shift' && ev.shiftKey)) {
|
||||
return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
|
||||
}
|
||||
|
||||
return amount * this._optionsService.options.scrollSensitivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchStart(ev: TouchEvent): void {
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent): boolean {
|
||||
const deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
this._viewportElement.scrollTop += deltaY;
|
||||
return this._bubbleScroll(ev, deltaY);
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface IPosition {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for handling compositionstart, compositionupdate and compositionend
|
||||
* events, displaying the in-progress composition to the UI and forwarding the final composition
|
||||
* to the handler.
|
||||
*/
|
||||
export class CompositionHelper {
|
||||
/**
|
||||
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
|
||||
* IME. This variable determines whether the compositionText should be displayed on the UI.
|
||||
*/
|
||||
private _isComposing: boolean;
|
||||
|
||||
/**
|
||||
* The position within the input textarea's value of the current composition.
|
||||
*/
|
||||
private _compositionPosition: IPosition;
|
||||
|
||||
/**
|
||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||
* in-progress composition.
|
||||
*/
|
||||
private _isSendingComposition: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly _textarea: HTMLTextAreaElement,
|
||||
private readonly _compositionView: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
) {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
this._compositionPosition = { start: 0, end: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionstart event, activating the composition view.
|
||||
*/
|
||||
public compositionstart(): void {
|
||||
this._isComposing = true;
|
||||
this._compositionPosition.start = this._textarea.value.length;
|
||||
this._compositionView.textContent = '';
|
||||
this._compositionView.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionupdate event, updating the composition view.
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
this._compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
this._compositionPosition.end = this._textarea.value.length;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionend event, hiding the composition view and sending the composition to
|
||||
* the handler.
|
||||
*/
|
||||
public compositionend(): void {
|
||||
this._finalizeComposition(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
|
||||
* @param ev The keydown event.
|
||||
* @return Whether the Terminal should continue processing the keydown event.
|
||||
*/
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
if (this._isComposing || this._isSendingComposition) {
|
||||
if (ev.keyCode === 229) {
|
||||
// Continue composing if the keyCode is the "composition character"
|
||||
return false;
|
||||
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
|
||||
// Continue composing if the keyCode is a modifier key
|
||||
return false;
|
||||
}
|
||||
// Finish composition immediately. This is mainly here for the case where enter is
|
||||
// pressed and the handler needs to be triggered before the command is executed.
|
||||
this._finalizeComposition(false);
|
||||
}
|
||||
|
||||
if (ev.keyCode === 229) {
|
||||
// If the "composition character" is used but gets to this point it means a non-composition
|
||||
// character (eg. numbers and punctuation) was pressed when the IME was active.
|
||||
this._handleAnyTextareaChanges();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||
* is ending.
|
||||
* @param waitForPropagation Whether to wait for events to propagate before sending
|
||||
* the input. This should be false if a non-composition keystroke is entered before the
|
||||
* compositionend event is triggered, such as enter, so that the composition is sent before
|
||||
* the command is executed.
|
||||
*/
|
||||
private _finalizeComposition(waitForPropagation: boolean): void {
|
||||
this._compositionView.classList.remove('active');
|
||||
this._isComposing = false;
|
||||
this._clearTextareaPosition();
|
||||
|
||||
if (!waitForPropagation) {
|
||||
// Cancel any delayed composition send requests and send the input immediately.
|
||||
this._isSendingComposition = false;
|
||||
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
|
||||
this._coreService.triggerDataEvent(input, true);
|
||||
} else {
|
||||
// Make a deep copy of the composition position here as a new compositionstart event may
|
||||
// fire before the setTimeout executes.
|
||||
const currentCompositionPosition = {
|
||||
start: this._compositionPosition.start,
|
||||
end: this._compositionPosition.end
|
||||
};
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||
// complete. This ensures the correct character is retrieved.
|
||||
// This solution was used because:
|
||||
// - The compositionend event's data property is unreliable, at least on Chromium
|
||||
// - The last compositionupdate event's data property does not always accurately describe
|
||||
// the character, a counter example being Korean where an ending consonsant can move to
|
||||
// the following character if the following input is a vowel.
|
||||
this._isSendingComposition = true;
|
||||
setTimeout(() => {
|
||||
// Ensure that the input has not already been sent
|
||||
if (this._isSendingComposition) {
|
||||
this._isSendingComposition = false;
|
||||
let input;
|
||||
if (this._isComposing) {
|
||||
// Use the end position to get the string if a new composition has started.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||
} else {
|
||||
// Don't use the end position here in order to pick up any characters after the
|
||||
// composition has finished, for example when typing a non-composition character
|
||||
// (eg. 2) after a composition character.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start);
|
||||
}
|
||||
this._coreService.triggerDataEvent(input, true);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply any changes made to the textarea after the current event chain is allowed to complete.
|
||||
* This should be called when not currently composing but a keydown event with the "composition
|
||||
* character" (229) is triggered, in order to allow non-composition text to be entered when an
|
||||
* IME is active.
|
||||
*/
|
||||
private _handleAnyTextareaChanges(): void {
|
||||
const oldValue = this._textarea.value;
|
||||
setTimeout(() => {
|
||||
// Ignore if a composition has started since the timeout
|
||||
if (!this._isComposing) {
|
||||
const newValue = this._textarea.value;
|
||||
const diff = newValue.replace(oldValue, '');
|
||||
if (diff.length > 0) {
|
||||
this._coreService.triggerDataEvent(diff, true);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the composition view on top of the cursor and the textarea just below it (so the
|
||||
* IME helper dialog is positioned correctly).
|
||||
* @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is
|
||||
* necessary as the IME events across browsers are not consistently triggered.
|
||||
*/
|
||||
public updateCompositionElements(dontRecurse?: boolean): void {
|
||||
if (!this._isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._bufferService.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
|
||||
const cursorTop = this._bufferService.buffer.y * cellHeight;
|
||||
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
|
||||
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
this._compositionView.style.height = cellHeight + 'px';
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this._compositionView.style.fontFamily = this._optionsService.options.fontFamily;
|
||||
this._compositionView.style.fontSize = this._optionsService.options.fontSize + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
this._textarea.style.left = cursorLeft + 'px';
|
||||
this._textarea.style.top = cursorTop + 'px';
|
||||
this._textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this._textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
}
|
||||
|
||||
if (!dontRecurse) {
|
||||
setTimeout(() => this.updateCompositionElements(true), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the textarea's position so that the cursor does not blink on IE.
|
||||
* @private
|
||||
*/
|
||||
private _clearTextareaPosition(): void {
|
||||
this._textarea.style.left = '';
|
||||
this._textarea.style.top = '';
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event. The result
|
||||
* is returned as an array in the form [x, y] instead of an object as it's a
|
||||
* little faster and this function is used in some low level code.
|
||||
* @param event The mouse event.
|
||||
* @param element The terminal's container element.
|
||||
* @param colCount The number of columns in the terminal.
|
||||
* @param rowCount The number of rows n the terminal.
|
||||
* @param isSelection Whether the request is for the selection or not. This will
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
// Coordinates cannot be measured if there are no valid
|
||||
if (!hasValidCharSize) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const coords = getCoordsRelativeToElement(event, element);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / actualCellHeight);
|
||||
|
||||
// Ensure coordinates are within the terminal viewport. Note that selections
|
||||
// need an addition point of precision to cover the end point (as characters
|
||||
// cover half of one char and half of the next).
|
||||
coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0));
|
||||
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event, wrapping
|
||||
* them to the bounds of the terminal and adding 32 to both the x and y values
|
||||
* as expected by xterm.
|
||||
*/
|
||||
export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined {
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// xterm sends raw bytes and starts at 32 (SP) for each.
|
||||
return { x: coords[0] + 32, y: coords[1] + 32 };
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { C0 } from 'common/data/EscapeSequences';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
const enum Direction {
|
||||
UP = 'A',
|
||||
DOWN = 'B',
|
||||
RIGHT = 'C',
|
||||
LEFT = 'D'
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates all the arrow sequences together.
|
||||
* Resets the starting row to an unwrapped row, moves to the requested row,
|
||||
* then moves to requested col.
|
||||
*/
|
||||
export function moveToCellSequence(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
const startX = bufferService.buffer.x;
|
||||
const startY = bufferService.buffer.y;
|
||||
|
||||
// The alt buffer should try to navigate between rows
|
||||
if (!bufferService.buffer.hasScrollback) {
|
||||
return resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) +
|
||||
moveToRequestedRow(startY, targetY, bufferService, applicationCursor) +
|
||||
moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor);
|
||||
}
|
||||
|
||||
// Only move horizontally for the normal buffer
|
||||
return moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the initial position of the cursor is on a row that is wrapped, move the
|
||||
* cursor up to the first row that is not wrapped to have accurate vertical
|
||||
* positioning.
|
||||
*/
|
||||
function resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) {
|
||||
return '';
|
||||
}
|
||||
return repeat(bufferLine(
|
||||
startX, startY, startX,
|
||||
startY - wrappedRowsForRow(bufferService, startY), false, bufferService
|
||||
).length, sequence(Direction.LEFT, applicationCursor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the reset starting and ending row, move to the requested row,
|
||||
* ignoring wrapped rows
|
||||
*/
|
||||
function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
const startRow = startY - wrappedRowsForRow(bufferService, startY);
|
||||
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
|
||||
const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService);
|
||||
|
||||
return repeat(rowsToMove, sequence(verticalDirection(startY, targetY), applicationCursor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the requested col on the ending row
|
||||
*/
|
||||
function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
let startRow;
|
||||
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
|
||||
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
} else {
|
||||
startRow = startY;
|
||||
}
|
||||
|
||||
const endRow = targetY;
|
||||
const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor);
|
||||
|
||||
return repeat(bufferLine(
|
||||
startX, startRow, targetX, endRow,
|
||||
direction === Direction.RIGHT, bufferService
|
||||
).length, sequence(direction, applicationCursor));
|
||||
}
|
||||
|
||||
function moveHorizontallyOnly(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor);
|
||||
return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows between the unwrapped starting and
|
||||
* ending rows. These rows need to ignored since the cursor skips over them.
|
||||
*/
|
||||
function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number {
|
||||
let wrappedRows = 0;
|
||||
const startRow = startY - wrappedRowsForRow(bufferService, startY);
|
||||
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
|
||||
const line = bufferService.buffer.lines.get(startRow + (direction * i));
|
||||
if (line && line.isWrapped) {
|
||||
wrappedRows++;
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows that make up a given row.
|
||||
* @param currentRow The row to determine how many wrapped rows make it up
|
||||
*/
|
||||
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
|
||||
let rowCount = 0;
|
||||
let line = bufferService.buffer.lines.get(currentRow);
|
||||
let lineWraps = line && line.isWrapped;
|
||||
|
||||
while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) {
|
||||
rowCount++;
|
||||
line = bufferService.buffer.lines.get(--currentRow);
|
||||
lineWraps = line && line.isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direction determiners
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if the right or left arrow is needed
|
||||
*/
|
||||
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
|
||||
let startRow;
|
||||
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
|
||||
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
} else {
|
||||
startRow = startY;
|
||||
}
|
||||
|
||||
if ((startX < targetX &&
|
||||
startRow <= targetY) || // down/right or same y/right
|
||||
(startX >= targetX &&
|
||||
startRow < targetY)) { // down/left or same y/left
|
||||
return Direction.RIGHT;
|
||||
}
|
||||
return Direction.LEFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the up or down arrow is needed
|
||||
*/
|
||||
function verticalDirection(startY: number, targetY: number): Direction {
|
||||
return startY > targetY ? Direction.UP : Direction.DOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the string of chars in the buffer from a starting row and col
|
||||
* to an ending row and col
|
||||
* @param startCol The starting column position
|
||||
* @param startRow The starting row position
|
||||
* @param endCol The ending column position
|
||||
* @param endRow The ending row position
|
||||
* @param forward Direction to move
|
||||
*/
|
||||
function bufferLine(
|
||||
startCol: number,
|
||||
startRow: number,
|
||||
endCol: number,
|
||||
endRow: number,
|
||||
forward: boolean,
|
||||
bufferService: IBufferService
|
||||
): string {
|
||||
let currentCol = startCol;
|
||||
let currentRow = startRow;
|
||||
let bufferStr = '';
|
||||
|
||||
while (currentCol !== endCol || currentRow !== endRow) {
|
||||
currentCol += forward ? 1 : -1;
|
||||
|
||||
if (forward && currentCol > bufferService.cols - 1) {
|
||||
bufferStr += bufferService.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
currentCol = 0;
|
||||
startCol = 0;
|
||||
currentRow++;
|
||||
} else if (!forward && currentCol < 0) {
|
||||
bufferStr += bufferService.buffer.translateBufferLineToString(
|
||||
currentRow, false, 0, startCol + 1
|
||||
);
|
||||
currentCol = bufferService.cols - 1;
|
||||
startCol = currentCol;
|
||||
currentRow--;
|
||||
}
|
||||
}
|
||||
|
||||
return bufferStr + bufferService.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the escape sequence for clicking an arrow
|
||||
* @param direction The direction to move
|
||||
*/
|
||||
function sequence(direction: Direction, applicationCursor: boolean): string {
|
||||
const mod = applicationCursor ? 'O' : '[';
|
||||
return C0.ESC + mod + direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string repeated a given number of times
|
||||
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
|
||||
* @param count The number of times to repeat the string
|
||||
* @param string The string that is to be repeated
|
||||
*/
|
||||
function repeat(count: number, str: string): string {
|
||||
count = Math.floor(count);
|
||||
let rpt = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
rpt += str;
|
||||
}
|
||||
return rpt;
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
|
||||
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { toCss, ensureContrastRatioRgba, opaque } from 'browser/Color';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
private _scaledCellHeight: number = 0;
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
protected _charAtlas: BaseCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||
*/
|
||||
private _currentGlyphIdentifier: IGlyphIdentifier = {
|
||||
chars: '',
|
||||
code: 0,
|
||||
bg: 0,
|
||||
fg: 0,
|
||||
bold: false,
|
||||
dim: false,
|
||||
italic: false
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet,
|
||||
private _rendererId: number,
|
||||
protected readonly _bufferService: IBufferService,
|
||||
protected readonly _optionsService: IOptionsService
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
this._charAtlas?.dispose();
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {}
|
||||
public onBlur(): void {}
|
||||
public onFocus(): void {}
|
||||
public onCursorMove(): void {}
|
||||
public onGridChanged(startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||
|
||||
public setColors(colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(colorSet);
|
||||
}
|
||||
|
||||
protected _setTransparency(alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Cloning preserves properties
|
||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(this._colors);
|
||||
this.onGridChanged(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(this._colors);
|
||||
}
|
||||
|
||||
public abstract reset(): void;
|
||||
|
||||
/**
|
||||
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at
|
||||
* @param width The number of columns to fill.
|
||||
* @param height The number of rows to fill.
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
window.devicePixelRatio,
|
||||
this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
|
||||
* strokeStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
||||
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
||||
width * this._scaledCellWidth - window.devicePixelRatio,
|
||||
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire canvas.
|
||||
*/
|
||||
protected _clearAll(): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears 1+ cells completely.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at.
|
||||
* @param width The number of columns to clear.
|
||||
* @param height The number of rows to clear.
|
||||
*/
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a truecolor character at the cell. The character will be clipped to
|
||||
* ensure that it fits with the cell, including the cell to the right if it's
|
||||
* a wide character. This uses the existing fillStyle on the context.
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._clipRow(y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
* @param chars The character or characters.
|
||||
* @param code The character code.
|
||||
* @param width The width of the characters.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param bg The background color, in the format stored within the attributes.
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
||||
const contrastColor = this._getContrastColor(cell);
|
||||
|
||||
// skip cache right away if we draw in RGB
|
||||
// Note: to avoid bad runtime JoinedCellData will be skipped
|
||||
// in the cache handler itself (atlasDidDraw == false) and
|
||||
// fall through to uncached later down below
|
||||
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
|
||||
this._drawUncachedChars(cell, x, y, contrastColor);
|
||||
return;
|
||||
}
|
||||
|
||||
let fg;
|
||||
let bg;
|
||||
if (cell.isInverse()) {
|
||||
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
|
||||
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
|
||||
} else {
|
||||
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
|
||||
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
|
||||
}
|
||||
|
||||
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
|
||||
this._currentGlyphIdentifier.bg = bg;
|
||||
this._currentGlyphIdentifier.fg = fg;
|
||||
this._currentGlyphIdentifier.bold = !!cell.isBold();
|
||||
this._currentGlyphIdentifier.dim = !!cell.isDim();
|
||||
this._currentGlyphIdentifier.italic = !!cell.isItalic();
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
this._currentGlyphIdentifier,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one or more characters at one or more cells. The character(s) will be
|
||||
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||
* right if the last character is a wide character.
|
||||
* @param chars The character.
|
||||
* @param width The width of the character.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.textBaseline = 'middle';
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (fgOverride) {
|
||||
this._ctx.fillStyle = fgOverride.css;
|
||||
} else if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = opaque(this._colors.background).css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
} else {
|
||||
if (fgOverride) {
|
||||
this._ctx.fillStyle = fgOverride.css;
|
||||
} else if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
this._clipRow(y);
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (cell.isDim()) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param y The row to clip.
|
||||
*/
|
||||
private _clipRow(y: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
this._bufferService.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current font.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
|
||||
}
|
||||
|
||||
private _getContrastColor(cell: CellData): IColor | undefined {
|
||||
if (this._optionsService.options.minimumContrastRatio === 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
}
|
||||
|
||||
let fgColor = cell.getFgColor();
|
||||
let fgColorMode = cell.getFgColorMode();
|
||||
let bgColor = cell.getBgColor();
|
||||
let bgColorMode = cell.getBgColorMode();
|
||||
const isInverse = !!cell.isInverse();
|
||||
const isBold = !!cell.isInverse();
|
||||
if (isInverse) {
|
||||
const temp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = temp;
|
||||
const temp2 = fgColorMode;
|
||||
fgColorMode = bgColorMode;
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
|
||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
|
||||
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
|
||||
|
||||
if (!result) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const color: IColor = {
|
||||
css: toCss(
|
||||
(result >> 24) & 0xFF,
|
||||
(result >> 16) & 0xFF,
|
||||
(result >> 8) & 0xFF
|
||||
),
|
||||
rgba: result
|
||||
};
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
return this._colors.ansi[bgColor].rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return bgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
return this._colors.background.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._colors.ansi[fgColor].rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return fgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._colors.background.rgba;
|
||||
}
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferLine, ICellData, CharData } from 'common/Types';
|
||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from 'browser/renderer/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
export class JoinedCellData extends AttributeData implements ICellData {
|
||||
private _width: number;
|
||||
// .content carries no meaning for joined CellData, simply nullify it
|
||||
// thus we have to overload all other .content accessors
|
||||
public content: number = 0;
|
||||
public fg: number;
|
||||
public bg: number;
|
||||
public combinedData: string = '';
|
||||
|
||||
constructor(firstCell: ICellData, chars: string, width: number) {
|
||||
super();
|
||||
this.fg = firstCell.fg;
|
||||
this.bg = firstCell.bg;
|
||||
this.combinedData = chars;
|
||||
this._width = width;
|
||||
}
|
||||
|
||||
public isCombined(): number {
|
||||
// always mark joined cell data as combined
|
||||
return Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public getChars(): string {
|
||||
return this.combinedData;
|
||||
}
|
||||
|
||||
public getCode(): number {
|
||||
// code always gets the highest possible fake codepoint (read as -1)
|
||||
// this is needed as code is used by caches as identifier
|
||||
return 0x1FFFFF;
|
||||
}
|
||||
|
||||
public setFromCharData(value: CharData): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
}
|
||||
|
||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
|
||||
private _characterJoiners: ICharacterJoiner[] = [];
|
||||
private _nextCharacterJoinerId: number = 0;
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
constructor(private _bufferService: IBufferService) { }
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
const joiner: ICharacterJoiner = {
|
||||
id: this._nextCharacterJoinerId++,
|
||||
handler
|
||||
};
|
||||
|
||||
this._characterJoiners.push(joiner);
|
||||
return joiner.id;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
for (let i = 0; i < this._characterJoiners.length; i++) {
|
||||
if (this._characterJoiners[i].id === joinerId) {
|
||||
this._characterJoiners.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getJoinedCharacters(row: number): [number, number][] {
|
||||
if (this._characterJoiners.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const line = this._bufferService.buffer.lines.get(row);
|
||||
if (!line || line.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ranges: [number, number][] = [];
|
||||
const lineStr = line.translateToString(true);
|
||||
|
||||
// Because some cells can be represented by multiple javascript characters,
|
||||
// we track the cell and the string indexes separately. This allows us to
|
||||
// translate the string ranges we get from the joiners back into cell ranges
|
||||
// for use when rendering
|
||||
let rangeStartColumn = 0;
|
||||
let currentStringIndex = 0;
|
||||
let rangeStartStringIndex = 0;
|
||||
let rangeAttrFG = line.getFg(0);
|
||||
let rangeAttrBG = line.getBg(0);
|
||||
|
||||
for (let x = 0; x < line.getTrimmedLength(); x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
// If this character is of width 0, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of range
|
||||
if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) {
|
||||
// If we ended up with a sequence of more than one character,
|
||||
// look for ranges to join.
|
||||
if (x - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our markers for a new range.
|
||||
rangeStartColumn = x;
|
||||
rangeStartStringIndex = currentStringIndex;
|
||||
rangeAttrFG = this._workCell.fg;
|
||||
rangeAttrBG = this._workCell.bg;
|
||||
}
|
||||
|
||||
currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length;
|
||||
}
|
||||
|
||||
// Process any trailing ranges.
|
||||
if (this._bufferService.cols - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a segment of a line of text, find all ranges of text that should be
|
||||
* joined in a single rendering unit. Ranges are internally converted to
|
||||
* column ranges, rather than string ranges.
|
||||
* @param line String representation of the full line of text
|
||||
* @param startIndex Start position of the range to search in the string (inclusive)
|
||||
* @param endIndex End position of the range to search in the string (exclusive)
|
||||
*/
|
||||
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
|
||||
const text = line.substring(startIndex, endIndex);
|
||||
// At this point we already know that there is at least one joiner so
|
||||
// we can just pull its value and assign it directly rather than
|
||||
// merging it into an empty array, which incurs unnecessary writes.
|
||||
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
|
||||
for (let i = 1; i < this._characterJoiners.length; i++) {
|
||||
// We merge any overlapping ranges across the different joiners
|
||||
const joinerRanges = this._characterJoiners[i].handler(text);
|
||||
for (let j = 0; j < joinerRanges.length; j++) {
|
||||
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
|
||||
}
|
||||
}
|
||||
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
|
||||
return joinedRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the provided ranges in-place to adjust for variations between
|
||||
* string length and cell width so that the range represents a cell range,
|
||||
* rather than the string range the joiner provides.
|
||||
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
||||
* @param line Cell data for the relevant line in the terminal
|
||||
* @param startCol Offset within the line to start from
|
||||
*/
|
||||
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
|
||||
let currentRangeIndex = 0;
|
||||
let currentRangeStarted = false;
|
||||
let currentStringIndex = 0;
|
||||
let currentRange = ranges[currentRangeIndex];
|
||||
|
||||
// If we got through all of the ranges, stop searching
|
||||
if (!currentRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x = startCol; x < this._bufferService.cols; x++) {
|
||||
const width = line.getWidth(x);
|
||||
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
|
||||
|
||||
// We skip zero-width characters when creating the string to join the text
|
||||
// so we do the same here
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust the start of the range
|
||||
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
}
|
||||
|
||||
// Adjust the end of the range
|
||||
if (currentRange[1] <= currentStringIndex) {
|
||||
currentRange[1] = x;
|
||||
|
||||
// We're finished with this range, so we move to the next one
|
||||
currentRange = ranges[++currentRangeIndex];
|
||||
|
||||
// If there are no more ranges left, stop searching
|
||||
if (!currentRange) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Ranges can be on adjacent characters. Because the end index of the
|
||||
// ranges are exclusive, this means that the index for the start of a
|
||||
// range can be the same as the end index of the previous range. To
|
||||
// account for the start of the next range, we check here just in case.
|
||||
if (currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
} else {
|
||||
currentRangeStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the string index based on the character length to line up with
|
||||
// the column adjustment
|
||||
currentStringIndex += length;
|
||||
}
|
||||
|
||||
// If there is still a range left at the end, it must extend all the way to
|
||||
// the end of the line.
|
||||
if (currentRange) {
|
||||
currentRange[1] = this._bufferService.cols;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
* @param ranges Existing range list
|
||||
* @param newRange Tuple of two numbers representing the new range to merge in.
|
||||
* @returns The ranges input with the new range merged in place
|
||||
*/
|
||||
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, newRange);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[0] < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
inRange = true;
|
||||
}
|
||||
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
} else {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRange[1];
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
||||
ranges.splice(i, 1);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRange[1];
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push(newRange);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
y: number;
|
||||
isFocused: boolean;
|
||||
style: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time between cursor blinks.
|
||||
*/
|
||||
const BLINK_INTERVAL = 600;
|
||||
|
||||
export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _state: ICursorState;
|
||||
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
|
||||
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRefreshRowsEvent>,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService,
|
||||
private readonly _coreService: ICoreService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
this._cursorRenderers = {
|
||||
'bar': this._renderBarCursor.bind(this),
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._clearCursor();
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
this.onOptionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.pause();
|
||||
}
|
||||
this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.resume();
|
||||
} else {
|
||||
this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
if (this._optionsService.options.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
|
||||
this._render(true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}
|
||||
// Request a refresh from the terminal as management of rendering is being
|
||||
// moved back to the terminal
|
||||
this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
public onGridChanged(startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(false);
|
||||
} else {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private _render(triggeredByAnimationFrame: boolean): void {
|
||||
// Don't draw the cursor if it's hidden
|
||||
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
|
||||
const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
|
||||
|
||||
// Don't draw the cursor if it's off-screen
|
||||
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
this._bufferService.buffer.lines.get(cursorY)!.loadCell(this._bufferService.buffer.x, this._cell);
|
||||
if (this._cell.content === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
const cursorStyle = this._optionsService.options.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
|
||||
}
|
||||
this._ctx.restore();
|
||||
this._state.x = this._bufferService.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't draw the cursor if it's blinking
|
||||
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state) {
|
||||
// The cursor is already in the correct spot, don't redraw
|
||||
if (this._state.x === this._bufferService.buffer.x &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === this._coreBrowserService.isFocused &&
|
||||
this._state.style === this._optionsService.options.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
this._clearCursor();
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = this._bufferService.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = this._optionsService.options.cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
class CursorBlinkStateManager {
|
||||
public isCursorVisible: boolean;
|
||||
|
||||
private _animationFrame: number | undefined;
|
||||
private _blinkStartTimeout: number | undefined;
|
||||
private _blinkInterval: number | undefined;
|
||||
|
||||
/**
|
||||
* The time at which the animation frame was restarted, this is used on the
|
||||
* next render to restart the timers so they don't need to restart the timers
|
||||
* multiple times over a short period.
|
||||
*/
|
||||
private _animationTimeRestarted: number | undefined;
|
||||
|
||||
constructor(
|
||||
isFocused: boolean,
|
||||
private _renderCallback: () => void
|
||||
) {
|
||||
this.isCursorVisible = true;
|
||||
if (isFocused) {
|
||||
this._restartInterval();
|
||||
}
|
||||
}
|
||||
|
||||
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
|
||||
|
||||
public dispose(): void {
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public restartBlinkAnimation(): void {
|
||||
if (this.isPaused) {
|
||||
return;
|
||||
}
|
||||
// Save a timestamp so that the restart can be done on the next interval
|
||||
this._animationTimeRestarted = Date.now();
|
||||
// Force a cursor render to ensure it's visible and in the correct position
|
||||
this.isCursorVisible = true;
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
|
||||
// Clear any existing interval
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
}
|
||||
|
||||
// Setup the initial timeout which will hide the cursor, this is done before
|
||||
// the regular interval is setup in order to support restarting the blink
|
||||
// animation in a lightweight way (without thrashing clearInterval and
|
||||
// setInterval).
|
||||
this._blinkStartTimeout = <number><any>setTimeout(() => {
|
||||
// Check if another animation restart was requested while this was being
|
||||
// started
|
||||
if (this._animationTimeRestarted) {
|
||||
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
|
||||
this._animationTimeRestarted = undefined;
|
||||
if (time > 0) {
|
||||
this._restartInterval(time);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the cursor
|
||||
this.isCursorVisible = false;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
|
||||
// Setup the blink interval
|
||||
this._blinkInterval = <number><any>setInterval(() => {
|
||||
// Adjust the animation time if it was restarted
|
||||
if (this._animationTimeRestarted) {
|
||||
// calc time diff
|
||||
// Make restart interval do a setTimeout initially?
|
||||
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
|
||||
this._animationTimeRestarted = undefined;
|
||||
this._restartInterval(time);
|
||||
return;
|
||||
}
|
||||
|
||||
// Invert visibility and render
|
||||
this.isCursorVisible = !this.isCursorVisible;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}, BLINK_INTERVAL);
|
||||
}, timeToStart);
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.isCursorVisible = true;
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this._animationTimeRestarted = undefined;
|
||||
this._restartInterval();
|
||||
this.restartBlinkAnimation();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export class GridCache<T> {
|
||||
public cache: (T | undefined)[][];
|
||||
|
||||
public constructor() {
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
public resize(width: number, height: number): void {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (this.cache.length <= x) {
|
||||
this.cache.push([]);
|
||||
}
|
||||
for (let y = this.cache[x].length; y < height; y++) {
|
||||
this.cache[x].push(undefined);
|
||||
}
|
||||
this.cache[x].length = height;
|
||||
}
|
||||
this.cache.length = width;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (let x = 0; x < this.cache.length; x++) {
|
||||
for (let y = 0; y < this.cache[x].length; y++) {
|
||||
this.cache[x][y] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
linkifier: ILinkifier,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = undefined;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
|
||||
private _clearCurrentLink(): void {
|
||||
if (this._state) {
|
||||
this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
|
||||
const middleRowCount = this._state.y2 - this._state.y1 - 1;
|
||||
if (middleRowCount > 0) {
|
||||
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
|
||||
}
|
||||
this._clearCells(0, this._state.y2, this._state.x2, 1);
|
||||
this._state = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (e.fg && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
// Single line link
|
||||
this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||
} else {
|
||||
// Multi-line link
|
||||
this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
|
||||
for (let y = e.y1 + 1; y < e.y2; y++) {
|
||||
this._fillBottomLineAtCells(0, y, e.cols);
|
||||
}
|
||||
this._fillBottomLineAtCells(0, e.y2, e.x2);
|
||||
}
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
|
||||
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
|
||||
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
|
||||
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
|
||||
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifier } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
|
||||
let nextRendererId = 1;
|
||||
|
||||
export class Renderer extends Disposable implements IRenderer {
|
||||
private _id = nextRendererId++;
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
|
||||
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
|
||||
|
||||
constructor(
|
||||
private _colors: IColorSet,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _linkifier: ILinkifier,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICoreService readonly coreService: ICoreService,
|
||||
@ICoreBrowserService readonly coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super();
|
||||
const allowTransparency = this._optionsService.options.allowTransparency;
|
||||
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
|
||||
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
|
||||
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, this._linkifier, this._bufferService, _optionsService),
|
||||
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRefreshRows, this._bufferService, _optionsService, coreService, coreBrowserService)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this._updateDimensions();
|
||||
this.onOptionsChanged();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
removeTerminalFromCache(this._id);
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== window.devicePixelRatio) {
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this.onResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
l.setColors(this._colors);
|
||||
l.reset();
|
||||
});
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this.dimensions));
|
||||
|
||||
// Resize the screen
|
||||
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._runOperation(l => l.onBlur());
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._runOperation(l => l.onFocus());
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._runOperation(l => l.onCursorMove());
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._runOperation(l => l.onOptionsChanged());
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._runOperation(l => l.reset());
|
||||
}
|
||||
|
||||
private _runOperation(operation: (layer: IRenderLayer) => void): void {
|
||||
this._renderLayers.forEach(l => operation(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the refresh loop callback, calling refresh only if a refresh is
|
||||
* necessary before queueing up the next one.
|
||||
*/
|
||||
public renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(start, end));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case
|
||||
// devicePixelRatio is a floating point number in order to ensure there is
|
||||
// enough space to draw the character to the cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. ICharSizeService.width/height by itself
|
||||
// is insufficient when the page is not at 100% zoom level as it's measured
|
||||
// in CSS pixels, but the actual char size on the canvas can differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
start?: [number, number];
|
||||
end?: [number, number];
|
||||
columnSelectMode?: boolean;
|
||||
ydisp?: number;
|
||||
}
|
||||
|
||||
export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state!: ISelectionState;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
private _clearState(): void {
|
||||
this._state = {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
columnSelectMode: undefined,
|
||||
ydisp: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
if (this._state.start && this._state.end) {
|
||||
this._clearState();
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
// Selection has not changed
|
||||
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all selections
|
||||
this._clearAll();
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
this._clearState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ctx.fillStyle = this._colors.selection.css;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = start[0];
|
||||
const width = end[0] - startCol;
|
||||
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
||||
this._fillCells(startCol, viewportCappedStartRow, width, height);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
||||
this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount);
|
||||
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
this._fillCells(0, viewportCappedEndRow, endCol, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Save state for next render
|
||||
this._state.start = [start[0], start[1]];
|
||||
this._state.end = [end[0], end[1]];
|
||||
this._state.columnSelectMode = columnSelectMode;
|
||||
this._state.ydisp = this._bufferService.buffer.ydisp;
|
||||
}
|
||||
|
||||
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean, ydisp: number): boolean {
|
||||
return !this._areCoordinatesEqual(start, this._state.start) ||
|
||||
!this._areCoordinatesEqual(end, this._state.end) ||
|
||||
columnSelectMode !== this._state.columnSelectMode ||
|
||||
ydisp !== this._state.ydisp;
|
||||
}
|
||||
|
||||
private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean {
|
||||
if (!coord1 || !coord2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
* when the character changes (a regular space ' ' character may not as it's
|
||||
* drawn state is a cleared cell).
|
||||
*/
|
||||
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
||||
|
||||
export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _state: GridCache<CharData>;
|
||||
private _characterWidth: number = 0;
|
||||
private _characterFont: string = '';
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
characterJoinerRegistry: ICharacterJoinerRegistry,
|
||||
alpha: boolean,
|
||||
rendererId: number,
|
||||
readonly bufferService: IBufferService,
|
||||
readonly optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
|
||||
// Clear the character width cache if the font or width has changed
|
||||
const terminalFont = this._getFont(false, false);
|
||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
this._characterFont = terminalFont;
|
||||
this._characterOverlapCache = {};
|
||||
}
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state.clear();
|
||||
this._state.resize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._state.clear();
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
private _forEachCell(
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
cell: ICellData,
|
||||
x: number,
|
||||
y: number
|
||||
) => void
|
||||
): void {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const line = this._bufferService.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < this._bufferService.cols; x++) {
|
||||
line!.loadCell(x, this._workCell);
|
||||
let cell = this._workCell;
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by
|
||||
// the char at x-1
|
||||
if (cell.getWidth() === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift()!;
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
|
||||
cell = new JoinedCellData(
|
||||
this._workCell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
range[1] - range[0]
|
||||
);
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the
|
||||
// right is a space, take ownership of the cell to the right. We skip
|
||||
// this check for joined characters because their rendering likely won't
|
||||
// yield the same result as rendering the last character individually.
|
||||
if (!isJoined && this._isOverlapping(cell)) {
|
||||
// If the character is overlapping, we want to force a re-render on every
|
||||
// frame. This is specifically to work around the case where two
|
||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||
// space is added. Without this, the first half of `b` would never
|
||||
// get removed, and `a` would not re-render because it thinks it's
|
||||
// already in the correct state.
|
||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
|
||||
// patch width to 2
|
||||
cell.content &= ~Content.WIDTH_MASK;
|
||||
cell.content |= 2 << Content.WIDTH_SHIFT;
|
||||
// this._clearChar(x + 1, y);
|
||||
// The overlapping char's char data will force a clear and render when the
|
||||
// overlapping char is no longer to the left of the character and also when
|
||||
// the space changes to another character.
|
||||
// this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
callback(
|
||||
cell,
|
||||
x,
|
||||
y
|
||||
);
|
||||
|
||||
x = lastCharX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
|
||||
* same color together to reduce draw calls.
|
||||
*/
|
||||
private _drawBackground(firstRow: number, lastRow: number): void {
|
||||
const ctx = this._ctx;
|
||||
const cols = this._bufferService.cols;
|
||||
let startX: number = 0;
|
||||
let startY: number = 0;
|
||||
let prevFillStyle: string | null = null;
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isFgDefault()) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
|
||||
}
|
||||
} else if (cell.isBgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else if (cell.isBgPalette()) {
|
||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
// This is either the first iteration, or the default background was set. Either way, we
|
||||
// don't need to draw anything.
|
||||
startX = x;
|
||||
startY = y;
|
||||
}
|
||||
|
||||
if (y !== startY) {
|
||||
// our row changed, draw the previous row
|
||||
ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
|
||||
this._fillCells(startX, startY, cols - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
} else if (prevFillStyle !== nextFillStyle) {
|
||||
// our color changed, draw the previous characters in this row
|
||||
ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
|
||||
this._fillCells(startX, startY, x - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
}
|
||||
|
||||
prevFillStyle = nextFillStyle;
|
||||
});
|
||||
|
||||
// flush the last color we encountered
|
||||
if (prevFillStyle !== null) {
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
this._fillCells(startX, startY, cols - startX, 1);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
return;
|
||||
}
|
||||
this._drawChars(cell, x, y);
|
||||
if (cell.isUnderline()) {
|
||||
this._ctx.save();
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
} else {
|
||||
if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
this._ctx.restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onGridChanged(firstRow: number, lastRow: number): void {
|
||||
// Resize has not been called yet
|
||||
if (this._state.cache.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.beginFrame();
|
||||
}
|
||||
|
||||
this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
|
||||
this._drawBackground(firstRow, lastRow);
|
||||
this._drawForeground(firstRow, lastRow);
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._setTransparency(this._optionsService.options.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a character is overlapping to the next cell.
|
||||
*/
|
||||
private _isOverlapping(cell: ICellData): boolean {
|
||||
// Only single cell characters can be overlapping, rendering issues can
|
||||
// occur without this check
|
||||
if (cell.getWidth() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We assume that any ascii character will not overlap
|
||||
if (cell.getCode() < 256) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
|
||||
// Deliver from cache if available
|
||||
if (this._characterOverlapCache.hasOwnProperty(chars)) {
|
||||
return this._characterOverlapCache[chars];
|
||||
}
|
||||
|
||||
// Setup the font
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._characterFont;
|
||||
|
||||
// Measure the width of the character, but Math.floor it
|
||||
// because that is what the renderer does when it calculates
|
||||
// the character dimensions we are comparing against
|
||||
const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
|
||||
|
||||
// Restore the original context
|
||||
this._ctx.restore();
|
||||
|
||||
// Cache and return
|
||||
this._characterOverlapCache[chars] = overlaps;
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the charcater at the cell specified.
|
||||
* @param x The column of the char.
|
||||
* @param y The row of the char.
|
||||
*/
|
||||
// private _clearChar(x: number, y: number): void {
|
||||
// let colsToClear = 1;
|
||||
// // Clear the adjacent character if it was wide
|
||||
// const state = this._state.cache[x][y];
|
||||
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// colsToClear = 2;
|
||||
// }
|
||||
// this.clearCells(x, y, colsToClear, 1);
|
||||
// }
|
||||
}
|
||||
124
web/src/utils/xterm/src/browser/renderer/Types.d.ts
vendored
124
web/src/utils/xterm/src/browser/renderer/Types.d.ts
vendored
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
|
||||
export interface IRenderDimensions {
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharLeft: number;
|
||||
scaledCharTop: number;
|
||||
scaledCanvasWidth: number;
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
actualCellWidth: number;
|
||||
actualCellHeight: number;
|
||||
}
|
||||
|
||||
export interface IRequestRefreshRowsEvent {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
readonly dimensions: IRenderDimensions;
|
||||
|
||||
readonly onRequestRefreshRows: IEvent<IRequestRefreshRowsEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
}
|
||||
|
||||
export interface ICharacterJoiner {
|
||||
id: number;
|
||||
handler: CharacterJoinerHandler;
|
||||
}
|
||||
|
||||
export interface ICharacterJoinerRegistry {
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(colorSet: IColorSet): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
resize(dim: IRenderDimensions): void;
|
||||
|
||||
/**
|
||||
* Clear the state of the render layer.
|
||||
*/
|
||||
reset(): void;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export abstract class BaseCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
public dispose(): void { }
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
|
||||
* Implement _doWarmUp instead if you only want to get called once.
|
||||
*/
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. Used by the default
|
||||
* implementation of warmUp(), and will only be called once.
|
||||
*/
|
||||
protected _doWarmUp(): void { }
|
||||
|
||||
/**
|
||||
* Called when we start drawing a new frame.
|
||||
*
|
||||
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
|
||||
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
|
||||
* of BaseRenderLayer.
|
||||
*/
|
||||
public beginFrame(): void { }
|
||||
|
||||
/**
|
||||
* May be called before warmUp finishes, however it is okay for the implementation to
|
||||
* do nothing and return false in that case.
|
||||
*
|
||||
* @param ctx Where to draw the character onto.
|
||||
* @param glyph Information about what to draw
|
||||
* @param x The position on the context to start drawing at
|
||||
* @param y The position on the context to start drawing at
|
||||
* @returns The success state. True if we drew the character.
|
||||
*/
|
||||
public abstract draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas';
|
||||
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ITerminalOptions } from 'common/services/Services';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
atlas: BaseCharAtlas;
|
||||
config: ICharAtlasConfig;
|
||||
// N.B. This implementation potentially holds onto copies of the terminal forever, so
|
||||
// this may cause memory leaks.
|
||||
ownedBy: number[];
|
||||
}
|
||||
|
||||
const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
*/
|
||||
export function acquireCharAtlas(
|
||||
options: ITerminalOptions,
|
||||
rendererId: number,
|
||||
colors: IColorSet,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number
|
||||
): BaseCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors);
|
||||
|
||||
// Check to see if the renderer already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.atlas;
|
||||
}
|
||||
// The configs differ, release the renderer from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
entry.atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the renderer to the cache entry and return
|
||||
entry.ownedBy.push(rendererId);
|
||||
return entry.atlas;
|
||||
}
|
||||
}
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
atlas: new DynamicCharAtlas(
|
||||
document,
|
||||
newConfig
|
||||
),
|
||||
config: newConfig,
|
||||
ownedBy: [rendererId]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a terminal reference from the cache, allowing its memory to be freed.
|
||||
*/
|
||||
export function removeTerminalFromCache(rendererId: number): void {
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
|
||||
if (index !== -1) {
|
||||
if (charAtlasCache[i].ownedBy.length === 1) {
|
||||
// Remove the cache entry if it's the only renderer
|
||||
charAtlasCache[i].atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
// Remove the reference from the cache entry
|
||||
charAtlasCache[i].ownedBy.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
|
||||
import { DEFAULT_COLOR } from 'common/buffer/Constants';
|
||||
import { IColorSet, IPartialColorSet } from 'browser/Types';
|
||||
import { ITerminalOptions } from 'common/services/Services';
|
||||
|
||||
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors = <IPartialColorSet>{
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: undefined,
|
||||
cursorAccent: undefined,
|
||||
selection: undefined,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(0, 16)
|
||||
};
|
||||
return {
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: options.fontFamily,
|
||||
fontSize: options.fontSize,
|
||||
fontWeight: options.fontWeight,
|
||||
fontWeightBold: options.fontWeightBold,
|
||||
allowTransparency: options.allowTransparency,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
}
|
||||
|
||||
export function is256Color(colorCode: number): boolean {
|
||||
return colorCode < DEFAULT_COLOR;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = 257;
|
||||
export const DIM_OPACITY = 0.5;
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
@@ -1,370 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
|
||||
import { LRUMap } from 'browser/renderer/atlas/LRUMap';
|
||||
import { isFirefox, isSafari } from 'common/Platform';
|
||||
import { IColor } from 'browser/Types';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { opaque } from 'browser/Color';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
|
||||
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
|
||||
// frame ASAP.
|
||||
//
|
||||
// This helps to limit the amount of damage a program can do when it would otherwise thrash the
|
||||
// cache.
|
||||
const FRAME_CACHE_DRAW_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
|
||||
* the operation as window.createImageBitmap is asynchronous.
|
||||
*/
|
||||
const GLYPH_BITMAP_COMMIT_DELAY = 100;
|
||||
|
||||
interface IGlyphCacheValue {
|
||||
index: number;
|
||||
isEmpty: boolean;
|
||||
inBitmap: boolean;
|
||||
}
|
||||
|
||||
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
|
||||
// Note that this only returns a valid key when code < 256
|
||||
// Layout:
|
||||
// 0b00000000000000000000000000000001: italic (1)
|
||||
// 0b00000000000000000000000000000010: dim (1)
|
||||
// 0b00000000000000000000000000000100: bold (1)
|
||||
// 0b00000000000000000000111111111000: fg (9)
|
||||
// 0b00000000000111111111000000000000: bg (9)
|
||||
// 0b00011111111000000000000000000000: code (8)
|
||||
// 0b11100000000000000000000000000000: unused (3)
|
||||
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||
}
|
||||
|
||||
export class DynamicCharAtlas extends BaseCharAtlas {
|
||||
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
|
||||
// It's ordered so that we can determine when to remove the old entries.
|
||||
private _cacheMap: LRUMap<IGlyphCacheValue>;
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
private _cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// The number of characters stored in the atlas by width/height
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
private _drawToCacheCount: number = 0;
|
||||
|
||||
// An array of glyph keys that are waiting on the bitmap to be generated.
|
||||
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
|
||||
|
||||
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
|
||||
private _bitmapCommitTimeout: number | null = null;
|
||||
|
||||
// The bitmap to draw from, this is much faster on other browsers than others.
|
||||
private _bitmap: ImageBitmap | null = null;
|
||||
|
||||
constructor(document: Document, private _config: ICharAtlasConfig) {
|
||||
super();
|
||||
this._cacheCanvas = document.createElement('canvas');
|
||||
this._cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this._cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true}));
|
||||
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
tmpCanvas.width = this._config.scaledCharWidth;
|
||||
tmpCanvas.height = this._config.scaledCharHeight;
|
||||
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
|
||||
|
||||
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
|
||||
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
|
||||
const capacity = this._width * this._height;
|
||||
this._cacheMap = new LRUMap(capacity);
|
||||
this._cacheMap.prealloc(capacity);
|
||||
|
||||
// This is useful for debugging
|
||||
// document.body.appendChild(this._cacheCanvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._bitmapCommitTimeout !== null) {
|
||||
window.clearTimeout(this._bitmapCommitTimeout);
|
||||
this._bitmapCommitTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
public beginFrame(): void {
|
||||
this._drawToCacheCount = 0;
|
||||
}
|
||||
|
||||
public draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean {
|
||||
// Space is always an empty cell, special case this as it's so common
|
||||
if (glyph.code === 32) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exit early for uncachable glyphs
|
||||
if (!this._canCache(glyph)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const glyphKey = getGlyphCacheKey(glyph);
|
||||
const cacheValue = this._cacheMap.get(glyphKey);
|
||||
if (cacheValue !== null && cacheValue !== undefined) {
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
} else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
|
||||
let index;
|
||||
if (this._cacheMap.size < this._cacheMap.capacity) {
|
||||
index = this._cacheMap.size;
|
||||
} else {
|
||||
// we're out of space, so our call to set will delete this item
|
||||
index = this._cacheMap.peek()!.index;
|
||||
}
|
||||
const cacheValue = this._drawToCache(glyph, index);
|
||||
this._cacheMap.set(glyphKey, cacheValue);
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _canCache(glyph: IGlyphIdentifier): boolean {
|
||||
// Only cache ascii and extended characters for now, to be safe. In the future, we could do
|
||||
// something more complicated to determine the expected width of a character.
|
||||
//
|
||||
// If we switch the renderer over to webgl at some point, we may be able to use blending modes
|
||||
// to draw overlapping glyphs from the atlas:
|
||||
// https://github.com/servo/webrender/issues/464#issuecomment-255632875
|
||||
// https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
|
||||
return glyph.code < 256;
|
||||
}
|
||||
|
||||
private _toCoordinateX(index: number): number {
|
||||
return (index % this._width) * this._config.scaledCharWidth;
|
||||
}
|
||||
|
||||
private _toCoordinateY(index: number): number {
|
||||
return Math.floor(index / this._width) * this._config.scaledCharHeight;
|
||||
}
|
||||
|
||||
private _drawFromCache(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
cacheValue: IGlyphCacheValue,
|
||||
x: number,
|
||||
y: number
|
||||
): void {
|
||||
// We don't actually need to do anything if this is whitespace.
|
||||
if (cacheValue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
const cacheX = this._toCoordinateX(cacheValue.index);
|
||||
const cacheY = this._toCoordinateY(cacheValue.index);
|
||||
ctx.drawImage(
|
||||
cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
|
||||
cacheX,
|
||||
cacheY,
|
||||
this._config.scaledCharWidth,
|
||||
this._config.scaledCharHeight,
|
||||
x,
|
||||
y,
|
||||
this._config.scaledCharWidth,
|
||||
this._config.scaledCharHeight
|
||||
);
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx < this._config.colors.ansi.length) {
|
||||
return this._config.colors.ansi[idx];
|
||||
}
|
||||
return DEFAULT_ANSI_COLORS[idx];
|
||||
}
|
||||
|
||||
private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
|
||||
if (this._config.allowTransparency) {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return TRANSPARENT_COLOR;
|
||||
} else if (glyph.bg === INVERTED_DEFAULT_COLOR) {
|
||||
return this._config.colors.foreground;
|
||||
} else if (glyph.bg < 256) {
|
||||
return this._getColorFromAnsiIndex(glyph.bg);
|
||||
}
|
||||
return this._config.colors.background;
|
||||
}
|
||||
|
||||
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
|
||||
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
|
||||
return opaque(this._config.colors.background);
|
||||
} else if (glyph.fg < 256) {
|
||||
// 256 color support
|
||||
return this._getColorFromAnsiIndex(glyph.fg);
|
||||
}
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
|
||||
// TODO: We do this (or something similar) in multiple places. We should split this off
|
||||
// into a shared function.
|
||||
private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
|
||||
this._drawToCacheCount++;
|
||||
|
||||
this._tmpCtx.save();
|
||||
|
||||
// draw the background
|
||||
const backgroundColor = this._getBackgroundColor(glyph);
|
||||
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
|
||||
// transparency in backgroundColor
|
||||
this._tmpCtx.globalCompositeOperation = 'copy';
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
|
||||
this._tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = glyph.italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'middle';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (glyph.dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
const imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
|
||||
);
|
||||
let isEmpty = false;
|
||||
if (!this._config.allowTransparency) {
|
||||
isEmpty = clearColor(imageData, backgroundColor);
|
||||
}
|
||||
|
||||
// copy the data from imageData to _cacheCanvas
|
||||
const x = this._toCoordinateX(index);
|
||||
const y = this._toCoordinateY(index);
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(imageData, x, y);
|
||||
|
||||
// Add the glyph and queue it to the bitmap (if the browser supports it)
|
||||
const cacheValue = {
|
||||
index,
|
||||
isEmpty,
|
||||
inBitmap: false
|
||||
};
|
||||
this._addGlyphToBitmap(cacheValue);
|
||||
|
||||
return cacheValue;
|
||||
}
|
||||
|
||||
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
|
||||
// Support is patchy for createImageBitmap at the moment, pass a canvas back
|
||||
// if support is lacking as drawImage works there too. Firefox is also
|
||||
// included here as ImageBitmap appears both buggy and has horrible
|
||||
// performance (tested on v55).
|
||||
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the glyph to the queue
|
||||
this._glyphsWaitingOnBitmap.push(cacheValue);
|
||||
|
||||
// Check if bitmap generation timeout already exists
|
||||
if (this._bitmapCommitTimeout !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
|
||||
}
|
||||
|
||||
private _generateBitmap(): void {
|
||||
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
|
||||
this._glyphsWaitingOnBitmap = [];
|
||||
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
|
||||
// Set bitmap
|
||||
this._bitmap = bitmap;
|
||||
|
||||
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
|
||||
// the bitmap was requested
|
||||
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
|
||||
const value = glyphsMovingToBitmap[i];
|
||||
// It doesn't matter if the value was already evicted, it will be
|
||||
// released from memory after this block if so.
|
||||
value.inBitmap = true;
|
||||
}
|
||||
});
|
||||
this._bitmapCommitTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
|
||||
// `new NoneCharAtlas`.
|
||||
export class NoneCharAtlas extends BaseCharAtlas {
|
||||
constructor(document: Document, config: ICharAtlasConfig) {
|
||||
super();
|
||||
}
|
||||
|
||||
public draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a partiicular rgb color in an ImageData completely transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, color: IColor): boolean {
|
||||
let isEmpty = true;
|
||||
const r = color.rgba >>> 24;
|
||||
const g = color.rgba >>> 16 & 0xFF;
|
||||
const b = color.rgba >>> 8 & 0xFF;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (imageData.data[offset] === r &&
|
||||
imageData.data[offset + 1] === g &&
|
||||
imageData.data[offset + 2] === b) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
return isEmpty;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface ILinkedListNode<T> {
|
||||
prev: ILinkedListNode<T> | null;
|
||||
next: ILinkedListNode<T> | null;
|
||||
key: number | null;
|
||||
value: T | null;
|
||||
}
|
||||
|
||||
export class LRUMap<T> {
|
||||
private _map: { [key: number]: ILinkedListNode<T> } = {};
|
||||
private _head: ILinkedListNode<T> | null = null;
|
||||
private _tail: ILinkedListNode<T> | null = null;
|
||||
private _nodePool: ILinkedListNode<T>[] = [];
|
||||
public size: number = 0;
|
||||
|
||||
constructor(public capacity: number) { }
|
||||
|
||||
private _unlinkNode(node: ILinkedListNode<T>): void {
|
||||
const prev = node.prev;
|
||||
const next = node.next;
|
||||
if (node === this._head) {
|
||||
this._head = next;
|
||||
}
|
||||
if (node === this._tail) {
|
||||
this._tail = prev;
|
||||
}
|
||||
if (prev !== null) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next !== null) {
|
||||
next.prev = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private _appendNode(node: ILinkedListNode<T>): void {
|
||||
const tail = this._tail;
|
||||
if (tail !== null) {
|
||||
tail.next = node;
|
||||
}
|
||||
node.prev = tail;
|
||||
node.next = null;
|
||||
this._tail = node;
|
||||
if (this._head === null) {
|
||||
this._head = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
|
||||
* they're more likely to live next to each other in memory, which seems to improve performance.
|
||||
*
|
||||
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
|
||||
* large maps.
|
||||
*/
|
||||
public prealloc(count: number): void {
|
||||
const nodePool = this._nodePool;
|
||||
for (let i = 0; i < count; i++) {
|
||||
nodePool.push({
|
||||
prev: null,
|
||||
next: null,
|
||||
key: null,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: number): T | null {
|
||||
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
|
||||
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
this._unlinkNode(node);
|
||||
this._appendNode(node);
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from a key without marking it as the most recently used item.
|
||||
*/
|
||||
public peekValue(key: number): T | null {
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
const head = this._head;
|
||||
return head === null ? null : head.value;
|
||||
}
|
||||
|
||||
public set(key: number, value: T): void {
|
||||
// This is unsafe: See note above.
|
||||
let node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
// already exists, we just need to mutate it and move it to the end of the list
|
||||
node = this._map[key];
|
||||
this._unlinkNode(node);
|
||||
node.value = value;
|
||||
} else if (this.size >= this.capacity) {
|
||||
// we're out of space: recycle the head node, move it to the tail
|
||||
node = this._head!;
|
||||
this._unlinkNode(node);
|
||||
delete this._map[node.key!];
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
this._map[key] = node;
|
||||
} else {
|
||||
// make a new element
|
||||
const nodePool = this._nodePool;
|
||||
if (nodePool.length > 0) {
|
||||
// use a preallocated node if we can
|
||||
node = nodePool.pop()!;
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
} else {
|
||||
node = {
|
||||
prev: null,
|
||||
next: null,
|
||||
key,
|
||||
value
|
||||
};
|
||||
}
|
||||
this._map[key] = node;
|
||||
this.size++;
|
||||
}
|
||||
this._appendNode(node);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'common/services/Services';
|
||||
import { IPartialColorSet } from 'browser/Types';
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
bold: boolean;
|
||||
dim: boolean;
|
||||
italic: boolean;
|
||||
}
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
devicePixelRatio: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
colors: IPartialColorSet;
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
|
||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { opaque } from 'browser/Color';
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||
const FG_CLASS_PREFIX = 'xterm-fg-';
|
||||
const BG_CLASS_PREFIX = 'xterm-bg-';
|
||||
const FOCUS_CLASS = 'xterm-focus';
|
||||
const SELECTION_CLASS = 'xterm-selection';
|
||||
|
||||
let nextTerminalId = 1;
|
||||
|
||||
/**
|
||||
* A fallback renderer for when canvas is slow. This is not meant to be
|
||||
* particularly fast or feature complete, more just stable and usable for when
|
||||
* canvas is not an option.
|
||||
*/
|
||||
export class DomRenderer extends Disposable implements IRenderer {
|
||||
private _rowFactory: DomRendererRowFactory;
|
||||
private _terminalClass: number = nextTerminalId++;
|
||||
|
||||
private _themeStyleElement!: HTMLStyleElement;
|
||||
private _dimensionsStyleElement!: HTMLStyleElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[] = [];
|
||||
private _selectionContainer: HTMLElement;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
|
||||
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
|
||||
|
||||
constructor(
|
||||
private _colors: IColorSet,
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _linkifier: ILinkifier,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
|
||||
this._rowContainer.style.lineHeight = 'normal';
|
||||
this._rowContainer.setAttribute('aria-hidden', 'true');
|
||||
this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
|
||||
this._selectionContainer = document.createElement('div');
|
||||
this._selectionContainer.classList.add(SELECTION_CLASS);
|
||||
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this._updateDimensions();
|
||||
this._injectCss();
|
||||
|
||||
this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
|
||||
|
||||
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
this._screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
this._linkifier.onLinkHover(e => this._onLinkHover(e));
|
||||
this._linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._screenElement.removeChild(this._rowContainer);
|
||||
this._screenElement.removeChild(this._selectionContainer);
|
||||
this._screenElement.removeChild(this._themeStyleElement);
|
||||
this._screenElement.removeChild(this._dimensionsStyleElement);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _updateDimensions(): void {
|
||||
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
|
||||
this.dimensions.scaledCharLeft = 0;
|
||||
this.dimensions.scaledCharTop = 0;
|
||||
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
|
||||
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
|
||||
this._rowElements.forEach(element => {
|
||||
element.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
element.style.height = `${this.dimensions.actualCellHeight}px`;
|
||||
element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
|
||||
// Make sure rows don't overflow onto following row
|
||||
element.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
if (!this._dimensionsStyleElement) {
|
||||
this._dimensionsStyleElement = document.createElement('style');
|
||||
this._screenElement.appendChild(this._dimensionsStyleElement);
|
||||
}
|
||||
|
||||
const styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
|
||||
` display: inline-block;` +
|
||||
` height: 100%;` +
|
||||
` vertical-align: top;` +
|
||||
` width: ${this.dimensions.actualCellWidth}px` +
|
||||
`}`;
|
||||
|
||||
this._dimensionsStyleElement.innerHTML = styles;
|
||||
|
||||
this._selectionContainer.style.height = this._viewportElement.style.height;
|
||||
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
this._injectCss();
|
||||
}
|
||||
|
||||
private _injectCss(): void {
|
||||
if (!this._themeStyleElement) {
|
||||
this._themeStyleElement = document.createElement('style');
|
||||
this._screenElement.appendChild(this._themeStyleElement);
|
||||
}
|
||||
|
||||
// Base CSS
|
||||
let styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
|
||||
` color: ${this._colors.foreground.css};` +
|
||||
` background-color: ${this._colors.background.css};` +
|
||||
` font-family: ${this._optionsService.options.fontFamily};` +
|
||||
` font-size: ${this._optionsService.options.fontSize}px;` +
|
||||
`}`;
|
||||
// Text styles
|
||||
styles +=
|
||||
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
|
||||
` font-weight: ${this._optionsService.options.fontWeight};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${BOLD_CLASS} {` +
|
||||
` font-weight: ${this._optionsService.options.fontWeightBold};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
|
||||
` font-style: italic;` +
|
||||
`}`;
|
||||
// Blink animation
|
||||
styles +=
|
||||
`@keyframes blink_box_shadow {` +
|
||||
` 50% {` +
|
||||
` box-shadow: none;` +
|
||||
` }` +
|
||||
`}`;
|
||||
styles +=
|
||||
`@keyframes blink_block {` +
|
||||
` 0% {` +
|
||||
` background-color: ${this._colors.cursor.css};` +
|
||||
` color: ${this._colors.cursorAccent.css};` +
|
||||
` }` +
|
||||
` 50% {` +
|
||||
` background-color: ${this._colors.cursorAccent.css};` +
|
||||
` color: ${this._colors.cursor.css};` +
|
||||
` }` +
|
||||
`}`;
|
||||
// Cursor
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` outline: 1px solid ${this._colors.cursor.css};` +
|
||||
` outline-offset: -1px;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
|
||||
` animation: blink_box_shadow 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` animation: blink_block 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` background-color: ${this._colors.cursor.css};` +
|
||||
` color: ${this._colors.cursorAccent.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
|
||||
`}`;
|
||||
// Selection
|
||||
styles +=
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} {` +
|
||||
` position: absolute;` +
|
||||
` top: 0;` +
|
||||
` left: 0;` +
|
||||
` z-index: 1;` +
|
||||
` pointer-events: none;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${this._colors.selection.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
this._colors.ansi.forEach((c, i) => {
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
});
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${opaque(this._colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
|
||||
|
||||
this._themeStyleElement.innerHTML = styles;
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
private _refreshRowElements(cols: number, rows: number): void {
|
||||
// Add missing elements
|
||||
for (let i = this._rowElements.length; i <= rows; i++) {
|
||||
const row = document.createElement('div');
|
||||
this._rowContainer.appendChild(row);
|
||||
this._rowElements.push(row);
|
||||
}
|
||||
// Remove excess elements
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop()!);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
this._refreshRowElements(cols, rows);
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._rowContainer.classList.remove(FOCUS_CLASS);
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._rowContainer.classList.add(FOCUS_CLASS);
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
// Remove all selections
|
||||
while (this._selectionContainer.children.length) {
|
||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the selections
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
|
||||
if (columnSelectMode) {
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||
}
|
||||
}
|
||||
this._selectionContainer.appendChild(documentFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a selection element at the specified position.
|
||||
* @param row The row of the selection.
|
||||
* @param colStart The start column.
|
||||
* @param colEnd The end columns.
|
||||
*/
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
|
||||
element.style.top = `${row * this.dimensions.actualCellHeight}px`;
|
||||
element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
|
||||
element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
|
||||
return element;
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
// No-op, the cursor is drawn when rows are drawn
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
// Force a refresh
|
||||
this._updateDimensions();
|
||||
this._injectCss();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._rowElements.forEach(e => e.innerHTML = '');
|
||||
}
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
|
||||
const cursorX = this._bufferService.buffer.x;
|
||||
const cursorBlink = this._optionsService.options.cursorBlink;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const rowElement = this._rowElements[y];
|
||||
rowElement.innerHTML = '';
|
||||
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const lineData = this._bufferService.buffer.lines.get(row);
|
||||
const cursorStyle = this._optionsService.options.cursorStyle;
|
||||
rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
|
||||
}
|
||||
}
|
||||
|
||||
private get _terminalSelector(): string {
|
||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
|
||||
}
|
||||
|
||||
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
|
||||
while (x !== x2 || y !== y2) {
|
||||
const row = this._rowElements[y];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const span = <HTMLElement>row.children[x];
|
||||
if (span) {
|
||||
span.style.textDecoration = enabled ? 'underline' : 'none';
|
||||
}
|
||||
if (++x >= cols) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { ensureContrastRatio, rgbaToColor } from 'browser/Color';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
export const ITALIC_CLASS = 'xterm-italic';
|
||||
export const UNDERLINE_CLASS = 'xterm-underline';
|
||||
export const CURSOR_CLASS = 'xterm-cursor';
|
||||
export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
|
||||
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
|
||||
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
|
||||
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||
|
||||
export class DomRendererRowFactory {
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private _colors: IColorSet
|
||||
) {
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
// Find the line length first, this prevents the need to output a bunch of
|
||||
// empty cells at the end. This cannot easily be integrated into the main
|
||||
// loop below because of the colCount feature (which can be removed after we
|
||||
// properly support reflow and disallow data to go beyond the right-side of
|
||||
// the viewport).
|
||||
let lineLength = 0;
|
||||
for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) {
|
||||
if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
|
||||
lineLength = x + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
lineData.loadCell(x, this._workCell);
|
||||
const width = this._workCell.getWidth();
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by the char at x-1
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const charElement = this._document.createElement('span');
|
||||
if (width > 1) {
|
||||
charElement.style.width = `${cellWidth * width}px`;
|
||||
}
|
||||
|
||||
if (isCursorRow && x === cursorX) {
|
||||
charElement.classList.add(CURSOR_CLASS);
|
||||
|
||||
if (cursorBlink) {
|
||||
charElement.classList.add(CURSOR_BLINK_CLASS);
|
||||
}
|
||||
|
||||
switch (cursorStyle) {
|
||||
case 'bar':
|
||||
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
|
||||
break;
|
||||
case 'underline':
|
||||
charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
|
||||
break;
|
||||
default:
|
||||
charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._workCell.isBold()) {
|
||||
charElement.classList.add(BOLD_CLASS);
|
||||
}
|
||||
|
||||
if (this._workCell.isItalic()) {
|
||||
charElement.classList.add(ITALIC_CLASS);
|
||||
}
|
||||
|
||||
if (this._workCell.isDim()) {
|
||||
charElement.classList.add(DIM_CLASS);
|
||||
}
|
||||
|
||||
if (this._workCell.isUnderline()) {
|
||||
charElement.classList.add(UNDERLINE_CLASS);
|
||||
}
|
||||
|
||||
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
|
||||
let fg = this._workCell.getFgColor();
|
||||
let fgColorMode = this._workCell.getFgColorMode();
|
||||
let bg = this._workCell.getBgColor();
|
||||
let bgColorMode = this._workCell.getBgColorMode();
|
||||
const isInverse = !!this._workCell.isInverse();
|
||||
if (isInverse) {
|
||||
const temp = fg;
|
||||
fg = bg;
|
||||
bg = temp;
|
||||
const temp2 = fgColorMode;
|
||||
fgColorMode = bgColorMode;
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
// Foreground
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
|
||||
fg += 8;
|
||||
}
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const color = rgbaToColor(
|
||||
(fg >> 16) & 0xFF,
|
||||
(fg >> 8) & 0xFF,
|
||||
(fg ) & 0xFF
|
||||
);
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, color)) {
|
||||
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) {
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
|
||||
if (this._optionsService.options.minimumContrastRatio === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg);
|
||||
|
||||
// Calculate and store in cache
|
||||
if (adjustedColor === undefined) {
|
||||
adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
|
||||
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
|
||||
}
|
||||
|
||||
if (adjustedColor) {
|
||||
this._addStyle(element, `color:${adjustedColor.css}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _addStyle(element: HTMLElement, style: string): void {
|
||||
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
|
||||
}
|
||||
}
|
||||
|
||||
function padStart(text: string, padChar: string, length: number): string {
|
||||
while (text.length < length) {
|
||||
text = padChar + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* Represents a selection within the buffer. This model only cares about column
|
||||
* and row coordinates, not wide characters.
|
||||
*/
|
||||
export class SelectionModel {
|
||||
/**
|
||||
* Whether select all is currently active.
|
||||
*/
|
||||
public isSelectAllActive: boolean = false;
|
||||
|
||||
/**
|
||||
* The minimal length of the selection from the start position. When double
|
||||
* clicking on a word, the word will be selected which makes the selection
|
||||
* start at the start of the word and makes this variable the length.
|
||||
*/
|
||||
public selectionStartLength: number = 0;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection starts at.
|
||||
*/
|
||||
public selectionStart: [number, number] | undefined;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection ends at.
|
||||
*/
|
||||
public selectionEnd: [number, number] | undefined;
|
||||
|
||||
constructor(
|
||||
private _bufferService: IBufferService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current selection.
|
||||
*/
|
||||
public clearSelection(): void {
|
||||
this.selectionStart = undefined;
|
||||
this.selectionEnd = undefined;
|
||||
this.isSelectAllActive = false;
|
||||
this.selectionStartLength = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection start, taking into consideration select all.
|
||||
*/
|
||||
public get finalSelectionStart(): [number, number] | undefined {
|
||||
if (this.isSelectAllActive) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
if (!this.selectionEnd || !this.selectionStart) {
|
||||
return this.selectionStart;
|
||||
}
|
||||
|
||||
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection end, taking into consideration select all, double click
|
||||
* word selection and triple click line selection.
|
||||
*/
|
||||
public get finalSelectionEnd(): [number, number] | undefined {
|
||||
if (this.isSelectAllActive) {
|
||||
return [this._bufferService.cols, this._bufferService.buffer.ybase + this._bufferService.rows - 1];
|
||||
}
|
||||
|
||||
if (!this.selectionStart) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Use the selection start + length if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||
if (startPlusLength > this._bufferService.cols) {
|
||||
return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)];
|
||||
}
|
||||
return [startPlusLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
// Ensure the the word/line is selected after a double/triple click
|
||||
if (this.selectionStartLength) {
|
||||
// Select the larger of the two when start and end are on the same line
|
||||
if (this.selectionEnd[1] === this.selectionStart[1]) {
|
||||
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
|
||||
}
|
||||
}
|
||||
return this.selectionEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the selection start and end are reversed.
|
||||
*/
|
||||
public areSelectionValuesReversed(): boolean {
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
* @return Whether a refresh is necessary.
|
||||
*/
|
||||
public onTrim(amount: number): boolean {
|
||||
// Adjust the selection position based on the trimmed amount.
|
||||
if (this.selectionStart) {
|
||||
this.selectionStart[1] -= amount;
|
||||
}
|
||||
if (this.selectionEnd) {
|
||||
this.selectionEnd[1] -= amount;
|
||||
}
|
||||
|
||||
// The selection has moved off the buffer, clear it.
|
||||
if (this.selectionEnd && this.selectionEnd[1] < 0) {
|
||||
this.clearSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the selection start is trimmed, ensure the start column is 0.
|
||||
if (this.selectionStart && this.selectionStart[1] < 0) {
|
||||
this.selectionStart[1] = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface ISelectionRedrawRequestEvent {
|
||||
start: [number, number] | undefined;
|
||||
end: [number, number] | undefined;
|
||||
columnSelectMode: boolean;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
|
||||
export class CharSizeService implements ICharSizeService {
|
||||
serviceBrand: any;
|
||||
|
||||
public width: number = 0;
|
||||
public height: number = 0;
|
||||
private _measureStrategy: IMeasureStrategy;
|
||||
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
|
||||
private _onCharSizeChange = new EventEmitter<void>();
|
||||
public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; }
|
||||
|
||||
constructor(
|
||||
readonly document: Document,
|
||||
readonly parentElement: HTMLElement,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
|
||||
}
|
||||
|
||||
public measure(): void {
|
||||
const result = this._measureStrategy.measure();
|
||||
if (result.width !== this.width || result.height !== this.height) {
|
||||
this.width = result.width;
|
||||
this.height = result.height;
|
||||
this._onCharSizeChange.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IMeasureStrategy {
|
||||
measure(): IReadonlyMeasureResult;
|
||||
}
|
||||
|
||||
interface IReadonlyMeasureResult {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
interface IMeasureResult {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText
|
||||
class DomMeasureStrategy implements IMeasureStrategy {
|
||||
private _result: IMeasureResult = { width: 0, height: 0 };
|
||||
private _measureElement: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private _document: Document,
|
||||
private _parentElement: HTMLElement,
|
||||
private _optionsService: IOptionsService
|
||||
) {
|
||||
this._measureElement = this._document.createElement('span');
|
||||
this._measureElement.classList.add('xterm-char-measure-element');
|
||||
this._measureElement.textContent = 'W';
|
||||
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||
this._parentElement.appendChild(this._measureElement);
|
||||
}
|
||||
|
||||
public measure(): IReadonlyMeasureResult {
|
||||
this._measureElement.style.fontFamily = this._optionsService.options.fontFamily;
|
||||
this._measureElement.style.fontSize = `${this._optionsService.options.fontSize}px`;
|
||||
|
||||
// Note that this triggers a synchronous layout
|
||||
const geometry = this._measureElement.getBoundingClientRect();
|
||||
|
||||
// If values are 0 then the element is likely currently display:none, in which case we should
|
||||
// retain the previous value.
|
||||
if (geometry.width !== 0 && geometry.height !== 0) {
|
||||
this._result.width = geometry.width;
|
||||
this._result.height = Math.ceil(geometry.height);
|
||||
}
|
||||
|
||||
return this._result;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user