8fde1d8597
MessageBubble.vue was keys off the (incomplete) type code, so many messages with real OSS media URLs (https://wx.oss.yunkecn.com/.../*.webp|*.mp4) showed as '[未知消息]'. Rendering is now URL-driven: pick the best http URL from file_url > file_hd_url > file_th_url, then render by extension (image/video/audio/file). Type code is only used as a last-resort label. - MessageBubble.vue: URL-driven cascade (URL by ext → duration → transcription → text → structured-type placeholder → localized tag). Image/video/audio/file children @click.stop so the all-fields drawer still opens on bubble click but not on media interactions. - format.ts: wechatTypeLabel(type_raw) maps known codes (3→图片, 34→语音, 43→图片/表情, 49→文件/名片, 42→名片, 9→表情, 20→红包/转账). - ChatTimeline.vue drawer type tag uses wechatTypeLabel for WeChat msgs. - style.css: compact .yk-msg__audio player for voice-with-URL messages. Verified against smoke.db: 25 type-3 images + 3 type-43 videos that previously showed '[未知消息]' now render as images/video. Build clean. Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
/** Formatting helpers for chat timeline + customer list. */
|
|
|
|
/** ms-epoch → `YYYY-MM-DD HH:mm:ss` in local time. `--` for missing/invalid. */
|
|
export function formatTime(ts?: number | null): string {
|
|
if (!ts || !Number.isFinite(ts) || ts <= 0) return '--'
|
|
const d = new Date(ts)
|
|
if (Number.isNaN(d.getTime())) return '--'
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
return (
|
|
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
)
|
|
}
|
|
|
|
/** ms-epoch → short `MM-DD HH:mm` for dense timelines. */
|
|
export function formatTimeShort(ts?: number | null): string {
|
|
if (!ts || !Number.isFinite(ts) || ts <= 0) return ''
|
|
const d = new Date(ts)
|
|
if (Number.isNaN(d.getTime())) return ''
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
}
|
|
|
|
/**
|
|
* For voice messages the backend stores duration in seconds (best-effort).
|
|
* Render as `N"` or `M'SS"` to mirror the messaging-app look.
|
|
*/
|
|
export function formatDuration(seconds?: number | null): string {
|
|
if (!seconds || !Number.isFinite(seconds) || seconds <= 0) return ''
|
|
const s = Math.round(seconds)
|
|
if (s < 60) return `${s}"`
|
|
const m = Math.floor(s / 60)
|
|
const rem = s % 60
|
|
return `${m}'${String(rem).padStart(2, '0')}"`
|
|
}
|
|
|
|
/** Friendly label for a Channel value. */
|
|
export function channelLabel(channel?: string | null): string {
|
|
switch (channel) {
|
|
case 'wechat':
|
|
return '微信'
|
|
case 'whatsapp':
|
|
return 'WhatsApp'
|
|
default:
|
|
return channel ?? '未知'
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WeChat message type labels
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 中文 label for the upstream WeChat `type_raw` numeric code. The label is
|
|
* best-effort — different upstream sources disagree on some codes (e.g. 43 is
|
|
* variously 图片/表情/视频). Rendering is URL-driven (see MessageBubble.vue),
|
|
* so this label only informs the unknown-fallback bubble and the message
|
|
* detail drawer tag — never the actual media display.
|
|
*
|
|
* WhatsApp type codes (0→文字, 1→图片) are intentionally NOT mapped here:
|
|
* the structured `type` field already classifies them, and WA media uses a
|
|
* `mediaName` filename rather than an http URL, so it can't render without the
|
|
* download endpoint and degrades to a placeholder either way.
|
|
*/
|
|
const WECHAT_TYPE_LABELS: Record<number, string> = {
|
|
3: '图片',
|
|
34: '语音',
|
|
42: '名片',
|
|
43: '图片/表情',
|
|
49: '文件/名片',
|
|
9: '表情',
|
|
20: '红包/转账',
|
|
}
|
|
|
|
/** 中文 label for a WeChat type_raw code; `未知(typeRaw)` when unknown. */
|
|
export function wechatTypeLabel(typeRaw?: number | null): string {
|
|
if (typeRaw == null) return '未知'
|
|
return WECHAT_TYPE_LABELS[typeRaw] ?? `未知(${typeRaw})`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phone / voice call rendering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Call-type (PhoneCall.channel) label — 电话 vs 语音通话. */
|
|
export function callTypeLabel(channel?: string | null): string {
|
|
switch (channel) {
|
|
case 'phone_call':
|
|
return '电话'
|
|
case 'voice_call':
|
|
return '语音通话'
|
|
default:
|
|
return channel ?? '未知'
|
|
}
|
|
}
|
|
|
|
/** Call direction. ``incoming_call``: 1 = 呼入 (incoming), 0 = 呼出 (outgoing). */
|
|
export function callDirectionLabel(incoming?: number | null): string {
|
|
if (incoming == null) return '--'
|
|
return incoming === 1 ? '呼入' : '呼出'
|
|
}
|
|
|
|
/**
|
|
* Call status. The upstream schema uses 1 = 已接通 (answered/connected) and
|
|
* 0 = 未接通 (missed/failed); other codes fall back to the raw value so the UI
|
|
* never hides information.
|
|
*/
|
|
export function callStatusLabel(status?: number | null): string {
|
|
if (status == null) return '--'
|
|
switch (status) {
|
|
case 1:
|
|
return '已接通'
|
|
case 0:
|
|
return '未接通'
|
|
default:
|
|
return `状态 ${status}`
|
|
}
|
|
}
|
|
|
|
/** Render call_duration (seconds) as `N"s` / `M'SS"`; ``--`` when missing. */
|
|
export function formatCallDuration(seconds?: number | null): string {
|
|
if (!seconds || !Number.isFinite(seconds) || seconds <= 0) return '--'
|
|
return formatDuration(seconds)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SMS rendering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* SMS direction. Conventions: ``direction`` 1 = 发送 (outbound, staff →
|
|
* customer), 0 = 接收 (inbound). Other values fall back to the raw code.
|
|
*/
|
|
export function smsDirectionLabel(direction?: number | null): string {
|
|
if (direction == null) return '--'
|
|
switch (direction) {
|
|
case 1:
|
|
return '发送'
|
|
case 0:
|
|
return '接收'
|
|
default:
|
|
return `方向 ${direction}`
|
|
}
|
|
}
|