feat: 前端页面和组件全面完善
- 优化通用组件:导航栏、页脚、图片上传、搜索 - 完善业务组件:商品卡片、秒杀卡片 - 更新用户端页面:首页、商品、秒杀、订单、购物车、个人中心 - 新增用户收藏页面 - 完善管理后台:仪表盘、商品/订单/用户/秒杀管理 - 新增管理后台:收藏管理、评价管理、系统监控页面
This commit is contained in:
123
flash-sale-frontend/src/pages/admin/reviews.vue
Normal file
123
flash-sale-frontend/src/pages/admin/reviews.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">评价管理</h2>
|
||||
<p class="page-subtitle">查看、隐藏和回复用户评价</p>
|
||||
</div>
|
||||
<el-button @click="reloadData"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="mini-stat blue"><div class="mini-stat__value">{{ stats.totalReviews }}</div><div class="mini-stat__label">评价总数</div></div>
|
||||
<div class="mini-stat green"><div class="mini-stat__value">{{ stats.todayReviews }}</div><div class="mini-stat__label">今日新增</div></div>
|
||||
<div class="mini-stat orange"><div class="mini-stat__value">{{ stats.averageRating.toFixed(1) }}</div><div class="mini-stat__label">平均评分</div></div>
|
||||
<div class="mini-stat purple"><div class="mini-stat__value">{{ stats.fiveStarReviews }}</div><div class="mini-stat__label">五星评价</div></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card filter-card">
|
||||
<el-input v-model="keyword" clearable placeholder="搜索用户 / 商品 / 评价内容" @keyup.enter="loadReviews" />
|
||||
<el-button type="primary" @click="loadReviews">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<el-table v-loading="loading" :data="reviews" stripe>
|
||||
<el-table-column prop="productName" label="商品" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="username" label="用户" width="120" />
|
||||
<el-table-column prop="rating" label="评分" width="120"><template #default="{ row }"><el-rate :model-value="row.rating" disabled /></template></el-table-column>
|
||||
<el-table-column prop="content" label="评价内容" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="statusText" label="状态" width="90" />
|
||||
<el-table-column prop="adminReply" label="回复" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createdAt" label="时间" min-width="170" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" @click="openReply(row)">回复</el-button>
|
||||
<el-button text :type="row.status === 1 ? 'warning' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '隐藏' : '显示' }}</el-button>
|
||||
<el-button text type="danger" @click="removeReview(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="table-footer">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="loadReviews" @size-change="loadReviews" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="replyVisible" title="评价回复" width="600px">
|
||||
<el-input v-model="replyText" type="textarea" :rows="5" maxlength="500" show-word-limit placeholder="请输入管理员回复" />
|
||||
<template #footer>
|
||||
<el-button @click="replyVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitReply">保存回复</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { adminApi } from '@/api/modules/admin'
|
||||
import type { AdminReviewRow, AdminReviewStats } from '@/types/admin'
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const size = ref(10)
|
||||
const total = ref(0)
|
||||
const reviews = ref<AdminReviewRow[]>([])
|
||||
const stats = reactive<AdminReviewStats>({ totalReviews: 0, todayReviews: 0, averageRating: 0, fiveStarReviews: 0 })
|
||||
const replyVisible = ref(false)
|
||||
const currentReviewId = ref<number | null>(null)
|
||||
const replyText = ref('')
|
||||
|
||||
const loadStats = async () => {
|
||||
const res = await adminApi.getReviewStats()
|
||||
Object.assign(stats, res.data)
|
||||
}
|
||||
const loadReviews = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await adminApi.getReviews({ page: page.value, size: size.value, keyword: keyword.value || undefined })
|
||||
reviews.value = res.data.reviews
|
||||
total.value = res.data.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
const openReply = (row: AdminReviewRow) => { currentReviewId.value = row.id; replyText.value = row.adminReply || ''; replyVisible.value = true }
|
||||
const submitReply = async () => {
|
||||
if (!currentReviewId.value) return
|
||||
await adminApi.updateReview(currentReviewId.value, { adminReply: replyText.value, status: 1 })
|
||||
ElMessage.success('回复已保存')
|
||||
replyVisible.value = false
|
||||
loadReviews()
|
||||
}
|
||||
const toggleStatus = async (row: AdminReviewRow) => {
|
||||
await adminApi.updateReview(row.id, { status: row.status === 1 ? 0 : 1 })
|
||||
ElMessage.success('状态已更新')
|
||||
loadStats(); loadReviews()
|
||||
}
|
||||
const removeReview = async (id: number) => {
|
||||
await ElMessageBox.confirm('确定删除该评价吗?', '提示', { type: 'warning' })
|
||||
await adminApi.deleteReview(id)
|
||||
ElMessage.success('删除成功')
|
||||
loadStats(); loadReviews()
|
||||
}
|
||||
const reloadData = async () => { await Promise.all([loadStats(), loadReviews()]) }
|
||||
onMounted(() => { reloadData() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-shell { display:flex; flex-direction:column; gap:20px; }
|
||||
.page-header { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; }
|
||||
.page-title { @apply text-2xl font-bold text-slate-900; }
|
||||
.page-subtitle { @apply text-sm text-slate-500 mt-1; }
|
||||
.stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; }
|
||||
.mini-stat { @apply rounded-xl text-white p-5 shadow-sm; }
|
||||
.mini-stat.blue { background:linear-gradient(135deg,#3b82f6,#2563eb); }
|
||||
.mini-stat.green { background:linear-gradient(135deg,#10b981,#059669); }
|
||||
.mini-stat.orange { background:linear-gradient(135deg,#f59e0b,#ea580c); }
|
||||
.mini-stat.purple { background:linear-gradient(135deg,#8b5cf6,#7c3aed); }
|
||||
.mini-stat__value { @apply text-3xl font-bold; }
|
||||
.mini-stat__label { @apply text-sm opacity-90 mt-2; }
|
||||
.panel-card { @apply bg-white rounded-xl shadow-sm p-5; }
|
||||
.filter-card { display:grid; grid-template-columns:1fr 100px; gap:12px; }
|
||||
.table-footer { @apply flex justify-end mt-4; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user