Show the real percentage on the card, but sort with delta / (baseline + prior) so a small baseline cannot dominate. Same-day publishes stay off the rate board.
158 lines
4.7 KiB
JavaScript
158 lines
4.7 KiB
JavaScript
const COUNT_UNITS = [
|
|
{ value: 1e8, suffix: "亿" },
|
|
{ value: 1e4, suffix: "万" },
|
|
];
|
|
|
|
export function formatCount(value) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
|
const number = Number(value);
|
|
for (const unit of COUNT_UNITS) {
|
|
if (Math.abs(number) >= unit.value) {
|
|
const scaled = number / unit.value;
|
|
return `${trimZero(scaled.toFixed(Math.abs(scaled) >= 1000 ? 0 : 1))}${unit.suffix}`;
|
|
}
|
|
}
|
|
return Math.round(number).toLocaleString("zh-CN");
|
|
}
|
|
|
|
export function formatAxis(value) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "";
|
|
const number = Number(value);
|
|
if (Math.abs(number) >= 1e8) return `${trimZero((number / 1e8).toFixed(2))}亿`;
|
|
if (Math.abs(number) >= 1e6) return `${trimZero((number / 1e4).toFixed(2))}万`;
|
|
// 小于一百万时直接显示精确值,避免 59,798 与 59,800 都被写成“6万”。
|
|
return Math.round(number).toLocaleString("zh-CN");
|
|
}
|
|
|
|
export function formatNumber(value) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
|
return Number(value).toLocaleString("zh-CN");
|
|
}
|
|
|
|
export function formatDelta(value) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
|
const number = Number(value);
|
|
const sign = number > 0 ? "+" : "";
|
|
return `${sign}${formatCount(number)}`;
|
|
}
|
|
|
|
export function formatScore(value, digits = 2) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
|
return Number(value).toFixed(digits).replace(/\.?0+$/, "");
|
|
}
|
|
|
|
export function formatPercent(value, digits = 2) {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
|
return `${(Number(value) * 100).toFixed(digits)}%`;
|
|
}
|
|
|
|
export function formatSignedPercent(value, digits = 2) {
|
|
const text = formatPercent(value, digits);
|
|
if (text === "—" || Number(value) <= 0) return text;
|
|
return `+${text}`;
|
|
}
|
|
|
|
export function formatDate(timestamp) {
|
|
if (!timestamp) return "—";
|
|
const date = new Date(Number(timestamp) * 1000);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
year: "numeric", month: "2-digit", day: "2-digit",
|
|
}).format(date);
|
|
}
|
|
|
|
|
|
export function formatClock(timestamp) {
|
|
if (!timestamp) return "—";
|
|
const date = new Date(Number(timestamp) * 1000);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
timeZone: "Asia/Shanghai",
|
|
}).format(date);
|
|
}
|
|
|
|
export function formatDataDate(value) {
|
|
if (!value) return "—";
|
|
return String(value).replace(/-/g, "/");
|
|
}
|
|
|
|
export function formatDateTime(timestamp) {
|
|
if (!timestamp) return "—";
|
|
const date = new Date(Number(timestamp) * 1000);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit",
|
|
}).format(date);
|
|
}
|
|
|
|
export function formatDuration(seconds) {
|
|
if (seconds === null || seconds === undefined || Number(seconds) <= 0) return "—";
|
|
let total = Math.round(Number(seconds));
|
|
const hours = Math.floor(total / 3600);
|
|
total -= hours * 3600;
|
|
const minutes = Math.floor(total / 60);
|
|
const rest = total - minutes * 60;
|
|
if (hours > 0) {
|
|
return `${hours}:${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
|
|
}
|
|
return `${minutes}:${String(rest).padStart(2, "0")}`;
|
|
}
|
|
|
|
export function formatDay(value) {
|
|
if (!value) return "—";
|
|
return String(value).slice(5);
|
|
}
|
|
|
|
export function trimZero(text) {
|
|
return String(text).replace(/\.0$/, "").replace(/(\.\d)0$/, "$1");
|
|
}
|
|
|
|
export function escapeHtml(value) {
|
|
if (value === null || value === undefined) return "";
|
|
return String(value)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
export function escapeAttr(value) {
|
|
return escapeHtml(value).replace(/`/g, "`");
|
|
}
|
|
|
|
export function coverUrl(value) {
|
|
if (!value) return "";
|
|
return value.startsWith("//") ? `https:${value}` : value;
|
|
}
|
|
|
|
export function metricLabel(metric) {
|
|
return ({
|
|
view: "播放",
|
|
like: "点赞",
|
|
coin: "投币",
|
|
favorite: "收藏",
|
|
share: "转发",
|
|
reply: "评论",
|
|
danmaku: "弹幕",
|
|
})[metric] || metric;
|
|
}
|
|
|
|
export function statusLabel(status) {
|
|
return ({
|
|
active: "在库",
|
|
unfaved: "已取消收藏",
|
|
removed: "已失效",
|
|
dropped: "已移出库",
|
|
})[status] || status || "—";
|
|
}
|
|
|
|
export function debounce(fn, wait = 280) {
|
|
let timer = null;
|
|
return (...args) => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => fn(...args), wait);
|
|
};
|
|
}
|