|
@@ -0,0 +1,638 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="graph-container r-20">
|
|
|
|
|
+ <view class="canvas-wrapper r-20">
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ canvas-id="graphCanvas"
|
|
|
|
|
+ class="graph-canvas r-20"
|
|
|
|
|
+ @touchstart="onTouchStart"
|
|
|
|
|
+ @touchmove="onTouchMove"
|
|
|
|
|
+ @touchend="onTouchEnd"
|
|
|
|
|
+ ></canvas>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 控制面板 -->
|
|
|
|
|
+<!-- <view class="control-panel">-->
|
|
|
|
|
+<!-- <view class="control-group">-->
|
|
|
|
|
+<!-- <text class="control-label">布局算法:</text>-->
|
|
|
|
|
+<!-- <picker @change="onLayoutChange" :value="layoutIndex" :range="layoutOptions">-->
|
|
|
|
|
+<!-- <view class="picker">{{ layoutOptions[layoutIndex] }}</view>-->
|
|
|
|
|
+<!-- </picker>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+
|
|
|
|
|
+<!-- <view class="control-group">-->
|
|
|
|
|
+<!-- <text class="control-label">显示标签:</text>-->
|
|
|
|
|
+<!-- <switch :checked="showLabels" @change="toggleLabels" />-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+
|
|
|
|
|
+<!-- <view class="button-group">-->
|
|
|
|
|
+<!-- <button class="btn primary" @tap="addRandomNode">添加节点</button>-->
|
|
|
|
|
+<!-- <button class="btn secondary" @tap="resetView">重置视图</button>-->
|
|
|
|
|
+<!-- <button class="btn warning" @tap="exportData">导出数据</button>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 节点信息面板 -->
|
|
|
|
|
+<!-- <view v-if="selectedNode" class="node-info-panel">-->
|
|
|
|
|
+<!-- <view class="panel-header">-->
|
|
|
|
|
+<!-- <text class="panel-title">节点详情</text>-->
|
|
|
|
|
+<!-- <text class="close-btn" @tap="deselectNode">×</text>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- <view class="node-details">-->
|
|
|
|
|
+<!-- <view class="detail-item">-->
|
|
|
|
|
+<!-- <text class="detail-label">名称:</text>-->
|
|
|
|
|
+<!-- <text class="detail-value">{{ selectedNode.name }}</text>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- <view class="detail-item">-->
|
|
|
|
|
+<!-- <text class="detail-label">ID:</text>-->
|
|
|
|
|
+<!-- <text class="detail-value">{{ selectedNode.id }}</text>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- <view class="detail-item">-->
|
|
|
|
|
+<!-- <text class="detail-label">类型:</text>-->
|
|
|
|
|
+<!-- <text class="detail-value">{{ selectedNode.type }}</text>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- <view class="detail-item">-->
|
|
|
|
|
+<!-- <text class="detail-label">连接数:</text>-->
|
|
|
|
|
+<!-- <text class="detail-value">{{ getNodeDegree(selectedNode) }}</text>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+<!-- </view>-->
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 统计信息 -->
|
|
|
|
|
+ <view class="stats-panel">
|
|
|
|
|
+ <view class="stats-box"> <view class="stats-icon-debt stats-icon"></view> <text class="stat-item">债务</text></view>
|
|
|
|
|
+ <view class="stats-box"> <view class="stats-icon-borrower stats-icon"></view> <text class="stat-item">借款人</text></view>
|
|
|
|
|
+ <view class="stats-box"> <view class="stats-icon-guarantor stats-icon"></view> <text class="stat-item">担保人</text></view>
|
|
|
|
|
+ <view class="stats-box"> <view class="stats-icon-collateral stats-icon"></view> <text class="stat-item">抵押物</text></view>
|
|
|
|
|
+<!-- <view class="stats-box"> <view class="stats-icon-company stats-icon"></view> <text class="stat-item">借款人</text></view>-->
|
|
|
|
|
+<!-- <view class="stats-box"> <view class="stats-icon-g stats-icon"></view> <text class="stat-item">法院</text></view>-->
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+export default {
|
|
|
|
|
+ name:'graph',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ctx: null,
|
|
|
|
|
+ canvasWidth: 750,
|
|
|
|
|
+ canvasHeight: 500,
|
|
|
|
|
+ graphData: {
|
|
|
|
|
+ nodes: [],
|
|
|
|
|
+ links: []
|
|
|
|
|
+ },
|
|
|
|
|
+ selectedNode: null,
|
|
|
|
|
+ hoveredNode: null,
|
|
|
|
|
+ showLabels: true,
|
|
|
|
|
+ layoutIndex: 2,
|
|
|
|
|
+ layoutOptions: ['力导向布局', '环形布局', '树状布局'],
|
|
|
|
|
+ isDragging: false,
|
|
|
|
|
+ dragStartX: 0,
|
|
|
|
|
+ dragStartY: 0,
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 0,
|
|
|
|
|
+ scale: 1,
|
|
|
|
|
+ animationId: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onUnload() {
|
|
|
|
|
+ this.stopAnimation();
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.initCanvas();
|
|
|
|
|
+ this.initGraphData();
|
|
|
|
|
+ this.startAnimation();
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ initCanvas() {
|
|
|
|
|
+ this.ctx = uni.createCanvasContext('graphCanvas', this);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取系统信息设置Canvas尺寸
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ this.canvasWidth = systemInfo.windowWidth;
|
|
|
|
|
+ this.canvasHeight = systemInfo.windowHeight * 0.6;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ initGraphData() {
|
|
|
|
|
+ // 示例数据
|
|
|
|
|
+ this.graphData = {
|
|
|
|
|
+ nodes: [
|
|
|
|
|
+ { id: 'node1', name: '债务', type: 'center', x: 275, y: 150, size: 20, color: '#ff6b35' },
|
|
|
|
|
+ { id: 'node2', name: '大连迈世农业发展有限公司', type: 'user', x: 275, y: 150, size: 15, color: '#8b5cf6' },
|
|
|
|
|
+ { id: 'node3', name: '迈世集团有限公司', type: 'product', x: 275, y: 150, size: 15, color: '#3b82f6' },
|
|
|
|
|
+ { id: 'node4', name: '宜居园81号2单元1层1号房屋', type: 'category', x: 275, y: 150, size: 15, color: '#10b981' },
|
|
|
|
|
+ { id: 'node6', name: '景山东园7号13层2号房屋', type: 'category', x: 275, y: 150, size: 15, color: '#10b981' },
|
|
|
|
|
+ { id: 'node5', name: '中信银行股份有限公司大连分行', type: 'service', x: 275, y: 150, size: 15, color: '#6366f1' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ links: [
|
|
|
|
|
+ { source: 'node1', target: 'node2' },
|
|
|
|
|
+ { source: 'node1', target: 'node3' },
|
|
|
|
|
+ { source: 'node1', target: 'node4' },
|
|
|
|
|
+ { source: 'node1', target: 'node5' },
|
|
|
|
|
+ { source: 'node1', target: 'node6' },
|
|
|
|
|
+ { source: 'node2', target: 'node4' },
|
|
|
|
|
+ { source: 'node3', target: 'node6' },
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ startAnimation() {
|
|
|
|
|
+ const animate = () => {
|
|
|
|
|
+ this.updateLayout();
|
|
|
|
|
+ this.drawGraph();
|
|
|
|
|
+ // this.animationId = requestAnimationFrame(animate);
|
|
|
|
|
+ };
|
|
|
|
|
+ animate();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ stopAnimation() {
|
|
|
|
|
+ if (this.animationId) {
|
|
|
|
|
+ cancelAnimationFrame(this.animationId);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ updateLayout() {
|
|
|
|
|
+ // 简单的力导向布局模拟
|
|
|
|
|
+ if (this.layoutIndex === 0) {
|
|
|
|
|
+ this.applyForceDirectedLayout();
|
|
|
|
|
+ } else if (this.layoutIndex === 1) {
|
|
|
|
|
+ this.applyCircularLayout();
|
|
|
|
|
+ } else if (this.layoutIndex === 2) {
|
|
|
|
|
+ this.applyTreeLayout();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ applyForceDirectedLayout() {
|
|
|
|
|
+ const centerX = this.canvasWidth / 2 + this.offsetX;
|
|
|
|
|
+ const centerY = this.canvasHeight / 2 + this.offsetY;
|
|
|
|
|
+
|
|
|
|
|
+ // 简化的力导向布局
|
|
|
|
|
+ this.graphData.nodes.forEach((node, i) => {
|
|
|
|
|
+ if (node.id === 'node1') {
|
|
|
|
|
+ // 中心节点保持在中心
|
|
|
|
|
+ node.x = centerX;
|
|
|
|
|
+ node.y = centerY;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 向中心节点的引力
|
|
|
|
|
+ const centerNode = this.graphData.nodes[0];
|
|
|
|
|
+ const dx = centerNode.x - node.x;
|
|
|
|
|
+ const dy = centerNode.y - node.y;
|
|
|
|
|
+ const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
+
|
|
|
|
|
+ if (distance > 0) {
|
|
|
|
|
+ const force = 0.1;
|
|
|
|
|
+ node.x += dx * force;
|
|
|
|
|
+ node.y += dy * force;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 节点间的斥力
|
|
|
|
|
+ this.graphData.nodes.forEach((otherNode, j) => {
|
|
|
|
|
+ if (i !== j) {
|
|
|
|
|
+ const dx = node.x - otherNode.x;
|
|
|
|
|
+ const dy = node.y - otherNode.y;
|
|
|
|
|
+ const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
+
|
|
|
|
|
+ if (distance > 0 && distance < 100) {
|
|
|
|
|
+ const force = 50 / (distance * distance);
|
|
|
|
|
+ node.x += dx * force * 0.1;
|
|
|
|
|
+ node.y += dy * force * 0.1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ applyCircularLayout() {
|
|
|
|
|
+ const centerX = this.canvasWidth / 2 + this.offsetX;
|
|
|
|
|
+ const centerY = this.canvasHeight / 2 + this.offsetY;
|
|
|
|
|
+ const radius = 150;
|
|
|
|
|
+
|
|
|
|
|
+ this.graphData.nodes.forEach((node, i) => {
|
|
|
|
|
+ if (node.id === 'node1') {
|
|
|
|
|
+ node.x = centerX;
|
|
|
|
|
+ node.y = centerY;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const angle = ((i - 1) / (this.graphData.nodes.length - 1)) * Math.PI * 2;
|
|
|
|
|
+ node.x = centerX + Math.cos(angle) * radius;
|
|
|
|
|
+ node.y = centerY + Math.sin(angle) * radius;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ applyTreeLayout() {
|
|
|
|
|
+ const centerX = this.canvasWidth / 2 + this.offsetX;
|
|
|
|
|
+ const centerY = this.canvasHeight / 2 + this.offsetY;
|
|
|
|
|
+
|
|
|
|
|
+ this.graphData.nodes.forEach((node, i) => {
|
|
|
|
|
+ if (node.id === 'node1') {
|
|
|
|
|
+ node.x = centerX;
|
|
|
|
|
+ node.y = centerY;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const angle = ((i - 1) / (this.graphData.nodes.length - 1)) * Math.PI * 2;
|
|
|
|
|
+ const radius = 120;
|
|
|
|
|
+ node.x = centerX + Math.cos(angle) * radius;
|
|
|
|
|
+ node.y = centerY + Math.sin(angle) * radius;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ drawGraph() {
|
|
|
|
|
+ if (!this.ctx) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 清空画布
|
|
|
|
|
+ this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置缩放
|
|
|
|
|
+ this.ctx.scale(this.scale, this.scale);
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制连接线
|
|
|
|
|
+ this.drawLinks();
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制节点
|
|
|
|
|
+ this.drawNodes();
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制标签
|
|
|
|
|
+ if (this.showLabels) {
|
|
|
|
|
+ this.drawLabels();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.ctx.draw();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ drawLinks() {
|
|
|
|
|
+ this.graphData.links.forEach(link => {
|
|
|
|
|
+ const sourceNode = this.graphData.nodes.find(n => n.id === link.source);
|
|
|
|
|
+ const targetNode = this.graphData.nodes.find(n => n.id === link.target);
|
|
|
|
|
+
|
|
|
|
|
+ if (sourceNode && targetNode) {
|
|
|
|
|
+ const isHighlighted = this.isLinkHighlighted(link);
|
|
|
|
|
+
|
|
|
|
|
+ this.ctx.beginPath();
|
|
|
|
|
+ this.ctx.moveTo(sourceNode.x, sourceNode.y);
|
|
|
|
|
+ this.ctx.lineTo(targetNode.x, targetNode.y);
|
|
|
|
|
+ this.ctx.strokeStyle = isHighlighted ? '#ff6b6b' : '#cccccc';
|
|
|
|
|
+ this.ctx.lineWidth = isHighlighted ? 3 : 2;
|
|
|
|
|
+ this.ctx.stroke();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ drawNodes() {
|
|
|
|
|
+ this.graphData.nodes.forEach(node => {
|
|
|
|
|
+ const isSelected = this.selectedNode && this.selectedNode.id === node.id;
|
|
|
|
|
+ const isHovered = this.hoveredNode && this.hoveredNode.id === node.id;
|
|
|
|
|
+ const isHighlighted = isSelected || isHovered || this.isNodeConnected(node);
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制节点
|
|
|
|
|
+ this.ctx.beginPath();
|
|
|
|
|
+ this.ctx.arc(node.x, node.y, isHighlighted ? node.size + 3 : node.size, 0, 2 * Math.PI);
|
|
|
|
|
+ this.ctx.fillStyle = node.color;
|
|
|
|
|
+ this.ctx.fill();
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制边框
|
|
|
|
|
+ this.ctx.strokeStyle = isHighlighted ? '#ffffff' : node.color;
|
|
|
|
|
+ this.ctx.lineWidth = isHighlighted ? 3 : 2;
|
|
|
|
|
+ this.ctx.stroke();
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ drawLabels() {
|
|
|
|
|
+ this.graphData.nodes.forEach(node => {
|
|
|
|
|
+ this.ctx.setFontSize(12);
|
|
|
|
|
+ this.ctx.setFillStyle('#333333');
|
|
|
|
|
+ this.ctx.setTextAlign('center');
|
|
|
|
|
+ this.ctx.fillText(node.name, node.x, node.y - node.size - 8);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ isNodeConnected(node) {
|
|
|
|
|
+ if (!this.selectedNode && !this.hoveredNode) return false;
|
|
|
|
|
+
|
|
|
|
|
+ const targetNode = this.selectedNode || this.hoveredNode;
|
|
|
|
|
+ return this.graphData.links.some(link =>
|
|
|
|
|
+ (link.source === node.id && link.target === targetNode.id) ||
|
|
|
|
|
+ (link.target === node.id && link.source === targetNode.id)
|
|
|
|
|
+ );
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ isLinkHighlighted(link) {
|
|
|
|
|
+ if (!this.selectedNode && !this.hoveredNode) return false;
|
|
|
|
|
+
|
|
|
|
|
+ const targetNode = this.selectedNode || this.hoveredNode;
|
|
|
|
|
+ return link.source === targetNode.id || link.target === targetNode.id;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getNodeDegree(node) {
|
|
|
|
|
+ return this.graphData.links.filter(link =>
|
|
|
|
|
+ link.source === node.id || link.target === node.id
|
|
|
|
|
+ ).length;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onTouchStart(e) {
|
|
|
|
|
+ this.isDragging = true;
|
|
|
|
|
+ this.dragStartX = e.touches[0].x;
|
|
|
|
|
+ this.dragStartY = e.touches[0].y;
|
|
|
|
|
+
|
|
|
|
|
+ // 检测点击的节点
|
|
|
|
|
+ const touchX = e.touches[0].x / this.scale;
|
|
|
|
|
+ const touchY = e.touches[0].y / this.scale;
|
|
|
|
|
+ this.selectedNode = this.getNodeAtPosition(touchX, touchY);
|
|
|
|
|
+ if (this.selectedNode) {
|
|
|
|
|
+ this.$emit('setNode',this.selectedNode)
|
|
|
|
|
+ // uni.vibrateShort(); // 触觉反馈
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onTouchMove(e) {
|
|
|
|
|
+ if (!this.isDragging) return;
|
|
|
|
|
+
|
|
|
|
|
+ const touchX = e.touches[0].x;
|
|
|
|
|
+ const touchY = e.touches[0].y;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.selectedNode) {
|
|
|
|
|
+ // 拖动节点
|
|
|
|
|
+ this.selectedNode.x = touchX / this.scale;
|
|
|
|
|
+ this.selectedNode.y = touchY / this.scale;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 拖动画布
|
|
|
|
|
+ const deltaX = touchX - this.dragStartX;
|
|
|
|
|
+ const deltaY = touchY - this.dragStartY;
|
|
|
|
|
+
|
|
|
|
|
+ this.offsetX += deltaX;
|
|
|
|
|
+ this.offsetY += deltaY;
|
|
|
|
|
+
|
|
|
|
|
+ this.dragStartX = touchX;
|
|
|
|
|
+ this.dragStartY = touchY;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onTouchEnd() {
|
|
|
|
|
+ this.isDragging = false;
|
|
|
|
|
+ this.hoveredNode = null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getNodeAtPosition(x, y) {
|
|
|
|
|
+ for (let i = this.graphData.nodes.length - 1; i >= 0; i--) {
|
|
|
|
|
+ const node = this.graphData.nodes[i];
|
|
|
|
|
+ const distance = Math.sqrt(Math.pow(node.x - x, 2) + Math.pow(node.y - y, 2));
|
|
|
|
|
+ if (distance <= node.size + 5) {
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onLayoutChange(e) {
|
|
|
|
|
+ this.layoutIndex = parseInt(e.detail.value);
|
|
|
|
|
+ this.resetView();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ toggleLabels(e) {
|
|
|
|
|
+ this.showLabels = e.detail.value;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ addRandomNode() {
|
|
|
|
|
+ const nodeTypes = ['user', 'product', 'category', 'service'];
|
|
|
|
|
+ const colors = ['#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'];
|
|
|
|
|
+
|
|
|
|
|
+ const newNode = {
|
|
|
|
|
+ id: `node${this.graphData.nodes.length + 1}`,
|
|
|
|
|
+ name: `新节点${this.graphData.nodes.length}`,
|
|
|
|
|
+ type: nodeTypes[Math.floor(Math.random() * nodeTypes.length)],
|
|
|
|
|
+ x: Math.random() * this.canvasWidth,
|
|
|
|
|
+ y: Math.random() * this.canvasHeight,
|
|
|
|
|
+ size: 12 + Math.random() * 8,
|
|
|
|
|
+ color: colors[Math.floor(Math.random() * colors.length)]
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.graphData.nodes.push(newNode);
|
|
|
|
|
+
|
|
|
|
|
+ // 连接到随机现有节点
|
|
|
|
|
+ if (this.graphData.nodes.length > 1) {
|
|
|
|
|
+ const randomIndex = Math.floor(Math.random() * (this.graphData.nodes.length - 1));
|
|
|
|
|
+ this.graphData.links.push({
|
|
|
|
|
+ source: newNode.id,
|
|
|
|
|
+ target: this.graphData.nodes[randomIndex].id
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ resetView() {
|
|
|
|
|
+ this.offsetX = 0;
|
|
|
|
|
+ this.offsetY = 0;
|
|
|
|
|
+ this.scale = 1;
|
|
|
|
|
+ this.selectedNode = null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ deselectNode() {
|
|
|
|
|
+ this.selectedNode = null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ exportData() {
|
|
|
|
|
+ const exportData = {
|
|
|
|
|
+ nodes: this.graphData.nodes,
|
|
|
|
|
+ links: this.graphData.links
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '导出数据',
|
|
|
|
|
+ content: JSON.stringify(exportData, null, 2),
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ confirmText: '确定'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.graph-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: calc(100vh - 295px);
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ margin: 0 20rpx ;
|
|
|
|
|
+ border-radius: 50rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.canvas-wrapper {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.graph-canvas {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-panel {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ border-top: 1rpx solid #eee;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ padding: 15rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-label {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.picker {
|
|
|
|
|
+ background: #f0f0f0;
|
|
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.button-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ margin: 0 10rpx;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn.primary {
|
|
|
|
|
+ background: #4ecdc4;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn.secondary {
|
|
|
|
|
+ background: #45b7d1;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn.warning {
|
|
|
|
|
+ background: #ff6b6b;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.node-info-panel {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20rpx;
|
|
|
|
|
+ right: 20rpx;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
+ padding: 30rpx;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ max-width: 300rpx;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.close-btn {
|
|
|
|
|
+ font-size: 40rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.node-details {
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-label {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-value {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-panel {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 40rpx;
|
|
|
|
|
+ left: 30rpx;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: start;
|
|
|
|
|
+ width: 180rpx;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-box{
|
|
|
|
|
+ width: 90rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: start;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.stats-icon{
|
|
|
|
|
+ width: 10rpx;
|
|
|
|
|
+ height: 10rpx;
|
|
|
|
|
+ margin-right: 8rpx;
|
|
|
|
|
+ background-color:#6b7280 ;
|
|
|
|
|
+}
|
|
|
|
|
+.stats-icon-debt{
|
|
|
|
|
+ background-color:#ff6b35 ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-icon-borrower{
|
|
|
|
|
+ background-color:#8b5cf6 ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-icon-guarantor{
|
|
|
|
|
+ background-color:#3b82f6 ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-icon-collateral{
|
|
|
|
|
+ background-color:#10b981 ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-icon-property{
|
|
|
|
|
+ background-color:#f59e0b ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+.stats-icon-company{
|
|
|
|
|
+ background-color:#6366f1 ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-item {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ margin-bottom: 5rpx;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|