feat: vue3 frontend scaffold + org tree + customer list + chat timeline
M5b viewing frontend for the yunke data-mirror, under yunkes/. - Scaffold: Vite + Vue 3 + TS, with element-plus, @element-plus/icons-vue, pinia, vue-router@4, axios. Dev server on :5173 with /api and /api/media proxied to the FastAPI backend on :8000. - API client (src/api/): axios instance (baseURL '/api') whose response interceptor unwraps the axios envelope; typed wrappers (getCustomers, getMessages, getDepartments, getStaff) returning Page<T> / arrays; types mirroring the backend *Out schemas (Message typed as a permissive superset so file_th_url/duration/transcription_text degrade gracefully today and light up once MessageOut exposes them). - Layout: el-header top bar (title + channel switch) + el-aside department tree (built from the flat parent_id list via useOrgStore, with search filter) + el-main router-view. - CustomerList (/customers): el-table with channel filter, department filter driven by the tree selection (route query), pagination, and row-click into the chat timeline (channel + owner_account_id + talker_id). - ChatTimeline (/chat/:channel/:ownerId/:talkerId): asc-ordered message bubbles keyed by is_send (direction) and type (text/image/video/voice/file/ divider/system/unknown); lands on the most-recent page and prepends older pages via load-more with scroll anchoring. - Home '/' redirects to '/customers'. npm run build (vue-tsc -b + vite build) is clean. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/** 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 ?? '未知'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user