@@ -117,6 +117,13 @@
< / el-form >
< / div >
<!-- 操作提示 -- >
< el-alert type = "info" :closable = "false" style = "margin-bottom: 12px" >
< template # title >
< span > 操作提示 : 从左侧拖拽课程到画布 | 拖拽节点右侧圆点可连接到其他课程 ( 箭头表示前置依赖 ) | 节点位置决定所属阶段 < / span >
< / template >
< / el-alert >
< div class = "path-designer" >
<!-- 左侧课程库 -- >
< div class = "course-library card" >
@@ -172,17 +179,22 @@
class = "canvas-content"
@dragover.prevent
@drop ="handleDrop"
@mousemove ="handleCanvasMouseMove"
@mouseup ="handleCanvasMouseUp"
ref = "canvasRef"
>
< div class = "canvas-inner" >
<!-- 阶段分隔线 -- >
< div class = "canvas-inner" ref = "canvasInnerRef" >
<!-- 阶段分隔线和区域 -- >
< div
class = "stage-divider"
v-for = "(stage, index) in editingPath.stages"
:key = "stage.name"
: style = "{ top: (index * 200 + 100) + 'px' } "
class = "stage-zone "
: style = "{ top: (index * stageHeight) + 'px', height: stageHeight + 'px' }"
: class = "{ 'stage-highlight': getStageAtY(draggedNode?.y || 0) === stage.name }"
>
< span class = "stage-label" > { { stage . name } } < / span >
< div class = "stage-divider" >
< span class = "stage-label" > { { stage . name } } < / span >
< / div >
< / div >
<!-- 空状态 -- >
@@ -191,15 +203,52 @@
< p > 拖拽课程到这里开始设计成长路径 < / p >
< / div >
<!-- 连接线 ( 箭头 ) -- >
< svg class = "connections" >
< defs >
< marker id = "arrowhead" markerWidth = "10" markerHeight = "7" refX = "9" refY = "3.5" orient = "auto" >
< polygon points = "0 0, 10 3.5, 0 7" fill = "#667eea" / >
< / marker >
< marker id = "arrowhead-drawing" markerWidth = "10" markerHeight = "7" refX = "9" refY = "3.5" orient = "auto" >
< polygon points = "0 0, 10 3.5, 0 7" fill = "#f56c6c" / >
< / marker >
< / defs >
<!-- 已建立的连接 -- >
< path
v-for = "conn in connections"
:key = "`${conn.from}-${conn.to}`"
:d = "getConnectionPath(conn)"
stroke = "#667eea"
stroke -width = " 2 "
fill = "none"
marker -end = " url ( # arrowhead ) "
class = "connection-line"
/ >
<!-- 正在绘制的连接线 -- >
< path
v-if = "isDrawingConnection && drawingLine"
:d = "drawingLine"
stroke = "#f56c6c"
stroke -width = " 2 "
stroke -dasharray = " 5 , 5 "
fill = "none"
marker -end = " url ( # arrowhead -drawing ) "
/ >
< / svg >
<!-- 路径节点 -- >
< div
v-for = "node in pathNodes"
:key = "node.id"
class = "path-node"
: class = "{ 'is-required': node.is_required, 'is-selected': selectedNode?.id === node.id }"
: class = "{
'is-required': node.is_required,
'is-selected': selectedNode?.id === node.id,
'is-dragging': draggedNode?.id === node.id,
'can-connect': isDrawingConnection && connectionStart?.id !== node.id
}"
: style = "{ left: node.x + 'px', top: node.y + 'px' }"
@mousedown ="startNodeDrag($event, node)"
@click.stop ="selectNode(node)"
@mousedown.stop ="startNodeDrag($event, node)"
>
< div class = "node-header" >
< span class = "node-title" > { { node . title } } < / span >
@@ -212,7 +261,6 @@
< el-dropdown-item command = "required" >
{ { node . is _required ? '设为选修' : '设为必修' } }
< / el-dropdown-item >
< el-dropdown-item command = "dependency" > 设置前置课程 < / el-dropdown-item >
< el-dropdown-item command = "delete" divided > 删除节点 < / el-dropdown-item >
< / el-dropdown-menu >
< / template >
@@ -222,27 +270,25 @@
< el-tag size = "small" : type = "node.is_required ? 'danger' : ''" >
{ { node . is _required ? '必修' : '选修' } }
< / el-tag >
< span class = "node-duration " > { { node . e stimated _days || 1 } } 天 < / span >
< span class = "node-stage " > { { node . stage _name } } < / span >
< / div >
<!-- 连接点 : 右侧拖出箭头 -- >
< div
class = "connection-handle output"
@mousedown.stop ="startDrawConnection($event, node)"
title = "拖拽到其他课程建立前置关系"
>
< div class = "handle-dot" > < / div >
< / div >
<!-- 连接点 : 左侧接收箭头 -- >
< div
class = "connection-handle input"
@mouseup.stop ="endDrawConnection(node)"
: class = "{ 'can-drop': isDrawingConnection && connectionStart?.id !== node.id }"
>
< div class = "handle-dot" > < / div >
< / div >
< / div >
<!-- 连接线 ( 箭头 ) -- >
< svg class = "connections" v-if = "connections.length > 0" >
< defs >
< marker id = "arrowhead" markerWidth = "10" markerHeight = "7" refX = "9" refY = "3.5" orient = "auto" >
< polygon points = "0 0, 10 3.5, 0 7" fill = "#667eea" / >
< / marker >
< / defs >
< path
v-for = "conn in connections"
:key = "`${conn.from}-${conn.to}`"
:d = "getConnectionPath(conn)"
stroke = "#667eea"
stroke -width = " 2 "
fill = "none"
marker -end = " url ( # arrowhead ) "
/ >
< / svg >
< / div >
< / div >
@@ -261,33 +307,15 @@
< span class = "stat-label" > 总学时 : < / span >
< span class = "stat-value" > { { totalDuration } } h < / span >
< / div >
< div class = "stat-item" >
< span class = "stat-label" > 连接数 : < / span >
< span class = "stat-value" > { { connections . length } } < / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / template >
<!-- 设置前置课程弹窗 -- >
< el-dialog v-model = "dependencyDialogVisible" title="设置前置课程" width="500px" >
< div class = "dependency-content" >
< p > 为课程 < strong > { { currentNode ? . title } } < / strong > 设置前置课程 : < / p >
< p class = "dependency-hint" > 选中的课程将作为当前课程的前置条件 , 用箭头连接显示 < / p >
< el-checkbox-group v-model = "selectedDependencies" >
< el -checkbox
v-for = "node in availableDependencies"
:key = "node.id"
:label = "node.id"
>
{ { node . title } }
< / el-checkbox >
< / el-checkbox-group >
< el-empty v-if = "availableDependencies.length === 0" description="暂无其他课程可选" :image-size="60" / >
< / div >
< template # footer >
< el-button @click ="dependencyDialogVisible = false" > 取消 < / el -button >
< el-button type = "primary" @click ="saveDependencies" > 确定 < / el -button >
< / template >
< / el-dialog >
< / div >
< / template >
@@ -321,7 +349,6 @@ interface Course {
name ? : string
title ? : string
duration _hours ? : number
category ? : string
}
interface PathNode {
@@ -332,7 +359,7 @@ interface PathNode {
estimated _days : number
x : number
y : number
stage _name ? : string
stage _name : string
}
interface Connection {
@@ -350,6 +377,9 @@ interface EditingPath {
is _active : boolean
}
// ========== 常量 ==========
const stageHeight = 200 // 每个阶段的高度
// ========== 状态 ==========
const loading = ref ( false )
const saving = ref ( false )
@@ -363,6 +393,7 @@ const filters = ref<{ position_id?: number }>({})
const editingPath = ref < EditingPath | null > ( null )
const courseSearch = ref ( '' )
const canvasRef = ref < HTMLElement > ( )
const canvasInnerRef = ref < HTMLElement > ( )
// 画布节点和连接
const pathNodes = ref < PathNode [ ] > ( [ ] )
@@ -373,10 +404,11 @@ const selectedNode = ref<PathNode | null>(null)
const draggedNode = ref < PathNode | null > ( null )
const dragOffset = ref ( { x : 0 , y : 0 } )
// 前置课程
const dependencyDialogVisible = ref ( false )
const currentNode = ref < PathNode | null > ( null )
const selectedDependencies = ref < string [ ] > ( [ ] )
// 连线绘制
const isDrawingConnection = ref ( false )
const connectionStart = ref < PathNode | null > ( null )
const drawingLine = ref ( '' )
const mousePos = ref ( { x : 0 , y : 0 } )
// 基础数据
const positions = ref < Position [ ] > ( [ ] )
@@ -400,11 +432,26 @@ const totalDuration = computed(() => {
} , 0 )
} )
const availableDependencies = computed ( ( ) =>
pathNodes . value . filter ( n => n . id !== currentNode . value ? . id )
)
// ========== 工具方法 ==========
// ========== 方法 ==========
/**
* 根据Y坐标获取所属阶段
*/
const getStageAtY = ( y : number ) : string => {
if ( ! editingPath . value ) return ''
const stageIndex = Math . floor ( y / stageHeight )
const clampedIndex = Math . max ( 0 , Math . min ( stageIndex , editingPath . value . stages . length - 1 ) )
return editingPath . value . stages [ clampedIndex ] ? . name || ''
}
/**
* 更新节点的阶段(根据位置)
*/
const updateNodeStage = ( node : PathNode ) => {
node . stage _name = getStageAtY ( node . y )
}
// ========== 数据加载 ==========
const loadPositions = async ( ) => {
try {
@@ -482,24 +529,26 @@ const handleEditPath = async (row: GrowthPathListItem) => {
is _active : detail . is _active ,
}
// 转换节点数据
pathNodes . value = ( detail . nodes || [ ] ) . map ( ( n , index ) => ( {
id : ` n ${ n . course _id } ` ,
course _id : n . course _id ,
title : n . title ,
is _require d: n . is _require d,
estimated _days : n . estimated _days || 1 ,
stage _name : n . stage _name ,
// 根据stage和顺序计算位置
x : 50 + ( index % 3 ) * 220 ,
y : 50 + Math . floor ( index / 3 ) * 15 0,
} ) )
// 转换节点数据 - 使用保存的位置,如果没有则自动布局
pathNodes . value = ( detail . nodes || [ ] ) . map ( ( n : any , index : number ) => {
const hasPosition = n . position _x !== undefined && n . position _x !== null && n . position _x > 0
return {
id : ` n ${ n . course _id } ` ,
course _i d : n . course _i d,
title : n . title ,
is _required : n . is _required ,
estimated _days : n . estimated _days || 1 ,
stage _name : n . stage _name || editingPath . value ! . stages [ 0 ] ? . name || '入门阶段' ,
x : hasPosition ? n . position _x : 50 + ( index % 3 ) * 22 0,
y : hasPosition ? n . position _y : 50 + Math . floor ( index / 3 ) * stageHeight ,
}
} )
// 转换前置课程为连接线
connections . value = [ ]
; ( detail . nodes || [ ] ) . forEach ( n => {
; ( detail . nodes || [ ] ) . forEach ( ( n : any ) => {
if ( n . prerequisites && n . prerequisites . length > 0 ) {
n . prerequisites . forEach ( preId => {
n . prerequisites . forEach ( ( preId : number ) => {
connections . value . push ( {
from : ` n ${ preId } ` ,
to : ` n ${ n . course _id } `
@@ -534,7 +583,7 @@ const handleSavePath = async () => {
saving . value = true
try {
// 转换节点数据
// 转换节点数据,包含位置信息
const nodes : CreateGrowthPathNode [ ] = pathNodes . value . map ( ( node , index ) => {
// 获取前置课程
const prerequisites = connections . value
@@ -548,11 +597,13 @@ const handleSavePath = async () => {
return {
course _id : node . course _id ,
title : node . title ,
stage _name : node . stage _name || editingPath . value ! . stages [ 0 ] ? . name || '入门阶段' ,
stage _name : node . stage _name ,
order _num : index + 1 ,
is _required : node . is _required ,
estimated _days : node . estimated _days ,
prerequisites ,
position _x : Math . round ( node . x ) ,
position _y : Math . round ( node . y ) ,
}
} )
@@ -628,9 +679,11 @@ const handleDrop = (event: DragEvent) => {
const rect = canvasRef . value . getBoundingClientRect ( )
const scrollLeft = canvasRef . value . scrollLeft
const scrollTop = canvasRef . value . scrollTop
const x = event . clientX - rect . left + scrollLeft - 7 5
const x = event . clientX - rect . left + scrollLeft - 8 5
const y = event . clientY - rect . top + scrollTop - 40
const stageName = getStageAtY ( Math . max ( 0 , y ) )
const newNode : PathNode = {
id : ` n ${ course . id } ` ,
course _id : course . id ,
@@ -639,16 +692,23 @@ const handleDrop = (event: DragEvent) => {
estimated _days : Math . ceil ( ( course . duration _hours || 2 ) / 2 ) ,
x : Math . max ( 0 , x ) ,
y : Math . max ( 0 , y ) ,
stage _name : stageName ,
}
pathNodes . value . push ( newNode )
ElMessage . success ( '课程添加成功' )
ElMessage . success ( ` 已添加到" ${ stageName } " ` )
}
const startNodeDrag = ( event : MouseEvent , node : PathNode ) => {
if ( ! canvasRef . value ) return
// 检查是否点击的是连接点
const target = event . target as HTMLElement
if ( target . closest ( '.connection-handle' ) ) return
draggedNode . value = node
selectedNode . value = node
const rect = canvasRef . value . getBoundingClientRect ( )
const scrollLeft = canvasRef . value . scrollLeft
const scrollTop = canvasRef . value . scrollTop
@@ -658,27 +718,150 @@ const startNodeDrag = (event: MouseEvent, node: PathNode) => {
y : event . clientY - rect . top + scrollTop - node . y
}
const handleM ouseM ove = ( e : MouseEvent ) => {
if ( ! draggedNode . value || ! canvasRef . value ) return
document . addEventListener ( 'm ousem ove' , handleNodeMouseMove )
document . addEventListener ( 'mouseup' , handleNodeMouseUp )
}
const handleNodeMouseMove = ( e : MouseEvent ) => {
if ( ! draggedNode . value || ! canvasRef . value ) return
const rect = canvasRef . value . getBoundingClientRect ( )
const scrollLeft = canvasRef . value . scrollLeft
const scrollTop = canvasRef . value . scrollTop
draggedNode . value . x = Math . max ( 0 , e . clientX - rect . left + scrollLeft - dragOffset . value . x )
draggedNode . value . y = Math . max ( 0 , e . clientY - rect . top + scrollTop - dragOffset . value . y )
// 实时更新阶段
updateNodeStage ( draggedNode . value )
}
const handleNodeMouseUp = ( ) => {
if ( draggedNode . value ) {
// 最终更新阶段
updateNodeStage ( draggedNode . value )
}
draggedNode . value = null
document . removeEventListener ( 'mousemove' , handleNodeMouseMove )
document . removeEventListener ( 'mouseup' , handleNodeMouseUp )
}
// ========== 连线绘制 ==========
const startDrawConnection = ( event : MouseEvent , node : PathNode ) => {
event . preventDefault ( )
isDrawingConnection . value = true
connectionStart . value = node
if ( canvasRef . value ) {
const rect = canvasRef . value . getBoundingClientRect ( )
const scrollLeft = canvasRef . value . scrollLeft
const scrollTop = canvasRef . value . scrollTop
draggedNode . value . x = Math . max ( 0 , e . clientX - rect . left + scrollLeft - dragOffset . value . x )
draggedNode . value . y = Math . max ( 0 , e . clientY - rect . top + scrollTop - dragOffset . value . y )
mousePos . value = {
x : event . clientX - rect . left + scrollLeft ,
y : event . clientY - rect . top + scrollTop
}
updateDrawingLine ( )
}
const handleMouseUp = ( ) => {
draggedNode . value = null
document . removeEventListener ( 'mousemove' , handleMouseMove )
document . removeEventListener ( 'mouseup' , handleMouseUp )
}
document . addEventListener ( 'mousemove' , handleMouseMove )
document . addEventListener ( 'mouseup' , handleMouseUp )
}
const selectNod e = ( node : PathNode ) => {
selectedNode . value = selectedNode . value ? . id === node . id ? null : node
const handleCanvasMouseMov e = ( event : MouseEvent ) => {
if ( ! isDrawingConnection . value || ! canvasRef . value ) return
const rect = canvasRef . value . getBoundingClientRect ( )
const scrollLeft = canvasRef . value . scrollLeft
const scrollTop = canvasRef . value . scrollTop
mousePos . value = {
x : event . clientX - rect . left + scrollLeft ,
y : event . clientY - rect . top + scrollTop
}
updateDrawingLine ( )
}
const handleCanvasMouseUp = ( ) => {
if ( isDrawingConnection . value ) {
// 取消连线
isDrawingConnection . value = false
connectionStart . value = null
drawingLine . value = ''
}
}
const endDrawConnection = ( targetNode : PathNode ) => {
if ( ! isDrawingConnection . value || ! connectionStart . value ) return
if ( connectionStart . value . id === targetNode . id ) return
// 检查是否已存在连接
const exists = connections . value . some (
c => c . from === connectionStart . value ! . id && c . to === targetNode . id
)
if ( exists ) {
ElMessage . warning ( '该连接已存在' )
} else {
// 检查是否会形成循环
const wouldCycle = checkCycle ( connectionStart . value . id , targetNode . id )
if ( wouldCycle ) {
ElMessage . warning ( '不能形成循环依赖' )
} else {
connections . value . push ( {
from : connectionStart . value . id ,
to : targetNode . id
} )
ElMessage . success ( '连接已建立' )
}
}
isDrawingConnection . value = false
connectionStart . value = null
drawingLine . value = ''
}
const checkCycle = ( fromId : string , toId : string ) : boolean => {
// 简单的循环检测:检查是否 toId 能到达 fromId
const visited = new Set < string > ( )
const queue = [ toId ]
while ( queue . length > 0 ) {
const current = queue . shift ( ) !
if ( current === fromId ) return true
if ( visited . has ( current ) ) continue
visited . add ( current )
// 找到所有从 current 出发的连接
const outgoing = connections . value . filter ( c => c . from === current )
outgoing . forEach ( c => queue . push ( c . to ) )
}
return false
}
const updateDrawingLine = ( ) => {
if ( ! connectionStart . value ) return
const startX = connectionStart . value . x + 170 // 节点右侧
const startY = connectionStart . value . y + 45 // 节点中间
const endX = mousePos . value . x
const endY = mousePos . value . y
const midX = ( startX + endX ) / 2
drawingLine . value = ` M ${ startX } ${ startY } C ${ midX } ${ startY } , ${ midX } ${ endY } , ${ endX } ${ endY } `
}
const getConnectionPath = ( conn : Connection ) => {
const fromNode = pathNodes . value . find ( n => n . id === conn . from )
const toNode = pathNodes . value . find ( n => n . id === conn . to )
if ( ! fromNode || ! toNode ) return ''
const x1 = fromNode . x + 170 // 从右侧出
const y1 = fromNode . y + 45
const x2 = toNode . x // 到左侧入
const y2 = toNode . y + 45
const midX = ( x1 + x2 ) / 2
return ` M ${ x1 } ${ y1 } C ${ midX } ${ y1 } , ${ midX } ${ y2 } , ${ x2 } ${ y2 } `
}
const handleNodeCommand = ( command : string , node : PathNode ) => {
@@ -686,13 +869,6 @@ const handleNodeCommand = (command: string, node: PathNode) => {
case 'required' :
node . is _required = ! node . is _required
break
case 'dependency' :
currentNode . value = node
selectedDependencies . value = connections . value
. filter ( c => c . to === node . id )
. map ( c => c . from )
dependencyDialogVisible . value = true
break
case 'delete' :
deleteNode ( node )
break
@@ -710,36 +886,6 @@ const deleteNode = (node: PathNode) => {
. catch ( ( ) => { } )
}
const saveDependencies = ( ) => {
if ( ! currentNode . value ) return
// 移除旧连接
connections . value = connections . value . filter ( c => c . to !== currentNode . value ! . id )
// 添加新连接
selectedDependencies . value . forEach ( fromId => {
connections . value . push ( { from : fromId , to : currentNode . value ! . id } )
} )
dependencyDialogVisible . value = false
ElMessage . success ( '前置课程设置成功' )
}
const getConnectionPath = ( conn : Connection ) => {
const fromNode = pathNodes . value . find ( n => n . id === conn . from )
const toNode = pathNodes . value . find ( n => n . id === conn . to )
if ( ! fromNode || ! toNode ) return ''
const x1 = fromNode . x + 85
const y1 = fromNode . y + 80
const x2 = toNode . x + 85
const y2 = toNode . y
const midY = ( y1 + y2 ) / 2
return ` M ${ x1 } ${ y1 } C ${ x1 } ${ midY } , ${ x2 } ${ midY } , ${ x2 } ${ y2 } `
}
const clearCanvas = ( ) => {
ElMessageBox . confirm ( '确定要清空画布吗?' , '清空确认' , { type : 'warning' } )
. then ( ( ) => {
@@ -758,15 +904,26 @@ const autoLayout = () => {
const cols = 3
const xSpacing = 220
const ySpacing = 15 0
const startX = 50
const startY = 50
const yOffset = 3 0
pathNodes . value . forEach ( ( node , index ) => {
const row = Math . floor ( index / cols )
const col = index % cols
node . x = startX + col * xSpacing
n ode. y = startY + row * ySpacing
// 按阶段分组
const stages = editingPath . value ? . stages || [ ]
stages . forEach ( ( stage , stageIndex ) => {
const stageNodes = pathNodes . value . filter ( n => n . stage _name === stage . name )
stageN odes . forEach ( ( node , nodeIndex ) => {
const col = nodeIndex % cols
const row = Math . floor ( nodeIndex / cols )
node . x = 50 + col * xSpacing
node . y = stageIndex * stageHeight + yOffset + row * 100
} )
} )
// 处理未分配阶段的节点
const unassigned = pathNodes . value . filter ( n => ! stages . some ( s => s . name === n . stage _name ) )
unassigned . forEach ( ( node , index ) => {
node . x = 50 + ( index % cols ) * xSpacing
node . y = 30 + Math . floor ( index / cols ) * 100
node . stage _name = stages [ 0 ] ? . name || '入门阶段'
} )
ElMessage . success ( '自动布局完成' )
@@ -834,7 +991,7 @@ onMounted(() => {
background : # fff ;
border - radius : 8 px ;
padding : 12 px 20 px ;
margin - bottom : 16 px ;
margin - bottom : 12 px ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ) ;
flex - shrink : 0 ;
@@ -858,7 +1015,7 @@ onMounted(() => {
}
. course - library {
width : 28 0 px ;
width : 26 0 px ;
display : flex ;
flex - direction : column ;
@@ -950,32 +1107,42 @@ onMounted(() => {
position : relative ;
width : 100 % ;
min - width : 900 px ;
min - height : 8 00px ;
min - height : 7 00px ;
background - image :
linear - gradient ( rgba ( 102 , 126 , 234 , .08 ) 1 px , transparent 1 px ) ,
linear - gradient ( 90 deg , rgba ( 102 , 126 , 234 , .08 ) 1 px , transparent 1 px ) ;
linear - gradient ( rgba ( 102 , 126 , 234 , .06 ) 1 px , transparent 1 px ) ,
linear - gradient ( 90 deg , rgba ( 102 , 126 , 234 , .06 ) 1 px , transparent 1 px ) ;
background - size : 20 px 20 px ;
}
. stage - divider {
. stage - zone {
position : absolute ;
left : 0 ;
right : 0 ;
height : 1 px ;
background : linear - gradient ( 90 deg , transparent , # 667 eea 20 % , # 667 eea 80 % , transparent ) ;
opacity : 0.3 ;
border - bottom : 1 px dashed rgba ( 102 , 126 , 234 , 0.2 ) ;
transition : background 0.2 s ;
. stage - label {
& . stage - highlight {
background : rgba ( 102 , 126 , 234 , 0.05 ) ;
}
. stage - divider {
position : absolute ;
left : 16 px ;
top : - 10 px ;
background : # fafafa ;
padding : 2 px 10 px ;
font - size : 12 px ;
color : # 667 eea ;
font - weigh t: 500 ;
border - radius : 10 px ;
border : 1 px solid rgba ( 102 , 126 , 234 , 0.3 ) ;
top : 0 ;
left : 0 ;
right : 0 ;
. stage - label {
position : absolute ;
lef t : 12 px ;
top : 8 px ;
background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 100 % ) ;
color : # fff ;
padding : 4 px 12 px ;
font - size : 12 px ;
font - weight : 500 ;
border - radius : 12 px ;
box - shadow : 0 2 px 6 px rgba ( 102 , 126 , 234 , 0.3 ) ;
}
}
}
@@ -996,7 +1163,7 @@ onMounted(() => {
. path - node {
position : absolute ;
width : 170 px ;
padding : 14 px ;
padding : 12 px 14px ;
background : # fff ;
border : 2 px solid # e4e7ed ;
border - radius : 10 px ;
@@ -1004,10 +1171,12 @@ onMounted(() => {
transition : all 0.2 s ;
user - select : none ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.08 ) ;
z - index : 10 ;
& : hover {
box - shadow : 0 4 px 16 px rgba ( 102 , 126 , 234 , 0.2 ) ;
box - shadow : 0 4 px 16 px rgba ( 102 , 126 , 234 , 0.25 ) ;
border - color : # 667 eea ;
z - index : 20 ;
}
& . is - required {
@@ -1020,11 +1189,25 @@ onMounted(() => {
box - shadow : 0 0 0 3 px rgba ( 102 , 126 , 234 , 0.2 ) ;
}
& . is - dragging {
opacity : 0.8 ;
z - index : 100 ;
}
& . can - connect {
. connection - handle . input {
. handle - dot {
transform : scale ( 1.5 ) ;
background : # 67 c23a ;
}
}
}
. node - header {
display : flex ;
justify - content : space - between ;
align - items : flex - start ;
margin - bottom : 10 px ;
margin - bottom : 8 px ;
. node - title {
font - size : 13 px ;
@@ -1044,11 +1227,54 @@ onMounted(() => {
justify - content : space - between ;
align - items : center ;
. node - duration {
font - size : 12 px ;
. node - stage {
font - size : 11 px ;
color : # 909399 ;
}
}
. connection - handle {
position : absolute ;
width : 20 px ;
height : 20 px ;
display : flex ;
align - items : center ;
justify - content : center ;
cursor : crosshair ;
& . output {
right : - 10 px ;
top : 50 % ;
transform : translateY ( - 50 % ) ;
}
& . input {
left : - 10 px ;
top : 50 % ;
transform : translateY ( - 50 % ) ;
}
& . can - drop . handle - dot {
transform : scale ( 1.5 ) ;
background : # 67 c23a ;
box - shadow : 0 0 8 px rgba ( 103 , 194 , 58 , 0.5 ) ;
}
. handle - dot {
width : 12 px ;
height : 12 px ;
background : # 667 eea ;
border - radius : 50 % ;
border : 2 px solid # fff ;
box - shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.2 ) ;
transition : all 0.2 s ;
& : hover {
transform : scale ( 1.3 ) ;
background : # f56c6c ;
}
}
}
}
. connections {
@@ -1058,7 +1284,16 @@ onMounted(() => {
width : 100 % ;
height : 100 % ;
pointer - events : none ;
z - index : 1 ;
z - index : 5 ;
. connection - line {
transition : stroke 0.2 s ;
& : hover {
stroke : # f56c6c ;
stroke - width : 3 ;
}
}
}
}
@@ -1091,26 +1326,6 @@ onMounted(() => {
}
}
}
. dependency - content {
p {
margin - bottom : 12 px ;
font - size : 14 px ;
color : # 333 ;
}
. dependency - hint {
font - size : 12 px ;
color : # 909399 ;
margin - bottom : 16 px ;
}
. el - checkbox - group {
display : flex ;
flex - direction : column ;
gap : 10 px ;
}
}
}
@ media ( max - width : 1024 px ) {
@@ -1120,7 +1335,7 @@ onMounted(() => {
. course - library {
width : 100 % ;
max - height : 25 0 px ;
max - height : 20 0 px ;
}
}
}