OL-LearnPlatform-Frontend/src/views/teacher/course/DiscussionManagement.vue

587 lines
13 KiB
Vue
Raw Normal View History

2025-08-21 19:39:07 +08:00
<template>
<div class="discussion-management">
<!-- 页面头部 -->
<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>
2025-08-21 19:39:07 +08:00
</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>
2025-08-21 19:39:07 +08:00
</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'
})
}
2025-08-21 19:39:07 +08:00
</script>
<style scoped>
.discussion-management {
background: #fff;
height: 100%;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
2025-08-21 19:39:07 +08:00
padding: 20px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1.5px solid #f6f6f6;
}
.page-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0;
}
.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;
2025-08-21 19:39:07 +08:00
background: #fff;
transition: all 0.3s ease;
position: relative;
2025-08-21 19:39:07 +08:00
}
/* .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;
2025-08-21 19:39:07 +08:00
}
.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;
2025-08-21 19:39:07 +08:00
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;
2025-08-21 19:39:07 +08:00
}
.topic-title {
2025-08-21 19:39:07 +08:00
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 样式已移除 */
.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;
}
2025-08-21 19:39:07 +08:00
}
</style>