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>
This commit is contained in:
浅念
2026-08-01 16:38:47 +08:00
parent 6c4cd92ae6
commit b822e87d4d
14 changed files with 1401 additions and 36 deletions
+53 -8
View File
@@ -21,6 +21,18 @@ const selectedDeptId = computed<string | null>(
() => (route.query.department_id as string | undefined) ?? null,
)
// `el-menu` with `router` mode emits when an item is clicked; we drive
// `default-active` from the current path so the active item highlights on load
// and on browser back/forward. Group/DM chat routes don't appear in the menu —
// the active item falls back to none there, which is fine.
const activeMenu = computed(() => {
// Map chat timeline routes back to their entry list so the nav reflects the
// "owning" section while a conversation is open.
if (route.name === 'chat') return '/customers'
if (route.name === 'group-chat') return '/groups'
return route.path
})
const treeProps = { label: 'label', children: 'children' }
// el-tree's `filter-node-method` is called for every node on each keystroke.
@@ -53,10 +65,6 @@ function onChannelChange(value: string | number | boolean | undefined): void {
channelStore.setChannel(value as Channel | 'all')
}
function goCustomers(): void {
router.push({ name: 'customers' })
}
async function reloadTree(): Promise<void> {
try {
await org.ensureLoaded(true)
@@ -100,10 +108,34 @@ onMounted(async () => {
</el-radio-button>
</el-radio-group>
<span class="yk-header__spacer" />
<el-button text @click="goCustomers">
<el-icon><ChatLineSquare /></el-icon>
<span style="margin-left: 4px">客户列表</span>
</el-button>
<el-menu
:default-active="activeMenu"
mode="horizontal"
:ellipsis="false"
class="yk-header__nav"
router
>
<el-menu-item index="/customers">
<el-icon><ChatLineSquare /></el-icon>
<span>客户</span>
</el-menu-item>
<el-menu-item index="/groups">
<el-icon><User /></el-icon>
<span>群</span>
</el-menu-item>
<el-menu-item index="/calls">
<el-icon><Phone /></el-icon>
<span>通话</span>
</el-menu-item>
<el-menu-item index="/sms">
<el-icon><Message /></el-icon>
<span>短信</span>
</el-menu-item>
<el-menu-item index="/sync">
<el-icon><Refresh /></el-icon>
<span>同步</span>
</el-menu-item>
</el-menu>
</el-header>
<el-container>
@@ -190,4 +222,17 @@ onMounted(async () => {
align-items: center;
font-size: 13px;
}
/* The horizontal nav lives inside the app header, so strip the default menu
* border/background that would otherwise clash with the header's own styling. */
.yk-header__nav.el-menu--horizontal {
border-bottom: none;
background: transparent;
flex: 0 0 auto;
}
.yk-header__nav.el-menu--horizontal :deep(.el-menu-item) {
height: var(--yk-header-height);
line-height: var(--yk-header-height);
}
</style>