提交 b1b50306 authored 作者: 王鹏飞's avatar 王鹏飞

chore(bbs): 完善bbs功能

上级 e0e5fd07
...@@ -67,8 +67,8 @@ export function getQuestionCategory(params: { project_tag: string }) { ...@@ -67,8 +67,8 @@ export function getQuestionCategory(params: { project_tag: string }) {
export function collectionResource(data: { export function collectionResource(data: {
course_id: string course_id: string
semester_id: string semester_id: string
chapter_id: string chapter_id?: string
section_id: string section_id?: string
source_id: string source_id: string
type: number type: number
status: number status: number
......
...@@ -6,14 +6,16 @@ interface Props { ...@@ -6,14 +6,16 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
function show(fileType: string) { function show(fileType: string) {
return props.info?.type?.includes(fileType) const type = props.info?.type || ''
if (typeof type === 'string') type.includes(fileType)
return false
} }
</script> </script>
<template> <template>
<div class="icon-resource"> <div class="icon-resource">
<img src="@/assets/images/icon_mp4.png" v-if="resourceType === 2 || show('mp4')" /> <img src="@/assets/images/icon_mp4.png" v-if="resourceType === 2" />
<img src="@/assets/images/icon_work.png" v-else-if="resourceType === 3" /> <img src="@/assets/images/icon_work.png" v-else-if="resourceType === 3 || resourceType === 12" />
<img src="@/assets/images/icon_live.png" v-else-if="resourceType === 6" /> <img src="@/assets/images/icon_live.png" v-else-if="resourceType === 6" />
<img src="@/assets/images/icon_exam.png" v-else-if="resourceType === 9" /> <img src="@/assets/images/icon_exam.png" v-else-if="resourceType === 9" />
<img src="@/assets/images/icon_ppt.png" v-else-if="show('pptx')" /> <img src="@/assets/images/icon_ppt.png" v-else-if="show('pptx')" />
......
...@@ -63,3 +63,9 @@ const init = { ...@@ -63,3 +63,9 @@ const init = {
<template> <template>
<editor :init="init" v-bind="$attrs" style="width: 100%" /> <editor :init="init" v-bind="$attrs" style="width: 100%" />
</template> </template>
<style lang="scss">
.tox-tinymce-aux {
z-index: 3000 !important;
}
</style>
import { getCourseList, getCourseChapterList } from '@/api/base' import { getCourseList, getCourseChapterList } from '@/api/base'
import { val } from 'dom7'
interface Semester { interface Semester {
id: string id: string
...@@ -21,7 +20,7 @@ interface Chapter { ...@@ -21,7 +20,7 @@ interface Chapter {
} }
export function useGetCourseList() { export function useGetCourseList() {
const courseId = ref('') const courseValue = ref('')
const courses = ref<Course[]>([]) const courses = ref<Course[]>([])
const chapters = ref<Chapter[]>([]) const chapters = ref<Chapter[]>([])
// 获取课程列表 // 获取课程列表
...@@ -32,20 +31,20 @@ export function useGetCourseList() { ...@@ -32,20 +31,20 @@ export function useGetCourseList() {
} }
// 获取章节列表 // 获取章节列表
function getChapters() { function getChapters() {
if (!courseId.value) { if (!courseValue.value) {
chapters.value = [] chapters.value = []
return return
} }
getCourseChapterList({ course_id: courseId.value }).then(res => { getCourseChapterList({ course_id: courseValue.value }).then(res => {
chapters.value = res.data.items chapters.value = res.data.items
}) })
} }
getCourses() getCourses()
watch(courseId, () => { watch(courseValue, () => {
getChapters() getChapters()
}) })
return { courses, courseId, chapters } return { courses, courseValue, chapters }
} }
...@@ -34,7 +34,7 @@ function handleSubmit() { ...@@ -34,7 +34,7 @@ function handleSubmit() {
const create = () => { const create = () => {
const params = Object.assign({}, form, { files: form.files.length ? JSON.stringify(form.files) : '' }) const params = Object.assign({}, form, { files: form.files.length ? JSON.stringify(form.files) : '' })
replyToPost(params).then(() => { replyToPost(params).then(() => {
ElMessage({ message: '发成功', type: 'success' }) ElMessage({ message: '发成功', type: 'success' })
emit('update') emit('update')
emit('update:modelValue', false) emit('update:modelValue', false)
}) })
...@@ -42,7 +42,13 @@ const create = () => { ...@@ -42,7 +42,13 @@ const create = () => {
</script> </script>
<template> <template>
<el-dialog title="发表回复" width="800px" @update:modelValue="$emit('update:modelValue')"> <el-dialog
append-to-body
align-center
title="发表回复"
width="800px"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk label-position="top"> <el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk label-position="top">
<el-form-item prop="content"> <el-form-item prop="content">
<AppEditor v-model="form.content" :height="300" /> <AppEditor v-model="form.content" :height="300" />
......
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { DiscussItem } from '../types' import type { DiscussItem } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage, ElInput } from 'element-plus'
import DiscussItemCommentList from './DiscussItemCommentList.vue' import DiscussItemCommentList from './DiscussItemCommentList.vue'
import FileItem from './FileItem.vue' import FileList from './FileList.vue'
import { replyToPost } from '../api' import { replyToPost } from '../api'
interface Props { interface Props {
landlordId: string
data: DiscussItem data: DiscussItem
} }
const props = defineProps<Props>() const props = defineProps<Props>()
...@@ -16,7 +17,8 @@ const username = $computed(() => { ...@@ -16,7 +17,8 @@ const username = $computed(() => {
return user.realname || user.nickname || user.username return user.realname || user.nickname || user.username
}) })
const formVisible = $ref(false) const commentListRef = $ref<InstanceType<typeof DiscussItemCommentList> | null>(null)
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form = reactive({
content: '', content: '',
...@@ -25,6 +27,15 @@ const form = reactive({ ...@@ -25,6 +27,15 @@ const form = reactive({
const rules = ref<FormRules>({ const rules = ref<FormRules>({
content: [{ required: true, message: '请输入回复内容', trigger: 'blur' }] content: [{ required: true, message: '请输入回复内容', trigger: 'blur' }]
}) })
let formVisible = $ref(false)
const inputRef = $ref<InstanceType<typeof ElInput> | null>(null)
function handleReply() {
formVisible = !formVisible
nextTick(() => {
formVisible && inputRef?.focus()
})
}
// 发布回复 // 发布回复
function handleSubmit() { function handleSubmit() {
formRef?.validate().then(() => { formRef?.validate().then(() => {
...@@ -35,6 +46,9 @@ function handleSubmit() { ...@@ -35,6 +46,9 @@ function handleSubmit() {
reply_id: props.data.id, reply_id: props.data.id,
content: form.content content: form.content
}).then(() => { }).then(() => {
formVisible = false
form.content = ''
commentListRef?.refresh()
ElMessage({ message: '回复成功', type: 'success' }) ElMessage({ message: '回复成功', type: 'success' })
}) })
}) })
...@@ -44,32 +58,61 @@ function handleSubmit() { ...@@ -44,32 +58,61 @@ function handleSubmit() {
<template> <template>
<section class="discuss-item"> <section class="discuss-item">
<div class="discuss-item__left"> <div class="discuss-item__left">
<div class="discuss-item__landlord" v-if="data.sso_id === landlordId"><span>楼主</span></div>
<div class="discuss-item__avatar"> <div class="discuss-item__avatar">
<img :src="data.sso_user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/default.jpg'" /> <img :src="data.sso_user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/default.jpg'" />
</div> </div>
<p class="discuss-item__username">{{ username }}</p> <p class="discuss-item__username">{{ username }}</p>
<template v-if="data.sso_type === 2">
<el-button round plain size="small" type="primary">老师</el-button>
</template>
<template v-else>
<img
src="@/assets/images/icon_bbs_level_1.png"
width="40"
title="学徒"
v-if="data.sso_user.discussion_level === 0" />
<img
src="@/assets/images/icon_bbs_level_2.png"
width="40"
title="学者"
v-if="data.sso_user.discussion_level === 1" />
<img
src="@/assets/images/icon_bbs_level_3.png"
width="40"
title="大师"
v-if="data.sso_user.discussion_level === 2" />
<img
src="@/assets/images/icon_bbs_level_4.png"
width="40"
title="智者"
v-if="data.sso_user.discussion_level === 3" />
</template>
</div> </div>
<div class="discuss-item__right"> <div class="discuss-item__right">
<div class="discuss-item__main"> <div class="discuss-item__main">
<div class="discuss-item__content" v-html="data.content"></div> <div class="discuss-item__content" v-html="data.content"></div>
<ul class="discuss-item__files" v-if="data.files.length"> <FileList preview :data="data.files" v-if="data.files.length" />
<li v-for="(item, index) in data.files" :key="index"><FileItem :data="item" /></li>
</ul>
</div> </div>
<p class="discuss-item__time">{{ data.updated_time }}</p> <p class="discuss-item__time">{{ data.updated_time }}</p>
<div class="discuss-item__comment"><p @click="formVisible = !formVisible">回复</p></div> <div class="discuss-item__comment"><p @click="handleReply">回复</p></div>
<div class="discuss-item-form" v-if="formVisible"> <div class="discuss-item-form" v-if="formVisible">
<p class="discuss-item-form__title">回复本楼</p> <p class="discuss-item-form__title">回复本楼</p>
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk> <el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk>
<el-form-item prop="content"> <el-form-item prop="content">
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }" v-model="form.content" /> <el-input
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
:maxlength="100"
v-model="form.content"
ref="inputRef" />
</el-form-item> </el-form-item>
<el-row justify="end"> <el-row justify="end">
<el-button round type="primary" @click="handleSubmit">发表回复</el-button> <el-button round type="primary" @click="handleSubmit">发表回复</el-button>
</el-row> </el-row>
</el-form> </el-form>
</div> </div>
<DiscussItemCommentList :data="data" /> <DiscussItemCommentList :data="data" ref="commentListRef" />
</div> </div>
</section> </section>
</template> </template>
...@@ -80,12 +123,30 @@ function handleSubmit() { ...@@ -80,12 +123,30 @@ function handleSubmit() {
border-bottom: 4px solid #e8e8e8; border-bottom: 4px solid #e8e8e8;
} }
.discuss-item__left { .discuss-item__left {
position: relative;
width: 240px; width: 240px;
padding: 40px; padding: 40px;
text-align: center; text-align: center;
background-color: #f9f9fc; background-color: #f9f9fc;
box-sizing: border-box; box-sizing: border-box;
} }
.discuss-item__landlord {
position: absolute;
left: 0;
top: 0;
width: 60px;
height: 60px;
color: #fff;
background-color: var(--main-color);
z-index: 2000;
clip-path: polygon(0 0, 100% 0, 0% 100%);
span {
position: absolute;
left: 5px;
top: 10px;
transform: rotate(-45deg);
}
}
.discuss-item__avatar { .discuss-item__avatar {
width: 100px; width: 100px;
height: 100px; height: 100px;
...@@ -98,6 +159,7 @@ function handleSubmit() { ...@@ -98,6 +159,7 @@ function handleSubmit() {
} }
.discuss-item__username { .discuss-item__username {
margin-top: 14px; margin-top: 14px;
margin-bottom: 10px;
font-size: 16px; font-size: 16px;
color: #333; color: #333;
} }
...@@ -116,6 +178,9 @@ function handleSubmit() { ...@@ -116,6 +178,9 @@ function handleSubmit() {
.discuss-item__content { .discuss-item__content {
font-size: 16px; font-size: 16px;
color: #666; color: #666;
img {
max-width: 100%;
}
} }
.discuss-item__time { .discuss-item__time {
margin-top: 20px; margin-top: 20px;
......
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { DiscussItem, DiscussCommentItem, User } from '../types' import type { DiscussItem, DiscussCommentItem, User } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage, ElInput } from 'element-plus'
import { getCommitList, replyToPost } from '../api' import { getCommitList, replyToPost } from '../api'
interface Props { interface Props {
...@@ -9,21 +9,34 @@ interface Props { ...@@ -9,21 +9,34 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
let page = $ref(1) const params = reactive({ page: 1, limit: 3, floor_id: props.data.id })
let list = $ref<DiscussCommentItem[]>([]) let list = $ref<DiscussCommentItem[]>([])
let hasMore = $ref(true)
let loading = $ref(false)
function fetchList() { function fetchList() {
getCommitList({ floor_id: props.data.id, page }).then(res => { if (loading) return
list = page === 1 ? res.data.data : list.concat(res.data.data) loading = true
page++ getCommitList(params)
}) .then(res => {
list = params.page === 1 ? res.data.data : list.concat(res.data.data)
hasMore = res.data.data.length >= params.limit
params.page++
})
.finally(() => {
loading = false
})
} }
function refetch() { function refresh() {
page = 1 params.page = 1
fetchList() fetchList()
} }
// 加载更多 // 加载更多
function loadMore() { function loadMore() {
if (params.limit === 3) {
params.page = 1
}
params.limit = 20
fetchList() fetchList()
} }
...@@ -32,11 +45,23 @@ const currentList = $computed(() => { ...@@ -32,11 +45,23 @@ const currentList = $computed(() => {
return props.data.child_replies return props.data.child_replies
}) })
onMounted(() => {
hasMore = props.data.child_replies.length >= 3
})
const inputRef = $ref<InstanceType<typeof ElInput> | null>(null)
let activeComment = $ref<DiscussCommentItem>() let activeComment = $ref<DiscussCommentItem>()
function handleReply(data: DiscussCommentItem) { function handleReply(data: DiscussCommentItem) {
if (data.id === activeComment?.id && formVisible) {
formVisible = false
return
}
activeComment = data activeComment = data
formVisible = true formVisible = true
form.content = `回复@${getUsername(data.sso_user)}:` form.content = `回复@${getUsername(data.sso_user)}:`
nextTick(() => {
inputRef?.focus()
})
} }
let formVisible = $ref(false) let formVisible = $ref(false)
...@@ -58,7 +83,8 @@ function handleSubmit() { ...@@ -58,7 +83,8 @@ function handleSubmit() {
reply_id: activeComment.id, reply_id: activeComment.id,
content: form.content.replace(`回复@${getUsername(activeComment.sso_user)}:`, '') content: form.content.replace(`回复@${getUsername(activeComment.sso_user)}:`, '')
}).then(() => { }).then(() => {
refetch() formVisible = false
refresh()
ElMessage({ message: '回复成功', type: 'success' }) ElMessage({ message: '回复成功', type: 'success' })
}) })
}) })
...@@ -66,10 +92,12 @@ function handleSubmit() { ...@@ -66,10 +92,12 @@ function handleSubmit() {
function getUsername(data: User) { function getUsername(data: User) {
return data.realname || data.username || data.nickname return data.realname || data.username || data.nickname
} }
defineExpose({ refresh })
</script> </script>
<template> <template>
<div class="discuss-comment-list"> <div class="discuss-comment-list" v-if="currentList.length">
<div class="discuss-comment-item" v-for="item in currentList" :key="item.id"> <div class="discuss-comment-item" v-for="item in currentList" :key="item.id">
<img <img
:src="item.sso_user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/default.jpg'" :src="item.sso_user.avatar || 'https://webapp-pub.ezijing.com/website/base/images/default.jpg'"
...@@ -89,14 +117,19 @@ function getUsername(data: User) { ...@@ -89,14 +117,19 @@ function getUsername(data: User) {
</div> </div>
</div> </div>
</div> </div>
<p class="more"><span @click="loadMore">查看更多</span></p> <p class="more" v-if="hasMore"><span @click="loadMore">查看更多</span></p>
<div class="discuss-item-form" v-if="formVisible"> <div class="discuss-item-form" v-if="formVisible">
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk> <el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk>
<el-form-item prop="content"> <el-form-item prop="content">
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }" v-model="form.content" /> <el-input
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
:maxlength="100"
v-model="form.content"
ref="inputRef" />
</el-form-item> </el-form-item>
<el-row justify="end"> <el-row justify="end">
<el-button round type="primary" auto-insert-space @click="handleSubmit">发表</el-button> <el-button round type="primary" @click="handleSubmit">发表回复</el-button>
</el-row> </el-row>
</el-form> </el-form>
</div> </div>
......
<script setup lang="ts">
import type { File } from '../types'
interface Props {
data: File
}
const props = defineProps<Props>()
const isVideo = $computed(() => {
return props.data.url.includes('.mp4')
})
</script>
<template>
<div class="file-item">
<video :src="data.url" controls v-if="isVideo"></video>
<img :src="data.url" v-else />
</div>
</template>
<style lang="scss">
.file-item {
img {
width: 160px;
height: 90px;
object-fit: cover;
}
video {
width: 400px;
}
}
</style>
<script setup lang="ts">
import type { File } from '../types'
import { VideoPlay } from '@element-plus/icons-vue'
interface Props {
preview?: boolean
data: File[] | Record<string, File[]>
}
const props = defineProps<Props>()
const images = $computed(() => {
if (!Array.isArray(props.data)) return []
return props.data.filter(item => !isVideo(item.url))
})
const currentImages = $computed(() => {
if (props.preview) return images
return images.filter((item, index) => index < 3)
})
const imageSrcList = $computed(() => {
return props.preview ? images.map(item => item.url) : []
})
const videos = $computed(() => {
if (!Array.isArray(props.data)) return []
return props.data.filter(item => isVideo(item.url))
})
function isVideo(url: string) {
return url.includes('.mp4')
}
</script>
<template>
<div class="file-list">
<div class="file-item" v-for="(item, index) in currentImages" :key="index">
<p class="file-item__count" v-if="images.length > 3 && index === 0">{{ images.length }}</p>
<el-image
style="width: 100%; height: 100%"
:src="item.url + '?x-oss-process=image/resize,m_fill,h_90,w_160'"
:initial-index="index"
:preview-src-list="imageSrcList"
fit="cover" />
</div>
<div class="file-item" v-for="(item, index) in videos" :key="index">
<video :src="item.url" />
<el-icon class="icon-video-play"><VideoPlay /></el-icon>
</div>
</div>
</template>
<style lang="scss">
.file-list {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.file-item {
position: relative;
width: 160px;
height: 90px;
video {
width: 160px;
height: 90px;
}
.icon-video-play {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
color: #fff;
}
}
.file-item__count {
position: absolute;
left: 0;
top: 10px;
padding: 0 10px;
height: 18px;
font-size: 12px;
color: #fff;
line-height: 18px;
text-align: center;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
}
</style>
...@@ -7,18 +7,25 @@ import { createPost } from '../api' ...@@ -7,18 +7,25 @@ import { createPost } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useGetCourseList } from '@/composables/useGetCourseList' import { useGetCourseList } from '@/composables/useGetCourseList'
interface Props {
courseId?: string
semesterId?: string
}
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update'): void (e: 'update'): void
(e: 'update:modelValue', visible: boolean): void (e: 'update:modelValue', visible: boolean): void
}>() }>()
const { courses, courseId, chapters } = useGetCourseList() const { courses, courseValue, chapters } = useGetCourseList()
const types = useMapStore().getMapValuesByKey('learning_discussion_type') const types = useMapStore().getMapValuesByKey('learning_discussion_type')
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form = reactive({
semester_id: '', semester_id: props.semesterId || '',
course_id: '', course_id: props.courseId || '',
chapter_id: '', chapter_id: '',
type: '', type: '',
title: '', title: '',
...@@ -33,7 +40,7 @@ const rules = ref<FormRules>({ ...@@ -33,7 +40,7 @@ const rules = ref<FormRules>({
content: [{ required: true, message: '请输入正文内容', trigger: 'blur' }] content: [{ required: true, message: '请输入正文内容', trigger: 'blur' }]
}) })
watchEffect(() => { watchEffect(() => {
courseId.value = form.course_id courseValue.value = form.course_id
const course = courses.value.find(item => item.course_id === form.course_id) const course = courses.value.find(item => item.course_id === form.course_id)
form.semester_id = course ? course.semester.id : '' form.semester_id = course ? course.semester.id : ''
}) })
...@@ -53,10 +60,16 @@ const create = () => { ...@@ -53,10 +60,16 @@ const create = () => {
</script> </script>
<template> <template>
<el-dialog title="发帖" width="800px" @update:modelValue="$emit('update:modelValue')"> <el-dialog
append-to-body
align-center
title="发帖"
width="800px"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk label-position="top"> <el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk label-position="top">
<el-row justify="space-between"> <el-row justify="space-between">
<el-form-item label="相关课程" prop="course_id"> <el-form-item label="相关课程" prop="course_id" v-if="!courseId">
<el-select filterable v-model="form.course_id"> <el-select filterable v-model="form.course_id">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.course_id"></el-option> <el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.course_id"></el-option>
</el-select> </el-select>
......
<script setup lang="ts"> <script setup lang="ts">
import type { PostItem } from '../types' import type { PostItem } from '../types'
import FileItem from './FileItem.vue' import FileList from './FileList.vue'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
interface Props { interface Props {
...@@ -24,20 +24,20 @@ const username = $computed(() => { ...@@ -24,20 +24,20 @@ const username = $computed(() => {
<template> <template>
<section class="post-item"> <section class="post-item">
<div class="post-item-hd"> <router-link :to="`/bbs/${data.id}`" target="_blank">
<p class="post-item__type" :class="`type-${data.type}`">{{ typeText }}</p> <div class="post-item-hd">
<h2 class="post-item__title">{{ data.title }}</h2> <p class="post-item__type" :class="`type-${data.type}`">{{ typeText }}</p>
<p class="post-item__reply_count">{{ data.reply_count }}回帖</p> <h2 class="post-item__title">{{ data.title }}<span class="is-hot" v-if="data.is_hot"></span></h2>
</div> <p class="post-item__reply_count">{{ data.reply_count }}回帖</p>
<div class="post-item-bd"> </div>
<ul class="post-item__files" v-if="data.reply?.files.length"> <div class="post-item-bd">
<li v-for="(item, index) in data.reply.files" :key="index"><FileItem :data="item" /></li> <FileList :data="data.reply?.files" />
</ul> </div>
</div> <div class="post-item-ft">
<div class="post-item-ft"> <p class="post-item__username">{{ username }}</p>
<p class="post-item__username">{{ username }}</p> <p class="post-item__time">{{ data.updated_time }}</p>
<p class="post-item__time">{{ data.updated_time }}</p> </div>
</div> </router-link>
</section> </section>
</template> </template>
...@@ -45,7 +45,17 @@ const username = $computed(() => { ...@@ -45,7 +45,17 @@ const username = $computed(() => {
.post-item { .post-item {
padding: 30px 0; padding: 30px 0;
border-bottom: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
border-top: 1px solid #e6e6e6;
&:hover {
.post-item__title {
color: var(--main-color);
}
}
} }
.post-item + .post-item {
border-top: none;
}
.post-item-hd { .post-item-hd {
display: flex; display: flex;
} }
...@@ -67,6 +77,18 @@ const username = $computed(() => { ...@@ -67,6 +77,18 @@ const username = $computed(() => {
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
color: #666; color: #666;
.is-hot {
display: inline-block;
margin-left: 14px;
width: 20px;
height: 20px;
font-size: 14px;
line-height: 20px;
color: #fff;
background-color: #ff8923;
text-align: center;
border-radius: 2px;
}
} }
.post-item__reply_count { .post-item__reply_count {
font-size: 14px; font-size: 14px;
......
<script setup lang="ts"> <script setup lang="ts">
import type { TopPostItem } from '../types' import type { TopPostItem } from '../types'
import { getTopPostList } from '../api' import { getTopPostList } from '../api'
interface Props {
courseId?: string
semesterId?: string
}
const props = defineProps<Props>()
let list = $ref<TopPostItem[]>([]) let list = $ref<TopPostItem[]>([])
function fetchList() { function fetchList() {
getTopPostList().then(res => { getTopPostList({ course_id: props.courseId, semester_id: props.semesterId }).then(res => {
list = res.data.list list = res.data.list
}) })
} }
...@@ -15,12 +21,14 @@ onMounted(fetchList) ...@@ -15,12 +21,14 @@ onMounted(fetchList)
<template> <template>
<section class="pined-post" v-if="list.length"> <section class="pined-post" v-if="list.length">
<section class="pined-post-item" v-for="item in list" :key="item.id"> <section class="pined-post-item" v-for="item in list" :key="item.id">
<p class="t1">置顶</p> <router-link :to="`/bbs/${item.id}`" target="_blank" class="pined-post-item__inner">
<p class="t2">{{ item.title }}</p> <p class="t1">置顶</p>
<p class="t3"> <p class="t2">{{ item.title }}</p>
<span>{{ item.type }}</span> <p class="t3">
</p> <span>{{ item.course.name }}</span>
<p class="t4">{{ item.updated_time }}</p> </p>
<p class="t4">{{ item.updated_time }}</p>
</router-link>
</section> </section>
</section> </section>
</template> </template>
...@@ -28,12 +36,18 @@ onMounted(fetchList) ...@@ -28,12 +36,18 @@ onMounted(fetchList)
<style lang="scss"> <style lang="scss">
.pined-post { .pined-post {
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px solid #e6e6e6;
} }
.pined-post-item { .pined-post-item__inner {
display: flex; display: flex;
align-items: center; align-items: center;
}
.pined-post-item {
padding: 14px 0; padding: 14px 0;
&:hover {
.t2 {
color: var(--main-color);
}
}
.t1 { .t1 {
padding: 0 4px; padding: 0 4px;
height: 20px; height: 20px;
...@@ -56,6 +70,7 @@ onMounted(fetchList) ...@@ -56,6 +70,7 @@ onMounted(fetchList)
text-align: center; text-align: center;
span { span {
display: inline-block; display: inline-block;
padding: 0 20px;
min-width: 90px; min-width: 90px;
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
......
...@@ -31,7 +31,9 @@ export interface PostItem extends Post { ...@@ -31,7 +31,9 @@ export interface PostItem extends Post {
sso_user: User sso_user: User
} }
export type TopPostItem = Pick<Post, 'id' | 'title' | 'type' | 'created_time' | 'updated_time' | 'top_time'> export type TopPostItem = Pick<Post, 'id' | 'title' | 'type' | 'created_time' | 'updated_time' | 'top_time'> & {
course: Course
}
export interface DiscussItem { export interface DiscussItem {
id: string id: string
...@@ -89,3 +91,9 @@ export interface File { ...@@ -89,3 +91,9 @@ export interface File {
size: string size: string
upload_time: string upload_time: string
} }
export interface Course {
id: string
name: string
cover: string
}
...@@ -7,9 +7,16 @@ import { getPostList } from '../api' ...@@ -7,9 +7,16 @@ import { getPostList } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useGetCourseList } from '@/composables/useGetCourseList' import { useGetCourseList } from '@/composables/useGetCourseList'
interface Props {
courseId?: string
semesterId?: string
}
const props = defineProps<Props>()
const PostForm = defineAsyncComponent(() => import('../components/PostForm.vue')) const PostForm = defineAsyncComponent(() => import('../components/PostForm.vue'))
const { courses, courseId, chapters } = useGetCourseList() const { courses, courseValue, chapters } = useGetCourseList()
const courseList = $computed(() => { const courseList = $computed(() => {
return [{ value: '', label: '全部课程' }, ...courses.value] return [{ value: '', label: '全部课程' }, ...courses.value]
}) })
...@@ -24,9 +31,38 @@ const currentTypes = $computed(() => { ...@@ -24,9 +31,38 @@ const currentTypes = $computed(() => {
const appList = $ref<InstanceType<typeof AppList> | null>(null) const appList = $ref<InstanceType<typeof AppList> | null>(null)
const params = reactive({ search_by: 0, order_by: 0, course_id: '', semester_id: '', chapter_id: '', type: '' }) const params = reactive({
search_by: 0,
order_by: 0,
course_id: props.courseId || '',
semester_id: props.semesterId || '',
chapter_id: '',
type: ''
})
// 列表配置 // 列表配置
const listOptions = computed(() => { const listOptions = computed(() => {
const filters = [
{
type: 'select',
prop: 'chapter_id',
placeholder: '请选择',
options: chapterList,
labelKey: 'name',
valueKey: 'id'
},
{ type: 'select', prop: 'search_by', placeholder: '请选择', options: bbsSearchByList },
{ type: 'select', prop: 'type', placeholder: '请选择', options: currentTypes },
{ type: 'select', prop: 'order_by', placeholder: '请选择', options: bbsOrderByList }
]
!props.courseId &&
filters.unshift({
type: 'select',
prop: 'course_id',
placeholder: '请选择',
options: courseList,
labelKey: 'name',
valueKey: 'course_id'
})
return { return {
hasFilterButton: false, hasFilterButton: false,
remote: { remote: {
...@@ -37,34 +73,14 @@ const listOptions = computed(() => { ...@@ -37,34 +73,14 @@ const listOptions = computed(() => {
requestParams.chapter_id = '' requestParams.chapter_id = ''
} }
params.course_id = requestParams.course_id || '' params.course_id = requestParams.course_id || ''
courseId.value = params.course_id courseValue.value = params.course_id
return requestParams return requestParams
}, },
callback(res: { total: number; data: any }) { callback(res: { total: number; data: any }) {
return { total: res.total, list: res.data } return { total: res.total, list: res.data }
} }
}, },
filters: [ filters
{
type: 'select',
prop: 'course_id',
placeholder: '请选择',
options: courseList,
labelKey: 'name',
valueKey: 'course_id'
},
{
type: 'select',
prop: 'chapter_id',
placeholder: '请选择',
options: chapterList,
labelKey: 'name',
valueKey: 'id'
},
{ type: 'select', prop: 'search_by', placeholder: '请选择', options: bbsSearchByList },
{ type: 'select', prop: 'type', placeholder: '请选择', options: currentTypes },
{ type: 'select', prop: 'order_by', placeholder: '请选择', options: bbsOrderByList }
]
} }
}) })
// 刷新 // 刷新
...@@ -77,18 +93,23 @@ const postFormVisible = $ref(false) ...@@ -77,18 +93,23 @@ const postFormVisible = $ref(false)
<template> <template>
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-prepend> <template #header-prepend>
<el-button round type="primary" @click="postFormVisible = true">我要发帖</el-button> <el-button round type="primary" @click="postFormVisible = true" style="margin-right: 30px">我要发帖</el-button>
</template> </template>
<template #body="{ data }"> <template #body="{ data }">
<!-- 置顶帖子 --> <!-- 置顶帖子 -->
<PostPinned></PostPinned> <PostPinned :courseId="courseId" :semesterId="semesterId"></PostPinned>
<template v-if="data.length"> <template v-if="data.length">
<PostItem :data="item" v-for="item in data" :key="item.id"></PostItem> <PostItem :data="item" v-for="item in data" :key="item.id"></PostItem>
</template> </template>
<el-empty description="暂无数据" v-else /> <el-empty description="暂无数据" v-else />
</template> </template>
</AppList> </AppList>
<PostForm v-model="postFormVisible" @update="handleRefetch" v-if="postFormVisible"></PostForm> <PostForm
:courseId="courseId"
:semesterId="semesterId"
v-model="postFormVisible"
@update="handleRefetch"
v-if="postFormVisible"></PostForm>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
......
<script setup lang="ts"> <script setup lang="ts">
import type { Post } from '../types' import type { Post } from '../types'
import { ArrowUp } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import DiscussItem from '../components/DiscussItem.vue' import DiscussItem from '../components/DiscussItem.vue'
import { getPostAndDiscussList } from '../api' import { getPostAndDiscussList } from '../api'
import { collectionResource } from '@/api/base'
const DiscussForm = defineAsyncComponent(() => import('../components/DiscussForm.vue')) const DiscussForm = defineAsyncComponent(() => import('../components/DiscussForm.vue'))
...@@ -35,22 +36,40 @@ function handleRefetch() { ...@@ -35,22 +36,40 @@ function handleRefetch() {
appList?.refetch() appList?.refetch()
} }
const discussFormVisible = $ref(false) const discussFormVisible = $ref(false)
// 收藏/取消收藏
function toggleCollection() {
collectionResource({
course_id: detail.course_id,
semester_id: detail.semester_id,
chapter_id: detail.chapter_id,
source_id: detail.id,
type: 6,
status: 1
}).then(() => {
// data.collection_count = data.collection_count ? 0 : 1
})
}
</script> </script>
<template> <template>
<div class="bbs"> <div class="bbs">
<div class="bbs-hd" v-if="detail"> <div class="bbs-hd" v-if="detail">
<h1>{{ detail.title }}</h1> <h1>{{ detail.title }}</h1>
<el-button round auto-insert-space>收藏</el-button> <el-button round auto-insert-space @click="toggleCollection">收藏</el-button>
<el-button round type="primary" auto-insert-space @click="discussFormVisible = true">回复</el-button> <el-button round type="primary" auto-insert-space @click="discussFormVisible = true">回复</el-button>
</div> </div>
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #body="{ data }"> <template #body="{ data }">
<DiscussItem :data="item" v-for="item in data" :key="item.id"></DiscussItem> <DiscussItem :landlordId="detail.sso_id" :data="item" v-for="item in data" :key="item.id"></DiscussItem>
</template> </template>
</AppList> </AppList>
</div> </div>
<DiscussForm v-model="discussFormVisible" :id="id" @update="handleRefetch" v-if="discussFormVisible"></DiscussForm> <DiscussForm v-model="discussFormVisible" :id="id" @update="handleRefetch" v-if="discussFormVisible"></DiscussForm>
<el-backtop :right="22" :bottom="100">
<el-icon><ArrowUp /></el-icon>
<span class="t1">返回<br />顶部</span>
</el-backtop>
</template> </template>
<style lang="scss"> <style lang="scss">
...@@ -63,12 +82,34 @@ const discussFormVisible = $ref(false) ...@@ -63,12 +82,34 @@ const discussFormVisible = $ref(false)
} }
} }
.bbs-hd { .bbs-hd {
position: relative;
padding: 20px; padding: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid #e4e4e4; border-bottom: 1px solid #e4e4e4;
box-shadow: #e4e4e4 0px 5px 10px;
z-index: 10;
h1 { h1 {
flex: 1; flex: 1;
} }
} }
.el-backtop {
width: 60px;
height: 60px;
color: #000;
.t1 {
display: none;
font-size: 14px;
color: #fff;
}
&:hover {
background-color: var(--main-color);
.el-icon {
display: none;
}
.t1 {
display: inline-block;
}
}
}
</style> </style>
<!-- 论坛 --> <!-- 论坛 -->
<script setup lang="ts"></script> <script setup lang="ts">
import BBSIndex from '@/modules/bbs/views/Index.vue'
const route = useRoute()
const courseId = route.query.course_id as string
const semesterId = route.query.semester_id as string
</script>
<template> <template>
<el-empty description="暂无数据" /> <div style="margin: 0 -20px">
<BBSIndex :courseId="courseId" :semesterId="semesterId"></BBSIndex>
</div>
</template> </template>
import type { CourseType, ChapterType, ResourceType } from '@/types' import type { CourseType, ChapterType, ResourceType } from '@/types'
type CollectionInfo = ResourceType & {
id: string
sso_id: string
sso_type: number
title: string
semester_id: string
class_id: string
course_id: string
chapter_id: string
type: number
is_top: number
top_time: null
is_hot: number
pv: number
uv: number
reply_count: number
created_time: string
updated_time: string
delete_time: number
}
export interface CollectionType { export interface CollectionType {
chapter_id: string chapter_id: string
course_id: string course_id: string
id: string id: string
info: ResourceType info: CollectionInfo
section_id: string section_id: string
semester_id: string semester_id: string
semester: CollectionSemesterType semester: CollectionSemesterType
......
...@@ -148,7 +148,7 @@ function targetUrl2(item: CollectionType) { ...@@ -148,7 +148,7 @@ function targetUrl2(item: CollectionType) {
<p> <p>
<router-link :to="targetUrl(item)" target="_blank"> <router-link :to="targetUrl(item)" target="_blank">
<ResourceIcon :resourceType="item.resource_type" :info="item.info" /> <ResourceIcon :resourceType="item.resource_type" :info="item.info" />
{{ item.info.name || item.info.paper_title }} {{ item.info.name || item.info.title || item.info.paper_title }}
</router-link> </router-link>
</p> </p>
<p> <p>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论