2026-08-01 17:11:17 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-08-05 14:13:21 +08:00
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
2026-08-01 17:11:17 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-08-05 14:13:21 +08:00
|
|
|
|
import { getAccounts } from '@/api'
|
|
|
|
|
|
import type { Account, Channel } from '@/api/types'
|
|
|
|
|
|
import { channelLabel, formatTime } from '@/utils/format'
|
|
|
|
|
|
import { buildAccountSections, formatBytes, readField } from '@/utils/accountFields'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 员工 list — channel-tabbed account view mirroring the 云客 backend's
|
|
|
|
|
|
* 员工微信列表 (WeChat staff accounts) and WhatsApp 员工列表. The earlier sparse
|
|
|
|
|
|
* staff-record table is replaced by the underlying messaging accounts, which
|
|
|
|
|
|
* carry the verbatim upstream record on `raw_json` (nickName/alias/userName/
|
|
|
|
|
|
* sendNum/receiveNum/dbSize/onlineStatus/lastUpdate/talkerCount/...).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Each tab loads `/api/accounts?channel=<channel>` independently with its own
|
|
|
|
|
|
* pagination state. Row click opens the all-fields drawer reused from
|
|
|
|
|
|
* AccountList.vue (see accountFields.ts).
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
type TabKey = Channel
|
|
|
|
|
|
interface TabState {
|
|
|
|
|
|
items: Account[]
|
|
|
|
|
|
total: number
|
|
|
|
|
|
page: number
|
|
|
|
|
|
pageSize: number
|
|
|
|
|
|
loading: boolean
|
|
|
|
|
|
errorMsg: string | null
|
|
|
|
|
|
/** Has this tab been loaded at least once? Tabs lazy-load on first activation. */
|
|
|
|
|
|
loaded: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function freshTabState(): TabState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
errorMsg: null,
|
|
|
|
|
|
loaded: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
const activeTab = ref<TabKey>('wechat')
|
|
|
|
|
|
const tabState = reactive<Record<TabKey, TabState>>({
|
|
|
|
|
|
wechat: freshTabState(),
|
|
|
|
|
|
whatsapp: freshTabState(),
|
|
|
|
|
|
})
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
// All-fields drawer state (shared across tabs).
|
2026-08-01 17:47:50 +08:00
|
|
|
|
const drawerOpen = ref(false)
|
2026-08-05 14:13:21 +08:00
|
|
|
|
const drawerAccount = ref<Account | null>(null)
|
2026-08-01 17:47:50 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Cell helpers (structured-first, raw_json fallback via accountFields.readField)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function cell(row: Account, camelKey: string): unknown {
|
|
|
|
|
|
return readField(row, camelKey)
|
|
|
|
|
|
}
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function cellText(row: Account, camelKey: string): string {
|
|
|
|
|
|
const v = cell(row, camelKey)
|
|
|
|
|
|
return v === null || v === undefined || v === '' ? '--' : String(v)
|
|
|
|
|
|
}
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function cellNumber(row: Account, camelKey: string): number | undefined {
|
|
|
|
|
|
const v = cell(row, camelKey)
|
|
|
|
|
|
return typeof v === 'number' && Number.isFinite(v) ? v : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cellTime(row: Account, camelKey: string): number | undefined {
|
|
|
|
|
|
return cellNumber(row, camelKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function imageCell(row: Account, camelKey: string): string | undefined {
|
|
|
|
|
|
const v = cell(row, camelKey)
|
|
|
|
|
|
return typeof v === 'string' && v !== '' ? v : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** First character of userName/nickName/account_id for avatar fallbacks. */
|
|
|
|
|
|
function initials(row: Account): string {
|
|
|
|
|
|
const name =
|
|
|
|
|
|
cell(row, 'userName') ?? cell(row, 'nickName') ?? row.account_id ?? '?'
|
|
|
|
|
|
const s = String(name).trim()
|
|
|
|
|
|
return s ? s.slice(0, 1).toUpperCase() : '?'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function avatarUrl(row: Account): string | undefined {
|
|
|
|
|
|
// WeChat records key the avatar as `headImageUrl`; tolerate the legacy
|
|
|
|
|
|
// `headImg` / `headImgUrl` shapes too so the column degrades gracefully.
|
|
|
|
|
|
return (
|
|
|
|
|
|
imageCell(row, 'headImageUrl') ??
|
|
|
|
|
|
imageCell(row, 'headImgUrl') ??
|
|
|
|
|
|
imageCell(row, 'headImg')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-08-01 17:11:17 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function dbSizeText(row: Account): string {
|
|
|
|
|
|
const v = cellNumber(row, 'dbSize')
|
|
|
|
|
|
return v === undefined ? '--' : formatBytes(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在职: structured column is boolean; raw_json may carry 1/0. Handle both so
|
|
|
|
|
|
// the tag is correct regardless of which source populated the row.
|
|
|
|
|
|
function onJobTagType(v: unknown): 'success' | 'info' | '' {
|
|
|
|
|
|
if (v === true || v === 1 || v === '1') return 'success'
|
|
|
|
|
|
if (v === false || v === 0 || v === '0') return 'info'
|
2026-08-01 17:11:17 +08:00
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function onJobLabel(v: unknown): string {
|
|
|
|
|
|
if (v === true || v === 1 || v === '1') return '在职'
|
|
|
|
|
|
if (v === false || v === 0 || v === '0') return '离职'
|
2026-08-01 17:11:17 +08:00
|
|
|
|
return '--'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
// 在线状态: upstream sends "online"/"offline" (sometimes 1/0). Render as a tag
|
|
|
|
|
|
// when recognized; pass through other strings so info isn't hidden.
|
|
|
|
|
|
function onlineTagType(v: unknown): 'success' | 'info' | '' {
|
|
|
|
|
|
if (v === 'online' || v === 1 || v === '1') return 'success'
|
|
|
|
|
|
if (v === 'offline' || v === 0 || v === '0') return 'info'
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onlineLabel(v: unknown): string {
|
|
|
|
|
|
if (v === 'online' || v === 1 || v === '1') return '在线'
|
|
|
|
|
|
if (v === 'offline' || v === 0 || v === '0') return '离线'
|
|
|
|
|
|
if (v === null || v === undefined || v === '') return '--'
|
|
|
|
|
|
return String(v)
|
2026-08-01 17:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function channelTagType(c: Channel): 'success' | 'warning' {
|
|
|
|
|
|
return c === 'wechat' ? 'success' : 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Per-tab loading
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
async function loadTab(ch: TabKey): Promise<void> {
|
|
|
|
|
|
const st = tabState[ch]
|
|
|
|
|
|
st.loading = true
|
|
|
|
|
|
st.errorMsg = null
|
2026-08-01 17:11:17 +08:00
|
|
|
|
try {
|
2026-08-05 14:13:21 +08:00
|
|
|
|
const res = await getAccounts({
|
|
|
|
|
|
channel: ch,
|
|
|
|
|
|
page: st.page,
|
|
|
|
|
|
page_size: st.pageSize,
|
2026-08-01 17:11:17 +08:00
|
|
|
|
})
|
2026-08-05 14:13:21 +08:00
|
|
|
|
st.items = res.items
|
|
|
|
|
|
st.total = res.total
|
|
|
|
|
|
st.loaded = true
|
2026-08-01 17:11:17 +08:00
|
|
|
|
} catch (e) {
|
2026-08-05 14:13:21 +08:00
|
|
|
|
st.errorMsg = (e as Error).message
|
2026-08-01 17:11:17 +08:00
|
|
|
|
ElMessage.error(`员工列表加载失败:${(e as Error).message}`)
|
|
|
|
|
|
} finally {
|
2026-08-05 14:13:21 +08:00
|
|
|
|
st.loading = false
|
2026-08-01 17:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function handlePageChange(ch: TabKey, p: number): void {
|
|
|
|
|
|
tabState[ch].page = p
|
|
|
|
|
|
loadTab(ch)
|
2026-08-01 17:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function handlePageSizeChange(ch: TabKey, s: number): void {
|
|
|
|
|
|
tabState[ch].pageSize = s
|
|
|
|
|
|
tabState[ch].page = 1
|
|
|
|
|
|
loadTab(ch)
|
2026-08-01 17:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
// Per-tab wrappers so template bindings carry proper types (vue-tsc strict
|
|
|
|
|
|
// mode does not infer inline-arrow params in @event handlers).
|
|
|
|
|
|
function onWechatPageChange(p: number): void {
|
|
|
|
|
|
handlePageChange('wechat', p)
|
|
|
|
|
|
}
|
|
|
|
|
|
function onWechatPageSizeChange(s: number): void {
|
|
|
|
|
|
handlePageSizeChange('wechat', s)
|
|
|
|
|
|
}
|
|
|
|
|
|
function onWhatsappPageChange(p: number): void {
|
|
|
|
|
|
handlePageChange('whatsapp', p)
|
|
|
|
|
|
}
|
|
|
|
|
|
function onWhatsappPageSizeChange(s: number): void {
|
|
|
|
|
|
handlePageSizeChange('whatsapp', s)
|
2026-08-01 17:47:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
function onTabChange(name: string | number): void {
|
|
|
|
|
|
const ch = String(name) as TabKey
|
|
|
|
|
|
if (!tabState[ch].loaded) loadTab(ch)
|
2026-08-01 17:47:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Drawer
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function openDrawer(row: Account): void {
|
|
|
|
|
|
drawerAccount.value = row
|
2026-08-01 17:47:50 +08:00
|
|
|
|
drawerOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawerSections = computed(() =>
|
2026-08-05 14:13:21 +08:00
|
|
|
|
drawerAccount.value ? buildAccountSections(drawerAccount.value) : [],
|
2026-08-01 17:47:50 +08:00
|
|
|
|
)
|
2026-08-05 14:13:21 +08:00
|
|
|
|
|
|
|
|
|
|
const drawerAvatarSrc = computed<string | undefined>(() => {
|
|
|
|
|
|
const a = drawerAccount.value
|
|
|
|
|
|
return a ? avatarUrl(a) : undefined
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// The visible tab on first paint is 微信员工 — load it. The other tab
|
|
|
|
|
|
// lazy-loads on activation.
|
|
|
|
|
|
loadTab('wechat')
|
|
|
|
|
|
})
|
2026-08-01 17:11:17 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="yk-staff">
|
|
|
|
|
|
<div class="yk-staff__header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 style="margin: 0 0 4px; font-size: 16px">员工列表</h2>
|
2026-08-05 14:13:21 +08:00
|
|
|
|
<span class="yk-staff__sub">
|
|
|
|
|
|
按渠道展示员工账号(微信号 / WhatsApp 号),点击行查看全部字段。
|
|
|
|
|
|
</span>
|
2026-08-01 17:11:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
<el-tabs v-model="activeTab" class="yk-staff__tabs" @tab-change="onTabChange">
|
|
|
|
|
|
<!-- ============================================================
|
|
|
|
|
|
微信员工 (WeChat staff accounts)
|
|
|
|
|
|
============================================================ -->
|
|
|
|
|
|
<el-tab-pane label="微信员工" name="wechat">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
v-if="tabState.wechat.errorMsg"
|
|
|
|
|
|
:title="tabState.wechat.errorMsg"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
style="margin-bottom: 12px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
v-loading="tabState.wechat.loading"
|
|
|
|
|
|
:data="tabState.wechat.items"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
border
|
|
|
|
|
|
row-key="account_id"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:row-class-name="() => 'yk-clickable-row'"
|
|
|
|
|
|
empty-text="暂无微信员工账号"
|
|
|
|
|
|
@row-click="openDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column label="头像" width="64" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<template v-if="avatarUrl(row)">
|
|
|
|
|
|
<el-image
|
|
|
|
|
|
:src="avatarUrl(row)"
|
|
|
|
|
|
fit="cover"
|
|
|
|
|
|
class="yk-avatar-round"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #error>
|
|
|
|
|
|
<div class="yk-avatar-fallback">{{ initials(row) }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-image>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-avatar v-else :size="36" shape="circle">
|
|
|
|
|
|
{{ initials(row) }}
|
|
|
|
|
|
</el-avatar>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="微信昵称" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'nickName') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="微信号" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'alias') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="手机号" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'phone') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="员工姓名" min-width="110">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'userName') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="好友数" width="90" align="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ cellNumber(row, 'contactsNum') ?? 0 }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="群数" width="80" align="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ cellNumber(row, 'groupNum') ?? 0 }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="发送" width="90" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'sendNum') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="接收" width="90" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'receiveNum') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="数据库大小" width="110" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ dbSizeText(row) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="在职" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
:type="onJobTagType(cell(row, 'isOnJob'))"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ onJobLabel(cell(row, 'isOnJob')) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="在线状态" width="100" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
v-if="cell(row, 'onlineStatus') !== undefined && cell(row, 'onlineStatus') !== null && cell(row, 'onlineStatus') !== ''"
|
|
|
|
|
|
:type="onlineTagType(cell(row, 'onlineStatus'))"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ onlineLabel(cell(row, 'onlineStatus')) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span v-else style="color: #c0c4cc">--</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="最后更新" width="160">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatTime(cellTime(row, 'lastUpdate')) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="yk-staff__pager">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
:current-page="tabState.wechat.page"
|
|
|
|
|
|
:page-size="tabState.wechat.pageSize"
|
|
|
|
|
|
:page-sizes="[20, 50, 100]"
|
|
|
|
|
|
:total="tabState.wechat.total"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
background
|
|
|
|
|
|
@current-change="onWechatPageChange"
|
|
|
|
|
|
@size-change="onWechatPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============================================================
|
|
|
|
|
|
WhatsApp员工 (WhatsApp staff accounts)
|
|
|
|
|
|
============================================================ -->
|
|
|
|
|
|
<el-tab-pane label="WhatsApp员工" name="whatsapp">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
v-if="tabState.whatsapp.errorMsg"
|
|
|
|
|
|
:title="tabState.whatsapp.errorMsg"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
style="margin-bottom: 12px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
v-loading="tabState.whatsapp.loading"
|
|
|
|
|
|
:data="tabState.whatsapp.items"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
border
|
|
|
|
|
|
row-key="account_id"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:row-class-name="() => 'yk-clickable-row'"
|
|
|
|
|
|
empty-text="暂无 WhatsApp 员工账号"
|
|
|
|
|
|
@row-click="openDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column label="头像" width="64" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-avatar :size="36" shape="circle">
|
|
|
|
|
|
{{ initials(row) }}
|
|
|
|
|
|
</el-avatar>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="昵称" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'nickName') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="员工姓名" min-width="110">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'userName') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="手机" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'phone') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="销售手机" min-width="120">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'salePhone') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="好友数" width="90" align="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ cellNumber(row, 'contactsNum') ?? 0 }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="群数" width="80" align="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ cellNumber(row, 'groupNum') ?? 0 }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="联系人数" width="100" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'talkerCount') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="新增好友" width="100" align="right">
|
|
|
|
|
|
<template #default="{ row }">{{ cellText(row, 'friendAddNum') }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="在职" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
:type="onJobTagType(cell(row, 'isOnJob'))"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ onJobLabel(cell(row, 'isOnJob')) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="最后更新" width="160">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatTime(cellTime(row, 'lastUpdate')) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="yk-staff__pager">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
:current-page="tabState.whatsapp.page"
|
|
|
|
|
|
:page-size="tabState.whatsapp.pageSize"
|
|
|
|
|
|
:page-sizes="[20, 50, 100]"
|
|
|
|
|
|
:total="tabState.whatsapp.total"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
background
|
|
|
|
|
|
@current-change="onWhatsappPageChange"
|
|
|
|
|
|
@size-change="onWhatsappPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
2026-08-01 17:47:50 +08:00
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
<p class="yk-staff__hint">点击任意一行查看员工账号全部字段。</p>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- All-fields drawer (reused from AccountList.vue / accountFields.ts) -->
|
2026-08-01 17:47:50 +08:00
|
|
|
|
<el-drawer
|
|
|
|
|
|
v-model="drawerOpen"
|
2026-08-05 14:13:21 +08:00
|
|
|
|
:title="drawerAccount ? `员工详情 · ${drawerAccount.user_name ?? drawerAccount.nickname ?? drawerAccount.account_id}` : '员工详情'"
|
2026-08-01 17:47:50 +08:00
|
|
|
|
direction="rtl"
|
2026-08-05 14:13:21 +08:00
|
|
|
|
size="560px"
|
2026-08-01 17:47:50 +08:00
|
|
|
|
>
|
2026-08-05 14:13:21 +08:00
|
|
|
|
<template v-if="drawerAccount">
|
|
|
|
|
|
<div class="yk-drawer__avatar">
|
|
|
|
|
|
<el-avatar :size="56" :src="drawerAvatarSrc" shape="square">
|
|
|
|
|
|
{{ initials(drawerAccount) }}
|
|
|
|
|
|
</el-avatar>
|
|
|
|
|
|
<div class="yk-drawer__avatar-meta">
|
|
|
|
|
|
<div class="yk-drawer__avatar-name">
|
|
|
|
|
|
{{ drawerAccount.user_name ?? drawerAccount.nickname ?? drawerAccount.account_id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="yk-drawer__avatar-sub">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
:type="channelTagType(drawerAccount.channel)"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ channelLabel(drawerAccount.channel) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
:type="onJobTagType(cell(drawerAccount, 'isOnJob'))"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
style="margin-left: 6px"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ onJobLabel(cell(drawerAccount, 'isOnJob')) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-01 17:47:50 +08:00
|
|
|
|
<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>
|
2026-08-05 14:13:21 +08:00
|
|
|
|
<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>
|
2026-08-01 17:47:50 +08:00
|
|
|
|
<dd v-else>{{ f.text }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-drawer>
|
2026-08-01 17:11:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.yk-staff__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.yk-staff__sub {
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
.yk-staff__tabs {
|
|
|
|
|
|
margin-top: 4px;
|
2026-08-01 17:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.yk-staff__pager {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
2026-08-01 17:47:50 +08:00
|
|
|
|
|
|
|
|
|
|
.yk-staff__hint {
|
|
|
|
|
|
margin: 8px 2px 0;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
/* Round 36px avatar — el-image renders an <img> inside; size + round it. */
|
|
|
|
|
|
.yk-avatar-round {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.yk-avatar-round :deep(img) {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.yk-avatar-fallback {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
line-height: 36px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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: 4px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 17:47:50 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 14:13:21 +08:00
|
|
|
|
.yk-drawer__img :deep(.el-image) {
|
|
|
|
|
|
width: 64px;
|
|
|
|
|
|
height: 64px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 17:47:50 +08:00
|
|
|
|
.yk-drawer__empty {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
2026-08-01 17:11:17 +08:00
|
|
|
|
</style>
|