Compare commits
2 Commits
0e3d77019e
...
9106635460
Author | SHA1 | Date |
---|---|---|
JayChou | 9106635460 | 3 months ago |
JayChou | dbccafabdd | 3 months ago |
9 changed files with 924 additions and 721 deletions
@ -1,172 +1,238 @@ |
||||
<template> |
||||
<div id="3d-graph"></div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
// // import { useRouter } from 'vue-router' |
||||
// import { onMounted, ref, reactive, watch } from 'vue' |
||||
// import ForceGraph3D from '3d-force-graph' |
||||
// //@ts-ignore |
||||
// import { |
||||
// CSS2DRenderer, |
||||
// CSS2DObject, |
||||
// } from 'three/examples/jsm/renderers/CSS2DRenderer.js' |
||||
// //@ts-ignore |
||||
// import SpriteText from '../spritetext.js' |
||||
// // const $router = useRouter() |
||||
// // const jsonData = ref(null) |
||||
// let Graph: any = reactive({}) |
||||
// const props = defineProps({ |
||||
// width: { |
||||
// // type: Number, |
||||
// // default: |
||||
// // window.innerWidth || |
||||
// // document.documentElement.clientWidth || |
||||
// // document.body.clientWidth, |
||||
// }, |
||||
// height: { |
||||
// type: Number, |
||||
// // default: |
||||
// // window.innerHeight || |
||||
// // document.documentElement.clientHeight || |
||||
// // document.body.clientHeight, |
||||
// }, |
||||
// }) |
||||
// watch( |
||||
// () => props.width, |
||||
// (ne) => Graph.width(ne), |
||||
// ) |
||||
// watch( |
||||
// () => props.height, |
||||
// (ne) => Graph.height(ne), |
||||
// ) |
||||
// const dom = ref(null) |
||||
// onMounted(() => { |
||||
// Graph = ForceGraph3D({ |
||||
// extraRenderers: [new CSS2DRenderer()], |
||||
// })(document.getElementById('3d-graph') as HTMLElement) |
||||
// .jsonUrl('../../../public/data.json') |
||||
// // .nodeAutoColorBy('group') |
||||
// .nodeThreeObject((node: any) => { |
||||
// const nodeEl = document.createElement('div') |
||||
// nodeEl.textContent = node.label |
||||
// nodeEl.style.color = '#333333' |
||||
// nodeEl.style.borderRadius = '50%' |
||||
// // console.log(node, 111, Graph.graphData().nodes) |
||||
// return new CSS2DObject(nodeEl) |
||||
// }) |
||||
// .linkLabel((link: any) => link.label) // 指定链接上显示的文字属性 |
||||
// .linkWidth(0.8) |
||||
// .linkHoverPrecision(0.5) // 设置链接文字的显示精度,这里设置为一个足够小的值 |
||||
// .linkColor(() => '#dd92fd') // 设置连接线颜色为红色 |
||||
// .backgroundColor('#f5f6fd') |
||||
// .width(props.width) |
||||
// .height(props.height) |
||||
// .linkThreeObjectExtend(true) |
||||
// .nodeColor((node: any) => { |
||||
// return node.color |
||||
// }) |
||||
// .nodeRelSize(7) // 设置节点的相对大小为4 |
||||
// .nodeResolution(20) |
||||
// .linkDirectionalArrowLength(3) // 设置连接线箭头长度为3 |
||||
// .linkDirectionalArrowRelPos(1) // 设置连接线箭头位置为连接线末端 |
||||
// .nodeThreeObjectExtend(true) |
||||
// .onNodeClick((node: any) => { |
||||
// // Aim at node from outside it |
||||
// // 目标距离 |
||||
// const targetDistance = 200 // 设定一个一般的距离 |
||||
|
||||
// // 计算相机新位置 |
||||
// const distRatio = 1 + targetDistance / Math.hypot(node.x, node.y, node.z) |
||||
// const newPos = { |
||||
// x: node.x * distRatio, |
||||
// y: node.y * distRatio, |
||||
// z: node.z * distRatio, |
||||
// } |
||||
|
||||
// // 如果节点位于原点,则特殊处理 |
||||
// if (node.x === 0 && node.y === 0 && node.z === 0) { |
||||
// newPos.z = targetDistance // 将 z 轴设定为目标距离 |
||||
// } |
||||
|
||||
// // 移动相机至新位置 |
||||
// //@ts-ignore |
||||
// Graph.cameraPosition( |
||||
// newPos, // 新位置 |
||||
// node, // 视角焦点 |
||||
// 3000, // 过渡时长 |
||||
// ) |
||||
|
||||
// // 获取图表数据 |
||||
// //@ts-ignore |
||||
// const graphData = Graph.graphData() |
||||
|
||||
// // 遍历连接线,检查每条连接线的起点和终点是否与当前点击的节点相关联 |
||||
// graphData.links.forEach((link: any) => { |
||||
// // console.log(link); |
||||
|
||||
// if (link.source.id === node.id || link.target.id === node.id) { |
||||
// setLabel() |
||||
// // 将与当前点击节点相关联的连接线颜色设置为红色 |
||||
// // link.color = '#FF0000' // 设置连接线颜色为红色 |
||||
// //@ts-ignore |
||||
// Graph.linkColor((item: any): any => { |
||||
// if (item.source.id === node.id || item.target.id === node.id) { |
||||
// return 'red' |
||||
// } else { |
||||
// return '#dd92fd' |
||||
// } |
||||
// }) |
||||
// } else { |
||||
// // Graph.linkColor(() => '#a4c7fe') // 设置连接线颜色为红色 |
||||
// } |
||||
// }) |
||||
|
||||
// // 更新图形数据,以应用颜色变化 |
||||
// //@ts-ignore |
||||
// Graph.graphData(graphData) |
||||
// }) |
||||
// dom.value = document.querySelector('canvas') as any |
||||
// }) |
||||
|
||||
// const setLabel = () => { |
||||
// //@ts-ignore |
||||
// Graph.linkThreeObject((link: any) => { |
||||
// // extend link with text sprite |
||||
// const sprite = new SpriteText(`${link.label}`) |
||||
// sprite.color = '#ccc' |
||||
// sprite.textHeight = 1.5 |
||||
// return sprite |
||||
// }) |
||||
// //@ts-ignore |
||||
// Graph.linkPositionUpdate((sprite, { start, end }) => { |
||||
// //@ts-ignore |
||||
// const middlePos = Object.assign( |
||||
// ...['x', 'y', 'z'].map((c) => ({ |
||||
// //@ts-ignore |
||||
// [c]: start[c] + (end[c] - start[c]) / 2, // calc middle point |
||||
// })), |
||||
// ) |
||||
|
||||
// // Position sprite |
||||
// Object.assign(sprite.position, middlePos) |
||||
// }) |
||||
// //@ts-ignore |
||||
// Graph.d3Force('charge').strength(-120) |
||||
// } |
||||
// // const goToEditAtlas = () => { |
||||
// // console.log(jsonData.value) |
||||
// import { useRouter } from 'vue-router' |
||||
import { onMounted, ref, reactive ,watch} from 'vue' |
||||
import ForceGraph3D from '3d-force-graph' |
||||
//@ts-ignore |
||||
import { |
||||
CSS2DRenderer, |
||||
CSS2DObject, |
||||
} from 'three/examples/jsm/renderers/CSS2DRenderer.js' |
||||
//@ts-ignore |
||||
import SpriteText from '../spritetext.js' |
||||
// const $router = useRouter() |
||||
// const jsonData = ref(null) |
||||
import { getCourseAtlas} from '@/api/courseChaptersApi' |
||||
let Graph = reactive({}) |
||||
const props = defineProps({ |
||||
width: { |
||||
type: Number, |
||||
default: |
||||
window.innerWidth || |
||||
document.documentElement.clientWidth || |
||||
document.body.clientWidth, |
||||
}, |
||||
height: { |
||||
type: Number, |
||||
default: |
||||
window.innerHeight || |
||||
document.documentElement.clientHeight || |
||||
document.body.clientHeight, |
||||
}, |
||||
index: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
id: { |
||||
type: String || Number, |
||||
required: true, |
||||
}, |
||||
}) |
||||
const emits = defineEmits(['clickGraph']) |
||||
|
||||
// // $router.push({ name: 'EditAtlas', params: { id: 123 } }) |
||||
// // } |
||||
</script> |
||||
const dom = ref(null) |
||||
onMounted(async () => { |
||||
const res: any = await getCourseAtlas({ id: '719f91586a64413898253c5b7d046fd8' }) |
||||
// console.log(res,'res') |
||||
const gData:any = { |
||||
nodes: [...res.data.knowList], |
||||
links: [...res.data.linksList], |
||||
} |
||||
gData.links.forEach((link:any) => { |
||||
const a = gData.nodes.find((item:any) => item.id === link.source) |
||||
const b = gData.nodes.find((item:any) => item.id === link.target) |
||||
// console.log(a, b) |
||||
|
||||
<template> |
||||
<div class="grid-content knowledge-graph" style="height: 670px"> |
||||
<!-- <div class="graph"> |
||||
<div id="3d-graph"></div> |
||||
</div> --> |
||||
</div> |
||||
</template> |
||||
<style lang="scss" scoped> |
||||
.graph { |
||||
width: 200px; |
||||
height: 400px; |
||||
!a.neighbors && (a.neighbors = []) |
||||
!b.neighbors && (b.neighbors = []) |
||||
a.neighbors.push(b) |
||||
b.neighbors.push(a) |
||||
|
||||
!a.links && (a.links = []) |
||||
!b.links && (b.links = []) |
||||
a.links.push(link) |
||||
b.links.push(link) |
||||
}) |
||||
|
||||
const highlightNodes = new Set() |
||||
const highlightLinks = new Set() |
||||
let hoverNode:any = null |
||||
|
||||
Graph = ForceGraph3D({ |
||||
extraRenderers: [new CSS2DRenderer()], |
||||
})(document.getElementById('3d-graph') as HTMLElement) |
||||
.graphData(gData) |
||||
// .nodeAutoColorBy('group') |
||||
.nodeThreeObject((node: any) => { |
||||
const nodeEl = document.createElement('div') |
||||
nodeEl.textContent = node.label |
||||
nodeEl.style.color = '#333333' |
||||
nodeEl.style.borderRadius = '50%' |
||||
// console.log(node, 111, Graph.graphData().nodes) |
||||
return new CSS2DObject(nodeEl) |
||||
}) |
||||
.linkLabel((link: any) => link.label) // 指定链接上显示的文字属性 |
||||
.linkWidth(1) |
||||
.linkHoverPrecision(0.5) // 设置链接文字的显示精度,这里设置为一个足够小的值 |
||||
.linkColor(() => '#000') // 设置连接线颜色为红色 |
||||
.backgroundColor('#fff') |
||||
.width(props.width) |
||||
.height(props.height) |
||||
.linkThreeObjectExtend(true) |
||||
.nodeRelSize(7) // 设置节点的相对大小为4 |
||||
.nodeResolution(20) |
||||
.linkDirectionalArrowLength(3) // 设置连接线箭头长度为3 |
||||
.linkDirectionalArrowRelPos(1) // 设置连接线箭头位置为连接线末端 |
||||
.nodeThreeObjectExtend(true) |
||||
.nodeColor((node:any) => |
||||
highlightNodes.has(node) |
||||
? node === hoverNode |
||||
? 'rgb(255,0,0,1)' |
||||
: node.color |
||||
: node.color, |
||||
) |
||||
.linkWidth((link) => (highlightLinks.has(link) ? 4 : 1)) |
||||
.linkDirectionalParticles((link) => (highlightLinks.has(link) ? 4 : 0)) |
||||
.linkDirectionalParticleWidth(4) |
||||
.onNodeClick((node: any) => { |
||||
console.log(node); |
||||
emits('clickGraph',node.id) |
||||
// Aim at node from outside it |
||||
// 目标距离 |
||||
const targetDistance = 200 // 设定一个一般的距离 |
||||
|
||||
// 计算相机新位置 |
||||
const distRatio = 1 + targetDistance / Math.hypot(node.x, node.y, node.z) |
||||
const newPos = { |
||||
x: node.x * distRatio, |
||||
y: node.y * distRatio, |
||||
z: node.z * distRatio, |
||||
} |
||||
|
||||
// 如果节点位于原点,则特殊处理 |
||||
if (node.x === 0 && node.y === 0 && node.z === 0) { |
||||
newPos.z = targetDistance // 将 z 轴设定为目标距离 |
||||
} |
||||
|
||||
// 移动相机至新位置 |
||||
//@ts-ignore |
||||
Graph.cameraPosition( |
||||
newPos, // 新位置 |
||||
node, // 视角焦点 |
||||
3000, // 过渡时长 |
||||
) |
||||
|
||||
// 获取图表数据 |
||||
//@ts-ignore |
||||
const graphData = Graph.graphData() |
||||
|
||||
// 遍历连接线,检查每条连接线的起点和终点是否与当前点击的节点相关联 |
||||
graphData.links.forEach((link: any) => { |
||||
// console.log(link); |
||||
|
||||
if (link.source.id === node.id || link.target.id === node.id) { |
||||
setLabel() |
||||
// 将与当前点击节点相关联的连接线颜色设置为红色 |
||||
// link.color = '#FF0000' // 设置连接线颜色为红色 |
||||
//@ts-ignore |
||||
Graph.linkColor((item: any): any => { |
||||
if (item.source.id === node.id || item.target.id === node.id) { |
||||
return 'red' |
||||
} else { |
||||
return '#000' |
||||
} |
||||
}) |
||||
} else { |
||||
// Graph.linkColor(() => '#a4c7fe') // 设置连接线颜色为红色 |
||||
} |
||||
}) |
||||
|
||||
// 更新图形数据,以应用颜色变化 |
||||
//@ts-ignore |
||||
Graph.graphData(graphData) |
||||
}) |
||||
.onNodeHover((node:any) => { |
||||
// no state change |
||||
if ((!node && !highlightNodes.size) || (node && hoverNode === node)) |
||||
return |
||||
|
||||
highlightNodes.clear() |
||||
highlightLinks.clear() |
||||
if (node && node.neighbors ) { |
||||
highlightNodes.add(node) |
||||
node.neighbors.forEach((neighbor:any) => highlightNodes.add(neighbor)) |
||||
node.links.forEach((link :any) => highlightLinks.add(link)) |
||||
} |
||||
|
||||
hoverNode = node || null |
||||
|
||||
updateHighlight() |
||||
}) |
||||
.onLinkHover((link :any) => { |
||||
highlightNodes.clear() |
||||
highlightLinks.clear() |
||||
|
||||
if (link) { |
||||
highlightLinks.add(link) |
||||
highlightNodes.add(link.source) |
||||
highlightNodes.add(link.target) |
||||
} |
||||
|
||||
updateHighlight() |
||||
}) |
||||
dom.value = document.querySelector('canvas') as any |
||||
}) |
||||
const updateHighlight = () => { |
||||
// console.log(1111) |
||||
|
||||
// trigger update of highlighted objects in scene |
||||
Graph.nodeColor(Graph.nodeColor()) |
||||
.linkWidth(Graph.linkWidth()) |
||||
.linkDirectionalParticles(Graph.linkDirectionalParticles()) |
||||
} |
||||
const setLabel = () => { |
||||
//@ts-ignore |
||||
Graph.linkThreeObject((link: any) => { |
||||
// extend link with text sprite |
||||
const sprite = new SpriteText(`${link.label}`) |
||||
sprite.color = '#ccc' |
||||
sprite.textHeight = 1.5 |
||||
return sprite |
||||
}) |
||||
//@ts-ignore |
||||
Graph.linkPositionUpdate((sprite, { start, end }) => { |
||||
//@ts-ignore |
||||
const middlePos = Object.assign( |
||||
...['x', 'y', 'z'].map((c) => ({ |
||||
//@ts-ignore |
||||
[c]: start[c] + (end[c] - start[c]) / 2, // calc middle point |
||||
})), |
||||
) |
||||
|
||||
// Position sprite |
||||
Object.assign(sprite.position, middlePos) |
||||
}) |
||||
//@ts-ignore |
||||
Graph.d3Force('charge').strength(-120) |
||||
} |
||||
</style> |
||||
// const goToEditAtlas = () => { |
||||
// console.log(jsonData.value) |
||||
|
||||
// $router.push({ name: 'EditAtlas', params: { id: 123 } }) |
||||
// } |
||||
</script> |
||||
|
||||
<style lang="scss" scoped></style> |
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,260 @@ |
||||
<template> |
||||
<div class="container"> |
||||
<el-card style="width: 80%"> |
||||
<template #header> |
||||
<div class="card-header"> |
||||
<div class="left"> |
||||
<el-button type="primary" :icon="Plus" @click="add" /> |
||||
</div> |
||||
<div class="right"> |
||||
<el-button type="primary">知识图谱预览</el-button> |
||||
<el-button type="primary" @click="save">保存</el-button> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<el-alert |
||||
title="从右侧知识点集合中拖动知识点放置在前序、后续知识点列表中" |
||||
type="success" |
||||
/> |
||||
<el-table |
||||
:data="tableData" |
||||
border |
||||
style="width: 100%" |
||||
height="670" |
||||
:header-cell-style="{ |
||||
'text-align': 'center', |
||||
'background-color': '#f5f7fa', |
||||
}" |
||||
> |
||||
<el-table-column |
||||
prop="date" |
||||
align="center" |
||||
label="前序知识点(章节)" |
||||
width="120" |
||||
> |
||||
<template #default="{ row }"> |
||||
<div |
||||
@dragover.prevent="dragoverEvent" |
||||
@drop="dropEvent($event, row.id)" |
||||
style="width: 100%; min-height: 40px" |
||||
> |
||||
{{ row.before.name }} |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column prop="name" align="center" label="关系" width="120"> |
||||
<template #default="{ row }"> |
||||
<div> |
||||
<el-select v-model="row.relationship" placeholder="选择关系"> |
||||
<el-option |
||||
v-for="item in options" |
||||
:key="item.value" |
||||
:label="item.label" |
||||
:value="item.value" |
||||
/> |
||||
</el-select> |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column prop="address" align="center" label="后续知识点(章节)"> |
||||
<template #default="{ row }"> |
||||
<div |
||||
@dragover.prevent="dragoverEvent" |
||||
@drop="dropAfterEvent($event, row.id)" |
||||
style="width: 100%; min-height: 40px" |
||||
> |
||||
{{ row.after.name }} |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column prop="address" label="操作" align="center" width="180"> |
||||
<template #default="{ row }"> |
||||
<div> |
||||
<!-- <el-button type="text">编辑</el-button> --> |
||||
<el-button type="danger" link @click="del(row.id)">删除</el-button> |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
|
||||
<!-- <div style="display: flex; justify-content: center; margin-top: 10px"> |
||||
<el-pagination background layout="prev, pager, next" :total="1000" /> |
||||
</div> --> |
||||
</el-card> |
||||
<el-card style="width: 20%; margin-left: 10px"> |
||||
<template #header> |
||||
<div> |
||||
<el-input :prefix-icon="Search"></el-input> |
||||
</div> |
||||
</template> |
||||
<div class="title">本课程知识点集合</div> |
||||
<el-scrollbar height="800"> |
||||
<div class="knowledgeBox"> |
||||
<div class="item" v-for="i in knowList" :key="i.id"> |
||||
<el-tooltip |
||||
class="box-item" |
||||
effect="dark" |
||||
:content="i.label" |
||||
placement="top" |
||||
> |
||||
<el-tag |
||||
size="large" |
||||
type="primary" |
||||
draggable="true" |
||||
@dragstart="dragstartEvent(i)" |
||||
> |
||||
{{ splitText(i.label) }}(1h) |
||||
</el-tag> |
||||
</el-tooltip> |
||||
</div> |
||||
</div> |
||||
</el-scrollbar> |
||||
</el-card> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { Plus, Search } from '@element-plus/icons-vue' |
||||
import { ref } from 'vue' |
||||
import { getKonwledgeMangment ,editKonledgeMangment} from '@/api/courseChaptersApi' |
||||
const tableData = ref([ |
||||
{ |
||||
id: 1, |
||||
before: { |
||||
name: '', |
||||
id: '', |
||||
}, |
||||
after: { |
||||
name: '', |
||||
id: '', |
||||
}, |
||||
relationship: '', |
||||
}, |
||||
]) |
||||
|
||||
const options = [ |
||||
{ |
||||
value: 'contain', |
||||
label: '包含', |
||||
}, |
||||
{ |
||||
value: 'order', |
||||
label: '后续', |
||||
}, |
||||
{ |
||||
value: 'dependent', |
||||
label: '依赖', |
||||
}, |
||||
] |
||||
const knowList = ref<any>([]) |
||||
const getKonwledgeMangmentEvent = async () => { |
||||
const res = await getKonwledgeMangment({ |
||||
id: '719f91586a64413898253c5b7d046fd8', |
||||
}) |
||||
// console.log(res.data.knowList); |
||||
knowList.value = res.data.knowList |
||||
} |
||||
getKonwledgeMangmentEvent() |
||||
const splitText = (str: string) => { |
||||
if (str.length > 5) { |
||||
const newStr = str.slice(0, 5) |
||||
return newStr + '...' |
||||
} else { |
||||
return str |
||||
} |
||||
} |
||||
splitText('123456') |
||||
const darData = ref<any>({}) |
||||
const dragstartEvent = (data: any) => { |
||||
console.log(data) |
||||
darData.value = data |
||||
} |
||||
const dragoverEvent = () => {} |
||||
const dropEvent = (e: any, id: any) => { |
||||
console.log('dropEvent', e, id) |
||||
const index = tableData.value.findIndex((item) => item.id === id) |
||||
tableData.value[index].before.name = darData.value.label |
||||
tableData.value[index].before.id = darData.value.id |
||||
} |
||||
const dropAfterEvent = (e: any, id: any) => { |
||||
console.log('dropEvent', e, id) |
||||
const index = tableData.value.findIndex((item) => item.id === id) |
||||
tableData.value[index].after.name = darData.value.label |
||||
tableData.value[index].after.id = darData.value.id |
||||
} |
||||
const add = () => { |
||||
tableData.value.unshift({ |
||||
id: tableData.value.length + 1, |
||||
before: { |
||||
name: '', |
||||
id: '', |
||||
}, |
||||
after: { |
||||
name: '', |
||||
id: '', |
||||
}, |
||||
relationship: '', |
||||
}) |
||||
} |
||||
|
||||
const save = async () => { |
||||
let arr = tableData.value.map((item) => { |
||||
let link: any = { |
||||
source: '', |
||||
target: '', |
||||
label: '', |
||||
} |
||||
link.source = item.before.id |
||||
link.target = item.after.id |
||||
link.label = item.relationship |
||||
return link |
||||
}) |
||||
console.log(arr) |
||||
await editKonledgeMangment(arr) |
||||
} |
||||
const del = (id:any) => { |
||||
const index = tableData.value.findIndex((item) => item.id === id) |
||||
tableData.value.splice(index,1) |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.container { |
||||
width: 100%; |
||||
height: calc(100vh - 90px); |
||||
display: flex; |
||||
.card-header { |
||||
width: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
} |
||||
.title { |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
} |
||||
.knowledgeBox { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
justify-content: space-between; |
||||
margin-top: 20px; |
||||
// padding: 10px; |
||||
.item { |
||||
width: calc(50% - 10px); |
||||
height: 50px; |
||||
} |
||||
.item:nth-child(2n) { |
||||
margin-left: 5px; |
||||
} |
||||
.item:nth-child(2n + 1) { |
||||
margin-right: 5px; |
||||
} |
||||
} |
||||
} |
||||
:deep(.el-scrollbar__wrap) { |
||||
padding: 0; |
||||
} |
||||
:deep(.el-tag) { |
||||
width: 115px; |
||||
} |
||||
</style> |
Loading…
Reference in new issue