提交 6ae0e86f authored 作者: matian's avatar matian

chore:帖子管理部分开发

上级 301e4d40
...@@ -166,13 +166,13 @@ export const menus: IMenuItem[] = [ ...@@ -166,13 +166,13 @@ export const menus: IMenuItem[] = [
{ {
tag: 'v1-teaching', tag: 'v1-teaching',
name: '教学管理', name: '教学管理',
path: '/teach', path: '/teach/posts',
children: [ children: [
{ {
tag: 'v1-backend-lecturer-list', // tag: 'v1-backend-lecturer-list',
icon: QuestionFilled, icon: QuestionFilled,
name: '问答管理', name: '帖子管理',
path: '/teach/qa' path: '/teach/posts'
}, },
{ {
tag: 'v1-teaching-paper-paper-list', tag: 'v1-teaching-paper-paper-list',
......
...@@ -5,7 +5,7 @@ import type { UploadProps, UploadUserFile } from 'element-plus' ...@@ -5,7 +5,7 @@ import type { UploadProps, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5' import md5 from 'blueimp-md5'
import { getSignature } from '@/api/base' import { getSignature } from '@/api/base'
const props = withDefaults(defineProps<{ modelValue: string | []; prefix?: string }>(), { const props = withDefaults(defineProps<{ modelValue: string | []; prefix?: string; isPost?: string }>(), {
prefix: 'upload/admin/' prefix: 'upload/admin/'
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
...@@ -27,6 +27,7 @@ const showFileList = computed(() => { ...@@ -27,6 +27,7 @@ const showFileList = computed(() => {
// 上传之前 // 上传之前
const handleBeforeUpload = async (file: any) => { const handleBeforeUpload = async (file: any) => {
console.log(uploadData.value)
const fileName = file.name const fileName = file.name
const key = props.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.')) const key = props.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
const response: Record<string, any> = await getSignature() const response: Record<string, any> = await getSignature()
...@@ -39,6 +40,25 @@ const handleBeforeUpload = async (file: any) => { ...@@ -39,6 +40,25 @@ const handleBeforeUpload = async (file: any) => {
url: `${response.host}/${key}` url: `${response.host}/${key}`
} }
file.url = `${response.host}/${key}` file.url = `${response.host}/${key}`
if (props.isPost) {
if (
fileList.value.filter((item: any) => item.name.includes('.png')).length +
fileList.value.filter((item: any) => item.name.includes('.jpg')).length +
fileList.value.filter((item: any) => item.name.includes('.jpeg')).length >
10
) {
const videoList = fileList.value.filter((item: any) => item.name.includes('.mp4'))
const imgList = fileList.value.filter((item: any) => !item.name.includes('.mp4')).slice(0, 10)
fileList.value = [...videoList, ...imgList]
ElMessage.warning('支持最多上传10张图片,格式支持jpg,jpeg,png')
} else if (fileList.value.filter((item: any) => item.name.includes('.mp4')).length > 1) {
const videoList = fileList.value.filter((item: any) => item.name.includes('.mp4')).slice(0, 1)
const imgList = fileList.value.filter((item: any) => !item.name.includes('.mp4'))
fileList.value = [...videoList, ...imgList]
console.log(fileList.value)
ElMessage.warning('视频最多长传1个')
}
}
} }
// 上传成功 // 上传成功
......
...@@ -71,7 +71,6 @@ const bytesToSize = (bytes: number) => { ...@@ -71,7 +71,6 @@ const bytesToSize = (bytes: number) => {
overflow: hidden; overflow: hidden;
margin-right: 20px; margin-right: 20px;
margin-bottom: 20px; margin-bottom: 20px;
// box-shadow: #666666;
box-shadow: 0 0 10px 0 rgb(0 0 0 / 10%); box-shadow: 0 0 10px 0 rgb(0 0 0 / 10%);
background: #fff; background: #fff;
.card-item-top { .card-item-top {
......
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util' import { splitStrLast } from '@/utils/util'
import { importStudent } from '../api' import { importStudent } from '../api'
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
...@@ -20,7 +19,6 @@ interface Emits { ...@@ -20,7 +19,6 @@ interface Emits {
// 取消 // 取消
const handleCancel = () => { const handleCancel = () => {
emit('update:isShowImportDialog', false) emit('update:isShowImportDialog', false)
} }
const beforeUpload = (file: any) => { const beforeUpload = (file: any) => {
......
import httpRequest from '@/utils/axios'
// 获取搜索条件
export function getConditionList(params?: {
type: string
organ_id?: string
semester_id: string
course_id?: string
}) {
return httpRequest.get('/api/resource/v1/teaching/discussion/search-list', { params })
}
// 搜索帖子列表
export function getPostsList(params?: {
organ_id?: string
semester_id?: string
class_id?: string
course_id?: string
chapter_id?: string
type?: string
is_top?: string
title?: string
name?: string
page?: string
['per-page']: string
}) {
return httpRequest.get('/api/resource/v1/teaching/discussion/list', { params })
}
// 搜索帖子列表
export function createPosts(data?: {
organ_id: string
semester_id: string
class_id: string
course_id: string
chapter_id: string
type: string
is_top: string
title: string
name: string
content: string
files: string
}) {
return httpRequest.post('/api/resource/v1/teaching/discussion/create', data)
}
// 删除帖子
export function delPosts(data?: { type: string; id: string }) {
return httpRequest.post('/api/resource/v1/teaching/discussion/delete', data)
}
// 置顶帖子
export function setTopPosts(data?: { discussion_id: string }) {
return httpRequest.post('/api/resource/v1/teaching/discussion/set-top', data)
}
// 帖子楼层的列表(一级评论列表)
export function getPostFirst(params?: { discussion_id: string; page?: string; ['per-page']: string }) {
return httpRequest.get('/api/resource/v1/teaching/discussion/replies', { params })
}
// 回复帖子
export function replyPosts(data?: { discussion_id: string; reply_type: string; content: string; reply_id: string }) {
return httpRequest.post('/api/resource/v1/teaching/discussion/reply', data)
}
// 帖子楼层回复的列表(二级评论列表)
export function getReplyPostItem(params?: { reply_id: string; page?: string; ['per-page']: string }) {
return httpRequest.get('/api/resource/v1/teaching/discussion/comments', { params })
}
<script lang="ts" setup>
import AppUpload from '@/components/base/AppUpload.vue'
import VEditor from '@/components/tinymce/Index.vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useProjectList } from '@/composables/useGetProjectList'
import { createPosts, getConditionList } from '../api'
import { useMapStore } from '@/stores/map'
const store = useMapStore()
const typeList = store.getMapValuesByKey('learning_discussion_type')
const departmentList: any = useProjectList('', '79806610719731712').departmentList
const ruleFormRef = ref<FormInstance>()
const emit = defineEmits<Emits>()
defineProps({
isShowAddDialog: {
type: Boolean
}
})
interface Emits {
(e: 'update:isShowAddDialog', isShowAddDialog: boolean): void
(e: 'create'): void
}
const form: any = reactive({
organ_id: '',
semester_id: '',
class_id: '',
course_id: '',
chapter_id: '',
type: '',
title: '',
content: '',
files: []
})
const rules = reactive<FormRules>({
organ_id: [{ required: true, message: '请选择所属机构', trigger: 'change' }],
semester_id: [{ required: true, message: '请选择所属学期', trigger: 'change' }],
class_id: [{ required: true, message: '请选择所属班级', trigger: 'change' }],
course_id: [{ required: true, message: '请选择所属课程', trigger: 'change' }],
chapter_id: [{ required: true, message: '请选择所属章节', trigger: 'change' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
title: [{ required: true, message: '请填写标题', trigger: 'change' }],
content: [{ required: true, message: '请填写正文内容', trigger: 'blur' }]
})
const semesterList: any = ref([])
const classList: any = ref([])
const courseList: any = ref([])
const chapterList: any = ref([])
watch(
() => form.organ_id,
value => {
if (value !== undefined || value !== '') {
handleGetSemesterList()
}
}
)
watch(
() => form.semester_id,
value => {
console.log(value, '999')
if ((form.organ_id !== undefined && value !== undefined) || (form.organ_id !== '' && value !== '')) {
handleGetClassList()
}
}
)
watch(
() => form.class_id,
value => {
console.log(value, '1111')
if (value !== undefined && form.organ_id !== undefined && form.semester_id !== undefined) {
handleGetCourseList()
}
}
)
watch(
() => form.course_id,
value => {
console.log(value, '1111')
if (
value !== undefined &&
form.organ_id !== undefined &&
form.semester_id !== undefined &&
form.course_id !== undefined
) {
handleGetChapterList()
}
}
)
// 获取学期列表
const handleGetSemesterList = () => {
if (form.organ_id !== '') {
const params: any = { type: 'semester', organ_id: form.organ_id }
getConditionList(params).then((res: any) => {
semesterList.value = res.data
})
}
}
// 获取班级列表
const handleGetClassList = () => {
if (form.organ_id !== '' && form.semester_id !== '') {
const params: any = { type: 'class', organ_id: form.organ_id, semester_id: form.semester_id }
getConditionList(params).then((res: any) => {
classList.value = res.data
})
}
}
// 获取课程列表
const handleGetCourseList = () => {
if (form.organ_id !== '' && form.semester_id !== '' && form.class_id !== '') {
const params: any = { type: 'course', organ_id: form.organ_id, semester_id: form.semester_id }
getConditionList(params).then((res: any) => {
courseList.value = res.data
})
}
}
// 获取课程章节列表
const handleGetChapterList = () => {
if (form.organ_id !== '' && form.semester_id !== '' && form.class_id !== '' && form.course_id !== '') {
const params: any = {
type: 'chapter',
organ_id: form.organ_id,
semester_id: form.semester_id,
course_id: form.course_id
}
getConditionList(params).then((res: any) => {
chapterList.value = res.data
})
}
}
// 取消
const handleCancel = () => {
emit('update:isShowAddDialog', false)
}
// 确认
const handleConfirm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(valid => {
if (valid) {
form.files = JSON.stringify(form.files)
const params: any = Object.assign({}, form)
createPosts(params).then(() => {
emit('update:isShowAddDialog', false)
emit('create')
})
}
})
}
const handleChangeOrgan = () => {
form.semester_id = ''
form.class_id = ''
form.course_id = ''
form.chapter_id = ''
}
const handleChangeSemester = () => {
form.class_id = ''
form.course_id = ''
form.chapter_id = ''
}
const handleChangeClass = () => {
form.course_id = ''
form.chapter_id = ''
}
const handleChangeCourse = () => {
form.chapter_id = ''
}
</script>
<template>
<el-dialog :model-value="isShowAddDialog" draggable title="新建帖子" :before-close="handleCancel">
<div style="height: 60vh; overflow-y: auto">
<el-form
:model="form"
label-position="right"
label-width="auto"
:rules="rules"
ref="ruleFormRef"
label-suffix=":"
>
<el-form-item label="所属机构" prop="organ_id">
<el-select
clearable
v-model="form.organ_id"
placeholder="所属机构"
style="width: 100%"
@change="handleChangeOrgan"
>
<el-option v-for="item in departmentList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属学期" prop="semester_id">
<el-select
clearable
v-model="form.semester_id"
placeholder="所属学期"
style="width: 100%"
@change="handleChangeSemester"
>
<el-option v-for="item in semesterList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属班级" prop="class_id">
<el-select
clearable
v-model="form.class_id"
placeholder="所属班级"
style="width: 100%"
@change="handleChangeClass"
>
<el-option v-for="item in classList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属课程" prop="course_id">
<el-select
clearable
v-model="form.course_id"
placeholder="所属课程"
style="width: 100%"
@change="handleChangeCourse"
>
<el-option v-for="item in courseList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属章节" prop="chapter_id">
<el-select clearable v-model="form.chapter_id" placeholder="所属章节" style="width: 100%">
<el-option v-for="item in chapterList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select clearable v-model="form.type" placeholder="类型" style="width: 100%">
<el-option v-for="item in typeList" :key="item.id" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="正文内容" prop="content">
<v-editor v-model="form.content" class="editor" :height="200"></v-editor>
</el-form-item>
<el-form-item>
<div>
<div class="upload-video">
<div class="upload-box">
<AppUpload
accept=".mp4,.png,.jpeg,.jpg"
:limit="11"
:multiple="true"
v-model="form.files"
:isPost="'1'"
></AppUpload>
</div>
</div>
<div class="tips">
支持最多上传10张图片,格式支持jpg,jpeg,png,2MB以内 <br />视频最多长传1个,100Mb以内
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm(ruleFormRef)">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.upload-video {
display: flex;
align-items: center;
.upload-btn {
position: absolute;
top: 0;
left: 0;
width: 94px;
line-height: 36px;
background: rgba(250, 223, 230, 0.39);
border: 1px solid #aa1941;
border-radius: 20px;
font-size: 14px;
color: #aa1941;
margin-right: 30px;
text-align: center;
}
}
.tips {
font-size: 12px;
color: #989898;
}
</style>
<script lang="ts" setup>
import AppUpload from '@/components/base/AppUpload.vue'
import VEditor from '@/components/tinymce/Index.vue'
import type { FormInstance, FormRules } from 'element-plus'
import { replyPosts } from '../api'
const ruleFormRef = ref<FormInstance>()
const emit = defineEmits<Emits>()
const props = defineProps({
isShowReplyDialog: {
type: Boolean
},
discussionId: {
type: String
}
})
interface Emits {
(e: 'update:isShowReplyDialog', isShowReplyDialog: boolean): void
(e: 'create'): void
}
const form: any = reactive({
content: '',
files: []
})
const rules = reactive<FormRules>({
content: [{ required: true, message: '请填写正文内容', trigger: 'blur' }]
})
// 取消
const handleCancel = () => {
emit('update:isShowReplyDialog', false)
}
// 确认
const handleConfirm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(valid => {
if (valid) {
form.files = JSON.stringify(form.files)
const params: any = Object.assign({ discussion_id: props.discussionId, reply_type: '1', reply_id: '' }, form)
replyPosts(params).then(() => {
emit('update:isShowReplyDialog', false)
emit('create')
})
}
})
}
</script>
<template>
<el-dialog :model-value="isShowReplyDialog" draggable title="回复新楼层" :before-close="handleCancel">
<div style="height: 60vh; overflow-y: auto">
<el-form
:model="form"
label-position="right"
label-width="auto"
:rules="rules"
ref="ruleFormRef"
label-suffix=":"
>
<el-form-item label="正文内容" prop="content">
<v-editor v-model="form.content" class="editor" :height="200"></v-editor>
</el-form-item>
<el-form-item>
<div>
<div class="upload-video">
<div class="upload-box">
<AppUpload
accept=".mp4,.png,.jpeg,.jpg"
:limit="11"
:multiple="true"
v-model="form.files"
:isPost="'1'"
></AppUpload>
</div>
</div>
<div class="tips">
支持最多上传10张图片,格式支持jpg,jpeg,png,2MB以内 <br />视频最多长传1个,100Mb以内
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm(ruleFormRef)">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.upload-video {
display: flex;
align-items: center;
.upload-btn {
position: absolute;
top: 0;
left: 0;
width: 94px;
line-height: 36px;
background: rgba(250, 223, 230, 0.39);
border: 1px solid #aa1941;
border-radius: 20px;
font-size: 14px;
color: #aa1941;
margin-right: 30px;
text-align: center;
}
}
.tips {
font-size: 12px;
color: #989898;
}
</style>
<script lang="ts" setup>
import { replyPosts, getReplyPostItem } from '../api'
const emit = defineEmits<Emits>()
const props = defineProps({
firstFloor: {
type: Object
},
discussionId: {
type: String
}
})
interface Emits {
(e: 'update'): void
}
const reply_content = ref('')
const isShowReply = ref(false)
const id = ref('')
const postItem: any = ref({})
const postItemList = ref(JSON.parse(JSON.stringify(props.firstFloor?.comments)))
// 最上面的回复
const handleReply = (val: any) => {
isShowReply.value = true
id.value = val.id
}
// 回复列表下面的回复
const handleReplyItem = (val: any) => {
isShowReply.value = true
postItem.value = val
}
// 提交回复
const handleReplySubmit = () => {
const params: any = {
discussion_id: props.discussionId,
reply_type: '2',
content: reply_content.value,
reply_id: props.firstFloor?.id
}
replyPosts(params).then(() => {
emit('update')
reply_content.value = ''
})
}
// 查看更多
const handleViewMore = (val: any) => {
handleGetItemFloor(val)
}
// 获取评论二级列表数据
const handleGetItemFloor = (val: any) => {
const params: any = {
reply_id: val
}
getReplyPostItem(params).then(res => {
postItemList.value = res.data.list
})
}
const imgUrl = computed(() => {
if (props.firstFloor?.tag === '学者') {
return 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/xz.png'
} else if (props.firstFloor?.tag === '大师') {
return 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/ds.png'
} else if (props.firstFloor?.tag === '智者') {
return 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/zz.png'
} else if (props.firstFloor?.tag === '学徒') {
return 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/xt.png'
}
})
</script>
<template>
<div class="main_post">
<div class="post_left">
<img
:src="
props.firstFloor?.avatar ||
'https://img2.baidu.com/it/u=655740671,4003943401&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400'
"
class="left_avatar"
/>
<div class="left_name">{{ props.firstFloor?.name }}</div>
<div class="left_remark" v-if="props.firstFloor?.tag !== '老师'">
<img :src="imgUrl" class="remark_img" />
<div class="remark_name">{{ props.firstFloor?.tag }}</div>
</div>
<div class="left_remark" v-else>
<div class="remark_name1">{{ props.firstFloor?.tag }}</div>
</div>
</div>
<div class="post_right">
<div class="right_con" v-html="props.firstFloor?.content"></div>
<template v-if="props.firstFloor?.files.length > 0">
<div class="right_files" v-for="(it, index) in props.firstFloor?.files" :key="index">
<img
:src="it?.url"
alt=""
v-if="it?.name.includes('.png') || it?.name.includes('.jpg') || it?.name.includes('.jpeg')"
style="width: 100px"
/>
<video :src="it?.url" v-if="it?.name.includes('.mp4')"></video>
</div>
</template>
<div class="right_time">发表于 {{ props.firstFloor?.updated_time }}</div>
<el-link :underline="false" class="right_btn" @click="handleReply(props.firstFloor)">
<img src="https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/reply_btn.png" alt="" />
<div class="btn_txt">回复</div>
</el-link>
<div class="right_list" v-if="props.firstFloor?.comments.length > 0">
<div v-for="(item, index) in postItemList" :key="index">
<div class="list_top">
<img
:src="item.avatar || 'https://webapp-pub.ezijing.com/center_resource/avatar.png'"
alt=""
class="top_img"
/>
<div
class="top_name"
v-html="
item.from_name !== item.to_name
? `${item.from_name} &nbsp;回复 &nbsp;${item.to_name}`
: `${item.to_name}`
"
></div>
</div>
<div class="list_con" v-html="item.content"></div>
<div class="list_bottom">
<div class="bottom_time">来自于{{ item.updated_time }}</div>
<el-link :underline="false" class="bottom_reply" @click="handleReplyItem(item)">
<img src="https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/reply_btn.png" alt="" />
<div class="reply_txt" @click="handleReplyItem(item)">回复</div>
</el-link>
</div>
</div>
<div class="list_more" v-if="props.firstFloor?.comments_total > 3">
<el-link :underline="false" type="primary" size="mini" @click="handleViewMore(props.firstFloor?.id)"
>查看更多</el-link
>
</div>
</div>
<div class="reply_input" v-if="isShowReply === true">
<el-input
v-model="reply_content"
:autosize="{ minRows: 2, maxRows: 4 }"
type="textarea"
style="width: 100%"
:placeholder="postItem.to_name ? `回复${postItem.to_name}` : ''"
/>
<el-button class="input_btn" type="primary" @click="handleReplySubmit">发表回复</el-button>
</div>
</div>
</div>
<div class="line"></div>
</template>
<style lang="scss" scoped>
.main_post {
width: 100%;
display: flex;
.post_left {
width: 240px;
padding: 40px;
background: #f9f9fc;
display: flex;
flex-direction: column;
align-items: center;
.left_avatar {
width: 100px;
height: 100px;
}
.left_name {
font-size: 16px;
font-weight: 400;
color: #333333;
margin-top: 14px;
}
.left_remark {
text-align: center;
margin-top: 8px;
.remark_img {
width: 40px;
height: 40px;
}
.remark_name {
font-size: 10px;
font-weight: 400;
color: #666666;
margin-top: 2px;
}
.remark_name1 {
width: 42px;
height: 20px;
border: 1px solid #ba143e;
border-radius: 10px;
font-size: 10px;
font-weight: 400;
color: #ba143e;
line-height: 20px;
text-align: center;
}
}
}
.post_right {
width: 100%;
padding: 40px 21px 0 40px;
display: flex;
flex-direction: column;
.right_con {
font-size: 16px;
font-weight: 400;
color: #666666;
line-height: 27px;
}
.right_time {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: #b4b4b4;
margin-top: 20px;
}
.right_btn {
align-self: flex-end;
display: flex;
img {
width: 22px;
height: 22px;
}
.btn_txt {
margin-left: 9px;
}
}
.reply_input {
margin-top: 20px;
display: flex;
flex-direction: column;
.input_txt {
font-size: 18px;
font-weight: 400;
line-height: 31px;
color: #333333;
margin: 20px 0;
}
.input_btn {
align-self: flex-end;
margin: 20px 0;
}
}
.right_list {
padding: 20px;
background: #f4f5f8;
margin: 20px 0 40px 0;
border-radius: 6px;
display: flex;
flex-direction: column;
.list_more {
align-self: center;
}
.list_top {
display: flex;
.top_img {
width: 30px;
height: 30px;
}
.top_name {
font-size: 16px;
font-weight: 400;
line-height: 27px;
color: #666666;
margin: 0 10px;
}
}
.list_con {
font-size: 16px;
font-weight: 400;
line-height: 27px;
color: #666666;
padding-left: 40px;
}
.list_bottom {
display: flex;
justify-content: space-between;
padding-left: 40px;
.bottom_time {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #b4b4b4;
}
.bottom_reply {
display: flex;
align-items: center;
cursor: pointer;
img {
width: 20px;
height: 20px;
}
.reply_txt {
font-size: 16px;
font-weight: 400;
line-height: 27px;
color: #9b9b9b;
margin-left: 9px;
}
}
}
}
}
}
.line {
width: 99%;
height: 4px;
background: #e8e8e8;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/teach/posts',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/List.vue') },
{ path: '/teach/posts/detail', component: () => import('./views/View.vue') }
]
}
]
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { getConditionList, getPostsList, delPosts, setTopPosts } from '../api'
import AddPostDialog from '../components/AddPostDialog.vue'
import { useProjectList } from '@/composables/useGetProjectList'
import { useUserStore } from '@/stores/user'
import { useMapStore } from '@/stores/map'
const router = useRouter()
const store = useMapStore()
const typeList = store.getMapValuesByKey('learning_discussion_type')
console.log(typeList, '123')
const departmentList: any = useProjectList('', '79806610719731712').departmentList
// 判断当前用户是不是超级管理员
const user = useUserStore().roles
const isAdmin = !!user.find((item: any) => item.name === '超级管理员')
const appList = ref()
const isShowAddDialog = ref(false)
const organ_id = ref('')
const semester_id = ref('')
const course_id = ref('')
const class_id = ref('')
const chapter_id = ref('')
const semesterList: any = ref([])
const classList: any = ref([])
const courseList: any = ref([])
const chapterList: any = ref([])
const listOptions = $computed(() => {
return {
remote: {
params: {
organ_id: '',
semester_id: '',
class_id: '',
course_id: '',
chapter_id: '',
type: '',
is_top: '',
title: '',
name: ''
},
beforeRequest(params: any) {
if (params.organ_id !== '') {
organ_id.value = params.organ_id
}
if (params.semester_id !== '') {
semester_id.value = params.semester_id
}
if (params.course_id !== '') {
course_id.value = params.course_id
}
if (params.chapter_id !== '') {
chapter_id.value = params.chapter_id
}
if (params.class_id !== '') {
class_id.value = params.class_id
}
return params
},
httpRequest: getPostsList
},
filters: [
{ type: 'select', prop: 'organ_id', slots: 'filter-department' },
{ type: 'select', prop: 'semester_id', slots: 'filter-semester' },
{ type: 'select', prop: 'class_id', slots: 'filter-class' },
{ type: 'select', prop: 'course_id', slots: 'filter-course' },
{ type: 'select', prop: 'chapter_id', slots: 'filter-chapter' },
{
type: 'select',
prop: 'type',
label: '帖子类型:',
placeholder: '帖子类型',
options: typeList,
labelKey: 'label',
valueKey: 'value'
},
{
type: 'select',
prop: 'is_top',
label: '是否置顶:',
placeholder: '是否置顶',
options: [
{ label: '是', value: '1' },
{ label: '否', value: '0' }
]
},
{ type: 'input', prop: 'title', label: '帖子标题:', placeholder: '帖子标题' },
{ type: 'input', prop: 'name', label: '发帖人:', placeholder: '发帖人' }
],
columns: [
{ label: '序号', type: 'index', align: 'center', width: '100px' },
{ label: '帖子标题', prop: 'title', align: 'center' },
{ label: '帖子类型', prop: 'type_name', align: 'center' },
{ label: '发帖人', prop: 'sso_name', align: 'center' },
{ label: '所属课程', prop: 'course_name', align: 'center' },
{ label: '所属班级', prop: 'class_name', align: 'center' },
{ label: '所属学期', prop: 'semester_name', align: 'center' },
{ label: '所属机构/院校', prop: 'organ_name', align: 'center' },
{ label: '回复数量', prop: 'reply_count', align: 'center' },
{ label: '更新时间', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', fixed: 'right', width: 300 }
]
}
})
const handleAddPosts = () => {
isShowAddDialog.value = true
}
// 刷新页面
const handleRefresh = () => {
appList.value.refetch()
}
console.log(listOptions)
watch(
() => organ_id.value,
value => {
if (value !== undefined) {
handleGetSemesterList()
}
}
)
watch(
() => semester_id.value,
value => {
console.log(value, '999')
if (value !== undefined) {
handleGetClassList()
}
}
)
watch(
() => course_id.value,
value => {
console.log(value, '1111')
if (value !== undefined && organ_id.value !== undefined && semester_id.value !== undefined) {
handleGetchaptereList()
}
}
)
// 获取学期列表
const handleGetSemesterList = () => {
const params: any = { type: 'semester', organ_id: organ_id.value }
getConditionList(params).then((res: any) => {
semesterList.value = res.data
})
}
// 获取班级列表
const handleGetClassList = () => {
const params: any = { type: 'class', organ_id: organ_id.value, semester_id: semester_id.value }
getConditionList(params).then((res: any) => {
classList.value = res.data
handleGetCourseList()
})
}
// 获取课程列表
const handleGetCourseList = () => {
const params: any = { type: 'course', organ_id: organ_id.value, semester_id: semester_id.value }
getConditionList(params).then((res: any) => {
courseList.value = res.data
})
}
// 获取课程章节列表
const handleGetchaptereList = () => {
const params: any = {
type: 'chapter',
organ_id: organ_id.value,
semester_id: semester_id.value,
course_id: course_id.value
}
getConditionList(params).then((res: any) => {
chapterList.value = res.data
})
}
// 删除帖子
const handleDel = (row: any) => {
const params: any = { id: row.id, type: 'discussion' }
delPosts(params).then(() => {
ElMessage.success('帖子删除成功')
handleRefresh()
})
}
// 置顶帖子
const handleSetTop = (row: any) => {
setTopPosts({ discussion_id: row.id }).then(() => {
ElMessage.success('帖子置顶成功')
handleRefresh()
})
}
const handleReply = (row: any) => {
router.push({
path: '/teach/posts/detail',
query: {
id: row.id
}
})
}
</script>
<template>
<AppCard title="帖子管理">
<AppList v-bind="listOptions" ref="appList" border stripe>
<el-button type="primary" round @click="handleAddPosts" style="margin-bottom: 20px">新建帖子</el-button>
<template v-if="isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div>
<el-select @change="handleRefresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校">
<el-option v-for="item in departmentList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #filter-semester="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属学期:</div>
<el-select
@change="handleRefresh"
clearable
v-model="params.semester_id"
placeholder="请选择所属学期"
no-data-text="请先选择所属部门/学校"
>
<el-option v-for="item in semesterList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #filter-class="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属班级:</div>
<el-select
@change="handleRefresh"
clearable
v-model="params.class_id"
placeholder="请选择所属班级"
no-data-text="请先选择所属学期"
>
<el-option v-for="item in classList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #filter-course="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属课程:</div>
<el-select
@change="handleRefresh"
clearable
v-model="params.course_id"
placeholder="请选择所属课程"
no-data-text="请先选择所属班级、学期"
>
<el-option v-for="item in courseList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #filter-chapter="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属章节:</div>
<el-select
@change="handleRefresh"
clearable
v-model="params.chapter_id"
placeholder="请选择所属章节"
no-data-text="请先选择所属课程"
>
<el-option v-for="item in chapterList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #table-operate="{ row }">
<el-link type="primary" plain @click="handleReply(row)">添加回复</el-link>
<el-link type="primary" plain style="margin: 0 10px" @click="handleSetTop(row)">{{
row.is_top === '1' ? '取消置顶' : '置顶'
}}</el-link>
<el-link type="primary" plain @click="handleDel(row)">删除</el-link>
</template>
</AppList>
</AppCard>
<AddPostDialog v-model:isShowAddDialog="isShowAddDialog" v-if="isShowAddDialog" @create="handleRefresh" />
</template>
<script setup lang="ts">
import ReplyPostDialog from '../components/ReplyPostDialog.vue'
import ReplyPostItem from '../components/ReplyPostItem.vue'
import { getPostFirst } from '../api'
const route = useRoute()
const isShowReplyDialog = ref(false)
const postInfo: any = ref({})
const firstPostList: any = ref([])
const discussionId: any = route.query.id
const currentPage = ref(1)
const page = reactive({
size: 10,
currentPage: 1
})
onMounted(() => {
handleGetFirstFloor()
})
const handleGetFirstFloor = () => {
const params: any = { discussion_id: discussionId }
getPostFirst(params).then(res => {
postInfo.value = res.data.info
firstPostList.value = res.data.list
firstPostList.value.map((item: any) => {
item.files = JSON.parse(item.files)
})
})
}
const handleReplyNewFloor = () => {
isShowReplyDialog.value = true
}
const handleSizeChange = (val: any) => {
page.size = val
}
const handleCurrentChange = (val: any) => {
page.currentPage = val
}
</script>
<template>
<AppCard title="帖子详情">
<div class="card">
<div class="card-header">
<span>{{ postInfo.title }}</span>
<div class="content_btn">
<el-button type="primary" round>删除当前帖子</el-button>
<el-button @click="handleReplyNewFloor" round>回复新楼层</el-button>
</div>
</div>
<ReplyPostItem
v-for="(item, index) in firstPostList.slice((page.currentPage - 1) * page.size, page.currentPage * page.size)"
:key="index"
:firstFloor="item"
:discussionId="discussionId"
@update="handleGetFirstFloor"
/>
<el-pagination
class="pagination"
v-model:currentPage="currentPage"
:page-size="page.size"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[5, 10, 20, 30, 50]"
:total="firstPostList.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
</AppCard>
<ReplyPostDialog
v-model:isShowReplyDialog="isShowReplyDialog"
v-if="isShowReplyDialog"
:discussionId="discussionId"
@create="handleGetFirstFloor"
/>
</template>
<style lang="scss" scoped>
.card {
border: 1px solid #e5e5e5;
border-radius: 6px;
background: #ffffff;
display: flex;
flex-direction: column;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 19px;
background: rgba(255, 255, 255, 0.39);
padding: 20px 46px 20px 19px;
box-shadow: 0 0 10px 0 #e4e4e4;
}
.pagination {
margin: 30px 20px 50px 0;
align-self: flex-end;
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论