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}); 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 * https://u-pre.boe.com/userbasic/audience/userAudiences
* 【当前代码中未查询到】获取当前用户受众信息 * 【当前代码中未查询到】获取当前用户受众信息
@@ -111,6 +119,7 @@ const selectUser = function(keyword = '') {
return ajax.postJson(baseURL,'/user/selectuser',{ keyword }); return ajax.postJson(baseURL,'/user/selectuser',{ keyword });
} }
export default { export default {
userParentOrg, userParentOrg,
findOrgsByKeyword, findOrgsByKeyword,
@@ -125,5 +134,6 @@ export default {
getUsersByIds, getUsersByIds,
updateUser, updateUser,
logout, logout,
getAllOrgTree,
selectUser selectUser
} }

View File

@@ -17,7 +17,7 @@ const hasSignup = function(courseId) {
* 课程报名,微课,录播课 * 课程报名,微课,录播课
* @param {Object} data * @param {Object} data
* { * {
courseId: courseId:
courseName: courseName:
courseType 课程类型,对应微课,录播课 courseType 课程类型,对应微课,录播课
signType:1,后名方式默认是1自主报名可以不传 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}`); 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, signup,
findSignup, findSignup,
deleteSignup, deleteSignup,
deleteNewSignUp,
importSignup, importSignup,
countSignup, countSignup,
studyIndex, studyIndex,

View File

@@ -402,7 +402,7 @@ li{
.el-message-box__header { .el-message-box__header {
// padding: 0 0 16px 0; // padding: 0 0 16px 0;
padding: 2px 8px 2px 16px; padding: 8px 8px 2px 16px;
display: block; display: block;
.el-message-box__title { .el-message-box__title {
@@ -412,7 +412,7 @@ li{
} }
.el-message-box__headerbtn { .el-message-box__headerbtn {
top: 8px; top: 14px;
right: 10px; right: 10px;
.el-message-box__close { .el-message-box__close {
font-size: 22px; font-size: 22px;
@@ -461,17 +461,47 @@ li{
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-right: 20px; padding-right: 20px;
margin-top: 0px; margin-top: 8px;
padding-bottom: 20px;
.el-button { .el-button {
width: 70px; width: 78px;
height: 32px; height: 32px;
font-size: 14px; font-size: 14px;
border-radius: 6px; border-radius: 6px;
padding: 0 18px; 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 { .el-button--default {
color: rgba(0, 0, 0, .3); color: rgba(0, 0, 0, .3);
border-color: rgba(0, 0, 0, .3); border-color: rgba(0, 0, 0, .3);

View File

@@ -1,8 +1,8 @@
<template> <template>
<el-dialog <el-dialog
class="common-course-dialog"
title="置顶排序" title="置顶排序"
:visible.sync="dialogVisible" :visible.sync="dialogVisible"
custom-class="g-dialog top-course-sorter-dialog"
width="820px" width="820px"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleClosed" @closed="handleClosed"
@@ -13,10 +13,9 @@
v-if="topList.length" v-if="topList.length"
ref="sortTable" ref="sortTable"
:data="topList" :data="topList"
class="top-course-sorter__table" class="top-course-sorter__table use-table"
row-key="id" row-key="id"
:row-class-name="getRowClassName" :row-class-name="getRowClassName"
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }"
> >
<el-table-column width="60" align="center"> <el-table-column width="60" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -262,39 +261,8 @@ export default {
min-height: 200px; min-height: 200px;
padding-top: 8px; padding-top: 8px;
} }
::v-deep .el-dialog__body {
.top-course-sorter__table { padding: 10px 20px 20px;
::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 { .drag-handle {

View File

@@ -1,20 +1,41 @@
<template> <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-tabs v-model="activeTab">
<el-tab-pane label="从受众中选择" name="quick"> <el-tab-pane label="从受众中选择" name="quick">
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="tab-search"> <div class="tab-search">
<el-input <el-select
v-model="keyword" class="member-select"
placeholder="姓名" ref="memberSelect"
size="small" v-model="memberSelected"
multiple
filterable
remote
clearable clearable
class="input" reserve-keyword
@keyup.enter.native="onSearch" placeholder="姓名"
/> :multiple-limit="5"
<el-button type="primary" size="small" @click="onSearch">查询</el-button> :remote-method="remoteSearchMember"
<el-button type="primary" size="small" @click="onReset">重置</el-button> :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> </div>
<el-table <el-table
class="use-table" class="use-table"
@@ -51,6 +72,7 @@
<script> <script>
import { fetchAudienceList, saveStu } from "@/api/signup/commonStudent"; import { fetchAudienceList, saveStu } from "@/api/signup/commonStudent";
import apiUserbasic from "@/api/boe/userbasic.js";
export default { export default {
name: "AudienceModal", name: "AudienceModal",
@@ -74,6 +96,10 @@ export default {
total: 0, total: 0,
pageNo: 1, pageNo: 1,
selectedRows: [], selectedRows: [],
// 成员远程搜索多选
memberSelected: [],
memberOptions: [],
memberLoading: false,
}; };
}, },
watch: { watch: {
@@ -89,15 +115,18 @@ export default {
}, },
methods: { methods: {
resetAndFetch() { resetAndFetch() {
this.keyword = "";
this.pageNo = 1; this.pageNo = 1;
this.selectedRows = []; this.selectedRows = [];
this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.fetchList(); this.fetchList();
}, },
fetchList() { fetchList() {
this.loading = true; this.loading = true;
fetchAudienceList({ fetchAudienceList({
keyword: this.keyword, // memberList 为多选成员 ID 列表
memberList: this.memberSelected || [],
pageNo: this.pageNo, pageNo: this.pageNo,
pageSize: this.pageSize, pageSize: this.pageSize,
audienceIdList: this.audienceIds || [], audienceIdList: this.audienceIds || [],
@@ -117,6 +146,8 @@ export default {
}, },
onReset() { onReset() {
this.keyword = ""; this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.onSearch(); this.onSearch();
}, },
onPageChange(page) { onPageChange(page) {
@@ -126,6 +157,79 @@ export default {
onSelectionChange(list) { onSelectionChange(list) {
this.selectedRows = 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() { handleClose() {
this.visibleSync = false; this.visibleSync = false;
}, },
@@ -181,29 +285,5 @@ export default {
left: 12px; 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> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="visible"> <div v-if="visible">
<el-dialog :visible="visible" :title="dialogTitle" width="1200px" top="8vh" append-to-body destroy-on-close <el-dialog class="signup-dialog common-course-dialog" :visible="visible" :title="dialogTitle" width="1200px" top="8vh" append-to-body destroy-on-close
class="signup-dialog" @close="handleClose"> @close="handleClose">
<div class="signup-wrap"> <div class="signup-wrap">
<el-tabs v-model="activeTab"> <el-tabs v-model="activeTab">
<!-- <el-tab-pane v-if="infoType" label="项目内学员" name="project"> <!-- <el-tab-pane v-if="infoType" label="项目内学员" name="project">
@@ -35,7 +35,7 @@
<el-button type="primary" size="small" @click="onSearchStu"> <el-button type="primary" size="small" @click="onSearchStu">
搜索 搜索
</el-button> </el-button>
<el-button type="primary" size="small" @click="resetStu">重置</el-button> <el-button size="small" @click="resetStu">重置</el-button>
</div> </div>
<div class="split"> <div class="split">
<div class="left-tree"> <div class="left-tree">
@@ -43,8 +43,15 @@
@node-click="onOrgSelect" /> @node-click="onOrgSelect" />
</div> </div>
<div class="table-area"> <div class="table-area">
<el-table class="use-table" border :data="stuTable.list" <el-table
@selection-change="onStuSelectionChange" :row-key="row => row.id || row.userId"> 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 type="selection" width="50" />
<el-table-column prop="realName" label="姓名" width="120" /> <el-table-column prop="realName" label="姓名" width="120" />
<el-table-column prop="userNo" label="工号" width="120" /> <el-table-column prop="userNo" label="工号" width="120" />
@@ -70,7 +77,14 @@
</el-button> </el-button>
<el-button size="small" @click="resetOrg">重置</el-button> <el-button size="small" @click="resetOrg">重置</el-button>
</div> </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" /> :load="loadOrgNodeFull" @check-change="onOrgCheckChange" :default-checked-keys="selectedOrgKeys" />
</el-tab-pane> </el-tab-pane>
@@ -83,8 +97,15 @@
</el-button> </el-button>
<el-button size="small" @click="resetAudienceInfo">重置</el-button> <el-button size="small" @click="resetAudienceInfo">重置</el-button>
</div> </div>
<el-table class="use-table" border :data="audienceTable.list" @selection-change="onAudienceSelectionChange" <el-table
:row-key="row => row.id"> 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 type="selection" width="50" />
<el-table-column prop="audienceName" label="受众名称" min-width="220" /> <el-table-column prop="audienceName" label="受众名称" min-width="220" />
<el-table-column prop="totalMember" label="总人数" width="100" /> <el-table-column prop="totalMember" label="总人数" width="100" />
@@ -104,7 +125,7 @@
<div class="already">已选</div> <div class="already">已选</div>
</div> </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="selecteds" v-if="infoType">
<div class="person">项目内学员</div> <div class="person">项目内学员</div>
<div v-for="(item, i) in projectSelectRows" :key="item.id || item.studentId"> <div v-for="(item, i) in projectSelectRows" :key="item.id || item.studentId">
@@ -309,6 +330,8 @@ export default {
visible(val) { visible(val) {
if (val) { if (val) {
this.initData(); this.initData();
} else {
this.resetState();
} }
}, },
}, },
@@ -318,6 +341,30 @@ export default {
} }
}, },
methods: { 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() { initData() {
this.courseDetail = JSON.parse(sessionStorage.getItem("courseDetail") || "{}"); this.courseDetail = JSON.parse(sessionStorage.getItem("courseDetail") || "{}");
this.projectParams = { this.projectParams = {
@@ -374,6 +421,10 @@ export default {
console.log("res", res); console.log("res", res);
this.stuTable.list = res.data?.list || []; this.stuTable.list = res.data?.list || [];
this.stuTable.total = res.data?.total || 0; this.stuTable.total = res.data?.total || 0;
// 根据右侧已选,恢复当前页的勾选状态
this.$nextTick(() => {
this.syncStuTableSelection();
});
}); });
}, },
onStuPageChange(page) { onStuPageChange(page) {
@@ -381,7 +432,22 @@ export default {
this.onSearchStu(); this.onSearchStu();
}, },
onStuSelectionChange(list) { 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() { resetStu() {
this.nameSearch = { keyword: "", departId: "" }; this.nameSearch = { keyword: "", departId: "" };
@@ -444,6 +510,10 @@ export default {
console.log('searchAudience', res); console.log('searchAudience', res);
this.audienceTable.list = res.data?.list || []; this.audienceTable.list = res.data?.list || [];
this.audienceTable.total = res.data?.total || 0; this.audienceTable.total = res.data?.total || 0;
// 根据右侧已选,恢复当前页的受众勾选状态
this.$nextTick(() => {
this.syncAudienceTableSelection();
});
}); });
}, },
onAudiencePageChange(page) { onAudiencePageChange(page) {
@@ -451,7 +521,50 @@ export default {
this.searchAudience(); this.searchAudience();
}, },
onAudienceSelectionChange(list) { 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() { resetAudienceInfo() {
this.audienceName.keyword = ""; this.audienceName.keyword = "";
@@ -467,13 +580,25 @@ export default {
this.stuSelectRows = this.stuSelectRows.filter( this.stuSelectRows = this.stuSelectRows.filter(
(i) => (i.id || i.userId) !== (item.id || item.userId) (i) => (i.id || i.userId) !== (item.id || item.userId)
); );
// 同步左侧快速选人表格的勾选状态
this.$nextTick(() => {
this.syncStuTableSelection();
});
}, },
removeOrg(item) { removeOrg(item) {
this.deptList = this.deptList.filter((i) => i.id !== item.id); this.deptList = this.deptList.filter((i) => i.id !== item.id);
this.selectedOrgKeys = this.deptList.map((d) => d.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) { removeAudience(item) {
this.auditSelectRows = this.auditSelectRows.filter((i) => i.id !== item.id); this.auditSelectRows = this.auditSelectRows.filter((i) => i.id !== item.id);
// 同步左侧受众表格的勾选状态
this.$nextTick(() => {
this.syncAudienceTableSelection();
});
}, },
submitAuth() { submitAuth() {
if (this.type === 2) { if (this.type === 2) {
@@ -524,28 +649,6 @@ export default {
background-color: #3b7cff; 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 { .signup-wrap {
display: flex; display: flex;
@@ -596,8 +699,23 @@ export default {
.right1 { .right1 {
border-left: 1px solid #f2f6fe; border-left: 1px solid #f2f6fe;
margin-left: 20px; 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 { .onerow {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -722,14 +722,18 @@ export default {
this.getSignupList(); this.getSignupList();
}, },
handleDeleteSignup(row) { 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: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
type: 'warning', type: 'warning',
customClass: 'custom-confirm-dialog' customClass: 'custom-confirm-dialog'
}).then(() => { }).then(() => {
apicourseStudy.deleteSignUp(row.id, this.courseDetail.id) apicourseStudy.deleteNewSignUp({
id: row.id,
courseId: this.courseDetail.id,
studentId: row.aid
})
.then((res) => { .then((res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
this.$showMessage("删除成功", 'success'); this.$showMessage("删除成功", 'success');

View File

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