- 删除所有 JSP 页面(20个文件),前端完全迁移至 Vue 3 SPA - 完善评价系统:ReviewDialog 组件、用户评价历史页、评价状态检查API - 新增通知系统:Notification 实体/仓库/服务/控制器,NotificationCenter 接入真实API - 新增拼团模块:GroupBuying 全套后端和前端页面 - 修复 review check API 参数双重包装导致请求格式错误 - 修复通知 API 路径缺少 /api 前缀和响应格式处理 - MessageListenerService 集成 NotificationService 创建持久化通知 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
4.7 KiB
Vue
103 lines
4.7 KiB
Vue
<template>
|
|
<div class="page-shell">
|
|
<div class="page-header">
|
|
<div>
|
|
<h2 class="page-title">收藏管理</h2>
|
|
<p class="page-subtitle">查看用户收藏关系并支持后台删除</p>
|
|
</div>
|
|
<div class="actions">
|
|
<el-button @click="reloadData"><el-icon><Refresh /></el-icon>刷新</el-button>
|
|
<el-button type="primary" @click="migrateItems">迁移旧订单明细</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="mini-stat blue"><div class="mini-stat__value">{{ stats.totalFavorites }}</div><div class="mini-stat__label">收藏总数</div></div>
|
|
<div class="mini-stat green"><div class="mini-stat__value">{{ stats.favoriteUsers }}</div><div class="mini-stat__label">收藏用户数</div></div>
|
|
<div class="mini-stat orange"><div class="mini-stat__value">{{ stats.favoriteProducts }}</div><div class="mini-stat__label">被收藏商品数</div></div>
|
|
<div class="mini-stat purple"><div class="mini-stat__value">{{ stats.todayFavorites }}</div><div class="mini-stat__label">今日新增收藏</div></div>
|
|
</div>
|
|
|
|
<div class="panel-card filter-card">
|
|
<el-input v-model="keyword" clearable placeholder="搜索用户 / 商品" @keyup.enter="loadFavorites" />
|
|
<el-button type="primary" @click="loadFavorites">搜索</el-button>
|
|
</div>
|
|
|
|
<div class="panel-card">
|
|
<el-table v-loading="loading" :data="favorites" stripe>
|
|
<el-table-column prop="productName" label="商品" min-width="180" show-overflow-tooltip />
|
|
<el-table-column prop="productCategory" label="分类" width="120" />
|
|
<el-table-column prop="username" label="用户" width="120" />
|
|
<el-table-column prop="createdAt" label="收藏时间" min-width="170" />
|
|
<el-table-column label="操作" width="100">
|
|
<template #default="{ row }"><el-button text type="danger" @click="removeFavorite(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="loadFavorites" @size-change="loadFavorites" />
|
|
</div>
|
|
</div>
|
|
</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 { AdminFavoriteRow, AdminFavoriteStats } from '@/types/admin'
|
|
|
|
const loading = ref(false)
|
|
const keyword = ref('')
|
|
const page = ref(1)
|
|
const size = ref(10)
|
|
const total = ref(0)
|
|
const favorites = ref<AdminFavoriteRow[]>([])
|
|
const stats = reactive<AdminFavoriteStats>({ totalFavorites: 0, favoriteUsers: 0, favoriteProducts: 0, todayFavorites: 0 })
|
|
|
|
const loadStats = async () => {
|
|
const res = await adminApi.getFavoriteStats()
|
|
Object.assign(stats, res.data)
|
|
}
|
|
|
|
const loadFavorites = async () => {
|
|
loading.value = true
|
|
try {
|
|
const res = await adminApi.getFavorites({ page: page.value, size: size.value, keyword: keyword.value || undefined })
|
|
favorites.value = res.data.favorites
|
|
total.value = res.data.total
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const removeFavorite = async (id: number) => {
|
|
await ElMessageBox.confirm('确定删除该收藏记录吗?', '提示', { type: 'warning' })
|
|
await adminApi.deleteFavorite(id)
|
|
ElMessage.success('删除成功')
|
|
loadStats(); loadFavorites()
|
|
}
|
|
|
|
const migrateItems = async () => {
|
|
const res = await adminApi.migrateLegacyOrderItems()
|
|
ElMessage.success(`迁移完成:迁移 ${res.data.migrated} 条,跳过 ${res.data.skipped} 条`)
|
|
}
|
|
|
|
const reloadData = async () => { await Promise.all([loadStats(), loadFavorites()]) }
|
|
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; }
|
|
.actions { display:flex; gap:12px; }
|
|
.stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; }
|
|
.mini-stat { @apply rounded-xl p-5 shadow-sm; background:#fffaf2; color:#171715; border:1px solid #d8cebf; box-shadow:0 10px 24px rgba(23,22,20,0.04); }
|
|
.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 p-5; border:1px solid #d8cebf; box-shadow:0 10px 24px rgba(23,22,20,0.04); }
|
|
.filter-card { display:grid; grid-template-columns:1fr 100px; gap:12px; }
|
|
.table-footer { @apply flex justify-end mt-4; }
|
|
</style>
|