feat: 移动端设备信息最大化采集(型号/UA-CH/视口/方向/触点/内存/网络/深浅色)

SDK:buildEnvContext 同步扩展 12 键(dpr/viewport/orientation/maxTouchPoints/
deviceMemory/languages/colorScheme/standalone/network downlink·rtt·saveData/
微信版本),逐键 try/catch 静默缺则不挂;新增 collectHighEntropyHints(UA-CH
model/platformVersion/architecture/bitness),init 后 Promise.race 2s 超时
异步并入 __ctk_ctx——批次 context flush 时读取,不阻塞首屏。

后端:Visitor 加 device_model/env_extra(迁移 0005);ingest 首批快照
EXTENDED_ENV_KEYS 白名单(值截 100),非首批不覆盖、唯 device_model 空且
本批有值时补填一次(晚到 hints);列表/详情 API 随行输出。

前端:详情访问环境卡设备行升级显示型号(手机 · Pixel 7),追加像素比/
视口/屏幕方向/触点/内存/网络质量/深浅色/微信版本行(env_extra 逐键判空,
旧数据整块不显示);列表设备列主行拼型号(手机·Pixel 7)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-16 19:12:20 +08:00
parent 7f2a746f66
commit 22d89a3f42
14 changed files with 484 additions and 14 deletions
+4
View File
@@ -18,6 +18,8 @@ export interface VisitorRow {
province: string
city: string
device_type: string
/** 设备型号(UA-CH model,如 Pixel 7;未采集/晚到前为空串) */
device_model: string
os: string
browser: string
language: string
@@ -99,6 +101,8 @@ export interface AttributionBlock {
export interface VisitorDetail extends VisitorRow {
screen: string
timezone_name: string
/** 扩展环境快照(EXTENDED_ENV_KEYS 白名单键,值均为字符串;旧数据为空对象——逐键判空跳行) */
env_extra: Record<string, string>
/** GenericIPAddressField(null=True),未采集到时为 null */
last_ip: string | null
first_utm: Record<string, string>
+57 -3
View File
@@ -428,6 +428,7 @@
import {
Back as BackIcon,
Calendar as CalendarIcon,
Cellphone as CellphoneIcon,
ChatDotRound as ChatDotRoundIcon,
Clock as ClockIcon,
Compass as CompassIcon,
@@ -435,9 +436,13 @@
Cpu as CpuIcon,
Document as DocumentIcon,
FullScreen as FullScreenIcon,
Iphone as IphoneIcon,
Lightning as LightningIcon,
Link as LinkIcon,
Location as LocationIcon,
Monitor as MonitorIcon,
Moon as MoonIcon,
Sunny as SunnyIcon,
Timer as TimerIcon,
User as UserIcon,
VideoPlay as VideoPlayIcon
@@ -617,14 +622,62 @@
{ label: '原始提问', value: searchTermText.value }
])
// ---------- 访问环境(复用档案字段;空值显示 —) ----------
// ---------- 访问环境(档案字段 + 扩展环境快照;空值显示 —,扩展键逐键判空跳行) ----------
/** env_extra 单键取值(空串/缺键 → undefined,对应行跳过——旧数据无扩展键时整块不显示) */
const extra = (key: string): string | undefined => {
const v = detail.value?.env_extra?.[key]
return v ? v : undefined
}
/** orientation 归一显示:portrait* → 竖屏 / landscape* → 横屏(其余原样) */
const formatOrientation = (v: string): string =>
v.startsWith('portrait') ? '竖屏' : v.startsWith('landscape') ? '横屏' : v
/** 网络质量组合值:downlink Mbps + rtt ms + 省流标记(各自存在才拼入;全缺 → undefined 跳行) */
const networkQuality = (): string | undefined => {
const parts: string[] = []
if (extra('network_downlink')) parts.push(`${extra('network_downlink')} Mbps`)
if (extra('network_rtt')) parts.push(`${extra('network_rtt')} ms`)
if (extra('save_data') === '1') parts.push('省流')
return parts.length ? parts.join(' · ') : undefined
}
/** 扩展行构造:值存在才成行(undefined → null 剔除;fmt 做展示格式化) */
const extendedRow = (
value: string | undefined,
icon: Component,
label: string,
fmt: (v: string) => string = (v) => v
): { icon: Component; label: string; value: string } | null =>
value ? { icon: markRaw(icon), label, value: fmt(value) } : null
const envRows = computed<{ icon: Component; label: string; value: string }[]>(() => {
const d = detail.value
const location = d ? [d.country, d.province, d.city].filter(Boolean).join(' · ') : ''
// 设备行升级:有型号时拼 `手机 · Pixel 7`(device_model 是独立列——hints 晚到补填也生效)
const type = d?.device_type || '—'
const deviceValue = d?.device_model ? `${type} · ${d.device_model}` : type
const dark = extra('color_scheme') === 'dark'
// 扩展行只显示存在的键:旧数据 env_extra 为空对象时整块不显示
const extended = [
extendedRow(extra('device_pixel_ratio'), FullScreenIcon, '像素比'),
extendedRow(extra('viewport'), MonitorIcon, '视口'),
extendedRow(extra('orientation'), CellphoneIcon, '屏幕方向', formatOrientation),
extendedRow(extra('max_touch_points'), IphoneIcon, '触点'),
extendedRow(extra('device_memory'), CpuIcon, '内存', (v) => `${v} GB`),
extendedRow(networkQuality(), LightningIcon, '网络质量'),
extendedRow(
extra('color_scheme'),
dark ? MoonIcon : SunnyIcon,
'深浅色',
() => (dark ? '深色' : '浅色')
),
extendedRow(extra('wechat_version'), ChatDotRoundIcon, '微信版本')
].filter((r): r is { icon: Component; label: string; value: string } => r !== null)
return [
{ icon: markRaw(CompassIcon), label: '浏览器', value: d?.browser || '—' },
{ icon: markRaw(CpuIcon), label: '操作系统', value: d?.os || '—' },
{ icon: markRaw(MonitorIcon), label: '设备类型', value: d?.device_type || '—' },
{ icon: markRaw(MonitorIcon), label: '设备类型', value: deviceValue },
{ icon: markRaw(FullScreenIcon), label: '分辨率', value: d?.screen || '—' },
{
icon: markRaw(LocationIcon),
@@ -633,7 +686,8 @@
},
{ icon: markRaw(ChatDotRoundIcon), label: '语言', value: d?.language || '—' },
{ icon: markRaw(ClockIcon), label: '时区', value: d?.timezone_name || '—' },
{ icon: markRaw(ConnectionIcon), label: 'IP', value: d?.last_ip || '—' }
{ icon: markRaw(ConnectionIcon), label: 'IP', value: d?.last_ip || '—' },
...extended
]
})
+5 -1
View File
@@ -230,8 +230,12 @@
const renderDevice = (row: VisitorRow) => {
const env = [row.os, row.browser].filter(Boolean).join(' · ')
// 有型号时主行拼 `手机·Pixel 7`(UA-CH model,未采集/晚到前只有设备类型)
const type = row.device_model
? `${row.device_type || '—'}·${row.device_model}`
: row.device_type || '—'
return h('div', null, [
h('div', { class: 'whitespace-nowrap' }, row.device_type || '—'),
h('div', { class: 'whitespace-nowrap' }, type),
env ? h('div', { class: 'text-xs text-g-500 whitespace-nowrap mt-0.5' }, env) : null
])
}