feat: full-field display for group/call/sms (raw_json + drawers)

Mirrors the Customer/Message/Staff/Accounts pattern: backend exposes the
verbatim upstream record on the response schema; the frontend list shows
source-matching columns and a row-click opens an all-fields el-drawer with
Chinese-labeled camelCase keys (unknown keys roll up under 其他).

Backend (yunke-server/app/api/schemas.py)
- GroupOut / PhoneCallOut / SmsOut: add raw_json: dict[str, Any] | None
  (Pydantic v2 from_attributes picks up the existing ORM column).
- Tests: extend tests/test_api_{groups,calls,sms}.py with a per-schema
  raw_json round-trip assertion (phone_call + voice_call payload shapes
  both covered for calls).

Frontend (yunkes)
- src/utils/{groupFields,callFields,smsFields}.ts: 中文 label map +
  grouped sections + readField (structured-column preference) +
  toDisplayField (image / time / audio / bool / status / direction).
- src/api/types.ts: add raw_json to Group / PhoneCall / Sms.
- src/views/GroupList.vue: richer columns (群名/群ID/成员数/群主/归属微信/
  消息数/最近聊天/渠道) + all-fields drawer (chatRoomId/chatRoomName,
  memberCount/memberIds, headImageUrl, wechatId/wechatNickName,
  userName/userPhone/userId, messageCount/ownMessageCount,
  lossMembers/addMembers, lastChatTime/updateTime).
- src/views/CallList.vue: keeps existing columns + adds all-fields drawer
  (id/startCallTime, callDuration/callSeconds/ringTime, callStatus/
  incomingCall, userId/userIdName/departmentId, callNumber/simPhone/
  simNumber/imei, recordFile/isUpload/downloadCount/aiAsr/qcStatus/
  planCustomerName/version + voice-call extras ossFileName/duration/
  startTime/endTime/remark). Drawer plays recording inline via audio
  render kind; existing play-record dialog retained.
- src/views/SmsList.vue: keeps existing columns + adds all-fields drawer
  (id/customerPhone, sms/smsSub, sender, direction, userId/
  senderDepartment, receiveName, createTime, simPhone/simNumber, source)
  with a full-body preview block at the top.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-05 10:12:49 +08:00
parent fd86adfb63
commit 0d466c4e4a
11 changed files with 1442 additions and 92 deletions
+162 -4
View File
@@ -10,6 +10,7 @@ import {
formatCallDuration,
formatTime,
} from '@/utils/format'
import { buildCallSections, readField } from '@/utils/callFields'
// Calls aren't tied to a messaging channel — `channel` on a call is the call
// type (phone_call / voice_call). We keep a LOCAL filter so it doesn't fight
@@ -28,6 +29,10 @@ const recordDialogVisible = ref(false)
const recordSrc = ref<string | null>(null)
const recordTitle = ref('')
// All-fields drawer state.
const drawerOpen = ref(false)
const drawerCall = ref<PhoneCall | null>(null)
const typeOptions: { value: CallChannel | 'all'; label: string }[] = [
{ value: 'all', label: '全部类型' },
{ value: 'phone_call', label: '电话' },
@@ -75,18 +80,45 @@ function onTypeChange(value: string | number | boolean | undefined): void {
load()
}
/**
* Read a raw_json camelCase key off a call, preferring the structured
* snake_case column when it is populated. The first argument is the row so the
* table can use the same helper as the drawer.
*/
function cell(row: PhoneCall, camelKey: string): unknown {
return readField(row, camelKey)
}
function timeCell(row: PhoneCall, camelKey: string): number | undefined {
const v = cell(row, camelKey)
return typeof v === 'number' ? v : undefined
}
function playRecord(row: PhoneCall): void {
if (!row.record_file) {
const file =
(typeof cell(row, 'recordFile') === 'string'
? (cell(row, 'recordFile') as string)
: null) ?? row.record_file
if (!file) {
ElMessage.info('该通话无录音文件')
return
}
recordSrc.value = row.record_file
recordSrc.value = file
const who = row.user_id_name || row.user_id || row.call_id
const when = formatTime(row.start_call_time)
recordTitle.value = `录音 · ${who}${when !== '--' ? ` · ${when}` : ''}`
recordDialogVisible.value = true
}
function openDrawer(row: PhoneCall): void {
drawerCall.value = row
drawerOpen.value = true
}
const drawerSections = computed(() =>
drawerCall.value ? buildCallSections(drawerCall.value) : [],
)
onMounted(load)
</script>
@@ -128,10 +160,14 @@ onMounted(load)
border
row-key="call_id"
style="width: 100%"
:row-class-name="() => 'yk-clickable-row'"
empty-text="暂无通话记录"
@row-click="openDrawer"
>
<el-table-column label="时间" width="160">
<template #default="{ row }">{{ formatTime(row.start_call_time) }}</template>
<template #default="{ row }">
{{ formatTime(row.start_call_time ?? timeCell(row, 'startCallTime')) }}
</template>
</el-table-column>
<el-table-column label="员工" min-width="130">
<template #default="{ row }">
@@ -158,7 +194,7 @@ onMounted(load)
<el-table-column label="录音" width="100" align="center">
<template #default="{ row }">
<el-button
v-if="row.record_file"
v-if="row.record_file || cell(row, 'recordFile')"
size="small"
type="primary"
link
@@ -183,7 +219,9 @@ onMounted(load)
@size-change="handlePageSizeChange"
/>
</div>
<p class="yk-calls__hint">点击任意一行查看通话全部字段。</p>
<!-- Recording playback dialog -->
<el-dialog
v-model="recordDialogVisible"
:title="recordTitle"
@@ -204,6 +242,54 @@ onMounted(load)
<a :href="recordSrc" target="_blank" rel="noopener">在新窗口打开录音文件</a>
</div>
</el-dialog>
<!-- All-fields drawer -->
<el-drawer
v-model="drawerOpen"
:title="drawerCall ? `通话详情 · ${drawerCall.user_id_name ?? drawerCall.user_id ?? drawerCall.call_id}` : '通话详情'"
direction="rtl"
size="560px"
>
<template v-if="drawerCall">
<div class="yk-drawer__avatar">
<div class="yk-drawer__avatar-meta">
<div class="yk-drawer__avatar-name">
{{ drawerCall.user_id_name || drawerCall.user_id || drawerCall.call_id }}
</div>
<div class="yk-drawer__avatar-sub">
{{ callTypeLabel(drawerCall.channel) }}
<template v-if="drawerCall.call_number">
· {{ drawerCall.call_number }}
</template>
</div>
</div>
</div>
<div v-for="section in drawerSections" :key="section.title" class="yk-drawer__section">
<el-divider content-position="left">{{ section.title }}</el-divider>
<dl class="yk-drawer__kv">
<div v-for="f in section.fields" :key="f.key" class="yk-drawer__kv-item">
<dt>{{ f.label }}</dt>
<dd v-if="f.render === 'audio'" class="yk-drawer__audio">
<audio
:src="String(f.value)"
controls
preload="metadata"
style="width: 100%; max-width: 320px"
>
您的浏览器不支持音频播放。
</audio>
<a :href="String(f.value)" target="_blank" rel="noopener" class="yk-drawer__audio-link">
打开录音
</a>
</dd>
<dd v-else-if="f.render === 'empty'" class="yk-drawer__empty">—</dd>
<dd v-else>{{ f.text }}</dd>
</div>
</dl>
</div>
</template>
</el-drawer>
</div>
</template>
@@ -240,4 +326,76 @@ onMounted(load)
font-size: 12px;
text-align: center;
}
.yk-calls__hint {
margin: 8px 2px 0;
font-size: 12px;
color: #c0c4cc;
}
.yk-drawer__avatar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.yk-drawer__avatar-meta {
min-width: 0;
}
.yk-drawer__avatar-name {
font-size: 16px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.yk-drawer__avatar-sub {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.yk-drawer__kv {
margin: 0 0 8px;
}
.yk-drawer__kv-item {
display: grid;
grid-template-columns: 110px 1fr;
gap: 8px 12px;
padding: 3px 0;
}
.yk-drawer__kv dt {
color: #909399;
font-size: 13px;
word-break: break-all;
}
.yk-drawer__kv dd {
margin: 0;
font-size: 13px;
color: #303133;
word-break: break-all;
min-width: 0;
}
.yk-drawer__audio {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.yk-drawer__audio-link {
font-size: 12px;
color: var(--el-color-primary, #409eff);
}
.yk-drawer__empty {
color: #c0c4cc;
}
</style>
+226 -22
View File
@@ -6,6 +6,7 @@ import { getGroups } from '@/api'
import type { Channel, Group } from '@/api/types'
import { useChannelStore } from '@/stores/channel'
import { channelLabel, formatTime } from '@/utils/format'
import { buildGroupSections, readField } from '@/utils/groupFields'
const router = useRouter()
const channelStore = useChannelStore()
@@ -17,6 +18,10 @@ const pageSize = ref(20)
const loading = ref(false)
const errorMsg = ref<string | null>(null)
// All-fields drawer state.
const drawerOpen = ref(false)
const drawerGroup = ref<Group | null>(null)
const channelOptions = [
{ value: 'all' as const, label: '全部渠道' },
{ value: 'wechat' as const, label: '微信' },
@@ -74,9 +79,52 @@ watch(
},
)
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.
/**
* Read a raw_json camelCase key off a group, preferring the structured
* snake_case column when it is populated. The first argument is the row so the
* table can use the same helper as the drawer.
*/
function cell(row: Group, camelKey: string): unknown {
return readField(row, camelKey)
}
function cellText(row: Group, camelKey: string): string {
const v = cell(row, camelKey)
return v === null || v === undefined || v === '' ? '--' : String(v)
}
function imageCell(row: Group, camelKey: string): string | undefined {
const v = cell(row, camelKey)
return typeof v === 'string' && v !== '' ? v : undefined
}
function timeCell(row: Group, camelKey: string): number | undefined {
const v = cell(row, camelKey)
return typeof v === 'number' ? v : undefined
}
function openDrawer(row: Group): void {
// Row click opens the all-fields drawer; the timeline is reached from the
// drawer's 查看聊天记录 action (preserves the owner/room/channel keying).
drawerGroup.value = row
drawerOpen.value = true
}
const drawerSections = computed(() =>
drawerGroup.value ? buildGroupSections(drawerGroup.value) : [],
)
const drawerAvatarSrc = computed<string | undefined>(() => {
const g = drawerGroup.value
if (!g) return undefined
return imageCell(g, 'headImageUrl') ?? undefined
})
const drawerCanChat = computed(() => !!drawerGroup.value?.owner_account_id)
function goChatFromDrawer(): void {
const row = drawerGroup.value
if (!row) return
if (!row.owner_account_id) {
ElMessage.warning('该群缺少归属账号,无法查看聊天记录')
return
@@ -138,11 +186,50 @@ onMounted(load)
style="width: 100%"
:row-class-name="() => 'yk-clickable-row'"
empty-text="暂无群数据"
@row-click="openTimeline"
@row-click="openDrawer"
>
<el-table-column label="群名" min-width="180">
<template #default="{ row }">
{{ row.room_name || row.room_id }}
{{ row.room_name || cellText(row, 'chatRoomName') || row.room_id }}
</template>
</el-table-column>
<el-table-column label="群ID" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span style="font-family: monospace; font-size: 12px">{{ row.room_id }}</span>
</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="群主" min-width="140">
<template #default="{ row }">
<span v-if="cell(row, 'userName') || cell(row, 'userPhone')">
<template v-if="cell(row, 'userName')">{{ cellText(row, 'userName') }}</template>
<span
v-if="cell(row, 'userName') && cell(row, 'userPhone')"
style="color: #c0c4cc"
>
/
</span>
<template v-if="cell(row, 'userPhone')">{{ cellText(row, 'userPhone') }}</template>
</span>
<span v-else style="color: #c0c4cc">--</span>
</template>
</el-table-column>
<el-table-column label="归属微信" min-width="130">
<template #default="{ row }">
<span v-if="row.owner_account_nickname">{{ row.owner_account_nickname }}</span>
<span v-else-if="cell(row, 'wechatNickName')">{{ cellText(row, 'wechatNickName') }}</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="消息数" prop="message_count" width="90" align="right">
<template #default="{ row }">{{ row.message_count ?? '--' }}</template>
</el-table-column>
<el-table-column label="最近聊天" width="160">
<template #default="{ row }">
{{ formatTime(row.last_chat_time ?? timeCell(row, 'lastChatTime')) }}
</template>
</el-table-column>
<el-table-column label="渠道" width="100">
@@ -152,22 +239,6 @@ onMounted(load)
</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">
@@ -182,7 +253,67 @@ onMounted(load)
@size-change="handlePageSizeChange"
/>
</div>
<p class="yk-groups__hint">点击任意一行查看该群的聊天记录。</p>
<p class="yk-groups__hint">点击任意一行查看群全部字段。</p>
<!-- All-fields drawer -->
<el-drawer
v-model="drawerOpen"
:title="drawerGroup ? `群详情 · ${drawerGroup.room_name ?? drawerGroup.room_id}` : '群详情'"
direction="rtl"
size="560px"
>
<template v-if="drawerGroup">
<div class="yk-drawer__topbar">
<el-button
type="primary"
size="small"
:disabled="!drawerCanChat"
@click="goChatFromDrawer"
>
查看聊天记录
</el-button>
<span v-if="!drawerCanChat" class="yk-drawer__topbar-hint">
该群缺少归属账号,无法查看聊天
</span>
</div>
<div class="yk-drawer__avatar">
<el-avatar :size="56" :src="drawerAvatarSrc" shape="square">
{{ (drawerGroup.room_name || drawerGroup.room_id || '?').slice(0, 1) }}
</el-avatar>
<div class="yk-drawer__avatar-meta">
<div class="yk-drawer__avatar-name">
{{ drawerGroup.room_name || drawerGroup.room_id }}
</div>
<div class="yk-drawer__avatar-sub">
{{ channelLabel(drawerGroup.channel) }}
<template v-if="cell(drawerGroup, 'wechatNickName')">
· {{ cell(drawerGroup, 'wechatNickName') }}
</template>
</div>
</div>
</div>
<div v-for="section in drawerSections" :key="section.title" class="yk-drawer__section">
<el-divider content-position="left">{{ section.title }}</el-divider>
<dl class="yk-drawer__kv">
<div v-for="f in section.fields" :key="f.key" class="yk-drawer__kv-item">
<dt>{{ f.label }}</dt>
<dd v-if="f.render === 'image'" class="yk-drawer__img">
<el-image
:src="String(f.value)"
:preview-src-list="[String(f.value)]"
fit="cover"
hide-on-click-modal
/>
</dd>
<dd v-else-if="f.render === 'empty'" class="yk-drawer__empty">—</dd>
<dd v-else>{{ f.text }}</dd>
</div>
</dl>
</div>
</template>
</el-drawer>
</div>
</template>
@@ -219,4 +350,77 @@ onMounted(load)
font-size: 12px;
color: #c0c4cc;
}
.yk-drawer__topbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.yk-drawer__topbar-hint {
color: #c0c4cc;
font-size: 12px;
}
.yk-drawer__avatar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.yk-drawer__avatar-meta {
min-width: 0;
}
.yk-drawer__avatar-name {
font-size: 16px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.yk-drawer__avatar-sub {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.yk-drawer__kv {
margin: 0 0 8px;
}
.yk-drawer__kv-item {
display: grid;
grid-template-columns: 110px 1fr;
gap: 8px 12px;
padding: 3px 0;
}
.yk-drawer__kv dt {
color: #909399;
font-size: 13px;
word-break: break-all;
}
.yk-drawer__kv dd {
margin: 0;
font-size: 13px;
color: #303133;
word-break: break-all;
min-width: 0;
}
.yk-drawer__img :deep(.el-image) {
width: 64px;
height: 64px;
border-radius: 4px;
border: 1px solid var(--el-border-color-light, #e4e7ed);
}
.yk-drawer__empty {
color: #c0c4cc;
}
</style>
+113 -63
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, 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'
import { buildSmsSections, readField } from '@/utils/smsFields'
const list = ref<Sms[]>([])
const total = ref(0)
@@ -12,10 +13,9 @@ 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)
// All-fields drawer state.
const drawerOpen = ref(false)
const drawerSms = ref<Sms | null>(null)
async function load(): Promise<void> {
loading.value = true
@@ -46,11 +46,46 @@ function handlePageSizeChange(s: number): void {
load()
}
function openDetail(row: Sms): void {
detailRow.value = row
detailVisible.value = true
/**
* Read a raw_json camelCase key off an SMS, preferring the structured
* snake_case column when it is populated. The first argument is the row so the
* table can use the same helper as the drawer.
*/
function cell(row: Sms, camelKey: string): unknown {
return readField(row, camelKey)
}
function cellText(row: Sms, camelKey: string): string {
const v = cell(row, camelKey)
return v === null || v === undefined || v === '' ? '--' : String(v)
}
function timeCell(row: Sms, camelKey: string): number | undefined {
const v = cell(row, camelKey)
return typeof v === 'number' ? v : undefined
}
function openDrawer(row: Sms): void {
// Row click opens the all-fields drawer (which surfaces the full body too).
drawerSms.value = row
drawerOpen.value = true
}
const drawerSections = computed(() =>
drawerSms.value ? buildSmsSections(drawerSms.value) : [],
)
const drawerFullBody = computed(() => {
const s = drawerSms.value
if (!s) return ''
return (
(typeof cell(s, 'sms') === 'string' ? (cell(s, 'sms') as string) : '') ||
s.content ||
s.content_preview ||
''
)
})
onMounted(load)
</script>
@@ -81,14 +116,16 @@ onMounted(load)
style="width: 100%"
:row-class-name="() => 'yk-clickable-row'"
empty-text="暂无短信记录"
@row-click="openDetail"
@row-click="openDrawer"
>
<el-table-column label="时间" width="160">
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
<template #default="{ row }">
{{ formatTime(row.create_time ?? timeCell(row, 'createTime')) }}
</template>
</el-table-column>
<el-table-column label="员工" min-width="130">
<template #default="{ row }">
{{ row.receive_name || row.user_id || '--' }}
{{ row.receive_name || row.user_id || cellText(row, 'userId') }}
</template>
</el-table-column>
<el-table-column label="客户号码" min-width="130">
@@ -120,43 +157,33 @@ onMounted(load)
@size-change="handlePageSizeChange"
/>
</div>
<p class="yk-sms__hint">点击任意一行查看完整短信内容。</p>
<p class="yk-sms__hint">点击任意一行查看短信全部字段。</p>
<el-dialog
v-model="detailVisible"
title="短信详情"
width="520px"
align-center
<!-- All-fields drawer -->
<el-drawer
v-model="drawerOpen"
:title="drawerSms ? `短信详情 · ${drawerSms.customer_phone ?? drawerSms.sms_id}` : '短信详情'"
direction="rtl"
size="560px"
>
<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>
<template v-if="drawerSms">
<div v-if="drawerFullBody" class="yk-sms__full">
<div class="yk-sms__full-label">短信全文</div>
<div class="yk-sms__full-body">{{ drawerFullBody }}</div>
</div>
<div class="yk-sms__detail-row">
<span class="yk-sms__detail-label">员工</span>
<span>{{ detailRow.receive_name || detailRow.user_id || '--' }}</span>
<div v-for="section in drawerSections" :key="section.title" class="yk-drawer__section">
<el-divider content-position="left">{{ section.title }}</el-divider>
<dl class="yk-drawer__kv">
<div v-for="f in section.fields" :key="f.key" class="yk-drawer__kv-item">
<dt>{{ f.label }}</dt>
<dd v-if="f.render === 'empty'" class="yk-drawer__empty">—</dd>
<dd v-else>{{ f.text }}</dd>
</div>
</dl>
</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>
</template>
</el-drawer>
</div>
</template>
@@ -187,31 +214,54 @@ onMounted(load)
color: #c0c4cc;
}
.yk-sms__detail {
display: flex;
flex-direction: column;
gap: 10px;
font-size: 13px;
.yk-sms__full {
background: var(--el-fill-color-light, #f5f7fa);
border: 1px solid var(--el-border-color-light, #e4e7ed);
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 12px;
}
.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;
.yk-sms__full-label {
color: #909399;
font-size: 12px;
margin-bottom: 6px;
}
.yk-sms__detail-content {
.yk-sms__full-body {
font-size: 13px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
}
.yk-drawer__kv {
margin: 0 0 8px;
}
.yk-drawer__kv-item {
display: grid;
grid-template-columns: 110px 1fr;
gap: 8px 12px;
padding: 3px 0;
}
.yk-drawer__kv dt {
color: #909399;
font-size: 13px;
word-break: break-all;
}
.yk-drawer__kv dd {
margin: 0;
font-size: 13px;
color: #303133;
word-break: break-all;
min-width: 0;
}
.yk-drawer__empty {
color: #c0c4cc;
}
</style>