Files
deerflow-code/situation-overview-web/src/components/ActionOverview.vue
T
2026-05-23 17:23:15 +08:00

703 lines
25 KiB
Vue

<template>
<div class="action-overview">
<t-loading :loading="loading" show-overlay>
<t-layout>
<t-aside width="568px"></t-aside>
<t-layout>
<div class="overview-head"></div>
<div class="overview-body">
<div v-if="!loading" ref="graphRef" class="graph-container"></div>
</div>
</t-layout>
<t-aside width="562px">
<div class="graph-map-content">
<div class="collapse-content collapse-content-right flex-center-center cursor-pointer"
v-if="expandRightShow" @click="expandRightChange">
<img src="@/assets/map-icon/expand-logo.svg" alt="" />
</div>
<div class="expand-content" :class="{ 'expand-hidden': expandRightShow }">
<div class="graph-map">
<div class="list-content">
<div class="expand-logo cursor-pointer" @click="expandRightChange">
<img src="../assets/map-icon/expand-logo.svg" alt="" />
</div>
<div class="list-item" v-for="(item, index) in graphList" :key="index">
<div class="item-logo">
<img :src="item.imgUrl" alt="" />
</div>
<div class="item-title">{{ item.title }}</div>
</div>
</div>
</div>
</div>
</div>
</t-aside>
</t-layout>
</t-loading>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { Graph, Quadratic, register, ExtensionCategory, Circle } from '@antv/g6'
import { requestAxiosService } from '@/service/request-axios-service';
import { deepCopy, uniqueById } from '@/common/chatData';
const props = defineProps({
msg: String,
actionData: Object,
actionId: [String, Number, Object, Boolean, Array]
})
const count = ref(0);
let graph = null;
const graphRef = ref(null);
let treeMapData = {
nodes: [],
edges: [],
}
const service = requestAxiosService;
const loading = ref(false);
import xdLogo from "@/assets/map-icon/xd-logo.svg"
import xdLogoPng from "@/assets/map-icon/xd-logo.png"
import bcLogo from "@/assets/map-icon/boci-logo.svg"
import bcLogoPng from "@/assets/map-icon/boci-logo.png"
import characterLogo from "@/assets/map-icon/character-logo.svg"
import characterLogoPng from "@/assets/map-icon/character-logo.png"
import userSpreadLogo from "@/assets/map-icon/user-spread-logo.svg"
import userSpreadLogoPng from "@/assets/map-icon/user-spread-logo.png"
import audienceLogo from "@/assets/map-icon/audience-logo.svg"
import audienceLogoPng from "@/assets/map-icon/audience-logo.png"
import strategyLogo from "@/assets/map-icon/strategy-logo.png"
const graphList = [
{ imgUrl: xdLogo, title: 'XD' },
{ imgUrl: bcLogo, title: '波次' },
{ imgUrl: characterLogo, title: '人设' },
{ imgUrl: userSpreadLogo, title: '关键传播节点' },
{ imgUrl: audienceLogo, title: '受众群体' },
// { imgUrl: `${environment['g6ImgUrl'] || '/src/assets'}/map-icon/user-logo.svg`, title: '受众目标' },
]
const expandRightShow = ref(false);
onMounted(() => {
// initGraph()
})
const expandRightChange = () => {
expandRightShow.value = !expandRightShow.value;
}
// 查询关键传播节点-未完成
const copRelationingSearch = async (actionId) => {
const parmy = {
actionId,
}
loading.value = true;
const res = await service.get(`${environment['b1ConsumerUrl']}/cop/copRelationing`, parmy);
loading.value = false;
console.log(res);
treeMapData.nodes = [];
treeMapData.edges = [];
let result = res.data;
treeMapData.nodes.push({
id: actionId,
data: {
type: 'action',
bgColor: '#cf1322',
name: '行动'
},
size: 30
});
result?.designs?.forEach((item) => {
treeMapData.nodes.push({
id: item.designId,
data: { type: 'audience', bgColor: '#1569ef', name: item.targetAudience, item: deepCopy(item) },
size: 25
});
treeMapData.edges.push({
source: actionId, target: item.designId, data: { type: 'audience', lineColor: '#23D0FF', name: item.targetAudience }
});
item.designAccount.forEach((aItem) => {
//添加受众目标
// treeMapData.nodes.push({
// id: aItem.id,
// data: { type: 'userList', bgColor: '#1eb07f', name: aItem.accountName, item: deepCopy(aItem) },
// size: 25
// });
// treeMapData.edges.push({
// source: item.designId, target: aItem.id, data: { type: 'userList', lineColor: '#8263FF', name: aItem.accountName }
// });
});
// if (item.strategy) {
// item.strategy.forEach((cItem) => {
// try {
// cItem.extendInfo = JSON.parse(cItem.extendInfo);
// console.log('策略详细信息', cItem.extendInfo)
// } catch (error) {
// console.log('error',error)
// // cItem.namme
// }
// treeMapData.nodes.push({
// id: cItem.id,
// data: { type: 'strategy', bgColor: '#ac77e8', name: cItem.topic, item: deepCopy(cItem) },
// size: 25
// });
// treeMapData.edges.push({
// source: item.designId, target: cItem.id, data: { type: 'strategy', lineColor: '#ac77e8', name: cItem.topic }
// });
// })
// }
});
result?.pathway?.forEach((item) => {
treeMapData.nodes.push({
id: item.pathwayId,
data: { type: 'userSpread', bgColor: '#1eb07f', name: item.accountName, item: deepCopy(item) },
size: 25
});
treeMapData.edges.push({
source: actionId, target: item.pathwayId, data: { type: 'userSpread', lineColor: '#B0DC00', name: item.accountName }
});
// if (item.strategy) {
// item.strategy.forEach((aItem) => {
// treeMapData.nodes.push({
// id: aItem.id,
// data: { type: 'strategy', bgColor: '#ac77e8', name: aItem.topic, item: deepCopy(aItem) },
// size: 25
// });
// treeMapData.edges.push({
// source: item.pathwayId, target: aItem.id, data: { type: 'strategy', lineColor: '#ac77e8', name: aItem.topic }
// });
// })
// }
})
// await graph.value?.clear();
setTimeout(async () => {
await initGraph();
}, 10)
}
// 查询关键传播节点关系图-已完成
const copRelationSearch = async (actionId) => {
console.log('actionData', props.actionData);
const parmy = {
actionId,
}
loading.value = true;
const res = await service.get(`${environment['b1ConsumerUrl']}/cop/copRelations`, parmy);
loading.value = false;
console.log('查询关系图', res);
let result = res.data;
treeMapData.nodes = [];
treeMapData.edges = [];
treeMapData.nodes.push({
id: actionId,
data: {
type: 'action',
bgColor: '#cf1322',
name: '行动'
},
size: 30
})
result.bocis.forEach((item) => {
treeMapData.nodes.push({
id: item.bociId,
data: { type: 'boci', bgColor: '#fff6ea', name: item.bociName, item: deepCopy(item) },
size: 25
})
treeMapData.edges.push({
source: actionId, target: item.bociId, data: { type: 'action', lineColor: '#3683FF', name: item.bociName }
})
item.strategies.forEach((aItem) => {
// treeMapData.nodes.push({
// id: aItem.strategyId,
// data: { type: 'strategy', bgColor: '#ac77e8', name: aItem.targetAudience, item: deepCopy(aItem) },
// size: 25
// })
// treeMapData.edges.push({
// source: item.bociId, target: aItem.strategyId, data: { type: 'strategy', lineColor: '#ac77e8', name: aItem.targetAudience }
// })
aItem.personas.forEach((bItem, bIndex) => {
treeMapData.nodes.push({
id: 'persona' + bIndex + bItem.persona,
data: { type: 'character', bgColor: '#fff6ea', name: bItem.persona, item: deepCopy(bItem) },
size: 25
})
treeMapData.edges.push({
source: item.bociId, target: 'persona' + bIndex + bItem.persona, data: { type: 'character', lineColor: '#8263FF', name: bItem.persona }
})
bItem.pathwayRelations.forEach((cItem) => {
treeMapData.nodes.push({
id: cItem.pathwayId,
data: { type: 'userSpread', bgColor: '#1eb07f', name: cItem.pathwayAccountName, item: deepCopy(cItem) },
size: 25
})
treeMapData.edges.push({
source: 'persona' + bIndex + bItem.persona, target: cItem.pathwayId, data: { type: 'userSpread', lineColor: '#B0DC00', name: cItem.pathwayAccountName }
})
})
bItem.designRelations.forEach((dItem) => {
treeMapData.nodes.push({
id: dItem.designId,
data: { type: 'audience', bgColor: '#1569ef', name: dItem.targetAudience, item: deepCopy(dItem) },
size: 25
})
treeMapData.edges.push({
source: 'persona' + bIndex + bItem.persona, target: dItem.designId, data: { type: 'audience', lineColor: '#23D0FF', name: dItem.targetAudience }
})
})
})
})
});
treeMapData.nodes = uniqueById(treeMapData.nodes)
console.log('查询关系处理', treeMapData);
// await graph.value?.clear();
setTimeout(async () => {
await initGraph();
}, 10)
}
watch(() => props.actionData, (newVal, oldVal) => {
console.log('count changed from-------------', oldVal, 'to', newVal);
if (newVal) {
if (newVal.actionStatus === 25) {
copRelationSearch(newVal.id);
} else if (newVal.actionStatus === 20) {
copRelationingSearch(newVal.id)
}
}
}, { immediate: true });
// 渲染关系图
const initGraph = () => {
console.log('environment', environment)
// 自定义边
class AntLine extends Quadratic {
onCreate() {
const shape = this.shapeMap.key;
shape.animate([{ lineDashOffset: 20 }, { lineDashOffset: 0 }], { // 添加动画
duration: 500,
iterations: Infinity,
})
}
}
register(ExtensionCategory.EDGE, 'ant-line', AntLine);
// 定义自定义节点
class CustomImageCircleNode extends Circle {
render(attributes = this.parsedAttributes, container) {
super.render(attributes, container); // 渲染圆形节点
const [width, height] = this.getSize(attributes);
// 获取节点数据中的图片源
const { type, platform } = attributes;
// console.log('attributes', attributes);
// 如果有图片源,则添加图片元素
if (type === 'action') { // 当节点类型为行动
// console.log('imgUrl', imgUrl, width);
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: xdLogoPng,
},
container
);
this.upsert('subtitle', 'text', {
x: 0,
y: 0, // 节点中心
text: '行动',
fontSize: 16,
fill: '#fff',
textAlign: 'center',
textBaseline: 'middle',
fontWeight: 'bold' // 设置字体加粗
}, container)
} else if (type === 'boci') { // 节点类型为波次
// this.upsert(
// 'node-circle',
// 'rect',
// {
// x: -width / 2, // 图片的x坐标,使其居中
// y: -height / 2, // 图片的y坐标,使其居中
// width: width - 2, // 图片宽度略小于圆形节点
// height: height - 2, // 图片高度略小于圆形节点
// fill: 'transparent',
// stroke: '#fc5',
// lineWidth: 5,
// radius: 50,
// labelBackground: true,
// },
// container
// );
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: bcLogoPng,
},
container
);
this.upsert('subtitle', 'text', {
x: 10,
y: 0, // 节点中心
text: '波次',
fontSize: 16,
fill: '#fff',
textAlign: 'center',
textBaseline: 'middle',
fontWeight: 'bold' // 设置字体加粗
}, container)
} else if (type === 'character') { // 节点类型为人设
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: characterLogoPng,
},
container
);
} else if (type === 'userSpread') { // 节点类型为关键传播节点
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: userSpreadLogoPng,
},
container
);
} else if (type === 'audience') { // 节点类型为受众群体
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: audienceLogoPng,
},
container
);
} else if (type === 'userList') { // 节点类型为受众目标
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: userSpreadLogoPng,
},
container
);
} else if (type === 'strategy') {
this.upsert(
'node-image',
'image',
{
x: -width / 2, // 图片的x坐标,使其居中
y: -height / 2, // 图片的y坐标,使其居中
width: width, // 图片宽度略小于圆形节点
height: height, // 图片高度略小于圆形节点
src: strategyLogo,
},
container
);
}
}
}
// 2. 注册自定义节点
register(ExtensionCategory.NODE, 'custom-image-circle', CustomImageCircleNode);
const data = {
nodes: [
{ id: 'node1', data: { type: 'action', bgColor: '#cf1322', name: '行动1' }, size: 30 },
{ id: 'node2', data: { type: 'boci', bgColor: '#fff6ea', name: '波次1' }, size: 25 },
{ id: 'node3', data: { type: 'boci', bgColor: '#fff6ea', name: '波次2' }, size: 25 },
{ id: 'node4', data: { type: 'boci', bgColor: '#fff6ea', name: '波次3' }, size: 25 },
{ id: 'node5', data: { type: 'character', bgColor: '#fff6ea', name: '人设01' }, size: 25 },
{ id: 'node6', data: { type: 'character', bgColor: '#fff6ea', name: '人设02' }, size: 20 },
{ id: 'node7', data: { type: 'character', bgColor: '#fff6ea', name: '人设03' }, size: 25 },
{ id: 'node8', data: { type: 'character', bgColor: '#fff6ea', name: '人设04' }, size: 20 },
{ id: 'node9', data: { type: 'character', lineColor: '#fff6ea', name: '人设05' }, size: 25 },
{ id: 'node10', data: { type: 'character', bgColor: '#fff6ea', name: '人设06' }, size: 20 },
{ id: 'node11', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node12', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node13', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node14', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node15', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node16', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node17', data: { type: 'userSpread', platform: 'facebook', bgColor: '#1eb07f', name: '关键传播节点01' }, size: 20 },
{ id: 'node18', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node19', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node20', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node21', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node22', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node23', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
{ id: 'node24', data: { type: 'audience', platform: 'facebook', bgColor: '#1569ef', name: '关键传播节点01' }, size: 20 },
],
edges: [
{ source: 'node1', target: 'node2', data: { type: 'action', lineColor: '#cf1322', name: '行动1' } },
{ source: 'node2', target: 'node5', data: { type: 'boci', lineColor: '#dd8717', name: '波次1' } },
{ source: 'node5', target: 'node11', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node11', target: 'node18', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node2', target: 'node6', data: { type: 'audience', platform: 'facebook', lineColor: '#1569ef', name: '关键传播节点01' } },
{ source: 'node6', target: 'node12', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node12', target: 'node19', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node1', target: 'node3', data: { type: 'action', lineColor: '#cf1322', name: '行动1' } },
{ source: 'node3', target: 'node7', data: { type: 'boci', lineColor: '#dd8717', name: '波次1' } },
{ source: 'node7', target: 'node13', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node13', target: 'node20', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node3', target: 'node8', data: { type: 'audience', platform: 'facebook', lineColor: '#1569ef', name: '关键传播节点01' } },
{ source: 'node8', target: 'node14', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node14', target: 'node21', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node1', target: 'node4', data: { type: 'action', lineColor: '#cf1322', name: '行动1' } },
{ source: 'node4', target: 'node9', data: { type: 'boci', lineColor: '#dd8717', name: '波次1' } },
{ source: 'node9', target: 'node15', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node15', target: 'node22', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node4', target: 'node10', data: { type: 'audience', platform: 'facebook', lineColor: '#1569ef', name: '关键传播节点01' } },
{ source: 'node10', target: 'node16', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node16', target: 'node23', data: { type: 'userSpread', platform: 'facebook', lineColor: '#1eb07f', name: '关键传播节点01' } },
{ source: 'node10', target: 'node17', data: { type: 'character', lineColor: '#fff6ea', name: '人设01' } },
{ source: 'node17', target: 'node24', data: { type: 'audience', platform: 'facebook', lineColor: '#1569ef', name: '关键传播节点01' } },
]
}
// const size = (node) => Math.max(...node.style.size);
graph = new Graph({
container: graphRef.value,
// width: 800,
// height: 600,
data: treeMapData,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node']
},
node: {
type: 'custom-image-circle', // 使用自定义节点类型
style: {
label: true,
labelText: (d) => d.data?.name,
labelFill: '#fff',
labelBackground: false,
// fill: (d) => d.data.bgColor || '#fc5',
fill: 'transparent',
type: (d) => d.data.type,
platform: (d) => d.data.platform,
size: (d) => {
if (d.data.type === 'action') {
return [120, 60]
} else if (d.data.type === 'boci') {
return [130, 40]
} else {
return [60, 70]
}
}
}
},
edge: {
type: 'ant-line',
style: {
label: false,
// labelText: (d) => d.data?.type,
labelBackground: true,
stroke: (d) => d.data?.lineColor || '#fc5', // 边的颜色,可以根据需要调整
lineWidth: 2.5, // 边的宽度,可以根据需要调整
lineDash: [10, 10],
endArrow: true, // 添加箭头
arrowSize: 6, // 箭头大小
},
},
layout: {
// type: 'radial',
// unitRadius: 220,
// linkDistance: 200,
// type: 'd3-force',
// link: 200,
// // collide: { radius: (node) => size(node) },
// // manyBody: { strength: (node) => -4 * size(node) }
animation: false,
// preventOverlap: true
type: 'd3-force',
preventOverlap: true,
nodeSize: 100,
link: {
distance: 200,
strength: 2
},
collide: { radius: 100 },
manyBody: {
strangth: (d) => {
if (d.isLeaf) {
return -50;
}
return -10;
}
}
},
autoFit: 'view',
behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas',]
})
graph.render();
graph.on('node:click', (event) => {
const nodeItem = event.item; // 节点实例
console.log(event.target.id);
const model = graph.getNodeData(event.target.id);
console.log('节点数据:', model.data);
// const model = nodeItem.getModel(); // 数据模型
// console.log('点击了节点:', model.id);
});
// window.addPanel((gui) => {
// const config = {
// lineDash: 10,
// }
// gui.add(config,'lineDash',1,20,1).onChange((lineDash) => {
// graph.setEdge({
// type: 'ant-line',
// style: {
// lineDash: [lineDash,lineDash]
// }
// })
// })
// graph.draw()
// })
}
</script>
<style scoped lang="less">
.action-overview {
z-index: 500;
color: #fff;
position: relative;
.overview-head {
width: 100%;
height: 115px;
}
.overview-body {
// width: 100%;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: calc(100vh - 115px);
.graph-container {
width: 100%;
height: 100%;
}
}
.graph-map-content {
z-index: 500;
position: fixed;
bottom: 100px;
right: 20px;
.graph-map {
width: 202px;
background: rgba(3, 38, 80, 0.8);
border: 1px solid;
border-color: rgba(43, 143, 254, 0.6);
z-index: 500;
border-radius: 4px;
padding: 10px 15px;
padding-top: 30px;
.list-content {
position: relative;
.expand-logo {
width: 20px;
height: 11px;
position: absolute;
top: -27px;
right: -8px;
img {
width: 100%;
}
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.item-logo {
width: 62px;
height: 28px;
img {
height: 28px;
}
}
.item-title {
flex: 1;
font-family: Noto Sans S Chinese;
font-weight: 300;
color: #ffffff;
font-size: 18px;
}
}
}
}
}
.collapse-content {
width: 30px;
height: 40px;
background: #032650cc;
border: 1px solid;
border-color: #2b8ffe99;
border-radius: 4px 0 0 4px;
z-index: 500;
position: absolute;
top: 0;
img {
width: 16px;
}
}
.collapse-content-right {
right: 0;
}
.expand-hidden {
width: 0px !important;
border: 0px !important;
overflow: hidden;
}
.expand-content {
width: 100%;
transition: all 0.5s ease;
float: right;
}
.expand-content-right {
float: right;
}
}
</style>