feat: 成长路径支持多岗位关联 + 增强拖拽功能
All checks were successful
continuous-integration/drone/push Build is passing

前端:
- 岗位选择改为多选模式
- 增强拖拽视觉反馈(高亮、动画提示)
- 列表显示多个岗位标签

后端:
- 添加 position_ids 字段支持多岗位
- 兼容旧版 position_id 单选数据
- 返回 position_names 数组
This commit is contained in:
yuliang_guo
2026-01-30 16:19:40 +08:00
parent a92bfa2b0f
commit 920c6a64c8
4 changed files with 157 additions and 24 deletions

View File

@@ -53,11 +53,19 @@
</div>
</template>
</el-table-column>
<el-table-column prop="position_name" label="关联岗位" width="150">
<el-table-column prop="position_names" label="关联岗位" min-width="180">
<template #default="{ row }">
<el-tag v-if="row.position_name" type="primary" size="small">
{{ row.position_name }}
</el-tag>
<template v-if="row.position_names && row.position_names.length > 0">
<el-tag
v-for="(name, idx) in row.position_names"
:key="idx"
type="primary"
size="small"
style="margin-right: 4px; margin-bottom: 4px;"
>
{{ name }}
</el-tag>
</template>
<span v-else class="text-muted">未关联</span>
</template>
</el-table-column>
@@ -149,8 +157,11 @@
</el-form-item>
<el-form-item label="关联岗位">
<el-select
v-model="editingPath.position_id"
placeholder="选择关联岗位(可选)"
v-model="editingPath.position_ids"
placeholder="选择关联岗位(可选)"
multiple
collapse-tags
collapse-tags-tooltip
clearable
style="width: 100%"
>
@@ -292,7 +303,9 @@
</div>
<div
class="selected-content"
@dragover.prevent
:class="{ 'is-dragging-over': isDraggingOver }"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<template v-if="editingPath.nodes && editingPath.nodes.length > 0">
@@ -421,7 +434,8 @@ interface EditingPath {
id?: number
name: string
description?: string
position_id?: number
position_id?: number // 单选(兼容旧数据)
position_ids: number[] // 多选(新数据)
stages: StageConfig[]
estimated_duration_days?: number
is_active: boolean
@@ -449,6 +463,8 @@ const filters = ref<{
// 编辑状态
const editingPath = ref<EditingPath | null>(null)
const courseSearch = ref('')
const isDragging = ref(false)
const isDraggingOver = ref(false)
// 基础数据
const positions = ref<Position[]>([])
@@ -552,6 +568,7 @@ const handleCreatePath = () => {
name: '',
description: '',
position_id: undefined,
position_ids: [],
stages: [
{ name: '入门阶段', order: 1 },
{ name: '提升阶段', order: 2 },
@@ -570,11 +587,20 @@ const handleEditPath = async (row: GrowthPathListItem) => {
loading.value = true
try {
const detail = await getGrowthPathDetail(row.id)
// 兼容旧数据:如果有 position_id 但没有 position_ids则转换
let positionIds: number[] = []
if (detail.position_ids && detail.position_ids.length > 0) {
positionIds = detail.position_ids
} else if (detail.position_id) {
positionIds = [detail.position_id]
}
editingPath.value = {
id: detail.id,
name: detail.name,
description: detail.description,
position_id: detail.position_id,
position_ids: positionIds,
stages: detail.stages || [
{ name: '入门阶段', order: 1 },
{ name: '提升阶段', order: 2 },
@@ -637,7 +663,9 @@ const handleSavePath = async () => {
const payload = {
name: editingPath.value.name,
description: editingPath.value.description,
position_id: editingPath.value.position_id,
position_ids: editingPath.value.position_ids,
// 兼容旧接口:取第一个岗位作为 position_id
position_id: editingPath.value.position_ids.length > 0 ? editingPath.value.position_ids[0] : undefined,
stages: editingPath.value.stages,
estimated_duration_days: editingPath.value.estimated_duration_days,
is_active: editingPath.value.is_active,
@@ -779,8 +807,35 @@ const handleAddCourse = (course: Course) => {
* 拖拽开始
*/
const handleDragStart = (event: DragEvent, course: Course) => {
isDragging.value = true
event.dataTransfer!.effectAllowed = 'copy'
event.dataTransfer!.setData('course', JSON.stringify(course))
// 设置拖拽图像
const target = event.target as HTMLElement
if (target) {
target.classList.add('is-dragging')
}
}
/**
* 拖拽进入目标区域
*/
const handleDragOver = (event: DragEvent) => {
event.preventDefault()
isDraggingOver.value = true
}
/**
* 拖拽离开目标区域
*/
const handleDragLeave = (event: DragEvent) => {
// 检查是否真的离开了(避免子元素触发)
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
isDraggingOver.value = false
}
}
/**
@@ -788,6 +843,9 @@ const handleDragStart = (event: DragEvent, course: Course) => {
*/
const handleDrop = (event: DragEvent) => {
event.preventDefault()
isDragging.value = false
isDraggingOver.value = false
const courseData = event.dataTransfer!.getData('course')
if (!courseData) return
@@ -1030,6 +1088,31 @@ onMounted(() => {
flex: 1;
overflow-y: auto;
padding: 12px;
transition: all 0.3s ease;
}
.selected-content {
border: 2px dashed transparent;
&.is-dragging-over {
background: #ecf5ff;
border-color: #667eea;
&::before {
content: '松开鼠标添加课程';
display: block;
text-align: center;
padding: 12px;
color: #667eea;
font-weight: 500;
animation: pulse 1s infinite;
}
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.course-item {
@@ -1040,17 +1123,29 @@ onMounted(() => {
margin-bottom: 8px;
background: #f5f7fa;
border-radius: 6px;
cursor: pointer;
cursor: grab;
transition: all 0.2s;
border: 2px solid transparent;
&:hover {
background: #e6e8eb;
transform: translateX(4px);
border-color: #667eea;
}
&:active {
cursor: grabbing;
}
&.is-dragging {
opacity: 0.5;
border-color: #667eea;
}
&.is-added {
background: #f0f9eb;
border: 1px solid #67c23a;
border-color: #67c23a;
cursor: default;
}
.course-info {