feat(admin): 访客列表页
- 访客管理页:useTable 分页表(后端固定 20/页,paginationKey 映射 page, size 不发送),列含 截断 ID+复制 / 国旗 emoji+国家·省市 / 设备主行+os·browser 小字 / 首访·最近访问(共享 formatDateTime,上海时间直显)/ 访问次数 / 活跃天数 / 总停留 formatStay / 首触·最近来源 / 在线绿点;行点击进详情路由 - 筛选栏:关键词(visitor_id/IP 前缀)回车或查询触发、设备下拉、国家精确、 在线开关(online='1' 哨兵)、日期范围 date_from/date_to、显示爬虫开关 (默认排除);筛选变化重置 page=1,分页切换保留筛选 - 共享工具提取 admin/src/utils/tracker:formatDateTime/formatStay/countryFlag, site 页改引用;formatStay 智能省略(0 单位不显示,全 0 显示 0秒) - 红线:无 v-html,访客数据不绑 href/src;加载/空态齐全(错误态走请求层统一 toast) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 访客统计共享格式化工具(列表/详情页复用)
|
||||
*
|
||||
* 后端时间戳已统一 timezone.localtime().isoformat()(上海时间)序列化,
|
||||
* 前端切串直显即可,不做时区换算。
|
||||
*/
|
||||
|
||||
/** 国家 → 国旗 emoji(ip2region 中国 IP 为中文"中国",海外为英文名;未命中默认 🌐) */
|
||||
const COUNTRY_FLAGS: Record<string, string> = {
|
||||
中国: '🇨🇳',
|
||||
'United States': '🇺🇸',
|
||||
Japan: '🇯🇵',
|
||||
'South Korea': '🇰🇷',
|
||||
Singapore: '🇸🇬',
|
||||
'United Kingdom': '🇬🇧',
|
||||
Germany: '🇩🇪',
|
||||
France: '🇫🇷',
|
||||
Canada: '🇨🇦',
|
||||
Australia: '🇦🇺',
|
||||
Russia: '🇷🇺',
|
||||
India: '🇮🇳',
|
||||
Brazil: '🇧🇷',
|
||||
Vietnam: '🇻🇳',
|
||||
Thailand: '🇹🇭',
|
||||
Malaysia: '🇲🇾',
|
||||
Indonesia: '🇮🇩',
|
||||
Philippines: '🇵🇭',
|
||||
Netherlands: '🇳🇱',
|
||||
Italy: '🇮🇹',
|
||||
Spain: '🇪🇸',
|
||||
Turkey: '🇹🇷',
|
||||
Iran: '🇮🇷'
|
||||
}
|
||||
|
||||
/** 国家名 → 国旗 emoji(空/未命中返回 🌐) */
|
||||
export const countryFlag = (country?: string | null): string =>
|
||||
(country && COUNTRY_FLAGS[country]) || '🌐'
|
||||
|
||||
/** ISO 时间 → YYYY-MM-DD HH:mm(后端已是上海时间;空值显示 —) */
|
||||
export const formatDateTime = (iso?: string | null): string =>
|
||||
iso ? iso.replace('T', ' ').slice(0, 16) : '—'
|
||||
|
||||
/** 毫秒 → x天x小时x分x秒(智能省略:为 0 的单位不显示;全 0 / 不足 1 秒显示 0秒) */
|
||||
export const formatStay = (ms?: number | null): string => {
|
||||
const totalSec = Math.max(0, Math.floor((ms ?? 0) / 1000))
|
||||
const days = Math.floor(totalSec / 86400)
|
||||
const hours = Math.floor((totalSec % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSec % 3600) / 60)
|
||||
const seconds = totalSec % 60
|
||||
const parts: string[] = []
|
||||
if (days) parts.push(`${days}天`)
|
||||
if (hours) parts.push(`${hours}小时`)
|
||||
if (minutes) parts.push(`${minutes}分`)
|
||||
// 秒恒显示(含 0),避免"0秒/不足 1 秒"整行变空
|
||||
if (seconds || !parts.length) parts.push(`${seconds}秒`)
|
||||
return parts.join('')
|
||||
}
|
||||
Reference in New Issue
Block a user