mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-22 17:26:43 +08:00
347 lines
9.8 KiB
Vue
347 lines
9.8 KiB
Vue
<template>
|
||
<el-dialog
|
||
title="置顶排序"
|
||
:visible.sync="dialogVisible"
|
||
custom-class="g-dialog top-course-sorter-dialog"
|
||
width="820px"
|
||
:close-on-click-modal="false"
|
||
@closed="handleClosed"
|
||
append-to-body
|
||
>
|
||
<div class="top-course-sorter" v-loading="loading">
|
||
<el-table
|
||
v-if="topList.length"
|
||
ref="sortTable"
|
||
:data="topList"
|
||
class="top-course-sorter__table"
|
||
row-key="id"
|
||
:row-class-name="getRowClassName"
|
||
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }"
|
||
>
|
||
<el-table-column width="60" align="center">
|
||
<template slot-scope="scope">
|
||
<div class="drag-handle">
|
||
<i class="el-icon-s-operation"></i>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sort" label="排序" width="80" align="left">
|
||
<template slot-scope="scope">
|
||
<span class="common-cell">{{ scope.$index + 1 }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="name" label="课程名称" min-width="200">
|
||
<template slot-scope="scope">
|
||
<el-tooltip :content="scope.row.name" placement="top" :disabled="!scope.row.name">
|
||
<div class="cell-text common-cell">{{ scope.row.name }}</div>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="teacherName" label="授课教师" :min-width="teacherColumnWidth" align="center">
|
||
<template slot-scope="scope">
|
||
<span class="common-cell">{{ scope.row.teacherName || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-else-if="!loading" description="暂无置顶课程"></el-empty>
|
||
</div>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :disabled="!topList.length" :loading="saving" @click="handleSave">确定</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script>
|
||
import apiCourse from '@/api/modules/course.js';
|
||
|
||
export default {
|
||
name: 'TopCourseSorter',
|
||
computed: {
|
||
// 动态计算"授课教师"列的最小宽度
|
||
teacherColumnWidth() {
|
||
return this.calcColumnWidth('授课教师', 'teacherName');
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
dialogVisible: false,
|
||
loading: false,
|
||
saving: false,
|
||
topList: [],
|
||
draggingIndex: null,
|
||
};
|
||
},
|
||
watch: {
|
||
topList: {
|
||
handler(newVal, oldVal) {
|
||
// 只在列表长度变化或首次加载时初始化拖拽事件
|
||
if (!oldVal || newVal.length !== oldVal.length) {
|
||
this.$nextTick(() => {
|
||
this.initDragEvents();
|
||
});
|
||
}
|
||
},
|
||
immediate: false,
|
||
},
|
||
},
|
||
methods: {
|
||
open() {
|
||
this.dialogVisible = true;
|
||
this.fetchTopList();
|
||
},
|
||
async fetchTopList() {
|
||
this.loading = true;
|
||
try {
|
||
const res = await apiCourse.fetchTopCourseList();
|
||
if (res.status === 200) {
|
||
this.topList = res.result.map(item=>{
|
||
return {
|
||
...item,
|
||
// teacherName: '对对对柳在一、李玉冰、李国鑫、张添瑞、张子峰、 柳在一、李玉冰、李国鑫、张添瑞、张子峰'
|
||
}
|
||
})
|
||
} else {
|
||
this.$message.error(res.message || '获取置顶课程失败');
|
||
this.topList = [];
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(error.message || '获取置顶课程失败');
|
||
this.topList = [];
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
getRowClassName({ row, rowIndex }) {
|
||
if (this.draggingIndex === rowIndex) {
|
||
return 'is-dragging';
|
||
}
|
||
return '';
|
||
},
|
||
initDragEvents() {
|
||
if (!this.$refs.sortTable) return;
|
||
|
||
const tableBody = this.$refs.sortTable.$el.querySelector('.el-table__body-wrapper tbody');
|
||
if (!tableBody) return;
|
||
|
||
const rows = tableBody.querySelectorAll('tr');
|
||
rows.forEach((row, index) => {
|
||
// 移除旧的事件监听器
|
||
row.removeEventListener('dragstart', this.handleDragStart);
|
||
row.removeEventListener('dragover', this.handleDragOver);
|
||
row.removeEventListener('drop', this.handleDrop);
|
||
row.removeEventListener('dragend', this.handleDragEnd);
|
||
|
||
// 设置可拖拽
|
||
row.draggable = true;
|
||
row.setAttribute('data-index', index);
|
||
|
||
// 添加新的事件监听器
|
||
row.addEventListener('dragstart', (e) => this.handleDragStart(index, e));
|
||
row.addEventListener('dragover', (e) => this.handleDragOver(index, e));
|
||
row.addEventListener('drop', (e) => this.handleDrop(index, e));
|
||
row.addEventListener('dragend', () => this.handleDragEnd());
|
||
});
|
||
},
|
||
handleDragStart(index, event) {
|
||
this.draggingIndex = index;
|
||
if (event && event.dataTransfer) {
|
||
event.dataTransfer.effectAllowed = 'move';
|
||
event.dataTransfer.setData('text/plain', index);
|
||
}
|
||
},
|
||
handleDragOver(index, event) {
|
||
event.preventDefault();
|
||
if (this.draggingIndex !== null && this.draggingIndex !== index) {
|
||
const tableBody = this.$refs.sortTable.$el.querySelector('.el-table__body-wrapper tbody');
|
||
if (tableBody) {
|
||
const rows = tableBody.querySelectorAll('tr');
|
||
rows.forEach((row, idx) => {
|
||
if (idx === index) {
|
||
row.classList.add('drag-over');
|
||
} else {
|
||
row.classList.remove('drag-over');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
handleDrop(targetIndex, event) {
|
||
event.preventDefault();
|
||
if (this.draggingIndex === null || this.draggingIndex === targetIndex) {
|
||
return;
|
||
}
|
||
const movingItem = this.topList.splice(this.draggingIndex, 1)[0];
|
||
this.topList.splice(targetIndex, 0, movingItem);
|
||
this.draggingIndex = targetIndex;
|
||
|
||
// 清除拖拽样式
|
||
const tableBody = this.$refs.sortTable.$el.querySelector('.el-table__body-wrapper tbody');
|
||
if (tableBody) {
|
||
const rows = tableBody.querySelectorAll('tr');
|
||
rows.forEach((row) => {
|
||
row.classList.remove('drag-over');
|
||
});
|
||
}
|
||
|
||
// 重新初始化拖拽事件,因为 DOM 顺序已改变
|
||
this.$nextTick(() => {
|
||
this.initDragEvents();
|
||
});
|
||
},
|
||
handleDragEnd() {
|
||
this.draggingIndex = null;
|
||
// 清除所有拖拽样式
|
||
if (this.$refs.sortTable) {
|
||
const tableBody = this.$refs.sortTable.$el.querySelector('.el-table__body-wrapper tbody');
|
||
if (tableBody) {
|
||
const rows = tableBody.querySelectorAll('tr');
|
||
rows.forEach((row) => {
|
||
row.classList.remove('drag-over');
|
||
});
|
||
}
|
||
}
|
||
},
|
||
async handleSave() {
|
||
if (!this.topList.length) {
|
||
this.$message.warning('暂无需要保存的排序');
|
||
return;
|
||
}
|
||
const payload = this.topList.map((item, index) => ({
|
||
id: item.id,
|
||
sortWeight: index,
|
||
}));
|
||
this.saving = true;
|
||
try {
|
||
const res = await apiCourse.updateTopCourseSort(payload);
|
||
if (res.status === 200) {
|
||
this.$message.success('排序更新成功');
|
||
this.$emit('sorted');
|
||
this.dialogVisible = false;
|
||
} else {
|
||
throw new Error(res.message || '排序更新失败');
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(error.message || '排序更新失败');
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
handleClosed() {
|
||
this.topList = [];
|
||
this.draggingIndex = null;
|
||
this.loading = false;
|
||
this.saving = false;
|
||
},
|
||
// 计算文本宽度(通过隐藏 span 获取实际宽度)
|
||
getTextWidth(text = '') {
|
||
if (typeof document === 'undefined') return 0;
|
||
const span = document.createElement('span');
|
||
span.innerText = text;
|
||
span.style.cssText = 'position:absolute;visibility:hidden;font-size:14px;font-family:inherit;white-space:nowrap;';
|
||
document.body.appendChild(span);
|
||
const { width } = span.getBoundingClientRect();
|
||
document.body.removeChild(span);
|
||
return Math.ceil(width);
|
||
},
|
||
// 计算列宽(头+内容取最大值,加 padding,并控制最小值)
|
||
calcColumnWidth(label, prop, padding = 24, min = 130, max = Infinity) {
|
||
const contents = (this.topList || []).map(row => this.getTextWidth((row && row[prop]) || '-'));
|
||
const maxContentWidth = contents.length ? Math.max(...contents) : 0;
|
||
const labelWidth = this.getTextWidth(label || '');
|
||
const baseWidth = Math.max(maxContentWidth, labelWidth) + padding;
|
||
const clamped = Math.max(min, Math.min(baseWidth, max));
|
||
return `${clamped}px`;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.top-course-sorter {
|
||
min-height: 200px;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.top-course-sorter__table {
|
||
::v-deep .el-table__row {
|
||
cursor: move;
|
||
transition: background-color 0.2s ease;
|
||
|
||
&:hover {
|
||
background-color: #f9fbff;
|
||
}
|
||
|
||
&.is-dragging {
|
||
opacity: 0.7;
|
||
background-color: #ecf5ff;
|
||
}
|
||
|
||
&.drag-over {
|
||
border-top: 2px solid #409eff;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-table__body-wrapper {
|
||
.el-table__row {
|
||
&:last-child {
|
||
.el-table__cell {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
::v-deep .el-table__cell {
|
||
padding: 12px 0;
|
||
}
|
||
}
|
||
|
||
.drag-handle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: move;
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 0 16px;
|
||
|
||
i {
|
||
font-size: 20px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
&:hover i {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
.cell-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
width: 100%;
|
||
}
|
||
|
||
.common-cell {
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #000000;
|
||
}
|
||
|
||
.dialog-footer {
|
||
text-align: right;
|
||
}
|
||
</style>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|