align(店铺数据记录): 改回每条任务一张卡片(店铺名头+任务号/状态+分组/最新/国家/文件行+下载文件/删除)、全选口径 file_ready&&有resultId 不再附加终态、空态文案对齐(对齐 admin.js renderShopDataRecordTable/syncShopDataSelectionUi)

This commit is contained in:
2026-09-05 23:40:24 +08:00
parent 596933eea2
commit d30e8f3e8f
4 changed files with 113 additions and 73 deletions
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { formatDateTime } from '@/utils/datetime'
/** 店铺数据任务/记录页:每店铺卡片网格(分组/任务状态/站点/文件 下载删除)+ 筛选 + 批量下载 + 数据范围授权。 */
/** 店铺数据任务/记录页:每条任务一张卡片(店铺名/任务号/状态/分组/最新/国家/文件 + 下载文件/删除)
* + 筛选 + 批量下载 + 数据范围授权(对齐 admin.js renderShopDataRecordTable)。 */
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { asinCountryLabel } from '../asin/asin-country.ts'
import { createShopDataFilter, type ShopDataFilter } from './shop-data-filter.ts'
import type { ShopDataResultRow, ShopDataTaskGroup } from './shop-data-model.ts'
import { isUsableShopDataResult } from './shop-data-model.ts'
import {
deleteShopDataResultHistory,
fetchShopDataResultDownload,
@@ -94,8 +96,9 @@ function resultFileCount(): number {
return groups.value.reduce((sum, group) => sum + group.results.length, 0)
}
/** 可勾选行(对齐 admin.js syncShopDataSelectionUifile_ready && resultId>0,不附加终态约束)。 */
function selectableRows(): ShopDataResultRow[] {
return allResultRows.value.filter((row) => row.fileReady && isTerminalStatus(row.status))
return allResultRows.value.filter((row) => isUsableShopDataResult(row))
}
function toggleRow(row: ShopDataResultRow) {
@@ -150,9 +153,9 @@ async function downloadBatch() {
}
}
async function removeRow(group: ShopDataTaskGroup, row: ShopDataResultRow) {
async function removeRowByRow(row: ShopDataResultRow) {
try {
await ElMessageBox.confirm(`确认删除店铺“${group.shopName}”的任务 ${row.taskNo || row.resultId} 及结果文件`, '删除前确认', {
await ElMessageBox.confirm(`确认删除店铺“${row.shopName}”的任务 ${row.taskNo || row.resultId} 及结果文件?`, '删除前确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
@@ -275,63 +278,65 @@ onMounted(load)
/>
</div>
<div v-if="groups.length" class="shop-card-grid">
<section v-for="group in groups" :key="group.shopId || group.shopName" class="shop-card">
<div v-if="allResultRows.length" class="shop-card-grid">
<article v-for="row in allResultRows" :key="row.resultId || `${row.taskNo}-${row.filename}`" class="shop-card">
<header class="shop-card-head">
<span class="shop-name">{{ group.shopName }}</span>
<el-tag v-if="group.groupName" size="small" effect="plain">{{ group.groupName }}</el-tag>
<span v-if="group.latestCreatedAt" class="shop-latest">{{ formatDateTime(group.latestCreatedAt) }}</span>
<span class="shop-name" :title="row.shopName || '-'">{{ row.shopName || '-' }}</span>
</header>
<div class="file-list">
<div v-for="row in group.results" :key="row.resultId" class="file-row">
<div class="shop-card-select-row">
<el-checkbox
:model-value="rowSelected(row)"
:disabled="!(row.fileReady && isTerminalStatus(row.status))"
:disabled="!isUsableShopDataResult(row)"
@change="toggleRow(row)"
/>
<div class="file-main">
<div class="file-top">
<span class="file-task">任务 {{ row.taskNo || row.resultId }}</span>
<span class="shop-card-task-no">任务 {{ row.taskNo || row.resultId }}</span>
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
</div>
<div class="shop-card-row">
<span class="shop-card-label">分组</span>
<span class="shop-card-value">{{ row.groupName || '-' }}</span>
</div>
<div class="shop-card-row">
<span class="shop-card-label">最新</span>
<span class="shop-card-value">{{ formatDateTime(row.updatedAt || row.createdAt) || '-' }}</span>
</div>
<div class="shop-card-row">
<span class="shop-card-label">国家</span>
<span class="shop-card-value">
<span v-for="code in row.countryCodes" :key="code" class="country-chip">{{ asinCountryLabel(code) }}</span>
<span v-if="!row.countryCodes.length" class="dim">-</span>
</span>
</div>
<div class="file-name" :title="row.filename">{{ row.filename || '未命名文件' }}</div>
<div class="file-meta">
<span>{{ row.username || '' }}</span>
<span v-if="row.rowCount != null">· {{ row.rowCount }} 行</span>
<span v-if="row.fileSize != null">· {{ row.fileSize }} 字节</span>
<span>· {{ formatDateTime(row.createdAt) }}</span>
<div class="shop-card-row">
<span class="shop-card-label">文件</span>
<span class="shop-card-value shop-card-file" :title="row.filename">{{ row.filename || '-' }}</span>
</div>
</div>
<div class="file-actions">
<div class="shop-card-actions">
<el-button
text
type="success"
size="small"
:loading="busyKey === row.resultId"
:disabled="!row.fileReady"
:disabled="!isUsableShopDataResult(row)"
@click="downloadRow(row)"
>
下载
下载文件
</el-button>
<el-button
v-if="isTerminalStatus(row.status)"
v-if="isTerminalStatus(row.status) && !!row.resultId"
text
type="danger"
size="small"
:loading="deletingKey === row.resultId"
@click="removeRow(group, row)"
@click="removeRowByRow(row)"
>
删除
</el-button>
</div>
</div>
<div v-if="!group.results.length" class="file-empty">暂无结果文件</div>
</div>
</section>
</article>
</div>
<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>
@@ -365,17 +370,13 @@ onMounted(load)
.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); }
.shop-name { font-weight: 600; color: var(--el-text-color-primary); }
.shop-latest { margin-left: auto; color: var(--el-text-color-secondary); font-size: 12.5px; }
.file-list { padding: 4px 16px; }
.file-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-bottom: 1px dashed var(--el-border-color-lighter); }
.file-row:last-child { border-bottom: 0; }
.file-main { flex: 1; min-width: 0; }
.file-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.file-task { font-weight: 600; font-size: 13px; color: var(--el-text-color-primary); }
.country-chip { padding: 1px 8px; border-radius: 999px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); font-size: 12px; }
.file-name { margin-top: 6px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--el-text-color-regular); font-size: 13px; }
.file-meta { margin-top: 4px; color: var(--el-text-color-secondary); font-size: 12.5px; }
.file-actions { display: flex; gap: 6px; }
.file-empty { padding: 14px 0; color: var(--el-text-color-secondary); font-size: 13px; }
.shop-card-select-row { display: flex; align-items: center; gap: 10px; padding: 10px 16px 0; }
.shop-card-task-no { font-weight: 600; font-size: 13px; color: var(--el-text-color-primary); }
.shop-card-row { display: flex; align-items: flex-start; gap: 10px; padding: 6px 16px; font-size: 13px; }
.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; }
.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>
@@ -135,3 +135,8 @@ export function parseShopDataTaskPage(payload: unknown): ShopDataTaskPageResult
if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize)
return out
}
/** 结果是否可下载/勾选(对齐 admin.jsfile_ready && resultId>0Vue 中 resultId 为品牌串,非空即有效)。 */
export function isUsableShopDataResult(row: Pick<ShopDataResultRow, 'fileReady' | 'resultId'>): boolean {
return row.fileReady === true && (row.resultId || '').length > 0
}
@@ -0,0 +1,32 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
/** 对齐 admin.js:1819-1860 renderShopDataRecordTable:每条任务一张卡片(店铺名头 + 任务号/状态 + 分组/最新/国家/文件行 + 下载文件/删除)。 */
test('align_shop_data_cards_per_task', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
// 每任务一张卡:以 resultRow 迭代渲染卡片,不再按店铺聚合。
assert.match(page, /v-for="row in allResultRows"/, '逐任务卡片渲染')
assert.doesNotMatch(page, /v-for="group in groups"/, '去掉按店铺聚合卡片')
// 卡片结构:店铺名头 + 任务号/状态 + 分组/最新/国家/文件 信息行 + 下载文件/删除。
assert.match(page, /任务 {{ row\.taskNo \|\| row\.resultId }}|任务 {{ row\.taskNo/, '卡片含任务号')
assert.match(page, />分组</, '卡片含分组行')
assert.match(page, />最新</, '卡片含最新行')
assert.match(page, />国家</, '卡片含国家行')
assert.match(page, />文件</, '卡片含文件行')
assert.match(page, /下载文件/, '按钮名对齐参考「下载文件」')
})
test('align_shop_data_cards_selection_semantics', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
// 参考 selectable = file_ready && resultId>0(不附加终态约束)。
assert.match(page, /fileReady/, '可选依赖 fileReady')
assert.doesNotMatch(page, /fileReady && isTerminalStatus/, '可选不再附加终态约束')
assert.match(page, /全选可下载/, '全选语义保留')
})
test('align_shop_data_cards_empty_text', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /暂无符合条件的店铺数据任务/, '空态文案对齐参考')
})
+7 -5
View File
@@ -7,10 +7,12 @@ import { isTerminalStatus } from '../src/pages/tasks/image-video-view.ts'
// module 13 task 266:店铺数据任务视图改“每店铺卡片”,单文件下载/删除真实端点。
test('test_task_266_view_normal_primary_path', () => {
// 视图已改为「每条任务一张卡」(对齐 admin.js renderShopDataRecordTable)。
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /shop-card-grid/, '需每店铺卡片网格')
assert.match(page, /group\.results/, '卡片内遍历该店结果文件')
assert.match(page, /暂无结果文件/, '店空态')
assert.match(page, /shop-card-grid/, '需任务卡片网格')
assert.match(page, /v-for="row in allResultRows"/, '逐任务渲染卡片')
assert.match(page, /shop-card-task-no/, '卡片含任务号')
assert.match(page, /暂无符合条件的店铺数据任务/, '空态文案')
})
test('test_task_266_view_normal_variant_input', () => {
@@ -34,7 +36,7 @@ test('test_task_266_view_boundary_empty_input', () => {
test('test_task_266_view_boundary_single_item', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /fileReady/, '未就绪禁下载')
assert.match(page, /isTerminalStatus/, '终态门控下载/删除')
assert.match(page, /isTerminalStatus/, '终态门控删除')
assert.equal(isTerminalStatus('SUCCESS'), true)
assert.equal(isTerminalStatus('RUNNING'), false)
})
@@ -54,5 +56,5 @@ test('test_task_266_delete_boundary_limit_or_missing_field', () => {
test('test_task_266_dependency_failure_returns_actionable_message', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.equal(/<el-table[^>]*:data="(rows|allResultRows)"/.test(page), false, '结果文件不再用整表渲染')
assert.match(page, /暂无店铺数据任务/, '空态文案')
assert.match(page, /暂无符合条件的店铺数据任务/, '空态文案')
})