類似微博、微信的相對時間顯示 (「3分鐘前」、「昨天 10:23」) 能顯著提升用戶體驗。本文提供生產就緒的 PHP 實現,含邊界處理、時區支援、擴展建議。
<?php
/**
* 格式化友好相對時間
* @param int $timestamp Unix 時間戳
* @param string $timezone 時區 (預設 UTC)
* @return string
*/
function friendly_time($timestamp, $timezone = 'UTC') {
// 時區處理
date_default_timezone_set($timezone);
$now = time();
// 修正未來時間
if ($timestamp > $now) $timestamp = $now;
$diff = $now - $timestamp;
// 1. 一分鐘內
if ($diff < 60) {
return $diff . '秒前';
}
// 2. 一小時內
if ($diff < 3600) {
return floor($diff / 60) . '分鐘前';
}
// 3. 今日內 (超過 1 小時)
$todayStart = strtotime('today');
if ($timestamp >= $todayStart) {
return floor($diff / 3600) . '小時前';
}
// 4. 昨天
$yesterdayStart = strtotime('yesterday');
if ($timestamp >= $yesterdayStart) {
return '昨天 ' . date('H:i', $timestamp);
}
// 5. 前天
$beforeYesterdayStart = strtotime('day before yesterday');
if ($timestamp >= $beforeYesterdayStart) {
return '前天 ' . date('H:i', $timestamp);
}
// 6. 今年內 (超過 3 天)
$yearStart = strtotime('January 1');
if ($timestamp >= $yearStart) {
return date('m月d日 H:i', $timestamp);
}
// 7. 更早
return date('Y年m月d日 H:i', $timestamp);
}
// 使用範例
echo friendly_time(strtotime('-30 seconds')); // 30秒前
echo friendly_time(strtotime('-45 minutes')); // 45分鐘前
echo friendly_time(strtotime('-3 hours')); // 3小時前
echo friendly_time(strtotime('yesterday 14:30')); // 昨天 14:30
echo friendly_time(strtotime('2 days ago 09:15')); // 前天 09:15
echo friendly_time(strtotime('2023-05-20 10:00')); // 05月20日 10:00
echo friendly_time(strtotime('2020-01-01 00:00')); // 2020年01月01日 00:00
class TimeFormatter {
private $locale = 'zh_CN';
public function __construct($locale = 'zh_CN') {
$this->locale = $locale;
}
public function format($timestamp, $options = []) {
$now = time();
$diff = $now - $timestamp;
$thresholds = [
'just_now' => 5, // 幾秒內顯示"剛剛"
'seconds' => 60,
'minutes' => 3600,
'hours' => 86400, // 1天
'yesterday' => 172800, // 2天
'days' => 604800, // 7天
'weeks' => 2592000, // 30天
];
if ($diff < $thresholds['just_now']) {
return $this->t('just_now');
}
if ($diff < $thresholds['seconds']) {
return $this->plural($diff, 'seconds');
}
if ($diff < $thresholds['minutes']) {
return $this->plural(floor($diff/60), 'minutes');
}
if ($diff < $thresholds['hours']) {
return $this->plural(floor($diff/3600), 'hours');
}
if ($diff < $thresholds['yesterday']) {
return $this->t('yesterday') . ' ' . date('H:i', $timestamp);
}
if ($diff < $thresholds['days']) {
return $this->plural(floor($diff/86400), 'days');
}
if ($diff < $thresholds['weeks']) {
return $this->plural(floor($diff/604800), 'weeks');
}
// 超過閾值顯示完整日期
return date($options['format'] ?? 'Y-m-d H:i', $timestamp);
}
private function plural($count, $unit) {
$map = [
'zh_CN' => ['seconds'=>'%d秒前', 'minutes'=>'%d分鐘前', 'hours'=>'%d小時前', 'days'=>'%d天前', 'weeks'=>'%d周前'],
'en_US' => ['seconds'=>'%d sec ago', 'minutes'=>'%d min ago', 'hours'=>'%d hours ago', 'days'=>'%d days ago', 'weeks'=>'%d weeks ago'],
];
return sprintf($map[$this->locale][$unit] ?? $map['zh_CN'][$unit], $count);
}
private function t($key) {
$map = ['zh_CN'=>['just_now'=>'剛剛', 'yesterday'=>'昨天'], 'en_US'=>['just_now'=>'just now', 'yesterday'=>'yesterday']];
return $map[$this->locale][$key] ?? $key;
}
}
<!-- HTML -->
<time class="friendly-time" data-timestamp="1699833600">載入中...</time>
<script>
// 頁面載入後每分鐘更新一次
document.querySelectorAll('.friendly-time').forEach(el => {
const ts = parseInt(el.dataset.timestamp);
const update = () => {
const diff = Math.floor(Date.now()/1000) - ts;
let text = '';
if (diff < 60) text = diff + '秒前';
else if (diff < 3600) text = Math.floor(diff/60) + '分鐘前';
else if (diff < 86400) text = Math.floor(diff/3600) + '小時前';
else text = new Date(ts*1000).toLocaleString();
el.textContent = text;
};
update();
setInterval(update, 60000);
});
</script>
| 問題 | 解決 |
|---|---|
| 時區不一致 | 統一用 UTC 存儲,顯示時按用戶時區轉換 |
| 未來時間顯示異常 | 修正 $timestamp = min($timestamp, time()) |
| 閏秒/夏令時 | 使用 DateTime/DateTimeZone 類處理複雜時區 |
| 緩存導致時間不更新 | 前端 JS 實時更新或緩存鍵包含時間版本 |
友好時間顯示核心邏輯簡單,關鍵在於邊界條件處理、時區統一、前端實時更新。建議封裝為工具類,配合前端 JS 實現最佳體驗。國際化專案可引入 IntlRelativeTimeFormat (JS) 或 PHP DateInterval 進階格式化。