@@ -28,3 +28,27 @@ export function deleteWatermarkTask(id) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getVideoParseLogs(params) {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/watermark/video-parse-logs',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVideoParseUnlocks(params) {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/watermark/video-parse-unlocks',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVideoDownloadFailures(params) {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/watermark/video-download-failures',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+64
-24
@@ -6,7 +6,6 @@
|
|||||||
@click="closeMobileMenu"
|
@click="closeMobileMenu"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
|
||||||
<el-aside :width="asideWidth" class="sidebar" :class="{ 'sidebar-mobile': isMobile }">
|
<el-aside :width="asideWidth" class="sidebar" :class="{ 'sidebar-mobile': isMobile }">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<span v-if="!isCollapse">管理后台</span>
|
<span v-if="!isCollapse">管理后台</span>
|
||||||
@@ -20,20 +19,36 @@
|
|||||||
router
|
router
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
>
|
>
|
||||||
<el-menu-item
|
<template v-for="routeItem in menuRoutes" :key="routeKey(routeItem)">
|
||||||
v-for="route in menuRoutes"
|
<el-sub-menu
|
||||||
:key="route.path"
|
v-if="hasChildren(routeItem)"
|
||||||
:index="route.path"
|
:index="resolvePath(routeItem.path)"
|
||||||
>
|
>
|
||||||
<el-icon><component :is="route.meta.icon" /></el-icon>
|
<template #title>
|
||||||
<template #title>{{ route.meta.title }}</template>
|
<el-icon><component :is="routeItem.meta.icon" /></el-icon>
|
||||||
</el-menu-item>
|
<span>{{ routeItem.meta.title }}</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="child in visibleChildren(routeItem)"
|
||||||
|
:key="routeKey(child)"
|
||||||
|
:index="resolvePath(routeItem.path, child.path)"
|
||||||
|
>
|
||||||
|
{{ child.meta?.title || child.name }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<el-menu-item
|
||||||
|
v-else
|
||||||
|
:index="resolvePath(routeItem.path)"
|
||||||
|
>
|
||||||
|
<el-icon><component :is="routeItem.meta.icon" /></el-icon>
|
||||||
|
<template #title>{{ routeItem.meta.title }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
|
||||||
<el-container>
|
<el-container>
|
||||||
<!-- 顶部导航栏 -->
|
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<el-icon class="collapse-icon" @click="toggleCollapse">
|
<el-icon class="collapse-icon" @click="toggleCollapse">
|
||||||
@@ -59,7 +74,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<!-- 主内容 -->
|
|
||||||
<el-main class="main-content">
|
<el-main class="main-content">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</el-main>
|
||||||
@@ -122,26 +136,51 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取菜单路由(过滤掉隐藏的路由)
|
|
||||||
const menuRoutes = computed(() => {
|
const menuRoutes = computed(() => {
|
||||||
const routes = router.getRoutes()
|
const rootRoute = router.options.routes.find((item) => item.path === '/')
|
||||||
const mainRoute = routes.find(r => r.path === '/')
|
if (!rootRoute || !rootRoute.children) return []
|
||||||
if (!mainRoute || !mainRoute.children) return []
|
return rootRoute.children.filter((item) => !item.meta?.hidden)
|
||||||
|
|
||||||
return mainRoute.children.filter(r => !r.meta?.hidden)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当前激活的菜单
|
const visibleChildren = (routeItem) => {
|
||||||
|
return (routeItem.children || []).filter((child) => !child.meta?.hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChildren = (routeItem) => {
|
||||||
|
return visibleChildren(routeItem).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKey = (routeItem) => {
|
||||||
|
return `${routeItem.path || ''}-${routeItem.name || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvePath = (parentPath = '', childPath = '') => {
|
||||||
|
const normalize = (value) => {
|
||||||
|
if (!value) return '/'
|
||||||
|
return value.startsWith('/') ? value : `/${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = normalize(parentPath)
|
||||||
|
if (!childPath) {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
if (childPath.startsWith('/')) {
|
||||||
|
return childPath
|
||||||
|
}
|
||||||
|
return `${parent.replace(/\/$/, '')}/${childPath}`.replace(/\/{2,}/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
const { path } = route
|
const { path } = route
|
||||||
// 如果是子路由,返回父路由路径
|
if (path === '/watermark') {
|
||||||
|
return '/watermark/video-parse-logs'
|
||||||
|
}
|
||||||
if (path.includes('/create') || path.includes('/edit') || /\/\d+$/.test(path)) {
|
if (path.includes('/create') || path.includes('/edit') || /\/\d+$/.test(path)) {
|
||||||
return '/' + path.split('/')[1]
|
return '/' + path.split('/')[1]
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换侧边栏折叠状态
|
|
||||||
const toggleCollapse = () => {
|
const toggleCollapse = () => {
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
mobileMenuVisible.value = !mobileMenuVisible.value
|
mobileMenuVisible.value = !mobileMenuVisible.value
|
||||||
@@ -167,7 +206,6 @@ const handleResize = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理下拉菜单命令
|
|
||||||
const handleCommand = async (command) => {
|
const handleCommand = async (command) => {
|
||||||
if (command === 'logout') {
|
if (command === 'logout') {
|
||||||
try {
|
try {
|
||||||
@@ -222,11 +260,13 @@ const handleCommand = async (command) => {
|
|||||||
background: #304156;
|
background: #304156;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item) {
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
color: #bfcbd9;
|
color: #bfcbd9;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item:hover) {
|
:deep(.el-menu-item:hover),
|
||||||
|
:deep(.el-sub-menu__title:hover) {
|
||||||
background: #263445 !important;
|
background: #263445 !important;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -72,8 +72,29 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: 'watermark',
|
path: 'watermark',
|
||||||
name: 'Watermark',
|
name: 'Watermark',
|
||||||
component: () => import('../views/watermark/index.vue'),
|
component: () => import('../views/watermark/layout.vue'),
|
||||||
meta: { title: '去水印管理', icon: 'Brush' }
|
redirect: '/watermark/video-parse-logs',
|
||||||
|
meta: { title: '去水印小程序', icon: 'Brush' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'video-parse-logs',
|
||||||
|
name: 'VideoParseLogs',
|
||||||
|
component: () => import('../views/watermark/video-parse-logs.vue'),
|
||||||
|
meta: { title: '解析日志' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'video-parse-unlocks',
|
||||||
|
name: 'VideoParseUnlocks',
|
||||||
|
component: () => import('../views/watermark/video-parse-unlocks.vue'),
|
||||||
|
meta: { title: '广告解锁' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'video-download-failures',
|
||||||
|
name: 'VideoDownloadFailures',
|
||||||
|
component: () => import('../views/watermark/video-download-failures.vue'),
|
||||||
|
meta: { title: '下载失败上报' }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="filters">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
placeholder="搜索失败链接/错误信息/UA"
|
||||||
|
clearable
|
||||||
|
style="width: 260px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="query.domain"
|
||||||
|
placeholder="来源域名"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="domain" label="域名" min-width="140" />
|
||||||
|
<el-table-column label="失败链接" min-width="280" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :href="row.failed_url" target="_blank" type="primary" :underline="false">
|
||||||
|
{{ row.failed_url }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="错误信息" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.error_message || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="client_ip" label="IP" width="140" />
|
||||||
|
<el-table-column label="UA" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.user_agent || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="上报时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.reported_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
v-model:page-size="query.page_size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { getVideoDownloadFailures } from '../../api/watermark'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
keyword: '',
|
||||||
|
domain: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatDateTime = (value) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildParams = () => {
|
||||||
|
const params = {
|
||||||
|
page: query.page,
|
||||||
|
page_size: query.page_size,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
domain: query.domain || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
||||||
|
params.date_from = dateRange.value[0]
|
||||||
|
params.date_to = dateRange.value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getVideoDownloadFailures(buildParams())
|
||||||
|
const payload = res.data || {}
|
||||||
|
list.value = payload.list || []
|
||||||
|
total.value = payload.total || 0
|
||||||
|
query.page = payload.page || query.page
|
||||||
|
query.page_size = payload.page_size || query.page_size
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
query.keyword = ''
|
||||||
|
query.domain = ''
|
||||||
|
dateRange.value = []
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="filters">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
placeholder="搜索原文/解析链接/错误信息"
|
||||||
|
clearable
|
||||||
|
style="width: 260px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-model="query.mini_program_id"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
placeholder="全部小程序"
|
||||||
|
@change="handleSearch"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in miniProgramOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-model="query.user_id"
|
||||||
|
:min="1"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="用户ID"
|
||||||
|
style="width: 130px"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-model="query.free_quota_used"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
placeholder="免费次数"
|
||||||
|
@change="handleSearch"
|
||||||
|
>
|
||||||
|
<el-option label="计入免费" :value="true" />
|
||||||
|
<el-option label="不计入免费" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="小程序" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.mini_program_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="90" />
|
||||||
|
<el-table-column label="原始内容" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.request_content || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="解析链接" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link
|
||||||
|
v-if="row.parsed_url"
|
||||||
|
:href="row.parsed_url"
|
||||||
|
target="_blank"
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
>
|
||||||
|
{{ row.parsed_url }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="免费次数" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.free_quota_used ? 'success' : 'info'">
|
||||||
|
{{ row.free_quota_used ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="duration_ms" label="耗时(ms)" width="100" />
|
||||||
|
<el-table-column label="错误信息" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.error_message || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
v-model:page-size="query.page_size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { getMiniPrograms } from '../../api/miniProgram'
|
||||||
|
import { getVideoParseLogs } from '../../api/watermark'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const miniProgramOptions = ref([])
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
keyword: '',
|
||||||
|
mini_program_id: undefined,
|
||||||
|
user_id: undefined,
|
||||||
|
free_quota_used: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatDateTime = (value) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMiniProgramOptions = async () => {
|
||||||
|
const res = await getMiniPrograms({ page: 1, page_size: 200 })
|
||||||
|
const payload = res.data || {}
|
||||||
|
miniProgramOptions.value = payload.list || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildParams = () => {
|
||||||
|
const params = {
|
||||||
|
page: query.page,
|
||||||
|
page_size: query.page_size,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
mini_program_id: query.mini_program_id,
|
||||||
|
user_id: query.user_id,
|
||||||
|
free_quota_used: query.free_quota_used
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
||||||
|
params.date_from = dateRange.value[0]
|
||||||
|
params.date_to = dateRange.value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getVideoParseLogs(buildParams())
|
||||||
|
const payload = res.data || {}
|
||||||
|
list.value = payload.list || []
|
||||||
|
total.value = payload.total || 0
|
||||||
|
query.page = payload.page || query.page
|
||||||
|
query.page_size = payload.page_size || query.page_size
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
query.keyword = ''
|
||||||
|
query.mini_program_id = undefined
|
||||||
|
query.user_id = undefined
|
||||||
|
query.free_quota_used = undefined
|
||||||
|
dateRange.value = []
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadMiniProgramOptions(), loadData()])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="filters">
|
||||||
|
<el-select
|
||||||
|
v-model="query.mini_program_id"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
placeholder="全部小程序"
|
||||||
|
@change="handleSearch"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in miniProgramOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-model="query.user_id"
|
||||||
|
:min="1"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="用户ID"
|
||||||
|
style="width: 130px"
|
||||||
|
/>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="小程序" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.mini_program_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||||
|
<el-table-column label="解锁日期" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.unlock_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="广告完成时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.ad_watched_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
v-model:page-size="query.page_size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { getMiniPrograms } from '../../api/miniProgram'
|
||||||
|
import { getVideoParseUnlocks } from '../../api/watermark'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const miniProgramOptions = ref([])
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
mini_program_id: undefined,
|
||||||
|
user_id: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatDateTime = (value) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (value) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return dayjs(value).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMiniProgramOptions = async () => {
|
||||||
|
const res = await getMiniPrograms({ page: 1, page_size: 200 })
|
||||||
|
const payload = res.data || {}
|
||||||
|
miniProgramOptions.value = payload.list || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildParams = () => {
|
||||||
|
const params = {
|
||||||
|
page: query.page,
|
||||||
|
page_size: query.page_size,
|
||||||
|
mini_program_id: query.mini_program_id,
|
||||||
|
user_id: query.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
||||||
|
params.date_from = dateRange.value[0]
|
||||||
|
params.date_to = dateRange.value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getVideoParseUnlocks(buildParams())
|
||||||
|
const payload = res.data || {}
|
||||||
|
list.value = payload.list || []
|
||||||
|
total.value = payload.total || 0
|
||||||
|
query.page = payload.page || query.page
|
||||||
|
query.page_size = payload.page_size || query.page_size
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
query.mini_program_id = undefined
|
||||||
|
query.user_id = undefined
|
||||||
|
dateRange.value = []
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = () => {
|
||||||
|
query.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadMiniProgramOptions(), loadData()])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user