feat(web): add mobile-first read-only dashboard frontend
This commit is contained in:
1 parent
257fbe009b
commit
cbba405271
18 files changed
+2628
-2
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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) >= 1e4) {
|
||||
const scaled = number / 1e4;
|
||||
return `${trimZero(scaled.toFixed(Math.abs(scaled) >= 100 ? 1 : 2))}万`;
|
||||
}
|
||||
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 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 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);
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user