86086b1275
## 修改内容 - 数据大盘顶部日期改为可点击、可选择的日期范围控件 - 点击日期区域强制展开日历,选中后自动刷新看板 - 日期完整显示为 `YYYY-MM-DD`,禁选今日及未来日期 - 固定后台侧边栏并调整菜单顺序 - 优化数据大盘比价、领券、商业化核心指标及趋势 - 完善领券记录、比价记录、广告收益页面的筛选、统计口径和明细列 ## 关键口径 - 比价完成次数:成功 + 失败,不包含中途退出 - 比价成功率:成功次数 /(发起次数 - 中途退出次数) - 领券成功率:完成数 /(发起数 - 中途退出数) - 比价耗时统计排除中途退出 ## 验证 - `npm run build` 通过 - TypeScript、Lint、静态页面生成通过 - 旧合并请求 #49 已关闭且未合并 ## 后端依赖 - 领券逐场点位成功率及逐点明细仍需接口返回相应字段 - 广告逐条明细的来源广告网络仍需 `sub_rewards` 返回 ADN 字段 --------- Co-authored-by: linkeyu <798648091@qq.com> Reviewed-on: #50 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
62 lines
2.6 KiB
TypeScript
62 lines
2.6 KiB
TypeScript
// 全站统一的金额 / 时间格式化。
|
|
//
|
|
// 后端有两种时间口径,序列化后从字符串无法区分,必须按字段语义选对函数:
|
|
// 1. UTC 口径:`server_default=func.now()` 的字段(用户/提现单/审计日志/广告发奖记录)。
|
|
// 入库是 UTC,SQLite 下序列化不带时区、Postgres 下带 +00:00。统一当 UTC 解析后转北京显示。
|
|
// 2. 北京 wall-clock 口径:钱包流水(coin_transaction / cash_transaction)。入库即
|
|
// `datetime.now(CN_TZ).replace(tzinfo=None)`,字面就是北京时间且无时区,**不能再做时区换算**。
|
|
// 历史上各页面混用 `new Date().toLocaleString`(按本地解析)与 `apiTime`(补 Z 当 UTC),
|
|
// 导致提现详情里现金流水时间快 8 小时。此模块收口,按口径区分。
|
|
import dayjs from 'dayjs';
|
|
import utc from 'dayjs/plugin/utc';
|
|
import timezone from 'dayjs/plugin/timezone';
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
import 'dayjs/locale/zh-cn';
|
|
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
dayjs.extend(relativeTime);
|
|
dayjs.locale('zh-cn');
|
|
|
|
const TZ = 'Asia/Shanghai';
|
|
|
|
/** 金额:分 → ¥元(两位小数)。 */
|
|
export const yuan = (cents: number) => `¥${(cents / 100).toFixed(2)}`;
|
|
|
|
/**
|
|
* 线性插值分位数。
|
|
* 耗时等整数指标默认四舍五入;计数分位需要保留插值小数时传 round=false。
|
|
*/
|
|
export function percentile(values: readonly number[], q: number, round = true): number | null {
|
|
if (!values.length) return null;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const idx = (sorted.length - 1) * q;
|
|
const lo = Math.floor(idx);
|
|
const hi = Math.min(lo + 1, sorted.length - 1);
|
|
const result = sorted[lo] * (hi - idx) + sorted[hi] * (idx - lo);
|
|
return round ? Math.round(result) : result;
|
|
}
|
|
|
|
const hasTz = (v: string) => /(?:Z|[+-]\d{2}:?\d{2})$/i.test(v);
|
|
|
|
/** 把 UTC 口径字符串解析为带时区的 dayjs(无时区后缀的补 Z 当 UTC)。 */
|
|
export const utcDayjs = (v: string) => dayjs(hasTz(v) ? v : `${v}Z`);
|
|
|
|
/** UTC 口径时间 → 北京时间显示(用户/提现单/审计/广告发奖记录)。 */
|
|
export function formatUtcTime(v?: string | null, fmt = 'YYYY-MM-DD HH:mm'): string {
|
|
if (!v) return '-';
|
|
return utcDayjs(v).tz(TZ).format(fmt);
|
|
}
|
|
|
|
/** UTC 口径相对时间(如「3 分钟前」)。 */
|
|
export function utcFromNow(v?: string | null): string {
|
|
if (!v) return '-';
|
|
return utcDayjs(v).fromNow();
|
|
}
|
|
|
|
/** 北京 wall-clock 口径时间 → 原样格式化,不做时区换算(钱包金币/现金流水)。 */
|
|
export function formatWallTime(v?: string | null, fmt = 'YYYY-MM-DD HH:mm'): string {
|
|
if (!v) return '-';
|
|
return dayjs(v).format(fmt);
|
|
}
|