SAMS/ruoyi-ui/src/views/sams/active/detail/index.vue

429 lines
11 KiB
Vue

<template>
<div class="activity-detail-page">
<el-card class="activity-card" shadow="hover">
<!-- 活动基本信息 -->
<div class="header">
<h2 class="title">{{ detail.title }}</h2>
<span class="sub">{{ detail.clubName }} | {{ detail.deptName }}</span>
</div>
<div v-if="detail.coverImage" class="cover-wrapper">
<el-image :src="resolveImageUrl(detail.coverImage)" class="cover-image" fit="cover"/>
</div>
<el-descriptions :label-style="{ width: '120px' }" border column="2">
<el-descriptions-item label="开始时间">{{ formatDate(detail.startTime, false) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ formatDate(detail.endTime, false) }}</el-descriptions-item>
<el-descriptions-item label="活动地点">{{ detail.location }}</el-descriptions-item>
</el-descriptions>
<div class="section">
<h3>活动介绍</h3>
<div class="rich-text" v-html="detail.description"/>
</div>
<el-divider/>
<div class="section interactions">
<h3>互动</h3>
<div class="interaction-row">
<div class="reaction-buttons">
<el-button type="text" @click="handleReaction('like')">👍 {{ reactionStats.like }}</el-button>
<el-button type="text" @click="handleReaction('dislike')">👎 {{ reactionStats.dislike }}</el-button>
</div>
<div class="signup-buttons">
<el-button v-if="!hasRegistered" type="primary" @click="handleRegister">报名参加</el-button>
<el-button v-else-if="hasRegistered && !hasAttended" type="success" @click="handleSignIn">签到</el-button>
<el-button v-else disabled type="success">已签到</el-button>
</div>
</div>
</div>
<el-divider/>
<div class="section comments">
<h3>评论列表</h3>
<div v-for="comment in nestedComments" :key="comment.commentId" class="comment-block">
<div class="comment-item">
<span class="comment-user">{{ comment.userName }}</span>
<span class="comment-time">{{ formatDate(comment.createdAt) }}</span>
<div class="comment-content">{{ comment.content }}</div>
<div class="comment-actions">
<el-button size="mini" type="text" @click="replyTo(comment)">回复</el-button>
</div>
</div>
<div v-if="comment.children.length" class="comment-children">
<div
v-for="child in comment.children"
:key="child.commentId"
class="comment-item child"
>
<span class="comment-user">{{ child.userName }}</span>
<span class="comment-time">{{ formatDate(child.createdAt) }}</span>
<div class="comment-content">{{ child.content }}</div>
<div class="comment-actions">
<el-button size="mini" type="text" @click="replyTo(child)">回复</el-button>
</div>
</div>
</div>
</div>
<!-- 评论输入区 -->
<div class="add-comment">
<el-input
v-model="newCommentContent"
:rows="3"
placeholder="写下你的评论或回复..."
type="textarea"
/>
<el-button :disabled="!newCommentContent.trim()" size="small" type="primary" @click="submitComment">提交
</el-button>
<el-button v-if="replyTarget" size="small" @click="cancelReply"></el-button>
</div>
</div>
</el-card>
</div>
</template>
<script>
import {getActivity} from '@/api/ams/activity'
import {addRegistration, listRegistration, updateRegistration} from '@/api/ams/registration'
import {addComment} from '@/api/ams/comment'
import {addReaction, delReaction, updateReaction} from '@/api/ams/reaction'
import {parseTime} from '@/utils/ruoyi'
import {listComment, listReaction} from '@/api/ui/stufront'
export default {
name: 'ActivityDetail',
data() {
return {
detail: {},
registration: null,
commentQuery: {pageNum: 1, pageSize: 5, actId: null},
commentList: [],
commentTotal: 0,
loadingComments: false,
newCommentContent: '',
replyTarget: null,
reactions: [],
reactionStats: {like: 0, dislike: 0},
reactionQuery: {pageNum: 1, pageSize: 5, actId: null},
reactionList: [],
reactionTotal: 0,
loadingReactions: false,
}
},
computed: {
userId() {
return this.$store.state.user.id
},
hasRegistered() {
return !!this.registration
},
hasAttended() {
return this.registration && this.registration.attendTime !== null
},
nestedComments() {
const map = {}
this.commentList.forEach(c => {
map[c.commentId] = { ...c, children: [] }
})
const tree = []
this.commentList.forEach(c => {
const parentId = c.parentCommentId
if (parentId && map[parentId]) {
map[parentId].children.push(map[c.commentId])
} else {
tree.push(map[c.commentId])
}
})
return tree
}
},
created() {
const actId = this.$route.params.id
this.commentQuery.actId = actId
this.reactionQuery.actId = actId
this.fetchData(actId)
this.fetchRegistration(actId)
this.getReactions()
this.fetchReactions()
this.getComments()
},
methods: {
fetchData(id) {
getActivity(id).then(r => (this.detail = r.data || {}))
},
fetchRegistration(actId) {
listRegistration({actId, userId: this.userId})
.then(r => (this.registration = r.rows[0] || null))
},
fetchReactions() {
listReaction({actId: this.detail.actId}).then(r => {
this.reactionStats = r.data || {like: 0, dislike: 0}
})
},
getComments() {
this.loadingComments = true
listComment(this.commentQuery).then(r => {
this.commentList = r.rows
this.commentTotal = r.total
}).finally(() => this.loadingComments = false)
},
submitComment() {
if (!this.newCommentContent.trim()) {
return this.$message.warning('评论不能为空')
}
addComment({
actId: this.detail.actId,
userId: this.userId,
content: this.newCommentContent,
parentCommentId: this.replyTarget ? this.replyTarget.commentId : null
}).then(() => {
this.newCommentContent = ''
this.replyTarget = null
this.getComments()
})
},
replyTo(comment) {
this.replyTarget = comment
this.newCommentContent = `@${comment.createBy || comment.userId} `
},
cancelReply() {
this.replyTarget = null
this.newCommentContent = ''
},
getReactions() {
this.loadingReactions = true
listReaction(this.reactionQuery).then(r => {
this.reactionList = r.rows
this.reactionTotal = r.total
}).finally(() => this.loadingReactions = false)
},
handleReaction(type) {
const exist = this.reactions.find(r => r.userId === this.userId)
if (!exist) {
addReaction({
actId: this.detail.actId,
userId: this.userId,
reactionType: type
}).then(() => this.fetchReactions())
} else if (exist.reactionType === type) {
delReaction(exist.reactionId).then(() => this.fetchReactions())
} else {
updateReaction({reactionId: exist.reactionId, reactionType: type}).then(() => this.fetchReactions())
}
},
handleRegister() {
addRegistration({
actId: this.detail.actId,
userId: this.userId,
registerTime: this.formatDateOnly(new Date())
}).then(() => this.fetchRegistration(this.detail.actId))
},
handleSignIn() {
updateRegistration({
regId: this.registration.regId,
actId: this.registration.actId,
userId: this.registration.userId,
attendTime: this.formatDateOnly(new Date()),
status: 'attended' // <-- 同步设置状态为已签到
}).then(() => {
this.$message.success('签到成功')
this.fetchRegistration(this.detail.actId)
})
},
formatDate(date, showTime = true) {
return parseTime(date, showTime ? '{y}-{m}-{d} {h}:{i}' : '{y}-{m}-{d}')
},
formatDateOnly(date) {
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
return `${y}-${m}-${d}`
},
resolveImageUrl(url) {
if (!url) return require('@/assets/images/loading2.png').default
return url.startsWith('http') ? url : process.env.VUE_APP_BASE_API + url
}
}
}
</script>
<style scoped>
.activity-detail-page {
padding: 20px;
}
.activity-card {
max-width: 1000px;
margin: auto;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header .title {
font-size: 26px;
font-weight: bold;
}
.header .sub {
font-size: 14px;
color: #888;
}
.cover-wrapper {
text-align: center;
margin-bottom: 24px;
}
.cover-image {
width: 100%;
max-height: 400px;
border-radius: 8px;
object-fit: cover;
}
.section {
margin-top: 30px;
}
.section h3 {
margin-bottom: 12px;
font-weight: bold;
}
.rich-text {
background: #fafafa;
padding: 16px;
border-radius: 4px;
}
.interactions .interaction-row {
display: flex;
align-items: center;
gap: 24px;
}
.reaction-buttons {
font-size: 18px;
}
.signup-buttons el-button {
margin-left: 8px;
}
.comments .add-comment {
display: flex;
gap: 8px;
margin-top: 12px;
}
.comment-block {
margin-bottom: 20px;
}
.comment-item {
background: #f9f9f9;
padding: 10px;
border-radius: 6px;
margin-bottom: 6px;
}
.comment-user {
font-weight: bold;
margin-right: 8px;
}
.comment-time {
color: #999;
font-size: 12px;
}
.comment-content {
margin: 6px 0;
}
.comment-actions {
text-align: right;
}
.comment-children {
margin-left: 24px;
}
.child {
background: #f0f0f0;
}
.comment-block {
margin-bottom: 6px;
}
.comment-item {
background: #fafafa;
padding: 6px 10px;
border-radius: 3px;
font-size: 13px;
line-height: 1.4;
margin-bottom: 4px;
border: 1px solid #eee;
position: relative;
}
.comment-user {
font-weight: 500;
margin-right: 6px;
font-size: 13px;
color: #333;
}
.comment-time {
color: #bbb;
font-size: 12px;
}
.comment-content {
margin: 4px 0;
font-size: 13px;
}
.comment-actions {
position: absolute;
top: 6px;
right: 8px;
}
.comment-children {
margin-left: 14px;
}
.comment-item.child {
background: #f3f3f3;
border: 1px solid #e6e6e6;
}
.add-comment {
margin-top: 10px;
display: flex;
gap: 6px;
}
.add-comment .el-input__inner {
font-size: 13px;
padding: 6px 8px;
}
.add-comment .el-button {
padding: 6px 12px;
font-size: 13px;
}
</style>