feat(staff-friends): 员工客户列表独立页——对齐云客员工详情,好友数/微信群数下钻
员工列表点「好友数」原先跳全局 /customers?owner_account_id=x,布局与全局
列表无异且计数/分页绑全局声明口径(同日 dfa4fdd 修的显示 bug),与云客
「员工名下客户列表」体验割裂。新页 /staff-friends/:ownerId/:tab:
- 微信客户/微信群两 tab(对齐云客截图);标题=员工显示名(入口直带,
直链经 /api/accounts 解析),返回箭头回员工列表
- 搜索(昵称/微信号/备注名/手机号,服务端 q)+ 查询/导出 CSV
- 发送/接收条数=Customer.send_num/receive_num 累计值(CustomerOut 补
两列,同步自云客 friend 记录,与截图同源)
- 勾选批量「导出聊天记录」:新端点 GET /api/export/chat-html-zip,复用
chat-html 渲染器每会话一个 HTML 打 zip(文件名=备注>昵称>friend_id,
非法字符清洗;单会话 5000 行截断告警;一次上限 50 会话防误选打爆)
- 行内:查看聊天记录→staff-chat;导出HTML聊天记录(时间范围弹窗)
- WA 行保持原入口(该页暂仅微信)
后端 605 测试绿(zip 新 4 例);本地浏览器端到端验证:过滤 9 条/勾选
激活/群 tab/返回箭头;zip 产物 application/zip 双会话双气泡。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 员工客户列表页(2026-09-08,对齐云客员工详情客户列表,用户截图版式)。
|
||||
*
|
||||
* 入口:员工列表行内「好友数/微信群数」(微信行)——账号维度下钻,与全局
|
||||
* 客户列表(/customers)分离:固定 owner 过滤、微信客户/微信群两 tab、
|
||||
* 搜索(昵称/微信号/备注名/手机号)、勾选批量「导出聊天记录」(服务端打
|
||||
* zip,每会话一个 HTML)、行内查看/导出聊天记录(沿用 staff-chat 与
|
||||
* /export/chat-html)。
|
||||
*
|
||||
* 列口径:发送/接收条数用 Customer.send_num/receive_num(云客 friend 记录
|
||||
* 累计值,与截图同源);分页用真实过滤 total(本页恒有 owner 过滤,不存在
|
||||
* 全局声明口径问题)。
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { getAccounts, getCustomers, getGroups } from '@/api'
|
||||
import { downloadBlob } from '@/api/client'
|
||||
import type { Account, Customer, Group } from '@/api/types'
|
||||
import { exportListToCsv } from '@/utils/csvExport'
|
||||
import { formatTime } from '@/utils/format'
|
||||
import { fromTypeLabel, readField } from '@/utils/customerFields'
|
||||
|
||||
const props = defineProps<{
|
||||
/** 归属员工账号(wechatId) */
|
||||
ownerId: string
|
||||
/** customers=微信客户 tab(默认) / groups=微信群 tab */
|
||||
tab?: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref<'customers' | 'groups'>(props.tab === 'groups' ? 'groups' : 'customers')
|
||||
|
||||
// ---- 员工名:入口 query ?name= 直带;直链/刷新时经 /api/accounts 解析 ----
|
||||
const staffName = ref<string | null>(
|
||||
route.query.name ? String(route.query.name) : null,
|
||||
)
|
||||
const resolvedName = ref<string | null>(null)
|
||||
const displayTitle = computed(() => staffName.value ?? resolvedName.value ?? props.ownerId)
|
||||
|
||||
async function resolveName(): Promise<void> {
|
||||
if (staffName.value) return
|
||||
try {
|
||||
const res = await getAccounts({ channel: 'wechat', page: 1, page_size: 100 })
|
||||
const hit = res.items.find((a: Account) => a.account_id === props.ownerId)
|
||||
resolvedName.value = hit?.nickname || hit?.user_name || null
|
||||
} catch {
|
||||
// 静默降级:标题显示 ownerId
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 搜索(作用于当前 tab;/api/customers 与 /api/groups 均支持 q) ----
|
||||
const keyword = ref('')
|
||||
|
||||
// ---- 微信客户 tab ----
|
||||
const customers = ref<Customer[]>([])
|
||||
const custTotal = ref(0)
|
||||
const custPage = ref(1)
|
||||
const custPageSize = ref(10)
|
||||
const custLoading = ref(false)
|
||||
const custError = ref<string | null>(null)
|
||||
|
||||
// ---- 微信群 tab ----
|
||||
const groups = ref<Group[]>([])
|
||||
const groupTotal = ref(0)
|
||||
const groupPage = ref(1)
|
||||
const groupPageSize = ref(10)
|
||||
const groupLoading = ref(false)
|
||||
const groupError = ref<string | null>(null)
|
||||
|
||||
async function loadCustomers(): Promise<void> {
|
||||
custLoading.value = true
|
||||
custError.value = null
|
||||
try {
|
||||
const res = await getCustomers({
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: custPage.value,
|
||||
page_size: custPageSize.value,
|
||||
})
|
||||
customers.value = res.items
|
||||
custTotal.value = res.total
|
||||
} catch (e) {
|
||||
custError.value = (e as Error).message
|
||||
ElMessage.error(`客户列表加载失败:${(e as Error).message}`)
|
||||
} finally {
|
||||
custLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
groupLoading.value = true
|
||||
groupError.value = null
|
||||
try {
|
||||
const res = await getGroups({
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: groupPage.value,
|
||||
page_size: groupPageSize.value,
|
||||
})
|
||||
groups.value = res.items
|
||||
groupTotal.value = res.total
|
||||
} catch (e) {
|
||||
groupError.value = (e as Error).message
|
||||
ElMessage.error(`群列表加载失败:${(e as Error).message}`)
|
||||
} finally {
|
||||
groupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch(): void {
|
||||
custPage.value = 1
|
||||
groupPage.value = 1
|
||||
clearSelection()
|
||||
if (activeTab.value === 'customers') loadCustomers()
|
||||
else loadGroups()
|
||||
}
|
||||
|
||||
// tab 切换:懒加载目标 tab(搜索词共享);URL param 同步,便于直链/刷新。
|
||||
function onTabChange(t: 'customers' | 'groups'): void {
|
||||
clearSelection()
|
||||
router.replace({
|
||||
name: 'staff-friends',
|
||||
params: { ownerId: props.ownerId, tab: t === 'groups' ? 'groups' : 'customers' },
|
||||
query: route.query.name ? { name: String(route.query.name) } : undefined,
|
||||
})
|
||||
if (t === 'customers') {
|
||||
if (customers.value.length === 0 && custTotal.value === 0) loadCustomers()
|
||||
} else {
|
||||
if (groups.value.length === 0 && groupTotal.value === 0) loadGroups()
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 勾选 + 批量导出聊天记录(服务端 zip) ----
|
||||
const selected = ref<Customer[]>([])
|
||||
|
||||
function onSelectionChange(rows: Customer[]): void {
|
||||
selected.value = rows
|
||||
}
|
||||
|
||||
function clearSelection(): void {
|
||||
selected.value = []
|
||||
custTableRef.value?.clearSelection()
|
||||
}
|
||||
const custTableRef = ref<{
|
||||
clearSelection: () => void
|
||||
} | null>(null)
|
||||
|
||||
const ZIP_LIMIT = 50
|
||||
const batchExporting = ref(false)
|
||||
|
||||
async function exportChatZip(): Promise<void> {
|
||||
if (selected.value.length === 0) return
|
||||
if (selected.value.length > ZIP_LIMIT) {
|
||||
ElMessage.warning(`一次最多导出 ${ZIP_LIMIT} 个会话,请减少勾选`)
|
||||
return
|
||||
}
|
||||
batchExporting.value = true
|
||||
try {
|
||||
await downloadBlob('/export/chat-html-zip', {
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
talker_ids: selected.value.map((c) => c.friend_id).join(','),
|
||||
})
|
||||
ElMessage.success(`已导出 ${selected.value.length} 个会话的聊天记录`)
|
||||
} catch (e) {
|
||||
ElMessage.error(`批量导出失败:${(e as Error).message}`)
|
||||
} finally {
|
||||
batchExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 行内:查看聊天记录 → 员工聊天窗口(single/group tab) ----
|
||||
function viewChat(row: Customer): void {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'single', talkerId: row.friend_id },
|
||||
})
|
||||
}
|
||||
|
||||
function viewGroupChat(row: Group): void {
|
||||
router.push({
|
||||
name: 'staff-chat',
|
||||
params: { ownerId: props.ownerId, tab: 'group', talkerId: row.room_id },
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 行内:导出HTML聊天记录(可选时间范围,沿用全局客户列表的弹窗模式) ----
|
||||
const exportOpen = ref(false)
|
||||
const exportRow = ref<Customer | null>(null)
|
||||
// value-format="X" 绑秒级 unix 字符串;confirm 时 *1000 转毫秒。
|
||||
const exportRange = ref<[string, string] | null>(null)
|
||||
|
||||
function openExport(row: Customer): void {
|
||||
exportRow.value = row
|
||||
exportRange.value = null
|
||||
exportOpen.value = true
|
||||
}
|
||||
|
||||
async function confirmExport(): Promise<void> {
|
||||
const row = exportRow.value
|
||||
if (!row) return
|
||||
const params: Record<string, unknown> = {
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
talker_id: row.friend_id,
|
||||
}
|
||||
if (exportRange.value) {
|
||||
params.start = Number(exportRange.value[0]) * 1000
|
||||
params.end = Number(exportRange.value[1]) * 1000
|
||||
}
|
||||
exportOpen.value = false
|
||||
try {
|
||||
await downloadBlob('/export/chat-html', params)
|
||||
ElMessage.success('导出已开始')
|
||||
} catch (e) {
|
||||
ElMessage.error(`导出失败,请稍后重试:${(e as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 导出 CSV(当前 tab + 搜索词 + owner 过滤) ----
|
||||
const exporting = ref(false)
|
||||
|
||||
async function exportCsv(): Promise<void> {
|
||||
exporting.value = true
|
||||
try {
|
||||
if (activeTab.value === 'customers') {
|
||||
const r = await exportListToCsv<Customer>({
|
||||
fetchPage: (page, pageSize) =>
|
||||
getCustomers({
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}),
|
||||
columns: [
|
||||
{ key: 'friend_nickname', label: '客户昵称' },
|
||||
{ key: 'friend_id', label: '微信号' },
|
||||
{ key: 'friend_remark', label: '备注名' },
|
||||
{ key: 'from_type', label: '数据来源', format: (r) => fromTypeLabel(readField(r, 'fromType')) },
|
||||
{
|
||||
key: 'last_chat_time',
|
||||
label: '最近沟通时间',
|
||||
format: (r) => (r.last_chat_time ? formatTime(r.last_chat_time) : '-'),
|
||||
},
|
||||
{ key: 'send_num', label: '发送条数', format: (r) => r.send_num ?? '-' },
|
||||
{ key: 'receive_num', label: '接收条数', format: (r) => r.receive_num ?? '-' },
|
||||
],
|
||||
filename: `微信客户_${props.ownerId}`,
|
||||
})
|
||||
ElMessage.success(`已导出 ${r.rows} 行`)
|
||||
} else {
|
||||
const r = await exportListToCsv<Group>({
|
||||
fetchPage: (page, pageSize) =>
|
||||
getGroups({
|
||||
channel: 'wechat',
|
||||
owner_account_id: props.ownerId,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}),
|
||||
columns: [
|
||||
{ key: 'room_name', label: '群名称' },
|
||||
{ key: 'room_id', label: '群ID' },
|
||||
{ key: 'owner_name', label: '群主昵称' },
|
||||
{ key: 'owner_wxid', label: '群主wxId' },
|
||||
{ key: 'member_count', label: '群成员数量' },
|
||||
{ key: 'message_count', label: '消息数量' },
|
||||
],
|
||||
filename: `微信群_${props.ownerId}`,
|
||||
})
|
||||
ElMessage.success(`已导出 ${r.rows} 行`)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(`导出失败:${(e as Error).message}`)
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 展示 helper ----
|
||||
function avatarUrl(c: Customer): string | undefined {
|
||||
const v = readField(c, 'friendHeadImageUrl')
|
||||
return typeof v === 'string' && v !== '' ? v : undefined
|
||||
}
|
||||
|
||||
function groupAvatarUrl(g: Group): string | undefined {
|
||||
const v = g.raw_json?.headImageUrl
|
||||
return typeof v === 'string' && v !== '' ? v : undefined
|
||||
}
|
||||
|
||||
function initials(c: Customer): string {
|
||||
const nick = c.friend_nickname || c.friend_id
|
||||
return nick ? nick.slice(0, 1).toUpperCase() : '?'
|
||||
}
|
||||
|
||||
function groupInitials(g: Group): string {
|
||||
const n = g.room_name || g.room_id
|
||||
return n ? n.slice(0, 1) : '?'
|
||||
}
|
||||
|
||||
function goBack(): void {
|
||||
if (window.history.length > 1) router.back()
|
||||
else router.push({ path: '/staff', query: { channel: 'wechat' } })
|
||||
}
|
||||
|
||||
// ---- 生命周期 + 路由参数变化(同组件不同员工切换) ----
|
||||
watch(
|
||||
() => [props.ownerId, props.tab] as const,
|
||||
() => {
|
||||
activeTab.value = props.tab === 'groups' ? 'groups' : 'customers'
|
||||
resetAndLoad()
|
||||
},
|
||||
)
|
||||
|
||||
function resetAndLoad(): void {
|
||||
custPage.value = 1
|
||||
groupPage.value = 1
|
||||
clearSelection()
|
||||
loadCustomers()
|
||||
if (activeTab.value === 'groups') loadGroups()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
resolveName()
|
||||
loadCustomers()
|
||||
if (activeTab.value === 'groups') loadGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sf-page">
|
||||
<!-- 头部:返回 + 员工名 + 两 tab + 搜索/导出行 -->
|
||||
<div class="sf-header">
|
||||
<div class="sf-header__title">
|
||||
<el-button :icon="ArrowLeft" circle size="small" @click="goBack" />
|
||||
<h1>{{ displayTitle }}</h1>
|
||||
<span class="sf-header__sub">微信号:{{ ownerId }}</span>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" class="sf-tabs" @tab-change="onTabChange($event as 'customers' | 'groups')">
|
||||
<el-tab-pane label="微信客户" name="customers" />
|
||||
<el-tab-pane label="微信群" name="groups" />
|
||||
</el-tabs>
|
||||
<div class="sf-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="sf-search"
|
||||
placeholder="支持微信昵称、微信号、备注名、手机号搜索"
|
||||
clearable
|
||||
@keyup.enter="onSearch"
|
||||
@clear="onSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||
<el-button :loading="exporting" @click="exportCsv">导出</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'customers'"
|
||||
:disabled="selected.length === 0"
|
||||
:loading="batchExporting"
|
||||
@click="exportChatZip"
|
||||
>
|
||||
导出聊天记录{{ selected.length > 0 ? `(${selected.length})` : '' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 微信客户表 -->
|
||||
<div v-show="activeTab === 'customers'" class="sf-card" v-loading="custLoading">
|
||||
<el-alert v-if="custError" :title="`客户列表加载失败:${custError}`" type="error" show-icon :closable="false" />
|
||||
<el-table
|
||||
ref="custTableRef"
|
||||
:data="customers"
|
||||
row-key="friend_id"
|
||||
empty-text="该员工暂无客户"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="42" reserve-selection />
|
||||
<el-table-column label="客户微信" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="sf-wechat">
|
||||
<el-avatar :size="36" :src="avatarUrl(row)" shape="square">
|
||||
{{ initials(row) }}
|
||||
</el-avatar>
|
||||
<div class="sf-wechat__meta">
|
||||
<span class="sf-wechat__nick">{{ row.friend_nickname || '--' }}</span>
|
||||
<span class="sf-wechat__id">{{ row.friend_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据来源" width="110">
|
||||
<template #default="{ row }">{{ fromTypeLabel(readField(row, 'fromType')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户备注名" min-width="120">
|
||||
<template #default="{ row }">{{ row.friend_remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近沟通时间" width="170">
|
||||
<template #default="{ row }">{{ row.last_chat_time ? formatTime(row.last_chat_time) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发送条数" width="95" align="right">
|
||||
<template #default="{ row }">{{ row.send_num ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收条数" width="95" align="right">
|
||||
<template #default="{ row }">{{ row.receive_num ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="sf-actions">
|
||||
<el-button link type="primary" size="small" @click="viewChat(row)">查看聊天记录</el-button>
|
||||
<el-button link type="primary" size="small" @click="openExport(row)">导出HTML聊天记录</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="sf-pager">
|
||||
<el-pagination
|
||||
:current-page="custPage"
|
||||
:page-size="custPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="custTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="(p: number) => { custPage = p; loadCustomers() }"
|
||||
@size-change="(s: number) => { custPageSize = s; custPage = 1; loadCustomers() }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 微信群表 -->
|
||||
<div v-show="activeTab === 'groups'" class="sf-card" v-loading="groupLoading">
|
||||
<el-alert v-if="groupError" :title="`群列表加载失败:${groupError}`" type="error" show-icon :closable="false" />
|
||||
<el-table :data="groups" row-key="room_id" empty-text="该员工暂无群">
|
||||
<el-table-column label="群名称" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="sf-wechat">
|
||||
<el-avatar :size="36" :src="groupAvatarUrl(row)" shape="square">
|
||||
{{ groupInitials(row) }}
|
||||
</el-avatar>
|
||||
<div class="sf-wechat__meta">
|
||||
<span class="sf-wechat__nick">{{ row.room_name || '--' }}</span>
|
||||
<span class="sf-wechat__id">{{ row.room_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="群主" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="sf-wechat__meta">
|
||||
<span>{{ row.owner_name || '-' }}</span>
|
||||
<span v-if="row.owner_wxid" class="sf-wechat__id">{{ row.owner_wxid }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="群成员数量" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.member_count ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消息数量" width="100" align="right">
|
||||
<template #default="{ row }">{{ row.message_count ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近消息时间" width="170">
|
||||
<template #default="{ row }">{{ row.last_chat_time ? formatTime(row.last_chat_time) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="viewGroupChat(row)">查看群聊记录</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="sf-pager">
|
||||
<el-pagination
|
||||
:current-page="groupPage"
|
||||
:page-size="groupPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="groupTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="(p: number) => { groupPage = p; loadGroups() }"
|
||||
@size-change="(s: number) => { groupPageSize = s; groupPage = 1; loadGroups() }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 行内导出时间范围弹窗(对齐全局客户列表) -->
|
||||
<el-dialog v-model="exportOpen" title="导出HTML聊天记录" width="420px">
|
||||
<el-date-picker
|
||||
v-model="exportRange"
|
||||
type="datetimerange"
|
||||
value-format="X"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="exportOpen = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmExport">导出</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sf-page {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.sf-header {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
.sf-header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.sf-header__title h1 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
.sf-header__sub {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.sf-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
.sf-search {
|
||||
width: 340px;
|
||||
}
|
||||
.sf-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.sf-wechat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.sf-wechat__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.4;
|
||||
min-width: 0;
|
||||
}
|
||||
.sf-wechat__nick {
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sf-wechat__id {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sf-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
.sf-actions .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.sf-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -239,6 +239,16 @@ function waSerial(index: number): number {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function viewCustomers(row: Account): void {
|
||||
// 微信行 → 员工客户列表独立页(2026-09-08 对齐云客员工详情,账号维度
|
||||
// 下钻:两 tab/搜索/批量导出);WA 行保持全局客户列表(该页暂仅微信)。
|
||||
if (row.channel === 'wechat') {
|
||||
router.push({
|
||||
name: 'staff-friends',
|
||||
params: { ownerId: row.account_id, tab: 'customers' },
|
||||
query: { name: row.nickname || row.user_name || '' },
|
||||
})
|
||||
return
|
||||
}
|
||||
channelStore.setChannel(row.channel)
|
||||
router.push({
|
||||
path: '/customers',
|
||||
@@ -247,6 +257,15 @@ function viewCustomers(row: Account): void {
|
||||
}
|
||||
|
||||
function viewGroups(row: Account): void {
|
||||
// 同上:微信群数 → 员工客户列表的群 tab;WA 保持原路径。
|
||||
if (row.channel === 'wechat') {
|
||||
router.push({
|
||||
name: 'staff-friends',
|
||||
params: { ownerId: row.account_id, tab: 'groups' },
|
||||
query: { name: row.nickname || row.user_name || '' },
|
||||
})
|
||||
return
|
||||
}
|
||||
channelStore.setChannel(row.channel)
|
||||
if (row.channel === 'whatsapp') {
|
||||
// WA:对齐云客,群列表在客户列表页的子 tab(2026-08-24)
|
||||
|
||||
Reference in New Issue
Block a user