audiobookshelf/client/components/tables/ReviewsTable.vue
fannta1990 633bc4805e Add review deletion functionality and UI enhancements
- Implemented delete functionality for reviews in both the ReviewModal and ReviewsTable components.
- Added confirmation prompts for review deletion actions.
- Updated ReviewController to allow deletion of reviews by admins or the review owner.
- Enhanced event handling to refresh the review list upon deletion.
- Improved UI to include delete buttons for admins in the ratings page and reviews table.
2026-02-09 22:31:44 +08:00

147 lines
5 KiB
Vue

<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
<p class="pr-2 md:pr-4">{{ $strings.LabelReviews }}</p>
<div v-if="averageRating" class="flex items-center">
<ui-star-rating :value="averageRating" readonly :size="18" />
<span class="text-sm text-gray-300 ml-2">({{ averageRating.toFixed(1) }})</span>
</div>
<div class="grow" />
<ui-btn small :color="userReview ? '' : 'bg-success'" class="mr-4" @click.stop="writeReview">
{{ userReview ? $strings.ButtonReviewEdit : $strings.ButtonReviewWrite }}
</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showReviews ? 'transform rotate-180' : ''">
<span class="material-symbols text-4xl">&#xe313;</span>
</div>
</div>
<transition name="slide">
<div class="w-full bg-bg/20" v-show="showReviews">
<div v-if="!reviews.length" class="p-6 text-center text-gray-400 italic">
{{ $strings.LabelNoReviews }}
</div>
<div v-else class="divide-y divide-white/5">
<div v-for="review in reviews" :key="review.id" class="p-4 md:p-6">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<p class="font-semibold text-gray-100 mr-3">{{ review.user.username }}</p>
<ui-star-rating :value="review.rating" readonly :size="16" />
</div>
<div class="flex items-center gap-2">
<p class="text-xs text-gray-400">{{ $formatDate(review.createdAt, dateFormat) }}</p>
<button v-if="isAdmin && review.userId !== user.id" class="p-1 rounded hover:bg-white/10 text-gray-400 hover:text-error transition-colors" title="Delete Review" @click.stop="deleteReviewAdmin(review)">
<span class="material-symbols text-base">delete</span>
</button>
</div>
</div>
<p v-if="review.reviewText" class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ review.reviewText }}</p>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
/**
* A table component to display reviews for a specific library item.
* Listens for global 'review-updated' and 'review-deleted' events to refresh the view locally.
*/
export default {
props: {
/** The library item object to show reviews for */
libraryItem: {
type: Object,
required: true
}
},
data() {
return {
showReviews: false,
reviews: [],
loading: false
}
},
computed: {
user() {
return this.$store.state.user.user
},
isAdmin() {
return this.user.type === 'admin' || this.user.type === 'root'
},
userReview() {
return this.reviews.find((r) => r.userId === this.user.id)
},
averageRating() {
if (!this.reviews.length) return 0
const sum = this.reviews.reduce((acc, r) => acc + r.rating, 0)
return sum / this.reviews.length
},
dateFormat() {
return this.$store.getters['getServerSetting']('dateFormat')
}
},
methods: {
clickBar() {
this.showReviews = !this.showReviews
},
async fetchReviews() {
this.loading = true
try {
this.reviews = await this.$axios.$get(`/api/items/${this.libraryItem.id}/reviews`)
} catch (error) {
console.error('Failed to fetch reviews', error)
} finally {
this.loading = false
}
},
writeReview() {
this.$store.commit('globals/setReviewModal', {
libraryItem: this.libraryItem,
review: this.userReview
})
},
async deleteReviewAdmin(review) {
if (!confirm(`Are you sure you want to delete ${review.user.username}'s review?`)) return
try {
await this.$axios.$delete(`/api/reviews/${review.id}`)
this.reviews = this.reviews.filter(r => r.id !== review.id)
this.$toast.success('Review deleted')
} catch (error) {
console.error('Failed to delete review', error)
this.$toast.error('Failed to delete review')
}
},
onReviewUpdated(review) {
const index = this.reviews.findIndex((r) => r.id === review.id)
if (index !== -1) {
this.$set(this.reviews, index, review)
} else {
this.reviews.unshift(review)
}
}
},
mounted() {
this.fetchReviews()
this.$root.$on('review-updated', (review) => {
if (review.libraryItemId === this.libraryItem.id) {
this.onReviewUpdated(review)
}
})
this.$root.$on('review-deleted', ({ libraryItemId, reviewId }) => {
if (libraryItemId === this.libraryItem.id) {
this.reviews = this.reviews.filter(r => r.id !== reviewId)
}
})
},
beforeDestroy() {
this.$root.$off('review-updated')
this.$root.$off('review-deleted')
}
}
</script>