@@ -1,367 +0,0 @@
< script setup lang = "ts" >
import { computed , onMounted , ref , watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getAccounts } from '@/api'
import type { Account , Channel } from '@/api/types'
import { useChannelStore } from '@/stores/channel'
import { channelLabel , formatTime } from '@/utils/format'
import { buildAccountSections , readField } from '@/utils/accountFields'
/**
* 账号 list — unified WeChat + WhatsApp staff messaging accounts. Mirrors the
* 云客 微信号管理 / WhatsApp views: 渠道 / 账号ID / 昵称 / 微信号 / 归属员工 /
* 好友数 / 群数 / 收发 / 在职 / 最后更新. Clicking a row opens the all-fields
* drawer (every raw_json key, grouped + 中文-labeled).
*/
const channelStore = useChannelStore ( )
const accounts = ref < Account [ ] > ( [ ] )
const total = ref ( 0 )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const errorMsg = ref < string | null > ( null )
// All-fields drawer state.
const drawerOpen = ref ( false )
const drawerAccount = ref < Account | null > ( null )
const channelOptions = [
{ value : 'all' as const , label : '全部渠道' } ,
{ value : 'wechat' as const , label : '微信' } ,
{ value : 'whatsapp' as const , label : 'WhatsApp' } ,
]
const channelLabelCurrent = computed ( ( ) =>
channelLabel ( channelStore . channel === 'all' ? null : channelStore . channel ) ,
)
function channelTagType ( c : string ) : 'success' | 'warning' {
return c === 'wechat' ? 'success' : 'warning'
}
function onChannelChange ( value : string | number | boolean | undefined ) : void {
channelStore . setChannel ( value as Channel | 'all' )
}
/** Read a raw_json camelCase key off an account, preferring the structured
* snake_case column when it is populated. */
function cell ( row : Account , camelKey : string ) : unknown {
return readField ( row , camelKey )
}
function cellText ( row : Account , camelKey : string ) : string {
const v = cell ( row , camelKey )
return v === null || v === undefined || v === '' ? '--' : String ( v )
}
function timeCell ( row : Account , camelKey : string ) : number | undefined {
const v = cell ( row , camelKey )
return typeof v === 'number' ? v : undefined
}
function onJobTagType ( onJob ? : boolean | null ) : 'success' | 'info' | '' {
if ( onJob === true ) return 'success'
if ( onJob === false ) return 'info'
return ''
}
function onJobLabel ( onJob ? : boolean | null ) : string {
if ( onJob === true ) return '在职'
if ( onJob === false ) return '离职'
return '--'
}
async function load ( ) : Promise < void > {
loading . value = true
errorMsg . value = null
try {
const res = await getAccounts ( {
channel : channelStore . channel ,
page : page . value ,
page _size : pageSize . value ,
} )
accounts . 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 ( )
}
// React to the top-bar channel selector.
watch (
( ) => channelStore . channel ,
( ) => {
page . value = 1
load ( )
} ,
)
function openDrawer ( row : Account ) : void {
drawerAccount . value = row
drawerOpen . value = true
}
const drawerSections = computed ( ( ) =>
drawerAccount . value ? buildAccountSections ( drawerAccount . value ) : [ ] ,
)
onMounted ( load )
< / script >
< template >
< div class = "yk-accounts" >
< div class = "yk-accounts__header" >
< div >
< h2 style = "margin: 0 0 4px; font-size: 16px" > 账号列表 < / h2 >
< span class = "yk-accounts__sub" >
共 { { total } } 条 · 渠道 : { { channelLabelCurrent } }
< / span >
< / div >
< div class = "yk-accounts__filters" >
< el-radio-group
:model-value = "channelStore.channel"
size = "small"
@change ="onChannelChange"
>
< el-radio-button
v-for = "opt in channelOptions"
:key = "opt.value"
:value = "opt.value"
>
{ { opt . label } }
< / el-radio-button >
< / el-radio-group >
< / 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 = "accounts"
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=" 100 ">
<template #default=" { row } ">
<el-tag :type=" channelTagType ( row . channel ) " size=" small " effect=" plain ">
{{ channelLabel(row.channel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label=" 账号ID " min-width=" 160 ">
<template #default=" { row } ">{{ row.account_id }}</template>
</el-table-column>
<el-table-column label=" 昵称 " min-width=" 120 ">
<template #default=" { row } ">
<span v-if=" row . nickname ">{{ row.nickname }}</span>
<span v-else style=" color : # c0c4cc ">--</span>
</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 } ">
<span v-if=" cell ( row , 'userName' ) || row . user _name ">
{{ cellText(row, 'userName') }}
</span>
<span v-else style=" color : # c0c4cc ">--</span>
</template>
</el-table-column>
<el-table-column label=" 好友数 " prop=" contacts _num " width=" 90 " align=" right ">
<template #default=" { row } ">
{{ row.contacts_num ?? 0 }}
</template>
</el-table-column>
<el-table-column label=" 群数 " prop=" group _num " width=" 80 " align=" right ">
<template #default=" { row } ">
{{ row.group_num ?? 0 }}
</template>
</el-table-column>
<el-table-column label=" 发消息数 " width=" 100 " align=" right ">
<template #default=" { row } ">{{ cellText(row, 'sendNum') }}</template>
</el-table-column>
<el-table-column label=" 收消息数 " width=" 100 " align=" right ">
<template #default=" { row } ">{{ cellText(row, 'receiveNum') }}</template>
</el-table-column>
<el-table-column label=" 在职 " width=" 90 " align=" center ">
<template #default=" { row } ">
<el-tag
:type=" onJobTagType ( row . is _on _job ) "
size=" small "
effect=" plain "
>
{{ onJobLabel(row.is_on_job) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label=" 最后更新 " width=" 160 ">
<template #default=" { row } ">
{{ formatTime(timeCell(row, 'lastUpdate')) }}
</template>
</el-table-column>
</el-table>
<div class=" yk - accounts _ _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 - accounts _ _hint ">点击任意一行查看账号全部字段。</p>
<!-- All-fields drawer -->
<el-drawer
v-model=" drawerOpen "
:title=" drawerAccount ? ` 账号详情 · ${ drawerAccount . nickname ? ? drawerAccount . account _id } ` : '账号详情' "
direction=" rtl "
size=" 560 px "
>
<template v-if=" drawerAccount ">
<div class=" yk - drawer _ _topmeta ">
<el-tag :type=" channelTagType ( drawerAccount . channel ) " size=" small " effect=" plain ">
{{ channelLabel(drawerAccount.channel) }}
</el-tag>
<el-tag
:type=" onJobTagType ( drawerAccount . is _on _job ) "
size=" small "
effect=" plain "
>
{{ onJobLabel(drawerAccount.is_on_job) }}
</el-tag>
</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 >
< style scoped >
. yk - accounts _ _header {
display : flex ;
justify - content : space - between ;
align - items : flex - start ;
gap : 16 px ;
flex - wrap : wrap ;
margin - bottom : 12 px ;
}
. yk - accounts _ _sub {
color : # 909399 ;
font - size : 13 px ;
}
. yk - accounts _ _filters {
display : flex ;
gap : 8 px ;
align - items : center ;
flex - wrap : wrap ;
}
. yk - accounts _ _pager {
margin - top : 12 px ;
display : flex ;
justify - content : flex - end ;
}
. yk - accounts _ _hint {
margin : 8 px 2 px 0 ;
font - size : 12 px ;
color : # c0c4cc ;
}
. yk - drawer _ _topmeta {
display : flex ;
gap : 6 px ;
flex - wrap : wrap ;
margin - bottom : 12 px ;
}
. yk - drawer _ _kv {
margin : 0 0 8 px ;
}
. yk - drawer _ _kv - item {
display : grid ;
grid - template - columns : 110 px 1 fr ;
gap : 8 px 12 px ;
padding : 3 px 0 ;
}
. yk - drawer _ _kv dt {
color : # 909399 ;
font - size : 13 px ;
word - break : break - all ;
}
. yk - drawer _ _kv dd {
margin : 0 ;
font - size : 13 px ;
color : # 303133 ;
word - break : break - all ;
min - width : 0 ;
}
. yk - drawer _ _img : deep ( . el - image ) {
width : 64 px ;
height : 64 px ;
border - radius : 4 px ;
border : 1 px solid var ( -- el - border - color - light , # e4e7ed ) ;
}
. yk - drawer _ _empty {
color : # c0c4cc ;
}
< / style >