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:
@@ -0,0 +1,217 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getSms } from '@/api'
|
||||
import type { Sms } from '@/api/types'
|
||||
import { formatTime, smsDirectionLabel } from '@/utils/format'
|
||||
|
||||
const list = ref<Sms[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
// Detail dialog state — shows the full SMS body (vs the truncated preview in
|
||||
// the table).
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<Sms | null>(null)
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
errorMsg.value = null
|
||||
try {
|
||||
const res = await getSms({
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
list.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()
|
||||
}
|
||||
|
||||
function openDetail(row: Sms): void {
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="yk-sms">
|
||||
<div class="yk-sms__header">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 4px; font-size: 16px">短信记录</h2>
|
||||
<span class="yk-sms__sub">共 {{ total }} 条</span>
|
||||
</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="list"
|
||||
stripe
|
||||
border
|
||||
row-key="sms_id"
|
||||
style="width: 100%"
|
||||
:row-class-name="() => 'yk-clickable-row'"
|
||||
empty-text="暂无短信记录"
|
||||
@row-click="openDetail"
|
||||
>
|
||||
<el-table-column label="时间" width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="员工" min-width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.receive_name || row.user_id || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户号码" min-width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.customer_phone || row.sender || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">{{ smsDirectionLabel(row.direction) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内容预览" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.content_preview">{{ row.content_preview }}</span>
|
||||
<span v-else-if="row.content">{{ row.content }}</span>
|
||||
<span v-else style="color: #c0c4cc">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="yk-sms__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-sms__hint">点击任意一行查看完整短信内容。</p>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="短信详情"
|
||||
width="520px"
|
||||
align-center
|
||||
>
|
||||
<div v-if="detailRow" class="yk-sms__detail">
|
||||
<div class="yk-sms__detail-row">
|
||||
<span class="yk-sms__detail-label">时间</span>
|
||||
<span>{{ formatTime(detailRow.create_time) }}</span>
|
||||
</div>
|
||||
<div class="yk-sms__detail-row">
|
||||
<span class="yk-sms__detail-label">员工</span>
|
||||
<span>{{ detailRow.receive_name || detailRow.user_id || '--' }}</span>
|
||||
</div>
|
||||
<div class="yk-sms__detail-row">
|
||||
<span class="yk-sms__detail-label">客户号码</span>
|
||||
<span>{{ detailRow.customer_phone || '--' }}</span>
|
||||
</div>
|
||||
<div class="yk-sms__detail-row">
|
||||
<span class="yk-sms__detail-label">发送方</span>
|
||||
<span>{{ detailRow.sender || '--' }}</span>
|
||||
</div>
|
||||
<div class="yk-sms__detail-row">
|
||||
<span class="yk-sms__detail-label">方向</span>
|
||||
<span>{{ smsDirectionLabel(detailRow.direction) }}</span>
|
||||
</div>
|
||||
<div class="yk-sms__detail-row yk-sms__detail-row--top">
|
||||
<span class="yk-sms__detail-label">内容</span>
|
||||
<span class="yk-sms__detail-content">{{
|
||||
detailRow.content || detailRow.content_preview || '(空)'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.yk-sms__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.yk-sms__sub {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yk-sms__pager {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.yk-sms__hint {
|
||||
margin: 8px 2px 0;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.yk-sms__detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yk-sms__detail-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.yk-sms__detail-row--top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.yk-sms__detail-label {
|
||||
flex: 0 0 64px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.yk-sms__detail-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user