/** * Chinese-label map + grouping for the SMS all-fields drawer. * * Mirrors the Customer approach (see customerFields.ts): the 云客 upstream * SMS record (`Sms.raw_json`) carries the camelCase fields the source 短信记录 * page shows (id/customerPhone, sms/smsSub, sender, direction, userId, * senderDepartment, receiveName, createTime, simPhone/simNumber, source). * This module turns that blob into a labeled, grouped, render-ready structure * for the drawer opened by clicking an SMS row. * * Structured columns are preferred over the verbatim raw_json value when both * exist (the structured column is type-normalized on ingest). Unknown keys * (anything not in the label map) fall back to the raw key under 其他, so a * new field added upstream still shows up instead of disappearing. */ import type { Sms } from '@/api/types' import { formatTime } from './format' /** camelCase raw_json key → 中文 label. Missing keys fall back to the key. */ export const SMS_FIELD_LABELS: Record = { // 标识与时间 id: '短信ID', createTime: '时间', // 客户与号码 customerPhone: '客户号码', sender: '发送者', receiveName: '接收者', // 内容 sms: '全文', smsSub: '预览', // 方向与归属 direction: '方向', userId: '员工ID', senderDepartment: '部门', // 设备与来源 simPhone: 'SIM手机', simNumber: 'SIM号', source: '来源', } /** * raw_json camelCase key → the structured snake_case column that mirrors it. * When a structured column is populated we prefer it (type-normalized); we * fall back to the verbatim raw_json value otherwise. */ const STRUCTURED_ALIAS: Record = { id: 'sms_id', customerPhone: 'customer_phone', sms: 'content', smsSub: 'content_preview', sender: 'sender', direction: 'direction', userId: 'user_id', receiveName: 'receive_name', createTime: 'create_time', simPhone: 'sim_phone', } /** Fields whose value is a millisecond epoch → format to local datetime. */ const TIMESTAMP_KEYS = new Set(['createTime']) /** Fields whose value is a direction code (0=接收 / 1=发送). */ const DIRECTION_KEYS = new Set(['direction']) /** Drawer sections, rendered in this order. */ export interface SmsFieldGroup { title: string /** camelCase raw_json keys to surface in this section. */ keys: string[] } export const SMS_FIELD_GROUPS: SmsFieldGroup[] = [ { title: '标识与时间', keys: ['id', 'createTime'], }, { title: '客户与号码', keys: ['customerPhone', 'sender', 'receiveName'], }, { title: '内容', keys: ['sms', 'smsSub'], }, { title: '方向与归属', keys: ['direction', 'userId', 'senderDepartment'], }, { title: '设备与来源', keys: ['simPhone', 'simNumber', 'source'], }, ] /** Every key already surfaced via a group (so 其他 can skip duplicates). */ const KNOWN_KEYS = new Set(SMS_FIELD_GROUPS.flatMap((g) => g.keys)) /** Resolve the 中文 label for a raw_json key (falls back to the key). */ export function fieldLabel(key: string): string { return SMS_FIELD_LABELS[key] ?? key } /** * Read a field value with structured-column preference. * Returns the verbatim raw_json value when no structured column maps. */ export function readField(s: Sms, camelKey: string): unknown { const alias = STRUCTURED_ALIAS[camelKey] if (alias) { const v = s[alias] if (v !== null && v !== undefined) return v } return s.raw_json?.[camelKey] } /** direction: 1 = 发送 (outbound), 0 = 接收 (inbound), other values pass through. */ function smsDirectionLabel(v: unknown): string { if (v === 1 || v === '1') return '发送' if (v === 0 || v === '0') return '接收' return String(v) } /** A single field prepared for drawer rendering. */ export interface DisplayField { key: string label: string /** Raw value (for text fallback). */ value: unknown /** How the template should render it. */ render: 'time' | 'direction' | 'text' | 'empty' /** Pre-formatted text (for non-empty kinds). Empty for empty. */ text: string } /** Classify + format a single field for the drawer. */ export function toDisplayField(s: Sms, camelKey: string): DisplayField { const value = readField(s, camelKey) const label = fieldLabel(camelKey) if (value === null || value === undefined || value === '') { return { key: camelKey, label, value, render: 'empty', text: '' } } if (TIMESTAMP_KEYS.has(camelKey) && typeof value === 'number') { return { key: camelKey, label, value, render: 'time', text: formatTime(value) } } if (DIRECTION_KEYS.has(camelKey)) { return { key: camelKey, label, value, render: 'direction', text: smsDirectionLabel(value), } } return { key: camelKey, label, value, render: 'text', text: String(value) } } /** * Build the grouped display model for an SMS row. * * Each known group only includes fields that have a value (empty ones are * dropped to keep the drawer compact). A trailing 其他 section collects every * raw_json key not already surfaced, so new upstream fields show up * automatically instead of being silently hidden. */ export interface SmsFieldSection { title: string fields: DisplayField[] } export function buildSmsSections(s: Sms): SmsFieldSection[] { const sections: SmsFieldSection[] = SMS_FIELD_GROUPS.map((g) => ({ title: g.title, fields: g.keys .map((k) => toDisplayField(s, k)) .filter((f) => f.render !== 'empty'), })) // 其他: every raw_json key not already shown, in original insertion order. const raw = s.raw_json ?? {} const others: DisplayField[] = [] for (const key of Object.keys(raw)) { if (KNOWN_KEYS.has(key)) continue const f = toDisplayField(s, key) if (f.render === 'empty') continue others.push(f) } if (others.length > 0) { sections.push({ title: '其他字段', fields: others }) } return sections.filter((sec) => sec.fields.length > 0) }