Преглед на файлове

Merge branch 'dev' of http://192.168.1.245:11111/shanming/onlineEducation-front into dev

zhangsq преди 7 месеца
родител
ревизия
80112fc09e

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ NODE_ENV = development
 VITE_TITLE = Snowy
 
 # 接口地址
-VITE_API_BASEURL = http://192.168.31.14:9003
+VITE_API_BASEURL = http://192.168.31.81:19003
 # VITE_API_BASEURL = http://192.168.31.81:19003
 VITE_FILEURL = http://192.168.1.245:10005/education/
 # VITE_API_BASEURL = http://192.168.31.14:9003

+ 151 - 0
src/api/course/courseDetail.js

@@ -0,0 +1,151 @@
+// 课程详情 mock 数据
+export const mockCourseDetail = {
+	id: 1,
+	name: '课程名称',
+	status: '正常',
+	teacher: '赵小刚',
+	category: '起步小班',
+	type: '航空理论类-初级飞行训练',
+	duration: 16,
+	views: 10000,
+	updateTime: '05-22 10:49',
+	sections: [
+		{
+			id: 1,
+			title: '第一章 课程导学',
+			lessons: [
+				{
+					id: 11,
+					title: '1-1 课程简介',
+					duration: '02:30',
+					size: '300MB',
+					publishTime: '2025-07-01 10:23:59',
+					author: '张三'
+				},
+				{
+					id: 12,
+					title: '1-2 课程前瞻',
+					duration: '02:30',
+					size: '300MB',
+					publishTime: '2025-07-01 10:23:59',
+					author: '张三'
+				}
+			]
+		},
+		{
+			id: 2,
+			title: '第二章 课程XX',
+			lessons: [
+				{
+					id: 21,
+					title: '2-1 课程对题',
+					duration: '02:30',
+					size: '300MB',
+					publishTime: '2025-07-01 10:23:59',
+					author: '张三'
+				},
+				{
+					id: 22,
+					title: '2-2 课程对题',
+					duration: '02:30',
+					size: '300MB',
+					publishTime: '2025-07-01 10:23:59',
+					author: '张三'
+				},
+				{
+					id: 23,
+					title: '2-3 课程对题',
+					duration: '02:30',
+					size: '300MB',
+					publishTime: '2025-07-01 10:23:59',
+					author: '张三'
+				}
+			]
+		}
+	]
+}
+
+// 获取课程详情的 mock 方法
+export function getCourseDetail() {
+	return Promise.resolve(mockCourseDetail)
+}
+
+// 部门与成员 mock 数据
+export const mockDepartments = [
+	{
+		id: 1,
+		name: '总裁办',
+		members: [
+			{ id: 101, name: '张小刚' },
+			{ id: 102, name: '李小红' },
+			{ id: 103, name: '王小明' },
+			{ id: 104, name: '周小伟' }
+		]
+	},
+	{
+		id: 2,
+		name: '技术部',
+		members: [
+			{ id: 201, name: '技术A' },
+			{ id: 202, name: '技术B' }
+		]
+	},
+	{
+		id: 3,
+		name: '销售部',
+		members: [
+			{ id: 301, name: '销售A' },
+			{ id: 302, name: '销售B' }
+		]
+	},
+	{
+		id: 4,
+		name: '人事部',
+		members: [
+			{ id: 401, name: '人事A' },
+			{ id: 402, name: '人事B' }
+		]
+	},
+	{
+		id: 5,
+		name: '财务部',
+		members: [
+			{ id: 501, name: '财务A' },
+			{ id: 502, name: '财务B' }
+		]
+	},
+	{
+		id: 6,
+		name: '市场部',
+		members: [
+			{ id: 601, name: '市场A' },
+			{ id: 602, name: '市场B' }
+		]
+	}
+]
+
+// 获取部门与成员的 mock 方法
+export function getDepartmentMembers() {
+	return Promise.resolve(mockDepartments)
+}
+
+// 学生详情 mock 数据
+export const mockStudentDetail = {
+  id: '20208447466',
+  name: '张三',
+  phone: '18088889999',
+  gender: '男',
+  college: '院系名称',
+  className: '班级名称',
+  birthday: '1984年6月22日',
+  city: '黑龙江省哈尔滨市',
+  educationStatus: '正常',
+  onlineStatus: '离线',
+  registerTime: '2017-07-24 17:25:38',
+  lastLogin: '2020-11-24 10:00:00'
+}
+
+// 获取学生详情的 mock 方法
+export function getStudentDetail() {
+  return Promise.resolve(mockStudentDetail)
+}

+ 14 - 1
src/api/question/tQuestionApi.js

@@ -24,4 +24,17 @@ export default {
 	// 获取t_question详情
 	tQuestionDetail(data) {
 		return request('detail', data, 'get')
-	}}
+	},
+	pageList(data){
+		return request('api/admin/question/page', data, 'post')
+	},
+	edit(data){
+		return request('api/admin/question/edit', data)
+	},
+	select(id){
+		return request('api/admin/question/select/'+ id)
+	},
+	deleteQuestion(id){
+		return request('api/admin/question/delete/' + id)
+	}
+}

+ 256 - 0
src/views/courseDetails/components/AddClassHours.vue

@@ -0,0 +1,256 @@
+<template>
+	<a-modal
+		v-model:visible="modalVisible"
+		title="添加课时"
+		:footer="null"
+		width="700px"
+		@cancel="handleCancel"
+		class="add-class-hours-modal"
+	>
+		<a-form :model="form" :rules="rules" ref="formRef" layout="vertical">
+			<a-form-item label="课时名称:" name="title" required>
+				<a-input v-model:value="form.title" placeholder="输入内容" />
+			</a-form-item>
+			<a-form-item label="选择视频:" required>
+				<div class="video-select-row">
+					<a-select v-model:value="form.video" style="width: 220px; margin-right: 12px" placeholder="选择已有资源">
+						<a-select-option v-for="item in videoList" :key="item.id" :value="item.id">{{ item.name }}</a-select-option>
+					</a-select>
+					<a-upload :show-upload-list="false" :before-upload="beforeUploadVideo" :custom-request="dummyRequest">
+						<a-button type="primary">上传新资源</a-button>
+					</a-upload>
+				</div>
+			</a-form-item>
+			<a-form-item label="上传封面:" required>
+				<div class="cover-upload-row">
+					<a-upload
+						:show-upload-list="false"
+						:before-upload="beforeUploadImg"
+						:custom-request="dummyRequest"
+						accept=".jpg,.png"
+					>
+						<div class="cover-upload-box">
+							<img v-if="form.coverUrl" :src="form.coverUrl" class="cover-img" />
+							<div v-else class="cover-placeholder">
+								<PictureOutlined style="font-size: 32px; color: #bbb" />
+							</div>
+						</div>
+					</a-upload>
+					<div class="cover-tip">支持jpg、png等格式文件上传,文件大小不超过10MB</div>
+				</div>
+			</a-form-item>
+			<a-form-item label="上传讲义:">
+				<a-upload
+					:show-upload-list="false"
+					:before-upload="beforeUploadDoc"
+					:custom-request="dummyRequest"
+					accept=".ppt,.pptx,.doc,.docx,.pdf"
+				>
+					<a-button><CloudUploadOutlined /> 上传文件</a-button>
+				</a-upload>
+				<span class="upload-tip">支持ppt、word等格式文件上传,文件大小不超过10MB</span>
+			</a-form-item>
+			<a-form-item label="上传字幕:">
+				<a-upload
+					:show-upload-list="false"
+					:before-upload="beforeUploadSrt"
+					:custom-request="dummyRequest"
+					accept=".srt"
+				>
+					<a-button><CloudUploadOutlined /> 上传文件</a-button>
+				</a-upload>
+				<span class="upload-tip">仅支持srt格式文件上传,文件大小不超过1MB</span>
+			</a-form-item>
+			<div class="footer-btns">
+				<a-button @click="handleCancel">取消</a-button>
+				<a-button type="primary" @click="handleOk">确定</a-button>
+			</div>
+		</a-form>
+	</a-modal>
+</template>
+
+<script setup>
+	import { ref, reactive, watch, defineProps, defineEmits } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { PictureOutlined, CloudUploadOutlined } from '@ant-design/icons-vue'
+
+	const props = defineProps({
+		visible: Boolean
+	})
+	const emit = defineEmits(['update:visible', 'ok'])
+
+	const modalVisible = ref(props.visible)
+	watch(
+		() => props.visible,
+		(v) => {
+			modalVisible.value = v
+		}
+	)
+	watch(modalVisible, (v) => {
+		emit('update:visible', v)
+	})
+
+	const formRef = ref()
+	const form = reactive({
+		title: '',
+		video: '',
+		coverUrl: '',
+		docUrl: '',
+		srtUrl: ''
+	})
+
+	const rules = {
+		title: [{ required: true, message: '请输入课时名称' }],
+		video: [{ required: true, message: '请选择或上传视频' }],
+		coverUrl: [{ required: true, message: '请上传封面' }]
+	}
+
+	// mock视频资源
+	const videoList = ref([
+		{ id: 'v1', name: '示例视频1.mp4' },
+		{ id: 'v2', name: '示例视频2.mp4' }
+	])
+
+	function beforeUploadImg(file) {
+		const isImg = file.type === 'image/jpeg' || file.type === 'image/png'
+		const isLt10M = file.size / 1024 / 1024 < 10
+		if (!isImg) {
+			message.error('只能上传jpg/png图片')
+			return false
+		}
+		if (!isLt10M) {
+			message.error('图片不能超过10MB')
+			return false
+		}
+		// mock上传
+		const reader = new FileReader()
+		reader.onload = (e) => {
+			form.coverUrl = e.target.result
+		}
+		reader.readAsDataURL(file)
+		return false
+	}
+	function beforeUploadVideo(file) {
+		// 这里只做类型和大小校验,mock上传
+		const isVideo = file.type.startsWith('video/')
+		const isLt500M = file.size / 1024 / 1024 < 500
+		if (!isVideo) {
+			message.error('只能上传视频文件')
+			return false
+		}
+		if (!isLt500M) {
+			message.error('视频不能超过500MB')
+			return false
+		}
+		// mock添加到下拉
+		videoList.value.push({ id: 'mock_' + Date.now(), name: file.name })
+		form.video = videoList.value[videoList.value.length - 1].id
+		return false
+	}
+	function beforeUploadDoc(file) {
+		const isDoc = /\.(ppt|pptx|doc|docx|pdf)$/i.test(file.name)
+		const isLt10M = file.size / 1024 / 1024 < 10
+		if (!isDoc) {
+			message.error('仅支持ppt、word、pdf格式')
+			return false
+		}
+		if (!isLt10M) {
+			message.error('文件不能超过10MB')
+			return false
+		}
+		form.docUrl = file.name
+		return false
+	}
+	function beforeUploadSrt(file) {
+		const isSrt = file.name.endsWith('.srt')
+		const isLt1M = file.size / 1024 / 1024 < 1
+		if (!isSrt) {
+			message.error('仅支持srt格式')
+			return false
+		}
+		if (!isLt1M) {
+			message.error('文件不能超过1MB')
+			return false
+		}
+		form.srtUrl = file.name
+		return false
+	}
+	function dummyRequest({ onSuccess }) {
+		setTimeout(() => {
+			onSuccess && onSuccess()
+		}, 500)
+	}
+	function handleOk() {
+		formRef.value.validate().then(() => {
+			emit('ok', { ...form })
+			modalVisible.value = false
+		})
+	}
+	function handleCancel() {
+		modalVisible.value = false
+	}
+</script>
+
+<style lang="less" scoped>
+	.add-class-hours-modal {
+		.ant-modal-content {
+			border-radius: 10px;
+		}
+		.ant-modal-header {
+			border-radius: 10px 10px 0 0;
+		}
+		.ant-form-item {
+			margin-bottom: 24px;
+		}
+		.video-select-row {
+			display: flex;
+			align-items: center;
+		}
+		.cover-upload-row {
+			display: flex;
+			align-items: center;
+			.cover-upload-box {
+				width: 120px;
+				height: 120px;
+				background: #f7f8fa;
+				border-radius: 8px;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				margin-right: 24px;
+				border: 1px dashed #d9d9d9;
+				cursor: pointer;
+				.cover-img {
+					width: 100%;
+					height: 100%;
+					object-fit: cover;
+					border-radius: 8px;
+				}
+				.cover-placeholder {
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					width: 100%;
+					height: 100%;
+					color: #bbb;
+					font-size: 32px;
+				}
+			}
+			.cover-tip {
+				color: #888;
+				font-size: 13px;
+			}
+		}
+		.upload-tip {
+			color: #888;
+			font-size: 13px;
+			margin-left: 12px;
+		}
+		.footer-btns {
+			display: flex;
+			justify-content: flex-end;
+			gap: 16px;
+			margin-top: 24px;
+		}
+	}
+</style>

+ 187 - 0
src/views/courseDetails/components/EditCourse.vue

@@ -0,0 +1,187 @@
+<template>
+	<a-form
+		:model="form"
+		:rules="rules"
+		ref="formRef"
+		:label-col="{ span: 5 }"
+		:wrapper-col="{ span: 18 }"
+		labelAlign="left"
+		class="edit-course-form"
+	>
+		<a-form-item label="工单标题" name="title" required>
+			<a-input v-model:value="form.title" placeholder="工单标题" />
+		</a-form-item>
+		<a-form-item label="关联客户" name="customer" required>
+			<a-select v-model:value="form.customer" placeholder="选择客户">
+				<a-select-option v-for="item in customerList" :key="item.value" :value="item.value">{{
+					item.label
+				}}</a-select-option>
+			</a-select>
+		</a-form-item>
+		<a-form-item label="产品" name="product">
+			<a-select v-model:value="form.product" placeholder="选择订单">
+				<a-select-option v-for="item in productList" :key="item.value" :value="item.value">{{
+					item.label
+				}}</a-select-option>
+			</a-select>
+		</a-form-item>
+		<a-form-item label="工单类型" name="type" required>
+			<a-select v-model:value="form.type" placeholder="选择类型">
+				<a-select-option v-for="item in typeList" :key="item.value" :value="item.value">{{
+					item.label
+				}}</a-select-option>
+			</a-select>
+		</a-form-item>
+		<a-form-item label="情况说明" name="desc">
+			<a-textarea v-model:value="form.desc" :maxlength="300" :rows="4" placeholder="请输入情况说明" show-count />
+		</a-form-item>
+		<a-form-item label="信息来源" name="source">
+			<a-select v-model:value="form.source" placeholder="选择信息来源">
+				<a-select-option v-for="item in sourceList" :key="item.value" :value="item.value">{{
+					item.label
+				}}</a-select-option>
+			</a-select>
+		</a-form-item>
+		<a-form-item label="上传附件" name="attachments">
+			<a-upload
+				v-model:file-list="form.attachments"
+				:before-upload="beforeUpload"
+				:multiple="true"
+				:list-type="picture - card"
+				:show-upload-list="{ showPreviewIcon: false }"
+				:custom-request="dummyRequest"
+				:accept="'.jpg,.png'"
+			>
+				<a-button>上传文件</a-button>
+			</a-upload>
+			<div class="upload-tip">请上传jpg、png图片附件,最多可上传4张,单张大小不要超过8M。</div>
+		</a-form-item>
+		<a-form-item label="联系人" name="contact" required>
+			<a-input v-model:value="form.contact" placeholder="联系人" />
+		</a-form-item>
+		<a-form-item label="联系电话" name="phone">
+			<a-input v-model:value="form.phone" placeholder="联系电话" />
+		</a-form-item>
+		<a-form-item label="所属区域" name="region">
+			<a-select v-model:value="form.region" placeholder="请选择">
+				<a-select-option v-for="item in regionList" :key="item.value" :value="item.value">{{
+					item.label
+				}}</a-select-option>
+			</a-select>
+		</a-form-item>
+		<a-form-item label="详细地址" name="address">
+			<a-input v-model:value="form.address" placeholder="详细地址" />
+		</a-form-item>
+		<a-form-item style="text-align: right; margin-top: 32px">
+			<a-button @click="emit('close')" style="margin-right: 16px">取消</a-button>
+			<a-button type="primary" @click="onSubmit">保存</a-button>
+		</a-form-item>
+	</a-form>
+</template>
+
+<script setup>
+	import { ref, watch } from 'vue'
+	const props = defineProps({
+		course: { type: Object, default: () => ({}) }
+	})
+	const emit = defineEmits(['close'])
+
+	const formRef = ref()
+	const form = ref({
+		title: '',
+		customer: '',
+		product: '',
+		type: '',
+		desc: '',
+		source: '',
+		attachments: [],
+		contact: '',
+		phone: '',
+		region: '',
+		address: ''
+	})
+
+	// mock 下拉数据
+	const customerList = [
+		{ label: '张三公司', value: 'zhangsan' },
+		{ label: '李四企业', value: 'lisi' }
+	]
+	const productList = [
+		{ label: '订单A', value: 'a' },
+		{ label: '订单B', value: 'b' }
+	]
+	const typeList = [
+		{ label: '售后', value: 'after' },
+		{ label: '投诉', value: 'complain' }
+	]
+	const sourceList = [
+		{ label: '电话', value: 'phone' },
+		{ label: '微信', value: 'wechat' }
+	]
+	const regionList = [
+		{ label: '浙江舟山', value: 'zhoushan' },
+		{ label: '杭州', value: 'hangzhou' }
+	]
+
+	watch(
+		() => props.course,
+		(val) => {
+			form.value = {
+				...form.value,
+				...val
+			}
+		},
+		{ deep: true, immediate: true }
+	)
+
+	// 校验规则
+	const rules = {
+		title: [{ required: true, message: '请输入工单标题' }],
+		customer: [{ required: true, message: '请选择客户' }],
+		type: [{ required: true, message: '请选择工单类型' }],
+		contact: [{ required: true, message: '请输入联系人' }]
+	}
+
+	// 附件上传限制
+	function beforeUpload(file, fileList) {
+		const isImg = file.type === 'image/jpeg' || file.type === 'image/png'
+		const isLt8M = file.size / 1024 / 1024 < 8
+		if (!isImg) {
+			window.$message?.error('只能上传jpg/png图片')
+			return false
+		}
+		if (!isLt8M) {
+			window.$message?.error('单张图片不能超过8M')
+			return false
+		}
+		if (form.value.attachments.length + fileList.length > 4) {
+			window.$message?.error('最多上传4张图片')
+			return false
+		}
+		return true
+	}
+	// mock上传
+	function dummyRequest({ onSuccess }) {
+		setTimeout(() => {
+			onSuccess && onSuccess()
+		}, 500)
+	}
+
+	function onSubmit() {
+		formRef.value.validate().then(() => {
+			console.log('保存数据', form.value)
+			emit('close')
+		})
+	}
+</script>
+
+<style lang="less" scoped>
+	.edit-course-form {
+		padding-top: 24px;
+		.upload-tip {
+			color: #888;
+			font-size: 12px;
+			margin-top: 4px;
+		}
+	}
+</style>

+ 303 - 0
src/views/courseDetails/components/LearningStatistics.vue

@@ -0,0 +1,303 @@
+<template>
+	<div class="learning-statistics">
+		<div class="stat-picker">
+			<a-range-picker v-model:value="dateRange" style="width: 240px" />
+		</div>
+		<!-- 顶部统计卡片 -->
+		<div class="stat-cards">
+			<a-card class="stat-card">
+				<div class="stat-title">学习人数</div>
+				<div class="stat-value">{{ statData.studentCount }}<span class="stat-unit">人</span></div>
+				<div class="stat-desc">较上周 +1000</div>
+			</a-card>
+			<a-card class="stat-card">
+				<div class="stat-title">学习总时长</div>
+				<div class="stat-value">{{ statData.totalStudyTime }}<span class="stat-unit">小时</span></div>
+				<div class="stat-desc">较上周 -200</div>
+			</a-card>
+			<a-card class="stat-card">
+				<div class="stat-title">人均学习时长</div>
+				<div class="stat-value">{{ statData.avgStudyTime }}<span class="stat-unit">小时</span></div>
+				<div class="stat-desc">较上周 +40</div>
+			</a-card>
+			<a-card class="stat-card">
+				<div class="stat-title">完课率</div>
+				<div class="stat-value">{{ statData.completionRate }}<span class="stat-unit">%</span></div>
+				<div class="stat-desc">较上周 -200</div>
+			</a-card>
+		</div>
+
+		<!-- 学习人数趋势图 -->
+		<div class="chart-section">
+			<a-card class="chart-card">
+				<div class="chart-title">学习人数趋势</div>
+				<div ref="mainChartRef" style="height: 260px; width: 100%"></div>
+				<div class="chart-legend">
+					<span class="legend-item blue">学习人数</span>
+					<span class="legend-item">学习时长</span>
+					<span class="legend-item">人均学习时长</span>
+					<span class="legend-item">完课率</span>
+				</div>
+			</a-card>
+		</div>
+
+		<!-- 学习时段平均人数分布 -->
+		<div class="chart-section">
+			<a-card class="chart-card">
+				<div class="chart-title">学习时段平均人数分布</div>
+				<div ref="timeChartRef" style="height: 220px; width: 100%"></div>
+				<div class="chart-legend">
+					<span class="legend-item blue">学习人数</span>
+				</div>
+			</a-card>
+		</div>
+
+		<!-- 用户学习数据表格 -->
+		<div class="user-table-section">
+			<div class="table-toolbar">
+				<a-input-search
+					v-model:value="searchValue"
+					placeholder="姓名/学号/手机"
+					style="width: 200px; margin-right: 12px"
+				/>
+				<a-select v-model:value="selectedClass" placeholder="选择学生班别" style="width: 160px; 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>
+				<a-select v-model:value="selectedStatus" 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>
+				<a-button type="primary" style="margin-right: 8px">导出</a-button>
+				<a-button>重置</a-button>
+			</div>
+			<a-table
+				:columns="columns"
+				:data-source="tableData"
+				:pagination="pagination"
+				row-key="id"
+				bordered
+				size="middle"
+				style="margin-top: 16px"
+			>
+				<template #bodyCell="{ column }">
+					<template v-if="column.key === 'action'">
+						<a-space>
+							<a-button type="link" size="small">详情</a-button>
+							<a-button type="link" size="small">编辑</a-button>
+							<a-button type="link" size="small">导出</a-button>
+							<a-button type="link" size="small" danger>删除</a-button>
+						</a-space>
+					</template>
+				</template>
+			</a-table>
+		</div>
+	</div>
+</template>
+
+<script setup>
+	import { ref, onMounted } from 'vue'
+	import * as echarts from 'echarts'
+
+	const statData = ref({
+		studentCount: 900,
+		totalStudyTime: 1800,
+		avgStudyTime: 2.0,
+		completionRate: 20
+	})
+	const dateRange = ref([])
+
+	const mainChartRef = ref(null)
+	const timeChartRef = ref(null)
+
+	const mainChartOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 40, bottom: 40 },
+		xAxis: {
+			type: 'category',
+			data: ['03-01', '03-02', '03-03', '03-04', '03-05', '03-06', '03-07']
+		},
+		yAxis: { type: 'value' },
+		series: [
+			{
+				name: '学习人数',
+				type: 'line',
+				data: [200, 220, 210, 500, 230, 210, 200],
+				smooth: true,
+				areaStyle: { color: 'rgba(52,122,255,0.08)' },
+				lineStyle: { color: '#347aff', width: 3 },
+				itemStyle: { color: '#347aff' },
+				markLine: {
+					data: [{ xAxis: '03-04' }],
+					lineStyle: { color: '#347aff', type: 'dashed' },
+					label: { show: false }
+				},
+				symbolSize: 8
+			}
+		]
+	}
+
+	const timeChartOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 40, bottom: 40 },
+		xAxis: {
+			type: 'category',
+			data: ['00:00', '00:04', '00:08', '00:12', '00:16', '00:20', '00:24']
+		},
+		yAxis: { type: 'value' },
+		series: [
+			{
+				name: '学习人数',
+				type: 'line',
+				data: [1000, 1200, 1100, 1520, 1400, 1200, 1000],
+				smooth: true,
+				areaStyle: { color: 'rgba(52,122,255,0.08)' },
+				lineStyle: { color: '#347aff', width: 3 },
+				itemStyle: { color: '#347aff' },
+				markLine: {
+					data: [{ xAxis: '00:12' }],
+					lineStyle: { color: '#347aff', type: 'dashed' },
+					label: { show: false }
+				},
+				symbolSize: 8
+			}
+		]
+	}
+
+	const searchValue = ref('')
+	const selectedClass = ref('')
+	const selectedStatus = ref('')
+
+	const columns = [
+		{ title: '学号/账号', dataIndex: 'id', key: 'id', align: 'center' },
+		{ title: '姓名', dataIndex: 'name', key: 'name', align: 'center' },
+		{ title: '进度', dataIndex: 'progress', key: 'progress', align: 'center' },
+		{ title: '速率', dataIndex: 'rate', key: 'rate', align: 'center' },
+		{ title: '今日学习时长', dataIndex: 'todayTime', key: 'todayTime', align: 'center' },
+		{ title: '累计学习时长', dataIndex: 'totalTime', key: 'totalTime', align: 'center' },
+		{ title: '首次学习时间', dataIndex: 'firstTime', key: 'firstTime', align: 'center' },
+		{ title: '最近登录', dataIndex: 'lastLogin', key: 'lastLogin', align: 'center' },
+		{ title: '操作', key: 'action', align: 'center' }
+	]
+
+	const tableData = ref(
+		Array.from({ length: 10 }).map((_, i) => ({
+			id: `xy00000${i + 1}`,
+			name: '张小刚',
+			progress: '20%',
+			rate: '20%',
+			todayTime: '1h30min',
+			totalTime: '13h30min',
+			firstTime: '2020-11-25 23:26:08',
+			lastLogin: '2020-11-25 23:26:08'
+		}))
+	)
+
+	const pagination = {
+		pageSize: 10,
+		total: 50,
+		showTotal: (total) => `共${total}条`,
+		showQuickJumper: true,
+		showSizeChanger: true
+	}
+
+	onMounted(() => {
+		if (mainChartRef.value) {
+			const chart = echarts.init(mainChartRef.value)
+			chart.setOption(mainChartOption)
+		}
+		if (timeChartRef.value) {
+			const chart = echarts.init(timeChartRef.value)
+			chart.setOption(timeChartOption)
+		}
+	})
+</script>
+
+<style lang="less" scoped>
+	.learning-statistics {
+		width: 1200px;
+		margin: 0 auto;
+		height: calc(100vh - 400px);
+		overflow: auto;
+		.stat-cards {
+			display: flex;
+			align-items: flex-start;
+			margin-bottom: 24px;
+			.stat-card {
+				flex: 1;
+				margin-right: 16px;
+				border-radius: 12px;
+				box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+				border: none;
+				.stat-title {
+					font-size: 16px;
+					color: #888;
+					margin-bottom: 8px;
+				}
+				.stat-value {
+					font-size: 32px;
+					font-weight: 600;
+					color: #222;
+					.stat-unit {
+						font-size: 16px;
+						margin-left: 4px;
+					}
+				}
+				.stat-desc {
+					color: #aaa;
+					font-size: 13px;
+					margin-top: 8px;
+				}
+			}
+			.stat-card:last-child {
+				margin-right: 0;
+			}
+		}
+		.stat-picker {
+			display: flex;
+			flex-direction: row-reverse;
+			margin: 10px 0;
+		}
+		.chart-section {
+			margin-bottom: 24px;
+			.chart-card {
+				border-radius: 12px;
+				box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+				border: none;
+				padding-bottom: 12px;
+				.chart-title {
+					font-size: 16px;
+					font-weight: 600;
+					color: #222;
+					margin-bottom: 12px;
+				}
+				.chart-legend {
+					margin-top: 8px;
+					.legend-item {
+						display: inline-block;
+						margin-right: 24px;
+						font-size: 14px;
+						color: #888;
+						&.blue {
+							color: #347aff;
+							font-weight: 600;
+						}
+					}
+				}
+			}
+		}
+		.user-table-section {
+			background: #fff;
+			border-radius: 12px;
+			box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+			padding: 24px 24px 12px 24px;
+			.table-toolbar {
+				display: flex;
+				align-items: center;
+				margin-bottom: 8px;
+			}
+		}
+	}
+</style>

+ 137 - 64
src/views/courseDetails/components/LessonDetails.vue

@@ -1,74 +1,147 @@
 <template>
-	<div v-for="(chapter, index) in chapters" :key="index">
-		<a-card :title="chapter.title" style="margin-bottom: 20px">
-			<a-list item-layout="horizontal" :data-source="chapter.lessons">
-				<template #renderItem="{ item }">
-					<a-list-item>
-						<a-list-item-meta>
-							<template #avatar>
-								<a-image
-									width="100"
-									src="https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png"
-								/>
-							</template>
-							<template #title>
-								<a>{{ item.title }}</a>
-							</template>
-							<template #description>
-								<div>视频大小:{{ item.size }}MB</div>
-								<div>发布时间:{{ item.publishTime }}</div>
-								<div>发布人:{{ item.publisher }}</div>
-							</template>
-						</a-list-item-meta>
-						<template #actions>
-							<a-tooltip title="编辑">
-								<EditOutlined style="margin-right: 10px" />
-							</a-tooltip>
-							<a-tooltip title="删除">
-								<DeleteOutlined />
-							</a-tooltip>
-						</template>
-					</a-list-item>
-				</template>
-			</a-list>
-		</a-card>
+	<div class="section-list">
+		<div v-for="section in pagedSections" :key="section.id" class="section-block">
+			<div class="section-title">{{ section.title }}</div>
+			<div v-for="lesson in section.lessons" :key="lesson.id" class="lesson-row">
+				<div class="lesson-cover">
+					<a-avatar shape="square" :size="48" icon="play-circle" />
+				</div>
+				<div class="lesson-info">
+					<div class="lesson-title">{{ lesson.title }}</div>
+					<div class="lesson-meta">
+						<span>时长:{{ lesson.duration }}</span>
+						<span>视频大小:{{ lesson.size }}</span>
+						<span>发布时间:{{ lesson.publishTime }}</span>
+						<span>发布人:{{ lesson.author }}</span>
+					</div>
+				</div>
+				<div class="lesson-actions">
+					<EditOutlined class="action-icon" @click="$emit('edit-lesson', lesson)" />
+					<DeleteOutlined class="action-icon" @click="$emit('delete-lesson', lesson)" />
+				</div>
+			</div>
+		</div>
+		<!-- 分页 -->
+		<div class="pagination-box">
+			<a-pagination
+				:current="currentPage"
+				:page-size="pageSize"
+				:total="sectionsLength"
+				@change="$emit('page-change', $event)"
+				show-quick-jumper
+				:show-total="(total) => `共${total}章`"
+			/>
+			<a-select
+				v-model:value="localPageSize"
+				style="width: 100px; margin-left: 16px"
+				@change="emits('page-size-change', $event)"
+			>
+				<a-select-option :value="5">5/页</a-select-option>
+				<a-select-option :value="10">10/页</a-select-option>
+			</a-select>
+		</div>
 	</div>
 </template>
 
 <script setup>
-	import { ref } from 'vue'
+	import { defineProps, defineEmits, ref, watch } from 'vue'
 	import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+	const props = defineProps({
+		pagedSections: Array,
+		currentPage: Number,
+		pageSize: Number,
+		sectionsLength: Number
+	})
+	const emits = defineEmits(['edit-lesson', 'delete-lesson', 'page-change', 'page-size-change'])
 
-	const chapters = ref([
-		{
-			title: '第一章 课程导学',
-			lessons: [
-				{
-					title: '1-1 课程简介',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
-				},
-				{
-					title: '1-2 课程前瞻',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
-				}
-			]
-		},
-		{
-			title: '第二章 课程XX',
-			lessons: [
-				{
-					title: '2-1 课时标题',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
-				}
-			]
+	const localPageSize = ref(props.pageSize)
+	watch(
+		() => props.pageSize,
+		(newVal) => {
+			localPageSize.value = newVal
 		}
-	])
+	)
 </script>
 
-<style scoped></style>
+<style scoped>
+	.section-list {
+		width: 1200px;
+		margin: 0 auto;
+		background: #fff;
+		border-radius: 0 0 12px 12px;
+		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+		padding: 1px 32px 32px 32px;
+	}
+	.section-block {
+		margin-top: 32px;
+	}
+	.section-title {
+		font-size: 16px;
+		font-weight: 600;
+		color: #222;
+		margin-bottom: 18px;
+	}
+	.lesson-row {
+		display: flex;
+		align-items: center;
+		background: #f7f8fa;
+		border-radius: 8px;
+		margin-bottom: 16px;
+		padding: 16px 24px;
+		box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.03);
+		transition: box-shadow 0.2s;
+	}
+	.lesson-row:hover {
+		box-shadow: 0 4px 16px 0 rgba(52, 122, 255, 0.08);
+		background: #f0f6ff;
+	}
+	.lesson-cover {
+		margin-right: 20px;
+	}
+	.lesson-cover .ant-avatar {
+		background: #e6e9ef;
+		font-size: 24px;
+	}
+	.lesson-info {
+		flex: 1;
+	}
+	.lesson-title {
+		font-size: 15px;
+		font-weight: 500;
+		color: #222;
+		margin-bottom: 6px;
+	}
+	.lesson-meta {
+		font-size: 13px;
+		color: #888;
+	}
+	.lesson-meta span {
+		margin-right: 18px;
+	}
+	.lesson-actions {
+		display: flex;
+		align-items: center;
+	}
+	.action-icon {
+		font-size: 18px;
+		color: #347aff;
+		margin-left: 18px;
+		cursor: pointer;
+		transition: color 0.2s;
+	}
+	.action-icon:hover {
+		color: #1d5fd6;
+	}
+	.pagination-box {
+		display: flex;
+		align-items: center;
+		justify-content: flex-end;
+		margin-top: 32px;
+	}
+	.pagination-box .ant-pagination {
+		margin-right: 16px;
+	}
+	.pagination-box .ant-select {
+		min-width: 90px;
+	}
+</style>

+ 544 - 0
src/views/courseDetails/components/StudentDetails.vue

@@ -0,0 +1,544 @@
+<template>
+	<div class="student-details-page">
+		<!-- 搜索栏 -->
+		<div class="search-bar">
+			<a-input
+				v-model:value="searchForm.keyword"
+				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>
+			<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>
+			<a-range-picker 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>
+		</div>
+		<!-- 表格 -->
+		<a-table
+			row-key="id"
+			:columns="columns"
+			:data-source="pagedData"
+			:pagination="false"
+			:row-selection="rowSelection"
+			bordered
+			class="student-table"
+		>
+			<template #bodyCell="{ column, record }">
+				<template v-if="column.dataIndex === 'status'">
+					<span>
+						<a-badge status="success" v-if="record.status === '启用'" text="启用" />
+						<a-badge status="default" v-else text="禁用" />
+					</span>
+				</template>
+				<template v-else-if="column.dataIndex === 'online'">
+					<span>
+						<a-badge status="success" v-if="record.online" text="在线" />
+						<a-badge status="default" v-else text="离线" />
+					</span>
+				</template>
+				<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" danger @click="onDelete(record)">删除</a-button>
+					</a-space>
+				</template>
+			</template>
+		</a-table>
+		<!-- 批量操作和分页 -->
+		<div class="table-footer">
+			<div class="batch-actions">
+				<a-button @click="onSelectAll">选择全部</a-button>
+				<a-button @click="onInvertSelect">反向选择</a-button>
+				<a-dropdown>
+					<a-button>更多操作 <DownOutlined /></a-button>
+					<template #overlay>
+						<a-menu>
+							<a-menu-item>批量启用</a-menu-item>
+							<a-menu-item>批量禁用</a-menu-item>
+							<a-menu-item>批量删除</a-menu-item>
+						</a-menu>
+					</template>
+				</a-dropdown>
+			</div>
+			<a-pagination
+				:current="currentPage"
+				:page-size="pageSize"
+				:total="filteredData.length"
+				@change="onPageChange"
+				show-quick-jumper
+				:show-total="(total) => `共${total}条`"
+				style="margin-left: auto"
+			/>
+			<a-select v-model:value="pageSize" style="width: 100px; margin-left: 16px" @change="onPageSizeChange">
+				<a-select-option :value="10">10/页</a-select-option>
+				<a-select-option :value="20">20/页</a-select-option>
+				<a-select-option :value="50">50/页</a-select-option>
+			</a-select>
+		</div>
+		<!-- 新增学员弹窗 -->
+		<a-modal
+			v-model:visible="addStudentVisible"
+			title="新增学员"
+			width="720px"
+			: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>
+		</a-modal>
+		<!-- 详情弹窗 -->
+		<a-modal
+			v-model:visible="detailVisible"
+			title="个人实名认证"
+			width="900px"
+			:footer="null"
+			@cancel="() => (detailVisible = false)"
+		>
+			<div class="student-detail-modal">
+				<a-table
+					:pagination="false"
+					:show-header="false"
+					:data-source="[detailData]"
+					:columns="[{ title: '', dataIndex: 'id', key: 'id' }]"
+					style="display: none"
+				/>
+				<table class="detail-table">
+					<tr>
+						<th>ID编号</th>
+						<th>学生姓名</th>
+						<th>手机号码</th>
+						<th>性别</th>
+					</tr>
+					<tr>
+						<td>{{ detailData.id }}</td>
+						<td>{{ detailData.name }}</td>
+						<td>{{ detailData.phone }}</td>
+						<td>{{ detailData.gender }}</td>
+					</tr>
+					<tr>
+						<th>所在院系</th>
+						<th>所在班级</th>
+						<th>生日</th>
+						<th>所在城市</th>
+					</tr>
+					<tr>
+						<td>{{ detailData.college }}</td>
+						<td>{{ detailData.className }}</td>
+						<td>{{ detailData.birthday }}</td>
+						<td>{{ detailData.city }}</td>
+					</tr>
+					<tr>
+						<th>学籍状态</th>
+						<th>在线状态</th>
+						<th>注册时间</th>
+						<th>最后登录</th>
+					</tr>
+					<tr>
+						<td>{{ detailData.educationStatus }}</td>
+						<td>{{ detailData.onlineStatus }}</td>
+						<td>{{ detailData.registerTime }}</td>
+						<td>{{ detailData.lastLogin }}</td>
+					</tr>
+				</table>
+				<div class="modal-btn-bar">
+					<a-button type="primary" style="width: 120px; margin-top: 24px" @click="() => (detailVisible = false)">
+						关闭</a-button
+					>
+				</div>
+			</div>
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, computed, onMounted } from 'vue'
+	import { DownOutlined } from '@ant-design/icons-vue'
+	import { getDepartmentMembers } from '@/api/course/courseDetail'
+	import { getStudentDetail } from '@/api/course/courseDetail'
+
+	// mock数据
+	const allStudents = ref([
+		// 生成20条mock数据
+		...Array.from({ length: 20 }).map((_, i) => ({
+			id: i + 1,
+			studentNo: 'xy' + String(100000 + i),
+			name: '张小刚',
+			gender: i % 2 === 0 ? '男' : '女',
+			status: '启用',
+			phone: '18088889999',
+			online: true,
+			lastLogin: '2020-11-25 23:26:08'
+		}))
+	])
+
+	const searchForm = ref({
+		keyword: '',
+		type: '',
+		status: '',
+		date: []
+	})
+
+	const columns = [
+		{ title: '学号', dataIndex: 'studentNo', align: 'center' },
+		{ title: '姓名', dataIndex: 'name', 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 }
+	]
+
+	const currentPage = ref(1)
+	const pageSize = ref(10)
+	const selectedRowKeys = ref([])
+
+	const addStudentVisible = ref(false)
+	const departments = ref([])
+	const searchDeptMember = ref('')
+	const expandedDeptIds = ref([])
+	const checkedMemberIds = ref([])
+	const selectedMembers = ref([])
+
+	const detailVisible = ref(false)
+	const detailData = ref({})
+
+	const filteredData = computed(() => {
+		let data = allStudents.value
+		if (searchForm.value.keyword) {
+			data = data.filter(
+				(item) =>
+					item.name.includes(searchForm.value.keyword) ||
+					item.studentNo.includes(searchForm.value.keyword) ||
+					item.phone.includes(searchForm.value.keyword)
+			)
+		}
+		if (searchForm.value.status) {
+			data = data.filter((item) => item.status === searchForm.value.status)
+		}
+		// 这里可以加更多筛选条件
+		return data
+	})
+
+	const pagedData = computed(() => {
+		const start = (currentPage.value - 1) * pageSize.value
+		return filteredData.value.slice(start, start + pageSize.value)
+	})
+
+	const rowSelection = {
+		selectedRowKeys: selectedRowKeys.value,
+		onChange: (keys) => {
+			selectedRowKeys.value = keys
+		}
+	}
+
+	function onSearch() {
+		currentPage.value = 1
+	}
+	function onReset() {
+		searchForm.value = { keyword: '', type: '', status: '', date: [] }
+		currentPage.value = 1
+	}
+	function onPageChange(page) {
+		currentPage.value = page
+	}
+	function onPageSizeChange(size) {
+		pageSize.value = size
+		currentPage.value = 1
+	}
+	function onSelectAll() {
+		selectedRowKeys.value = pagedData.value.map((item) => item.id)
+	}
+	function onInvertSelect() {
+		const currentIds = pagedData.value.map((item) => item.id)
+		selectedRowKeys.value = currentIds.filter((id) => !selectedRowKeys.value.includes(id))
+	}
+	function onAddStudent() {
+		addStudentVisible.value = true
+		// 默认展开所有部门
+		expandedDeptIds.value = departments.value.map((d) => d.id)
+	}
+	function handleAddStudentCancel() {
+		addStudentVisible.value = false
+	}
+	function handleAddStudentOk() {
+		// TODO: 选中成员加入主表
+		addStudentVisible.value = false
+	}
+	function onDeptMemberSearch() {
+		// 搜索时自动展开所有部门
+		expandedDeptIds.value = filteredDepartments.value.map((d) => d.id)
+	}
+	function toggleDept(id) {
+		if (expandedDeptIds.value.includes(id)) {
+			expandedDeptIds.value = expandedDeptIds.value.filter((did) => did !== id)
+		} else {
+			expandedDeptIds.value.push(id)
+		}
+	}
+	function onMemberCheckChange(ids) {
+		// 限制最多30人
+		if (ids.length > 30) {
+			ids = ids.slice(0, 30)
+		}
+		checkedMemberIds.value = ids
+		updateSelectedMembers()
+	}
+	function isCheckedLimit(id) {
+		return checkedMemberIds.value.length >= 30 && !checkedMemberIds.value.includes(id)
+	}
+	function updateSelectedMembers() {
+		// 根据 checkedMemberIds 更新 selectedMembers
+		const all = []
+		departments.value.forEach((dept) => {
+			dept.members.forEach((m) => all.push({ ...m, deptName: dept.name }))
+		})
+		selectedMembers.value = all.filter((m) => checkedMemberIds.value.includes(m.id))
+	}
+	function removeSelected(id) {
+		checkedMemberIds.value = checkedMemberIds.value.filter((mid) => mid !== id)
+		updateSelectedMembers()
+	}
+	function clearSelected() {
+		checkedMemberIds.value = []
+		updateSelectedMembers()
+	}
+	const filteredDepartments = computed(() => {
+		if (!searchDeptMember.value) return departments.value
+		// 部门名或成员名匹配
+		return departments.value
+			.map((dept) => {
+				const matchDept = dept.name.includes(searchDeptMember.value)
+				const filteredMembers = dept.members.filter((m) => m.name.includes(searchDeptMember.value))
+				if (matchDept || filteredMembers.length) {
+					return {
+						...dept,
+						members: matchDept ? dept.members : filteredMembers
+					}
+				}
+				return null
+			})
+			.filter(Boolean)
+	})
+	onMounted(async () => {
+		departments.value = await getDepartmentMembers()
+	})
+	function onDetail(record) {
+		getStudentDetail().then((data) => {
+			detailData.value = data
+			detailVisible.value = true
+		})
+	}
+	function onEdit(record) {
+		// TODO: 编辑弹窗
+	}
+	function onSetting(record) {
+		// TODO: 设置弹窗
+	}
+	function onDelete(record) {
+		// TODO: 删除确认
+	}
+</script>
+
+<style lang="less" scoped>
+	.student-details-page {
+		background: #fff;
+		border-radius: 12px;
+		width: 1200px;
+		margin: 0 auto;
+		padding: 32px 32px 24px 32px;
+		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+		.search-bar {
+			display: flex;
+			align-items: center;
+			margin-bottom: 18px;
+		}
+		.action-bar {
+			margin-bottom: 16px;
+			display: flex;
+			align-items: center;
+		}
+		.student-table {
+			margin-bottom: 12px;
+			.ant-table-thead > tr > th {
+				background: #f7f8fa;
+				font-weight: 600;
+				color: #222;
+				font-size: 15px;
+			}
+			.ant-table-tbody > tr > td {
+				font-size: 14px;
+				color: #333;
+			}
+			.ant-btn {
+				font-size: 13px;
+				padding: 0 10px;
+				border-radius: 4px;
+			}
+			.ant-btn-dangerous {
+				background: #fff1f0;
+				color: #ff4d4f;
+				border: 1px solid #ffccc7;
+			}
+		}
+		.table-footer {
+			display: flex;
+			align-items: center;
+			margin-top: 12px;
+			.batch-actions {
+				display: flex;
+				align-items: center;
+				gap: 8px;
+			}
+		}
+	}
+	.add-student-modal {
+		display: flex;
+		min-height: 400px;
+		.left-panel {
+			width: 320px;
+			border-right: 1px solid #f0f0f0;
+			padding-right: 16px;
+			.search-input {
+				margin-bottom: 12px;
+			}
+			.dept-list {
+				max-height: 340px;
+				overflow-y: auto;
+			}
+			.dept-name {
+				font-weight: 600;
+				margin: 8px 0 4px 0;
+				cursor: pointer;
+				display: flex;
+				align-items: center;
+			}
+			.member-list {
+				margin-left: 20px;
+				margin-bottom: 8px;
+			}
+			.member-item {
+				margin: 4px 0;
+				display: flex;
+				align-items: center;
+			}
+		}
+		.right-panel {
+			flex: 1;
+			padding-left: 24px;
+			.selected-header {
+				font-weight: 600;
+				margin-bottom: 8px;
+			}
+			.selected-list {
+				min-height: 340px;
+				max-height: 340px;
+				overflow-y: auto;
+				.selected-item {
+					display: flex;
+					align-items: center;
+					background: #f5f5f5;
+					border-radius: 4px;
+					padding: 4px 8px;
+					margin-bottom: 6px;
+					.remove-icon {
+						margin-left: auto;
+						color: #999;
+						cursor: pointer;
+					}
+				}
+			}
+		}
+	}
+	.modal-footer {
+		display: flex;
+		justify-content: flex-end;
+		margin-top: 18px;
+	}
+	.student-detail-modal {
+		.detail-table {
+			width: 100%;
+			border-collapse: collapse;
+			margin-bottom: 0;
+			th,
+			td {
+				border: 1px solid #e8e8e8;
+				padding: 12px 16px;
+				text-align: left;
+				font-size: 15px;
+				background: #fff;
+			}
+			th {
+				background: #fafafa;
+				font-weight: 600;
+				color: #222;
+			}
+			tr:not(:first-child) td {
+				color: #333;
+			}
+		}
+		.modal-btn-bar {
+			text-align: center;
+		}
+	}
+</style>

+ 240 - 105
src/views/courseDetails/index.vue

@@ -1,120 +1,255 @@
 <template>
-	<div style="overflow-y: auto">
-		<a-layout>
-			<div style="width: 71%; margin-left: 10%">
-				<!-- 顶部信息栏 -->
-				<a-row :gutter="16" style="margin-bottom: 20px; align-items: center">
-					<a-col :span="4">
-						<a-image width="100" src="https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png" />
-					</a-col>
-					<a-col :span="16">
-						<div style="margin-bottom: 10px">
-							<span style="font-weight: bold">课程名称</span>
-							<span style="margin-left: 20px">当前状态</span>
-							<span style="margin-left: 20px">授课教师</span>
-							<span style="margin-left: 20px">课程分类</span>
-							<span style="margin-left: 20px">课时数量</span>
+	<div class="course-detail-page">
+		<!-- 顶部课程信息卡片 -->
+		<a-card class="course-info-card" bordered>
+			<div class="course-info-main">
+				<div class="cover-box">
+					<a-avatar shape="square" :size="120" icon="play-circle" />
+				</div>
+				<div class="info-box">
+					<div class="title">{{ course.name }}</div>
+					<div class="meta">
+						<p><EyeOutlined class="mr-0" /> {{ course.views }}</p>
+						<p><ClockCircleOutlined class="mr-0" /> {{ course.updateTime }}</p>
+					</div>
+				</div>
+				<div class="action-box">
+					<div class="btn-group">
+						<a-button @click="editVisible = true"> <EditOutlined /> 编辑课程</a-button>
+						<a-button> <DownOutlined /> 下架课程</a-button>
+						<a-button> <DeleteOutlined /> 删除课程</a-button>
+					</div>
+					<div class="extra-box">
+						<div class="row"><span>当前状态</span><span class="status-normal">● 正常</span></div>
+						<div class="row">
+							<span>授课教师</span><span>{{ course.teacher }}</span>
 						</div>
-						<div style="margin-bottom: 10px">
-							<span>👁️ 10000</span>
-							<span style="margin-left: 20px"><a-tag color="green">正常</a-tag></span>
-							<span style="margin-left: 20px">赵小刚</span>
-							<span style="margin-left: 20px">航空理论系-初级飞行训练</span>
-							<span style="margin-left: 20px">16</span>
+						<div class="row">
+							<span>课程分类</span><span>{{ course.category }}</span>
 						</div>
-						<div>
-							<span>📅 05-22 10:49</span>
+						<div class="row">
+							<span>课时数量</span><span>{{ course.duration }}</span>
 						</div>
-					</a-col>
-					<a-col :span="4" style="text-align: right">
-						<a-button type="primary">编辑课程</a-button>
-						<a-button style="margin-left: 10px">下架课程</a-button>
-						<a-button style="margin-left: 10px">删除课程</a-button>
-					</a-col>
-				</a-row>
-
-				<!-- 标签页导航 -->
-				<a-tabs default-active-key="1">
-					<a-tab-pane key="1" tab="课时详情">
-						<!-- 章节和课时列表 -->
-						<div v-for="(chapter, index) in chapters" :key="index">
-							<a-card :title="chapter.title" style="margin-bottom: 20px">
-								<a-list item-layout="horizontal" :data-source="chapter.lessons">
-									<template #renderItem="{ item }">
-										<a-list-item>
-											<a-list-item-meta>
-												<template #avatar>
-													<a-image
-														width="100"
-														src="https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png"
-													/>
-												</template>
-												<template #title>
-													<a>{{ item.title }}</a>
-												</template>
-												<template #description>
-													<div>视频大小:{{ item.size }}MB</div>
-													<div>发布时间:{{ item.publishTime }}</div>
-													<div>发布人:{{ item.publisher }}</div>
-												</template>
-											</a-list-item-meta>
-											<template #actions>
-												<a-tooltip title="编辑">
-													<EditOutlined style="margin-right: 10px" />
-												</a-tooltip>
-												<a-tooltip title="删除">
-													<DeleteOutlined />
-												</a-tooltip>
-											</template>
-										</a-list-item>
-									</template>
-								</a-list>
-							</a-card>
-						</div>
-					</a-tab-pane>
-					<!-- 其他标签页可以类似添加 -->
-				</a-tabs>
+					</div>
+				</div>
 			</div>
-		</a-layout>
-		<Footer />
+		</a-card>
+
+		<!-- tab 导航 -->
+		<a-tabs v-model:activeKey="activeTab" class="course-tabs">
+			<a-tab-pane key="detail" tab="课程详情" />
+			<a-tab-pane key="homework" tab="批改作业" />
+			<a-tab-pane key="test" tab="批改测试" />
+			<a-tab-pane key="student" tab="学员详情" />
+			<a-tab-pane key="stat" tab="学习统计" />
+		</a-tabs>
+
+		<LessonDetails
+			v-if="activeTab === 'detail'"
+			:pagedSections="pagedSections"
+			:currentPage="currentPage"
+			:pageSize="pageSize"
+			:sectionsLength="course.sections?.length || 0"
+			@edit-lesson="onEditLesson"
+			@delete-lesson="onDeleteLesson"
+			@page-change="onPageChange"
+			@page-size-change="onPageSizeChange"
+		/>
+		<StudentDetails v-if="activeTab === 'student'" />
+		<LearningStatistics v-if="activeTab === 'stat'" />
+
+		<!-- 编辑课程弹窗 -->
+		<a-modal v-model:visible="editVisible" title="新建工单" :footer="null" width="700px" @cancel="editVisible = false">
+			<EditCourse :course="course" @close="editVisible = false" />
+		</a-modal>
+		<!-- 编辑课时弹窗 -->
+		<AddClassHours v-model:visible="addClassHoursVisible" @ok="onAddClassHoursOk" />
 	</div>
 </template>
 
 <script setup>
-	import { ref } from 'vue'
-	import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+	import { ref, computed, onMounted } from 'vue'
+	import { EyeOutlined, ClockCircleOutlined, EditOutlined, DeleteOutlined, DownOutlined } from '@ant-design/icons-vue'
+	import EditCourse from './components/EditCourse.vue'
+	import AddClassHours from './components/AddClassHours.vue'
+	import LessonDetails from './components/LessonDetails.vue'
+	import StudentDetails from './components/StudentDetails.vue'
+	import LearningStatistics from './components/LearningStatistics.vue'
+	import { getCourseDetail } from '@/api/course/courseDetail.js'
+
+	const course = ref({})
+	const activeTab = ref('detail')
+	const currentPage = ref(1)
+	const pageSize = ref(10)
+	const editVisible = ref(false)
+	const addClassHoursVisible = ref(false)
+	const editingLesson = ref(null)
+
+	const pagedSections = computed(() => {
+		if (!course.value.sections) return []
+		const start = (currentPage.value - 1) * pageSize.value
+		return course.value.sections.slice(start, start + pageSize.value)
+	})
+
+	onMounted(() => {
+		getCourseDetail().then((data) => {
+			course.value = data
+		})
+	})
+
+	function onPageChange(page) {
+		currentPage.value = page
+	}
+	function onPageSizeChange(size) {
+		pageSize.value = size
+		currentPage.value = 1
+	}
+	function onEditLesson(lesson) {
+		editingLesson.value = lesson
+		addClassHoursVisible.value = true
+	}
+	function onDeleteLesson(lesson) {
+		// 这里只做mock删除
+		course.value.sections.forEach((section) => {
+			section.lessons = section.lessons.filter((l) => l.id !== lesson.id)
+		})
+	}
+	function onAddClassHoursOk(data) {
+		// 这里只做mock保存
+		if (editingLesson.value) {
+			Object.assign(editingLesson.value, data)
+		}
+		addClassHoursVisible.value = false
+	}
+</script>
+
+<style lang="less" scoped>
+	.course-detail-page {
+		background: #f7f8fa;
+		min-height: 100vh;
+		padding: 32px 0 0 0;
 
-	const chapters = ref([
-		{
-			title: '第一章 课程导学',
-			lessons: [
-				{
-					title: '1-1 课程简介',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
-				},
-				{
-					title: '1-2 课程前瞻',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
+		.course-info-card {
+			width: 1200px;
+			margin: 0 auto 24px auto;
+			border-radius: 12px;
+			box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+			border: none;
+			.course-info-main {
+				display: flex;
+				align-items: flex-start;
+				padding: 32px 32px 24px 32px;
+				.cover-box {
+					width: 160px;
+					height: 120px;
+					background: #f2f3f5;
+					border-radius: 8px;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					margin-right: 32px;
+					.ant-avatar {
+						background: #e6e9ef;
+						font-size: 48px;
+					}
 				}
-			]
-		},
-		{
-			title: '第二章 课程XX',
-			lessons: [
-				{
-					title: '2-1 课时标题',
-					size: 300,
-					publishTime: '2025-07-01 10:23:59',
-					publisher: '张三'
+				.info-box {
+					flex: 1;
+					.title {
+						font-size: 22px;
+						font-weight: 600;
+						color: #222;
+						margin-bottom: 12px;
+					}
+					.meta {
+						color: #999;
+						font-size: 14px;
+						span {
+							margin-right: 24px;
+							display: inline-flex;
+							align-items: center;
+							.anticon {
+								margin-right: 4px;
+							}
+						}
+					}
 				}
-			]
+				.action-box {
+					flex: 1;
+					color: #999;
+					.extra-box {
+						min-width: 180px;
+						margin-left: 32px;
+						display: flex;
+						justify-content: space-evenly;
+						.row {
+							display: flex;
+							flex-direction: column;
+							justify-content: space-between;
+							font-size: 14px;
+							margin-bottom: 8px;
+							span:last-child {
+								font-weight: 500;
+							}
+							.status-normal {
+								margin-top: 10px;
+								color: #52c41a;
+								font-weight: 600;
+								margin-left: 8px;
+							}
+						}
+					}
+					.btn-group {
+						display: flex;
+						flex-direction: row;
+						margin-left: 40px;
+						gap: 10px;
+						justify-content: flex-end;
+						.ant-btn {
+							margin-bottom: 12px;
+							width: 120px;
+							font-size: 14px;
+							border-radius: 6px;
+						}
+						.ant-btn-primary {
+							background: #347aff;
+							border-color: #347aff;
+						}
+					}
+				}
+			}
 		}
-	])
-</script>
 
-<style scoped>
+		.course-tabs {
+			width: 1200px;
+			margin: 0 auto 0 auto;
+			background: #fff;
+			border-radius: 12px 12px 0 0;
+			padding-left: 20px;
+			.ant-tabs-bar {
+				border-bottom: 1px solid #f0f0f0;
+				margin-bottom: 0;
+			}
+			.ant-tabs-nav {
+				font-size: 16px;
+				.ant-tabs-tab {
+					padding: 18px 32px 14px 32px;
+					font-weight: 500;
+					color: #666;
+					&.ant-tabs-tab-active {
+						color: #347aff;
+						font-weight: 600;
+					}
+				}
+				.ant-tabs-ink-bar {
+					background: #347aff;
+					height: 3px;
+					border-radius: 2px 2px 0 0;
+				}
+			}
+		}
+		.mr-0 {
+			margin-right: 5px !important;
+		}
+	}
 </style>

+ 62 - 0
src/views/exm/question/components/Show.vue

@@ -0,0 +1,62 @@
+<template>
+  <div style="line-height:1.8">
+    <div v-if="qType==1" v-loading="qLoading">
+      <div class="q-title" v-html="question.title"/>
+      <div class="q-content">
+          <span :key="item.id" v-for="item in question.items" class="q-item-contain">
+            <span class="q-item-prefix">{{item.prefix}}</span>
+            <span v-html="item.content" class="q-item-content"></span>
+          </span>
+      </div>
+    </div>
+    <div v-else-if="qType==2" v-loading="qLoading">
+      <div class="q-title" v-html="question.title"/>
+      <div class="q-content">
+          <span :key="item.id" v-for="item in question.items" class="q-item-contain">
+            <span class="q-item-prefix">{{item.prefix}}</span>
+            <span v-html="item.content" class="q-item-content"></span>
+          </span>
+      </div>
+    </div>
+    <div v-else-if="qType==3" v-loading="qLoading">
+      <div class="q-title" v-html="question.title" style="display: inline;margin-right: 10px"/>
+      <span>(</span>
+      <span :key="item.id" v-for="item in question.items">
+        <span v-html="item.content" class="q-item-content"></span>
+       </span>
+      <span>)</span>
+    </div>
+    <div v-else-if="qType==4" v-loading="qLoading">
+      <div class="q-title" v-html="question.title"/>
+    </div>
+    <div v-else-if="qType==5" v-loading="qLoading">
+      <div class="q-title" v-html="question.title"/>
+    </div>
+    <div v-else>
+    </div>
+  </div>
+
+</template>
+
+<script>
+export default {
+  name: 'QuestionShow',
+  props: {
+    question: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    qLoading: {
+      type: Boolean,
+      default: false
+    },
+    qType: {
+      type: Number,
+      default: 0
+    }
+  },
+  methods: {}
+}
+</script>

+ 279 - 0
src/views/exm/question/edit/single-choice.vue

@@ -0,0 +1,279 @@
+<template>
+	<div class="app-container">
+		<a-spin :spinning="formLoading" tip="加载中...">
+			<a-form :model="form" ref="formRef" label-width="100px" :rules="rules">
+				<a-form-item label="年级:" prop="gradeLevel" required>
+					<a-select v-model:value="form.gradeLevel" placeholder="年级" :options="gradeLevelOptions"  clearable />
+				</a-form-item>
+				<a-form-item label="学科:" prop="subjectId" required>
+					<a-select v-model:value="form.subjectId" placeholder="学科" :options="susbjectOptions" />
+				</a-form-item>
+				<a-form-item label="题干:" prop="title" required>
+					<a-input v-model="form.title"   @focus="inputClick(form,'title')" />
+					<xn-editor v-model:value="form.title"  :height="150"/>
+				</a-form-item>
+				<a-form-item label="选项:" required>
+					<a-form-item :key="item.prefix" v-for="(item, index) in form.items" label-width="50px" class="question-item-label">
+						<template #label>
+							{{ item.prefix }}
+						</template>
+						<a-input v-model:value="item.prefix" style="width:50px;" />
+						<a-button type="primary" danger class="question-item-remove" @click="questionItemRemove(index)">删除</a-button>
+						<xn-editor v-model:value="item.content"  :height="100"/>
+					</a-form-item>
+				</a-form-item>
+				<a-form-item label="解析:" prop="analyze" required>
+					<xn-editor v-model:value="form.analyze" :height="150"/>
+				</a-form-item>
+				<a-form-item label="分数:" prop="score" required>
+					<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+				</a-form-item>
+				<a-form-item label="难度:" required>
+					<a-rate v-model:value="form.difficult" class="question-item-rate" />
+				</a-form-item>
+				<a-form-item label="正确答案:" prop="correct" required>
+					<a-radio-group v-model:value="form.correct">
+						<a-radio v-for="item in form.items" :key="item.prefix" :value="item.prefix">{{ item.prefix }}</a-radio>
+					</a-radio-group>
+				</a-form-item>
+				<a-form-item>
+					<a-button type="primary" @click="submitForm" style="margin-right: 10px;">提交</a-button>
+					<a-button @click="resetForm" style="margin-right: 10px;">重置</a-button>
+					<a-button type="success" @click="questionItemAdd" style="margin-right: 10px;">添加选项</a-button>
+					<a-button type="success" @click="showQuestion">预览</a-button>
+				</a-form-item>
+			</a-form>
+		</a-spin>
+
+		<a-modal
+			v-model:visible="richEditor.dialogVisible"
+			:footer="null"
+			:body-style="{ padding: '0' }"
+			:width="800"
+			centered
+			destroy-on-close
+		>
+			<Ueditor @ready="editorReady"/>
+			<a-button type="primary" @click="editorConfirm">确 定</a-button>
+			<a-button @click="richEditor.dialogVisible = false">取 消</a-button>
+		</a-modal>
+
+		<a-modal
+			v-model:visible="questionShow.dialog"
+			:footer="null"
+			:body-style="{ padding: '0' }"
+			:width="800"
+			centered
+			destroy-on-close
+		>
+			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import questionApi from '@/api/question/tQuestionApi'
+import QuestionShow from '@/views/exm/question/components/Show.vue'
+import tool from "@/utils/tool"
+import XnEditor from '@/components/Editor/index.vue'
+import Ueditor from '@/components/Ueditor/index.vue'
+
+
+const route = useRoute()
+const router = useRouter()
+
+const formLoading = ref(false)
+// 表单引用
+const formRef = ref()
+
+// 数据初始化
+const form = ref({
+	id: null,
+	questionType: 1,
+	gradeLevel: null,
+	subjectId: null,
+	title: '',
+	items: [
+		{ prefix: 'A', content: '' },
+		{ prefix: 'B', content: '' },
+		{ prefix: 'C', content: '' },
+		{ prefix: 'D', content: '' }
+	],
+	analyze: '',
+	correct: '',
+	score: '',
+	difficult: 0
+})
+
+const richEditor = reactive({
+	dialogVisible: false,
+	object: null,
+	parameterName: '',
+	instance: null
+})
+
+const questionShow = reactive({
+	qType: 0,
+	dialog: false,
+	question: null,
+	loading: false
+})
+
+const gradeLevelOptions = tool.dictList('SEMESTER')
+const susbjectOptions = tool.dictList('SUBJECT')
+const subjectFilter = ref(null)
+
+// 校验规则
+const rules = {
+	gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+	subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+	title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+	analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+	score: [{ required: true, message: '请输入分数', trigger: 'blur' }],
+	correct: [{ required: true, message: '请选择正确答案', trigger: 'change' }]
+}
+
+// 生命周期:组件挂载时处理路由参数
+onMounted(() => {
+	const id = route.query.id
+	if (id && parseInt(id) !== 0) {
+		formRef.value?.resetFields()
+		questionApi.select(id).then(re => {
+			Object.assign(form, re.response)
+		})
+	}
+})
+
+// 方法定义
+
+function editorReady(instance) {
+	richEditor.instance = instance
+	let currentContent = richEditor.object[richEditor.parameterName]
+	richEditor.instance.setContent(currentContent)
+	richEditor.instance.focus(true)
+}
+
+function inputClick(object, parameterName) {
+	// 获取当前聚焦的元素并移除焦点
+	const activeElement = document.activeElement
+	if (activeElement && typeof activeElement.blur === 'function') {
+		activeElement.blur()
+	}
+
+	richEditor.object = object
+	richEditor.parameterName = parameterName
+	richEditor.dialogVisible = true
+}
+
+function editorConfirm() {
+	console.log(richEditor)
+	let content = richEditor.instance.getContent()
+	richEditor.object[richEditor.parameterName] = content
+	richEditor.dialogVisible = false
+}
+
+function questionItemRemove(index) {
+	form.value.items.splice(index, 1)
+}
+
+function questionItemAdd() {
+	const items = form.value.items
+	let newLastPrefix
+	console.log('last', items)
+	if (items.length > 0) {
+		let last = items[items.length - 1]
+		newLastPrefix = String.fromCharCode(last.prefix.charCodeAt() + 1)
+	} else {
+		newLastPrefix = 'A'
+	}
+	items.push({ id: null, prefix: newLastPrefix, content: '' })
+}
+
+function submitForm() {
+	formRef.value.validate().then(valid => {
+		if (valid) {
+			formLoading.value = true
+			questionApi.edit(form).then(re => {
+				if (re.code === 1) {
+					store.dispatch('tagsView/delCurrentView').then(() => {
+						router.push('/exam/question/list')
+					})
+				}
+			}).catch(e => {
+				console.error(e)
+			}).finally(() => {
+				formLoading.value = false
+			})
+		} else {
+			return false
+		}
+	})
+}
+
+
+function resetForm() {
+	const lastId = form.id
+	formRef.value.resetFields()
+	Object.assign(form, {
+		id: null,
+		questionType: 1,
+		gradeLevel: null,
+		subjectId: null,
+		title: '',
+		items: [
+			{ prefix: 'A', content: '' },
+			{ prefix: 'B', content: '' },
+			{ prefix: 'C', content: '' },
+			{ prefix: 'D', content: '' }
+		],
+		analyze: '',
+		correct: '',
+		score: '',
+		difficult: 0
+	})
+	form.id = lastId
+}
+
+function showQuestion() {
+	questionShow.dialog = true
+	questionShow.qType = form.value.questionType
+	console.log(form)
+	console.log(questionShow)
+	questionShow.question = form.value
+	form.value.items.forEach((item, index) => {
+		console.log(`选项 ${item.prefix} 的内容:`, item.content)
+	})
+}
+</script>
+
+<style scoped>
+.question-item-label {
+	margin-top: 10px;
+	margin-bottom: 10px !important;
+}
+
+.question-item-remove {
+	margin-left: 20px;
+}
+
+.question-item-content-input {
+	margin-left: 8px;
+	width: 60%;
+	height: 20px;
+}
+
+.question-item-span {
+	vertical-align: middle;
+	font-size: 14px;
+	color: #606266;
+	font-weight: 700;
+	box-sizing: border-box;
+	margin-left: 10px;
+}
+
+.question-item-rate {
+	line-height: 2.5;
+}
+</style>

+ 109 - 32
src/views/exm/question/index.vue

@@ -3,19 +3,37 @@
         <a-form ref="searchFormRef" name="advanced_search" :model="searchFormState" class="ant-advanced-search-form">
             <a-row :gutter="24">
                 <a-col :span="6">
-                    <a-form-item label="QUESTION_TYPE" name="questionType">
-                        <a-input v-model:value="searchFormState.questionType" placeholder="请输入QUESTION_TYPE" />
-                    </a-form-item>
+					<a-form-item label="题型名称:" name="questionType">
+						<a-select
+							v-model:value="searchFormState.questionType"
+							:options="questionTypeOptions"
+							style="width: 70%"
+							placeholder="请选择题型"
+						>
+						</a-select>
+					</a-form-item>
                 </a-col>
                 <a-col :span="6">
-                    <a-form-item label="SUBJECT_ID" name="subjectId">
-                        <a-input v-model:value="searchFormState.subjectId" placeholder="请输入SUBJECT_ID" />
-                    </a-form-item>
+					<a-form-item label="学科名称:" name="subjectId">
+						<a-select
+							v-model:value="searchFormState.subjectId"
+							:options="susbjectOptions"
+							style="width: 70%"
+							placeholder="请选择学科"
+						>
+						</a-select>
+					</a-form-item>
                 </a-col>
                 <a-col :span="6">
-                    <a-form-item label="GRADE_LEVEL" name="gradeLevel">
-                        <a-input v-model:value="searchFormState.gradeLevel" placeholder="请输入GRADE_LEVEL" />
-                    </a-form-item>
+					<a-form-item label="学期名称:" name="gradeLevel">
+						<a-select
+							v-model:value="searchFormState.gradeLevel"
+							:options="gradeLevelOptions"
+							style="width: 70%"
+							placeholder="请选择学期"
+						>
+						</a-select>
+					</a-form-item>
                 </a-col>
                 <a-col :span="6">
                     <a-button type="primary" @click="table.refresh(true)">查询</a-button>
@@ -35,10 +53,26 @@
         >
             <template #operator class="table-operator">
                 <a-space>
-                    <a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('tQuestionAdd')">
-                        <template #icon><plus-outlined /></template>
-                        新增
-                    </a-button>
+					<a-dropdown v-model:visible="isDropdownVisible" :trigger="['click']">
+						<template #overlay>
+							<a-menu slot="overlay" >
+								<a-menu @click="handleMenuClick">
+									<a-menu-item v-for="item in aMenuSelect" :key="item.key">
+										{{ item.label }}
+									</a-menu-item>
+								</a-menu>
+								<a-menu-divider />
+							</a-menu>
+						</template>
+						<a-button type="primary"  v-if="hasPerm('tQuestionAdd')">
+							创建
+							<DownOutlined />
+						</a-button>
+					</a-dropdown>
+<!--                    <a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('tQuestionAdd')">-->
+<!--                        <template #icon><plus-outlined /></template>-->
+<!--                        新增-->
+<!--                    </a-button>-->
                     <xn-batch-delete
                         v-if="hasPerm('tQuestionBatchDelete')"
                         :selectedRowKeys="selectedRowKeys"
@@ -47,6 +81,15 @@
                 </a-space>
             </template>
             <template #bodyCell="{ column, record }">
+				<template v-if="column.dataIndex === 'questionType'">
+					{{ $TOOL.dictTypeData('QUESTION_TYPE', String(record.questionType)) }}
+				</template>
+				<template v-if="column.dataIndex === 'subjectId'">
+					{{ $TOOL.dictTypeData('SUBJECT', String(record.subjectId)) }}
+				</template>
+				<template v-if="column.dataIndex === 'gradeLevel'">
+					{{ $TOOL.dictTypeData('SEMESTER', String(record.gradeLevel)) }}
+				</template>
                 <template v-if="column.dataIndex === 'action'">
                     <a-space>
                         <a @click="formRef.onOpen(record)" v-if="hasPerm('tQuestionEdit')">编辑</a>
@@ -60,52 +103,63 @@
         </s-table>
     </a-card>
     <Form ref="formRef" @successful="table.refresh(true)" />
+	<!-- 模态框部分 -->
+	<a-modal v-model:visible="showModal" title="创建题目" :footer="null" width="80%">
+		<component :is="getComponentByType(selectedQuestionType)" />
+	</a-modal>
 </template>
 
 <script setup name="question">
     import Form from './form.vue'
+	import { DownOutlined } from '@ant-design/icons-vue';
     import tQuestionApi from '@/api/question/tQuestionApi'
+	import SingleChoice from '@/views/exm/question/edit/single-choice.vue'
+	import tool from "@/utils/tool";
     let searchFormState = reactive({})
     const searchFormRef = ref()
     const table = ref()
     const formRef = ref()
     const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
+	const gradeLevelOptions = tool.dictList('SEMESTER')
+	const susbjectOptions = tool.dictList('SUBJECT')
+	const questionTypeOptions = tool.dictList('QUESTION_TYPE')
+
+	let aMenuSelect = questionTypeOptions.map(item => {
+		return {
+			label: item.label,
+			key: item.value
+		}
+	})
+	console.log(aMenuSelect)
+
     const columns = [
+		{
+			title: 'ID',
+			dataIndex: 'id'
+		},
         {
-            title: 'QUESTION_TYPE',
+            title: '题型',
             dataIndex: 'questionType'
         },
         {
-            title: 'SUBJECT_ID',
+            title: '学科',
             dataIndex: 'subjectId'
         },
         {
-            title: 'SCORE',
+            title: '分数',
             dataIndex: 'score'
         },
         {
-            title: 'GRADE_LEVEL',
+            title: '学期',
             dataIndex: 'gradeLevel'
         },
         {
-            title: 'DIFFICULT',
+            title: '难度',
             dataIndex: 'difficult'
         },
         {
-            title: 'CORRECT',
-            dataIndex: 'correct'
-        },
-        {
-            title: 'INFO_TEXT_CONTENT_ID',
-            dataIndex: 'infoTextContentId'
-        },
-        {
-            title: 'STATUS',
-            dataIndex: 'status'
-        },
-        {
-            title: 'DELETED',
-            dataIndex: 'deleted'
+            title: '创建时间',
+            dataIndex: 'createTime'
         },
     ]
     // 操作栏通过权限判断是否显示
@@ -161,4 +215,27 @@
             table.value.clearRefreshSelected()
         })
     }
+	const isDropdownVisible = ref(false)
+	const showModal = ref(false)
+	const selectedQuestionType = ref(null)
+
+	const handleMenuClick = (e) => {
+		const selectedKey = e.key
+		selectedQuestionType.value = selectedKey
+		showModal.value = true
+		isDropdownVisible.value = false // 点击后隐藏下拉菜单
+	}
+	// 根据题型返回对应组件
+	const getComponentByType = (type) => {
+		switch (type) {
+			case '1': // 单选题
+				return SingleChoice
+			case '2': // 多选题
+				return MultipleChoice
+			case '3': // 填空题
+				return FillInBlank
+			default:
+				return null
+		}
+	}
 </script>