feat: 实现课程讨论管理和学习监控系统:新增讨论管理、评论查看、添加讨论页面;修复打包问题
BIN
public/images/teacher/@-ash.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/teacher/@.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/teacher/Image-ash.png
Normal file
After Width: | Height: | Size: 906 B |
BIN
public/images/teacher/Image.png
Normal file
After Width: | Height: | Size: 858 B |
BIN
public/images/teacher/delete2.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
public/images/teacher/expression-ash.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/teacher/expression.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/teacher/like.png
Normal file
After Width: | Height: | Size: 599 B |
BIN
public/images/teacher/reply.png
Normal file
After Width: | Height: | Size: 780 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 488 B |
@ -53,9 +53,12 @@ import FileViewer from '@/views/teacher/course/FileViewer.vue'
|
||||
import FolderBrowser from '@/views/teacher/course/FolderBrowser.vue'
|
||||
import CertificateManagement from '@/views/teacher/certificate/CertificateManagement.vue'
|
||||
import DiscussionManagement from '@/views/teacher/course/DiscussionManagement.vue'
|
||||
import CommentView from '@/views/teacher/course/CommentView.vue'
|
||||
import AddDiscussion from '@/views/teacher/course/AddDiscussion.vue'
|
||||
import StatisticsManagement from '@/views/teacher/statistics/StatisticsManagement.vue'
|
||||
import NotificationManagement from '@/views/teacher/course/NotificationManagement.vue'
|
||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||
import UserAgreement from '@/views/UserAgreement.vue'
|
||||
|
||||
// 作业子组件
|
||||
import HomeworkLibrary from '@/views/teacher/course/HomeworkLibrary.vue'
|
||||
@ -258,6 +261,18 @@ const routes: RouteRecordRaw[] = [
|
||||
component: DiscussionManagement,
|
||||
meta: { title: '讨论管理' }
|
||||
},
|
||||
{
|
||||
path: 'discussion/add',
|
||||
name: 'AddDiscussion',
|
||||
component: AddDiscussion,
|
||||
meta: { title: '添加讨论' }
|
||||
},
|
||||
{
|
||||
path: 'comment/:id',
|
||||
name: 'CommentView',
|
||||
component: CommentView,
|
||||
meta: { title: '评论详情' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'StatisticsManagement',
|
||||
@ -449,6 +464,14 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { title: '帮助中心' }
|
||||
},
|
||||
|
||||
// 用户协议
|
||||
{
|
||||
path: '/agreement',
|
||||
name: 'UserAgreement',
|
||||
component: UserAgreement,
|
||||
meta: { title: '用户协议' }
|
||||
},
|
||||
|
||||
// 学习中心(积分中心)
|
||||
{
|
||||
path: '/learning-center',
|
||||
|
@ -119,10 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
|
||||
|
||||
// 视频播放弹窗相关状态
|
||||
@ -484,6 +481,7 @@ const handleVideoError = (error: any) => {
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
305
src/views/UserAgreement.vue
Normal file
@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="user-agreement">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<span class="breadcrumb-side"></span>
|
||||
<div class="custom-breadcrumb">
|
||||
<span class="breadcrumb-item clickable first-item" @click="navigateTo('/teacher/course-management')">课程管理</span>
|
||||
<div class="breadcrumb-path">
|
||||
<span class="breadcrumb-separator">></span>
|
||||
<span class="breadcrumb-item clickable" @click="navigateTo('/teacher/course-management')">课程管理</span>
|
||||
<span class="breadcrumb-separator">></span>
|
||||
<span class="breadcrumb-item clickable" @click="navigateToDiscussion()">讨论</span>
|
||||
<span class="breadcrumb-separator">></span>
|
||||
<span class="breadcrumb-item clickable" @click="navigateToComment()">查看讨论</span>
|
||||
<span class="breadcrumb-separator">></span>
|
||||
<span class="breadcrumb-item current">用户协议</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 协议内容 -->
|
||||
<div class="agreement-content">
|
||||
<h1 class="agreement-title">用户协议</h1>
|
||||
<div class="agreement-date">2025.08.10 09:22</div>
|
||||
|
||||
<div class="agreement-text">
|
||||
<p>我们用户协议(以下简称"本协议")由以下双方根据《中华人民共和国合同法》等相关法律法规签署并遵照执行。</p>
|
||||
|
||||
<p><strong>用户</strong>:在手机、平板电脑、电脑等电子客户端注册成为我们应用软件(以下简称"我们""我们平台"或"平台方")的使用者,通过我们发布、查看、接收图文/音频/视频信息或其他文件,或与其他用户进行延时/实时交流的终端用户(以下简称"用户")。</p>
|
||||
|
||||
<p><strong>平台方</strong>:北京世纪超星信息技术发展有限责任公司(以下简称"平台方")。</p>
|
||||
|
||||
<p>本协议所指"用户"包括但不限于教师身份用户、家长身份用户和学生身份用户。</p>
|
||||
|
||||
<h2>第一条 用户注册</h2>
|
||||
<p>1.1 用户下载我们并根据我们的注册规则完成注册流程(包括但不限于设置用户名称及密码)后,即有权通过我们发布、查看、接收图文/音频/视频信息或其他文件,或与其他用户进行延时/实时交流。用户在我们上有效注册(或有效变更注册)的用户名及密码共同构成用户在我们的唯一有效身份证明。</p>
|
||||
|
||||
<p>1.2 用户一旦完成注册,则视为用户允许平台方通过短信、电子邮件、APP/服务器推送或其他方式向其发送与我们相关的信息。</p>
|
||||
|
||||
<p>1.3 用户应当保证其完成上述注册(或变更注册)时所提供的身份信息及电话、电子邮箱等必要信息真实、准确、有效;如此类信息有任何变动,用户应当在三日内通过我们完成信息更新。因用户提供虚假或无效信息导致平台方或其他用户遭受损失的,用户应当承担全部责任。</p>
|
||||
|
||||
<p>1.4 用户应自行妥善保管用户名及密码,除经平台方事先书面同意或本协议另有约定外,不得将其赠予、转让、出售或出借于他人使用;如用户发现其用户名及密码遭他人使用,应立即通知平台方以避免损失或损失扩大。因网络攻击、用户保管用户名及密码不当、转让或出借用户名及密码、怠于履行本协议及其他相关协议下的通知义务等情形或其他非平台方原因,导致用户未能正常使用我们或遭受任何损失的,由用户自行担责,平台方不承担任何责任。</p>
|
||||
|
||||
<p>1.5 用户需自行确认,在开始注册使用我们前,其应当具备中华人民共和国法律规定的与其行为相适应的民事行为能力。若用户不具备前述与用户行为相适应的民事行为能力,则其监护人应依照法律规定承担此用户行为产生的一切法律后果(未成年人应在其监护人监督指导下使用我们)。</p>
|
||||
|
||||
<p>1.6 用户注册的账户为用户自行设置并由用户自行保管,平台方任何时候均不会主动要求用户提供其账户密码。用户账户因用户的主动泄露或遭受他人攻击、诈骗等行为导致的损失及后果,平台方不承担责任,用户应自行通过司法、行政等救济途径向侵权行为人追偿。</p>
|
||||
|
||||
<p>1.7 如发生以下情形,平台方有权收回用户注册的账户,用户将不能再登录我们平台,相应服务同时终止:</p>
|
||||
<p>(一)经发现非由注册用户本人实际使用的;</p>
|
||||
<p>(二)连续三个月未用于登录我们平台;</p>
|
||||
<p>(三)其他平台方认为有必要的情形。</p>
|
||||
|
||||
<p>1.8 用户一旦完成注册,即视为完全了解、接受并同意遵守本协议项下的全部内容并受限于本协议相关条款。平台方有权根据法律法规、政策以及运营中的实际情况对协议进行修改,并对用户进行公布。修改后的协议条款一经平台方公布即替换本协议的原条款,构成用户与平台方之间就本协议的全部、最新协议。用户可以随时在我们应用程序中查阅最新协议条款。如果用户不接受平台方修改后的最新协议条款,可选择停止使用我们并注销其账户。如用户选择继续使用我们,则视为用户完全了解、接受并同意遵守平台方修改后的最新协议条款。用户承认并认可,已经完全了解并理解本协议相关内容,对本协议及相关内容不存在任何重大误解;同时认可本协议内容并不存在显失公平的情形,用户与平台方在协议中处于平等地位,用户对协议的接受与否具有自由选择的权利。</p>
|
||||
|
||||
<h2>第二条 服务内容</h2>
|
||||
<p>2.1 平台方为用户提供发布、查看、接收图文/音频/视频信息或其他文件,或与其他用户进行延时/实时交流的服务。</p>
|
||||
|
||||
<p>2.2 平台方作为我们软件的设计/开发/所有/经营者,有权对于我们软件不时作出更新和调整,只要用户仍为我们的注册用户,即视为其同意平台方作出的任何更新和调整。</p>
|
||||
|
||||
<p>2.3 平台方仅为用户提供发布、查看、接收图文/音频/视频信息或其他文件,或与其他用户进行延时/实时交流的技术支持,因用户自身原因造成的延迟确认、延迟回复、未按时保存、操作失误等情形,平台方不承担任何责任。</p>
|
||||
|
||||
<p>2.4 用户应当知晓在使用我们软件的过程中可能存在由其他用户进行通知、提醒、审批、定时、评论、发起问卷、投票等行为设置,平台方仅为前述行为提供技术支持,前述行为均为用户自身行为,与平台方无关。</p>
|
||||
|
||||
<p>2.5 用户应当知晓在使用我们软件的过程中可能涉及第三方服务软件的参与(包括但不限于微信、QQ、支付宝等),因前述第三方服务软件所造成的信息提供错误、信息提供延误、信息泄露、连接错误、服务器故障等情形与平台方无关。</p>
|
||||
|
||||
<h2>第三条 各方的权利义务</h2>
|
||||
<p>3.1 用户的权利义务</p>
|
||||
|
||||
<p>3.1.1 用户应遵守中国有关的法律法规规定及本协议约定条款。</p>
|
||||
|
||||
<p>3.1.2 用户向我们平台提供的各类信息应真实、准确、完整;用户在我们发布的各类信息(包括但不限于图文、音频、视频等)应符合中国有关的法律法规;用户通过我们进行的延时/实时交流活动应当符合中国有关的法律法规。经平台方发现用户违反前述约定的,用户向我们平台提供/发布的信息将不再适用本合同有关隐私保护的约定,平台方有权公示、向政府部门进行举报或直接向用户追究法律责任。平台方在任何时候有权验证用户提供/发布的信息,由于用户提供虚假或不完整信息所导致的任何责任和损失,应由用户自行承担。</p>
|
||||
|
||||
<p>3.1.3 用户利用我们平台发布虚假、侵犯他人隐私、侵犯他人知识产权、侮辱他人、造谣诽谤等违反法律法规或公序良俗的内容从而给平台方、其他用户或第三方造成损害的,由前述用户承担全部法律后果及赔偿责任。</p>
|
||||
|
||||
<p>3.1.4 用户不得有任何损害平台方权益的行为,若用户损害平台方权益的,平台方有权要求用户承担赔偿责任,情节严重的平台方将保留追究其法律责任的权利。</p>
|
||||
|
||||
<p>3.1.5 平台方暂时免费向用户提供本协议约定之服务,但平台方保留将来向用户收取费用的权利。用户之间自行产生的任何费用支付均与平台方无关。</p>
|
||||
|
||||
<p>3.2 平台方的权利义务</p>
|
||||
|
||||
<p>3.2.1 我们平台提供用户发布或接受信息/文件并与我们平台及时联接的终端设备(包括但不限于手机、平板电脑、电脑等电子客户端)中的应用程序,并应当在我们平台的变更、更新、优化后,及时通知或协助用户更新终端设备中的应用程序。</p>
|
||||
|
||||
<p>3.2.2 我们平台无法保证其所提供的信息中没有任何错误、缺陷、恶意软件或病毒。对于因使用(或无法使用)我们平台导致的任何损害,我们平台不承担责任(除非此类损害是由我们平台的故意或重大疏忽造成的)。此外,对于因使用(或无法使用)与我们平台的电子通信手段导致的任何损害,包括但不限于因电子通信传达失败或延时、第三方或用于电子通信的计算机程序对电子通信的拦截或操纵,以及病毒传输导致的损害,我们平台不承担责任。</p>
|
||||
|
||||
<h2>第四条 其他责任约定</h2>
|
||||
<p>4.1 因不可抗力(指本协议约定时不能预见、不能避免并不能克服的客观情况)导致平台方未能按约提供服务的,平台方不承担责任。</p>
|
||||
|
||||
<p>4.2 服务过程中因协议双方以外的其他第三方原因造成的损失,由该第三方承担法律后果及赔偿责任。</p>
|
||||
|
||||
<h2>第五条 用户信息</h2>
|
||||
<p>5.1 用户信息包含用户个人信息和用户非个人信息。</p>
|
||||
|
||||
<p>5.2 用户个人信息包括但不限于下列信息:用户真实姓名、性别、职业、任职/就读学校、头像、手机号码、IP地址等。</p>
|
||||
|
||||
<p>5.3 用户非个人信息包括但不限下列信息:一切属于第5.2条所述的用户个人信息范围以外的信息,均为普通信息,不属于用户个人信息;包括但不限于用户对我们服务的操作状态、使用记录、使用习惯等反应在平台方服务器端的全部记录信息。</p>
|
||||
|
||||
<p>5.4 重要提示:为向客户提供本协议所述服务,平台方将可能合理使用用户个人信息和非用户个人信息。用户一旦注册、登录、使用我们,将视为用户完全了解、同意并接受平台方通过包括但不限于收集、统计、分析、使用等方式合理使用用户信息,无需其他意思表示。为向用户完整地提供包括但不限于本协议所述的服务,平台方将可能要求用户上传用户信息(包括但不限于通讯录等),用户一旦选择上传用户信息,将视为用户完全了解、同意并接受平台方基于向用户提供服务的目的读取并合理使用用户信息。</p>
|
||||
|
||||
<p>5.5 用户认可其已完全了解平台方使用用户信息的目的在于为用户提供包括本协议所述的服务或将来可能新增的服务,平台方使用用户信息的方式包括但不限于:收集、统计、分析、商业用途的使用等方式;平台方使用用户信息的范围包括但不限于本条第5.2条、5.3条、5.4条所定义的信息等。</p>
|
||||
|
||||
<p>5.6 用户可以通过停止使用我们而不再向平台方提供用户信息。但是,在此之前已同意平台方使用的用户信息,平台方不承担主动删除、销毁的责任,并仍具有使用此类用户信息的权利。</p>
|
||||
|
||||
<p>5.7 除非用户另有特别声明,平台方对用户信息的使用无需向用户支付任何费用,并且在用户同意本协议的基础上,无需向用户另行取得授权。</p>
|
||||
|
||||
<p>5.8 平台方尊重用户的合法权利,不会以违反法律、行政法规以及本协议约定的方式收集、使用用户信息。</p>
|
||||
|
||||
<p>5.9 非因平台方违反本协议的约定而导致的用户信息的泄露与平台方无关。任何用户(包括但不限于教师身份用户、家长身份用户、学生身份用户)不得利用、泄露、散播通过我们平台获取的其他用户的用户信息;用户发生前述行为的,平台方不承担任何责任,由前述侵权用户承担全部责任。任何平台方、用户外的第三方不得利用、泄露、散播通过我们平台获取的其他用户的用户信息,第三方发生前述行为的,平台方不承担任何责任,由该第三方承担全部责任。</p>
|
||||
|
||||
<p>5.10 平台方对按照有关法律法规要求、按照相关政府主管部门/司法机关的要求而披露用户信息的行为不承担任何责任。</p>
|
||||
|
||||
<h2>第六条 知识产权</h2>
|
||||
<p>6.1 本协议所指知识产权是指与我们服务相关的各类过去有效的、现行有效的、或即将产生的知识产权,包括但不限于商标、著作权、计算机软件、发明、实用新型、外观设计以及提出相关申请的权利。</p>
|
||||
|
||||
<p>6.2 平台方为与我们服务相关的全部知识产权的权利人,对我们服务提供过程中包含的全部知识产权,包括但不限于文本、图片、图形、音频和/或视频资料享有及保留完整、独立的全部权利。未经平台方同意,用户及第三方不得在任何媒体直接或间接发布、播放、出于播放或发布目的而改写或再发行平台方享有的知识产权或者平台方提供的任何资料和信息,也不得将前述资料和信息用于任何商业目的。</p>
|
||||
|
||||
<p>6.3 对于用户本人创作并上传到我们的任何图文/音频/视频等,平台方保留对其内容进行实时监控的权利,并有权依平台方独立判断对任何违反本协议约定或涉嫌违法、违规的内容实施删除操作。平台方对于删除此类用户作品引起的任何后果或导致用户的任何损失不负任何责任。</p>
|
||||
|
||||
<p>6.4 知识产权条款持续有效,不因用户关闭我们账户或者停止使用我们服务而失效。</p>
|
||||
|
||||
<h2>第七条 其他免责声明</h2>
|
||||
<p>7.1 平台方或用户提供的全部信息仅依照此类信息提供时的现状提供并仅供用户参考,平台方不对前述信息的真实性、准确性、完整性、适用性等做任何承诺和保证。用户应对平台方或其他用户提供的信息自行判断,并承担因使用前述信息而引起的全部风险,包括因其对信息的真实性、准确性、完整性或适用性的任何依赖或信任而导致的风险。平台方无需对因用户使用信息的任何行为导致的任何损失承担责任。</p>
|
||||
|
||||
<p>7.2 对于因不可抗力或平台方不能预料、不能控制的原因(包括但不限于计算机病毒或黑客攻击、系统不稳定、用户不当使用账户、以及其他任何技术、互联网络、通信线路原因等)产生的包括但不限于用户计算机信息和数据的安全问题,用户个人信息的安全问题等给用户或任何第三方造成的损失,平台方不承担任何责任。</p>
|
||||
|
||||
<p>7.3 用户因违法、违规或违反本协议约定使用我们的行为,包括但不限于提供违法、不真实、不正当信息,侵犯第三方任何合法权益等,给平台方或其他第三方造成的任何损失,由用户自身承担由此造成的全部法律后果及损害赔偿责任。</p>
|
||||
|
||||
<p>7.4 用户完全了解并同意,在使用我们服务的过程中,可能存在来自任何其他用户的包括威胁性的、诽谤性的、令人反感的或非法的内容或行为,也可能存在对他人权利(包括知识产权)造成侵犯的匿名、冒名、或伪造的各种信息或行为,用户须自行判断相关内容、信息、行为的安全性等风险,并自行独立承担因上述行为给平台方、其他用户或第三方造成损害的一切法律后果。</p>
|
||||
|
||||
<p>7.5 平台方对其合作方通过我们提供的全部服务及内容不作任何形式的承诺或保证,不论是明确的或暗示的。前述承诺或保证包括但不限于:第三方通过我们提供的服务或内容的真实性、时效性、对特定用途的适用性、任何形式的所有权保证,非侵权保证等。平台方对因前述第三方服务内容导致的任何直接、间接、偶然、特殊及后续的损害,不承担任何责任。</p>
|
||||
|
||||
<h2>第八条 适用法律和争议解决</h2>
|
||||
<p>8.1 本协议的成立、生效、履行、解释及因本协议产生的任何争议,均适用中华人民共和国相关法律法规(不包括港澳台地区法律法规)。</p>
|
||||
|
||||
<p>8.2 用户和平台方之间与本协议有关的任何争议,首先应友好协商解决,在争议发生之日起三十日内仍不能通过协商达成一致的,用户同意将前述争议提交平台方所在地的人民法院进行诉讼。</p>
|
||||
|
||||
<p>8.3 如本协议中的任何条款因任何原因完全或部分无效或不具有执行力,均不影响本协议其他条款的效力。</p>
|
||||
|
||||
<p>8.4 本协议及本协议任何条款内容的最终解释权及修改权归平台方所有。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 通用跳转方法
|
||||
const navigateTo = (path) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 跳转到讨论页面
|
||||
const navigateToDiscussion = () => {
|
||||
// 从当前路由中获取课程ID,如果是从讨论页面跳转过来的
|
||||
const courseId = route.query.courseId || '1' // 默认课程ID,实际应该从路由参数获取
|
||||
router.push(`/teacher/course-editor/${courseId}/discussion`)
|
||||
}
|
||||
|
||||
// 跳转到查看讨论页面
|
||||
const navigateToComment = () => {
|
||||
// 从当前路由中获取课程ID和讨论ID
|
||||
const courseId = route.query.courseId || '1'
|
||||
const commentId = route.query.commentId || '1'
|
||||
router.push(`/teacher/course-editor/${courseId}/comment/${commentId}`)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-agreement {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
/* 面包屑导航 */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb-side {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0288D1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.custom-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-item.first-item {
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.breadcrumb-item.clickable:hover {
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.breadcrumb-item.current {
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.breadcrumb-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 8px;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 协议内容 */
|
||||
.agreement-content {
|
||||
background: white;
|
||||
padding: 40px 20px 20px 20px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.agreement-title {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.agreement-date {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1.5px solid #F1F3F4;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agreement-text h2 {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #666666;
|
||||
margin: 30px 0 15px 0;
|
||||
}
|
||||
|
||||
.agreement-text p {
|
||||
margin-bottom: 12px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.agreement-text strong {
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.user-agreement {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agreement-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agreement-text h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -434,6 +434,22 @@ const breadcrumbPathItems = computed(() => {
|
||||
path: currentPath
|
||||
}
|
||||
);
|
||||
} else if (currentPath.includes('discussion/add')) {
|
||||
// 添加讨论页面:课程管理 > 讨论 > 添加讨论
|
||||
breadcrumbs.push(
|
||||
{
|
||||
title: '课程管理',
|
||||
path: '/teacher/course-management'
|
||||
},
|
||||
{
|
||||
title: '讨论',
|
||||
path: `/teacher/course-editor/${courseId}/discussion`
|
||||
},
|
||||
{
|
||||
title: '添加讨论',
|
||||
path: currentPath
|
||||
}
|
||||
);
|
||||
} else if (currentPath.includes('discussion')) {
|
||||
breadcrumbs.push(
|
||||
{
|
||||
@ -445,6 +461,22 @@ const breadcrumbPathItems = computed(() => {
|
||||
path: `/teacher/course-editor/${courseId}`
|
||||
}
|
||||
);
|
||||
} else if (currentPath.includes('comment/')) {
|
||||
// 查看评论页面:课程管理 > 讨论 > 查看讨论
|
||||
breadcrumbs.push(
|
||||
{
|
||||
title: '课程管理',
|
||||
path: '/teacher/course-management'
|
||||
},
|
||||
{
|
||||
title: '讨论',
|
||||
path: `/teacher/course-editor/${courseId}/discussion`
|
||||
},
|
||||
{
|
||||
title: '查看讨论',
|
||||
path: currentPath
|
||||
}
|
||||
);
|
||||
} else if (currentPath.includes('statistics')) {
|
||||
breadcrumbs.push(
|
||||
{
|
||||
|
406
src/views/teacher/course/AddDiscussion.vue
Normal file
@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<div class="add-discussion">
|
||||
<!-- 页面标题 -->
|
||||
<h1 class="page-title">添加讨论</h1>
|
||||
|
||||
<!-- 讨论表单 -->
|
||||
<div class="discussion-form">
|
||||
<!-- 标题输入 -->
|
||||
<div class="form-group">
|
||||
<input v-model="discussionTitle" type="text" placeholder="请添加标题" class="title-input" />
|
||||
</div>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<div class="rich-editor">
|
||||
<div style="border: 1px solid #ccc">
|
||||
<Toolbar
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Editor
|
||||
style="height: 300px; overflow-y: hidden;"
|
||||
v-model="discussionContent"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 章节选择和操作按钮 -->
|
||||
<div class="section-actions-container">
|
||||
<!-- 章节选择 -->
|
||||
<div class="section-selector-wrapper">
|
||||
<button @click="toggleSectionSelector" class="section-selector" :class="{ active: showSectionSelector }">
|
||||
<span>{{ selectedSection || '选择章节' }}</span>
|
||||
<img :src="showSectionSelector ? '/images/teacher/箭头-蓝.png' : '/images/teacher/箭头-灰.png'" alt="箭头" class="arrow-icon" />
|
||||
</button>
|
||||
|
||||
<!-- 章节选择弹窗 -->
|
||||
<div v-if="showSectionSelector" class="section-popover">
|
||||
<div class="popover-header">选择章节:</div>
|
||||
<div class="section-list">
|
||||
<label v-for="section in sections" :key="section.id" class="section-item">
|
||||
<input type="radio" :value="section.id" v-model="selectedSectionId" class="section-radio" />
|
||||
<span class="section-name">{{ section.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="popover-actions">
|
||||
<n-button @click="cancelSectionSelection">取消</n-button>
|
||||
<n-button type="primary" @click="confirmSectionSelection">确认</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<n-button @click="cancelDiscussion">取消</n-button>
|
||||
<n-button type="primary" @click="publishDiscussion">发布</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议 -->
|
||||
<div class="agreement-text">
|
||||
发表该话题即表示您已阅读并接受<router-link to="/agreement" class="agreement-link">《用户协议》</router-link>,请遵守该协议
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton } from 'naive-ui'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// @ts-ignore
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef()
|
||||
|
||||
// 编辑器配置
|
||||
const toolbarConfig = {}
|
||||
const editorConfig = { placeholder: '请添加讨论内容...' }
|
||||
const mode = 'default'
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor // 记录 editor 实例,重要!
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const discussionTitle = ref('')
|
||||
const discussionContent = ref('')
|
||||
const selectedSection = ref('')
|
||||
const selectedSectionId = ref('')
|
||||
const showSectionSelector = ref(false)
|
||||
|
||||
// 章节数据
|
||||
const sections = ref([
|
||||
{ id: '1', name: '第一节 课程定位与目标' },
|
||||
{ id: '2', name: '第二节 课程定位与目标第二节课程定位与目标' },
|
||||
{ id: '3', name: '第二节 课程定位与目标第二节课程定位与目标' },
|
||||
{ id: '4', name: '第二节 课程定位与目标' }
|
||||
])
|
||||
|
||||
// 切换章节选择器
|
||||
const toggleSectionSelector = () => {
|
||||
showSectionSelector.value = !showSectionSelector.value
|
||||
}
|
||||
|
||||
// 取消章节选择
|
||||
const cancelSectionSelection = () => {
|
||||
showSectionSelector.value = false
|
||||
}
|
||||
|
||||
// 确认章节选择
|
||||
const confirmSectionSelection = () => {
|
||||
const section = sections.value.find(s => s.id === selectedSectionId.value)
|
||||
if (section) {
|
||||
selectedSection.value = section.name
|
||||
}
|
||||
showSectionSelector.value = false
|
||||
}
|
||||
|
||||
// 取消讨论
|
||||
const cancelDiscussion = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
// 发布讨论
|
||||
const publishDiscussion = () => {
|
||||
if (!discussionTitle.value.trim()) {
|
||||
alert('请输入讨论标题')
|
||||
return
|
||||
}
|
||||
if (!discussionContent.value.trim()) {
|
||||
alert('请输入讨论内容')
|
||||
return
|
||||
}
|
||||
if (!selectedSection.value) {
|
||||
alert('请选择章节')
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以调用API发布讨论
|
||||
console.log('发布讨论:', {
|
||||
title: discussionTitle.value,
|
||||
content: discussionContent.value,
|
||||
section: selectedSection.value
|
||||
})
|
||||
|
||||
// 发布成功后返回讨论列表
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时的初始化逻辑
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-discussion {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.discussion-form {
|
||||
background: white;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-actions-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-selector-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.title-input:focus {
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
.rich-editor {
|
||||
border: 1px solid #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* WangEditor 富文本编辑器样式已由组件处理 */
|
||||
|
||||
.section-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 256px;
|
||||
height: 39px;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-selector span {
|
||||
transition: color 0.2s;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.section-selector:hover,
|
||||
.section-selector.active {
|
||||
border-color: #0C99DA;
|
||||
}
|
||||
|
||||
.section-selector:hover span,
|
||||
.section-selector.active span {
|
||||
color: #0C99DA;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 13px;
|
||||
height: 8px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 444px;
|
||||
min-height: 200px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 24px 0px rgba(220, 220, 220, 0.5);
|
||||
z-index: 1000;
|
||||
margin-top: 45px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
padding-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
}
|
||||
|
||||
.section-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.section-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.section-radio {
|
||||
margin-right: 12px;
|
||||
accent-color: #0288D1;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popover-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button--default-type) {
|
||||
border: 1px solid #0288D1 !important;
|
||||
background: #E2F5FF !important;
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button--default-type:hover) {
|
||||
border-color: #0277BD !important;
|
||||
background: #D1F0FF !important;
|
||||
color: #0277BD !important;
|
||||
}
|
||||
|
||||
/* 自定义按钮样式已由n-button组件处理 */
|
||||
|
||||
.agreement-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #0C99DA;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.agreement-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-actions :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.form-actions :deep(.n-button--default-type) {
|
||||
border: 1px solid #0288D1 !important;
|
||||
background: #E2F5FF !important;
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
|
||||
.form-actions :deep(.n-button--default-type:hover) {
|
||||
border-color: #0277BD !important;
|
||||
background: #D1F0FF !important;
|
||||
color: #0277BD !important;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.add-discussion {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.discussion-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
863
src/views/teacher/course/CommentView.vue
Normal file
@ -0,0 +1,863 @@
|
||||
<template>
|
||||
<div class="comment-view">
|
||||
<!-- 讨论详情 -->
|
||||
<div class="discussion-detail">
|
||||
<div class="discussion-item">
|
||||
<!-- 操作按钮 -->
|
||||
<div class="discussion-actions">
|
||||
<!-- 更多操作菜单 -->
|
||||
<div class="more-actions">
|
||||
<n-dropdown :options="getMoreOptions(discussion)" @select="handleMoreAction" trigger="click">
|
||||
<n-button quaternary circle>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor"
|
||||
d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<img :src="discussion.avatar" :alt="discussion.author" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论内容 -->
|
||||
<div class="discussion-content">
|
||||
<div class="author-name">{{ discussion.author }}</div>
|
||||
<div class="topic-header">
|
||||
<h3 class="topic-title">{{ discussion.title }}</h3>
|
||||
<span v-if="discussion.isPinned" class="pinned-badge">置顶</span>
|
||||
</div>
|
||||
|
||||
<div class="topic-content">{{ discussion.content }}</div>
|
||||
|
||||
<div class="topic-meta">
|
||||
<span class="chapter-name">{{ discussion.chapterName }}</span>
|
||||
<div class="meta-row">
|
||||
<span class="timestamp">{{ discussion.timestamp }}</span>
|
||||
<div class="interaction-buttons">
|
||||
<n-button quaternary @click="toggleLike(discussion)" :class="{ liked: discussion.isLiked }">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/like.png" alt="点赞" style="width: 13px; height: 13px;" />
|
||||
</template>
|
||||
点赞
|
||||
</n-button>
|
||||
|
||||
<n-button quaternary @click="deleteDiscussion" style="color: #ff4d4f;">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/delete2.png" alt="删除" style="width: 13px; height: 13px;" />
|
||||
</template>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论区域 -->
|
||||
<div class="comments-section">
|
||||
<h3 class="section-title">全部评论 ({{ comments.length }}条)</h3>
|
||||
|
||||
<!-- 添加评论 -->
|
||||
<div class="add-comment">
|
||||
<div class="comment-input-area">
|
||||
<div class="user-avatar">
|
||||
<img src="/images/activity/1.png" alt="用户头像" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<n-input v-model:value="newComment" type="textarea" placeholder="回复讨论~" :maxlength="500"
|
||||
class="comment-textarea" :autosize="{ minRows: 3, maxRows: 3 }" />
|
||||
<div class="input-actions">
|
||||
<div class="action-icons">
|
||||
<div class="action-icon">
|
||||
<img src="/images/teacher/@-ash.png" alt="@" style="width: 26px; height: 22px;" />
|
||||
</div>
|
||||
<div class="action-icon">
|
||||
<img src="/images/teacher/expression-ash.png" alt="表情" style="width: 26px; height: 22px;" />
|
||||
</div>
|
||||
<div class="action-icon">
|
||||
<img src="/images/teacher/Image-ash.png" alt="图片" style="width: 26px; height: 22px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="agreement-text">
|
||||
上传图片、附件即表示您已阅读并接受<router-link to="/agreement" class="agreement-link">《用户协议》</router-link>,请遵守该协议。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-buttons">
|
||||
<n-button @click="cancelComment">取消</n-button>
|
||||
<n-button type="primary" @click="submitComment">发布</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div class="comment-list">
|
||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<div class="comment-avatar">
|
||||
<img :src="comment.avatar" :alt="comment.author" />
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">{{ comment.author }}</span>
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="action-btn time-btn">
|
||||
<span>{{ comment.timestamp }}</span>
|
||||
</button>
|
||||
<div v-if="!hasInstructorReply(comment)" class="action-buttons-right">
|
||||
<button class="action-btn">
|
||||
<img src="/images/teacher/like.png" alt="点赞" style="width: 12px; height: 12px;" />
|
||||
点赞
|
||||
</button>
|
||||
<button class="action-btn">
|
||||
<img src="/images/teacher/reply.png" alt="回复" style="width: 12px; height: 12px;" />
|
||||
回复
|
||||
</button>
|
||||
<button class="action-btn">
|
||||
<img src="/images/teacher/delete2.png" alt="删除" style="width: 12px; height: 12px;" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复列表 -->
|
||||
<div v-if="comment.replies && comment.replies.length > 0" class="comment-replies">
|
||||
<div v-for="reply in comment.replies" :key="reply.id" class="reply-item">
|
||||
<div class="reply-avatar">
|
||||
<img :src="reply.avatar" :alt="reply.author" />
|
||||
</div>
|
||||
<div class="reply-content">
|
||||
<div class="reply-main">
|
||||
<div class="reply-header">
|
||||
<span class="reply-username">{{ reply.author }}</span>
|
||||
<span v-if="reply.role" class="reply-badge" :class="getRoleClass(reply.role)">{{ reply.role
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="reply-text">{{ reply.content }}</div>
|
||||
</div>
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">{{ reply.timestamp }}</span>
|
||||
<div class="reply-actions">
|
||||
<button class="reply-action-btn">
|
||||
<img src="/images/teacher/like.png" alt="点赞" style="width: 12px; height: 12px;" />
|
||||
点赞
|
||||
</button>
|
||||
<button class="reply-action-btn">
|
||||
<img src="/images/teacher/reply.png" alt="回复" style="width: 12px; height: 12px;" />
|
||||
回复
|
||||
</button>
|
||||
<button class="reply-action-btn">
|
||||
<img src="/images/teacher/delete2.png" alt="删除" style="width: 12px; height: 12px;" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NButton, NInput, NIcon, NDropdown, useMessage } from 'naive-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const discussion = ref({
|
||||
id: 1,
|
||||
author: '王建',
|
||||
avatar: '/images/activity/1.png',
|
||||
title: '话题标题',
|
||||
content: '话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容',
|
||||
chapterName: '这是章节名称名称',
|
||||
timestamp: '7月20日 12:41',
|
||||
isPinned: true,
|
||||
isLiked: false,
|
||||
likes: 5
|
||||
})
|
||||
|
||||
const comments = ref([
|
||||
{
|
||||
id: 1,
|
||||
author: '春暖花开°C',
|
||||
avatar: '/images/activity/2.png',
|
||||
content: '为了让科学教育有效,它必须广泛包容,而且应该认识到科学教师、科学家、家庭和社区如何合作实现学习和教学的目标。',
|
||||
timestamp: '2025.07.23 16:28',
|
||||
likes: 2,
|
||||
replies: [
|
||||
{
|
||||
id: 11,
|
||||
author: '汪波',
|
||||
avatar: '/images/activity/3.png',
|
||||
role: '讲师',
|
||||
content: '欢迎大家又不懂的地方可以私信老师',
|
||||
timestamp: '2025.07.23 16:28',
|
||||
likes: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: '春暖花开°C',
|
||||
avatar: '/images/activity/2.png',
|
||||
content: '来了来了!老师讲的好好啊~',
|
||||
timestamp: '2025.07.23 16:28',
|
||||
likes: 1,
|
||||
replies: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: '春暖花开°C',
|
||||
avatar: '/images/activity/2.png',
|
||||
content: '为了让科学教育有效,它必须广泛包容,而且应该认识到科学教师、科学家、家庭和社区如何合作实现学习和教学的目标。',
|
||||
timestamp: '2025.07.23 16:28',
|
||||
likes: 0,
|
||||
replies: []
|
||||
}
|
||||
])
|
||||
|
||||
const newComment = ref('')
|
||||
|
||||
// 方法
|
||||
const toggleLike = (discussion: any) => {
|
||||
discussion.isLiked = !discussion.isLiked
|
||||
discussion.likes += discussion.isLiked ? 1 : -1
|
||||
}
|
||||
|
||||
const deleteDiscussion = () => {
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
const cancelComment = () => {
|
||||
newComment.value = ''
|
||||
}
|
||||
|
||||
const submitComment = () => {
|
||||
if (!newComment.value.trim()) {
|
||||
message.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
|
||||
const comment = {
|
||||
id: comments.value.length + 1,
|
||||
author: '当前用户',
|
||||
avatar: '/images/activity/1.png',
|
||||
content: newComment.value,
|
||||
timestamp: '刚刚',
|
||||
likes: 0,
|
||||
replies: []
|
||||
}
|
||||
|
||||
comments.value.push(comment)
|
||||
newComment.value = ''
|
||||
message.success('评论发布成功')
|
||||
}
|
||||
|
||||
// 获取更多操作选项
|
||||
const getMoreOptions = (discussion: any) => {
|
||||
const options = [
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/edit.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
if (discussion.isPinned) {
|
||||
options.push({
|
||||
label: '取消置顶',
|
||||
key: 'unpin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: '置顶',
|
||||
key: 'pin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// 处理更多操作
|
||||
const handleMoreAction = (key: string) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
message.info('编辑功能')
|
||||
break
|
||||
case 'pin':
|
||||
discussion.value.isPinned = true
|
||||
message.success('已置顶')
|
||||
break
|
||||
case 'unpin':
|
||||
discussion.value.isPinned = false
|
||||
message.success('已取消置顶')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色对应的CSS类名
|
||||
const getRoleClass = (role: string) => {
|
||||
const roleMap: { [key: string]: string } = {
|
||||
'讲师': 'instructor',
|
||||
'学员': 'user',
|
||||
'管理员': 'admin'
|
||||
}
|
||||
return roleMap[role] || 'user'
|
||||
}
|
||||
|
||||
// 检查评论是否有讲师回复
|
||||
const hasInstructorReply = (comment: any) => {
|
||||
if (!comment.replies || comment.replies.length === 0) {
|
||||
return false
|
||||
}
|
||||
return comment.replies.some((reply: any) => reply.role === '讲师')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const discussionId = route.params.id
|
||||
if (discussionId) {
|
||||
console.log('加载讨论ID:', discussionId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comment-view {
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 讨论详情 */
|
||||
.discussion-detail {}
|
||||
|
||||
.discussion-item {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
border-bottom: 1.5px solid #F1F3F4;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 用户信息 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 讨论内容 */
|
||||
.discussion-content {
|
||||
padding-top: 6px;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #0C99DA;
|
||||
margin: 0 8px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topic-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pinned-badge {
|
||||
background: #fff;
|
||||
color: #9AC1D6;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #9AC1D6;
|
||||
}
|
||||
|
||||
.topic-content {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topic-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chapter-name {
|
||||
background: #F5F8FB;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
width: 100%;
|
||||
min-height: 37px;
|
||||
line-height: 37px;
|
||||
padding: 0 20px;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.discussion-actions {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.more-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.interaction-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.interaction-buttons :deep(.n-button) {
|
||||
font-size: 12px !important;
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.interaction-buttons :deep(.n-button .n-button__content) {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.interaction-buttons .n-button {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.interaction-buttons .n-button.liked {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 评论区域 */
|
||||
.comments-section {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
/* 添加评论 */
|
||||
.add-comment {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.comment-input-area {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-textarea :deep(.n-input__textarea) {
|
||||
resize: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.comment-textarea :deep(.n-input__textarea-el) {
|
||||
resize: none !important;
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.comment-textarea :deep(.n-input-wrapper) {
|
||||
height: 57px !important;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
/* border: 1px solid #F1F3F4; */
|
||||
/* border-radius: 4px; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-icon:hover {
|
||||
border-color: #d9d9d9;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #0C99DA;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.agreement-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.comment-buttons {
|
||||
margin-top: -20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-buttons :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.comment-buttons :deep(.n-button--default-type) {
|
||||
border: 1px solid #0288D1 !important;
|
||||
background: #E2F5FF !important;
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
|
||||
.comment-buttons :deep(.n-button--default-type:hover) {
|
||||
border-color: #0277BD !important;
|
||||
background: #D1F0FF !important;
|
||||
color: #0277BD !important;
|
||||
}
|
||||
|
||||
/* 评论列表 */
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.comment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-username {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-buttons-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* 回复列表 */
|
||||
.comment-replies {}
|
||||
|
||||
.reply-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
}
|
||||
|
||||
.reply-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reply-main {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reply-header {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reply-username {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.reply-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reply-badge.instructor {
|
||||
background: #EEF9FF;
|
||||
color: #008BD7;
|
||||
}
|
||||
|
||||
.reply-badge.user {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reply-badge.admin {
|
||||
background: #722ed1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reply-text {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reply-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.reply-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.reply-action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.reply-action-btn:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.reply-action-btn img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
:deep(.n-dropdown-option-body__label) {
|
||||
font-size: 10px !important;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option-body__prefix) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -151,7 +151,9 @@ const hideSidebar = computed(() => {
|
||||
'template-import',
|
||||
'review/',
|
||||
'certificate/detail/',
|
||||
'certificate/add'
|
||||
'certificate/add',
|
||||
'comment/', // 查看讨论页面
|
||||
'discussion/add'
|
||||
]
|
||||
|
||||
// 检查当前路径是否包含需要隐藏侧边栏的路径
|
||||
|
@ -1,36 +1,586 @@
|
||||
<template>
|
||||
<div class="discussion-management">
|
||||
<div class="content-placeholder">
|
||||
<h2>讨论管理</h2>
|
||||
<p>讨论管理功能正在开发中...</p>
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">全部讨论</h1>
|
||||
<div class="header-actions">
|
||||
<n-button type="primary" @click="goToAddDiscussion">
|
||||
添加讨论
|
||||
</n-button>
|
||||
<div class="search-box">
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px;" clearable />
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论列表 -->
|
||||
<div class="discussion-list">
|
||||
<div v-for="discussion in sortedDiscussions" :key="discussion.id" class="discussion-item">
|
||||
<!-- 操作按钮 -->
|
||||
<div class="discussion-actions">
|
||||
<!-- 更多操作菜单 -->
|
||||
<div class="more-actions">
|
||||
<n-dropdown :options="getMoreOptions(discussion)" @select="handleMoreAction" trigger="click">
|
||||
<n-button quaternary circle>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor"
|
||||
d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<img :src="discussion.avatar" :alt="discussion.author" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论内容 -->
|
||||
<div class="discussion-content">
|
||||
<div class="author-name">{{ discussion.author }}</div>
|
||||
<div class="topic-header">
|
||||
<h3 class="topic-title" @click="viewComments(discussion)">{{ discussion.title }}</h3>
|
||||
<span v-if="discussion.isPinned" class="pinned-badge">置顶</span>
|
||||
</div>
|
||||
|
||||
<div class="topic-content">{{ discussion.content }}</div>
|
||||
|
||||
<div class="topic-meta">
|
||||
<span class="chapter-name">{{ discussion.chapterName }}</span>
|
||||
<div class="meta-row">
|
||||
<span class="timestamp">{{ discussion.timestamp }}</span>
|
||||
<div class="interaction-buttons">
|
||||
<n-button quaternary @click="toggleLike(discussion)" :class="{ liked: discussion.isLiked }">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/like.png" alt="点赞" style="width: 13px; height: 13px;" />
|
||||
</template>
|
||||
点赞
|
||||
</n-button>
|
||||
|
||||
<n-button quaternary @click="deleteDiscussion" style="color: #ff4d4f;">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/delete2.png" alt="删除" style="width: 13px; height: 13px;" />
|
||||
</template>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加讨论模态框 -->
|
||||
<n-modal v-model:show="showAddDiscussionModal" preset="card" title="添加讨论" style="width: 600px">
|
||||
<div class="add-discussion-form">
|
||||
<n-form :model="newDiscussion" label-placement="top">
|
||||
<n-form-item label="讨论标题">
|
||||
<n-input v-model:value="newDiscussion.title" placeholder="请输入讨论标题" />
|
||||
</n-form-item>
|
||||
<n-form-item label="所属章节">
|
||||
<n-select v-model:value="newDiscussion.chapterId" :options="chapterOptions" placeholder="请选择章节" />
|
||||
</n-form-item>
|
||||
<n-form-item label="讨论内容">
|
||||
<n-input v-model:value="newDiscussion.content" type="textarea" placeholder="请输入讨论内容" :rows="6" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="showAddDiscussionModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="addDiscussion">确定</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 讨论管理逻辑
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NButton, NInput, NDropdown, NIcon, NModal, NForm, NFormItem, NSelect, useMessage } from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const searchKeyword = ref('')
|
||||
const showAddDiscussionModal = ref(false)
|
||||
const newDiscussion = ref({
|
||||
title: '',
|
||||
chapterId: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 计算属性:排序后的讨论列表(置顶的在前)
|
||||
const sortedDiscussions = computed(() => {
|
||||
return [...discussions.value].sort((a, b) => {
|
||||
// 置顶的排在前面
|
||||
if (a.isPinned && !b.isPinned) return -1
|
||||
if (!a.isPinned && b.isPinned) return 1
|
||||
// 如果都是置顶或都不是置顶,按ID排序
|
||||
return a.id - b.id
|
||||
})
|
||||
})
|
||||
|
||||
// 讨论数据
|
||||
const discussions = ref([
|
||||
{
|
||||
id: 1,
|
||||
author: '王建',
|
||||
avatar: '/images/activity/1.png',
|
||||
title: '话题标题',
|
||||
content: '话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容',
|
||||
chapterName: '这是章节名称名称',
|
||||
timestamp: '7月20日 12:41',
|
||||
isPinned: true,
|
||||
isLiked: false,
|
||||
likes: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: '李小明',
|
||||
avatar: '/images/activity/2.png',
|
||||
title: '话题标题',
|
||||
content: '话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容',
|
||||
chapterName: '这是章节名称名称',
|
||||
timestamp: '7月20日 12:41',
|
||||
isPinned: false,
|
||||
isLiked: false,
|
||||
likes: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: '张伟',
|
||||
avatar: '/images/activity/3.png',
|
||||
title: '关于课程内容的疑问',
|
||||
content: '老师,关于第三章的内容我有些疑问,能否详细解释一下数组和指针的关系?',
|
||||
chapterName: '第三章:数据结构基础',
|
||||
timestamp: '7月19日 15:30',
|
||||
isPinned: false,
|
||||
isLiked: true,
|
||||
likes: 5
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
author: '刘芳',
|
||||
avatar: '/images/activity/4.png',
|
||||
title: '作业提交问题',
|
||||
content: '请问老师,本周的编程作业什么时候截止?我还有一些问题需要解决。',
|
||||
chapterName: '第二章:编程基础',
|
||||
timestamp: '7月18日 20:15',
|
||||
isPinned: false,
|
||||
isLiked: false,
|
||||
likes: 2
|
||||
}
|
||||
])
|
||||
|
||||
// 章节选项
|
||||
const chapterOptions = [
|
||||
{ label: '第一章:基础概念', value: 'chapter1' },
|
||||
{ label: '第二章:进阶应用', value: 'chapter2' },
|
||||
{ label: '第三章:实战项目', value: 'chapter3' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
message.info('搜索: ' + searchKeyword.value)
|
||||
}
|
||||
|
||||
const getMoreOptions = (discussion: any) => {
|
||||
const options = [
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/edit.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
if (discussion.isPinned) {
|
||||
options.push({
|
||||
label: '取消置顶',
|
||||
key: 'unpin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: '置顶',
|
||||
key: 'pin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
const handleMoreAction = (key: string, option: any) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
message.info('编辑功能')
|
||||
break
|
||||
case 'pin':
|
||||
// 找到对应的讨论项并置顶
|
||||
const discussionToPin = discussions.value.find((d: any) => d.id === option.discussionId)
|
||||
if (discussionToPin) {
|
||||
discussionToPin.isPinned = true
|
||||
message.success('已置顶')
|
||||
}
|
||||
break
|
||||
case 'unpin':
|
||||
// 找到对应的讨论项并取消置顶
|
||||
const discussionToUnpin = discussions.value.find((d: any) => d.id === option.discussionId)
|
||||
if (discussionToUnpin) {
|
||||
discussionToUnpin.isPinned = false
|
||||
message.success('已取消置顶')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const toggleLike = (discussion: any) => {
|
||||
discussion.isLiked = !discussion.isLiked
|
||||
discussion.likes += discussion.isLiked ? 1 : -1
|
||||
}
|
||||
|
||||
const deleteDiscussion = () => {
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
const addDiscussion = () => {
|
||||
message.success('添加成功')
|
||||
showAddDiscussionModal.value = false
|
||||
newDiscussion.value = {
|
||||
title: '',
|
||||
chapterId: '',
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
|
||||
const viewComments = (discussion: any) => {
|
||||
router.push({
|
||||
name: 'CommentView',
|
||||
params: { id: discussion.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到添加讨论页面
|
||||
const goToAddDiscussion = () => {
|
||||
router.push({
|
||||
name: 'AddDiscussion'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discussion-management {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1.5px solid #f6f6f6;
|
||||
}
|
||||
|
||||
.content-placeholder h2 {
|
||||
font-size: 24px;
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content-placeholder p {
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.search-box :deep(.n-input) {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.search-box :deep(.n-input .n-input__border) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.search-box :deep(.n-button) {
|
||||
border-radius: 0 !important;
|
||||
border-left: none !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.header-actions :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 讨论列表 */
|
||||
.discussion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.discussion-item {
|
||||
display: flex;
|
||||
padding: 0 20px 20px 20px;
|
||||
border-bottom: 1.5px solid #F1F3F4;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* .discussion-item:hover {
|
||||
border-color: #d9d9d9;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
} */
|
||||
|
||||
/* 用户信息 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 讨论内容 */
|
||||
.discussion-content {
|
||||
padding-top: 6px;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
color: #0C99DA;
|
||||
margin: 0 8px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topic-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pinned-badge {
|
||||
background: #fff;
|
||||
color: #9AC1D6;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #9AC1D6;
|
||||
}
|
||||
|
||||
.topic-content {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topic-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chapter-name {
|
||||
background: #F5F8FB;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
width: 100%;
|
||||
min-height: 37px;
|
||||
line-height: 37px;
|
||||
padding: 0 20px;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.discussion-actions {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* .more-actions 样式已移除 */
|
||||
|
||||
.interaction-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.interaction-buttons :deep(.n-button) {
|
||||
font-size: 12px !important;
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.interaction-buttons :deep(.n-button .n-button__content) {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.interaction-buttons .n-button {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.interaction-buttons .n-button.liked {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.add-discussion-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
:deep(.n-dropdown-option-body__label) {
|
||||
font-size: 10px !important;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option-body__prefix) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.discussion-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.discussion-content {
|
||||
margin-right: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.discussion-actions {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.interaction-buttons {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -60,7 +60,8 @@
|
||||
<h3 class="section-title">教学建设</h3>
|
||||
<div class="construction-grid">
|
||||
<!-- 课件/视频 -->
|
||||
<div class="construction-card blue" style="background-image: url('/images/teacher/teaching-construction1.png');">
|
||||
<div class="construction-card blue"
|
||||
style="background-image: url('/images/teacher/teaching-construction1.png');">
|
||||
<div class="card-icon">
|
||||
<img src="/images/teacher/teaching-construction1.png" alt="课件/视频" />
|
||||
</div>
|
||||
@ -71,7 +72,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 资料/文档 -->
|
||||
<div class="construction-card orange" style="background-image: url('/images/teacher/teaching-construction2.png');">
|
||||
<div class="construction-card orange"
|
||||
style="background-image: url('/images/teacher/teaching-construction2.png');">
|
||||
<div class="card-icon">
|
||||
<img src="/images/teacher/teaching-construction2.png" alt="资料/文档" />
|
||||
</div>
|
||||
@ -82,7 +84,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 题库总数 -->
|
||||
<div class="construction-card blue-gray" style="background-image: url('/images/teacher/teaching-construction3.png');">
|
||||
<div class="construction-card blue-gray"
|
||||
style="background-image: url('/images/teacher/teaching-construction3.png');">
|
||||
<div class="card-icon">
|
||||
<img src="/images/teacher/teaching-construction3.png" alt="题库总数" />
|
||||
</div>
|
||||
@ -93,7 +96,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 试卷总数 -->
|
||||
<div class="construction-card yellow" style="background-image: url('/images/teacher/teaching-construction4.png');">
|
||||
<div class="construction-card yellow"
|
||||
style="background-image: url('/images/teacher/teaching-construction4.png');">
|
||||
<div class="card-icon">
|
||||
<img src="/images/teacher/teaching-construction4.png" alt="试卷总数" />
|
||||
</div>
|
||||
@ -118,7 +122,7 @@ console.log('BasicData component loaded')
|
||||
|
||||
/* 顶部统计卡片区域 */
|
||||
.stats-cards {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
@ -205,6 +209,8 @@ console.log('BasicData component loaded')
|
||||
overflow: hidden;
|
||||
min-height: 95px;
|
||||
padding: 40px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.construction-card:hover {
|
||||
@ -227,13 +233,17 @@ console.log('BasicData component loaded')
|
||||
.card-icon img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: brightness(0) invert(1); /* 使图标变为白色 */
|
||||
filter: brightness(0) invert(1);
|
||||
/* 使图标变为白色 */
|
||||
}
|
||||
|
||||
.card-content {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
@ -242,12 +252,19 @@ console.log('BasicData component loaded')
|
||||
color: #646464;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
color: #646464;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@ -270,6 +287,73 @@ console.log('BasicData component loaded')
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (min-width: 1400px) {
|
||||
.construction-grid {
|
||||
max-width: 1420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.construction-card {
|
||||
padding: 30px;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.construction-card {
|
||||
padding: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.construction-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.construction-card {
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.card-icon img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-row {
|
||||
flex-wrap: wrap;
|
||||
@ -284,6 +368,32 @@ console.log('BasicData component loaded')
|
||||
.construction-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.construction-card {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.card-icon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,5 +401,35 @@ console.log('BasicData component loaded')
|
||||
.stat-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.construction-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.construction-card {
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.card-icon img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,34 +1,603 @@
|
||||
<template>
|
||||
<div class="learning-monitor">
|
||||
<div class="content-placeholder">
|
||||
<h3>学习监控</h3>
|
||||
<p>学习监控功能正在开发中...</p>
|
||||
<div class="student-grades">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="header-section">
|
||||
<div class="header-left">
|
||||
<div class="class-selector">
|
||||
<div class="selector-container">
|
||||
<select v-model="selectedClass" class="class-select">
|
||||
<option value="">班级名称</option>
|
||||
<option value="cs1">计算机科学1班</option>
|
||||
<option value="cs2">计算机科学2班</option>
|
||||
<option value="se1">软件工程1班</option>
|
||||
<option value="se2">软件工程2班</option>
|
||||
</select>
|
||||
<div class="selector-arrow">
|
||||
<img src="/images/teacher/箭头-黑.png" alt="箭头">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-button @click="openMonitoringSettings"
|
||||
style="--n-border: 1.5px solid #0288D1; --n-color: transparent; --n-text-color: #0288D1; --n-font-size: 14px; --n-height: 32px;">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<img src="/images/teacher/monitor.png" alt="监控" style="width: 15px; height: 15px;" />
|
||||
</n-icon>
|
||||
</template>
|
||||
监控设置
|
||||
</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入姓名或学号" style="width: 200px;" />
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学习异常监控表格 -->
|
||||
<div class="table-section">
|
||||
<n-data-table :columns="monitorColumns" :data="learningAnomalies" :row-key="rowKey"
|
||||
:checked-row-keys="selectedStudents" @update:checked-row-keys="handleStudentSelection" :bordered="false"
|
||||
:single-line="false" size="small" class="monitor-data-table" />
|
||||
</div>
|
||||
|
||||
<!-- 监控设置模态框 -->
|
||||
<n-modal v-model:show="showMonitoringModal" preset="card" title="监控设置" style="width: 1000px;" :closable="false">
|
||||
<template #header>
|
||||
<div style="font-size: 18px; font-weight: 500;">监控设置</div>
|
||||
</template>
|
||||
<div class="monitoring-settings-content">
|
||||
<div class="toggle-section">
|
||||
<div class="toggle-label">开启学习监控</div>
|
||||
<n-switch v-model:value="monitoringEnabled" @update:value="handleMonitoringToggle" />
|
||||
</div>
|
||||
|
||||
<div class="options-section">
|
||||
<div class="options-title">监控设置</div>
|
||||
<div class="option-list">
|
||||
<div class="option-item">
|
||||
<input type="checkbox" id="opt-videoWatching" v-model="monitoringOptions.videoWatching"
|
||||
class="custom-checkbox" />
|
||||
<label for="opt-videoWatching" class="option-label">
|
||||
<span class="dot" :class="{ active: monitoringOptions.videoWatching }"><span
|
||||
class="dot-icon"></span></span>
|
||||
<div class="option-text">学员观看章节视频,监控异常观看行为</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="checkbox" id="opt-assignmentCompletion" v-model="monitoringOptions.assignmentCompletion"
|
||||
class="custom-checkbox" />
|
||||
<label for="opt-assignmentCompletion" class="option-label">
|
||||
<span class="dot" :class="{ active: monitoringOptions.assignmentCompletion }"><span
|
||||
class="dot-icon"></span></span>
|
||||
<div class="option-text">学员完成作业、练习和考试,监控异常行为</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="checkbox" id="opt-discussionParticipation"
|
||||
v-model="monitoringOptions.discussionParticipation" class="custom-checkbox" />
|
||||
<label for="opt-discussionParticipation" class="option-label">
|
||||
<span class="dot" :class="{ active: monitoringOptions.discussionParticipation }">
|
||||
<span class="dot-icon"></span>
|
||||
</span>
|
||||
<div class="option-text">学员参与小组讨论、课程评论和论坛话题,监控异常行为</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="checkbox" id="opt-loginFrequency" v-model="monitoringOptions.loginFrequency"
|
||||
class="custom-checkbox" />
|
||||
<label for="opt-loginFrequency" class="option-label">
|
||||
<span class="dot" :class="{ active: monitoringOptions.loginFrequency }"><span
|
||||
class="dot-icon"></span></span>
|
||||
<div class="option-text">学员登录学习平台频率,监控异常行为</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="closeMonitoringModal"
|
||||
style="--n-border: 1px solid #0288D1; --n-color: #E2F5FF; --n-text-color: #0288D1; --n-font-size: 16px; --n-height: 32px;">
|
||||
取消
|
||||
</n-button>
|
||||
<n-button type="primary" @click="saveMonitoringSettings" style="--n-font-size: 16px; --n-height: 32px;">
|
||||
保存
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
console.log('LearningMonitor component loaded')
|
||||
import { ref, watch } from 'vue'
|
||||
import { NButton, NDataTable, NInput, NIcon, NModal, NSwitch, useMessage } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedClass = ref<string>('')
|
||||
const searchKeyword = ref<string>('')
|
||||
const selectedStudents = ref<number[]>([])
|
||||
const showMonitoringModal = ref<boolean>(false)
|
||||
const monitoringEnabled = ref<boolean>(true)
|
||||
const monitoringOptions = ref({
|
||||
videoWatching: true,
|
||||
assignmentCompletion: true,
|
||||
discussionParticipation: true,
|
||||
loginFrequency: true
|
||||
})
|
||||
|
||||
// 学习异常数据
|
||||
const learningAnomalies = ref([
|
||||
{
|
||||
id: 1,
|
||||
serialNumber: 1,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '参与度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '连续多日(如≥7天) 未登录学习平台'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
serialNumber: 2,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '参与度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '持续登录,但所有学习行为(看视频、下载资料、做作业、讨论) 为零或接近零。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
serialNumber: 3,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '参与度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '之前活跃,但从某个时间点开始,所有学习活动突然停止'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
serialNumber: 4,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '努力程度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '在同一章节或同一个视频上花费远超常人的时间(观看次数5次)'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
serialNumber: 5,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '努力程度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '视频观看总时长很长,但完成度很低,总是看前几分钟就退出, 在不同内容间频繁跳跃,注意力无法集中'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
serialNumber: 6,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '努力程度异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '所有作业都在截止日期前的最后一两个小时提交,几乎没有提前量'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
serialNumber: 7,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '成绩异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '作业成绩很高,但视频观看时长很短、完成度低'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
serialNumber: 8,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '成绩异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '花费了大量时间学习,但作业测验成绩依然很差'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
serialNumber: 9,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '成绩异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '成绩出现断崖式下跌(如从80分降到40分)'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
serialNumber: 10,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '成绩异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '成绩一直处于低位(如持续低于60分),且无改善迹象'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
serialNumber: 11,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '行为异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '不按课程设计路径学习,严重跳跃章节'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
serialNumber: 12,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '行为异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '只做作业/练习/考试,几乎不看任何学习材料'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
serialNumber: 13,
|
||||
name: '陈诚',
|
||||
studentId: '556654774552',
|
||||
learningStatus: '异常',
|
||||
anomalyType: '行为异常',
|
||||
anomalyCount: '1次',
|
||||
anomalyAnalysis: '从不参与小组讨论、课程评论或论坛互动'
|
||||
}
|
||||
])
|
||||
|
||||
// 监控表格列配置
|
||||
const monitorColumns: DataTableColumns = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 50
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'serialNumber',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '学号',
|
||||
key: 'studentId',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '学习状态',
|
||||
key: 'learningStatus',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '异常类型',
|
||||
key: 'anomalyType',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '异常次数',
|
||||
key: 'anomalyCount',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '异常分析',
|
||||
key: 'anomalyAnalysis',
|
||||
width: 300,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 方法
|
||||
const rowKey = (row: { id: number }) => row.id
|
||||
|
||||
const handleStudentSelection = (keys: number[]) => {
|
||||
selectedStudents.value = keys
|
||||
}
|
||||
|
||||
const openMonitoringSettings = () => {
|
||||
showMonitoringModal.value = true
|
||||
}
|
||||
|
||||
const closeMonitoringModal = () => {
|
||||
showMonitoringModal.value = false
|
||||
}
|
||||
|
||||
const handleMonitoringToggle = (value: boolean) => {
|
||||
// 当开关切换时,同步更新所有选项的选中状态
|
||||
monitoringOptions.value.videoWatching = value
|
||||
monitoringOptions.value.assignmentCompletion = value
|
||||
monitoringOptions.value.discussionParticipation = value
|
||||
monitoringOptions.value.loginFrequency = value
|
||||
}
|
||||
|
||||
const saveMonitoringSettings = () => {
|
||||
message.success('监控设置已保存')
|
||||
showMonitoringModal.value = false
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
message.info('搜索: ' + searchKeyword.value)
|
||||
}
|
||||
|
||||
// 监听选项变化,同步开关状态
|
||||
watch(monitoringOptions, (newOptions: any) => {
|
||||
const allSelected = newOptions.videoWatching &&
|
||||
newOptions.assignmentCompletion &&
|
||||
newOptions.discussionParticipation &&
|
||||
newOptions.loginFrequency
|
||||
monitoringEnabled.value = allSelected
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.learning-monitor {
|
||||
padding: 20px;
|
||||
.student-grades {
|
||||
padding: 0 20px 20px 20px;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
/* 顶部操作栏 */
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #E6E6E6;
|
||||
}
|
||||
|
||||
.content-placeholder h3 {
|
||||
font-size: 18px;
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.class-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 下拉框样式 */
|
||||
.selector-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.class-select {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 30px 0 12px;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.class-select:hover {
|
||||
border-color: #0277BD;
|
||||
}
|
||||
|
||||
.class-select:focus {
|
||||
border-color: #0277BD;
|
||||
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
|
||||
}
|
||||
|
||||
.selector-arrow {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.selector-arrow img {
|
||||
width: 12px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.table-section {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 监控数据表格样式 */
|
||||
:deep(.monitor-data-table .n-data-table-td) {
|
||||
padding: 20px 8px;
|
||||
font-size: 12px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
:deep(.monitor-data-table .n-data-table-th) {
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
/* 表格边框和高度 */
|
||||
:deep(.monitor-data-table) {
|
||||
border: 1px solid #E6E6E6;
|
||||
}
|
||||
|
||||
:deep(.monitor-data-table .n-data-table-table) {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
:deep(.monitor-data-table .n-data-table-thead) {
|
||||
background-color: #F1F3F4;
|
||||
}
|
||||
|
||||
:deep(.monitor-data-table .n-data-table-tbody tr:hover) {
|
||||
background-color: #F8F9FA;
|
||||
}
|
||||
|
||||
/* 监控设置模态框样式 */
|
||||
.monitoring-settings-content {
|
||||
padding: 14px 24px;
|
||||
}
|
||||
|
||||
.toggle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.options-section {
|
||||
margin-left: 30px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.options-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-placeholder p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
.option-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 自定义多选:隐藏原生复选框,使用蓝色圆点 */
|
||||
.custom-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ccc;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dot-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
border: 2px solid #0288D1;
|
||||
}
|
||||
|
||||
.dot.active .dot-icon {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.monitor-data-table .n-data-table-td),
|
||||
:deep(.monitor-data-table .n-data-table-th) {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -29,16 +29,16 @@
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">章节学习总人次</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">0</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
<span class="stat-number">0</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">章节平均学习次数</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">0</span>
|
||||
<span class="stat-unit">次</span>
|
||||
</div>
|
||||
<span class="stat-number">0</span>
|
||||
<span class="stat-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -66,134 +66,134 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 考试和练习容器 -->
|
||||
<div class="exam-practice-container">
|
||||
<!-- 考试统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">考试</span>
|
||||
<span class="title-sub">(共5场)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">参与人数</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">70</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均成绩</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">分</span>
|
||||
</div>
|
||||
<!-- 考试和练习容器 -->
|
||||
<div class="exam-practice-container">
|
||||
<!-- 考试统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">考试</span>
|
||||
<span class="title-sub">(共5场)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">参与人数</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">70</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">成绩占比</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="examScoreOption" style="height: 100%;" />
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均成绩</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 练习统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">练习</span>
|
||||
<span class="title-sub">(共5场)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">参与人数</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">70</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均成绩</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">成绩占比</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="practiceScoreOption" style="height: 100%;" />
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">成绩占比</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="examScoreOption" style="height: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 证书和讨论容器 -->
|
||||
<div class="exam-practice-container">
|
||||
<!-- 证书统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">证书</span>
|
||||
<span class="title-sub">(共1个)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<div class="chart-section full-width">
|
||||
<h4 class="chart-title">证书获取率</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="certificateOption" style="height: 100%;" />
|
||||
<!-- 练习统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">练习</span>
|
||||
<span class="title-sub">(共5场)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">参与人数</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">70</span>
|
||||
<span class="stat-unit">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均成绩</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">讨论</span>
|
||||
<span class="title-sub">(共5个)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">讨论话题</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">7</span>
|
||||
<span class="stat-unit">个</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">回复数量</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">讨论活跃度</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="discussionOption" style="height: 100%;" />
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">成绩占比</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="practiceScoreOption" style="height: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 证书和讨论容器 -->
|
||||
<div class="exam-practice-container">
|
||||
<!-- 证书统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">证书</span>
|
||||
<span class="title-sub">(共1个)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<div class="chart-section full-width">
|
||||
<h4 class="chart-title">证书获取率</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="certificateOption" style="height: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论统计卡片 -->
|
||||
<div class="statistics-card">
|
||||
<h3 class="card-title">
|
||||
<span class="title-main">讨论</span>
|
||||
<span class="title-sub">(共5个)</span>
|
||||
</h3>
|
||||
<div class="card-content">
|
||||
<!-- 左侧文本统计 -->
|
||||
<div class="text-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">讨论话题</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">7</span>
|
||||
<span class="stat-unit">个</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">回复数量</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number">60</span>
|
||||
<span class="stat-unit">条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧饼图 -->
|
||||
<div class="chart-section">
|
||||
<h4 class="chart-title">讨论活跃度</h4>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="discussionOption" style="height: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|