feat: 成长路径支持多岗位关联 + 增强拖拽功能
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
前端: - 岗位选择改为多选模式 - 增强拖拽视觉反馈(高亮、动画提示) - 列表显示多个岗位标签 后端: - 添加 position_ids 字段支持多岗位 - 兼容旧版 position_id 单选数据 - 返回 position_names 数组
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user