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,54 @@
|
||||
import type { Department } from '@/api/types'
|
||||
|
||||
/**
|
||||
* Tree node consumed by `<el-tree :data>`. Element Plus looks at `label` for
|
||||
* the text and `children` for sub-nodes (both configurable via props).
|
||||
*/
|
||||
export interface DeptTreeNode {
|
||||
dept_id: string
|
||||
label: string
|
||||
raw: Department
|
||||
children: DeptTreeNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a department forest from the backend's flat `parent_id` list.
|
||||
*
|
||||
* - Roots = departments with no `parent_id` (or whose parent is missing).
|
||||
* - Children are sorted by `path` then `dept_id` to mirror the backend ordering
|
||||
* (the API already returns ordered by path, but orphans re-ordering is cheap
|
||||
* insurance).
|
||||
* - Falls back gracefully on cycles / dangling parent_ids (orphans become roots
|
||||
* rather than disappearing).
|
||||
*/
|
||||
export function buildDeptTree(departments: Department[]): DeptTreeNode[] {
|
||||
const nodes = new Map<string, DeptTreeNode>()
|
||||
for (const d of departments) {
|
||||
nodes.set(d.dept_id, {
|
||||
dept_id: d.dept_id,
|
||||
label: d.name?.trim() || d.dept_id,
|
||||
raw: d,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
|
||||
const roots: DeptTreeNode[] = []
|
||||
for (const d of departments) {
|
||||
const node = nodes.get(d.dept_id)!
|
||||
const parent = d.parent_id ? nodes.get(d.parent_id) : undefined
|
||||
if (parent) {
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
const sortRec = (list: DeptTreeNode[]) => {
|
||||
list.sort((a, b) =>
|
||||
(a.raw.path ?? a.dept_id).localeCompare(b.raw.path ?? b.dept_id),
|
||||
)
|
||||
for (const n of list) sortRec(n.children)
|
||||
}
|
||||
sortRec(roots)
|
||||
return roots
|
||||
}
|
||||
Reference in New Issue
Block a user