feat(selfop): submit turnover exports to backend download center

This commit is contained in:
2026-10-15 14:59:44 +08:00
parent 68284ad2bf
commit 9a10fb6657
8 changed files with 58 additions and 128 deletions
@@ -24,8 +24,8 @@ import AnalyticsChart from './AnalyticsChart.vue'
import ChartDetailsDialog from './ChartDetailsDialog.vue'
import SkuTrend from './SkuTrend.vue'
import { useOwnerDirectory } from '../useOwnerDirectory'
import { collectTurnoverExport, turnoverCsv, turnoverPercent } from '../turnoverExport'
import { saveCsv } from '@/shared/downloadCsv'
import { turnoverPercent } from '../turnoverPresentation'
import { useExportTask } from '@/modules/downloads'
const route = useRoute(), router = useRouter(), access = useAccessPreviewStore()
const ownerDirectory = useOwnerDirectory()
@@ -41,22 +41,20 @@ const fence = new RequestFence(), runtimeFence = new RequestFence(), trendFence
const trendOpen = ref(false), trendLoading = ref(false), daily = ref<DailyResponse | null>(null), trendError = ref<ApiErrorDetails | null>(null)
const evidence = ref<TurnoverRow | null>(null), evidenceOpen = ref(false)
const applied = ref<TurnoverQuery | null>(null)
const exporting = ref(false), exportProgress = ref(0), exportFeedback = ref('')
let exportController: AbortController | undefined
const exporting = ref(false), exportFeedback = ref('')
const submitExport = useExportTask()
const canExport = computed(() => access.can('platform:self-operated-analytics:export'))
async function exportRows() {
if (exporting.value || loading.value || !applied.value || !data.value) return
const controller = new AbortController(); exportController = controller
const q = { ...applied.value, customerCodes: [...(applied.value.customerCodes ?? [])], snapshotContext: data.value.context.snapshotContext }
const exportColumns = columns.value.map(c => ({ prop: String(c.prop), label: String(c.label) }))
const total = Number(data.value.total)
exporting.value = true; exportProgress.value = 0; exportFeedback.value = ''
if (exporting.value || loading.value || !canExport.value || !applied.value || !data.value) return
const { current: _current, size: _size, includeOverview: _overview, ...query } = applied.value
const parameters = { ...query, customerCodes: [...(query.customerCodes ?? [])], snapshotContext: data.value.context.snapshotContext,
columns: columns.value.map(c => String(c.prop)), expectedTotal: Number(data.value.total) }
exporting.value = true; exportFeedback.value = ''
try {
const rows = await collectTurnoverExport(q, total, analyticsApi.turnover, controller.signal, count => { exportProgress.value = count })
controller.signal.throwIfAborted()
saveCsv(new Blob([turnoverCsv(rows, exportColumns, q)], { type: 'text/csv;charset=utf-8' }), `库存周转_${q.warehouseCode}_${q.groupBy}_${q.endDate}.csv`)
exportFeedback.value = `已导出 ${rows.length} 条记录`
} catch (cause) { exportFeedback.value = controller.signal.aborted ? '导出已取消' : getApiErrorDetails(cause, '导出失败,请重试').message }
finally { exporting.value = false; exportController = undefined }
await submitExport('SELFOP_INVENTORY_TURNOVER', `库存周转_${query.warehouseCode}_${query.endDate}`, parameters)
exportFeedback.value = '导出任务已提交,请到下载中心查看进度和下载 Excel'
} catch (cause) { exportFeedback.value = getApiErrorDetails(cause, '导出任务提交失败,请重试').message }
finally { exporting.value = false }
}
let linkedSnapshot: string | undefined
let ownerReturn: { query: TurnoverQuery; result: TurnoverResponse; filters: QueryModel } | null = null
@@ -166,7 +164,6 @@ function scopeKey(q: TurnoverQuery) { return [q.warehouseCode, q.endDate, q.base
function overviewKey(q: TurnoverQuery) { return [scopeKey(q), JSON.stringify(q.customerCodes ?? []), q.groupBy].join('|') }
async function load(refresh = false) {
if (!permitted.value || !active) return
exportController?.abort()
fence.cancel(); inputError.value = ''; evidenceOpen.value = false; evidence.value = null; trendOpen.value = false
let q: TurnoverQuery
try { q = query(refresh) } catch (cause) { loading.value = false; inputError.value = (cause as Error).message; return }
@@ -267,9 +264,9 @@ async function initialize() {
watch(() => route.query, () => { if (active && readRoute()) void load() })
watch(chartsOpen, value => { try { sessionStorage.setItem('selfop-turnover-charts', String(value)) } catch { /* optional preference */ } })
onMounted(() => { initialized = true; try { chartsOpen.value = sessionStorage.getItem('selfop-turnover-charts') !== 'false' } catch { /* optional preference */ } void initialize() })
onDeactivated(() => { active = false; exportController?.abort(); fence.cancel(); runtimeFence.cancel(); trendFence.cancel(); loading.value = false })
onDeactivated(() => { active = false; fence.cancel(); runtimeFence.cancel(); trendFence.cancel(); loading.value = false })
onActivated(() => { const resume = initialized && !active; active = true; if (resume) void initialize() })
onUnmounted(() => { exportController?.abort(); fence.cancel(); runtimeFence.cancel(); trendFence.cancel() })
onUnmounted(() => { fence.cancel(); runtimeFence.cancel(); trendFence.cancel() })
</script>
<template>
<AdminPage class="admin-density turnover-page">
@@ -297,7 +294,7 @@ onUnmounted(() => { exportController?.abort(); fence.cancel(); runtimeFence.canc
</span>
</template>
<template #toolbar-start><slot name="view-tabs"><h2 class="turnover-title">库存周转<span>销量 · 库存 · 周转效率</span></h2></slot></template>
<template #toolbar-actions><el-button :disabled="loading || !data || exporting" title="导出当前查询的全部记录与当前显示列(CSV)" @click="exportRows">{{ exporting ? `导出中 ${exportProgress} 条` : '导出' }}</el-button><el-button v-if="exporting" @click="exportController?.abort()">取消导出</el-button><el-button @click="router.push('/self-operated/collection')">数据采集</el-button><el-button @click="page = 1; load(true)">整体刷新</el-button><el-button @click="expanded = !expanded">{{ expanded ? '收起扩展列' : '扩展列' }}</el-button><el-popover title="统计说明" trigger="click" placement="bottom-end" :width="460" teleported><template #reference><el-button aria-label="库存周转统计说明" title="统计说明"><Info :size="16" /></el-button></template><div class="turnover-rules"><p>7/15/30天均为截至所选日期的完整滚动窗口。日均仅使用完整窗口;预计可售天数使用查询时固定的当前正品可售库存,不是库龄。</p><p>期间件数周转次数=同期完整出库÷流水还原并经库存锚点核对的每日末实物库存均值。缺日、缺商品或单位异常不补零;当前库存不代替历史平均库存。件数不是占用空间,快慢标签仅供人工复核。</p><p>货主视图按“含此类SKU的货主”筛选;未知数量保留已知小计,不平均SKU可售天数。库存过期不抹去有完整证据的历史销量与周转。</p>
<template #toolbar-actions><el-button v-if="canExport" :disabled="loading || !data || exporting" title="后台导出全部匹配记录与当前显示列,在下载中心下载 Excel" @click="exportRows">{{ exporting ? '提交中…' : '导出' }}</el-button><el-button @click="router.push('/self-operated/collection')">数据采集</el-button><el-button @click="page = 1; load(true)">整体刷新</el-button><el-button @click="expanded = !expanded">{{ expanded ? '收起扩展列' : '扩展列' }}</el-button><el-popover title="统计说明" trigger="click" placement="bottom-end" :width="460" teleported><template #reference><el-button aria-label="库存周转统计说明" title="统计说明"><Info :size="16" /></el-button></template><div class="turnover-rules"><p>7/15/30天均为截至所选日期的完整滚动窗口。日均仅使用完整窗口;预计可售天数使用查询时固定的当前正品可售库存,不是库龄。</p><p>期间件数周转次数=同期完整出库÷流水还原并经库存锚点核对的每日末实物库存均值。缺日、缺商品或单位异常不补零;当前库存不代替历史平均库存。件数不是占用空间,快慢标签仅供人工复核。</p><p>货主视图按“含此类SKU的货主”筛选;未知数量保留已知小计,不平均SKU可售天数。库存过期不抹去有完整证据的历史销量与周转。</p>
<p>历史库存记录用于计算期间平均库存和周转次数,与已采集的销量天数不同;记录不足不代表销量采集失败。</p>
<template v-if="data?.rows.length">
<label>查看本页历史库存记录