feat(admin): 新增收藏与点赞列表页
全量 lint 仍有 5 处既有 vue/no-mutating-props 基线错误, 位于 LocaleTextFields.vue 与 ClassificationDramaDialogs.vue,本次未处理。
This commit is contained in:
1 parent
d661b808d1
commit
5580f9b99b
3 files changed
+399
No files matched your search
@@ -0,0 +1,50 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
/** 收藏与点赞只读分页筛选条件。 */
|
||||
export interface EngagementPageParams {
|
||||
pageIndex?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
userId?: string | number
|
||||
deviceId?: string
|
||||
dramaId?: string | number
|
||||
dramaTitle?: string
|
||||
targetType?: string
|
||||
targetId?: string | number
|
||||
episodeId?: string | number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
/** 后台互动列表行。 */
|
||||
export interface EngagementItem {
|
||||
id: string
|
||||
userId: string | null
|
||||
uid: string | null
|
||||
nickname: string | null
|
||||
deviceId: string | null
|
||||
anonymous: boolean
|
||||
dramaId: string | null
|
||||
dramaTitle: string | null
|
||||
dramaCover: string | null
|
||||
dramaStatus: string | null
|
||||
dramaPublished: boolean
|
||||
targetType: string
|
||||
targetId: string
|
||||
episodeId: string | null
|
||||
episodeNo: number | null
|
||||
episodeTitle: string | null
|
||||
commentSummary: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 查询收藏分页。 */
|
||||
export const pageFavoritesApi = (params: EngagementPageParams) =>
|
||||
request.get<PageResult<EngagementItem>>({ url: '/admin/favorites/page', params })
|
||||
|
||||
/** 查询点赞分页。 */
|
||||
export const pageLikesApi = (params: EngagementPageParams) =>
|
||||
request.get<PageResult<EngagementItem>>({ url: '/admin/likes/page', params })
|
||||
@@ -0,0 +1,172 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
import { pageFavoritesApi } from '@/api/admin/engagement'
|
||||
|
||||
const PAGE_SIZES = [10, 20, 50]
|
||||
|
||||
const filters = reactive({ keyword: '', deviceId: '', dramaTitle: '', range: null })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pageFavoritesApi({
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
deviceId: filters.deviceId.trim() || undefined,
|
||||
dramaTitle: filters.dramaTitle.trim() || undefined,
|
||||
|
||||
startTime: filters.range?.[0] || undefined,
|
||||
endTime: filters.range?.[1] || undefined
|
||||
})
|
||||
rows.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
Object.assign(filters, { keyword: '', deviceId: '', dramaTitle: '', range: null })
|
||||
search()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading eyebrow="用户管理" title="收藏列表" />
|
||||
<section class="panel">
|
||||
<div class="console-toolbar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="uid / 昵称"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filters.deviceId"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="设备 ID"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filters.dramaTitle"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="短剧名称"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
v-model="filters.range"
|
||||
class="engagement-range"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<span class="console-count">共 {{ total }} 条</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
row-key="id"
|
||||
border
|
||||
class="drama-el-table"
|
||||
empty-text="暂无收藏"
|
||||
>
|
||||
<el-table-column label="用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="app-user-meta">
|
||||
<template v-if="row.userId == null">
|
||||
<el-tag type="info" size="small">游客</el-tag>
|
||||
<span class="app-user-uid">{{ row.deviceId || '—' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="app-user-name">{{ row.nickname || '未设置昵称' }}</span>
|
||||
<span class="app-user-uid">{{ row.uid || `用户 #${row.userId}` }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="短剧" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="engagement-drama">
|
||||
<el-image
|
||||
v-if="row.dramaCover"
|
||||
:src="row.dramaCover"
|
||||
fit="cover"
|
||||
class="engagement-cover"
|
||||
/>
|
||||
<span>{{ row.dramaTitle || `短剧 #${row.dramaId || '—'}` }}</span>
|
||||
<el-tag v-if="row.dramaPublished === false" type="info" size="small" effect="plain"
|
||||
>未发布</el-tag
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dramaId" label="短剧 ID" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="收藏时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pageIndex"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="PAGE_SIZES"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.engagement-range {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.engagement-drama {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.engagement-cover {
|
||||
width: 40px;
|
||||
height: 56px;
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
import { pageLikesApi } from '@/api/admin/engagement'
|
||||
|
||||
const PAGE_SIZES = [10, 20, 50]
|
||||
const TARGET_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'DRAMA', label: '短剧' },
|
||||
{ value: 'EPISODE', label: '剧集' },
|
||||
{ value: 'COMMENT', label: '评论' }
|
||||
]
|
||||
|
||||
const filters = reactive({ keyword: '', deviceId: '', dramaTitle: '', range: null, targetType: '' })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pageLikesApi({
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
deviceId: filters.deviceId.trim() || undefined,
|
||||
dramaTitle: filters.dramaTitle.trim() || undefined,
|
||||
targetType: filters.targetType || undefined,
|
||||
startTime: filters.range?.[0] || undefined,
|
||||
endTime: filters.range?.[1] || undefined
|
||||
})
|
||||
rows.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
Object.assign(filters, { keyword: '', deviceId: '', dramaTitle: '', range: null, targetType: '' })
|
||||
search()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'
|
||||
}
|
||||
|
||||
function targetText(row) {
|
||||
if (row.targetType === 'COMMENT') return row.commentSummary || '—'
|
||||
if (row.targetType === 'EPISODE') return `${row.dramaTitle || '—'} 第 ${row.episodeNo ?? '—'} 集`
|
||||
return row.dramaTitle || '—'
|
||||
}
|
||||
|
||||
function typeText(type) {
|
||||
return TARGET_OPTIONS.find((item) => item.value === type)?.label || type || '—'
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading eyebrow="用户管理" title="点赞列表" />
|
||||
<section class="panel">
|
||||
<div class="console-toolbar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="uid / 昵称"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filters.deviceId"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="设备 ID"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filters.dramaTitle"
|
||||
class="console-keyword"
|
||||
clearable
|
||||
placeholder="短剧名称"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-select v-model="filters.targetType" class="console-select" placeholder="全部目标类型">
|
||||
<el-option
|
||||
v-for="item in TARGET_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="filters.range"
|
||||
class="engagement-range"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<span class="console-count">共 {{ total }} 条</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
row-key="id"
|
||||
border
|
||||
class="drama-el-table"
|
||||
empty-text="暂无点赞"
|
||||
>
|
||||
<el-table-column label="用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="app-user-meta">
|
||||
<template v-if="row.userId == null">
|
||||
<el-tag type="info" size="small">游客</el-tag>
|
||||
<span class="app-user-uid">{{ row.deviceId || '—' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="app-user-name">{{ row.nickname || '未设置昵称' }}</span>
|
||||
<span class="app-user-uid">{{ row.uid || `用户 #${row.userId}` }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标类型" width="110">
|
||||
<template #default="{ row }">{{ typeText(row.targetType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }"
|
||||
><span :title="targetText(row)">{{ targetText(row) }}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dramaTitle" label="所属短剧" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="点赞时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pageIndex"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="PAGE_SIZES"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.engagement-range {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user