task-284(收尾/在途批次): 页头按钮归位卡内toolbar+店铺数据记录改名+弹窗报错清除; Java page_size配对/按角色数据范围过滤/撞款导出XLSX/Excel图改WPS DISPIMG/V110查询索引

This commit is contained in:
2026-09-06 20:17:14 +08:00
parent 898fae4cb3
commit 26e062d32c
24 changed files with 269 additions and 153 deletions
@@ -80,7 +80,7 @@ async function create(): Promise<void> {
>
<el-form label-position="top" @submit.prevent>
<el-form-item label="用户名" :error="errors.username">
<el-input v-model="form.username" autocomplete="username" placeholder="用户名至少 2 个字符" />
<el-input v-model="form.username" autocomplete="username" placeholder="用户名至少 2 个字符" @input="errors.username = undefined" />
</el-form-item>
<el-form-item label="密码" :error="errors.password">
<el-input
@@ -89,6 +89,7 @@ async function create(): Promise<void> {
show-password
autocomplete="new-password"
placeholder="密码至少 6 个字符"
@input="errors.password = undefined"
/>
</el-form-item>
<el-form-item v-if="operatorSuper" label="角色">
@@ -87,6 +87,7 @@ async function save(): Promise<void> {
show-password
autocomplete="new-password"
placeholder="留空则不修改密码"
@input="errors.password = undefined"
/>
</el-form-item>
<el-form-item v-if="operatorSuper" label="角色">
@@ -299,10 +299,6 @@ onMounted(loadTree)
<h2>商品类目</h2>
<p>维护商品类目树与类目名称/说明有子类目节点不可删除</p>
</div>
<div class="actions">
<el-button type="primary" @click="openCreate(null)">新增类目</el-button>
<el-button @click="exportCategories">导出</el-button>
</div>
</div>
<el-card shadow="never" style="margin-bottom: 14px">
@@ -314,6 +310,14 @@ onMounted(loadTree)
</el-card>
<el-card v-if="!searching" shadow="never">
<div class="table-toolbar">
<div class="tb-buttons">
<el-button type="primary" @click="openCreate(null)">新增类目</el-button>
<el-button :disabled="!selected" @click="openCreate(selected)">新增子类目</el-button>
<el-button @click="exportCategories">导出</el-button>
</div>
<span class="dim">{{ selected ? `已选:${selected.path || selected.name}` : '未选中节点' }}</span>
</div>
<el-table v-loading="loading" :data="treeRows" row-key="node.id" :class="{ 'tree-table': true }">
<el-table-column label="类目名称" min-width="320">
<template #default="{ row }">
@@ -379,18 +383,15 @@ onMounted(loadTree)
</template>
</el-table-column>
</el-table>
<div class="tree-actions">
<span class="dim">{{ selected ? `已选:${selected.path || selected.name}` : '未选中节点' }}</span>
<span class="btn-group">
<el-button size="small" :disabled="!selected" @click="openCreate(selected)">新增子类目</el-button>
</span>
</div>
</el-card>
<el-card v-else shadow="never">
<div class="search-head">
<span>搜索结果 {{ searchTotal.toLocaleString() }} </span>
<span class="dim">{{ keyword }}</span>
<div class="table-toolbar">
<div class="tb-buttons">
<el-button type="primary" @click="openCreate(null)">新增类目</el-button>
<el-button @click="exportCategories">导出</el-button>
</div>
<span class="dim">搜索结果 {{ searchTotal.toLocaleString() }} {{ keyword ? ` · ${keyword}` : '' }}</span>
</div>
<el-table v-loading="searchLoading" :data="searchRows" stripe border>
<el-table-column prop="name" label="类目名称" min-width="180" />
@@ -449,13 +450,10 @@ onMounted(loadTree)
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: center; }
.actions { display: flex; align-items: center; gap: 10px; }
.table-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.tb-buttons { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.filter-row { display: flex; gap: 10px; align-items: center; }
.tree-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 14px; border-top: 1px solid var(--el-border-color-lighter); padding-top: 12px; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.btn-group { display: inline-flex; gap: 6px; }
.search-head { display: flex; justify-content: space-between; margin-bottom: 12px; }
.load-more-row { text-align: center; margin-top: 12px; }
/* 树形表格(对齐旧版 panel-product-categories):缩进 + [+/] 折叠标记 + 粗体名称。 */
.tree-name-cell { display: flex; align-items: center; gap: 8px; min-height: 24px; }
.tree-indent { flex: 0 0 auto; width: var(--indent, 0px); }
@@ -25,7 +25,7 @@ export async function searchProductCategories(
page: number,
pageSize: number,
): Promise<ProductCategorySearchPage> {
const params: Record<string, string | number> = { page, pageSize }
const params: Record<string, string | number> = { page, page_size: pageSize }
const trimmed = keyword.trim()
if (trimmed) params.keyword = trimmed
const { data } = await http.get<unknown>(`${PRODUCT_CATEGORY_ENDPOINT}/search`, { params })
@@ -182,12 +182,12 @@ onMounted(load)
<h2>数字人版本管理</h2>
<p>管理数字人客户端的草稿/已发布/已废弃版本</p>
</div>
<div class="actions">
<el-button type="primary" @click="uploadVisible = true">上传草稿状态</el-button>
</div>
</div>
<el-card shadow="never">
<div class="table-toolbar">
<el-button type="primary" @click="uploadVisible = true">上传草稿状态</el-button>
</div>
<el-table v-loading="loading" :data="pagedItems" stripe border empty-text="暂无数字人版本">
<el-table-column prop="version" label="版本号" min-width="150" />
<el-table-column label="状态" min-width="100">
@@ -271,7 +271,6 @@ onMounted(load)
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; }
.actions { display: flex; align-items: center; gap: 10px; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.star { color: #e6a23c; font-weight: 700; }
.upload-desc { margin: 0 0 12px; color: var(--el-text-color-secondary); font-size: 12.5px; line-height: 1.6; }
@@ -138,9 +138,6 @@ onMounted(() => {
<h2>生成记录</h2>
<p>查看各用户在后台生成的历史记录与结果图</p>
</div>
<div class="heading-actions">
<el-button type="danger" plain :loading="clearing" @click="clearAll">清理数据</el-button>
</div>
</div>
<el-card shadow="never" class="filter-card">
@@ -180,6 +177,9 @@ onMounted(() => {
</el-card>
<el-card shadow="never">
<div class="table-toolbar">
<el-button type="danger" plain :loading="clearing" @click="clearAll">清理数据</el-button>
</div>
<el-alert
v-if="errorText"
:title="errorText"
@@ -93,12 +93,12 @@ onMounted(load)
<h2>软件版本管理</h2>
<p>管理客户端更新包版本用于 /api/version/latest 下发</p>
</div>
<div class="actions">
<el-button type="primary" @click="openUpload">上传新版本</el-button>
</div>
</div>
<el-card shadow="never">
<div class="table-toolbar">
<el-button type="primary" @click="openUpload">上传新版本</el-button>
</div>
<el-table v-loading="loading" :data="pagedItems" stripe border empty-text="暂无版本记录">
<el-table-column prop="version" label="版本号" min-width="180" />
<el-table-column label="下载链接" min-width="240">
@@ -147,7 +147,6 @@ onMounted(load)
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; }
.actions { display: flex; align-items: center; gap: 10px; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.upload-desc { margin: 0 0 12px; color: var(--el-text-color-secondary); font-size: 12.5px; line-height: 1.6; }
.zip-hint { margin: 4px 0 0; color: var(--el-text-color-secondary); font-size: 12px; }
@@ -239,13 +239,6 @@ onMounted(load)
<h2>视频任务记录</h2>
<p>查看图生视频任务卡片在线预览与下载视频配置数据范围授权</p>
</div>
<div class="actions">
<el-checkbox :model-value="allSelected" @change="toggleSelectAll">全选当前页</el-checkbox>
<el-button :disabled="selectionCount === 0" :loading="downloading" @click="downloadBatch">
批量下载{{ selectionCount ? `(${selectionCount})` : '' }}
</el-button>
<el-button @click="openPermission">权限配置</el-button>
</div>
</div>
<el-card shadow="never" style="margin-bottom: 14px">
@@ -265,16 +258,15 @@ onMounted(load)
</div>
</el-card>
<div v-loading="loading" class="video-summary-row">
<span> {{ totalTasks }} 个任务 · 本页 {{ currentVideoCount }} 个视频</span>
<el-pagination
background
layout="prev, pager, next, jumper"
:total="totalTasks"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p; selection = new Set(); load() }"
/>
<div v-loading="loading" class="list-toolbar">
<div class="lt-buttons">
<el-checkbox :model-value="allSelected" @change="toggleSelectAll">全选当前页</el-checkbox>
<el-button :disabled="selectionCount === 0" :loading="downloading" @click="downloadBatch">
批量下载{{ selectionCount ? `(${selectionCount})` : '' }}
</el-button>
<el-button @click="openPermission">权限配置</el-button>
</div>
<span class="list-toolbar-summary"> {{ totalTasks }} 个任务 · 本页 {{ currentVideoCount }} 个视频</span>
</div>
<div v-if="tasks.length" class="task-card-grid">
@@ -324,6 +316,16 @@ onMounted(load)
</div>
<el-empty v-else-if="!loading" description="暂无视频任务记录" />
<div class="table-footer">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="totalTasks"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p; selection = new Set(); load() }"
/>
</div>
<el-drawer v-model="detailVisible" title="任务明细" size="720px">
<div v-loading="detailLoading">
@@ -380,6 +382,10 @@ onMounted(load)
.f-item.wide { width: 340px; }
.f-item.btn-row { flex-direction: row; align-items: flex-end; gap: 8px; width: auto; margin-left: auto; flex: none; }
.video-summary-row { display: flex; justify-content: space-between; align-items: center; color: var(--el-text-color-secondary); font-size: 13px; }
.list-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; color: var(--el-text-color-secondary); font-size: 13px; margin-bottom: 14px; }
.lt-buttons { display: inline-flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.list-toolbar-summary { white-space: nowrap; }
.table-footer { display: flex; justify-content: flex-end; margin-top: 14px; }
.task-card-grid { display: flex; flex-direction: column; gap: 14px; }
.task-card { border: 1px solid var(--el-border-color); border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgba(39, 67, 94, 0.04), 0 14px 30px -24px rgba(39, 67, 94, 0.28); overflow: hidden; }
.task-card-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--el-border-color-lighter); }
@@ -74,7 +74,7 @@ async function load() {
totalShops.value = result.total
page.value = result.page
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '店铺数据任务加载失败')
ElMessage.error(error instanceof Error ? error.message : '店铺数据记录加载失败')
} finally {
loading.value = false
}
@@ -230,22 +230,9 @@ onMounted(load)
<div class="page-stack">
<div class="page-heading">
<div>
<h2>店铺数据任务/记录</h2>
<h2>店铺数据记录</h2>
<p>按店铺查看数据抓取结果文件支持筛选下载删除与数据范围授权</p>
</div>
<div class="actions">
<el-checkbox
:model-value="selectableRows().length > 0 && selectableRows().every((row) => rowSelected(row))"
:indeterminate="selectionCount > 0 && selectionCount < selectableRows().length"
@change="toggleSelectAllReady"
>
全选可下载
</el-checkbox>
<el-button :disabled="selectionCount === 0" :loading="busyKey === 'batch'" @click="downloadBatch">
批量下载{{ selectionCount ? `(${selectionCount})` : '' }}
</el-button>
<el-button @click="openPermission">权限配置</el-button>
</div>
</div>
<el-card shadow="never" style="margin-bottom: 14px">
@@ -275,19 +262,25 @@ onMounted(load)
</div>
</el-card>
<div v-loading="loading" class="shop-summary-row">
<span>共 {{ totalShops }} 家店铺 · 本页 {{ resultFileCount() }} 个结果文件</span>
<el-pagination
background
layout="prev, pager, next, jumper"
:total="totalShops"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p; cardPage = 1; selection = new Set(); load() }"
/>
<div v-loading="loading" class="list-toolbar">
<div class="lt-buttons">
<el-checkbox
:model-value="selectableRows().length > 0 && selectableRows().every((row) => rowSelected(row))"
:indeterminate="selectionCount > 0 && selectionCount < selectableRows().length"
@change="toggleSelectAllReady"
>
全选可下载
</el-checkbox>
<el-button :disabled="selectionCount === 0" :loading="busyKey === 'batch'" @click="downloadBatch">
批量下载{{ selectionCount ? `(${selectionCount})` : '' }}
</el-button>
<el-button @click="openPermission">权限配置</el-button>
</div>
<span class="list-toolbar-summary">共 {{ totalShops }} 家店铺 · 本页 {{ resultFileCount() }} 个结果文件</span>
</div>
<div v-if="allResultRows.length" class="shop-card-grid">
<template v-if="allResultRows.length">
<div class="shop-card-grid">
<article v-for="row in pagedResultRows" :key="row.resultId || `${row.taskNo}-${row.filename}`" class="shop-card">
<header class="shop-card-head">
<span class="shop-name" :title="row.shopName || '-'">{{ row.shopName || '-' }}</span>
@@ -322,7 +315,6 @@ onMounted(load)
</div>
<div class="shop-card-actions">
<el-button
text
type="success"
size="small"
:loading="busyKey === row.resultId"
@@ -333,7 +325,6 @@ onMounted(load)
</el-button>
<el-button
v-if="isTerminalStatus(row.status) && !!row.resultId"
text
type="danger"
size="small"
:loading="deletingKey === row.resultId"
@@ -343,22 +334,33 @@ onMounted(load)
</el-button>
</div>
</article>
</div>
<div v-if="allResultRows.length > cardPageSize" class="table-footer">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="allResultRows.length"
:page-size="cardPageSize"
:current-page="cardPage"
@current-change="changeCardPage"
/>
</div>
</div>
<div class="table-footer">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="totalShops"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p; cardPage = 1; selection = new Set(); load() }"
/>
</div>
<div v-if="allResultRows.length > cardPageSize" class="table-footer">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="allResultRows.length"
:page-size="cardPageSize"
:current-page="cardPage"
@current-change="changeCardPage"
/>
</div>
</template>
<el-empty v-else-if="!loading" description="暂无符合条件的店铺数据任务" />
<el-empty v-else-if="!loading" description="暂无符合条件的店铺数据记录" />
<el-dialog v-model="permissionVisible" title="店铺数据任务数据范围授权" width="620px">
<p class="dim">勾选允许查看/操作店铺数据任务的用户(仅数据范围)。当前已授权 {{ grantedCount }} 人。</p>
<el-dialog v-model="permissionVisible" title="店铺数据记录数据范围授权" width="620px">
<p class="dim">勾选允许查看/操作店铺数据记录的用户(仅数据范围)。当前已授权 {{ grantedCount }} 人。</p>
<el-table :data="permissionItems" border size="small" max-height="420">
<el-table-column prop="id" label="ID" min-width="80" />
<el-table-column prop="username" label="用户名" min-width="180" />
@@ -385,6 +387,10 @@ onMounted(load)
.f-item.wide { width: 340px; }
.f-item.btn-row { flex-direction: row; align-items: flex-end; gap: 8px; width: auto; }
.shop-summary-row { display: flex; justify-content: space-between; align-items: center; color: var(--el-text-color-secondary); font-size: 13px; }
.list-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; color: var(--el-text-color-secondary); font-size: 13px; margin-bottom: 14px; }
.lt-buttons { display: inline-flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.list-toolbar-summary { white-space: nowrap; }
.table-footer { display: flex; justify-content: flex-end; margin-top: 14px; }
.shop-card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.shop-card { border: 1px solid var(--el-border-color); border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgba(39, 67, 94, 0.04), 0 14px 30px -24px rgba(39, 67, 94, 0.28); overflow: hidden; }
.shop-card-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--el-border-color-lighter); }
@@ -395,7 +401,7 @@ onMounted(load)
.shop-card-label { flex: none; width: 52px; color: var(--el-text-color-secondary); }
.shop-card-value { color: var(--el-text-color-regular); min-width: 0; }
.shop-card-file { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.shop-card-actions { display: flex; gap: 6px; padding: 4px 16px 12px; }
.shop-card-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 6px 16px 14px; }
.country-chip { padding: 1px 8px; margin-right: 4px; border-radius: 999px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); font-size: 12px; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
</style>
+1 -1
View File
@@ -24,7 +24,7 @@ export const adminPages: AdminPageDef[] = [
{ path: 'records/software-version', menuKey: 'admin_version', title: '软件版本管理', load: () => import('@/pages/records/RecordsSoftwareVersionPage.vue') },
{ path: 'records/digital-human-version', menuKey: 'digital_human_version', title: '数字人版本管理', load: () => import('@/pages/records/RecordsDigitalHumanVersionPage.vue') },
{ path: 'records/image-video-tasks', menuKey: 'admin_image_video_tasks', title: '视频任务记录', load: () => import('@/pages/tasks/ImageVideoTasksPage.vue') },
{ path: 'shop-center/data-tasks', menuKey: 'admin_shop_data_crawl_tasks', title: '店铺数据任务/记录', load: () => import('@/pages/tasks/ShopDataTasksPage.vue') },
{ path: 'shop-center/data-tasks', menuKey: 'admin_shop_data_crawl_tasks', title: '店铺数据记录', load: () => import('@/pages/tasks/ShopDataTasksPage.vue') },
{ path: 'asin-center/registry', menuKey: 'admin_dedupe_total_data', title: '数据去重总数据', load: () => import('@/pages/asin/DedupeRegistryPage.vue') },
{ path: 'asin-center/invalid', menuKey: 'admin_invalid_asin_data', title: '不符合ASIN数据', load: () => import('@/pages/asin/AsinInvalidPage.vue') },
{ path: 'asin-center/query', menuKey: 'admin_query_asin', title: '查询ASIN', load: () => import('@/pages/asin/QueryAsinPage.vue') },