Files
UserclassFront/src/views/schoolSetting.vue
T
lijing a99b3073cf fix(ui): 页面补通道卡片骨架,白卡与分页恢复正常
- App 不统一包卡片(会与页面级卡片叠加),改由各页面根节点提供,与 Notice 一致
- 25 个视图套 .page-wrapper.yh-fill > .main-container;日志管理重写为
  .search-wrap + .table-region + .page-panel 规范结构(卡片自动转 flex 纵向)
- 新增 element_height mixin:ResizeObserver 实测 .table-region 高度喂 Table,
  替代视口高度减常量的旧模型
- self_viewport 高度基准扣除卡片留白 64px,避免表格溢出被卡片裁切
- .env.dev 恢复真实后端地址
2026-10-20 15:16:53 +08:00

673 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="page-wrap">
<Tabs v-model="activeTab">
<TabPane label="校区管理" name="campus"></TabPane>
<TabPane label="系部管理" name="dept"></TabPane>
<TabPane label="专业管理" name="major"></TabPane>
</Tabs>
<!-- ====================== 校区管理【hover显示删除】 ====================== -->
<div v-if="activeTab === 'campus'" class="tab-panel">
<div class="btn-container">
<draggable
v-model="campuslist"
item-key="name"
:animation="150"
ghost-class="drag-ghost"
class="drag-box"
group="people"
@start="drag = true"
@end="drag = false"
:move="getdata1"
@change="datadragEnd1"
>
<template #item="{ element, index }">
<div class="tag-wrap" @dblclick="openEditModal('campus', index)">
<div class="tag-btn">{{ element.name }}</div>
<span class="close-btn" @click.stop="delCampus(index)">×</span>
</div>
</template>
</draggable>
<Button icon="ios-add" size="large" class="add-btn" @click="openAddModal('campus')">添加校区</Button>
</div>
<div class="tip-info">
<Icon type="ios-information-circle-outline"></Icon>
双击编辑,拖动排序
</div>
</div>
<!-- ====================== 系部管理【hover显示删除】 ====================== -->
<div v-if="activeTab === 'dept'" class="tab-panel">
<div class="btn-container">
<draggable
v-model="divisionlist"
item-key="name"
:animation="150"
ghost-class="drag-ghost"
class="drag-box"
group="people"
@start="drag = true"
@end="drag = false"
:move="getdata2"
@change="datadragEnd2"
>
<template #item="{ element, index }">
<div class="tag-wrap" @dblclick="openEditModal('dept', index)">
<div class="tag-btn">{{ element.name }}</div>
<span class="close-btn" @click.stop="delDept(index)">×</span>
</div>
</template>
</draggable>
<Button icon="ios-add" size="large" class="add-btn" @click="openAddModal('dept')">添加系部</Button>
</div>
<div class="tip-info">
<Icon type="ios-information-circle-outline"></Icon>
双击编辑,拖动排序
</div>
</div>
<!-- ====================== 专业管理【修复拖拽失效】 ====================== -->
<div v-if="activeTab === 'major'" class="tab-panel major-adapt-wrap">
<div class="major-top-bar">
<Button type="primary" icon="ios-add" @click="openMajorAdd">添加专业</Button>
<div class="search-box">
<i-Input
v-model="majorKeyword"
placeholder="输入代码或名称搜索"
@keyup.enter="searchMajor"
>
<template #suffix><Icon type="ios-search" @click="searchMajor" style="cursor:pointer;"></Icon></template>
</i-Input>
</div>
</div>
<div class="table-scroll-box" ref="tableBoxRef">
<Table
stripe
:columns="majorColumns"
:data="majorTableData"
class="major-table"
ref="tableRef"
></Table>
</div>
<!-- <div class="pagination-fixed-bottom">
<span class="total-desc">共 {{ majorTotal }} 条</span>
<Page
v-model="majorPageNum"
:total="majorTotal"
:page-size="majorPageSize"
:page-size-opts="[10,20,50]"
show-sizer
@on-change="majorPageChange"
@on-page-size-change="majorSizeChange"
></Page>
</div> -->
</div>
<!-- 校区/系部 新增编辑弹窗 -->
<Modal v-model="modalVisible" :title="modalTitle" width="420">
<Form ref="formRef" :model="formData" :rules="formRule" :label-width="90">
<FormItem label="校区名称" v-if="modalType === 'campus'" prop="name">
<Input v-model="formData.name" placeholder="请输入校区名称" />
</FormItem>
<FormItem label="系部名称" v-if="modalType === 'dept'" prop="name">
<Input v-model="formData.name" placeholder="请输入系部名称" />
</FormItem>
</Form>
<template #footer><div align="right">
<Button @click="modalVisible = false" style="margin-right: 10px">取消</Button>
<Button type="primary" @click="submitModal">提交</Button>
</div></template>
</Modal>
<!-- 专业新增编辑弹窗 -->
<Modal v-model="majorModalVisible" :title="majorModalTitle" width="460">
<Form ref="majorFormRef" :model="majorForm" :rules="majorFormRule" :label-width="90">
<FormItem label="专业代码">
<Input v-model="majorForm.code" placeholder="请输入专业代码" />
</FormItem>
<FormItem label="专业名称" prop="name">
<Input v-model="majorForm.name" placeholder="请输入专业名称" />
</FormItem>
<FormItem label="学制" prop="schsystemLeng">
<Input v-model="majorForm.schsystemLeng" type="number" placeholder="请输入学制" />
</FormItem>
</Form>
<template #footer><div align="right">
<Button @click="majorModalVisible = false" style="margin-right: 10px">取消</Button>
<Button type="primary" @click="submitMajorModal">提交</Button>
</div></template>
</Modal>
</div>
</div>
</div>
</template>
<script>
import { Button, Icon } from "view-ui-plus";
import draggable from 'vuedraggable'
import Sortable from 'sortablejs'
export default {
name: 'SchoolSetting',
components: { draggable },
data() {
return {
activeTab: 'campus',
campuslist: [],
divisionlist: [],
modalVisible: false,
modalType: '',
modalTitle: '',
editIndex: null,
formData: { id:0,name:'' },
formRule: {
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
},
majorKeyword: '',
majorPageNum: 1,
majorPageSize: 10,
majorTotal: 22,
majorColumns: [
{
title: '序号',
key: '_index',
align: 'center',
width: 70,
render: (h, params) => h('span', params.index + 1)
},
{ title: 'ID', key: 'id', align: 'center' },
{ title: '专业代码', key: 'code', align: 'center' },
{ title: '专业名称', key: 'name', align: 'center' },
{ title: '学制', key: 'schsystemLeng', align: 'center' },
{
title: '操作',
key: 'action',
align: 'center',
width: 170,
render: (h, params) => {
return h('div', { style: 'display:flex;gap:8px;justify-content:center;align-items:center' }, [
h(Button, {type: 'text', icon: 'ios-create', size: 'small',
onClick: () => this.openMajorEdit(params.index)}),
h(Button, {type: 'text', icon: 'ios-trash', size: 'small',
onClick: () => this.delMajor(params.index)}),
// 拖拽把手class固定为 drag-handle
h(Icon, {type: 'ios-move', size: 18, title: '拖动排序',
class: 'drag-handle drag-icon'})
])
}
}
],
majorTableData: [],
majorModalVisible: false,
majorModalTitle: '',
majorEditIndex: null,
majorForm: { code: '', name: '', schsystemLeng: '' },
majorFormRule: {
code: [{ required: true, message: '专业代码不能为空', trigger: 'blur' }],
name: [{ required: true, message: '专业名称不能为空', trigger: 'blur' }],
schsystemLeng: [{ required: true, message: '学制不能为空', trigger: 'blur' }]
},
tableDrag: null
}
},
watch: {
// 切换标签页重新初始化拖拽
activeTab(val) {
if (val === 'major') {
this.$nextTick(() => this.initTableDrag())
this.getmajorslist();
} else if(val === 'campus') {
if (this.tableDrag) this.tableDrag.destroy();
this.getcampuslist();
}else{
if (this.tableDrag) this.tableDrag.destroy();
this.getdivisionlist();
}
}
},
created(){
this.getcampuslist();
},
mounted() {
// 延时确保表格完全渲染完毕再初始化
setTimeout(() => {
if (this.activeTab === 'major') {
this.initTableDrag()
}
}, 120)
},
beforeUnmount() {
if (this.tableDrag) this.tableDrag.destroy()
},
methods: {
//获取学校分校列表
getcampuslist() {
const _this = this;
var item = {};
_this.$api.get(APIConfig.BschClassSystem.getcampuslist, item, resp => {
if (resp.code == 0) {
_this.campuslist = resp.data;
} else {
_this.campuslist = [];
}
});
},
//获取系部列表
getdivisionlist() {
const _this = this;
var item = {};
_this.$api.get(APIConfig.BschClassSystem.getdivisionlist, item, resp => {
if (resp.code == 0) {
_this.divisionlist = resp.data;
} else {
_this.divisionlist = [];
}
});
},
//获取专业列表
getmajorslist() {
const _this = this;
var item = {
keyword:_this.majorKeyword
};
_this.$api.get(APIConfig.BschClassSystem.getmajorslist, item, resp => {
if (resp.code == 0) {
_this.majorTableData = resp.data;
} else {
_this.majorTableData = [];
}
});
},
//校区拖动前
getdata1 (evt) {
//console.log(evt.draggedContext.element)
},
//校区拖动后
datadragEnd1 (evt) {
var _this=this;
//console.log('拖动后的索引 :' + evt.newIndex);
//console.log('拖动前的索引 :' + evt.oldIndex);
var ids=[],orders=[];
_this.campuslist.forEach(element => {
ids.push(element.id);
});
var url=APIConfig.BschClassSystem.campussort;
_this.$api.post(url,JSON.stringify(ids),(resp)=>{
_this.$Message.info('拖动排序成功!');
_this.getcampuslist();
});
},
//系部拖动前
getdata2 (evt) {
//console.log(evt.draggedContext.element)
},
//系部拖动后
datadragEnd2 (evt) {
var _this=this;
//console.log('拖动后的索引 :' + evt.newIndex);
//console.log('拖动前的索引 :' + evt.oldIndex);
var ids=[],orders=[];
_this.divisionlist.forEach(element => {
ids.push(element.id);
});
var url=APIConfig.BschClassSystem.divisionsort;
_this.$api.post(url,JSON.stringify(ids),(resp)=>{
_this.$Message.info('拖动排序成功!');
_this.getdivisionlist();
});
},
// 表格拖拽初始化【修复关键点】
initTableDrag() {
if (this.tableDrag) {
this.tableDrag.destroy()
this.tableDrag = null
}
const tableEl = this.$refs.tableRef?.$el
if (!tableEl) return
const tbody = tableEl.querySelector('tbody')
if (!tbody) return
this.tableDrag = new Sortable(tbody, {
animation: 0, // 关闭Sortable自带DOM位移动画
swap: false,
handle: '.drag-handle',
ghostClass: 'table-drag-ghost',
preventOnFilter: true,
// 关键:Sortable只计算下标,不改动页面DOM结构
forceAutoScroll: true,
onEnd: (evt) => {
const oldIndex = evt.oldIndex
const newIndex = evt.newIndex
if (oldIndex === newIndex) return
// ========== 关键 1:撤销 Sortable 的 DOM 操作 ==========
const tbody = evt.to
const rows = Array.from(tbody.querySelectorAll('tr'))
// 计算元素原来应该插回的位置
const refIndex = oldIndex < newIndex ? oldIndex : oldIndex + 1
const refNode = rows[refIndex]
if (refNode) {
tbody.insertBefore(evt.item, refNode)
} else {
tbody.appendChild(evt.item)
}
// ========== 关键 2:用 splice 移动数组元素(不是交换!) ==========
const movedItem = this.majorTableData[oldIndex]
this.majorTableData.splice(oldIndex, 1)
this.majorTableData.splice(newIndex, 0, movedItem)
// ========== 后端排序接口 ==========
const ids = this.majorTableData.map(item => item.id)
const url = APIConfig.BschClassSystem.majorssort
this.$api.post(url, JSON.stringify(ids), (resp) => {
this.$Message.info('拖动排序成功!');
})
}
})
},
// ========== 校区删除 ==========
delCampus(index) {
this.$Modal.confirm({
title: '提示',
content: '确定删除该校区?',
onOk: () => {
var url=APIConfig.BschClassSystem.campusdelete+this.campuslist[index].id;
this.$api.post(url,{},(resp)=>{
this.$Message.success('删除成功!');
this.getcampuslist();
});
}
})
},
// ========== 系部删除 ==========
delDept(index) {
this.$Modal.confirm({
title: '提示',
content: '确定删除该系部?',
onOk: () => {
var url=APIConfig.BschClassSystem.divisiondelete+this.divisionlist[index].id;
this.$api.post(url,{},(resp)=>{
this.$Message.success('删除成功!');
this.getdivisionlist();
});
}
})
},
// ========== 校区/系部 弹窗逻辑 ==========
openAddModal(type) {
this.modalType = type;
this.editIndex = null;
this.formData.id = 0;
this.formData.name = '';
this.modalTitle = type === 'campus' ? '新增校区' : '新增系部';
this.modalVisible = true;
this.$nextTick(() => this.$refs.formRef.clearValidate());
},
openEditModal(type, index) {
this.modalType = type;
this.editIndex = index;
const list = type === 'campus' ? this.campuslist : this.divisionlist;
this.formData = list[index];
this.modalTitle = type === 'campus' ? '校区信息' : '系部信息';
this.modalVisible = true;
this.$nextTick(() => this.$refs.formRef.clearValidate());
},
submitModal() {
this.$refs.formRef.validate(valid => {
if (!valid) return
const name = this.formData.name.trim()
const list = this.modalType === 'campus' ? this.campuslist : this.divisionlist;
const hasRepeat = list.some((item, i) => item.name === name && i !== this.editIndex);
if (hasRepeat) {
this.$Message.error('名称已存在,不能重复')
return
}
if (this.editIndex === null) {
const submitData = {
name: name
};
const url =this.modalType === 'campus' ? APIConfig.BschClassSystem.campussave:APIConfig.BschClassSystem.divisionsave;
this.$api.post(url, JSON.stringify(submitData), resp => {
if (resp.code == 0) {
this.$Message.success("添加成功!");
this.modalType === 'campus' ? this.getcampuslist() : this.getdivisionlist();
} else if (resp.code == 4) {
this.$Message.info(resp.msg);
}
});
} else {
const submitData = {
id:this.formData.id,
sort:this.formData.sort,
name: name
};
const url =this.modalType === 'campus' ? APIConfig.BschClassSystem.campussave:APIConfig.BschClassSystem.divisionsave;
this.$api.post(url, JSON.stringify(submitData), resp => {
if (resp.code == 0) {
this.$Message.success("修改成功!");
this.modalType === 'campus' ? this.getcampuslist() : this.getdivisionlist();
} else if (resp.code == 4) {
this.$Message.info(resp.msg);
}
});
}
this.modalVisible = false
})
},
// ========== 专业管理 ==========
openMajorAdd() {
this.majorEditIndex = null
this.majorForm = { code: '', name: '', schsystemLeng: '' }
this.majorModalTitle = '新增专业'
this.majorModalVisible = true
this.$nextTick(() => this.$refs.majorFormRef.clearValidate())
},
openMajorEdit(index) {
this.majorEditIndex = index
const row = this.majorTableData[index];
row.schsystemLeng=this.majorTableData[index].schsystemLeng+'';
this.majorForm = { ...row }
this.$nextTick(() => this.$refs.majorFormRef.clearValidate())
this.majorModalTitle = '编辑专业'
this.majorModalVisible = true
},
submitMajorModal() {
this.$refs.majorFormRef.validate(valid => {
if (!valid) return
if (this.majorEditIndex === null) {
const submitData =this.majorForm;
const url = APIConfig.BschClassSystem.majorssave;
this.$api.post(url, JSON.stringify(submitData), resp => {
if (resp.code == 0) {
this.$Message.success("新增专业成功!");
this.getmajorslist();
} else if (resp.code == 4) {
this.$Message.info(resp.msg);
}
});
} else {
const submitData =this.majorForm;
const url = APIConfig.BschClassSystem.majorssave;
this.$api.post(url, JSON.stringify(submitData), resp => {
if (resp.code == 0) {
this.$Message.success("编辑专业成功!");
this.getmajorslist();
} else if (resp.code == 4) {
this.$Message.info(resp.msg);
}
});
}
this.majorModalVisible = false
this.$nextTick(() => this.initTableDrag())
})
},
delMajor(index) {
this.$Modal.confirm({
title: '提示',
content: '确定删除该专业?',
onOk: () => {
var url=APIConfig.BschClassSystem.majorsdelete+this.majorTableData[index].id;
this.$api.post(url,{},(resp)=>{
this.$Message.success('删除成功!');
this.getmajorslist();
});
this.$nextTick(() => this.initTableDrag())
}
})
},
searchMajor() {
this.majorPageNum = 1
this.$nextTick(() => this.initTableDrag());
this.getmajorslist();
},
majorPageChange(page) {
this.majorPageNum = page
this.$nextTick(() => this.initTableDrag())
},
majorSizeChange(size) {
this.majorPageSize = size
this.majorPageNum = 1
this.$nextTick(() => this.initTableDrag())
}
}
}
</script>
<style scoped>
.page-wrap {
padding: 16px;
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.page-title {
margin: 0 0 12px;
font-size: 18px;
font-weight: 500;
}
.tab-panel {
margin-top: 20px;
flex-shrink: 0;
}
.btn-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.drag-box {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* 悬浮删除容器 */
.tag-wrap {
position: relative;
cursor: move;
user-select: none;
}
.tag-btn {
border: 1px solid #666;
padding: 8px 22px;
border-radius: 2px;
}
.close-btn {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #f56c6c;
color: #fff;
font-size: 14px;
text-align: center;
line-height: 18px;
display: none;
cursor: pointer;
}
.tag-wrap:hover .close-btn {
display: block;
}
.add-btn {
padding: 8px 22px;
}
.tip-info {
margin-top: 30px;
color: #999;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
::v-deep .drag-ghost {
opacity: 0.4;
background: #cde4ff;
}
/* 专业布局 */
.major-adapt-wrap {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 0px;
min-height: 0;
}
.major-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
flex-shrink: 0;
background: #F7F7F7;
padding: 10px;
}
.search-box {
width: 260px;
}
.table-scroll-box {
flex: 1;
overflow-y: auto;
}
.major-table {
width: 100%;
}
.drag-icon {
cursor: move;
margin-left: 8px;
font-size: 18px;
}
/* 关键:穿透scoped,Sortable能识别把手 */
::v-deep .drag-handle {
cursor: grab !important;
}
::v-deep .table-drag-ghost {
background-color: #e8f3ff !important;
}
.pagination-fixed-bottom {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 14px;
gap: 12px;
}
.total-desc {
color: #666;
font-size: 13px;
}
</style>