| 1 | /** |
| 2 | * 格式化工具函数 |
| 3 | */ |
| 4 | |
| 5 | import dayjs from 'dayjs' |
| 6 | |
| 7 | /** |
| 8 | * 格式化数字,大数字显示为 k/w 形式 |
| 9 | * @example formatNumber(1234) -> "1.2k" |
| 10 | * @example formatNumber(12345) -> "1.2w" |
| 11 | */ |
| 12 | export function formatNumber(value: number): string { |
| 13 | if (!value) |
| 14 | return `${value ?? 0}` |
| 15 | |
| 16 | if (value >= 10000) { |
| 17 | return `${(value / 10000).toFixed(1).replace(/\.0$/, '')}w` |
| 18 | } |
| 19 | else if (value >= 1000) { |
| 20 | return `${(value / 1000).toFixed(1).replace(/\.0$/, '')}k` |
| 21 | } |
| 22 | return value.toString() |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * 格式化 AI 推荐分,保留最多 1 位小数 |
| 27 | */ |
| 28 | export function formatRecommendationScore(score?: number | null) { |
| 29 | if (typeof score !== 'number' || !Number.isFinite(score)) |
| 30 | return null |
| 31 | |
| 32 | return Number.isInteger(score) |
| 33 | ? score.toString() |
| 34 | : score.toFixed(1).replace(/\.0$/, '') |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * 格式化文件大小,自动使用 B / KB / MB / GB / TB 单位 |
| 39 | */ |
| 40 | export function formatFileSize(bytes: number): string { |
| 41 | if (!Number.isFinite(bytes) || bytes <= 0) |
| 42 | return '0 B' |
| 43 | |
| 44 | const unitBase = 1024 |
| 45 | const units = ['B', 'KB', 'MB', 'GB', 'TB'] |
| 46 | const unitIndex = Math.min( |
| 47 | Math.floor(Math.log(bytes) / Math.log(unitBase)), |
| 48 | units.length - 1, |
| 49 | ) |
| 50 | const value = bytes / unitBase ** unitIndex |
| 51 | const formattedValue = Number.isInteger(value) |
| 52 | ? value.toString() |
| 53 | : value.toFixed(2).replace(/\.?0+$/, '') |
| 54 | |
| 55 | return `${formattedValue} ${units[unitIndex]}` |
| 56 | } |
| 57 | |
| 58 | export interface DurationParts { |
| 59 | hours: number |
| 60 | minutes: number |
| 61 | seconds: number |
| 62 | } |
| 63 | |
| 64 | /** 将秒数拆分为小时、分钟、秒,小时不按天折算 */ |
| 65 | export function getDurationPartsFromSeconds(seconds: number): DurationParts { |
| 66 | const safeSeconds = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0 |
| 67 | return { |
| 68 | hours: Math.floor(safeSeconds / 3600), |
| 69 | minutes: Math.floor((safeSeconds % 3600) / 60), |
| 70 | seconds: safeSeconds % 60, |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * 格式化日期 |
| 76 | * @param date 日期 |
| 77 | * @param format 格式,默认 'YYYY-MM-DD HH:mm' |
| 78 | */ |
| 79 | export function formatDate( |
| 80 | date: string | number | Date, |
| 81 | format: string = 'YYYY-MM-DD HH:mm', |
| 82 | ): string { |
| 83 | return dayjs(date).format(format) |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * 格式化相对时间。 |
| 88 | */ |
| 89 | export function formatRelativeTime(date: Date | number): string { |
| 90 | const now = new Date() |
| 91 | const target = date instanceof Date ? date : new Date(date) |
| 92 | const diffMs = now.getTime() - target.getTime() |
| 93 | const diffSec = Math.floor(diffMs / 1000) |
| 94 | const diffMin = Math.floor(diffSec / 60) |
| 95 | const diffHour = Math.floor(diffMin / 60) |
| 96 | const diffDay = Math.floor(diffHour / 24) |
| 97 | |
| 98 | if (diffSec < 60) |
| 99 | return 'Just now' |
| 100 | |
| 101 | if (diffMin < 60) |
| 102 | return `${diffMin}m ago` |
| 103 | |
| 104 | if (diffHour < 24) |
| 105 | return `${diffHour}h ago` |
| 106 | |
| 107 | if (diffDay < 7) |
| 108 | return `${diffDay}d ago` |
| 109 | |
| 110 | return dayjs(target).format('YYYY-MM-DD') |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * 格式化时间。 |
| 115 | */ |
| 116 | export function formatTime(time: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss') { |
| 117 | return dayjs(time).format(format) |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * 将秒数转换为 hh:mm:ss 格式的字符串。 |
| 122 | */ |
| 123 | export function formatSeconds(seconds: number): string { |
| 124 | const hours = Math.floor(seconds / 3600) |
| 125 | const minutes = Math.floor((seconds % 3600) / 60) |
| 126 | const secs = seconds % 60 |
| 127 | const padNumber = (value: number) => value.toString().padStart(2, '0') |
| 128 | |
| 129 | return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(secs)}` |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * 根据输入数值返回 k/w 简写描述。 |
| 134 | */ |
| 135 | export function describeNumber(value: number): string { |
| 136 | if (!value) |
| 137 | return `${value ?? 0}` |
| 138 | |
| 139 | if (value >= 10000) |
| 140 | return `${(value / 10000).toFixed(2).replace(/\.?0+$/, '')}w` |
| 141 | |
| 142 | if (value >= 1000) |
| 143 | return `${(value / 1000).toFixed(2).replace(/\.?0+$/, '')}k` |
| 144 | |
| 145 | return value.toString() |
| 146 | } |
| 147 |