zhangsq 7 月之前
父節點
當前提交
681ef77e84

+ 4 - 4
src/api/courseCenter/studentDetails.js

@@ -11,12 +11,12 @@ export default {
 	queryList(data) {
 		return request('disk/coursestudentrelate/page', data, 'get')
 	},
-	// 学员编辑
-	edit(data) {
-		return request('disk/coursestudentrelate/edit', data, 'post')
-	},
 	// 学员详情
 	detail(data) {
 		return request('disk/coursestudentrelate/detail', data, 'get')
+	},
+	// 学员导入
+	importStudents(data) {
+		return request('/disk/coursestudentrelate/import', data, 'post')
 	}
 }

+ 167 - 98
src/views/courseAdd/components/StudentDetails.vue

@@ -3,35 +3,48 @@
 		<!-- 搜索栏 -->
 		<div class="search-bar">
 			<a-input
-				v-model:value="searchForm.keyword"
+				v-model:value="searchForm.queryInfo"
 				placeholder="姓名/学号/手机"
 				style="width: 180px; margin-right: 12px"
 			/>
-			<a-select v-model:value="searchForm.type" placeholder="选择学生类型" style="width: 150px; margin-right: 12px">
-				<a-select-option value="">全部类型</a-select-option>
-				<a-select-option value="普通">普通</a-select-option>
-				<a-select-option value="VIP">VIP</a-select-option>
+			<a-select v-model:value="searchForm.gender" placeholder="选择学生性别" style="width: 150px; margin-right: 12px">
+				<a-select-option value="男">男</a-select-option>
+				<a-select-option value="女">女</a-select-option>
 			</a-select>
-			<a-select v-model:value="searchForm.status" placeholder="状态选项" style="width: 120px; margin-right: 12px">
-				<a-select-option value="">全部状态</a-select-option>
-				<a-select-option value="启用">启用</a-select-option>
-				<a-select-option value="禁用">禁用</a-select-option>
+			<a-select v-model:value="searchForm.userStatus" placeholder="请选择状态" style="width: 120px; margin-right: 12px">
+				<a-select-option v-for="(item, index) in statusOptions" :key="index" :value="item.value">{{
+					item.label
+				}}</a-select-option>
 			</a-select>
-			<a-range-picker v-model:value="searchForm.date" style="margin-right: 12px; width: 240px" />
+			<a-range-picker
+				@change="handleDateChange"
+				v-model:value="searchForm.date"
+				style="margin-right: 12px; width: 240px"
+			/>
 			<a-button type="primary" @click="onSearch" style="margin-right: 8px">查询</a-button>
 			<a-button @click="onReset">重置</a-button>
 		</div>
 		<!-- 操作按钮 -->
 		<div class="action-bar">
 			<a-button type="primary" @click="onAddStudent" style="margin-right: 8px">+ 新增学员</a-button>
-			<a-button>导入导出</a-button>
+			<a-upload
+				name="file"
+				:showUploadList="false"
+				:beforeUpload="beforeUpload"
+				:customRequest="customRequest"
+				accept=".xlsx,.xls"
+			>
+				<a-button>导入</a-button>
+			</a-upload>
 		</div>
 		<!-- 表格 -->
 		<a-table
 			:columns="columns"
-			:data-source="pagedData"
+			:row-key="rowKey"
+			:data-source="pagedDatas"
 			:pagination="false"
 			:row-selection="rowSelection"
+			:scroll="{ x: 1000, y: 320 }"
 			bordered
 			class="student-table"
 		>
@@ -51,8 +64,8 @@
 				<template v-else-if="column.dataIndex === 'actions'">
 					<a-space>
 						<a-button size="small" @click="onDetail(record)">详情</a-button>
-						<a-button size="small" @click="onEdit(record)">编辑</a-button>
-						<a-button size="small" @click="onSetting(record)">设置</a-button>
+						<!-- <a-button size="small" @click="onEdit(record)">编辑</a-button> -->
+						<!-- <a-button size="small" @click="onSetting(record)">设置</a-button> -->
 						<a-button size="small" danger @click="onDelete(record)">删除</a-button>
 					</a-space>
 				</template>
@@ -77,7 +90,7 @@
 			<a-pagination
 				:current="currentPage"
 				:page-size="pageSize"
-				:total="filteredData.length"
+				:total="total"
 				@change="onPageChange"
 				show-quick-jumper
 				:show-total="(total) => `共${total}条`"
@@ -97,53 +110,11 @@
 			:footer="null"
 			@cancel="handleAddStudentCancel"
 		>
-			<div class="add-student-modal">
-				<!-- 左侧部门成员树 -->
-				<div class="left-panel">
-					<a-input
-						v-model:value="searchDeptMember"
-						placeholder="输入部门或成员名称"
-						allow-clear
-						class="search-input"
-						@input="onDeptMemberSearch"
-					/>
-					<div class="dept-list">
-						<template v-for="dept in filteredDepartments" :key="dept.id">
-							<div class="dept-name" @click="toggleDept(dept.id)">
-								<DownOutlined v-if="expandedDeptIds.includes(dept.id)" style="margin-right: 4px" />
-								<RightOutlined v-else style="margin-right: 4px" />
-								{{ dept.name }}
-							</div>
-							<div v-if="expandedDeptIds.includes(dept.id)" class="member-list">
-								<a-checkbox-group :value="checkedMemberIds" @change="onMemberCheckChange">
-									<div v-for="member in dept.members" :key="member.id" class="member-item">
-										<a-checkbox :value="member.id" :disabled="isCheckedLimit(member.id)">
-											<UserOutlined style="margin-right: 4px" />{{ member.name }}
-										</a-checkbox>
-									</div>
-								</a-checkbox-group>
-							</div>
-						</template>
-					</div>
-				</div>
-				<!-- 右侧已选成员列表 -->
-				<div class="right-panel">
-					<div class="selected-header">
-						已选 {{ selectedMembers.length }}/30
-						<a @click="clearSelected" style="float: right; color: #1890ff">清空</a>
-					</div>
-					<div class="selected-list">
-						<div v-for="member in selectedMembers" :key="member.id" class="selected-item">
-							<UserOutlined style="margin-right: 4px" />{{ member.name }}
-							<CloseOutlined class="remove-icon" @click="removeSelected(member.id)" />
-						</div>
-					</div>
-				</div>
-			</div>
-			<div class="modal-footer">
-				<a-button @click="handleAddStudentCancel">取消</a-button>
-				<a-button type="primary" @click="handleAddStudentOk">确定</a-button>
-			</div>
+			<studentSelection
+				:studentIds="studentIdss"
+				@close="handleAddStudentCancel"
+				@confirm="confirmStudent"
+			></studentSelection>
 		</a-modal>
 		<!-- 详情弹窗 -->
 		<a-modal
@@ -210,11 +181,13 @@
 </template>
 
 <script setup>
-	import { ref, computed, onMounted } from 'vue'
+	import { ref, reactive, computed, onMounted, unref} from 'vue'
 	import { DownOutlined } from '@ant-design/icons-vue'
 	import { getDepartmentMembers } from '@/api/course/courseDetail'
 	import studentDetailsApi from '@/api/courseCenter/studentDetails.js'
 	import { getStudentDetail } from '@/api/course/courseDetail'
+	import studentSelection from './studentSelection.vue'
+	import tool from '@/utils/tool'
 
 	// mock数据
 	const allStudents = ref([
@@ -224,33 +197,37 @@
 			studentNo: 'xy' + String(100000 + i),
 			name: '张小刚',
 			gender: i % 2 === 0 ? '男' : '女',
-			status: '启用',
+			userStatus: '启用',
 			phone: '18088889999',
 			online: true,
 			lastLogin: '2020-11-25 23:26:08'
 		}))
 	])
 
-	const searchForm = ref({
-		keyword: '',
-		type: '',
-		status: '',
-		date: []
+	const searchForm = reactive({
+		queryInfo: null,
+		gender: null,
+		userStatus: null,
+		date: [],
+		startTime: null, // 新增开始时间字段
+		endTime: null // 新增结束时间字段
 	})
 
 	const columns = [
-		{ title: '学号', dataIndex: 'studentNo', align: 'center' },
-		{ title: '姓名', dataIndex: 'name', align: 'center' },
+		{ title: '学员编号', dataIndex: 'phone', align: 'center' },
+		{ title: '姓名', dataIndex: 'userIdName', align: 'center' },
 		{ title: '性别', dataIndex: 'gender', align: 'center' },
-		{ title: '账号状态', dataIndex: 'status', align: 'center' },
-		{ title: '手机', dataIndex: 'phone', align: 'center' },
-		{ title: '在线状态', dataIndex: 'online', align: 'center' },
-		{ title: '最后登录', dataIndex: 'lastLogin', align: 'center' },
-		{ title: '操作', dataIndex: 'actions', align: 'center', width: 220 }
+		{ title: '账号状态', dataIndex: 'userStatusName', align: 'center' },
+		{ title: '手机', dataIndex: 'phone', align: 'center' },
+		{ title: '在线状态', dataIndex: 'isLoginName', align: 'center' },
+		{ title: '最后登录', dataIndex: 'latestLoginTime', align: 'center' },
+		{ title: '操作', dataIndex: 'actions', fixed: 'right', align: 'center', width: 220 }
 	]
-
+	const studentIdss = ref([]) //学员回显id
+	const statusOptions = tool.dictList('COMMON_STATUS') //学员状态
 	const currentPage = ref(1)
 	const pageSize = ref(10)
+	const total = ref(0)
 	const selectedRowKeys = ref([])
 
 	const addStudentVisible = ref(false)
@@ -262,19 +239,21 @@
 
 	const detailVisible = ref(false)
 	const detailData = ref({})
+	const pagedDatas = ref([])
+	const rowKey = (record) => record.userId
 
 	const filteredData = computed(() => {
 		let data = allStudents.value
-		if (searchForm.value.keyword) {
+		if (searchForm.queryInfo) {
 			data = data.filter(
 				(item) =>
-					item.name.includes(searchForm.value.keyword) ||
-					item.studentNo.includes(searchForm.value.keyword) ||
-					item.phone.includes(searchForm.value.keyword)
+					item.name.includes(searchForm.queryInfo) ||
+					item.studentNo.includes(searchForm.queryInfo) ||
+					item.phone.includes(searchForm.queryInfo)
 			)
 		}
-		if (searchForm.value.status) {
-			data = data.filter((item) => item.status === searchForm.value.status)
+		if (searchForm.status) {
+			data = data.filter((item) => item.status === searchForm.status)
 		}
 		// 这里可以加更多筛选条件
 		return data
@@ -285,18 +264,47 @@
 		return filteredData.value.slice(start, start + pageSize.value)
 	})
 
-	const rowSelection = {
-		selectedRowKeys: selectedRowKeys.value,
-		onChange: (keys) => {
-			selectedRowKeys.value = keys
+	const rowSelection = computed(() => {
+		return {
+			selectedRowKeys: unref(selectedRowKeys),
+			onChange: (changeRowKeys) => {
+				console.log(changeRowKeys, 'changeRowKeyschangeRowKeys')
+				selectedRowKeys.value = changeRowKeys
+			},
+			hideDefaultSelections: true,
+		}
+	})
+	//添加学员确认
+	const confirmStudent = (StudentIds) => {
+		console.log(StudentIds, '学员id')
+		addStudentVisible.value = false
+		searchForm.StudentIds = StudentIds.join(',')
+		let params = {
+			userIds: searchForm.StudentIds
+			// courseId: pageSize.value
 		}
+		studentDetailsApi
+			.add(params)
+			.then((res) => {
+				console.log(res, '学员添加')
+				getList()
+			})
+			.catch((err) => {
+				console.log(err)
+			})
 	}
-
 	function onSearch() {
 		currentPage.value = 1
+		console.log(searchForm, '搜索参数')
+		getList()
 	}
 	const getList = () => {
-		let params = {
+		const params = {
+			queryInfo: searchForm.queryInfo,
+			gender: searchForm.gender,
+			userStatus: searchForm.userStatus,
+			latestLoginStartTime: searchForm.startTime,
+			latestLoginEndTime: searchForm.endTime,
 			current: currentPage.value,
 			size: pageSize.value
 		}
@@ -304,14 +312,66 @@
 			.queryList(params)
 			.then((res) => {
 				console.log(res, '学院列表')
+				pagedDatas.value = res.data.records
+				total.value = res.data.total
 			})
 			.catch((err) => {
 				console.log(err)
 			})
 	}
+	function handleDateChange(dates, dateStrings) {
+		// dates 是 Moment 对象数组,dateStrings 是格式化后的字符串数组
+		if (dates && dates.length === 2) {
+			searchForm.startTime = dateStrings[0] // 开始时间字符串
+			searchForm.endTime = dateStrings[1] // 结束时间字符串
+		} else {
+			searchForm.startTime = null
+			searchForm.endTime = null
+		}
+	}
+	const importLoading = ref(false)
+	// 文件上传前校验
+	const beforeUpload = (file) => {
+		const isExcel = file.type.includes('excel') || file.name.endsWith('.xlsx') || file.name.endsWith('.xls')
+		if (!isExcel) {
+			message.error('只能上传Excel文件!')
+			return false
+		}
+		return true
+	}
+	// 自定义上传实现
+	const customRequest = async (options) => {
+		const { file, onSuccess, onError } = options
+		importLoading.value = true
+		try {
+			// const formData = new FormData()
+			// formData.append('file', file)
+			let params = {
+				courseId: courseId.value,
+				file
+			}
+			// 调用API接口
+			const res = await studentDetailsApi.importStudents(params)
+			console.log(res)
+			message.success(`成功导入 ${res.data.count} 条数据`)
+			onSuccess(res, file)
+			getList() // 刷新列表
+		} catch (error) {
+			message.error(`导入失败: ${error.message}`)
+			onError(error)
+		} finally {
+			importLoading.value = false
+		}
+	}
 	function onReset() {
-		searchForm.value = { keyword: '', type: '', status: '', date: [] }
+		searchForm.queryInfo = null
+		searchForm.gender = null
+		searchForm.userStatus = null
+		searchForm.startTime = null
+		searchForm.date = null
+		searchForm.endTime = null
 		currentPage.value = 1
+		getList()
 	}
 	function onPageChange(page) {
 		currentPage.value = page
@@ -321,17 +381,17 @@
 		currentPage.value = 1
 	}
 	function onSelectAll() {
-		console.log(pagedData.value)
-		selectedRowKeys.value = pagedData.value.map((item) => item.id)
+		console.log(pagedDatas.value)
+		selectedRowKeys.value = pagedDatas.value.map((item) => item.userId)
 	}
 	function onInvertSelect() {
-		const currentIds = pagedData.value.map((item) => item.id)
+		const currentIds = pagedDatas.value.map((item) => item.userId)
 		selectedRowKeys.value = currentIds.filter((id) => !selectedRowKeys.value.includes(id))
 	}
 	function onAddStudent() {
 		addStudentVisible.value = true
 		// 默认展开所有部门
-		expandedDeptIds.value = departments.value.map((d) => d.id)
+		// expandedDeptIds.value = departments.value.map((d) => d.id)
 	}
 	function handleAddStudentCancel() {
 		addStudentVisible.value = false
@@ -400,10 +460,19 @@
 		getList()
 	})
 	function onDetail(record) {
-		getStudentDetail().then((data) => {
-			detailData.value = data
-			detailVisible.value = true
-		})
+		detailVisible.value = true
+		let params = {
+			id: record.userId
+		}
+		studentDetailsApi
+			.detail(params)
+			.then((res) => {
+				console.log(res, '学员详情')
+				detailData.value = res.data.records
+			})
+			.catch((err) => {
+				console.log(err)
+			})
 	}
 	function onEdit(record) {
 		// TODO: 编辑弹窗

+ 280 - 0
src/views/courseAdd/components/studentSelection.vue

@@ -0,0 +1,280 @@
+<template>
+	<!-- 左侧:树状结构成员列表 -->
+	<div class="left-panel">
+		<a-input-search
+			v-model:value="searchValue"
+			placeholder="输入学员名称"
+			style="margin-bottom: 16px"
+			@search="onSearch"
+		/>
+		<a-tree
+			v-if="treeData.length > 0"
+			:tree-data="filteredTreeData"
+			:field-names="{ key: 'id', title: 'name', children: 'children' }"
+			:checked-keys="checkedKeys"
+			:expanded-keys="expandedKeys"
+			:auto-expand-parent="autoExpandParent"
+			checkable
+			show-icon
+			@check="onCheck"
+			@expand="onExpand"
+		>
+			<template #title="{ name, avatar }">
+				<span style="display: inline-flex; align-items: center">
+					<a-avatar :src="avatar" size="small" style="margin-right: 8px" />
+					{{ name }}
+				</span>
+			</template>
+			<template #switcherIcon="{ expanded }">
+				<caret-down-outlined v-if="expanded" />
+				<caret-right-outlined v-else />
+			</template>
+		</a-tree>
+	</div>
+
+	<!-- 右侧:已选择成员列表 -->
+	<div class="right-panel">
+		<div class="header">
+			<span>已选 {{ selectedUsers.length }} / 30</span>
+			<a-button type="link" @click="clearSelection">清空</a-button>
+		</div>
+		<a-list item-layout="horizontal" :data-source="selectedUsers">
+			<template #renderItem="{ item }">
+				<a-list-item>
+					<a-list-item-meta>
+						<template #avatar>
+							<a-avatar :src="item.avatar" />
+						</template>
+						<template #title>
+							<a>{{ item.name }}</a>
+						</template>
+						<template #description>
+							<span>{{ item.department }}</span>
+						</template>
+					</a-list-item-meta>
+					<template #actions>
+						<a @click="removeUser(item)">删除</a>
+					</template>
+				</a-list-item>
+			</template>
+		</a-list>
+	</div>
+
+	<!-- 底部按钮 -->
+	<a-space>
+		<a-button @click="handleCancel">取消</a-button>
+		<a-button type="primary" @click="handleOk">确定</a-button>
+	</a-space>
+</template>
+
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import resourceAuditApi from '@/api/resourceAudit.js'
+	import { Modal, Input, Tree, List, Avatar, Button } from 'ant-design-vue'
+	const emit = defineEmits(['close', 'confirm'])
+	// const visible = ref(true)
+	const props = defineProps({
+		studentIds: {
+			type: Array,
+			default: () => {}
+		}
+	})
+
+	const searchValue = ref('')
+	const treeData = ref([
+		{
+			id: '1',
+			name: '组织1',
+			children: [
+				{
+					id: '1-1',
+					name: '张小刚',
+					avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
+					isLeaf: true
+				},
+				{
+					id: '1-2',
+					name: '李小红',
+					avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
+					isLeaf: true
+				}
+			]
+		},
+		{
+			id: '2',
+			name: '组织2',
+			children: [
+				{
+					id: '2-1',
+					name: '研发部',
+					children: [
+						{
+							id: '2-1-1',
+							name: '王小明',
+							avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
+							isLeaf: true
+						}
+					]
+				}
+			]
+		}
+	])
+	watch(
+		() => props.studentIds,
+		(newVal) => {
+			if (newVal) {
+				console.log(newVal, 'props.userRelateIds')
+				checkedKeys.value = newVal
+				// selectedUsers.value = flatTree(treeData.value)
+				// 	.filter((node) => newVal.includes(node.id))
+				// 	.map((node) => ({ id: node.id, name: node.name }))
+			}
+		},
+		{ deep: true }
+	)
+	const selectedKeys = ref([])
+	const selectedUsers = ref([])
+	const checkedKeys = ref([])
+	const expandedKeys = ref([]) // 默认展开第一层
+	const autoExpandParent = ref(true)
+	const filteredTreeData = computed(() => {
+		const filterFn = (node) => {
+			// 保留匹配节点及其所有祖先节点
+			if (node.name.includes(searchValue.value)) return true
+			if (node.children) {
+				const hasMatchingChild = node.children.some(filterFn)
+				if (hasMatchingChild) return true
+			}
+			return false
+		}
+
+		return treeData.value.filter(filterFn)
+	})
+
+	// 替换原来的onSelect方法
+	const onCheck = (checkedKeysValue, { checked, node, checkedNodes }) => {
+		// 过滤掉非叶子节点
+		const leafNodes = checkedNodes.filter((node) => node.isLeaf)
+		checkedKeys.value = leafNodes.map((node) => node.id)
+		selectedUsers.value = leafNodes.map((node) => ({
+			id: node.id,
+			name: node.name
+			// 可选: 保留部门信息
+			// department: findDepartmentName(node.id, treeData.value)
+		}))
+	}
+	// 查找部门名称的辅助函数
+	const findDepartmentName = (id, nodes) => {
+		for (const node of nodes) {
+			if (node.children) {
+				const found = node.children.find((child) => child.id === id)
+				if (found) return node.name
+				const result = findDepartmentName(id, node.children)
+				if (result) return result
+			}
+		}
+		return ''
+	}
+	const onExpand = (keys) => {
+		// console.log(keys, 'onExpand')
+		expandedKeys.value = keys
+		autoExpandParent.value = false
+	}
+
+	const onSearch = (value) => {
+		searchValue.value = value
+	}
+	const augmentNode = (node) => {
+		if (node.children) {
+			node.isLeaf = false // 有children的节点标记为非叶子节点
+			node.children.forEach((child) => augmentNode(child))
+		} else {
+			node.isLeaf = true // 无children的节点标记为叶子节点
+		}
+	}
+	const getOrgUserTreeRespectively = () => {
+		resourceAuditApi
+			.orgUserTreeSelector()
+			.then((res) => {
+				if (res?.data) {
+					console.log(res.data, 'getOrgUserTreeRespectively')
+					res.data.forEach((root) => augmentNode(root))
+					// if (treeData.value.length > 0 && treeData.value[0]?.id) {
+					// 	expandedKeys.value = [treeData.value[0].id]
+					// }
+					treeData.value = res.data
+				}
+			})
+			.catch((err) => {
+				console.log(err)
+			})
+	}
+
+	const clearSelection = () => {
+		selectedKeys.value = []
+		selectedUsers.value = []
+		checkedKeys.value = []
+	}
+
+	const removeUser = (user) => {
+		const index = selectedUsers.value.findIndex((u) => u.id === user.id)
+		if (index !== -1) {
+			selectedUsers.value.splice(index, 1)
+			selectedKeys.value = selectedUsers.value.map((u) => u.id)
+			checkedKeys.value = selectedUsers.value.map((u) => u.id)
+		}
+		// selectedUsers.value = selectedUsers.value.filter((u) => u.id !== user.id)
+		// checkedKeys.value = selectedUsers.value.map((u) => u.id)
+	}
+	const handleOk = () => {
+		console.log('Selected Users:', selectedUsers.value, checkedKeys.value)
+		emit('confirm', checkedKeys.value)
+	}
+
+	const handleCancel = () => {
+		emit('close')
+	}
+	onMounted(() => {
+		getOrgUserTreeRespectively()
+		// if (props.userRelateIds) {
+		// 	checkedKeys.value = props.userRelateIds // 直接赋值给树组件的checkedKeys
+		// }
+	})
+</script>
+
+<style scoped>
+	.left-panel,
+	.right-panel {
+		display: inline-block;
+		vertical-align: top;
+		width: 48%;
+		height: calc(100vh - 200px);
+		overflow-y: auto;
+		padding: 0 10px;
+	}
+
+	.right-panel {
+		border-left: 1px solid #f0f0f0;
+	}
+
+	.header {
+		display: flex;
+		justify-content: space-between;
+		margin-bottom: 16px;
+	}
+
+	.ant-tree-switcher {
+		width: 24px;
+		height: 24px;
+		line-height: 24px;
+	}
+
+	.ant-tree-switcher-icon {
+		font-size: 12px;
+		transition: transform 0.3s;
+	}
+
+	.ant-tree-switcher_close .ant-tree-switcher-icon {
+		transform: rotate(-90deg);
+	}
+</style>