task-266/267(admin.html观感对齐): 店铺数据任务改每店铺卡片(单文件下载/删除/批量/合计)+筛选权限对齐

This commit is contained in:
2026-09-05 22:11:36 +08:00
parent af49cbda64
commit ad5c7c02f9
5 changed files with 368 additions and 83 deletions
@@ -1,37 +1,50 @@
<script setup lang="ts">
import { formatDateTime } from '@/utils/datetime'
/** 店铺数据任务/记录页:店铺分组展示抓取结果文件,支持筛选、单文件下载、批量下载入口与数据范围授权。 */
/** 店铺数据任务/记录页:店铺卡片网格(分组/任务状态/站点/文件 下载删除)+ 筛选 + 批量下载 + 数据范围授权。 */
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 } from './shop-data-model.ts'
import { fetchShopDataTaskList, requestShopDataZipDownload, fetchShopDataTaskPermissionUsers, saveShopDataTaskPermissions } from './shop-data-api.ts'
import type { TaskStatus } from './task-model.ts'
import type { TaskPermissionItem } from './task-permission.ts'
import { grantedUserIds } from './task-permission.ts'
import type { ShopDataResultRow, ShopDataTaskGroup } from './shop-data-model.ts'
import {
deleteShopDataResultHistory,
fetchShopDataResultDownload,
fetchShopDataTaskList,
fetchShopDataTaskPermissionUsers,
requestShopDataZipDownload,
saveShopDataTaskPermissions,
} from './shop-data-api.ts'
import { isTerminalStatus } from './image-video-view.ts'
import { grantedUserIds, type TaskPermissionItem } from './task-permission.ts'
import {
addShopDataSelection,
countShopDataSelection,
createShopDataSelection,
toggleShopDataSelection,
} from './shop-data-selection.ts'
const loading = ref(false)
const rows = ref<ShopDataResultRow[]>([])
const total = ref(0)
const groups = ref<ShopDataTaskGroup[]>([])
const totalShops = ref(0)
const page = ref(1)
const pageSize = 20
const shopCount = ref(0)
const filter = reactive<ShopDataFilter & { dateRange: string[] }>({ ...createShopDataFilter(), dateRange: [] })
const selection = ref<Set<string>>(new Set())
const permissionVisible = ref(false)
const permissionItems = ref<TaskPermissionItem[]>([])
const permissionInitial = ref<number[]>([])
const permissionSaving = ref(false)
const downloadingKey = ref('')
function statusLabel(status: TaskStatus): string {
const busyKey = ref('')
const deletingKey = ref('')
function statusLabel(status: string): string {
const map: Record<string, string> = { PENDING: '排队中', RUNNING: '进行中', SUCCESS: '成功', FAILED: '失败', CANCELLED: '已取消', UNKNOWN: '未知' }
return map[status] || status
}
function statusType(status: TaskStatus): 'info' | 'warning' | 'success' | 'danger' {
function statusType(status: string): 'info' | 'warning' | 'success' | 'danger' {
if (status === 'SUCCESS') return 'success'
if (status === 'RUNNING') return 'warning'
if (status === 'FAILED') return 'danger'
@@ -52,10 +65,8 @@ async function load() {
loading.value = true
try {
const result = await fetchShopDataTaskList(toFilter(), page.value, pageSize)
const groups = result.items
shopCount.value = groups.length
rows.value = groups.flatMap((group) => group.results)
total.value = result.total
groups.value = result.items
totalShops.value = result.total
page.value = result.page
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '店铺数据任务加载失败')
@@ -66,14 +77,41 @@ async function load() {
function apply() {
page.value = 1
load()
void load()
}
function reset() {
Object.assign(filter, createShopDataFilter())
filter.dateRange = []
page.value = 1
load()
void load()
}
const allResultRows = computed(() => groups.value.flatMap((group) => group.results))
const selectionCount = computed(() => countShopDataSelection(selection.value))
function resultFileCount(): number {
return groups.value.reduce((sum, group) => sum + group.results.length, 0)
}
function selectableRows(): ShopDataResultRow[] {
return allResultRows.value.filter((row) => row.fileReady && isTerminalStatus(row.status))
}
function toggleRow(row: ShopDataResultRow) {
selection.value = toggleShopDataSelection(selection.value, row.resultId)
}
function toggleSelectAllReady() {
const rows = selectableRows()
const allIn = rows.length > 0 && rows.every((row) => selection.value.has(row.resultId))
selection.value = allIn
? createShopDataSelection()
: addShopDataSelection(new Set(selection.value), rows.map((row) => row.resultId))
}
function rowSelected(row: ShopDataResultRow): boolean {
return selection.value.has(row.resultId)
}
async function downloadRow(row: ShopDataResultRow) {
@@ -81,16 +119,56 @@ async function downloadRow(row: ShopDataResultRow) {
ElMessage.warning('文件未就绪,无法下载')
return
}
downloadingKey.value = row.resultId
busyKey.value = row.resultId
try {
const { blob, fileCount, errorCount } = await requestShopDataZipDownload([row.resultId])
saveBlob(blob, row.filename || `店铺数据_${row.shopName}.zip`)
const note = errorCount ? `${errorCount} 个失败` : ''
ElMessage.success(`已打包 ${fileCount ?? 1} 个文件${note}`)
const { blob, filename } = await fetchShopDataResultDownload(row.resultId)
saveBlob(blob, filename)
ElMessage.success('下载已开始')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '下载失败')
} finally {
downloadingKey.value = ''
busyKey.value = ''
}
}
async function downloadBatch() {
const resultIds = Array.from(selection.value)
if (!resultIds.length) {
ElMessage.warning('请先勾选要下载的结果文件')
return
}
busyKey.value = 'batch'
try {
const zip = await requestShopDataZipDownload(resultIds)
saveBlob(zip.blob, `店铺数据批量下载_${resultIds.length}.zip`)
const note = zip.errorCount ? `${zip.errorCount} 个失败(详见包内错误清单)` : ''
ElMessage.success(`已下载 ${zip.fileCount ?? resultIds.length} 个文件${note}`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '批量下载失败')
} finally {
busyKey.value = ''
}
}
async function removeRow(group: ShopDataTaskGroup, row: ShopDataResultRow) {
try {
await ElMessageBox.confirm(`确认删除店铺“${group.shopName}”的任务 ${row.taskNo || row.resultId} 及结果文件吗?`, '删除前确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
deletingKey.value = row.resultId
try {
await deleteShopDataResultHistory(row.resultId)
ElMessage.success('删除成功')
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : `正在删除任务 ${row.taskNo || row.resultId}...`)
} finally {
deletingKey.value = ''
}
}
@@ -106,7 +184,6 @@ function saveBlob(blob: Blob, filename: string) {
async function openPermission() {
try {
permissionItems.value = await fetchShopDataTaskPermissionUsers()
permissionInitial.value = grantedUserIds(permissionItems.value)
permissionVisible.value = true
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '权限用户加载失败')
@@ -122,8 +199,7 @@ async function savePermission() {
try {
const next = grantedUserIds(permissionItems.value)
await saveShopDataTaskPermissions(next)
permissionInitial.value = next
ElMessage.success('权限已保存')
ElMessage.success('保存成功')
permissionVisible.value = false
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
@@ -142,9 +218,19 @@ onMounted(load)
<div class="page-heading">
<div>
<h2>店铺数据任务/记录</h2>
<p>查看各店铺数据抓取结果文件下载结果配置数据范围授权</p>
<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>
@@ -152,12 +238,12 @@ onMounted(load)
<el-card shadow="never" style="margin-bottom: 14px">
<div class="filter-grid">
<div class="f-item">
<label>店铺</label>
<el-input v-model="filter.shopName" placeholder="店铺模糊" clearable @keyup.enter="apply" />
<label>店铺(模糊搜索)</label>
<el-input v-model="filter.shopName" placeholder="输入店铺关键字" clearable @keyup.enter="apply" />
</div>
<div class="f-item">
<label>分组</label>
<el-input v-model="filter.groupName" placeholder="分组模糊" clearable @keyup.enter="apply" />
<label>分组(模糊搜索)</label>
<el-input v-model="filter.groupName" placeholder="输入分组关键字" clearable @keyup.enter="apply" />
</div>
<div class="f-item">
<label>国家</label>
@@ -176,53 +262,82 @@ onMounted(load)
</div>
</el-card>
<el-card shadow="never">
<el-table v-loading="loading" :data="rows" stripe border>
<el-table-column label="店铺" min-width="140">
<template #default="{ row }">
{{ (row as ShopDataResultRow).shopName }}
<el-tag v-if="(row as ShopDataResultRow).groupName" size="small" effect="plain" style="margin-left: 4px">{{ (row as ShopDataResultRow).groupName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="username" label="用户" width="120" />
<el-table-column prop="taskNo" label="任务号" min-width="140" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusType((row as ShopDataResultRow).status)" size="small">{{ statusLabel((row as ShopDataResultRow).status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="站点" width="130">
<template #default="{ row }">
<span v-for="code in (row as ShopDataResultRow).countryCodes" :key="code" class="chip-gap">{{ asinCountryLabel(code) }}</span>
</template>
</el-table-column>
<el-table-column prop="filename" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column label="行数" width="90" align="center">
<template #default="{ row }">{{ (row as ShopDataResultRow).rowCount ?? '—' }}</template>
</el-table-column>
<el-table-column label="创建时间" min-width="160">
<template #default="{ row }">{{ formatDateTime((row as ShopDataResultRow).createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button text type="success" size="small" :loading="downloadingKey === (row as ShopDataResultRow).resultId" :disabled="!(row as ShopDataResultRow).fileReady" @click="downloadRow(row as ShopDataResultRow)">下载</el-button>
<el-button v-if="(row as ShopDataResultRow).fileUrl" text type="primary" size="small">
<el-link type="primary" :href="(row as ShopDataResultRow).fileUrl" target="_blank">查看</el-link>
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<span> <b>{{ rows.length }}</b> 个结果文件本页 {{ shopCount }} 家店铺 · 总计 {{ total.toLocaleString() }} 记录</span>
<el-pagination background layout="prev, pager, next, jumper" :total="total" :page-size="pageSize" :current-page="page" @current-change="(p: number) => { page = p; load() }" />
<div v-loading="loading" class="shop-summary-row">
<span>共 {{ totalShops }} 家店铺 · 本页 {{ resultFileCount() }} 个结果文件</span>
<el-pagination
background
layout="prev, pager, next, jumper"
small
:total="totalShops"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p; selection = new Set(); load() }"
/>
</div>
</el-card>
<div v-if="groups.length" class="shop-card-grid">
<section v-for="group in groups" :key="group.shopId || group.shopName" 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>
</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>
</section>
</div>
<el-empty v-else-if="!loading" description="暂无店铺数据任务" />
<el-dialog v-model="permissionVisible" title="店铺数据任务数据范围授权" width="620px">
<p class="dim">勾选允许查看/操作店铺数据任务的用户。当前已授权 {{ grantedCount }} 人。</p>
<p class="dim">勾选允许查看/操作店铺数据任务的用户(仅数据范围)。当前已授权 {{ grantedCount }} 人。</p>
<el-table :data="permissionItems" border size="small" max-height="420">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" min-width="160" />
<el-table-column prop="username" label="用户名" min-width="180" />
<el-table-column label="授权" width="90" align="center">
<template #default="{ row }">
<el-checkbox :model-value="(row as TaskPermissionItem).granted" @change="toggleGrant(row as TaskPermissionItem)" />
@@ -239,15 +354,28 @@ onMounted(load)
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: center; }
.actions { display: flex; align-items: center; gap: 10px; }
.actions { display: flex; align-items: center; gap: 12px; }
.filter-grid { display: flex; flex-wrap: wrap; gap: 12px 18px; }
.f-item { display: flex; flex-direction: column; gap: 6px; width: 170px; }
.f-item { display: flex; flex-direction: column; gap: 6px; width: 220px; }
.f-item label { color: var(--el-text-color-secondary); font-size: 12px; }
.f-item.wide { width: 340px; }
.f-item.btn-row { flex-direction: row; align-items: flex-end; gap: 8px; width: auto; }
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
.table-footer b { color: var(--el-text-color-primary); }
.shop-summary-row { display: flex; justify-content: space-between; align-items: center; color: var(--el-text-color-secondary); font-size: 13px; }
.shop-card-grid { display: flex; flex-direction: column; 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); }
.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; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.chip-gap { margin-right: 4px; }
</style>
@@ -2,12 +2,13 @@
import { http } from '@/api/http'
import { parseShopDataTaskPage, type ShopDataTaskPageResult } from './shop-data-model.ts'
import { toShopDataQuery, type ShopDataFilter } from './shop-data-filter.ts'
import { toShopDataZipRequest } from './shop-data-download.ts'
import { shopDataFilenameFromDisposition, toShopDataZipRequest } from './shop-data-download.ts'
import { downloadZipHeaderCounts } from './image-video-download.ts'
import { parseTaskPermissionPayload, toPermissionUserIdsPayload, type TaskPermissionItem } from './task-permission.ts'
export const SHOP_DATA_CRAWL_TASKS_ENDPOINT = '/api/admin/shop-data-crawl-tasks'
export const SHOP_DATA_TASK_PERMISSIONS_ENDPOINT = '/api/admin/shop-data-crawl-task-permissions'
export const SHOP_DATA_SINGLE_ENDPOINT = '/api/admin/shop-data-crawl'
export async function fetchShopDataTaskList(
filter: ShopDataFilter,
@@ -35,6 +36,25 @@ export async function requestShopDataZipDownload(resultIds: readonly (number | s
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
}
export interface ShopDataResultDownload {
blob: Blob
filename: string
}
/** 下载单个结果文件(服务端原文件名):GET /api/admin/shop-data-crawl/results/{resultId}/download。 */
export async function fetchShopDataResultDownload(resultId: number | string): Promise<ShopDataResultDownload> {
const { data, headers } = await http.get<Blob>(`${SHOP_DATA_SINGLE_ENDPOINT}/results/${resultId}/download`, {
responseType: 'blob',
})
const disposition = (headers as Record<string, string> | undefined)?.['content-disposition']
return { blob: data, filename: shopDataFilenameFromDisposition(disposition, `shop-data-task-${resultId}.xlsx`) }
}
/** 删除单条结果历史(仅终态):DELETE /api/admin/shop-data-crawl/history/{resultId}。 */
export async function deleteShopDataResultHistory(resultId: number | string): Promise<void> {
await http.delete<unknown>(`${SHOP_DATA_SINGLE_ENDPOINT}/history/${resultId}`)
}
/** 加载店铺数据任务数据范围授权用户:GET /api/admin/shop-data-crawl-task-permissions。 */
export async function fetchShopDataTaskPermissionUsers(): Promise<TaskPermissionItem[]> {
const { data } = await http.get<unknown>(SHOP_DATA_TASK_PERMISSIONS_ENDPOINT)
@@ -9,6 +9,22 @@ export interface ShopDataDownloadJob {
errorCount?: number
}
/** 从 Content-Disposition 解析单文件下载名(rfc5987 优先),失败回默认名。 */
export function shopDataFilenameFromDisposition(disposition: string | undefined, fallback: string): string {
const text = typeof disposition === 'string' ? disposition : ''
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(text)
if (encoded) {
try {
const decoded = decodeURIComponent(encoded[1])
if (decoded.trim()) return decoded.trim()
} catch {
// 回退普通 filename
}
}
const plain = /filename="?([^";]+)"?/i.exec(text)
return plain && plain[1] ? plain[1].trim() : fallback
}
function finitePositive(value: unknown): number | null {
const number = typeof value === 'number' && Number.isFinite(value)
? value
+58
View File
@@ -0,0 +1,58 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import { shopDataFilenameFromDisposition } from '../src/pages/tasks/shop-data-download.ts'
import { isTerminalStatus } from '../src/pages/tasks/image-video-view.ts'
// module 13 task 266:店铺数据任务视图改“每店铺卡片”,单文件下载/删除真实端点。
test('test_task_266_view_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /shop-card-grid/, '需每店铺卡片网格')
assert.match(page, /group\.results/, '卡片内遍历该店结果文件')
assert.match(page, /暂无结果文件/, '店空态')
})
test('test_task_266_view_normal_variant_input', () => {
const api = readSource('src/pages/tasks/shop-data-api.ts')
assert.match(api, /fetchShopDataResultDownload/, '单文件下载适配')
assert.match(api, /\/results\/\$\{resultId\}\/download/, '单文件下载走管理端真实端点')
assert.match(api, /deleteShopDataResultHistory/, '删除适配')
assert.match(api, /\/history\/\$\{resultId\}/, '删除走管理端真实端点')
})
test('test_task_266_view_normal_repeated_operation_is_idempotent', () => {
assert.equal(shopDataFilenameFromDisposition('attachment; filename="a.xlsx"', 'fallback.xlsx'), 'a.xlsx')
assert.equal(shopDataFilenameFromDisposition(undefined, 'fallback.xlsx'), 'fallback.xlsx')
})
test('test_task_266_view_boundary_empty_input', () => {
assert.equal(shopDataFilenameFromDisposition('', 'shop-data-task-9.xlsx'), 'shop-data-task-9.xlsx', '缺 header 回默认文件名')
assert.equal(shopDataFilenameFromDisposition('inline; filename="店铺.xlsx"', 'd.xlsx'), '店铺.xlsx')
})
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.equal(isTerminalStatus('SUCCESS'), true)
assert.equal(isTerminalStatus('RUNNING'), false)
})
test('test_task_266_delete_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /确认删除店铺/, '删除确认含店铺')
assert.match(page, /及结果文件/, '删除确认含结果文件')
assert.match(page, /删除成功/, '删除成功提示')
})
test('test_task_266_delete_boundary_limit_or_missing_field', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /正在删除任务/, '删除中有进行文案')
})
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, /暂无店铺数据任务/, '空态文案')
})
+63
View File
@@ -0,0 +1,63 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import { toShopDataQuery } from '../src/pages/tasks/shop-data-filter.ts'
import { toShopDataZipRequest } from '../src/pages/tasks/shop-data-download.ts'
// module 13 task 267:店铺数据任务筛选/批量下载/权限/合计对齐。
test('test_task_267_filter_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /店铺(模糊搜索)/, '需店铺模糊筛选')
assert.match(page, /分组(模糊搜索)/, '需分组模糊筛选')
assert.match(page, /国家/, '需国家筛选')
assert.match(page, /创建时间/, '需创建时间范围')
assert.match(page, /查询/, '需查询')
assert.match(page, /reset/, '需重置')
})
test('test_task_267_filter_normal_variant_input', () => {
const query = toShopDataQuery({ shopName: ' 店A ', groupName: ' 组1 ', country: 'DE', createdFrom: '', createdTo: '2026-09-05T09:30' }, 1, 20)
assert.equal(query.shop_name, '店A')
assert.equal(query.group_name, '组1')
assert.equal(query.country, 'DE')
assert.equal(query.created_to, '2026-09-05T09:30')
assert.equal(query.created_from, undefined, '空日期不下发')
})
test('test_task_267_batch_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /全选可下载/, '全选可下载文件')
assert.match(page, /批量下载/, '批量下载')
assert.match(page, /requestShopDataZipDownload/, '批量走打包端点')
})
test('test_task_267_batch_boundary_empty_input', () => {
assert.deepEqual(toShopDataZipRequest(['3', '3', 'abc', 4]), { result_ids: [3, 4] }, '过滤非法/去重')
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /请先勾选要下载的结果文件/, '未勾选提示')
})
test('test_task_267_batch_boundary_limit_or_missing_field', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /包内错误清单/, '部分失败提示包内错误清单')
})
test('test_task_267_total_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /家店铺/, '合计为店铺数')
assert.match(page, /结果文件/, '展示结果文件数')
})
test('test_task_267_permission_normal_primary_path', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /权限配置/, '权限入口')
assert.match(page, /fetchShopDataTaskPermissionUsers/, '权限加载')
assert.match(page, /saveShopDataTaskPermissions/, '权限保存')
})
test('test_task_267_dependency_failure_returns_actionable_message', () => {
const api = readSource('src/pages/tasks/shop-data-api.ts')
assert.match(api, /\/download-zip/)
assert.match(api, /\/api\/admin\/shop-data-crawl-task-permissions/)
})