feat(staffchat): 统计分析7tab(分型明细列+计数徽标+近7天窗+msgid定位,对齐云客)
- StatsPane 占位替换: 7 tab(label 带 (N) 计数)+daterange 默认近7天(清空后端兜底) - 明细列配置驱动(KIND_COLS);link 列跳 single/group 聊天;红包/转账行带 ?msgid 定位 - 语音通话列对齐 CallList(通话人/类型/呼入呼出/接通状态/时长/录音播放 dialog/时间/操作) - 敏感词 tab 空态卡(词库未配置,待 C1 接入,零请求) - 员工微信列用资料卡信息(明细行无 staff 字段,数据本属该员工) - counts 与明细解耦;双 seq 竞态守卫(兄弟组件惯例) - api: getStaffStatsDetail kind 收紧为 StaffStatsKind 联合(后端 Literal 6 值) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -378,10 +378,19 @@ export function getMoments(query: MomentsQuery): Promise<MomentsList> {
|
||||
)
|
||||
}
|
||||
|
||||
/** staff-stats 明细 kind(与后端 KindParam Literal 6 值一致;无 sensitive——词库未接入)。 */
|
||||
export type StaffStatsKind =
|
||||
| 'new_customers'
|
||||
| 'new_groups'
|
||||
| 'red_transfer'
|
||||
| 'del_customers'
|
||||
| 'blocked_customers'
|
||||
| 'voice_calls'
|
||||
|
||||
export interface StaffStatsQuery {
|
||||
wechat_id: string
|
||||
/** 传 kind 走明细模式,缺省返回 7 数字 counts。 */
|
||||
kind?: string
|
||||
kind?: StaffStatsKind
|
||||
start?: number
|
||||
end?: number
|
||||
}
|
||||
@@ -400,7 +409,7 @@ export function getStaffStatsCounts(
|
||||
}
|
||||
|
||||
export function getStaffStatsDetail(
|
||||
query: StaffStatsQuery & { kind: string },
|
||||
query: StaffStatsQuery & { kind: StaffStatsKind },
|
||||
): Promise<StaffStatsDetail> {
|
||||
return getJson<StaffStatsDetail>(
|
||||
'/staff-stats',
|
||||
|
||||
@@ -1,8 +1,623 @@
|
||||
<!-- 占位组件: Task 14 实现统计分析视图,签名锁定如下。 -->
|
||||
<!-- 员工聊天窗口右列·统计分析(对齐云客 wechat-window 统计 tab): 7 个计数 tab
|
||||
(label 带 (N) 徽标) + 近 7 天日期窗 + 分型明细表。明细列配置驱动
|
||||
(KIND_COLS);红包/转账行带 msgid 定位跳转;语音通话列对齐 CallList。
|
||||
根元素必须保持单 div——父组件 StaffChat 靠 attrs fallthrough 挂
|
||||
sc-root__pane 列宽。 -->
|
||||
<script setup lang="ts">
|
||||
defineProps<{ ownerId: string }>()
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getAccounts, getStaffStatsCounts, getStaffStatsDetail } from '@/api'
|
||||
import type { StaffStatsKind } from '@/api'
|
||||
import type { Account, StaffStatsCounts } from '@/api/types'
|
||||
import {
|
||||
callDirectionLabel,
|
||||
formatCallDuration,
|
||||
formatTime,
|
||||
} from '@/utils/format'
|
||||
|
||||
const props = defineProps<{ ownerId: string }>()
|
||||
const router = useRouter()
|
||||
|
||||
// ---- tab 定义(顺序/文案对齐云客;sensitive 是前端专属空态,后端无该 kind) ----
|
||||
|
||||
/** tab key = 后端 kind + 前端 sensitive 空态。 */
|
||||
type KindKey = StaffStatsKind | 'sensitive'
|
||||
|
||||
const TABS: { key: KindKey; label: string }[] = [
|
||||
{ key: 'new_customers', label: '新的客户' },
|
||||
{ key: 'new_groups', label: '新的群' },
|
||||
{ key: 'red_transfer', label: '红包转账' },
|
||||
{ key: 'sensitive', label: '单聊/群聊敏感词' },
|
||||
{ key: 'del_customers', label: '删除客户' },
|
||||
{ key: 'blocked_customers', label: '拉黑客户' },
|
||||
{ key: 'voice_calls', label: '语音通话' },
|
||||
]
|
||||
|
||||
/** tab key → counts 响应字段(sensitive 对应后端恒 0 占位字段)。 */
|
||||
const COUNT_KEY: Record<KindKey, keyof StaffStatsCounts> = {
|
||||
new_customers: 'new_customers',
|
||||
new_groups: 'new_groups',
|
||||
red_transfer: 'red_transfer',
|
||||
sensitive: 'sensitive_words',
|
||||
del_customers: 'del_customers',
|
||||
blocked_customers: 'blocked_customers',
|
||||
voice_calls: 'voice_calls',
|
||||
}
|
||||
|
||||
// ---- 通用明细行(后端每 kind 字段不同,统一宽行;取值见 staff_stats.py) ----
|
||||
|
||||
interface StatsRow {
|
||||
talker_id?: string | null
|
||||
room_id?: string | null
|
||||
nickname?: string | null
|
||||
avatar?: string | null
|
||||
remark?: string | null
|
||||
room_name?: string | null
|
||||
op_type?: string | null
|
||||
amount?: number | null
|
||||
time?: number | null
|
||||
msg_id?: string | null
|
||||
/** voice_calls 专属 */
|
||||
id?: number | string | null
|
||||
peer?: string | null
|
||||
incoming?: number | null
|
||||
connected?: boolean | null
|
||||
duration?: number | null
|
||||
start_time?: number | null
|
||||
media_url?: string | null
|
||||
}
|
||||
|
||||
// ---- 明细列配置(语音通话列富模板 + sensitive 空态走专属分支,不入表) ----
|
||||
|
||||
interface Col {
|
||||
prop: string
|
||||
label: string
|
||||
/** link=客户/群点击跳聊天;staff=员工微信(资料卡);缺省按 prop 特判文本。 */
|
||||
kind?: 'link' | 'staff'
|
||||
}
|
||||
|
||||
const KIND_COLS: Partial<Record<KindKey, Col[]>> = {
|
||||
new_customers: [
|
||||
{ prop: 'nickname', label: '客户微信', kind: 'link' },
|
||||
{ prop: 'remark', label: '客户备注' },
|
||||
{ prop: 'staff', label: '员工微信', kind: 'staff' },
|
||||
{ prop: 'time', label: '时间' },
|
||||
],
|
||||
new_groups: [
|
||||
{ prop: 'room_name', label: '微信群信息', kind: 'link' },
|
||||
{ prop: 'staff', label: '员工微信', kind: 'staff' },
|
||||
{ prop: 'time', label: '时间' },
|
||||
],
|
||||
red_transfer: [
|
||||
{ prop: 'op_type', label: '操作类型' },
|
||||
{ prop: 'nickname', label: '客户微信', kind: 'link' },
|
||||
{ prop: 'staff', label: '员工微信', kind: 'staff' },
|
||||
{ prop: 'amount', label: '金额' },
|
||||
{ prop: 'time', label: '发生时间' },
|
||||
{ prop: '_action', label: '操作' },
|
||||
],
|
||||
del_customers: [
|
||||
{ prop: 'nickname', label: '客户微信', kind: 'link' },
|
||||
{ prop: 'staff', label: '员工微信', kind: 'staff' },
|
||||
{ prop: 'time', label: '发生时间' },
|
||||
],
|
||||
blocked_customers: [
|
||||
{ prop: 'nickname', label: '客户微信', kind: 'link' },
|
||||
{ prop: 'staff', label: '员工微信', kind: 'staff' },
|
||||
{ prop: 'time', label: '发生时间' },
|
||||
],
|
||||
}
|
||||
|
||||
const activeTab = ref<KindKey>('new_customers')
|
||||
/** 当前 tab 列配置(voice_calls/sensitive 渲染专属分支,取空数组)。 */
|
||||
const activeCols = computed<Col[]>(() => KIND_COLS[activeTab.value] ?? [])
|
||||
|
||||
// ---- 日期窗(默认近 7 天:今天-6天 00:00:00 ~ 今天 23:59:59) ----
|
||||
|
||||
function defaultRange(): [Date, Date] {
|
||||
const start = new Date()
|
||||
start.setHours(0, 0, 0, 0)
|
||||
start.setDate(start.getDate() - 6)
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(defaultRange())
|
||||
|
||||
/** 日期窗 → start/end ms(闭区间,整日对齐);清空返回空对象让后端兜底近 7 天。 */
|
||||
function windowParams(): { start?: number; end?: number } {
|
||||
const r = dateRange.value
|
||||
if (!r || !r[0] || !r[1]) return {}
|
||||
const start = new Date(r[0])
|
||||
start.setHours(0, 0, 0, 0)
|
||||
const end = new Date(r[1])
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return { start: start.getTime(), end: end.getTime() }
|
||||
}
|
||||
|
||||
// ---- 员工微信列数据: 后端明细行无 staff 字段——数据本就是该员工的统计,
|
||||
// 用资料卡信息渲染(nickname/avatar,与 StaffChat 同款 getAccounts 手法) ----
|
||||
|
||||
const profile = ref<Account | null>(null)
|
||||
|
||||
async function loadProfile(): Promise<void> {
|
||||
try {
|
||||
const res = await getAccounts({ channel: 'wechat', page: 1, page_size: 100 })
|
||||
profile.value = res.items.find(a => a.account_id === props.ownerId) ?? null
|
||||
} catch {
|
||||
profile.value = null // 后端未起: 员工列退化为 ownerId 兜底
|
||||
}
|
||||
}
|
||||
|
||||
const staffName = computed(() => profile.value?.nickname || props.ownerId)
|
||||
|
||||
// ---- counts(一次拉全 7 类,徽标与明细解耦) ----
|
||||
|
||||
const counts = ref<StaffStatsCounts | null>(null)
|
||||
const countsLoading = ref(false)
|
||||
let countsSeq = 0
|
||||
|
||||
async function loadCounts(): Promise<void> {
|
||||
const mySeq = ++countsSeq
|
||||
countsLoading.value = true
|
||||
try {
|
||||
const res = await getStaffStatsCounts({
|
||||
wechat_id: props.ownerId,
|
||||
...windowParams(),
|
||||
})
|
||||
if (mySeq !== countsSeq) return
|
||||
counts.value = res
|
||||
} catch {
|
||||
if (mySeq !== countsSeq) return
|
||||
counts.value = null // 失败仅退化为无徽标,不阻塞明细
|
||||
} finally {
|
||||
if (mySeq === countsSeq) countsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** tab label: label (N)(counts 未到位/失败时无徽标)。 */
|
||||
function tabLabel(key: KindKey): string {
|
||||
const t = TABS.find(x => x.key === key)
|
||||
if (!t) return key
|
||||
const n = counts.value ? counts.value[COUNT_KEY[key]] : null
|
||||
return n == null ? t.label : `${t.label} (${n})`
|
||||
}
|
||||
|
||||
// ---- 明细(按当前 tab 拉;sensitive 不拉) ----
|
||||
|
||||
const rows = ref<StatsRow[]>([])
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref<string | null>(null)
|
||||
let detailSeq = 0
|
||||
|
||||
async function loadDetail(): Promise<void> {
|
||||
const kind = activeTab.value
|
||||
if (kind === 'sensitive') {
|
||||
// 词库未接入(spec 决策): 空态卡分支——作废在途请求并清表
|
||||
detailSeq++
|
||||
rows.value = []
|
||||
detailLoading.value = false
|
||||
detailError.value = null
|
||||
return
|
||||
}
|
||||
const mySeq = ++detailSeq
|
||||
rows.value = []
|
||||
detailLoading.value = true
|
||||
detailError.value = null
|
||||
try {
|
||||
const res = await getStaffStatsDetail({
|
||||
wechat_id: props.ownerId,
|
||||
kind,
|
||||
...windowParams(),
|
||||
})
|
||||
if (mySeq !== detailSeq) return
|
||||
rows.value = res.rows as StatsRow[]
|
||||
} catch (e) {
|
||||
if (mySeq !== detailSeq) return
|
||||
detailError.value = (e as Error).message
|
||||
rows.value = []
|
||||
} finally {
|
||||
if (mySeq === detailSeq) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 加载编排: 进入(ownerId 变更)拉 counts+明细+资料卡;切 tab 只拉明细;
|
||||
// 日期窗变更 counts+明细都重拉 ----
|
||||
|
||||
watch(activeTab, () => void loadDetail())
|
||||
|
||||
watch(
|
||||
() => props.ownerId,
|
||||
() => {
|
||||
dateRange.value = defaultRange()
|
||||
void loadProfile()
|
||||
void loadCounts()
|
||||
void loadDetail()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function onRangeChange(): void {
|
||||
void loadCounts()
|
||||
void loadDetail()
|
||||
}
|
||||
|
||||
// ---- 跳转: 客户/群 link 列 + red_transfer 操作列(msgid 定位) ----
|
||||
|
||||
/**
|
||||
* 跳本员工聊天窗: room_id 优先走 group(talkerId 语义为 roomId),否则 single。
|
||||
* withMsgid 用于红包/转账「查看聊天记录」——带 ?msgid= 让 ChatMain 定位消息。
|
||||
*/
|
||||
function goChat(row: StatsRow, withMsgid = false): void {
|
||||
const query =
|
||||
withMsgid && row.msg_id ? { msgid: String(row.msg_id) } : undefined
|
||||
if (row.room_id) {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'group', talkerId: row.room_id },
|
||||
query,
|
||||
})
|
||||
} else if (row.talker_id) {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'single', talkerId: row.talker_id },
|
||||
query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 语音通话行跳聊天: peer 是对方 wxid(@chatroom 后缀判群,同 CallList 手法)。 */
|
||||
function goVoiceChat(row: StatsRow): void {
|
||||
const peer = row.peer
|
||||
if (!peer) return
|
||||
if (peer.endsWith('@chatroom')) {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'group', talkerId: peer },
|
||||
})
|
||||
} else {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'single', talkerId: peer },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- cell helpers ----
|
||||
|
||||
/** link 列显示名: 昵称空时回退 wxid/群名/群ID。 */
|
||||
function linkText(row: StatsRow): string {
|
||||
return row.nickname || row.room_name || row.talker_id || row.room_id || ''
|
||||
}
|
||||
|
||||
/** 文本列: 空显 -。 */
|
||||
function cellText(row: StatsRow, prop: string): string {
|
||||
const v = (row as Record<string, unknown>)[prop]
|
||||
if (typeof v === 'string') return v.trim() ? v : '-'
|
||||
if (typeof v === 'number') return String(v)
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 接通状态(后端 connected 布尔,语义同 callStatusLabel 的 已接通/未接通)。 */
|
||||
function connectLabel(connected?: boolean | null): string {
|
||||
if (connected == null) return '--'
|
||||
return connected ? '已接通' : '未接通'
|
||||
}
|
||||
|
||||
// ---- 录音播放(手法抄 CallList: dialog + audio + 新窗口链接) ----
|
||||
|
||||
const recordDialogVisible = ref(false)
|
||||
const recordSrc = ref<string | null>(null)
|
||||
const recordTitle = ref('')
|
||||
|
||||
function playRecord(row: StatsRow): void {
|
||||
if (!row.media_url) return
|
||||
recordSrc.value = row.media_url
|
||||
const when = formatTime(row.start_time)
|
||||
recordTitle.value = `录音 · ${row.peer || '语音通话'}${when !== '--' ? ` · ${when}` : ''}`
|
||||
recordDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="yk-empty">统计分析 — Task 14 实现</div>
|
||||
<div class="sc-stats yk-panel-bg yk-panel-sep">
|
||||
<!-- ── 顶部: 7 计数 tab + 日期窗 ── -->
|
||||
<div class="sc-stats__top">
|
||||
<el-tabs v-model="activeTab" class="sc-stats__tabs">
|
||||
<el-tab-pane
|
||||
v-for="t in TABS"
|
||||
:key="t.key"
|
||||
:label="tabLabel(t.key)"
|
||||
:name="t.key"
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="sc-stats__row">
|
||||
<span class="sc-stats__range-label">统计时间</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
size="small"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
clearable
|
||||
class="sc-stats__date"
|
||||
@change="onRangeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 明细区(列内滚动) ── -->
|
||||
<div class="sc-stats__body">
|
||||
<!-- 敏感词: 词库未接入,空态卡(spec 决策,对齐云客但词库缺失) -->
|
||||
<div v-if="activeTab === 'sensitive'" class="sc-stats__sensitive">
|
||||
<div class="sc-stats__sensitive-title">单聊/群聊敏感词</div>
|
||||
<div class="sc-stats__sensitive-text">词库未配置,待 C1 敏感词功能接入</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="detailError"
|
||||
:title="detailError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
style="margin-bottom: 10px"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-loading="detailLoading"
|
||||
:data="rows"
|
||||
stripe
|
||||
empty-text="暂无数据"
|
||||
>
|
||||
<!-- ── 语音通话: 列对齐 CallList(通话人/类型/呼入呼出/接通状态/时长/录音/时间/操作) ── -->
|
||||
<template v-if="activeTab === 'voice_calls'">
|
||||
<el-table-column label="通话人" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.peer"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="goVoiceChat(row)"
|
||||
>{{ row.peer }}</el-button>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通话类型" width="100">
|
||||
<template #default>语音通话</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="呼入呼出" width="90">
|
||||
<template #default="{ row }">{{ callDirectionLabel(row.incoming) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接通状态" width="90">
|
||||
<template #default="{ row }">{{ connectLabel(row.connected) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通话时长" width="100">
|
||||
<template #default="{ row }">{{ formatCallDuration(row.duration) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通话录音" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.media_url"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="playRecord(row)"
|
||||
>播放</el-button>
|
||||
<span v-else style="color: #c0c4cc">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通话时间" width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.start_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!row.peer"
|
||||
@click="goVoiceChat(row)"
|
||||
>查看聊天记录</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<!-- ── 其余 5 类: KIND_COLS 配置驱动 ── -->
|
||||
<template v-else>
|
||||
<el-table-column
|
||||
v-for="col in activeCols"
|
||||
:key="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.kind === 'link' || col.kind === 'staff' ? 150 : col.prop === '_action' ? 110 : 110"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<!-- link 列: 客户/群 → 跳本员工聊天窗 -->
|
||||
<template v-if="col.kind === 'link'">
|
||||
<div class="sc-stats__user">
|
||||
<el-avatar
|
||||
v-if="row.avatar"
|
||||
:size="28"
|
||||
:src="row.avatar"
|
||||
shape="square"
|
||||
/>
|
||||
<el-button
|
||||
v-if="linkText(row)"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
class="sc-stats__link"
|
||||
@click="goChat(row)"
|
||||
>{{ linkText(row) }}</el-button>
|
||||
<span v-else>--</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- staff 列: 明细行无该字段,渲染资料卡信息(数据本就是该员工) -->
|
||||
<template v-else-if="col.kind === 'staff'">
|
||||
<div class="sc-stats__user">
|
||||
<el-avatar
|
||||
v-if="profile?.avatar"
|
||||
:size="28"
|
||||
:src="profile.avatar"
|
||||
shape="square"
|
||||
/>
|
||||
<div class="sc-stats__meta">
|
||||
<div class="sc-stats__name" :title="staffName">{{ staffName }}</div>
|
||||
<div class="sc-stats__sub" :title="props.ownerId">{{ props.ownerId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 时间/金额/操作列特判,其余纯文本 -->
|
||||
<template v-else-if="col.prop === 'time'">{{ formatTime(row.time) }}</template>
|
||||
<template v-else-if="col.prop === 'amount'">{{ row.amount ?? '—' }}</template>
|
||||
<template v-else-if="col.prop === '_action'">
|
||||
<el-button link type="primary" size="small" @click="goChat(row, true)">
|
||||
查看聊天记录
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>{{ cellText(row, col.prop) }}</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ── 录音播放 dialog(手法抄 CallList) ── -->
|
||||
<el-dialog
|
||||
v-model="recordDialogVisible"
|
||||
:title="recordTitle"
|
||||
width="480px"
|
||||
align-center
|
||||
@closed="recordSrc = null"
|
||||
>
|
||||
<audio
|
||||
v-if="recordSrc"
|
||||
controls
|
||||
:src="recordSrc"
|
||||
style="width: 100%"
|
||||
preload="metadata"
|
||||
>
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>
|
||||
<div v-if="recordSrc" class="sc-stats__record-link">
|
||||
<a :href="recordSrc" target="_blank" rel="noopener">在新窗口打开录音文件</a>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ══ 统计分析右列(墨玉主题,手法沿用 ContactsPane/MomentsPane) ══
|
||||
点阵底/右缘玉绿线走全局 yk-panel-bg/yk-panel-sep(style.css);这里只写布局。 */
|
||||
.sc-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── 顶部区(tab + 日期窗,随列固定不滚动) ── */
|
||||
.sc-stats__top {
|
||||
flex: none;
|
||||
padding: 4px 12px 0;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(23, 29, 43, 0.05);
|
||||
}
|
||||
.sc-stats__tabs :deep(.el-tabs__header) {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
/* 7 个 tab 一排放不下时滚动而非换行挤爆列宽 */
|
||||
.sc-stats__tabs :deep(.el-tabs__nav-scroll) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.sc-stats__tabs :deep(.el-tabs__nav) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sc-stats__row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.sc-stats__range-label {
|
||||
flex: none;
|
||||
font-size: 12.5px;
|
||||
color: #5a6472;
|
||||
}
|
||||
.sc-stats__date {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* ── 明细区(列内滚动) ── */
|
||||
.sc-stats__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 敏感词空态卡 */
|
||||
.sc-stats__sensitive {
|
||||
margin-top: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.sc-stats__sensitive-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2b3240;
|
||||
}
|
||||
.sc-stats__sensitive-text {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: #8a94a4;
|
||||
}
|
||||
|
||||
/* link/staff 富单元格(头像 + 名称/微信号,手法对齐 ContactsPane) */
|
||||
.sc-stats__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.sc-stats__user .el-avatar {
|
||||
flex: none;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(23, 29, 43, 0.05);
|
||||
box-shadow: 0 1px 3px rgba(23, 29, 43, 0.06);
|
||||
}
|
||||
.sc-stats__link {
|
||||
max-width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sc-stats__meta {
|
||||
min-width: 0;
|
||||
}
|
||||
.sc-stats__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #2b3240;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sc-stats__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 11.5px;
|
||||
color: #8a94a4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 录音 dialog 底部链接 */
|
||||
.sc-stats__record-link {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user