align(店铺数据记录): 改回每条任务一张卡片(店铺名头+任务号/状态+分组/最新/国家/文件行+下载文件/删除)、全选口径 file_ready&&有resultId 不再附加终态、空态文案对齐(对齐 admin.js renderShopDataRecordTable/syncShopDataSelectionUi)
This commit is contained in:
@@ -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 syncShopDataSelectionUi:file_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">
|
||||
<el-checkbox
|
||||
:model-value="rowSelected(row)"
|
||||
:disabled="!(row.fileReady && isTerminalStatus(row.status))"
|
||||
@change="toggleRow(row)"
|
||||
/>
|
||||
<div class="file-main">
|
||||
<div class="file-top">
|
||||
<span class="file-task">任务 {{ row.taskNo || row.resultId }}</span>
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
<span v-for="code in row.countryCodes" :key="code" class="country-chip">{{ asinCountryLabel(code) }}</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>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<el-button
|
||||
text
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="busyKey === row.resultId"
|
||||
:disabled="!row.fileReady"
|
||||
@click="downloadRow(row)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isTerminalStatus(row.status)"
|
||||
text
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="deletingKey === row.resultId"
|
||||
@click="removeRow(group, row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!group.results.length" class="file-empty">暂无结果文件</div>
|
||||
<div class="shop-card-select-row">
|
||||
<el-checkbox
|
||||
:model-value="rowSelected(row)"
|
||||
:disabled="!isUsableShopDataResult(row)"
|
||||
@change="toggleRow(row)"
|
||||
/>
|
||||
<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>
|
||||
</section>
|
||||
<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="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 class="shop-card-actions">
|
||||
<el-button
|
||||
text
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="busyKey === row.resultId"
|
||||
:disabled="!isUsableShopDataResult(row)"
|
||||
@click="downloadRow(row)"
|
||||
>
|
||||
下载文件
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isTerminalStatus(row.status) && !!row.resultId"
|
||||
text
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="deletingKey === row.resultId"
|
||||
@click="removeRowByRow(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</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.js:file_ready && resultId>0;Vue 中 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,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, /暂无符合条件的店铺数据任务/, '空态文案')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user