浏览代码

用户可以对个人信息/课程中心- 笔记/问答-同步视频/论坛添加展示操作系统/论坛顶-踩/章节测验

canghailong 6 月之前
父节点
当前提交
01a71a0723

+ 5 - 1
src/api/forum/forumApi.js

@@ -26,6 +26,10 @@ export default {
 	postlikeSubmit(data, isLike = 0) {
 		return request(`forum/postlike/${isLike == 0 ? 'add' : 'cancel'}`, data)
 	},
+	// 踩接口 // 取消踩接口
+	postPostnotlikeSubmit(data, isLike = 0) {
+		return request(`forum/postnotlike/${isLike == 0 ? 'add' : 'cancel'}`, data)
+	},
 	// 回复帖子接口 //编辑回复
 	submitPostreply(data, edit = false) {
 		return request(`forum/postreply/${edit ? 'edit' : 'add'}`, data)
@@ -44,7 +48,7 @@ export default {
 	},
 	// 所有用户
 	allUserList(data) {
-		return request('bus/user/allList', data, 'get')
+		return request('bus/user/busPageList', data, 'get')
 	},
 	// 后台-管理员是否关闭帖子接口
 	postinfoStatus(data) {

+ 7 - 6
src/components/Comment/index.vue

@@ -15,18 +15,19 @@
 							<HeartOutlined @click="like(item,1)" />
 						</template>
 					</a-tooltip>
-					<span style="padding-left: 8px; cursor: auto">
+					<span style="padding-left: 3px; cursor: auto">
 						{{ item.likeCount }}
 					</span>
 				</span>
-				<a-tooltip title="编辑">
+				<a-tooltip title="编辑" v-if="item.isSelf == 1">
 					<EditOutlined
-						v-if="item.isSelf == 1"
 						@click="replyFormRef.onOpen(item, props.params.targetId, item.replyId, true)"
 					/>
+					<span style="padding-left: 3px">编辑</span>
 				</a-tooltip>
-				<a-tooltip title="删除">
-					<DeleteOutlined v-if="item.isSelf == 1" @click="showDeleteConfirm(item)" />
+				<a-tooltip title="删除" v-if="item.isSelf == 1">
+					<DeleteOutlined @click="showDeleteConfirm(item)" />
+					<span style="padding-left: 3px">删除</span>
 				</a-tooltip>
 
 				<span
@@ -34,7 +35,7 @@
 					@click="replyFormRef.onOpen(item, props.params.targetId, item.replyId, false)"
 					v-if="item.parentId == -1"
 				>
-					回复
+					评论
 				</span>
 			</template>
 			<template #author>

+ 72 - 0
src/utils/Broadcast.js

@@ -0,0 +1,72 @@
+class Broadcast {
+	constructor() {
+		this.channels = {}
+	}
+
+	/**
+	 * 获取或创建一个BroadcastChannel实例
+	 * @param {string} channelName - 频道名称
+	 * @returns {BroadcastChannel} BroadcastChannel实例
+	 */
+	getChannel(channelName) {
+		if (!this.channels[channelName]) {
+			this.channels[channelName] = new BroadcastChannel(channelName)
+		}
+		return this.channels[channelName]
+	}
+
+	/**
+	 * 监听指定频道的消息
+	 * @param {string} channelName - 频道名称
+	 * @param {Function} callback - 回调函数
+	 * @returns {Function} 取消监听的函数
+	 */
+	on(channelName, callback) {
+		const channel = this.getChannel(channelName)
+		const handler = function (event) {
+			callback(event.data)
+		}
+		channel.addEventListener('message', handler)
+		
+		// 返回取消监听的函数
+		return () => {
+			channel.removeEventListener('message', handler)
+		}
+	}
+
+	/**
+	 * 向指定频道发送消息
+	 * @param {string} channelName - 频道名称
+	 * @param {any} data - 要发送的数据
+	 */
+	emit(channelName, data) {
+		const channel = this.getChannel(channelName)
+		channel.postMessage(data)
+	}
+
+	/**
+	 * 关闭指定频道
+	 * @param {string} channelName - 频道名称
+	 */
+	close(channelName) {
+		if (this.channels[channelName]) {
+			this.channels[channelName].close()
+			delete this.channels[channelName]
+		}
+	}
+
+	/**
+	 * 关闭所有频道
+	 */
+	closeAll() {
+		Object.keys(this.channels).forEach(channelName => {
+			this.channels[channelName].close()
+		})
+		this.channels = {}
+	}
+}
+
+// 创建全局实例
+const BroadcastObj = new Broadcast()
+
+export default BroadcastObj

+ 60 - 14
src/views/forum/addForum.vue

@@ -8,6 +8,28 @@
 							<span>{{ forumTitle }}</span>
 						</a-form-item>
 					</a-col>
+					<template v-if="route.query.postType == 1">
+						<a-col :span="8">
+							<a-form-item label="浏览器类型:">
+								<span>{{ browserObj.forumSupportEnvParam.browserType }}</span>
+							</a-form-item>
+						</a-col>
+						<a-col :span="8">
+							<a-form-item label="浏览器版本号:">
+								<span>{{ browserObj.forumSupportEnvParam.browserVersion }}</span>
+							</a-form-item>
+						</a-col>
+						<a-col :span="8">
+							<a-form-item label="操作系统:">
+								<span>{{ browserObj.forumSupportEnvParam.osType }}</span>
+							</a-form-item>
+						</a-col>
+						<a-col :span="16">
+							<a-form-item label="操作系统版本号:">
+								<span>{{ browserObj.forumSupportEnvParam.osVersion }}</span>
+							</a-form-item>
+						</a-col>
+					</template>
 					<a-col :span="8">
 						<a-form-item label="标题:" name="postTitle">
 							<a-input v-model:value="formData.postTitle" placeholder="请输入标题" allow-clear />
@@ -17,11 +39,9 @@
 						<a-form-item label="分类:" name="typeId">
 							<a-select
 								v-model:value="formData.typeId"
-								show-search
 								placeholder="请选择分类"
 								style="width: 100%"
 								:options="typeOptions"
-								:filter-option="filterOption"
 							></a-select>
 						</a-form-item>
 					</a-col>
@@ -35,6 +55,7 @@
 								style="width: 100%"
 								:options="usertypeOptions"
 								:filter-option="filterOption"
+								@popupScroll="popupScroll"
 							></a-select>
 						</a-form-item>
 					</a-col>
@@ -88,9 +109,40 @@
 		}
 	})
 	const filterOption = (input, option) => {
-		return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
+		return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+	}
+	const popupScroll = (el) => {
+		let offset = 0
+		if (el.target.scrollHeight - el.target.scrollTop - offset <= el.target.clientHeight) {
+			userPagination.value.current += 1
+			getUserAllList(true)
+		}
+	}
+	const userPagination = ref({
+		size: 10,
+		current: 1,
+	})
+	const userTotal = ref(0)
+	//获取用户
+	const getUserAllList = (add) => {
+		if (userPagination.value.size * userPagination.value.current < userTotal.value) {
+			forumApi.allUserList(userPagination.value).then((res) => {
+				userTotal.value = res.total
+				let userList = res.records.map((r) => {
+					return {
+						label: r.name,
+						value: r.id,
+						...r
+					}
+				})
+				if (add) {
+					usertypeOptions.value.push(userList)
+				} else {
+					usertypeOptions.value = userList
+				}
+			})
+		}
 	}
-
 	const getData = async () => {
 		await forumApi.forumTypeList().then((data) => {
 			typeOptions.value = data.map((r) => {
@@ -101,15 +153,7 @@
 				}
 			})
 		})
-		await forumApi.allUserList().then((data) => {
-			usertypeOptions.value = data.map((r) => {
-				return {
-					label: r.name,
-					value: r.id,
-					...r
-				}
-			})
-		})
+		getUserAllList()
 		if (route.query.postType == 1) {
 			getBower()
 		}
@@ -179,7 +223,9 @@
 		formData.value.postContent = html
 	}
 	//获取操作系统/浏览器等信息
-	const browserObj = ref({})
+	const browserObj = ref({
+		forumSupportEnvParam:{}
+	})
 	function getBower() {
 		const browser = Bowser.getParser(window.navigator.userAgent)
 		let obj = browser.parsedResult

+ 93 - 30
src/views/forum/detail.vue

@@ -2,26 +2,41 @@
 	<div style="display: flex; justify-content: center" class="main-content-wrapper">
 		<div style="width: 1200px">
 			<a-card>
-				<div style="display: flex; justify-content: space-between" v-if="detailObj.userNickName">
-					<div style="display: flex">
-						<a-avatar
-							style="width: 60px; height: 60px"
-							:src="detailObj.userAvatar"
-							:size="{ xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 }"
-						/>
-						<div class="snowy-index-card-left-one-username">
-							<span style="font-weight: 600; margin: 2px; font-size: 18px">{{ detailObj.userNickName }}</span>
-							<span style="color: #6d737b; margin: 2px">
-								{{ detailObj.typeName }} | {{ formatDateTime(detailObj.lastReplyTime) }}
-							</span>
+				<div class="flt">
+					<div class="flex-1">
+						<div style="display: flex; justify-content: space-between" v-if="detailObj.userNickName">
+							<div style="display: flex">
+								<a-avatar
+									style="width: 60px; height: 60px"
+									:src="detailObj.userAvatar"
+									:size="{ xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 }"
+								/>
+								<div class="snowy-index-card-left-one-username">
+									<span style="font-weight: 600; margin: 2px; font-size: 18px; min-height: 20px">{{
+										detailObj.userNickName
+									}}</span>
+									<span style="color: #6d737b; margin: 2px">
+										{{ detailObj.typeName ? detailObj.typeName + '|' : '' }}
+										{{ formatDateTime(detailObj.lastReplyTime) }}
+									</span>
+								</div>
+							</div>
 						</div>
+						<div class="forum-list-title">{{ detailObj.postTitle }}</div>
 					</div>
+					<a-tooltip :getPopupContainer="(trigger) => trigger.parentElement">
+						<template #title>
+							<span>返回</span>
+						</template>
+						<a-button @click="goToHome">
+							<rollback-outlined />
+						</a-button>
+					</a-tooltip>
 				</div>
-				<div class="forum-list-title">{{ detailObj.postTitle }}</div>
 				<div class="htmlContent" v-html="detailObj.postContent"></div>
-				<div>
-					<span v-if="detailObj.userNickName">
-						<a-tooltip title="点赞">
+				<div class="flc">
+					<div v-if="detailObj.userNickName">
+						<a-tooltip title="">
 							<template v-if="detailObj.isLike == 1">
 								<HeartOutlined :style="{ color: '#fa6c8d' }" @click="like(detailObj, 0)" />
 							</template>
@@ -29,23 +44,50 @@
 								<HeartOutlined @click="like(detailObj, 1)" />
 							</template>
 						</a-tooltip>
-						<span style="padding-left: 8px">
+						<span style="padding-left: 3px">
 							{{ detailObj.likeCount }}
 						</span>
-					</span>
-					<a-tooltip title="编辑" v-if="detailObj.isSelf == 1">
-						<EditOutlined class="ml-2" @click="formRef.onOpen(detailObj, detailObj.postId)" />
-					</a-tooltip>
-
-					<span class="ml-2" style="cursor: pointer" @click="replyFormRef.onOpen(detailObj, detailObj.postId)">
-						<span>回复</span>
-						<span style="padding-left: 8px">{{ detailObj.replyCount }}</span>
-					</span>
-					<a-tooltip title="举报" v-if="detailObj.userNickName">
-						<WarningOutlined class="ml-2" @click="reportFormRef.onOpen(detailObj, detailObj.postId)" />
-					</a-tooltip>
+					</div>
+					<div v-if="detailObj.userNickName" class="ml-2">
+						<a-tooltip title="踩">
+							<template v-if="detailObj.postnotlike == 1">
+								<dislike-outlined :style="{ color: '#fa6c8d' }" @click="postnotlike(detailObj, 0)" />
+							</template>
+							<template v-else>
+								<dislike-outlined @click="postnotlike(detailObj, 1)" />
+							</template>
+						</a-tooltip>
+						<span style="padding-left: 3px">
+							{{ detailObj.likeCount }}
+						</span>
+					</div>
+					<div
+						v-if="detailObj.isSelf == 1"
+						style="cursor: pointer"
+						@click="formRef.onOpen(detailObj, detailObj.postId)"
+					>
+						<EditOutlined class="ml-2" />
+						<span style="padding-left: 3px">编辑</span>
+					</div>
+					<div class="ml-2" style="cursor: pointer" @click="replyFormRef.onOpen(detailObj, detailObj.postId)">
+						<span>评论</span>
+						<span style="padding-left: 3px">{{ detailObj.replyCount }}</span>
+					</div>
+					<div
+						class="ml-2"
+						style="cursor: pointer"
+						v-if="detailObj.userNickName"
+						@click="reportFormRef.onOpen(detailObj, detailObj.postId)"
+					>
+						<WarningOutlined />
+						<span style="padding-left: 3px">举报</span>
+					</div>
 				</div>
 			</a-card>
+			<a-button type="primary" class="mt-2" @click="replyFormRef.onOpen(detailObj, detailObj.postId)">
+				<template #icon><comment-outlined /></template>
+				评论
+			</a-button>
 			<a-card class="mt-2">
 				<Comment
 					:commentList="detailObj.replyList?.records"
@@ -76,6 +118,7 @@
 	const replyFormRef = ref()
 	const formRef = ref()
 	const route = useRoute()
+	const router = useRouter()
 	const detailObj = ref({})
 	const moreType = ref(true)
 	const commentParams = ref({
@@ -92,6 +135,12 @@
 			resetGetList()
 		})
 	}
+	function postnotlike(item, l) {
+		forumApi.postPostnotlikeSubmit({ likeType: 0, targetId: route.query.postId }, item.postnotlike).then((data) => {
+			item.isLike = l
+			resetGetList()
+		})
+	}
 	function formatDateTime(val) {
 		if (!val) return ''
 		return parseTime(val, '{y}-{m}-{d} {h}:{i}:{s}')
@@ -140,7 +189,7 @@
 				chapterId: route.query.chapterId,
 				courseName: route.query.courseName,
 				chapterName: route.query.chapterName,
-				courseName:`${route.query.courseName}-${route.query.title}-${route.query.chapterName}`,
+				courseName: `${route.query.courseName}-${route.query.title}-${route.query.chapterName}`,
 				...pagination.value
 			})
 			.then((data) => {
@@ -156,6 +205,10 @@
 				}
 			})
 	}
+	//回到首页
+	const goToHome = () => {
+		router.go(-1)
+	}
 	onMounted(() => {
 		if (route.query.postId) {
 			getDetail()
@@ -184,4 +237,14 @@
 		font-size: 14px;
 		color: #696969;
 	}
+	.flt {
+		display: flex;
+		justify-content: flex-start;
+		align-items: flex-start;
+	}
+	.flc {
+		display: flex;
+		justify-content: flex-start;
+		align-items: center;
+	}
 </style>

+ 36 - 14
src/views/forum/form.vue

@@ -8,7 +8,7 @@
 		@close="onClose"
 		placement="bottom"
 		:mask="false"
-		style="width: 960px;left: calc(50% - 960px / 2);"
+		style="width: 960px; left: calc(50% - 960px / 2)"
 	>
 		<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
 			<a-row :gutter="16">
@@ -21,11 +21,9 @@
 					<a-form-item label="分类:" name="typeId">
 						<a-select
 							v-model:value="formData.typeId"
-							show-search
 							placeholder="请选择分类"
 							style="width: 100%"
 							:options="typeOptions"
-							:filter-option="filterOption"
 						></a-select>
 					</a-form-item>
 				</a-col>
@@ -39,6 +37,7 @@
 							style="width: 100%"
 							:options="usertypeOptions"
 							:filter-option="filterOption"
+							@popupScroll="popupScroll"
 						></a-select>
 					</a-form-item>
 				</a-col>
@@ -80,9 +79,40 @@
 	const moduleId = ref('')
 	const usertypeOptions = ref([])
 	const filterOption = (input, option) => {
-		return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
+		return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+	}
+	const popupScroll = (el) => {
+		let offset = 0
+		if (el.target.scrollHeight - el.target.scrollTop - offset <= el.target.clientHeight) {
+			userPagination.value.current += 1
+			getUserAllList(true)
+		}
+	}
+	const userPagination = ref({
+		size: 10,
+		current: 1,
+	})
+	const userTotal = ref(0)
+	//获取用户
+	const getUserAllList = (add) => {
+		if (userPagination.value.size * userPagination.value.current < userTotal.value) {
+			forumApi.allUserList(userPagination.value).then((res) => {
+				userTotal.value = res.total
+				let userList = res.records.map((r) => {
+					return {
+						label: r.name,
+						value: r.id,
+						...r
+					}
+				})
+				if (add) {
+					usertypeOptions.value.push(userList)
+				} else {
+					usertypeOptions.value = userList
+				}
+			})
+		}
 	}
-
 	// 打开抽屉
 	const onOpen = (record, module) => {
 		moduleId.value = module
@@ -96,15 +126,7 @@
 				}
 			})
 		})
-		forumApi.allUserList().then((data) => {
-			usertypeOptions.value = data.map((r) => {
-				return {
-					label: r.name,
-					value: r.id,
-					...r
-				}
-			})
-		})
+		getUserAllList()
 		if (module) {
 			if (record.appointUser) {
 				record.appointUserArr = record.appointUser.split(',')

+ 1 - 1
src/views/forum/replyForm.vue

@@ -1,6 +1,6 @@
 <template>
 	<xn-form-container
-		title="回复"
+		title="评论"
 		:height="500"
 		placement="bottom"
 		:mask="false"

+ 1 - 3
src/views/forum/reportForm.vue

@@ -1,6 +1,6 @@
 <template>
 	<xn-form-container
-		title="回复"
+		title="举报"
 		:height="500"
 		placement="bottom"
 		:mask="false"
@@ -15,11 +15,9 @@
 					<a-form-item label="举报类型:" name="reportReasonType">
 						<a-select
 							v-model:value="formData.reportReasonType"
-							show-search
 							placeholder="所有分类"
 							style="width: 100%"
 							:options="typeOptions"
-							:filter-option="filterOption"
 							@change="handleChange"
 						></a-select>
 					</a-form-item>

+ 1 - 1
src/views/portal/components/Header.vue

@@ -10,7 +10,7 @@
 				<a-sub-menu key="myList">
 					<template #title>我的</template>
 					<a-menu-item key="student/paper/1">我的考试</a-menu-item>
-					<!-- <a-menu-item key="student/paper/2">章节测验</a-menu-item> -->
+					<a-menu-item key="student/paper/2">章节测验</a-menu-item>
 					<a-menu-item key="student/paper/3">调查问卷</a-menu-item>
 					<a-menu-item key="student/paper/4">我的作业</a-menu-item>
 					<a-menu-item key="student/classCollect">课程收藏</a-menu-item>

+ 1 - 3
src/views/portal/index.vue

@@ -8,13 +8,11 @@
 				</div>
 			</a-layout-content>
 		</a-layout>
-		<Footer />
 	</div>
 </template>
 
 <script setup>
 	import Header from './components/Header.vue'
-	import Footer from './components/Footer.vue'
 	import { useRouter, useRoute } from 'vue-router'
 	const route = useRoute()
 	const router = useRouter()
@@ -27,6 +25,6 @@
 		justify-content: center;
 	}
 	.main-content-wrapper {
-		min-height: calc(100vh - 130px);
+		min-height: calc(100vh - 55px);
 	}
 </style>

+ 11 - 5
src/views/student/classCentre/ask.vue

@@ -3,7 +3,7 @@
 		<a-row :gutter="16">
 			<a-col :span="24">
 				<a-form-item name="info" label="问题">
-					<a-textarea v-model:value="formDataAdd.info" placeholder="请输入问题" :rows="4" />
+					<xn-editor v-model="formDataAdd.info" placeholder="请输入问题" :height="400"></xn-editor>
 				</a-form-item>
 			</a-col>
 		</a-row>
@@ -31,7 +31,7 @@
 								<a-avatar :src="item.avatar" />
 							</template>
 						</a-list-item-meta>
-						{{ item.info }}
+						<div v-html="item.info"></div>
 						<div class="frc mt-2" v-if="item.userId == userInfo.id">
 							<div @click="editNote(item)">
 								<a-tooltip title="编辑" :getPopupContainer="(trigger) => trigger.parentElement">
@@ -61,7 +61,7 @@
 									<a-avatar :src="item.avatar" />
 								</div>
 							</div>
-							<div>{{ item.info }}</div>
+							<div v-html="item.info"></div>
 						</div>
 					</div>
 				</a-skeleton>
@@ -73,7 +73,7 @@
 			<a-row :gutter="16">
 				<a-col :span="24">
 					<a-form-item name="info" label="问题">
-						<a-textarea v-model:value="formData.info" placeholder="请输入问题" :rows="4" />
+						<xn-editor v-model="formDataAdd.info" placeholder="请输入问题" :height="400"></xn-editor>
 					</a-form-item>
 				</a-col>
 			</a-row>
@@ -89,6 +89,7 @@
 	import askDiv from './ask.vue'
 	import tool from '@/utils/tool'
 	import { required } from '@/utils/formRules'
+	import XnEditor from '@/components/Editor/index.vue'
 	const userInfo = tool.data.get('USER_INFO')
 	// 表单数据,也就是默认给一些数据
 	const visible = ref(false)
@@ -108,7 +109,7 @@
 		}
 	})
 	const count = 3
-	const emit = defineEmits({ edit: null })
+	const emit = defineEmits({ videoStopTime: null })
 	const initLoading = ref(true)
 	const loading = ref(false)
 	const data = ref([])
@@ -196,6 +197,8 @@
 				})
 		})
 	}
+	//视频暂停的时间
+	const videoStopTime = ref(0)
 	//添加
 	const submitForm = (e) => {
 		formRefAdd.value
@@ -210,6 +213,9 @@
 					.then(() => {
 						formRefAdd.value.resetFields()
 						getList()
+						emit('videoStopTime', (e) => {
+							videoStopTime.value = e * 1000
+						})
 					})
 			})
 			.finally(() => {

+ 7 - 4
src/views/student/classCentre/form.vue

@@ -1,6 +1,6 @@
 <template>
 	<xn-form-container
-		:width="500"
+		:width="620"
 		:get-container="false"
 		:visible="visible"
 		:destroy-on-close="true"
@@ -10,11 +10,11 @@
 		<div v-if="itemObj.key == 2" style="height: 100%">
 			<handouts :itemObj="itemObj" :hourId="idsObj.hourId"></handouts>
 		</div>
-		<note v-if="itemObj.type == 2" :idsObj="idsObj" ref="noteRef" @edit="editForm"></note>
+		<note v-if="itemObj.type == 2" :idsObj="idsObj" ref="noteRef" @videoStopTime="videoStopTime"></note>
 		<div v-if="itemObj.key == 3" style="height: 100%">
 			<subtitleBox :url="itemObj.url" @videoSpeed="videoSpeed"></subtitleBox>
 		</div>
-		<askDiv v-if="itemObj.type == 4" :idsObj="idsObj" ref="noteRef" @edit="editForm"></askDiv>
+		<askDiv v-if="itemObj.type == 4" :idsObj="idsObj" ref="noteRef" @videoStopTime="videoStopTime"></askDiv>
 	</xn-form-container>
 </template>
 
@@ -43,7 +43,7 @@
 	const activeKey = ref('1')
 	// 默认是关闭状态
 	const visible = ref(false)
-	const emit = defineEmits({ videoSpeed: null })
+	const emit = defineEmits({ videoSpeed: null ,videoStopTime:null})
 	// 打开抽屉
 	const onOpen = (edit) => {
 		visible.value = true
@@ -64,6 +64,9 @@
 	const videoSpeed = (e) => {
 		emit('videoSpeed', e)
 	}
+	const videoStopTime = (e) => {
+		emit('videoStopTime', e)
+	}
 	const showPdf = ref(true)
 	function errorHandler() {
 		showPdf.value = false

文件差异内容过多而无法显示
+ 24 - 14
src/views/student/classCentre/index.vue


+ 10 - 4
src/views/student/classCentre/note.vue

@@ -3,7 +3,7 @@
 		<a-row :gutter="16">
 			<a-col :span="24">
 				<a-form-item name="noteContent" label="笔记">
-					<a-textarea v-model:value="formDataAdd.noteContent" placeholder="请输入笔记" :rows="4" />
+					<xn-editor v-model="formDataAdd.noteContent" placeholder="请输入笔记" :height="400"></xn-editor>
 				</a-form-item>
 			</a-col>
 		</a-row>
@@ -29,7 +29,7 @@
 								<a-avatar :src="item.avatar" />
 							</template>
 						</a-list-item-meta>
-						{{ item.noteContent }}
+						<div v-html="item.noteContent"></div>
 						<div class="frc">
 							<div @click="editNote(item)">
 								<a-tooltip title="编辑" :getPopupContainer="(trigger) => trigger.parentElement">
@@ -52,7 +52,7 @@
 			<a-row :gutter="16">
 				<a-col :span="24">
 					<a-form-item name="noteContent" label="问题">
-						<a-textarea v-model:value="formData.noteContent" placeholder="请输入问题" :rows="4" />
+						<xn-editor v-model="formDataAdd.noteContent" placeholder="请输入笔记" :height="400"></xn-editor>
 					</a-form-item>
 				</a-col>
 			</a-row>
@@ -66,6 +66,7 @@
 	import { createVNode } from 'vue'
 	import { Modal } from 'ant-design-vue'
 	import { required } from '@/utils/formRules'
+	import XnEditor from '@/components/Editor/index.vue'
 	// 表单数据,也就是默认给一些数据
 	const visible = ref(false)
 	const formData = ref({})
@@ -84,7 +85,7 @@
 		}
 	})
 	const count = 3
-	const emit = defineEmits({ edit: null })
+	const emit = defineEmits({ videoStopTime: null })
 	const initLoading = ref(true)
 	const loading = ref(false)
 	const data = ref([])
@@ -165,6 +166,8 @@
 				})
 		})
 	}
+	//视频暂停的时间
+	const videoStopTime = ref(0)
 	//添加
 	const submitForm = (e) => {
 		formRefAdd.value
@@ -179,6 +182,9 @@
 					.then(() => {
 						formRefAdd.value.resetFields()
 						getList()
+						emit('videoStopTime', (e) => {
+							videoStopTime.value = e * 1000
+						})
 					})
 			})
 			.finally(() => {

+ 5 - 2
src/views/student/classCentre/rightMenu.vue

@@ -8,7 +8,7 @@
 				<div class="fcc">{{ item.title }}</div>
 			</a-menu-item>
 		</a-menu>
-		<rightContent ref="formRef" :idsObj="props.idsObj" :rightItem="rightItem" @videoSpeed="videoSpeed"></rightContent>
+		<rightContent ref="formRef" :idsObj="props.idsObj" :rightItem="rightItem" @videoSpeed="videoSpeed" @videoStopTime="videoStopTime"></rightContent>
 	</div>
 </template>
 
@@ -19,7 +19,7 @@
 	import { message } from 'ant-design-vue'
 	const route = useRoute()
 	const router = useRouter()
-	const emit = defineEmits({ videoSpeed: null })
+	const emit = defineEmits({ videoSpeed: null,videoStopTime:null })
 	const props = defineProps({
 		dataList: {
 			type: [Array, Object],
@@ -133,6 +133,9 @@
 	const onClose = () => {
 		formRef.value.onClose()
 	}
+	const videoStopTime = (e) => {
+		emit('videoStopTime', e)
+	}
 	defineExpose({
 		selectBtn,
 		onClose

+ 3 - 3
src/views/student/exam/components/QuestionAnswerShow.vue

@@ -68,7 +68,7 @@
 				</div>
 			</div>
 			<!-- 结果、分数、难度、解析、正确答案 -->
-			<div class="question-answer-show-item" style="margin-top: 15px">
+			<div class="question-answer-show-item" style="margin-top: 15px" v-if="paperType!=5">
 				<span class="question-show-item">结果:</span>
 				<a-tag :color="doRightTagFormatter(answer.doRight)">
 					{{ doRightTextFormatter(answer.doRight) }}
@@ -78,7 +78,7 @@
 				<span class="question-show-item">分数:</span>
 				<span>{{ question.score }}</span>
 			</div>
-			<div class="question-answer-show-item">
+			<div class="question-answer-show-item" v-if="paperType!=5">
 				<span class="question-show-item">难度:</span>
 				<a-rate :value="question.difficult" disabled class="question-show-item" />
 			</div>
@@ -122,7 +122,7 @@
 			default: 0
 		},
 		paperType: {
-			type: String,
+			type: [String,Number],
 			default: ''
 		},
 		askType:{

+ 8 - 5
src/views/student/exam/paper/do.vue

@@ -11,7 +11,7 @@
 						{{ item.itemOrder }}
 					</a-tag>
 				</span>
-				<span class="do-exam-time">
+				<span class="do-exam-time" v-if="form.paperType!=5">
 					<label>剩余时间:</label>
 					<label>{{ formatSeconds(remainTime) }}</label>
 				</span>
@@ -30,7 +30,7 @@
 		<a-layout class="app-item-contain">
 			<a-layout-header class="align-center">
 				<h1>{{ form.name }}</h1>
-				<div>
+				<div v-if="form.paperType!=5">
 					<span class="question-title-padding">试卷总分:{{ form.score }}</span>
 					<span class="question-title-padding">考试时间:{{ form.suggestTime }}分钟</span>
 				</div>
@@ -80,6 +80,7 @@
 	import examPaperApi from '@/api/student/examPaper'
 	import examPaperAnswerApi from '@/api/student/examPaperAnswer'
 	import { Modal } from 'ant-design-vue'
+	import Broadcast from '@/utils/Broadcast.js'
 	import '../../style.less'
 	const route = useRoute()
 	const router = useRouter()
@@ -149,7 +150,6 @@
 			}
 		}, 1000)
 	}
-	const channel = new BroadcastChannel('getTaskList')
 	function submitForm() {
 		clearInterval(timer.value)
 		formLoading.value = true
@@ -161,7 +161,9 @@
 					content: `试卷得分:${re}分`,
 					okText: '返回',
 					onOk: () => {
-						channel.postMessage({ type: 1 })
+						// 发送消息
+						Broadcast.emit('getTaskList', { type: 1, message: 'Hello' })
+						Broadcast.emit('getClassDetail', { type: 1, message: 'Hello' })
 						window.close()
 					}
 				})
@@ -196,4 +198,5 @@
 			title: form.name
 		}
 	})
-</script>
+	</script>
+	

+ 1 - 1
src/views/student/exam/paper/edit.vue

@@ -23,7 +23,7 @@
 		<a-layout class="app-item-contain">
 			<a-layout-header class="align-center">
 				<h1>{{ form.name }}</h1>
-				<div>
+				<div v-if="form.paperType!=5">
 					<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
 					<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
 				</div>

+ 4 - 4
src/views/student/exam/paper/read.vue

@@ -23,8 +23,8 @@
 		<a-layout class="app-item-contain">
 			<a-layout-header class="align-center">
 				<h1>{{ form.name }}</h1>
-				<div>
-					<span class="question-title-padding" v-if="form.paperType!=5">试卷得分:{{ answer.score }}</span>
+				<div v-if="form.paperType != 5">
+					<span class="question-title-padding">试卷得分:{{ answer.score }}</span>
 					<span class="question-title-padding">试卷耗时:{{ formatSeconds(answer.doTime) }}</span>
 				</div>
 			</a-layout-header>
@@ -58,7 +58,7 @@
 				</a-spin>
 			</a-layout-content>
 		</a-layout>
-		<forumBtn :forumData="forumData" :resourceType="resourceType" :isShow="[1,2,3]"></forumBtn>
+		<forumBtn :forumData="forumData" :resourceType="resourceType" :isShow="[1, 2, 3]"></forumBtn>
 	</div>
 </template>
 
@@ -126,7 +126,7 @@
 	const forumData = computed(() => {
 		return {
 			id: route.query.id,
-			title: form.value.name ,
+			title: form.value.name
 		}
 	})
 </script>

+ 8 - 6
src/views/student/paper/index.vue

@@ -65,6 +65,7 @@
 	import { useRoute } from 'vue-router'
 	import tool from '@/utils/tool'
 	import { parseTime } from '@/utils/exam'
+	import Broadcast from '@/utils/Broadcast.js'
 	const route = useRoute()
 	const formatDateTime = (val) => {
 		if (!val) return ''
@@ -189,19 +190,20 @@
 	// lifecycle
 	const examType = ref()
 	const paperType = ref(2)
-	const channel = new BroadcastChannel('getTaskList')
-	channel.onmessage = function (event) {
+
+	// 监听消息
+	const stopListening = Broadcast.on('channelName', (event) => {
 		if (event.data.type == 1) {
 			getTaskList()
 		}
-	}
+	})
+
 	onMounted(() => {
 		examType.value = route.params && route.params.examType
 		getTaskList()
 	})
-	onUnmounted(() => {
-		channel.onmessage = null
-		channel.close()
+	onBeforeUnmount(() => {
+		stopListening()
 	})
 </script>
 

+ 191 - 32
src/views/student/user/index.vue

@@ -4,57 +4,216 @@
 			<a-card :bordered="false">
 				<div class="account-center-avatarHolder">
 					<div class="avatar">
-						<img :src="userInfo.avatar" />
+						<a-spin size="small" :spinning="avatarLoading">
+							<img :src="userInfo.avatar" />
+						</a-spin>
+						<a @click="uploadLogo">
+							<div :class="userInfo.avatar ? 'mask' : 'mask-notImg'"><upload-outlined /></div>
+						</a>
 					</div>
 					<div class="username">{{ userInfo.name }}</div>
 					<div class="bio">{{ userInfo.nickname }}</div>
 				</div>
-				<div class="account-center-detail">
-					<p>{{ userInfo.gradesIdName }}</p>
-					<p>{{ userInfo.majorIdName }}</p>
+				<div>
+					<p>班级:{{ userInfo.gradesIdName }}</p>
+					<p>专业:{{ userInfo.majorIdName }}</p>
 				</div>
 			</a-card>
 		</a-col>
 		<a-col :xs="24" :sm="24" :md="17" :lg="17" :xl="17">
 			<a-card>
-				<a-descriptions :title="`${userInfo.name}个人信息`" :column="1">
-					<a-descriptions-item label="性别">{{ userInfo.gender }}</a-descriptions-item>
-					<a-descriptions-item label="邮箱">{{ userInfo.email }}</a-descriptions-item>
-					<a-descriptions-item label="手机">{{ userInfo.phone }}</a-descriptions-item>
-					<a-descriptions-item label="生日">{{ userInfo.birthday }}</a-descriptions-item>
-					<a-descriptions-item label="学号">{{ userInfo.studentNum }}</a-descriptions-item>
-					<a-descriptions-item label="入学时间">{{ userInfo.fallDue }}</a-descriptions-item>
-					<a-descriptions-item label="班级">{{ userInfo.gradesIdName }}</a-descriptions-item>
-					<a-descriptions-item label="专业">{{ userInfo.majorIdName }}</a-descriptions-item>
-				</a-descriptions>
+				<a-form
+					ref="formRef"
+					:model="formData"
+					:rules="formRules"
+					v-bind="layout"
+					:label-col="{ ...layout.labelCol, offset: 0 }"
+					:wrapper-col="{ ...layout.wrapperCol, offset: 0 }"
+				>
+					<a-form-item label="账号:" name="account">
+						<span>{{ formData.account }}</span>
+					</a-form-item>
+					<a-form-item label="学号" name="account">
+						<span>{{ formData.studentNum }}</span>
+					</a-form-item>
+					<a-form-item label="班级" name="account">
+						<span>{{ formData.gradesIdName }}</span>
+					</a-form-item>
+					<a-form-item label="专业" name="account">
+						<span>{{ formData.majorIdName }}</span>
+					</a-form-item>
+					<a-form-item label="入学时间:" name="account">
+						<span>{{ formData.fallDue }}</span>
+					</a-form-item>
+					<a-form-item label="姓名:" name="name">
+						<a-input v-model:value="formData.name" placeholder="请输入姓名" allow-clear />
+					</a-form-item>
+					<a-form-item label="手机:" name="phone">
+						<a-input v-model:value="formData.phone" :maxlength="11" placeholder="请输入手机" allow-clear />
+					</a-form-item>
+					<a-form-item label="性别:" name="sex">
+						<a-radio-group v-model:value="formData.gender" :options="genderOptions" />
+					</a-form-item>
+					<a-form-item label="生日:" name="birthday">
+						<a-date-picker v-model:value="formData.birthday" value-format="YYYY-MM-DD" style="width: 100%" />
+					</a-form-item>
+					<a-form-item label="邮箱:" name="email">
+						<a-input v-model:value="formData.email" :maxlength="200" placeholder="请输入邮箱" allow-clear />
+					</a-form-item>
+					<a-form-item label="教育程度:" name="leval">
+						<a-select
+							v-model:value="formData.leval"
+							placeholder="请选择教育程度"
+							style="width: 100%"
+							:options="typeOptions"
+							allow-clear
+						></a-select>
+					</a-form-item>
+					<a-form-item label="所属单位:" name="unit">
+						<a-input v-model:value="formData.unit" :maxlength="200" placeholder="请输入所属单位" allow-clear />
+					</a-form-item>
+					<a-form-item label="兴趣爱好:" name="interests">
+						<a-input v-model:value="formData.interests" :maxlength="200" placeholder="请输入兴趣爱好" allow-clear />
+					</a-form-item>
+
+					<a-form-item :wrapper-col="{ ...layout.wrapperCol, offset: layout.labelCol.span }">
+						<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存基本信息</a-button>
+					</a-form-item>
+				</a-form>
 			</a-card>
 		</a-col>
+		<CropUpload ref="cropUpload" :img-src="userInfo.avatar" @successful="cropUploadSuccess" />
 	</a-row>
 </template>
 
 <script setup>
+	import CropUpload from '@/components/CropUpload/index.vue'
+	import userCenterApi from '@/api/sys/userCenterApi'
 	import tool from '@/utils/tool'
-	const userInfo = tool.data.get('USER_INFO')
-</script>
+	import { cloneDeep } from 'lodash-es'
+	import { globalStore } from '@/store'
+	import { required } from '@/utils/formRules'
+	const global_store = globalStore()
+	const userInfo = computed(() => {
+		return global_store.userInfo
+	})
+	const typeOptions = tool.dictList('LEVAL').sort(function (a, b) {
+		return a.value - b.value
+	})
+	const cropUpload = ref()
+	const avatarLoading = ref(false)
+	const uploadLogo = () => {
+		cropUpload.value.show()
+	}
+	// 头像裁剪图片回调
+	const cropUploadSuccess = (data) => {
+		// 转换为file类型
+		const result = new File([data.blobData], data.fileName, { type: 'image/jpeg', lastModified: Date.now() })
+		const fileData = new FormData()
+		fileData.append('file', result)
+		avatarLoading.value = true
+		userCenterApi.userUpdateAvatar(fileData).then((data) => {
+			avatarLoading.value = false
+			userInfo.value.avatar = data
+			// 更新缓存
+			tool.data.set('USER_INFO', userInfo.value)
+			global_store.setUserInfo(userInfo.value)
+		})
+	}
 
-<style lang="less" scoped>
-	.avatar {
-		margin: 0 auto;
-		width: 104px;
-		height: 104px;
-		margin-bottom: 20px;
-		border-radius: 50%;
-		overflow: hidden;
-		img {
-			height: 100%;
-			width: 100%;
+	const formRef = ref()
+	let formData = ref({})
+	formData.value = cloneDeep(global_store.userInfo)
+	const submitLoading = ref(false)
+	// 默认要校验的
+	const formRules = {
+		name: [required('请输入姓名')],
+		gender: [required('请选择性别')]
+	}
+	const genderOptions = tool.dictList('GENDER')
+	// 验证并提交数据
+	const onSubmit = () => {
+		formRef.value
+			.validate()
+			.then(() => {
+				submitLoading.value = true
+				userCenterApi.userUpdateUserInfo(formData.value).then(() => {
+					submitLoading.value = false
+					// 更新前端缓存
+					global_store.setUserInfo(cloneDeep(formData.value))
+					tool.data.set('USER_INFO', formData.value)
+				})
+			})
+			.catch(() => {
+				submitLoading.value = false
+			})
+	}
+	const layout = {
+		labelCol: {
+			span: 3
+		},
+		wrapperCol: {
+			span: 21
 		}
 	}
-	.username {
-		font-size: 20px;
-		line-height: 28px;
-		font-weight: 500;
-		margin-bottom: 4px;
+</script>
+
+<style lang="less" scoped>
+	.account-center-avatarHolder {
 		text-align: center;
+		margin-bottom: 24px;
+		& > .avatar {
+			margin: 0 auto;
+			width: 104px;
+			height: 104px;
+			margin-bottom: 20px;
+			border-radius: 50%;
+			overflow: hidden;
+			img {
+				height: 100%;
+				width: 100%;
+			}
+		}
+		.mask {
+			border-radius: 50%;
+			position: absolute;
+			margin-top: -104px;
+			width: 104px;
+			height: 104px;
+			background: rgba(101, 101, 101, 0.6);
+			color: #ffffff;
+			opacity: 0;
+			font-size: 25px;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+		.mask-notImg {
+			border-radius: 50%;
+			position: absolute;
+			margin-top: -24px;
+			width: 104px;
+			height: 104px;
+			background: rgba(101, 101, 101, 0.6);
+			color: #ffffff;
+			opacity: 0;
+			font-size: 25px;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+		.avatar a:hover .mask {
+			opacity: 1;
+		}
+		.avatar a:hover .mask-notImg {
+			opacity: 1;
+		}
+		.username {
+			font-size: 20px;
+			line-height: 28px;
+			font-weight: 500;
+			margin-bottom: 4px;
+			text-align: center;
+		}
 	}
 </style>

部分文件因为文件数量过多而无法显示