Files
ai-kefu-admin/admin/src/utils/statsRange.ts
T
浅念 230eb2d86c feat: 统计分析(卡片/漏斗/趋势/分布/推送健康,ECharts)
- admin 统计分析页(views/stats/overview): 指标卡四张(口径文案沿用
  仪表盘)/ 漏斗横向条形(有序单色明度阶梯,标注 数量·占上一级%)/
  按日趋势双线(窗口内无数据日期前端补零)/ 意向环形(分类三槽固定
  指派,色随实体不随筛选)/ 来源·站点·国家 Top10 横向条形(名义类目
  统一单色)/ 推送健康表(四状态计数 + duplicated 子集行)
- 筛选一行作用全部图表: 时间档(今天/近7天/近30天/自定义区间,自定义
  不完整不发请求并提示)+ 租户切换(超管含"全部"合并选项,停用租户
  标注;租户管理员固定自己)
- charts.ts 纯函数层(选项构建 + 双模式主题档),配色经
  validate_palette.js 明暗双模式校验(分类/漏斗 ordinal 全过;
  意向饼语义红绿组 CVD 硬伤弃用改分类槽)
- statsRange.ts 扩展自定义区间解析(既有仪表盘导出不动);
  新增 charts/resolveStatsRange 单测 19 例(vitest 120→139)
- 修复 merge_stats distributions 尾随逗号 1 元组 bug(合并端点曾把
  distributions 序列化成 JSON 数组,单租户路径不受累)+ 回归钉子;
  后端 595 测试全过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 22:33:55 +08:00

71 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 统计时间快捷范围(Phase 3 Task 4 仪表盘;Task 8 统计分析复用)
*
* 口径事实源:app/platform/routes/stats_routes.py 的 from/to 解析
* (data_routes._parse_dt)——前端只传本地日期串 YYYY-MM-DD:
* - 日期-only 的 to 后端视为含当日全天(23:59:59.999999,边界含);
* - 边界是 naive UTC(与库内 created_at 同一对比空间),前端不做时区换算;
* - "近 N 天" = 含今天往回共 N 天(今天 / 近7天 / 近30天)。
*/
export type StatsQuickRange = 'today' | 'last7' | 'last30'
/** 快捷范围清单(顺序即切换器展示序) */
export const STATS_QUICK_RANGES: Array<{
value: StatsQuickRange
label: string
/** 窗口天数(含今天) */
days: number
}> = [
{ value: 'today', label: '今天', days: 1 },
{ value: 'last7', label: '近7天', days: 7 },
{ value: 'last30', label: '近30天', days: 30 }
]
/** Date → 本地日期串 YYYY-MM-DD(月/日两位补零) */
function toDateKey(date: Date): string {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${date.getFullYear()}-${month}-${day}`
}
/** 快捷范围 → {from, to} 查询参数(本地日期;now 可注入便于测试) */
export function rangeToParams(
range: StatsQuickRange,
now: Date = new Date()
): { from: string; to: string } {
const days = STATS_QUICK_RANGES.find((item) => item.value === range)?.days ?? 1
return {
from: toDateKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - (days - 1))),
to: toDateKey(now)
}
}
// ---- 自定义区间扩展(Phase 3 Task 8 统计分析页) ----------------------------------
/** 时间筛选状态:快捷档 + 自定义(统计分析页;仪表盘只用快捷档) */
export type StatsRangeFilter = StatsQuickRange | 'custom'
/** 筛选档位清单(顺序即切换器展示序;自定义档配合日期区间选择器) */
export const STATS_RANGE_OPTIONS: Array<{ value: StatsRangeFilter; label: string }> = [
...STATS_QUICK_RANGES.map(({ value, label }) => ({ value, label })),
{ value: 'custom', label: '自定义' }
]
/**
* 筛选状态 → {from, to} 查询参数(custom 需完整且起≤止的区间)
*
* 返回 null = 区间不完整/非法(自定义只选了一半、起>止)——调用方不发请求,
* 等 ElDatePicker 补全;快速档永不为 null。
*/
export function resolveStatsRange(
filter: StatsRangeFilter,
custom: [string, string] | null | undefined,
now: Date = new Date()
): { from: string; to: string } | null {
if (filter !== 'custom') return rangeToParams(filter, now)
if (!custom || !custom[0] || !custom[1]) return null
const [from, to] = custom
if (from > to) return null // ISO 日期串字典序即时间序
return { from, to }
}