185 lines
5.0 KiB
Vue
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>
|