Files
yunke/yunkes/src/views/GroupList.vue
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

223 lines
5.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getGroups } from '@/api'
import type { Channel, Group } from '@/api/types'
import { useChannelStore } from '@/stores/channel'
import { channelLabel, formatTime } from '@/utils/format'
const router = useRouter()
const channelStore = useChannelStore()
const groups = ref<Group[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const errorMsg = ref<string | null>(null)
const channelOptions = [
{ value: 'all' as const, label: '全部渠道' },
{ value: 'wechat' as const, label: '微信' },
{ value: 'whatsapp' as const, label: 'WhatsApp' },
]
const channelLabelCurrent = computed(() =>
channelLabel(channelStore.channel === 'all' ? null : channelStore.channel),
)
function channelTagType(c: Group['channel']) {
return c === 'wechat' ? 'success' : 'warning'
}
function onChannelChange(value: string | number | boolean | undefined): void {
channelStore.setChannel(value as Channel | 'all')
}
async function load(): Promise<void> {
loading.value = true
errorMsg.value = null
try {
const res = await getGroups({
channel: channelStore.channel,
page: page.value,
page_size: pageSize.value,
})
groups.value = res.items
total.value = res.total
} catch (e) {
errorMsg.value = (e as Error).message
ElMessage.error(`群列表加载失败:${(e as Error).message}`)
} finally {
loading.value = false
}
}
function handlePageChange(p: number): void {
page.value = p
load()
}
function handlePageSizeChange(s: number): void {
pageSize.value = s
page.value = 1
load()
}
// React to the top-bar channel filter.
watch(
() => channelStore.channel,
() => {
page.value = 1
load()
},
)
function openTimeline(row: Group): void {
// Group messages are queried by room_id; the timeline view branches on the
// presence of a `roomId` route param to issue a room_id query.
if (!row.owner_account_id) {
ElMessage.warning('该群缺少归属账号,无法查看聊天记录')
return
}
router.push({
name: 'group-chat',
params: {
channel: row.channel,
ownerId: row.owner_account_id,
roomId: row.room_id,
},
})
}
onMounted(load)
</script>
<template>
<div class="yk-groups">
<div class="yk-groups__header">
<div>
<h2 style="margin: 0 0 4px; font-size: 16px">群列表</h2>
<span class="yk-groups__sub">
共 {{ total }} 条 · 渠道:{{ channelLabelCurrent }}
</span>
</div>
<div class="yk-groups__filters">
<el-radio-group
:model-value="channelStore.channel"
size="small"
@change="onChannelChange"
>
<el-radio-button
v-for="opt in channelOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</el-radio-button>
</el-radio-group>
</div>
</div>
<el-alert
v-if="errorMsg"
:title="errorMsg"
type="error"
show-icon
:closable="false"
style="margin-bottom: 12px"
/>
<el-table
v-loading="loading"
:data="groups"
stripe
border
row-key="room_id"
style="width: 100%"
:row-class-name="() => 'yk-clickable-row'"
empty-text="暂无群数据"
@row-click="openTimeline"
>
<el-table-column label="群名" min-width="180">
<template #default="{ row }">
{{ row.room_name || row.room_id }}
</template>
</el-table-column>
<el-table-column label="渠道" width="100">
<template #default="{ row }">
<el-tag :type="channelTagType(row.channel)" size="small" effect="plain">
{{ channelLabel(row.channel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="成员数" prop="member_count" width="90" align="right">
<template #default="{ row }">{{ row.member_count ?? '--' }}</template>
</el-table-column>
<el-table-column label="消息数" prop="message_count" width="90" align="right">
<template #default="{ row }">{{ row.message_count ?? '--' }}</template>
</el-table-column>
<el-table-column label="群主" min-width="140">
<template #default="{ row }">
<span v-if="row.owner_account_nickname">{{ row.owner_account_nickname }}</span>
<span v-else-if="row.owner_account_id">{{ row.owner_account_id }}</span>
<span v-else style="color: #c0c4cc">--</span>
</template>
</el-table-column>
<el-table-column label="最近聊天" width="160">
<template #default="{ row }">{{ formatTime(row.last_chat_time) }}</template>
</el-table-column>
</el-table>
<div class="yk-groups__pager">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@current-change="handlePageChange"
@size-change="handlePageSizeChange"
/>
</div>
<p class="yk-groups__hint">点击任意一行查看该群的聊天记录。</p>
</div>
</template>
<style scoped>
.yk-groups__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.yk-groups__sub {
color: #909399;
font-size: 13px;
}
.yk-groups__filters {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.yk-groups__pager {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.yk-groups__hint {
margin: 8px 2px 0;
font-size: 12px;
color: #c0c4cc;
}
</style>