Files
FlashSaleSystem/community-fresh-group-buy-frontend/src/pages/admin/favorites.vue
2026-05-06 23:30:54 +08:00

185 lines
5.0 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 label="商品" min-width="180" prop="productName" show-overflow-tooltip/>
<el-table-column label="分类" prop="productCategory" width="120"/>
<el-table-column label="用户" prop="username" width="120"/>
<el-table-column label="收藏时间" min-width="170" prop="createdAt"/>
<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" :page-sizes="[10,20,50]" :total="total"
layout="total, sizes, prev, pager, next, jumper" @current-change="loadFavorites"
@size-change="loadFavorites"/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
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 lang="scss" scoped>
.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>