fix(selfop): unify collection layout and show details in dialogs
This commit is contained in:
@@ -8,6 +8,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorMessage, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { analyticsApi } from '../api/analytics'
|
||||
@@ -27,6 +28,10 @@ const overview = ref<Overview | null>(null)
|
||||
const context = ref<QueryContext | null>(null)
|
||||
const rows = ref<(SalesSummary | Metric)[]>([])
|
||||
const daily = ref<DailyResponse | null>(null)
|
||||
const trendOpen = ref(false)
|
||||
const trendLoading = ref(false)
|
||||
const trendError = ref('')
|
||||
const trendErrorDetails = ref<ApiErrorDetails | null>(null)
|
||||
const attention = computed(() => (filters.value.attention || 'ALL') as InventoryAttention)
|
||||
const sortBy = computed(() => (filters.value.sortBy || 'AVAILABLE_DESC') as InventorySort)
|
||||
const thresholdInput = computed(() => String(filters.value.threshold ?? ''))
|
||||
@@ -109,6 +114,7 @@ async function load(reset = false) {
|
||||
if (problem) { error.value = problem; return }
|
||||
}
|
||||
if (reset) { context.value = null; daily.value = null; rows.value = []; total.value = 0; newData.value = false; current.value = 1 }
|
||||
trendOpen.value = false
|
||||
dailyFence.cancel()
|
||||
const request = dataFence.begin()
|
||||
loading.value = true
|
||||
@@ -129,8 +135,12 @@ async function load(reset = false) {
|
||||
finally { if (request.current()) loading.value = false }
|
||||
}
|
||||
async function showTrend(row: SalesSummary | Metric) {
|
||||
trendOpen.value = true
|
||||
trendLoading.value = true
|
||||
daily.value = null
|
||||
trendError.value = ''
|
||||
trendErrorDetails.value = null
|
||||
const request = dailyFence.begin()
|
||||
error.value = ''
|
||||
try {
|
||||
const params: BusinessQuery = { sourceAccount, warehouseCode: warehouseCode.value, customerCode: row.customerCode, sku: row.sku, current: 1, size: 20, ...contextParams(context.value), rangePreset: props.mode === 'inventory' ? 'CUSTOM' : preset.value }
|
||||
if (props.mode === 'inventory' && context.value?.rangeStartDate && context.value.rangeEndDateExclusive) {
|
||||
@@ -141,7 +151,8 @@ async function showTrend(row: SalesSummary | Metric) {
|
||||
if (!request.current()) return
|
||||
if (!context.value || !sameContext(context.value, result.context)) throw new Error('趋势版本与列表不一致,请重新查询')
|
||||
daily.value = result
|
||||
} catch (cause) { if (request.current()) error.value = getApiErrorMessage(cause, 'SKU趋势读取失败') }
|
||||
} catch (cause) { if (request.current()) { trendErrorDetails.value = getApiErrorDetails(cause, 'SKU趋势读取失败'); trendError.value = trendErrorDetails.value.message } }
|
||||
finally { if (request.current()) trendLoading.value = false }
|
||||
}
|
||||
async function readRuntime() {
|
||||
if (!permitted.value) return false
|
||||
@@ -159,6 +170,7 @@ async function readRuntime() {
|
||||
} catch (cause) { if (request.current()) runtimeError.value = getApiErrorMessage(cause, '状态更新失败,保留上次观测结果'); return false }
|
||||
}
|
||||
const poller = new RuntimePoller(readRuntime, () => runtimeFence.cancel())
|
||||
watch(trendOpen, open => { if (!open) { dailyFence.cancel(); daily.value = null; trendLoading.value = false; trendError.value = ''; trendErrorDetails.value = null } })
|
||||
function visibility() { poller.setVisible(!document.hidden) }
|
||||
function search() {
|
||||
const rawThreshold = thresholdInput.value.trim()
|
||||
@@ -173,7 +185,7 @@ function search() {
|
||||
}
|
||||
function reset() { filters.value = { ...initialFilters(), warehouseCode: '', sku: '' }; applied.value = { sku: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' } }
|
||||
function link(path: string) { void router.push({ path, query: { warehouseCode: warehouseCode.value, sku: applied.value.sku } }) }
|
||||
watch(warehouseCode, () => { dataFence.cancel(); dailyFence.cancel(); rows.value = []; total.value = 0; context.value = null; daily.value = null; error.value = ''; current.value = 1; void load(true) })
|
||||
watch(warehouseCode, () => { trendOpen.value = false; dataFence.cancel(); dailyFence.cancel(); rows.value = []; total.value = 0; context.value = null; daily.value = null; error.value = ''; current.value = 1; void load(true) })
|
||||
watch([preset, tab, windowDays], () => { current.value = 1; if (!summaryMode.value || preset.value !== 'CUSTOM' || customStart.value && customEnd.value) void load() })
|
||||
watch([current, size], () => { void load() })
|
||||
onMounted(() => { visibility(); poller.start(); document.addEventListener('visibilitychange', visibility); void load() })
|
||||
@@ -183,7 +195,9 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<AdminPage class="admin-density">
|
||||
<p v-if="!permitted" role="alert">没有自营经营数据查看权限。</p>
|
||||
<ListPageLayout v-else>
|
||||
<QueryToolbar v-model="filters" :fields="fields" ariaLabel="自营SKU筛选" :loading="loading" :search-on-reset="false" @search="search" @reset="reset" />
|
||||
<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 #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 #notices>
|
||||
<p v-if="runtimeError" class="analytics-note" role="status">{{ runtimeError }};最近观测:{{ beijingTime(overview?.observedAt) }}</p>
|
||||
<LoadErrorState v-if="error" title="查询未完成" :message="error" :loading="loading" compact @retry="load(true)"><template #details><ApiErrorMeta v-if="errorDetails" :details="errorDetails" /></template></LoadErrorState>
|
||||
<p v-if="newData || newDay" class="analytics-note" role="status">{{ newDay ? '北京时间已跨日。' : '有新数据。' }}点击查询更新数据。</p>
|
||||
@@ -195,7 +209,7 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<p v-if="context">实际计算区间:{{ context.rangeStartDate ?? '—' }} 至 {{ context.rangeEndDateExclusive ?? '—' }}(截止日不含)<template v-if="context.calculationWindowDays != null">;{{ context.calculationWindowDays }} 天</template>。</p>
|
||||
<p v-for="state in (overview?.warehouses ?? []).filter(item => item.warehouseCode === warehouseCode)" :key="state.dataFamily">{{ state.dataFamily === 'INVENTORY' ? '库存' : '销量' }}数据:{{ label(state.dataFreshness) }};最近发布时间 {{ beijingTime(state.lastPublishedAt) }}。</p>
|
||||
</details>
|
||||
<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>
|
||||
<template #toolbar-actions><el-button :disabled="!warehouseCode" @click="link('/self-operated/collection')">查看采集状态</el-button><el-button :disabled="!warehouseCode" @click="link(mode === 'sales' ? '/self-operated/inventory' : '/self-operated/sales')">{{ mode === 'sales' ? '查看库存' : '查看销量' }}</el-button></template>
|
||||
<template #actions="{ row }"><el-button class="density-action" @click="showTrend(asSummary(row))">每日趋势</el-button></template>
|
||||
<template #sku="{ row }"><el-button class="density-action" :aria-label="`查看 ${asMetric(row).customerCode} ${asMetric(row).sku} 每日趋势`" @click="showTrend(asMetric(row))">{{ asMetric(row).sku }}</el-button></template>
|
||||
@@ -208,11 +222,16 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<template #reason="{ row }">{{ reasonsText(summaryMode ? asSummary(row).reasonCodes : asMetric(row).reasonCodes) }}</template>
|
||||
<template v-for="key in ['physicalQuantity', 'availableQuantity', 'frozenQuantity', 'defectivePhysicalQuantity', 'transitQuantity', 'outboundQuantity30d', 'averageDailyOutboundQuantity', 'inventoryCoverageDays'] as const" :key="key" #[key]="{ row }">{{ quantityText(asMetric(row)[key]) }}<span v-if="asMetric(row).quantityStatuses?.[key] && asMetric(row).quantityStatuses?.[key] !== 'VALID'">({{ label(asMetric(row).quantityStatuses?.[key]) }})</span></template>
|
||||
</DataTable>
|
||||
<template v-if="daily && (summaryMode || mode === 'inventory')">
|
||||
<el-button @click="router.push({ path: mode === 'inventory' ? '/self-operated/sales' : '/self-operated/inventory', query: { warehouseCode, sku: daily.summary.sku } })">{{ mode === 'inventory' ? '在销量页查看该SKU(全部货主)' : '在库存页查看该SKU(全部货主)' }}</el-button>
|
||||
<SkuTrend :data="daily" />
|
||||
</template>
|
||||
</ListPageLayout>
|
||||
<AppDialog v-model="trendOpen" class="admin-density-dialog" title="SKU每日出库趋势" width="1120px" :loading="trendLoading" :show-confirm="false" cancel-text="关闭" fullscreenable>
|
||||
<div class="admin-density">
|
||||
<LoadErrorState v-if="trendError" title="趋势读取失败" :message="trendError" compact><template #details><ApiErrorMeta v-if="trendErrorDetails" :details="trendErrorDetails" /></template></LoadErrorState>
|
||||
<template v-if="daily">
|
||||
<SkuTrend :data="daily" />
|
||||
<el-button @click="router.push({ path: mode === 'inventory' ? '/self-operated/sales' : '/self-operated/inventory', query: { warehouseCode, sku: daily.summary.sku } })">{{ mode === 'inventory' ? '在销量页查看该SKU(全部货主)' : '在库存页查看该SKU(全部货主)' }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user