Compare commits

...

25 Commits

Author SHA1 Message Date
huweihang
f2e188c0e6 feat: 新增根据关键字检索用户的API,增强用户选择功能 2025-12-17 22:50:08 +08:00
huweihang
a9b36884c3 feat: 新增获取全量机构树的API,优化SignupModal和ManageListRemote组件的组织树交互逻辑 2025-12-17 18:48:29 +08:00
huweihang
e176769174 fix: 修改删除报名记录的确认提示文本,更新删除API调用以包含学生ID 2025-12-17 17:45:18 +08:00
huweihang
5f518b713a feat: 增强报名管理功能,新增删除课程学习记录API,优化受众选择弹窗的多选逻辑和搜索功能 2025-12-17 17:44:58 +08:00
huweihang
b2395fd3a2 style: 更新对话框样式,调整按钮尺寸和间距,增强用户界面一致性 2025-12-17 11:38:36 +08:00
huweihang
100ad78c9b style: 添加滚动条样式并调整选区区域的类名以提升用户体验
(cherry picked from commit a6c389b80b)
2025-12-17 02:07:30 +08:00
huweihang
4831da0c47 feat: Add 添加SignupModal组件,并在CourseManage中集成注册功能; 2025-12-17 01:56:52 +08:00
huweihang
6bee4e8e87 style: 调整对话框和按钮尺寸,更新工具提示文本以提高清晰度,并对教师和创建者的选择实施下拉可见性处理。 2025-12-17 01:23:03 +08:00
huweihang
4ebd8f5225 refactor: 更新注册和AudienceMode组件中的受众管理功能,包括API调整和UI修改。 2025-12-17 01:22:08 +08:00
huweihang
79f068198d feat: 通过注册管理功能增强CourseManage,包括添加和删除操作,以及用于受众选择和注册创建的模块。 2025-12-17 01:21:09 +08:00
huweihang
6cfd0e8720 Merge branch '251114-feature-course-online' into 251114-feature-course-online-hwh
# Conflicts:
#	src/views/course/CourseManage.vue
2025-12-17 01:11:21 +08:00
huweihang
ed672905f2 [FIX]新增受众弹窗 2025-12-16 18:42:37 +08:00
huweihang
2f1ea68c70 [FIX]"全部"移除 2025-12-16 15:19:40 +08:00
huweihang
7a28e362f7 [FIX]全部还原 2025-12-16 15:18:46 +08:00
huweihang
e12437484b feat: Add 添加SignupModal组件,并在CourseManage中集成注册功能; 2025-12-16 01:08:10 +08:00
huweihang
78be53013b Merge branch '251114-feature-course-online' into 251114-feature-course-online-hwh 2025-12-15 21:30:54 +08:00
huweihang
c9c714ddfa style: 更新index.scss以增强按钮的颜色对比度;调整ManageListRemote.vue中的过滤器布局,优化用户体验。 2025-12-15 17:30:38 +08:00
huweihang
ffeb5bf0b6 [FIX]置顶修改、表格居中修改 2025-12-12 23:32:06 +08:00
huweihang
f680e1c394 style: 更新ManageListRemote.vue以动态计算授课教师列的最小宽度,优化输入框的最大长度限制,增强用户体验;调整index.scss以修正列表项文本对齐方式。 2025-12-12 18:13:57 +08:00
huweihang
ef926e6418 style: 调整ManageListRemote.vue中的选择器样式,优化课程管理界面的操作按钮布局,增强用户体验;更新index.scss以修正列表项内文本对齐方式。 2025-12-12 17:20:29 +08:00
huweihang
2d06dde984 style: 更新样式以优化课程管理界面,调整消息框和过滤器布局,增强用户体验。 2025-12-12 15:13:37 +08:00
huweihang
5cd85a3e15 [FIX]新增 tooltip 2025-12-12 01:48:05 +08:00
huweihang
89364b1aef [FIX]资源归属样式修改 2025-12-12 01:23:20 +08:00
huweihang
a34f87410f style: 添加自定义确认对话框样式,优化ManageListRemote.vue的过滤器布局与样式,更新课程管理相关功能以提升用户体验。 2025-12-12 01:12:54 +08:00
huweihang
361675c369 feat: 添加课程管理相关功能,新增显示置顶功能的API接口,并优化ManageListRemote.vue的过滤器布局与样式,提升用户体验。 2025-12-11 18:47:15 +08:00
8 changed files with 459 additions and 170 deletions

View File

@@ -46,6 +46,14 @@ const getUserInfoById = function(id) {
return ajax.postJson(baseURL,'/user/list',{id});
}
/**
* 获取全量机构树
* GET /userbasic/organization/all/tree
*/
const getAllOrgTree = function () {
return ajax.get(baseURL, '/organization/all/tree');
};
/**
* https://u-pre.boe.com/userbasic/audience/userAudiences
* 【当前代码中未查询到】获取当前用户受众信息
@@ -111,6 +119,7 @@ const selectUser = function(keyword = '') {
return ajax.postJson(baseURL,'/user/selectuser',{ keyword });
}
export default {
userParentOrg,
findOrgsByKeyword,
@@ -125,5 +134,6 @@ export default {
getUsersByIds,
updateUser,
logout,
getAllOrgTree,
selectUser
}

View File

@@ -17,7 +17,7 @@ const hasSignup = function(courseId) {
* 课程报名,微课,录播课
* @param {Object} data
* {
courseId:
courseId:
courseName:
courseType 课程类型,对应微课,录播课
signType:1,后名方式默认是1自主报名可以不传
@@ -367,6 +367,15 @@ const deleteSignUp=function(id,courseId){
return ajax.post(`/xboe/school/study/course/delete-signup?id=${id}&couserId=${courseId}`);
}
/*
删除课程学习记录
@param id 学习id
@param courseId 课程id 用于删除课程报名信息和修改课程学习人数
*/
const deleteNewSignUp=function(data){
return ajax.post(`/xboe/school/study/course/delete-signup`, data);
}
/**
* 二次查询 用于个人主页/他人主页
* {
@@ -432,6 +441,7 @@ export default {
signup,
findSignup,
deleteSignup,
deleteNewSignUp,
importSignup,
countSignup,
studyIndex,

View File

@@ -402,7 +402,7 @@ li{
.el-message-box__header {
// padding: 0 0 16px 0;
padding: 2px 8px 2px 16px;
padding: 8px 8px 2px 16px;
display: block;
.el-message-box__title {
@@ -412,7 +412,7 @@ li{
}
.el-message-box__headerbtn {
top: 8px;
top: 14px;
right: 10px;
.el-message-box__close {
font-size: 22px;
@@ -461,17 +461,47 @@ li{
display: flex;
justify-content: flex-end;
padding-right: 20px;
margin-top: 0px;
padding-bottom: 20px;
margin-top: 8px;
.el-button {
width: 70px;
width: 78px;
height: 32px;
font-size: 14px;
border-radius: 6px;
padding: 0 18px;
&:nth-child(2){
margin-left: 10px !important;
}
}
.el-button--default {
color: rgba(0, 0, 0, .3);
border-color: rgba(0, 0, 0, .3);
background-color: #FFFFFF;
}
.el-button--primary {
background-color: #3b7cff;
border-color: #3b7cff;
color: #fff;
}
}
}
.common-course-dialog {
.el-dialog__footer {
padding: 10px 20px 16px;
.el-button {
width: 78px;
height: 32px;
font-size: 14px;
border-radius: 6px;
padding: 0 18px;
&:nth-child(2){
margin-left: 10px !important;
}
}
.el-button--default {
color: rgba(0, 0, 0, .3);
border-color: rgba(0, 0, 0, .3);

View File

@@ -1,8 +1,8 @@
<template>
<el-dialog
class="common-course-dialog"
title="置顶排序"
:visible.sync="dialogVisible"
custom-class="g-dialog top-course-sorter-dialog"
width="820px"
:close-on-click-modal="false"
@closed="handleClosed"
@@ -13,10 +13,9 @@
v-if="topList.length"
ref="sortTable"
:data="topList"
class="top-course-sorter__table"
class="top-course-sorter__table use-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">
@@ -262,39 +261,8 @@ export default {
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;
}
::v-deep .el-dialog__body {
padding: 10px 20px 20px;
}
.drag-handle {

View File

@@ -1,20 +1,41 @@
<template>
<el-dialog title="添加报名" :visible.sync="visibleSync" width="1200px" top="8vh" append-to-body @close="handleClose">
<el-dialog title="添加报名" class="common-course-dialog" :visible.sync="visibleSync" width="1000px" top="8vh" append-to-body @close="handleClose">
<el-tabs v-model="activeTab">
<el-tab-pane label="从受众中选择" name="quick">
</el-tab-pane>
</el-tabs>
<div class="tab-search">
<el-input
v-model="keyword"
placeholder="姓名"
size="small"
<el-select
class="member-select"
ref="memberSelect"
v-model="memberSelected"
multiple
filterable
remote
clearable
class="input"
@keyup.enter.native="onSearch"
/>
<el-button type="primary" size="small" @click="onSearch">查询</el-button>
<el-button type="primary" size="small" @click="onReset">重置</el-button>
reserve-keyword
placeholder="姓名"
:multiple-limit="5"
:remote-method="remoteSearchMember"
:loading="memberLoading"
@input.native="limitMemberInput"
@visible-change="handleMemberVisibleChange"
@change="handleMemberChange"
@clear="handleMemberClear"
size="small"
>
<el-option
v-for="item in memberOptions"
:key="item.userId"
:label="item.name"
:value="item.userId"
>
<span>{{ item.name }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</el-option>
</el-select>
<el-button type="primary" size="small" @click="onSearch" style="margin-left: 8px;">查询</el-button>
<el-button size="small" @click="onReset" style="margin-left: 4px;">重置</el-button>
</div>
<el-table
class="use-table"
@@ -51,6 +72,7 @@
<script>
import { fetchAudienceList, saveStu } from "@/api/signup/commonStudent";
import apiUserbasic from "@/api/boe/userbasic.js";
export default {
name: "AudienceModal",
@@ -74,6 +96,10 @@ export default {
total: 0,
pageNo: 1,
selectedRows: [],
// 成员远程搜索多选
memberSelected: [],
memberOptions: [],
memberLoading: false,
};
},
watch: {
@@ -89,15 +115,18 @@ export default {
},
methods: {
resetAndFetch() {
this.keyword = "";
this.pageNo = 1;
this.selectedRows = [];
this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.fetchList();
},
fetchList() {
this.loading = true;
fetchAudienceList({
keyword: this.keyword,
// memberList 为多选成员 ID 列表
memberList: this.memberSelected || [],
pageNo: this.pageNo,
pageSize: this.pageSize,
audienceIdList: this.audienceIds || [],
@@ -117,6 +146,8 @@ export default {
},
onReset() {
this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.onSearch();
},
onPageChange(page) {
@@ -126,6 +157,79 @@ export default {
onSelectionChange(list) {
this.selectedRows = list;
},
// ===== 成员远程搜索,多选逻辑(参考 ManageListRemote.vue 的创建人筛选) =====
async remoteSearchMember(keyword) {
const limited = (keyword || "").slice(0, 50);
if (this.$refs.memberSelect && this.$refs.memberSelect.query !== limited) {
this.$refs.memberSelect.query = limited;
this.$nextTick(() => {
if (
this.$refs.memberSelect &&
this.$refs.memberSelect.$refs &&
this.$refs.memberSelect.$refs.input
) {
this.$refs.memberSelect.$refs.input.value = limited;
}
});
}
const query = limited.trim();
if (!query || query.length <= 1) {
this.memberOptions = [];
return;
}
this.memberLoading = true;
try {
const res = await apiUserbasic.selectUser(query);
if (res && res.status === 200) {
const resultList = res.result || [];
this.memberOptions = resultList
.map((item) => this.formatMemberItem(item))
.filter((item) => item.userId);
} else {
this.memberOptions = [];
}
} catch (error) {
this.memberOptions = [];
} finally {
this.memberLoading = false;
}
},
formatMemberItem(item = {}) {
return {
userId: item.id,
name: item.realName,
code: item.userNo,
};
},
handleMemberChange(value = []) {
// 限制最多 5 个,保持与 ManageListRemote 中创建人筛选一致
this.memberSelected = (value || []).slice(0, 5);
},
handleMemberClear() {
this.memberSelected = [];
this.memberOptions = [];
},
handleMemberVisibleChange(visible) {
if (!visible) return;
const select = this.$refs.memberSelect;
const query = (select && select.query) || "";
if (!query) {
this.memberOptions = [];
}
},
limitMemberInput(event) {
const limited =
(event && event.target && event.target.value
? event.target.value
: ""
).slice(0, 50);
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
if (this.$refs.memberSelect) {
this.$refs.memberSelect.query = limited;
}
},
handleClose() {
this.visibleSync = false;
},
@@ -181,29 +285,5 @@ export default {
left: 12px;
}
}
::v-deep .el-dialog__footer {
.el-button {
min-width: 120px;
height: 40px;
font-size: 16px;
border-radius: 6px;
padding: 0 18px;
}
.el-button--default {
color: rgba(0, 0, 0, 0.3);
border-color: rgba(0, 0, 0, 0.3);
background-color: #ffffff;
}
.el-button--primary {
background-color: #3b7cff;
border-color: #3b7cff;
color: #fff;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="visible">
<el-dialog :visible="visible" :title="dialogTitle" width="1200px" top="8vh" append-to-body destroy-on-close
class="signup-dialog" @close="handleClose">
<el-dialog class="signup-dialog common-course-dialog" :visible="visible" :title="dialogTitle" width="1200px" top="8vh" append-to-body destroy-on-close
@close="handleClose">
<div class="signup-wrap">
<el-tabs v-model="activeTab">
<!-- <el-tab-pane v-if="infoType" label="项目内学员" name="project">
@@ -35,7 +35,7 @@
<el-button type="primary" size="small" @click="onSearchStu">
搜索
</el-button>
<el-button type="primary" size="small" @click="resetStu">重置</el-button>
<el-button size="small" @click="resetStu">重置</el-button>
</div>
<div class="split">
<div class="left-tree">
@@ -43,8 +43,15 @@
@node-click="onOrgSelect" />
</div>
<div class="table-area">
<el-table class="use-table" border :data="stuTable.list"
@selection-change="onStuSelectionChange" :row-key="row => row.id || row.userId">
<el-table
ref="stuTable"
class="use-table"
border
:data="stuTable.list"
@selection-change="onStuSelectionChange"
:row-key="row => row.id || row.userId"
:reserve-selection="true"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="realName" label="姓名" width="120" />
<el-table-column prop="userNo" label="工号" width="120" />
@@ -70,7 +77,14 @@
</el-button>
<el-button size="small" @click="resetOrg">重置</el-button>
</div>
<el-tree class="org-tree" show-checkbox node-key="id" :data="orgList" :props="orgTreeProps" lazy
<el-tree
ref="orgTree"
class="org-tree"
show-checkbox
node-key="id"
:data="orgList"
:props="orgTreeProps"
lazy
:load="loadOrgNodeFull" @check-change="onOrgCheckChange" :default-checked-keys="selectedOrgKeys" />
</el-tab-pane>
@@ -83,8 +97,15 @@
</el-button>
<el-button size="small" @click="resetAudienceInfo">重置</el-button>
</div>
<el-table class="use-table" border :data="audienceTable.list" @selection-change="onAudienceSelectionChange"
:row-key="row => row.id">
<el-table
ref="audienceTable"
class="use-table"
border
:data="audienceTable.list"
@selection-change="onAudienceSelectionChange"
:row-key="row => row.id"
:reserve-selection="true"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="audienceName" label="受众名称" min-width="220" />
<el-table-column prop="totalMember" label="总人数" width="100" />
@@ -104,7 +125,7 @@
<div class="already">已选</div>
</div>
</div>
<div :style="{ 'max-height': screenHeight - 235 + 'px' }" style="overflow-y: auto">
<div class="selected-area" :style="{ 'max-height': screenHeight - 235 + 'px' }" style="overflow-y: auto">
<div class="selecteds" v-if="infoType">
<div class="person">项目内学员</div>
<div v-for="(item, i) in projectSelectRows" :key="item.id || item.studentId">
@@ -309,6 +330,8 @@ export default {
visible(val) {
if (val) {
this.initData();
} else {
this.resetState();
}
},
},
@@ -318,6 +341,30 @@ export default {
}
},
methods: {
// 参考 CommonStudent.vue在关闭弹窗时清空各种选中与搜索条件
resetState() {
this.projectParams.studentName = "";
this.nameSearch = { keyword: "", departId: "" };
this.audienceName = { keyword: "" };
this.searchOrgName = { keyword: "", pageNo: 1, pageSize: 200 };
this.projectSelectRows = [];
this.stuSelectRows = [];
this.auditSelectRows = [];
this.deptList = [];
this.selectedOrgKeys = [];
this.member = false;
this.dept = false;
this.person = false;
this.group = false;
// 清空表格勾选
if (this.$refs.stuTable && this.$refs.stuTable.clearSelection) {
this.$refs.stuTable.clearSelection();
}
if (this.$refs.audienceTable && this.$refs.audienceTable.clearSelection) {
this.$refs.audienceTable.clearSelection();
}
},
initData() {
this.courseDetail = JSON.parse(sessionStorage.getItem("courseDetail") || "{}");
this.projectParams = {
@@ -374,6 +421,10 @@ export default {
console.log("res", res);
this.stuTable.list = res.data?.list || [];
this.stuTable.total = res.data?.total || 0;
// 根据右侧已选,恢复当前页的勾选状态
this.$nextTick(() => {
this.syncStuTableSelection();
});
});
},
onStuPageChange(page) {
@@ -381,7 +432,22 @@ export default {
this.onSearchStu();
},
onStuSelectionChange(list) {
this.stuSelectRows = list;
// Element 表格在翻页时会把当前页的 selection 通过 list 传进来
// 为了保留其他页已选,这里做合并而不是直接覆盖
const currentPageIds = this.stuTable.list.map((r) => r.id || r.userId);
// 先保留非当前页的已选
const otherPageSelected = this.stuSelectRows.filter(
(r) => !currentPageIds.includes(r.id || r.userId)
);
const merged = [...otherPageSelected, ...list];
// 根据 id/userId 去重
const seen = new Set();
this.stuSelectRows = merged.filter((r) => {
const key = r.id || r.userId;
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
},
resetStu() {
this.nameSearch = { keyword: "", departId: "" };
@@ -444,6 +510,10 @@ export default {
console.log('searchAudience', res);
this.audienceTable.list = res.data?.list || [];
this.audienceTable.total = res.data?.total || 0;
// 根据右侧已选,恢复当前页的受众勾选状态
this.$nextTick(() => {
this.syncAudienceTableSelection();
});
});
},
onAudiencePageChange(page) {
@@ -451,7 +521,50 @@ export default {
this.searchAudience();
},
onAudienceSelectionChange(list) {
this.auditSelectRows = list;
const currentPageIds = this.audienceTable.list.map((r) => r.id);
const otherPageSelected = this.auditSelectRows.filter(
(r) => !currentPageIds.includes(r.id)
);
const merged = [...otherPageSelected, ...list];
const seen = new Set();
this.auditSelectRows = merged.filter((r) => {
const key = r.id;
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
},
// 根据 stuSelectRows 恢复当前页表格里的勾选
syncStuTableSelection() {
if (!this.$refs.stuTable) return;
const table = this.$refs.stuTable;
if (!table.clearSelection || !table.toggleRowSelection) return;
const selectedMap = new Set(
(this.stuSelectRows || []).map((r) => r.id || r.userId)
);
table.clearSelection();
this.stuTable.list.forEach((row) => {
const key = row.id || row.userId;
if (key && selectedMap.has(key)) {
table.toggleRowSelection(row, true);
}
});
},
// 根据 auditSelectRows 恢复当前页受众表格里的勾选
syncAudienceTableSelection() {
if (!this.$refs.audienceTable) return;
const table = this.$refs.audienceTable;
if (!table.clearSelection || !table.toggleRowSelection) return;
const selectedMap = new Set(
(this.auditSelectRows || []).map((r) => r.id)
);
table.clearSelection();
this.audienceTable.list.forEach((row) => {
const key = row.id;
if (key && selectedMap.has(key)) {
table.toggleRowSelection(row, true);
}
});
},
resetAudienceInfo() {
this.audienceName.keyword = "";
@@ -467,13 +580,25 @@ export default {
this.stuSelectRows = this.stuSelectRows.filter(
(i) => (i.id || i.userId) !== (item.id || item.userId)
);
// 同步左侧快速选人表格的勾选状态
this.$nextTick(() => {
this.syncStuTableSelection();
});
},
removeOrg(item) {
this.deptList = this.deptList.filter((i) => i.id !== item.id);
this.selectedOrgKeys = this.deptList.map((d) => d.id);
// 同步左侧组织树的勾选状态
if (this.$refs.orgTree && this.$refs.orgTree.setCheckedKeys) {
this.$refs.orgTree.setCheckedKeys(this.selectedOrgKeys);
}
},
removeAudience(item) {
this.auditSelectRows = this.auditSelectRows.filter((i) => i.id !== item.id);
// 同步左侧受众表格的勾选状态
this.$nextTick(() => {
this.syncAudienceTableSelection();
});
},
submitAuth() {
if (this.type === 2) {
@@ -524,28 +649,6 @@ export default {
background-color: #3b7cff;
}
}
.signup-dialog ::v-deep .el-dialog__footer {
.el-button {
min-width: 120px;
height: 40px;
font-size: 16px;
border-radius: 6px;
padding: 0 18px;
}
.el-button--default {
color: rgba(0, 0, 0, 0.3);
border-color: rgba(0, 0, 0, 0.3);
background-color: #ffffff;
}
.el-button--primary {
background-color: #3b7cff;
border-color: #3b7cff;
color: #fff;
}
}
.signup-wrap {
display: flex;
@@ -596,8 +699,23 @@ export default {
.right1 {
border-left: 1px solid #f2f6fe;
margin-left: 20px;
max-width: 200px;
width: 200px;
.selected-area {
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #4284F7;
}
&::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color:rgba(0, 0, 0, .1);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #4284F7
}
}
.onerow {
display: flex;
justify-content: space-between;

View File

@@ -722,14 +722,18 @@ export default {
this.getSignupList();
},
handleDeleteSignup(row) {
this.$confirm(`<i class="el-icon-warning-outline"></i>确删除${row.name || ''}的报名记录吗?`, '删除确认', {
this.$confirm(`<i class="el-icon-warning-outline"></i>确删除${row.name || ''}的报名记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(() => {
apicourseStudy.deleteSignUp(row.id, this.courseDetail.id)
apicourseStudy.deleteNewSignUp({
id: row.id,
courseId: this.courseDetail.id,
studentId: row.aid
})
.then((res) => {
if (res && res.status === 200) {
this.$showMessage("删除成功", 'success');

View File

@@ -100,9 +100,12 @@
placeholder="资源归属"
clearable
:props="resOwnerCascaderProps"
:options="resOwnerOptions"
:show-all-levels="false"
@change="handleResOwnerChange"
@input.native="limitResOwnerInput"
@clear="handleResOwnerClear"
@visible-change="handleResOwnerVisibleChange"
filterable
:filter-method="resOwnerFilterMethod"
></el-cascader>
@@ -227,7 +230,7 @@
<span class="common-cell common-cell-right">{{ scope.row.openCourse == 1 ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="资源归属" prop="orgName" min-width="220" align="center" sortable="custom">
<el-table-column label="资源归属" prop="orgName" min-width="220" align="center" sortable="custom" show-overflow-tooltip>
<template slot-scope="scope">
<el-tooltip :content="scope.row.orgFullName || scope.row.orgName" placement="top" effect="dark">
<span class="common-cell common-cell-right">{{ scope.row.orgName }}</span>
@@ -464,14 +467,23 @@ export default {
computed: {
...mapGetters(['resOwnerMap', 'sysTypeMap', 'userInfo', 'identity']),
resOwnerCascaderProps() {
// 搜索模式:关闭懒加载,直接使用全量 options
if (this.resOwnerSearchMode) {
return {
value: 'id',
label: 'name',
children: 'children',
checkStrictly: true, // 允许选择任意一级选项
};
}
// 非搜索模式:开启前端“伪懒加载”,数据从本地缓存树中按需取
return {
value: 'id',
label: 'name',
children: 'children',
lazy: true,
lazyLoad: this.loadResOwnerNode,
leaf: 'leaf',
checkStrictly: true // 允许选择任意一级选项
lazyLoad: this.loadResOwnerNodeFromCache,
checkStrictly: true, // 允许选择任意一级选项
};
},
// 动态计算“授课教师”列的最小宽度,避免固定 260px
@@ -496,7 +508,11 @@ export default {
sysTypeList: [],
resOwnerListMap: [],
resOwnerSelected: [],
resOwnerCascaderOptions: [],
// 资源归属全量树 & 级联选项
resOwnerTreeAll: [],
resOwnerOptions: [],
// 资源归属搜索模式开关(有关键字时为 true
resOwnerSearchMode: false,
showSetTopFeature: false,
page: {
pageIndex: 1,//第几页
@@ -628,6 +644,8 @@ export default {
//已经加载tree的情况下不需要再单独的加载一次
this.loadResOwners();
this.loadSysTypes();
// 加载资源归属全量树,供级联和搜索使用
this.loadAllResOwnerTree();
document.querySelector('#app').style.overflowX = 'hidden';
this.applyAppScrollbarStyle();
@@ -838,8 +856,36 @@ export default {
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
if (this.$refs.resOwnerCascader) {
this.$refs.resOwnerCascader.inputValue = limited;
const keyword = (limited || '').trim();
// 根据是否有关键字切换搜索模式
this.resOwnerSearchMode = !!keyword;
if (this.resOwnerSearchMode) {
// 搜索模式:使用全量树作为 options交给 filter-method 过滤
this.resOwnerOptions = this.resOwnerTreeAll;
} else {
// 非搜索模式:清空 options交给 lazyLoad 从缓存树按需“懒加载”
this.resOwnerOptions = [];
}
},
// 清空选择时,恢复到懒加载模式
handleResOwnerClear() {
this.resOwnerSearchMode = false;
this.resOwnerOptions = [];
// 清空输入框的文字,避免残留关键字
this.$nextTick(() => {
if (this.$refs.resOwnerCascader && this.$refs.resOwnerCascader.inputValue !== undefined) {
this.$refs.resOwnerCascader.inputValue = '';
}
});
},
// 下拉面板打开时,如果当前没有关键字,也确保是懒加载模式
handleResOwnerVisibleChange(visible) {
if (!visible) return;
// 如果没有关键字,就强制回到懒加载模式
const keyword = (this.$refs.resOwnerCascader && this.$refs.resOwnerCascader.inputValue) || '';
if (!keyword) {
this.resOwnerSearchMode = false;
this.resOwnerOptions = [];
}
},
resOwnerFilterMethod(node, keyword) {
@@ -1460,59 +1506,49 @@ export default {
sessionStorage.setItem('courseDetail', JSON.stringify(row));
this.$router.push({ path: '/iframe/course/coursemanage-remote' });
},
async loadResOwnerNode(node, resolve) {
const { level } = node;
let parentId = '';
if (level === 0) {
// 第一层:获取根节点
try {
const res = await apiUserbasic.findOrgsByKeyword('');
if (res && res.result) {
const nodes = res.result.map(item => ({
id: item.id,
name: item.name,
hrbpId: item.hrbpId,
leaf: false, // 懒加载模式下,先假设所有节点都可能有子节点
children: [] // 空数组表示需要懒加载
}));
resolve(nodes);
} else {
resolve([]);
}
} catch (error) {
console.error('加载资源归属失败:', error);
resolve([]);
// 前端“伪懒加载”:根据本地全量机构树按需返回子节点
// 为了让 el-cascader 的 loading 动画有可见效果,这里故意加入一个很小的延时再 resolve
async loadResOwnerNodeFromCache(node, resolve) {
try {
const delayResolve = (nodes) => {
// 适当延时 150ms让 loading 动画可见,但又不会让用户感觉明显卡顿
setTimeout(() => resolve(nodes), 150);
};
// 根节点level === 0返回所有一级机构
if (node.level === 0) {
return delayResolve(this.resOwnerTreeAll || []);
}
} else {
// 子节点根据父节点ID加载
parentId = node.value || (node.data && node.data.id);
if (!parentId) {
resolve([]);
return;
// 其他层级:根据当前节点 id 在树中查找对应节点,再返回其 children
const currentId = node.value || (node.data && node.data.id);
if (!currentId) {
return delayResolve([]);
}
try {
const res = await apiUserbasic.getOrgInfo(parentId);
if (res && res.status === 200 && res.result) {
let treeList = [];
if (res.result.directChildList && res.result.directChildList.length > 0) {
treeList = res.result.directChildList.map(item => ({
id: item.id,
name: item.name,
hrbpId: item.hrbpId,
leaf: false, // 懒加载模式下,先假设所有节点都可能有子节点
children: [] // 空数组表示需要懒加载
}));
const findNodeById = (list = [], id) => {
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item.id === id) {
return item;
}
if (Array.isArray(item.children) && item.children.length > 0) {
const found = findNodeById(item.children, id);
if (found) return found;
}
resolve(treeList);
} else {
// 如果没有子节点,返回空数组,级联选择器会自动将其视为叶子节点
resolve([]);
}
} catch (error) {
console.error('加载资源归属子节点失败:', error);
resolve([]);
return null;
};
const target = findNodeById(this.resOwnerTreeAll || [], currentId);
if (target && Array.isArray(target.children)) {
delayResolve(target.children);
} else {
delayResolve([]);
}
} catch (e) {
console.error('本地懒加载资源归属节点失败:', e);
resolve([]);
}
},
handleResOwnerChange(value) {
@@ -1536,6 +1572,39 @@ export default {
}
});
},
// 将后端机构节点转换为级联组件所需结构
mapOrgToCascaderNode(node = {}) {
const children = Array.isArray(node.childList)
? node.childList.map(child => this.mapOrgToCascaderNode(child))
: [];
return {
id: node.organizationId,
name: node.orgName,
children,
};
},
// 加载资源归属全量机构树
async loadAllResOwnerTree() {
try {
const res = await apiUserbasic.getAllOrgTree();
if (res && res.status === 200 && res.result && Array.isArray(res.result.orgTreeList)) {
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
// 默认进入非搜索模式,由懒加载从本地树按需返回节点
this.resOwnerOptions = [];
} else if (res && res.result && Array.isArray(res.result.orgTreeList)) {
// 兼容没有 status 字段但有 result 的情况
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
this.resOwnerOptions = [];
} else {
this.resOwnerTreeAll = [];
this.resOwnerOptions = [];
}
} catch (error) {
console.error('加载资源归属全量树失败:', error);
this.resOwnerTreeAll = [];
this.resOwnerOptions = [];
}
},
showChooseCourse() {
this.courseChooseShow = true;
},