8 changed files with 841 additions and 5 deletions
@ -0,0 +1,152 @@ |
|||
<template> |
|||
<el-tabs v-model="activeName" style="margin-top: 8px"> |
|||
<el-tab-pane label="图谱" name="1"> |
|||
<treeNodeg6 ref="treeGraph" :mockData="mockData" /> |
|||
</el-tab-pane> |
|||
<el-tab-pane label="表格" name="2"> |
|||
<el-table |
|||
v-loading="loading" |
|||
:data="tableData" |
|||
@selection-change="handleSelectionChange" |
|||
border |
|||
style="width: 100%" |
|||
> |
|||
|
|||
<el-table-column label="代码编号" align="center" prop="codeNum" /> |
|||
<el-table-column label="代码名称" align="center" prop="codeName" /> |
|||
<el-table-column label="引用标准编号" align="center" prop="stdNum" /> |
|||
<el-table-column label="引用标准英文名" align="center" prop="stdCode" /> |
|||
<el-table-column label="引用标准中文名" align="center" prop="stdName" /> |
|||
<el-table-column label="引用字典编号" align="center" prop="dictNum" /> |
|||
<el-table-column label="引用字典英文名" align="center" prop="dictCode" /> |
|||
<el-table-column label="引用字典中文名" align="center" prop="dictName" /> |
|||
</el-table> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted, nextTick } from "vue"; |
|||
import { getStdCodeMap } from "@/api/datastd/std"; |
|||
import treeNodeg6 from "./treeNodeg6.vue"; |
|||
|
|||
const activeName = ref("1"); // 默认打开图谱 |
|||
|
|||
const treeGraph = ref(null); |
|||
const tableData = ref([]); |
|||
const selections = ref([]); |
|||
const loading = ref(false); |
|||
const props = defineProps({ |
|||
rowData: { |
|||
type: Object, |
|||
required: false |
|||
} |
|||
|
|||
}); |
|||
// mocked data |
|||
const mockData = ref({ |
|||
id: "g1", |
|||
name: props.rowData.codeName, |
|||
count: 123456, |
|||
label: props.rowData.codeNum, |
|||
rate: 1.0, |
|||
status: "G", |
|||
variableValue: "代码", |
|||
variableUp: true, |
|||
children:[] |
|||
// children: [ |
|||
// { |
|||
// id: "g12", |
|||
// name: "发行机构类型", |
|||
// count: 123456, |
|||
// label: "iss_ins_type", |
|||
// rate: 1.0, |
|||
// status: "R", |
|||
// currency: "xx001", |
|||
// variableValue: "标准", |
|||
// variableUp: true, |
|||
// children: [ |
|||
// { |
|||
// id: "g121", |
|||
// name: "发行机构类型", |
|||
// collapsed: true, |
|||
// count: 123456, |
|||
// label: "iss_ins_type", |
|||
// rate: 1.0, |
|||
// status: "B", |
|||
// currency: "xx002", |
|||
// variableValue: "词典", |
|||
// variableUp: true, |
|||
// }, |
|||
// ], |
|||
// }, |
|||
// { |
|||
// id: "g13", |
|||
// name: "发行人编号", |
|||
// label: "issr_no", |
|||
// rate: 1, |
|||
// status: "R", |
|||
// currency: "xx002", |
|||
// variableValue: "标准", |
|||
// variableUp: true, |
|||
// }, |
|||
// ], |
|||
}); |
|||
|
|||
// 获取数据列表 |
|||
const getList = async () => { |
|||
loading.value = true; |
|||
try { |
|||
const response = await listStdCodemap(); |
|||
tableData.value = response.rows || []; |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
}; |
|||
|
|||
const handleSelectionChange = (selection) => { |
|||
selections.value = selection; |
|||
}; |
|||
|
|||
// 默认加载图谱 |
|||
onMounted(() => { |
|||
nextTick(() => { |
|||
|
|||
getStdCodeMap(props.rowData.id).then(response => { |
|||
mockData.value.children = response.data.children; |
|||
tableData.value = response.data.tableData; |
|||
if (treeGraph.value) { |
|||
treeGraph.value.refreshGraph(); |
|||
} |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.app-container { |
|||
padding: 20px; |
|||
} |
|||
|
|||
.query-form-container { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.el-table .el-input { |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-container { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.info-text { |
|||
font-size: 18px; |
|||
line-height: 1.6; |
|||
color: #333; |
|||
font-weight: 500; |
|||
display: inline-block; |
|||
width: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,564 @@ |
|||
<template> |
|||
<div> |
|||
<div ref="container" id="container" class="graph-container"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { onMounted, ref } from 'vue'; |
|||
import G6 from '@antv/g6'; |
|||
import insertCss from 'insert-css'; |
|||
|
|||
insertCss(` |
|||
.g6-component-tooltip { |
|||
background-color: rgba(0,0,0, 0.65); |
|||
padding: 10px; |
|||
box-shadow: rgb(174, 174, 174) 0px 0px 10px; |
|||
width: fit-content; |
|||
color: #fff; |
|||
border-radius = 4px; |
|||
} |
|||
`); |
|||
|
|||
// mocked data |
|||
// const mockData = { |
|||
// id: 'g1', |
|||
// name: '发型机构类型代码', |
|||
// count: 123456, |
|||
// label: 'SYS049', |
|||
// // currency: '代码', |
|||
// rate: 1.0, |
|||
// status: 'G', |
|||
// // variableName: 'V1', |
|||
// variableValue:'代码', |
|||
// variableUp: true, |
|||
// children: [ |
|||
// { |
|||
// id: 'g12', |
|||
// name: '发行机构类型', |
|||
// count: 123456, |
|||
// label: 'iss_ins_type', |
|||
// rate: 1.0, |
|||
// status: 'R', |
|||
// currency: 'xx001', |
|||
// // variableName: 'V2', |
|||
// variableValue:'标准', |
|||
// variableUp: true, |
|||
// children: [ |
|||
// { |
|||
// id: 'g121', |
|||
// name: '发行机构类型', |
|||
// collapsed: true, |
|||
// count: 123456, |
|||
// label: 'iss_ins_type', |
|||
// rate: 1.0, |
|||
// status: 'B', |
|||
// currency: 'xx002', |
|||
// variableValue:'词典', |
|||
// variableUp: true, |
|||
|
|||
// }, |
|||
|
|||
// ], |
|||
// }, |
|||
// { |
|||
// id: 'g13', |
|||
// name: '发行人编号', |
|||
// label: 'issr_no', |
|||
// rate: 1, |
|||
// status: 'R', |
|||
// currency: 'xx002', |
|||
// // variableName: '标准', |
|||
// variableValue:'标准', |
|||
// variableUp: true, |
|||
|
|||
// } |
|||
|
|||
// ], |
|||
// }; |
|||
|
|||
const colors = { |
|||
B: '#5B8FF9', |
|||
R: '#F46649', |
|||
Y: '#EEBC20', |
|||
G: '#5BD8A6', |
|||
DI: '#A7A7A7', |
|||
}; |
|||
|
|||
|
|||
|
|||
export default { |
|||
name: 'G6TreeGraph', |
|||
props: { |
|||
// 这里声明传入的数据 prop,名称保持 mockData |
|||
mockData: { |
|||
type: Object, |
|||
required: true, |
|||
}, |
|||
}, |
|||
setup(props) { |
|||
const container = ref(null); |
|||
let graph = null; |
|||
// // 组件props配置 |
|||
const propsConfig = { |
|||
data: props.mockData, |
|||
config: { |
|||
padding: [20, 50], |
|||
defaultLevel: 3, |
|||
defaultZoom: 0.8, |
|||
modes: { default: ['zoom-canvas', 'drag-canvas'] }, |
|||
}, |
|||
}; |
|||
console.log(props.mockData,"mockDatamockDatamockData") |
|||
// 自定义节点和边的注册函数 |
|||
const registerFn = () => { |
|||
/** 自定义节点 **/ |
|||
G6.registerNode( |
|||
'flow-rect', |
|||
{ |
|||
shapeType: 'flow-rect', |
|||
draw(cfg, group) { |
|||
const { |
|||
name = '', |
|||
variableName, |
|||
variableValue, |
|||
variableUp, |
|||
label, |
|||
collapsed, |
|||
currency, |
|||
status, |
|||
rate, |
|||
} = cfg; |
|||
|
|||
const grey = '#CED4D9'; |
|||
const rectConfig = { |
|||
width: 202, |
|||
height: 60, |
|||
lineWidth: 1, |
|||
fontSize: 12, |
|||
fill: '#fff', |
|||
radius: 4, |
|||
stroke: grey, |
|||
opacity: 1, |
|||
}; |
|||
|
|||
const nodeOrigin = { |
|||
x: -rectConfig.width / 2, |
|||
y: -rectConfig.height / 2, |
|||
}; |
|||
|
|||
const textConfig = { |
|||
textAlign: 'left', |
|||
textBaseline: 'bottom', |
|||
}; |
|||
|
|||
const rect = group.addShape('rect', { |
|||
attrs: { |
|||
x: nodeOrigin.x, |
|||
y: nodeOrigin.y, |
|||
...rectConfig, |
|||
}, |
|||
}); |
|||
|
|||
const rectBBox = rect.getBBox(); |
|||
|
|||
// label title |
|||
group.addShape('text', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: 12 + nodeOrigin.x, |
|||
y: 20 + nodeOrigin.y, |
|||
text: name.length > 28 ? name.substr(0, 28) + '...' : name, |
|||
fontSize: 12, |
|||
opacity: 0.85, |
|||
fill: '#000', |
|||
cursor: 'pointer', |
|||
}, |
|||
name: 'name-shape', |
|||
}); |
|||
|
|||
// price |
|||
const price = group.addShape('text', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: 12 + nodeOrigin.x, |
|||
y: rectBBox.maxY - 12, |
|||
text: label, |
|||
fontSize: 16, |
|||
fill: '#000', |
|||
opacity: 0.85, |
|||
}, |
|||
}); |
|||
|
|||
// label currency |
|||
group.addShape('text', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: price.getBBox().maxX + 5, |
|||
y: rectBBox.maxY - 12, |
|||
text: currency, |
|||
fontSize: 12, |
|||
fill: '#000', |
|||
opacity: 0.75, |
|||
}, |
|||
}); |
|||
|
|||
// percentage |
|||
const percentText = group.addShape('text', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: rectBBox.maxX - 8, |
|||
y: rectBBox.maxY - 12, |
|||
// text: `${((variableValue || 0) * 100).toFixed(2)}%`, |
|||
text: variableValue, |
|||
fontSize: 12, |
|||
textAlign: 'right', |
|||
fill: colors[status], |
|||
}, |
|||
}); |
|||
|
|||
// percentage triangle |
|||
const symbol = variableUp ? 'triangle' : 'triangle-down'; |
|||
const triangle = group.addShape('marker', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: percentText.getBBox().minX - 10, |
|||
y: rectBBox.maxY - 12 - 6, |
|||
symbol, |
|||
r: 6, |
|||
fill: colors[status], |
|||
}, |
|||
}); |
|||
|
|||
// variable name |
|||
group.addShape('text', { |
|||
attrs: { |
|||
...textConfig, |
|||
x: triangle.getBBox().minX - 4, |
|||
y: rectBBox.maxY - 12, |
|||
text: variableName, |
|||
fontSize: 12, |
|||
textAlign: 'right', |
|||
fill: '#000', |
|||
opacity: 0.45, |
|||
}, |
|||
}); |
|||
|
|||
// bottom line background |
|||
group.addShape('rect', { |
|||
attrs: { |
|||
x: nodeOrigin.x, |
|||
y: rectBBox.maxY - 4, |
|||
width: rectConfig.width, |
|||
height: 4, |
|||
radius: [0, 0, rectConfig.radius, rectConfig.radius], |
|||
fill: '#E0DFE3', |
|||
}, |
|||
}); |
|||
|
|||
// bottom percent |
|||
group.addShape('rect', { |
|||
attrs: { |
|||
x: nodeOrigin.x, |
|||
y: rectBBox.maxY - 4, |
|||
width: rate * rectBBox.width, |
|||
height: 4, |
|||
radius: [0, 0, 0, rectConfig.radius], |
|||
fill: colors[status], |
|||
}, |
|||
}); |
|||
|
|||
// collapse rect |
|||
if (cfg.children && cfg.children.length) { |
|||
group.addShape('rect', { |
|||
attrs: { |
|||
x: rectConfig.width / 2 - 8, |
|||
y: -8, |
|||
width: 16, |
|||
height: 16, |
|||
stroke: 'rgba(0, 0, 0, 0.25)', |
|||
cursor: 'pointer', |
|||
fill: '#fff', |
|||
}, |
|||
name: 'collapse-back', |
|||
modelId: cfg.id, |
|||
}); |
|||
|
|||
// collapse text |
|||
group.addShape('text', { |
|||
attrs: { |
|||
x: rectConfig.width / 2, |
|||
y: -1, |
|||
textAlign: 'center', |
|||
textBaseline: 'middle', |
|||
text: collapsed ? '+' : '-', |
|||
fontSize: 16, |
|||
cursor: 'pointer', |
|||
fill: 'rgba(0, 0, 0, 0.25)', |
|||
}, |
|||
name: 'collapse-text', |
|||
modelId: cfg.id, |
|||
}); |
|||
} |
|||
|
|||
this.drawLinkPoints(cfg, group); |
|||
return rect; |
|||
}, |
|||
update(cfg, item) { |
|||
const { level, status, name } = cfg; |
|||
const group = item.getContainer(); |
|||
let mask = group.find((ele) => ele.get('name') === 'mask-shape'); |
|||
let maskLabel = group.find((ele) => ele.get('name') === 'mask-label-shape'); |
|||
if (level === 0) { |
|||
group.get('children').forEach((child) => { |
|||
if (child.get('name')?.includes('collapse')) return; |
|||
child.hide(); |
|||
}); |
|||
if (!mask) { |
|||
mask = group.addShape('rect', { |
|||
attrs: { |
|||
x: -101, |
|||
y: -30, |
|||
width: 202, |
|||
height: 60, |
|||
opacity: 0, |
|||
fill: colors[status], |
|||
}, |
|||
name: 'mask-shape', |
|||
}); |
|||
maskLabel = group.addShape('text', { |
|||
attrs: { |
|||
fill: '#fff', |
|||
fontSize: 20, |
|||
x: 0, |
|||
y: 10, |
|||
text: name.length > 28 ? name.substr(0, 16) + '...' : name, |
|||
textAlign: 'center', |
|||
opacity: 0, |
|||
}, |
|||
name: 'mask-label-shape', |
|||
}); |
|||
const collapseRect = group.find((ele) => ele.get('name') === 'collapse-back'); |
|||
const collapseText = group.find((ele) => ele.get('name') === 'collapse-text'); |
|||
collapseRect?.toFront(); |
|||
collapseText?.toFront(); |
|||
} else { |
|||
mask.show(); |
|||
maskLabel.show(); |
|||
} |
|||
mask.animate({ opacity: 1 }, 200); |
|||
maskLabel.animate({ opacity: 1 }, 200); |
|||
return mask; |
|||
} else { |
|||
group.get('children').forEach((child) => { |
|||
if (child.get('name')?.includes('collapse')) return; |
|||
child.show(); |
|||
}); |
|||
mask?.animate({ opacity: 0 }, { |
|||
duration: 200, |
|||
callback: () => mask.hide(), |
|||
}); |
|||
maskLabel?.animate({ opacity: 0 }, { |
|||
duration: 200, |
|||
callback: () => maskLabel.hide(), |
|||
}); |
|||
} |
|||
this.updateLinkPoints(cfg, group); |
|||
}, |
|||
setState(name, value, item) { |
|||
if (name === 'collapse') { |
|||
const group = item.getContainer(); |
|||
const collapseText = group.find((e) => e.get('name') === 'collapse-text'); |
|||
if (collapseText) { |
|||
if (!value) { |
|||
collapseText.attr({ text: '-' }); |
|||
} else { |
|||
collapseText.attr({ text: '+' }); |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
getAnchorPoints() { |
|||
return [ |
|||
[0, 0.5], |
|||
[1, 0.5], |
|||
]; |
|||
}, |
|||
}, |
|||
'rect' |
|||
); |
|||
|
|||
G6.registerEdge( |
|||
'flow-cubic', |
|||
{ |
|||
getControlPoints(cfg) { |
|||
let controlPoints = cfg.controlPoints; |
|||
if (!controlPoints || !controlPoints.length) { |
|||
const { startPoint, endPoint, sourceNode, targetNode } = cfg; |
|||
const { x: startX, y: startY, coefficientX, coefficientY } = sourceNode |
|||
? sourceNode.getModel() |
|||
: startPoint; |
|||
const { x: endX, y: endY } = targetNode ? targetNode.getModel() : endPoint; |
|||
let curveStart = (endX - startX) * coefficientX; |
|||
let curveEnd = (endY - startY) * coefficientY; |
|||
curveStart = curveStart > 40 ? 40 : curveStart; |
|||
curveEnd = curveEnd < -30 ? curveEnd : -30; |
|||
controlPoints = [ |
|||
{ x: startPoint.x + curveStart, y: startPoint.y }, |
|||
{ x: endPoint.x + curveEnd, y: endPoint.y }, |
|||
]; |
|||
} |
|||
return controlPoints; |
|||
}, |
|||
getPath(points) { |
|||
const path = []; |
|||
path.push(['M', points[0].x, points[0].y]); |
|||
path.push([ |
|||
'C', |
|||
points[1].x, |
|||
points[1].y, |
|||
points[2].x, |
|||
points[2].y, |
|||
points[3].x, |
|||
points[3].y, |
|||
]); |
|||
return path; |
|||
}, |
|||
}, |
|||
'single-line' |
|||
); |
|||
}; |
|||
|
|||
// 初始化图表 |
|||
const initGraph = (data) => { |
|||
if (!data) { |
|||
return; |
|||
} |
|||
const { onInit, config } = propsConfig; |
|||
const tooltip = new G6.Tooltip({ |
|||
offsetX: 20, |
|||
offsetY: 30, |
|||
itemTypes: ['node'], |
|||
getContent: (e) => { |
|||
const outDiv = document.createElement('div'); |
|||
const nodeName = e.item.getModel().name; |
|||
let formatedNodeName = ''; |
|||
for (let i = 0; i < nodeName.length; i++) { |
|||
formatedNodeName = `${formatedNodeName}${nodeName[i]}`; |
|||
if (i !== 0 && i % 20 === 0) |
|||
formatedNodeName = `${formatedNodeName}<br/>`; |
|||
} |
|||
outDiv.innerHTML = `${formatedNodeName}`; |
|||
return outDiv; |
|||
}, |
|||
shouldBegin: (e) => { |
|||
if ( |
|||
e.target.get('name') === 'name-shape' || |
|||
e.target.get('name') === 'mask-label-shape' |
|||
) |
|||
return true; |
|||
return false; |
|||
}, |
|||
}); |
|||
graph = new G6.TreeGraph({ |
|||
container: container.value, |
|||
...defaultConfig(), |
|||
...config, |
|||
plugins: [tooltip], |
|||
}); |
|||
if (typeof onInit === 'function') { |
|||
onInit(graph); |
|||
} |
|||
graph.data(data); |
|||
graph.render(); |
|||
|
|||
const handleCollapse = (e) => { |
|||
const target = e.target; |
|||
const id = target.get('modelId'); |
|||
const item = graph.findById(id); |
|||
const nodeModel = item.getModel(); |
|||
nodeModel.collapsed = !nodeModel.collapsed; |
|||
graph.layout(); |
|||
graph.setItemState(item, 'collapse', nodeModel.collapsed); |
|||
}; |
|||
graph.on('collapse-text:click', (e) => { |
|||
handleCollapse(e); |
|||
}); |
|||
graph.on('collapse-back:click', (e) => { |
|||
handleCollapse(e); |
|||
}); |
|||
|
|||
let currentLevel = 1; |
|||
const briefZoomThreshold = Math.max(graph.getZoom(), 0.5); |
|||
graph.on('viewportchange', (e) => { |
|||
if (e.action !== 'zoom') return; |
|||
const currentZoom = graph.getZoom(); |
|||
let toLevel = currentLevel; |
|||
if (currentZoom < briefZoomThreshold) { |
|||
toLevel = 0; |
|||
} else { |
|||
toLevel = 1; |
|||
} |
|||
if (toLevel !== currentLevel) { |
|||
currentLevel = toLevel; |
|||
graph.getNodes().forEach((node) => { |
|||
graph.updateItem(node, { level: toLevel }); |
|||
}); |
|||
} |
|||
}); |
|||
}; |
|||
// 默认配置,根据 container 的宽高动态生成 |
|||
const defaultConfig = () => { |
|||
const width = container.value.scrollWidth; |
|||
const height = container.value.scrollHeight || 500; |
|||
return { |
|||
width, |
|||
height, |
|||
modes: { |
|||
default: ['zoom-canvas', 'drag-canvas'], |
|||
}, |
|||
fitView: true, |
|||
animate: true, |
|||
defaultNode: { |
|||
type: 'flow-rect', |
|||
}, |
|||
defaultEdge: { |
|||
type: 'cubic-horizontal', |
|||
style: { |
|||
stroke: '#CED4D9', |
|||
}, |
|||
}, |
|||
layout: { |
|||
type: 'indented', |
|||
direction: 'LR', |
|||
dropCap: false, |
|||
indent: 300, |
|||
getHeight: () => 60, |
|||
}, |
|||
}; |
|||
}; |
|||
const refreshGraph = () => { |
|||
nextTick(() => { |
|||
registerFn(); |
|||
initGraph(propsConfig.data); |
|||
window.onresize = () => { |
|||
if (!graph || graph.get('destroyed')) return; |
|||
if (!container.value || !container.value.scrollWidth || !container.value.scrollHeight) |
|||
return; |
|||
graph.changeSize(container.value.scrollWidth, container.value.scrollHeight); |
|||
|
|||
} |
|||
}); |
|||
}; |
|||
return { container ,refreshGraph }; |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.graph-container { |
|||
width: 100%; |
|||
height: 600px; |
|||
/* border: 1px solid #ddd; */ |
|||
} |
|||
</style> |
Loading…
Reference in new issue