feat(selfop): show customer names and filter sales and turnover
This commit is contained in:
@@ -16,6 +16,7 @@ import { analyticsApi } from '../api/analytics'
|
||||
import type { BusinessQuery, DailyResponse, InventoryAttention, InventoryMetric, InventoryQuery, InventorySort, Metric, Overview, QueryContext, RangePreset, SalesSummary } from '../types'
|
||||
import { RequestFence, RuntimePoller, contextParams, hasActiveTask, label, beijingTime, reasonsText, quantityText, sameContext, shownQuantity, validateCustomRange } from '../state'
|
||||
import SkuTrend from './SkuTrend.vue'
|
||||
import { useOwnerDirectory } from '../useOwnerDirectory'
|
||||
import { expired, inventoryExpired, parseDashboardDrilldown, previousDate, type DashboardDrilldown } from '../dashboardNavigation'
|
||||
import { overviewRange, overviewRangeOptions } from '../overviewRange'
|
||||
import { inventorySortColumns, inventorySortOptions } from '../inventorySorting'
|
||||
@@ -37,9 +38,10 @@ function readDrilldown() {
|
||||
}
|
||||
readDrilldown()
|
||||
function beijingToday() { return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()) }
|
||||
function initialFilters(): QueryModel { return { warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', sku: typeof route.query.sku === 'string' ? route.query.sku : '', analysis: drilldown.value?.analysis ?? 'summary', rangePreset: drilldown.value?.rangePreset ?? 'LAST_30_DAYS', windowDays: 30, salesWindowDays: 30, salesWindowEndDate: previousDate(drilldown.value?.context.anchorDate ?? beijingToday()), attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC', threshold: '', customRange: drilldown.value?.startDate && drilldown.value?.endDate ? [drilldown.value.startDate, drilldown.value.endDate] : [] } }
|
||||
function initialFilters(): QueryModel { return { customerCode: drilldown.value?.customerCode ?? '', warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', sku: typeof route.query.sku === 'string' ? route.query.sku : '', analysis: drilldown.value?.analysis ?? 'summary', rangePreset: drilldown.value?.rangePreset ?? 'LAST_30_DAYS', windowDays: 30, salesWindowDays: 30, salesWindowEndDate: previousDate(drilldown.value?.context.anchorDate ?? beijingToday()), attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC', threshold: '', customRange: drilldown.value?.startDate && drilldown.value?.endDate ? [drilldown.value.startDate, drilldown.value.endDate] : [] } }
|
||||
const ownerDirectory = useOwnerDirectory()
|
||||
const filters = ref<QueryModel>(initialFilters())
|
||||
const applied = ref({ sku: String(filters.value.sku) })
|
||||
const applied = ref({ sku: String(filters.value.sku), customerCode: String(filters.value.customerCode || '') })
|
||||
const appliedWindow = ref({ salesWindowDays: 30 as number, salesWindowEndDate: String(filters.value.salesWindowEndDate) })
|
||||
const displayedWindowDays = computed(() => context.value?.calculationWindowDays ?? appliedWindow.value.salesWindowDays)
|
||||
const overview = ref<Overview | null>(null)
|
||||
@@ -94,6 +96,7 @@ watch(warehouseCode, code => { if (code) { selectedWarehouse = code; emit('wareh
|
||||
const fields = computed<QueryField[]>(() => {
|
||||
const result: QueryField[] = [
|
||||
{ key: 'warehouseCode', type: 'select', label: '仓库', width: 210, placeholder: '请选择仓库(必选)', props: { clearable: false }, options: [...new Map((overview.value?.warehouses ?? []).map(row => [row.warehouseCode, { value: row.warehouseCode, label: row.warehouseName || row.warehouseCode }])).values()] },
|
||||
{ key: 'customerCode', label: '客户', type: 'select', width: 210, options: ownerDirectory.options.value, placeholder: '名称或编码搜索', props: { filterable: true, clearable: true, disabled: !!drilldown.value } },
|
||||
{ key: 'sku', type: 'input', label: 'SKU', width: 230, placeholder: '输入SKU编码(精确查询)' },
|
||||
]
|
||||
if (props.mode === 'inventory') result.push(
|
||||
@@ -185,7 +188,7 @@ function asMetric(row: unknown) { return row as Metric }
|
||||
function asInventory(row: unknown) { return row as InventoryMetric }
|
||||
function query(): InventoryQuery {
|
||||
const result: InventoryQuery = { sourceAccount, warehouseCode: warehouseCode.value, sku: applied.value.sku || undefined, current: current.value, size: size.value, ...contextParams(context.value) }
|
||||
if (drilldown.value?.customerCode) result.customerCode = drilldown.value.customerCode
|
||||
if (drilldown.value?.customerCode || applied.value.customerCode) result.customerCode = drilldown.value?.customerCode || applied.value.customerCode
|
||||
if (summaryMode.value) {
|
||||
Object.assign(result, appliedSalesRange.value, { salesSort: salesSort.value })
|
||||
} else if (props.mode === 'sales' && tab.value === 'hot') {
|
||||
@@ -221,6 +224,7 @@ async function load(reset = false, keepContext = false) {
|
||||
if (context.value && !sameContext(context.value, result.context)) throw new Error('查询版本已变化,请重新查询')
|
||||
context.value = result.context
|
||||
rows.value = result.records
|
||||
void ownerDirectory.load(result.context, warehouseCode.value, request.signal)
|
||||
total.value = result.total
|
||||
daily.value = null
|
||||
if (props.mode === 'inventory') displayedInventorySort.value = params.sortBy ?? 'AVAILABLE_DESC'
|
||||
@@ -289,7 +293,7 @@ function search() {
|
||||
const keepContext = !newData.value && !newDay.value && props.mode === 'inventory' && nextWindow.salesWindowDays === appliedWindow.value.salesWindowDays && nextWindow.salesWindowEndDate === appliedWindow.value.salesWindowEndDate && sortBy.value !== appliedInventory.value.sortBy
|
||||
appliedWindow.value = nextWindow
|
||||
appliedInventory.value = { attention: attention.value, sortBy: sortBy.value, coverageDaysThreshold: rawThreshold ? Number(rawThreshold) : undefined }
|
||||
applied.value = { sku: String(filters.value.sku ?? '').trim() }
|
||||
applied.value = { sku: String(filters.value.sku ?? '').trim(), customerCode: String(filters.value.customerCode || '') }
|
||||
current.value = 1
|
||||
void load(true, keepContext)
|
||||
}
|
||||
@@ -297,8 +301,8 @@ async function reset() {
|
||||
salesSort.value = 'QUANTITY_DESC'
|
||||
applyingRoute = true
|
||||
drilldown.value = null; origin.value = null
|
||||
filters.value = { ...initialFilters(), warehouseCode: warehouseCode.value || selectedWarehouse || defaultWarehouse(), sku: '' }
|
||||
appliedSalesRange.value = draftSalesRange(); appliedWindow.value = { salesWindowDays: 30, salesWindowEndDate: String(filters.value.salesWindowEndDate) }; applied.value = { sku: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' }
|
||||
filters.value = { ...initialFilters(), warehouseCode: warehouseCode.value || selectedWarehouse || defaultWarehouse(), sku: '', customerCode: '' }
|
||||
appliedSalesRange.value = draftSalesRange(); appliedWindow.value = { salesWindowDays: 30, salesWindowEndDate: String(filters.value.salesWindowEndDate) }; applied.value = { sku: '', customerCode: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' }
|
||||
await nextTick(); applyingRoute = false; void load(true)
|
||||
}
|
||||
function returnOverview() {
|
||||
@@ -307,9 +311,9 @@ function returnOverview() {
|
||||
}
|
||||
async function clearDrilldown() {
|
||||
drilldown.value = null
|
||||
filters.value = { ...filters.value, sku: '', analysis: 'summary', rangePreset: 'LAST_30_DAYS', windowDays: 30, attention: 'ALL', sortBy: 'AVAILABLE_DESC', threshold: '', customRange: [] }
|
||||
filters.value = { ...filters.value, sku: '', customerCode: '', analysis: 'summary', rangePreset: 'LAST_30_DAYS', windowDays: 30, attention: 'ALL', sortBy: 'AVAILABLE_DESC', threshold: '', customRange: [] }
|
||||
appliedSalesRange.value = draftSalesRange()
|
||||
applied.value = { sku: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' }; current.value = 1
|
||||
applied.value = { sku: '', customerCode: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' }; current.value = 1
|
||||
await nextTick(); void load()
|
||||
}
|
||||
function link(path: string) { void router.push({ path, query: { warehouseCode: warehouseCode.value, sku: applied.value.sku } }) }
|
||||
@@ -329,7 +333,7 @@ onMounted(() => { visibility(); poller.start(); document.addEventListener('visib
|
||||
watch(() => route.query, async () => {
|
||||
if (route.path && route.path !== `/self-operated/${props.mode}`) return
|
||||
applyingRoute = true; dataFence.cancel(); dailyFence.cancel(); trendOpen.value = false; rows.value = []; total.value = 0
|
||||
readDrilldown(); context.value = drilldown.value?.context ?? null; filters.value = initialFilters(); appliedSalesRange.value = draftSalesRange(); appliedWindow.value = { salesWindowDays: 30, salesWindowEndDate: String(filters.value.salesWindowEndDate) }; applied.value = { sku: String(filters.value.sku ?? '') }; appliedInventory.value = { attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC' }; current.value = 1
|
||||
readDrilldown(); context.value = drilldown.value?.context ?? null; filters.value = initialFilters(); appliedSalesRange.value = draftSalesRange(); appliedWindow.value = { salesWindowDays: 30, salesWindowEndDate: String(filters.value.salesWindowEndDate) }; applied.value = { sku: String(filters.value.sku ?? ''), customerCode: String(filters.value.customerCode || '') }; appliedInventory.value = { attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC' }; current.value = 1
|
||||
if (!warehouseCode.value && !routeProblem.value) filters.value.warehouseCode = defaultWarehouse()
|
||||
await nextTick(); applyingRoute = false; void load()
|
||||
})
|
||||
@@ -343,7 +347,7 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<ListPageLayout v-else>
|
||||
<DataTable appearance="integrated" v-model:page="current" v-model:page-size="size" :data="rows" :columns="columns" :loading="loading" :total="total" :empty-text="!warehouseCode ? '请选择仓库后查询SKU' : error ? '查询失败,请重试' : '暂无符合条件的SKU数据'">
|
||||
<template v-if="$slots['view-tabs']" #toolbar-start><slot name="view-tabs" /></template>
|
||||
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="自营SKU筛选" :preference-key="`self-operated-${mode}-query`" :loading="loading" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||||
<template #filters><p v-if="ownerDirectory.error.value" role="alert">{{ ownerDirectory.error.value }}</p><QueryToolbar v-model="filters" :fields="fields" ariaLabel="自营SKU筛选" :preference-key="`self-operated-${mode}-query`" :loading="loading" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||||
<template #notices>
|
||||
<p v-if="mode === 'inventory' && context" class="analytics-note" data-testid="inventory-window-summary">销量统计(北京时间):{{ context.rangeStartDate ?? '—' }} 至 {{ context.rangeEndDateExclusive ? previousDate(context.rangeEndDateExclusive) : '—' }},共 {{ displayedWindowDays }} 天。{{ origin ? '沿用经营总览所选区间;未完整的日期不推算日均。' : '不含今天;今天的出库不计入日均及可售天数。' }}库存使用最新已查询的采集值。</p>
|
||||
<p v-if="routeProblem" role="alert">{{ routeProblem }};已停止查询,不能按全仓条件继续。<el-button @click="returnOverview">返回经营总览</el-button></p>
|
||||
|
||||
Reference in New Issue
Block a user