Compare commits

...

2 Commits

  1. 22
      src/api/courseChaptersApi.ts
  2. 2
      src/layout/tabbar/setting/index.vue
  3. 10
      src/router/routers.ts
  4. 402
      src/views/course/components/knowledge-graph.vue
  5. 2
      src/views/course/courseDetails.vue
  6. 903
      src/views/course/spritetext.js
  7. 3
      src/views/home/components/Lessonlist.vue
  8. 41
      src/views/home/index.vue
  9. 260
      src/views/knowledgeManagement/index.vue

@ -100,3 +100,25 @@ export const getCourseInfo = (params: any) => {
// method: 'get',
})
}
// 获取课程图谱
export const getCourseAtlas = (params: object) => {
return request({
url: '/api/knowNeo/getAllKnowByCourseId',
params,
})
}
// 获取课程下的知识点
export const getKonwledgeMangment = (params:any) => {
return request({
url:'/api/knowNeo/getAllKnowByCourseId',
params
})
}
// 批量修改知识点关系
export const editKonledgeMangment = (data:any) => {
return request({
url:'/api/knowNeo/updateLinks',
method:"POST",
data
})
}

@ -11,7 +11,7 @@
/> -->
<el-button size="small" icon="refresh" circle @click="refresh" />
<el-button size="small" icon="FullScreen" circle @click="fullScreen" />
<el-button size="small" icon="Setting" circle @click="$router.push('/configurationPage')"/>
<el-button v-if="userStore.userInfo.roleId[0] === '1'" size="small" icon="Setting" circle @click="$router.push('/configurationPage')" />
<el-button
size="small"
icon="Link"

@ -298,6 +298,16 @@ export const constantRoute: any = [
icon: 'Reading',
},
},
{
path: '/courseResourcesManagement/knowledgeManagement',
component: () => import('@/views/knowledgeManagement/index.vue'),
name: 'KnowledgeManagement',
meta: {
title: '知识点管理',
hidden: false,
icon: 'Reading',
},
}
],
},
]

@ -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>

@ -16,7 +16,7 @@ import resourcemanagement from './components/resource-management.vue'
<knowledgestatistic></knowledgestatistic>
</el-col>
<el-col :span="10">
<KnowledgeGraph></KnowledgeGraph>
<KnowledgeGraph :width="660" :height="670"></KnowledgeGraph>
</el-col>
</el-row>
<el-row :gutter="20">

File diff suppressed because it is too large Load Diff

@ -63,7 +63,7 @@ onMounted(() => {
</div>
<div class="lessonlist-item-info">
<h3 @click="onGetCourseObject(item.id)">{{ item.name }}</h3>
<div class="p" @click="onEditCourse(item)"><p>编辑</p></div>
<div class="p" @click="onEditCourse(item)" v-if="userStore.data.roles[0] == 1"><p>编辑</p></div>
</div>
</div>
</div>
@ -71,7 +71,6 @@ onMounted(() => {
</div>
<courseedit @click="open"></courseedit>
</template>
<style scoped>
.lesson_container {
/* background-color: purple; */

@ -172,9 +172,13 @@ onMounted(() => {
></conheader>
</a>
<div class="contt">
<p class="button" round v-for="item in topKnow" :key="item.id">
<!-- <p class="button" round v-for="item in topKnow" :key="item.id">
{{ item.label }}
</p>
</p> -->
<el-table :data="topKnow" border style="width: 100%" height="240" :header-cell-style="{'text-align':'center'}">
<el-table-column type="index" align=center label="序号" width="80"/>
<el-table-column prop="label" align=center label="课程名称" />
</el-table>
</div>
</div>
</el-col>
@ -193,9 +197,13 @@ onMounted(() => {
<conheader :title="`推荐知识点`"></conheader>
</a>
<div class="contt">
<p class="button" round v-for="item in maxKnow" :key="item.id">
<!-- <p class="button" round v-for="item in maxKnow" :key="item.id">
{{ item.label }}
</p>
</p> -->
<el-table :data="maxKnow" border style="width: 100%" height="240" :header-cell-style="{'text-align':'center'}">
<el-table-column type="index" align=center label="序号" width="80"/>
<el-table-column prop="label" align=center label="课程名称" />
</el-table>
</div>
</div>
</el-col>
@ -211,7 +219,7 @@ onMounted(() => {
></conheader>
</a>
<div class="con">
<ul>
<!-- <ul>
<li
class="lessonlist-item"
v-for="(item, index) in toplist"
@ -222,7 +230,11 @@ onMounted(() => {
<h5>{{ item.name || '暂无' }}</h5>
</div>
</li>
</ul>
</ul> -->
<el-table :data="toplist" border style="width: 100%" height="240" :header-cell-style="{'text-align':'center'}">
<el-table-column type="index" align=center label="序号" width="80"/>
<el-table-column prop="name" align=center label="课程名称" />
</el-table>
</div>
</div>
</el-col>
@ -238,7 +250,7 @@ onMounted(() => {
></conheader>
</a>
<div class="con">
<ul>
<!-- <ul>
<li
class="lessonlist-item"
v-for="(item, index) in courselist"
@ -249,7 +261,11 @@ onMounted(() => {
<h5>{{ item.name || '暂无' }}</h5>
</div>
</li>
</ul>
</ul> -->
<el-table :data="courselist" border style="width: 100%" height="240" :header-cell-style="{'text-align':'center'}">
<el-table-column type="index" align=center label="序号" width="80"/>
<el-table-column prop="name" align=center label="课程名称" />
</el-table>
</div>
</div>
</el-col>
@ -328,9 +344,12 @@ onMounted(() => {
// background-color: yellow;
padding: 5px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 11px;
// display: grid;
// grid-template-columns: repeat(2, 1fr);
// grid-gap: 11px;
}
:deep(.el-scrollbar__wrap) {
padding: 0;
}
.button {
justify-content: space-between;

@ -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…
Cancel
Save