feat(ar): 应收账龄看板 + 逾期预警 overdue 接入(阶段A)
T1 逾期预警(§6 财务 2026-10-20 23:23 口径):
- alert_rules 支持 rule_type=ar_overdue;阈值/责任人/排除名单全量走 params(代码零业务常量)
- 90 天占比红线 key 保留但默认 null 停用(不回落 0.25);黄线 15%-25% 保留
- 新增逐户红线「>30 天且 >5 万」+ 聚合红线「150+ 占比环比上升 = 结构恶化」
- assignee 派生:manual → 近 3 个月主业务员(zxt_masterbill emp_id → employee_dict.u_Name,
缺表时降级同库摘要制单人姓名)→ 部门负责人(渠道 118 / 商务 114 / 直属酒窖 117);
两路皆失败 ⇒ 该户不生成、进 skipped[],禁落空 assignee
- POST /run-ar-overdue(dry_run 默认 true)+ check-all 分支;只 INSERT 同一 message 幂等
T2 账龄看板:
- GET /api/cma/ar-aging/{meta,trend,overview,customers,customers/export};每条查询带 entity_id
- 三口径分列、caliber=all → 400;缺期 has_data=false 且金额 null(不补 0)
- ArAging.vue:口径后缀/截止期徽标/趋势断点/六档堆叠/逐户可排序导出
单测 21 项全通过(SQLite 夹具,不连生产库);vite build 通过。
阶段A 零生产写:未 push、未部署、未 --apply、未 dry_run=false。
This commit is contained in:
committed by
Hermes CI Fix
parent
790be2d6b1
commit
71867c4c7a
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div class="ar-aging-page">
|
||||
<!-- 页头:标题 + 口径后缀(强制显示)+ 数据截止期徽标 + 来源说明 -->
|
||||
<header class="page-head">
|
||||
<div class="head-main">
|
||||
<h2 class="page-title">
|
||||
应收账龄看板<span class="caliber-suffix">({{ caliberLabel }})</span>
|
||||
</h2>
|
||||
<div class="badge-row">
|
||||
<el-tag size="small" effect="plain" :type="overview.has_data === false ? 'warning' : 'success'">
|
||||
数据截止期 {{ displayPeriod || '无' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="meta.loaded_at" size="small" effect="plain">入表 {{ meta.loaded_at }}</el-tag>
|
||||
<el-tag size="small" effect="plain" type="primary">{{ entityLabel }}</el-tag>
|
||||
<el-tag size="small" effect="plain" type="info">口径分列 · 禁跨口径合计</el-tag>
|
||||
</div>
|
||||
<p class="source-note">
|
||||
来源:智享通「往来单位未收款账期统计」逐户导出;六档互斥(0-30/31-60/61-90/90-120/120-150/150+),
|
||||
逐期六档和 = 期末应收合计;期初只展示,不作账龄基数。
|
||||
</p>
|
||||
</div>
|
||||
<div class="head-controls">
|
||||
<el-select v-model="caliber" size="small" style="width:190px" @change="onCaliberChange">
|
||||
<el-option v-for="c in calibers" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="period" size="small" style="width:120px" :loading="loading"
|
||||
placeholder="选择期间" @change="loadPeriodData"
|
||||
>
|
||||
<el-option v-for="p in periodOptions" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
<el-button size="small" :loading="exporting" @click="doExport">
|
||||
<svg class="ico" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 3v12m0 0l-4-4m4 4l4-4M4 21h16" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
导出逐户 CSV
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 表不可用/无权限:降级可读,不留白、不补 0 -->
|
||||
<el-alert
|
||||
v-if="metaLoaded && meta.available === false"
|
||||
type="warning" :closable="false" show-icon
|
||||
title="该期无数据(表未就绪/无权限)"
|
||||
:description="meta.reason || 'hanke_ar_aging 不可用'"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<!-- 卡片区 -->
|
||||
<el-row :gutter="16" v-loading="loading">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="metric">
|
||||
<div class="metric-label">应收总额(期末)</div>
|
||||
<div class="metric-value">{{ money(overview.total_amt) }}</div>
|
||||
<div class="metric-sub">
|
||||
{{ overview.has_data ? `往来单位 ${overview.customers} 户` : '该期无数据' }}
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="metric">
|
||||
<div class="metric-label">逾期金额(>30 天)</div>
|
||||
<div class="metric-value">{{ money(overview.overdue_amt) }}</div>
|
||||
<div class="metric-sub">
|
||||
环比
|
||||
<span :class="momClass(overview.overdue_amt_mom_pct)">
|
||||
{{ pctSigned(overview.overdue_amt_mom_pct) }}
|
||||
</span>
|
||||
<span class="dim">{{ overview.mom_note || (overview.prev_period ? `上期 ${overview.prev_period}` : '') }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="metric">
|
||||
<div class="metric-label">90 天以上占比</div>
|
||||
<div class="metric-value" :style="{ color: shareColor }">{{ pct(overview.share_90plus) }}</div>
|
||||
<div class="metric-sub">
|
||||
<span v-if="thresholds.share_90plus_red_gt != null" class="th-red">
|
||||
红线 >{{ pct(thresholds.share_90plus_red_gt) }}
|
||||
</span>
|
||||
<span v-if="thresholds.share_90plus_yellow_min != null" class="th-yellow">
|
||||
黄线 {{ pct(thresholds.share_90plus_yellow_min) }}–{{ pct(thresholds.share_90plus_yellow_max) }}
|
||||
</span>
|
||||
<span v-if="thresholds.share_90plus_red_gt == null" class="dim">阈值未配置(阶段B 播种后显示)</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="metric">
|
||||
<div class="metric-label">逾期户数(>30 天)</div>
|
||||
<div class="metric-value">{{ overview.overdue_customers ?? '该期无数据' }}</div>
|
||||
<div class="metric-sub">
|
||||
环比
|
||||
<span :class="momClass(overview.overdue_customers_mom_pct)">
|
||||
{{ pctSigned(overview.overdue_customers_mom_pct) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图区 -->
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :xs="24" :lg="14">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<span>逐期趋势 · 应收总额 / 逾期户数</span>
|
||||
<span class="dim small">{{ trendRange }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="trendRef" class="chart-box"></div>
|
||||
<p class="chart-note">缺数据期以灰带标注「该期无数据」,曲线断点、不补 0。</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="10">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<span>六档账龄堆叠</span>
|
||||
<el-tag size="small" :type="bucketsBalanced ? 'success' : 'danger'" effect="plain">
|
||||
六档和=合计 {{ balancedCount }}/{{ dataPointCount }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="stackRef" class="chart-box"></div>
|
||||
<p class="chart-note">tooltip 内断言六档和与合计一致;不一致会标红。</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 逐户明细 -->
|
||||
<el-card shadow="never" class="table-card">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<span>逐户逾期清单 · {{ displayPeriod || '—' }}({{ caliberLabel }})</span>
|
||||
<div class="table-tools">
|
||||
<el-switch v-model="overdueOnly" size="small" active-text="仅逾期" @change="loadCustomers" />
|
||||
<span class="dim small">共 {{ customers.length }} 户</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="customers" size="small" stripe border
|
||||
:default-sort="{ prop: 'overdue_amt', order: 'descending' }"
|
||||
@sort-change="onSortChange"
|
||||
empty-text="该期无数据(不补 0)"
|
||||
>
|
||||
<el-table-column prop="unit_code" label="单位编码" width="170" sortable="custom" />
|
||||
<el-table-column prop="unit_name" label="单位名称" min-width="130" sortable="custom" />
|
||||
<el-table-column prop="total_amt" label="期末应收合计" width="130" align="right" sortable="custom">
|
||||
<template #default="{ row }">{{ money(row.total_amt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="overdue_amt" label="逾期额(>30天)" width="135" align="right" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'neg': row.overdue_amt > 0 }">{{ money(row.overdue_amt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pct_of_unit" label="逾期占该户" width="110" align="right">
|
||||
<template #default="{ row }">{{ pct(row.pct_of_unit) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="oldest_days" label="最老逾期档" width="110" align="center" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.oldest_days">{{ row.oldest_days }} 天以上</span>
|
||||
<span v-else class="dim">未逾期</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="b in buckets" :key="b.col" :prop="`buckets.${b.col}`"
|
||||
:label="b.label" width="108" align="right">
|
||||
<template #default="{ row }">{{ money(row.buckets[b.col]) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api, { arAgingApi } from '@/api'
|
||||
import { sessionEntityName } from '@/utils/entity'
|
||||
|
||||
const CALIBERS = ['借方应收户口径', '全户口径', '贷方户单列(净额)']
|
||||
const BUCKETS = [
|
||||
{ col: 'b_0_30', label: '0-30' },
|
||||
{ col: 'b_31_60', label: '31-60' },
|
||||
{ col: 'b_61_90', label: '61-90' },
|
||||
{ col: 'b_90_120', label: '90-120' },
|
||||
{ col: 'b_120_150', label: '120-150' },
|
||||
{ col: 'b_150p', label: '150+' },
|
||||
]
|
||||
|
||||
const entityLabel = ref(sessionEntityName())
|
||||
const calibers = ref<string[]>(CALIBERS)
|
||||
const caliber = ref(CALIBERS[0])
|
||||
const period = ref('')
|
||||
const periodOptions = ref<string[]>([])
|
||||
const meta = ref<any>({ available: null })
|
||||
const metaLoaded = ref(false)
|
||||
const overview = ref<any>({})
|
||||
const thresholds = computed<any>(() => overview.value.thresholds || {})
|
||||
const trend = ref<any[]>([])
|
||||
const customers = ref<any[]>([])
|
||||
const overdueOnly = ref(false)
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const sortState = ref<{ sort: string; order: string }>({ sort: 'overdue_amt', order: 'desc' })
|
||||
|
||||
const caliberLabel = computed(() => caliber.value)
|
||||
const displayPeriod = computed(() => overview.value.period || period.value || '')
|
||||
const buckets = BUCKETS
|
||||
|
||||
let trendChart: any = null
|
||||
let stackChart: any = null
|
||||
|
||||
/** 金额格式化:null/undefined ⇒ 该期无数据(绝不显示 0.00) */
|
||||
function money(v: any): string {
|
||||
if (v === null || v === undefined || v === '') return '该期无数据'
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) return '该期无数据'
|
||||
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
}
|
||||
|
||||
function pct(v: any): string {
|
||||
if (v === null || v === undefined || !Number.isFinite(Number(v))) return '—'
|
||||
return `${(Number(v) * 100).toFixed(2)}%`
|
||||
}
|
||||
|
||||
function pctSigned(v: any): string {
|
||||
if (v === null || v === undefined || !Number.isFinite(Number(v))) return '—'
|
||||
const n = Number(v) * 100
|
||||
return `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`
|
||||
}
|
||||
|
||||
function momClass(v: any): string {
|
||||
if (v === null || v === undefined) return 'dim'
|
||||
return Number(v) > 0 ? 'up' : Number(v) < 0 ? 'down' : 'dim'
|
||||
}
|
||||
|
||||
const shareColor = computed(() => {
|
||||
const s = overview.value?.share_90plus
|
||||
const th: any = thresholds.value
|
||||
if (s === null || s === undefined) return 'inherit'
|
||||
if (th.share_90plus_red_gt != null && Number(s) > Number(th.share_90plus_red_gt)) return '#f56c6c'
|
||||
if (th.share_90plus_yellow_min != null && Number(s) >= Number(th.share_90plus_yellow_min)) return '#e6a23c'
|
||||
return '#67c23a'
|
||||
})
|
||||
|
||||
const dataPoints = computed(() => trend.value.filter(p => p.has_data))
|
||||
const dataPointCount = computed(() => dataPoints.value.length)
|
||||
const balancedCount = computed(() => dataPoints.value.filter(p => {
|
||||
const sum = BUCKETS.reduce((acc, b) => acc + Number(p.buckets?.[b.col] ?? 0), 0)
|
||||
return Math.abs(sum - Number(p.total_amt ?? 0)) < 0.01
|
||||
}).length)
|
||||
const bucketsBalanced = computed(() => dataPointCount.value > 0 && balancedCount.value === dataPointCount.value)
|
||||
const trendRange = computed(() => {
|
||||
if (!trend.value.length) return ''
|
||||
return `${trend.value[0].period} ~ ${trend.value[trend.value.length - 1].period}(${trend.value.length} 期)`
|
||||
})
|
||||
|
||||
async function loadMeta() {
|
||||
const r: any = await arAgingApi.meta()
|
||||
meta.value = r || {}
|
||||
metaLoaded.value = true
|
||||
if (Array.isArray(r?.calibers) && r.calibers.length) calibers.value = r.calibers
|
||||
const periods: string[] = Array.isArray(r?.periods_available) ? r.periods_available : []
|
||||
// 期间候选 = 表内可用期 + 当前月(缺期也要能选中并显「该期无数据」)
|
||||
const nowPeriod = new Date().toISOString().slice(0, 7)
|
||||
const opts = [...periods]
|
||||
if (!opts.includes(nowPeriod)) opts.push(nowPeriod)
|
||||
periodOptions.value = opts.sort().reverse()
|
||||
if (!period.value) period.value = r?.latest_period || opts[0] || ''
|
||||
}
|
||||
|
||||
async function loadTrend() {
|
||||
const r: any = await arAgingApi.trend({ caliber: caliber.value })
|
||||
trend.value = Array.isArray(r?.points) ? r.points : []
|
||||
}
|
||||
|
||||
async function loadPeriodData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ov, cu] = await Promise.all([
|
||||
arAgingApi.overview({ period: period.value, caliber: caliber.value }),
|
||||
arAgingApi.customers({
|
||||
period: period.value, caliber: caliber.value,
|
||||
overdue_only: overdueOnly.value, sort: sortState.value.sort, order: sortState.value.order,
|
||||
}),
|
||||
])
|
||||
overview.value = (ov as any) || {}
|
||||
customers.value = Array.isArray((cu as any)?.items) ? (cu as any).items : []
|
||||
if (!overview.value.period && period.value) overview.value.period = period.value
|
||||
await nextTick()
|
||||
renderCharts()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '账龄数据加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomers() {
|
||||
const r: any = await arAgingApi.customers({
|
||||
period: period.value, caliber: caliber.value,
|
||||
overdue_only: overdueOnly.value, sort: sortState.value.sort, order: sortState.value.order,
|
||||
})
|
||||
customers.value = Array.isArray(r?.items) ? r.items : []
|
||||
}
|
||||
|
||||
function onSortChange({ prop, order }: any) {
|
||||
if (!prop || !order) {
|
||||
sortState.value = { sort: 'overdue_amt', order: 'desc' }
|
||||
} else {
|
||||
sortState.value = { sort: prop, order: order === 'ascending' ? 'asc' : 'desc' }
|
||||
}
|
||||
loadCustomers()
|
||||
}
|
||||
|
||||
async function onCaliberChange() {
|
||||
await Promise.all([loadTrend(), loadPeriodData()])
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
const periods = trend.value.map(p => p.period)
|
||||
const missing = trend.value.filter(p => !p.has_data).map(p => p.period)
|
||||
|
||||
if (trendRef.value) {
|
||||
trendChart = trendChart || echarts.init(trendRef.value)
|
||||
trendChart.setOption({
|
||||
grid: { left: 60, right: 60, top: 40, bottom: 40 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (ps: any[]) => {
|
||||
const p = ps[0]?.axisValue
|
||||
const pt = trend.value.find(t => t.period === p)
|
||||
if (!pt || !pt.has_data) return `${p}<br/>该期无数据`
|
||||
return `${p}<br/>应收总额:${money(pt.total_amt)}<br/>逾期额(>30天):${money(pt.overdue_amt)}<br/>往来单位:${pt.customers} 户`
|
||||
},
|
||||
},
|
||||
legend: { data: ['应收总额', '往来单位户数'], top: 0 },
|
||||
xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 11 } },
|
||||
yAxis: [
|
||||
{ type: 'value', name: '金额(元)', axisLabel: { formatter: (v: number) => `${(v / 10000).toFixed(0)}万` } },
|
||||
{ type: 'value', name: '户数', minInterval: 1 },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '应收总额', type: 'line', smooth: false, connectNulls: false,
|
||||
data: trend.value.map(p => (p.has_data ? p.total_amt : null)),
|
||||
itemStyle: { color: '#409EFF' }, areaStyle: { opacity: 0.08 },
|
||||
markArea: missing.length ? {
|
||||
silent: true,
|
||||
itemStyle: { color: 'rgba(144,147,153,0.12)' },
|
||||
label: { show: true, position: 'insideTop', color: '#909399', fontSize: 10, formatter: '该期无数据' },
|
||||
data: missing.map(p => [{ xAxis: p }, { xAxis: p }]),
|
||||
} : undefined,
|
||||
},
|
||||
{
|
||||
name: '往来单位户数', type: 'bar', yAxisIndex: 1,
|
||||
data: trend.value.map(p => (p.has_data ? p.customers : null)),
|
||||
itemStyle: { color: '#c8d6e5' }, barMaxWidth: 18,
|
||||
},
|
||||
],
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (stackRef.value) {
|
||||
stackChart = stackChart || echarts.init(stackRef.value)
|
||||
stackChart.setOption({
|
||||
grid: { left: 60, right: 24, top: 40, bottom: 40 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (ps: any[]) => {
|
||||
const p = ps[0]?.axisValue
|
||||
const pt = trend.value.find(t => t.period === p)
|
||||
if (!pt || !pt.has_data) return `${p}<br/>该期无数据`
|
||||
const sum = BUCKETS.reduce((acc, b) => acc + Number(pt.buckets?.[b.col] ?? 0), 0)
|
||||
const diff = sum - Number(pt.total_amt ?? 0)
|
||||
const ok = Math.abs(diff) < 0.01
|
||||
const lines = BUCKETS.map((b, i) => `${b.label}:${money(ps[i]?.value)}`)
|
||||
return [
|
||||
`${p}(${caliber.value})`,
|
||||
...lines,
|
||||
`六档之和:${money(sum)}`,
|
||||
`期末合计:${money(pt.total_amt)}`,
|
||||
ok ? '校验:六档和 = 合计' : `校验异常:差额 ${diff.toFixed(2)}`,
|
||||
].join('<br/>')
|
||||
},
|
||||
},
|
||||
legend: { data: BUCKETS.map(b => b.label), top: 0, itemWidth: 12, itemHeight: 8, textStyle: { fontSize: 11 } },
|
||||
xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 11 } },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: (v: number) => `${(v / 10000).toFixed(0)}万` } },
|
||||
series: BUCKETS.map((b, i) => ({
|
||||
name: b.label, type: 'bar', stack: 'aging', barMaxWidth: 26,
|
||||
data: trend.value.map(p => (p.has_data ? p.buckets?.[b.col] : null)),
|
||||
itemStyle: { color: ['#a3d0a3', '#e8c07d', '#e5a45f', '#e0765f', '#c74b4b', '#8f2f2f'][i] },
|
||||
})),
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function doExport() {
|
||||
exporting.value = true
|
||||
try {
|
||||
const res: any = await api.get('/ar-aging/customers/export', {
|
||||
params: {
|
||||
period: period.value, caliber: caliber.value,
|
||||
overdue_only: overdueOnly.value,
|
||||
},
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = window.URL.createObjectURL(res)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `应收账龄_${displayPeriod.value || 'noperiod'}_${caliber.value}.csv`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('逐户清单已导出')
|
||||
} catch (e: any) {
|
||||
ElMessage.error('导出失败(需 finance/ceo/business 权限且该期有数据)')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
trendChart?.resize()
|
||||
stackChart?.resize()
|
||||
}
|
||||
|
||||
const trendRef = ref<HTMLElement | null>(null)
|
||||
const stackRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', onResize)
|
||||
await loadMeta()
|
||||
await loadTrend()
|
||||
await loadPeriodData()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
trendChart?.dispose()
|
||||
stackChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ar-aging-page { max-width: 1440px; margin: 0 auto; padding: 16px; }
|
||||
.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 16px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; margin: 0 0 8px; }
|
||||
.caliber-suffix { color: #409eff; font-size: 16px; font-weight: 500; }
|
||||
.badge-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
||||
.source-note { font-size: 12px; color: #909399; margin: 0; line-height: 1.5; max-width: 720px; }
|
||||
.head-controls { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
|
||||
.metric { min-height: 108px; }
|
||||
.metric-label { font-size: 13px; color: #909399; }
|
||||
.metric-value { font-size: 26px; font-weight: 600; margin: 6px 0; word-break: break-all; }
|
||||
.metric-sub { font-size: 12px; color: #909399; }
|
||||
.chart-row { margin-top: 16px; }
|
||||
.chart-box { height: 320px; }
|
||||
.chart-note { font-size: 12px; color: #909399; margin: 6px 0 0; }
|
||||
.card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
||||
.table-card { margin-top: 16px; }
|
||||
.table-tools { display: flex; align-items: center; gap: 12px; }
|
||||
.small { font-size: 12px; }
|
||||
.dim { color: #909399; }
|
||||
.up { color: #f56c6c; font-weight: 600; }
|
||||
.down { color: #67c23a; font-weight: 600; }
|
||||
.neg { color: #f56c6c; }
|
||||
.th-red { color: #f56c6c; margin-right: 8px; }
|
||||
.th-yellow { color: #e6a23c; }
|
||||
.ico { width: 14px; height: 14px; margin-right: 4px; vertical-align: -2px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user