Files
yunke/yunkes/src/utils/format.ts
T
浅念 b822e87d4d feat: remaining frontend views (groups/calls/sms/sync) + messageout media fields
Backend (Part A):
- MessageOut: add file_th_url, file_hd_url, duration, transcription_text, md5
  (room_id + sender_head_url already existed) so the chat timeline can render
  image thumbnails, voice duration/transcription, and avatars from a single
  response.
- PhoneCallOut: add record_file so the Calls view can offer recording playback.
- SmsOut: add content (full body) so the SMS detail dialog shows the whole text.
- Tests: red->green for each new field (media fields default None + populated,
  record_file round-trip, content round-trip). Suite 267 -> 271.

Frontend (Part B):
- api/index.ts: wrappers getGroups/getGroupMembers/getCalls/getSms/getSyncStatus
  + Group/GroupMember/PhoneCall/Sms/SyncState/SyncRun/SyncStatus types in
  types.ts (mirror the *Out pydantic schemas).
- views/GroupList.vue (/groups): table + channel filter + pagination; row click
  opens the group timeline.
- views/CallList.vue (/calls): table + call-type filter (phone_call/voice_call/
  all); "播放录音" action opens an el-dialog with <audio controls>.
- views/SmsList.vue (/sms): table; row click opens a detail dialog with the full
  content.
- views/SyncStatus.vue (/sync): two tables (states + recent_runs) with a manual
  refresh; run status color-coded.
- ChatTimeline.vue: reused for both 1:1 DM (talkerId) and group (roomId) by
  branching the query; group title + per-member sender already handled by
  MessageBubble.
- router: add /groups /calls /sms /sync + /group/:channel/:ownerId/:roomId.
- layouts/MainLayout.vue: replace the single 客户列表 button with an el-menu
  (客户 / 群 / 通话 / 短信 / 同步); org tree kept on the left.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 16:38:47 +08:00

113 lines
3.6 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 ?? '未知'
}
}
// ---------------------------------------------------------------------------
// 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}`
}
}