@@ -1,14 +1,19 @@
import { useEffect , useState , useMemo } from 'react' ;
import { useEffect , useState , useCallback } from 'react' ;
import {
Card , Table , Button , Select , Input , Tag , Row , Col , Statistic ,
Space , Tabs , Typography , Badge , theme ,
Space , Tabs , Typography , Badge , theme , Tooltip ,
} from 'antd' ;
import { message } from '../utils/antdMessage' ;
import {
UserOutlined , CheckCircleOutlined , TeamOutlined ,
SwapOutlined , ClearOutlined , UsergroupAddOutlined ,
} from '@ant-design/icons' ;
import { accountApi , type AccountItem , type SupportUserItem } from '../api/modules' ;
import {
accountApi ,
type AccountItem ,
type SupportUserItem ,
type AssignmentsSummary ,
} from '../api/modules' ;
import { getErrorMessage } from '../utils/error' ;
const { Text } = Typography ;
@@ -17,13 +22,19 @@ const STORAGE_KEY_SELECTED_USER = 'assignments_selected_user_id';
export default function AssignmentsPage() {
const { token } = theme . useToken ( ) ;
const [ accounts , setAccounts ] = useState < AccountItem [ ] > ( [ ] ) ;
const [ summary , setSummary ] = useState < AssignmentsSummary | null > ( null ) ;
const [ supportUsers , setSupportUsers ] = useState < SupportUserItem [ ] > ( [ ] ) ;
const [ selectedUser , setSelectedUser ] = useState < SupportUserItem | null > ( null ) ;
const [ allTags , setAllTags ] = useState < string [ ] > ( [ ] ) ;
// 表格分页与筛选状态
const [ pagedAccounts , setPagedAccounts ] = useState < AccountItem [ ] > ( [ ] ) ;
const [ totalCount , setTotalCount ] = useState ( 0 ) ;
const [ tableLoading , setTableLoading ] = useState ( false ) ;
const [ selectedRowKeys , setSelectedRowKeys ] = useState < number [ ] > ( [ ] ) ;
const [ searchText , setSearchText ] = useState ( '' ) ;
const [ filterTag , setFilterTag ] = useState < string | undefined > ( undefined ) ;
const [ activeTab , setActiveTab ] = useState ( 'unassigned' ) ;
const [ activeTab , setActiveTab ] = useState < 'unassigned' | 'assigned' > ( 'unassigned' ) ;
const [ assigning , setAssigning ] = useState ( false ) ;
const [ pageSize , setPageSize ] = useState ( ( ) = > {
const v = localStorage . getItem ( 'assignment_page_size' ) ;
@@ -31,83 +42,86 @@ export default function AssignmentsPage() {
} ) ;
const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
// 加载分配概览
const loadSummary = async ( ) = > {
// 加载分配概览统计
const loadSummary = useCallback ( async ( ) = > {
try {
const data = await accountApi . assignmentsSummary ( ) ;
setSummary ( data ) ;
setSupportUsers ( data . support_users ) ;
return data . support_users ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
return [ ] ;
}
} ;
} , [ ] ) ;
// 加载账号(仅已成功登录过的)
const loadAccounts = async ( ) = > {
// 加载标签列表
const loadTags = useCallback ( async ( ) = > {
try {
const all = await accountApi . list ( { has_cookie : true , include_sensitive : false } ) ;
setAccounts ( all ) ;
const tags = await accountApi . listTags ( ) ;
setAllTags ( tags ) ;
} catch {
// 忽略标签加载异常
}
} , [ ] ) ;
// 分页按需加载账号(服务端过滤与分页)
const loadPagedAccounts = useCallback ( async ( ) = > {
setTableLoading ( true ) ;
try {
const params : Parameters < typeof accountApi.listPaged > [ 0 ] = {
page : currentPage ,
page_size : pageSize ,
has_cookie : true ,
include_sensitive : false ,
search : searchText.trim ( ) || undefined ,
tag : filterTag || undefined ,
} ;
if ( activeTab === 'unassigned' ) {
params . assign_status = 'unassigned' ;
} else if ( selectedUser ) {
params . assigned_user_id = selectedUser . id ;
}
const data = await accountApi . listPaged ( params ) ;
setPagedAccounts ( data . items ) ;
setTotalCount ( data . total ) ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
} finally {
setTableLoading ( false ) ;
}
} ;
} , [ activeTab , selectedUser , currentPage , pageSize , searchText , filterTag ] ) ;
// 初始化加载 + 自动选中客服
// 初始化加载
useEffect ( ( ) = > {
const init = async ( ) = > {
loadAccounts ( ) ;
loadTags ( ) ;
const users = await loadSummary ( ) ;
if ( users . length > 0 ) {
const savedId = localStorage . getItem ( STORAGE_KEY_SELECTED_USER ) ;
const savedUser = savedId ? users . find ( ( u : SupportUserItem ) = > u . id === Number ( savedId ) ) : null ;
const savedUser = savedId ? users . find ( ( u ) = > u . id === Number ( savedId ) ) : null ;
setSelectedUser ( savedUser || users [ 0 ] ) ;
}
} ;
init ( ) ;
} , [ ] ) ;
} , [ loadSummary , loadTags ] ) ;
// 标签列表
const allTags = useMemo ( ( ) = > {
const tags = [ . . . new Set ( accounts . map ( ( a ) = > ( a . tag || '' ) . trim ( ) ) . filter ( Boolean ) ) ] ;
return tags . sort ( ) ;
} , [ accounts ] ) ;
// 未分配账号
const unassignedAccounts = useMemo ( ( ) = > {
return accounts . filter ( ( a ) = > ! a . assigned_to ) ;
} , [ accounts ] ) ;
// 当前选中客服的已分配账号
const assignedAccounts = useMemo ( ( ) = > {
if ( ! selectedUser ) return [ ] ;
return accounts . filter ( ( a ) = > a . assigned_to === selectedUser . id ) ;
} , [ accounts , selectedUser ] ) ;
// 当前显示的账号集合
const displayAccounts = useMemo ( ( ) = > {
let list = activeTab === 'unassigned' ? unassignedAccounts : assignedAccounts ;
if ( searchText ) {
const s = searchText . toLowerCase ( ) ;
list = list . filter ( ( a ) = > a . username . toLowerCase ( ) . includes ( s ) ) ;
}
if ( filterTag ) {
list = list . filter ( ( a ) = > ( a . tag || '' ) . trim ( ) === filterTag ) ;
}
return list ;
} , [ activeTab , unassignedAccounts , assignedAccounts , searchText , filterTag ] ) ;
// 当筛选或选中客服变化时触发分页请求
useEffect ( ( ) = > {
loadPagedAccounts ( ) ;
} , [ loadPagedAccounts ] ) ;
// 切换客服
const handleSelectUser = ( user : SupportUserItem ) = > {
setSelectedUser ( user ) ;
setSelectedRowKeys ( [ ] ) ;
setActiveTab ( 'unassigned' ) ;
setSearchText ( '' ) ;
setFilterTag ( undefined ) ;
setCurrentPage ( 1 ) ;
localStorage . setItem ( STORAGE_KEY_SELECTED_USER , String ( user . id ) ) ;
} ;
// 批量分配
// 批量分配选中账号
const handleBatchAssign = async ( ) = > {
if ( selectedRowKeys . length === 0 ) {
message . warning ( '请先选择账号' ) ;
@@ -122,10 +136,8 @@ export default function AssignmentsPage() {
await accountApi . batchAssign ( selectedRowKeys , selectedUser . id ) ;
message . success ( ` 已将 ${ selectedRowKeys . length } 个账号分配给 ${ selectedUser . username } ` ) ;
setSelectedRowKeys ( [ ] ) ;
await loadAccounts ( ) ;
const users = await loadSummary ( ) ;
const refreshed = users . find ( ( u : SupportUserItem ) = > u . id === selectedUser . id ) ;
if ( refreshed ) setSelectedUser ( refreshed ) ;
await loadSummary ( ) ;
await loadPagedAccounts ( ) ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
} finally {
@@ -144,10 +156,8 @@ export default function AssignmentsPage() {
await accountApi . batchAssign ( selectedRowKeys , null ) ;
message . success ( ` 已取消 ${ selectedRowKeys . length } 个账号的分配 ` ) ;
setSelectedRowKeys ( [ ] ) ;
await loadAccounts ( ) ;
const users = await loadSummary ( ) ;
const refreshed = users . find ( ( u : SupportUserItem ) = > u . id === selectedUser ? . id ) ;
if ( refreshed ) setSelectedUser ( refreshed ) ;
await loadSummary ( ) ;
await loadPagedAccounts ( ) ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
} finally {
@@ -155,31 +165,29 @@ export default function AssignmentsPage() {
}
} ;
// 一键将未分配账号 全部分配给选中客服
// 一键将当前 未分配(含搜索/标签筛选) 全部分配给选中客服
const handleAssignAllUnassigned = async ( ) = > {
if ( ! selectedUser ) {
message . warning ( '请先选择客服' ) ;
return ;
}
const ids = unassignedAccounts
. filter ( ( a ) = > {
if ( searchText && ! a . username . toLowerCase ( ) . includes ( searchText . toLowerCase ( ) ) ) return false ;
if ( filterTag && ( a . tag || '' ) . trim ( ) !== filterTag ) return false ;
return true ;
} )
. map ( ( a ) = > a . id ) ;
if ( ids . length === 0 ) {
message . info ( '没有未分配的账号' ) ;
if ( totalCount === 0 ) {
message . info ( '当前筛选下没有未分配的账号' ) ;
return ;
}
setAssigning ( true ) ;
try {
await accountApi . batchAssign ( ids , selectedUser . id ) ;
message . success ( ` 已将 ${ ids . length } 个账号分配给 ${ selectedUser . username } ` ) ;
await loadAccounts ( ) ;
const users = await loadSummary ( ) ;
const refreshed = users . find ( ( u : SupportUserItem ) = > u . id === selectedUser . id ) ;
if ( refreshed ) setSelectedUser ( refreshed ) ;
const res = await accountApi . batchAssignSelection ( {
all_matching : true ,
assign_status : 'unassigned' ,
tag : filterTag || undefined ,
search : searchText.trim ( ) || undefined ,
assigned_to : selectedUser.id ,
} ) ;
message . success ( res . message || ` 已全部分配给 ${ selectedUser . username } ` ) ;
setSelectedRowKeys ( [ ] ) ;
await loadSummary ( ) ;
await loadPagedAccounts ( ) ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
} finally {
@@ -187,18 +195,52 @@ export default function AssignmentsPage() {
}
} ;
// 统计
const assignedCount = accounts . filter ( ( a ) = > ! ! a . assigned_to ) . length ;
const unassignedCount = accounts . length - assignedCount ;
// 统计数据
const assignedCount = summary ? . assigned_count ? ? 0 ;
const unassignedCount = summary ? . unassigned_count ? ? 0 ;
const totalAccountCount = summary ? . total_count ? ? ( assignedCount + unassignedCount ) ;
// 获取当前选中客服已分配总数
const currentSelectedUserAssignedCount =
( selectedUser ? supportUsers . find ( ( u ) = > u . id === selectedUser . id ) ? . assigned_count : 0 ) ? ?
selectedUser ? . assigned_count ? ?
0 ;
const columns = [
{ title : 'ID' , dataIndex : 'id' , width : 60 } ,
{ title : '用户名' , dataIndex : 'username' } ,
{ title : 'ID' , dataIndex : 'id' , width : 70 } ,
{
title : '用户名' ,
dataIndex : 'username' ,
render : ( text : string ) = > < Text strong > { text } < / Text > ,
} ,
{
title : '标签' ,
dataIndex : 'tag' ,
width : 100 ,
render : ( tag : string ) = > tag ? < Tag color = "blue" > { tag } < / Tag > : < Text type = "secondary" > - < / Text > ,
width : 120 ,
render : ( tag : string ) = > ( tag ? < Tag color = "blue" > { tag } < / Tag > : < Text type = "secondary" > - < / Text > ) ,
} ,
{
title : '积分 / 鱼翅' ,
dataIndex : 'points' ,
width : 140 ,
render : ( _ : unknown , record : AccountItem ) = > (
< Space size = { 4 } >
< span > 积 分 { record . points ? ? '-' } < / span >
< Text type = "secondary" style = { { fontSize : 12 } } >
/ 鱼 翅 { record . gold_balance ? ? '-' }
< / Text >
< / Space >
) ,
} ,
{
title : '状态' ,
dataIndex : 'status' ,
width : 110 ,
render : ( status : string , record : AccountItem ) = > (
< Tag color = { status === 'active' ? 'green' : 'default' } >
{ record . status_label || status || '-' }
< / Tag >
) ,
} ,
] ;
@@ -208,22 +250,38 @@ export default function AssignmentsPage() {
{ /* 统计卡片 */ }
< Row gutter = { 12 } style = { { flexShrink : 0 } } >
< Col span = { 6 } >
< Card size = "small" > < Statistic title = "总账号" value = { accounts . length } prefix = { < TeamOutlined / > } / > < / Card >
< / Col >
< Col span = { 6 } >
< Card size = "small" >
< Statistic title = "已分配 " value = { assignedCount } styles = { { content : { color : token.colorSuccess } } } prefix = { < CheckCircleOutlined / > } / >
< Statistic title = "总账号 (持CK) " value = { totalAccountCount } prefix = { < TeamOutlined / > } / >
< / Card >
< / Col >
< Col span = { 6 } >
< Card size = "small" >
< Statistic title = "未分配" value = { unassignedCount } styles = { { content : { color : token.colorError } } } prefix = { < SwapOutlined / > } / >
< Statistic
title = "已分配"
value = { assignedCount }
styles = { { content : { color : token.colorSuccess } } }
prefix = { < CheckCircleOutlined / > }
/ >
< / Card >
< / Col >
< Col span = { 6 } >
< Card size = "small" >
< Statistic title = "客服人数" value = { supportUsers . length } prefix = { < UsergroupAddOutlined / > } / >
< Statistic
title = "未分配"
value = { unassignedCount }
styles = { { content : { color : token.colorError } } }
prefix = { < SwapOutlined / > }
/ >
< / Card >
< / Col >
< Col span = { 6 } >
< Card size = "small" >
< Statistic
title = "客服人数"
value = { supportUsers . length }
prefix = { < UsergroupAddOutlined / > }
/ >
< / Card >
< / Col >
< / Row >
@@ -236,7 +294,7 @@ export default function AssignmentsPage() {
title = "客服列表"
size = "small"
style = { { flex : 1 , display : 'flex' , flexDirection : 'column' , minHeight : 0 } }
bodyStyle = { { flex : 1 , overflow : 'auto' , padding : '8px' } }
styles = { { body : { flex : 1 , overflow : 'auto' , padding : '8px' } } }
>
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 } } >
{ supportUsers . map ( ( user ) = > {
@@ -267,7 +325,14 @@ export default function AssignmentsPage() {
< UserOutlined style = { { color : token.colorPrimary } } / >
< Text strong > { user . username } < / Text >
< / Space >
< Badge count = { user . assigned_count } showZero style = { { backgroundColor : token.colorPrimary } } / >
< Tooltip title = { ` 已分配 ${ user . assigned_count } 个账号 ` } >
< Badge
count = { user . assigned_count }
overflowCount = { 999999 }
showZero
style = { { backgroundColor : token.colorPrimary } }
/ >
< / Tooltip >
< / div >
) ;
} ) }
@@ -290,7 +355,7 @@ export default function AssignmentsPage() {
< >
< span > 已 选 中 客 服 : < / span >
< Tag color = "blue" style = { { fontSize : 14 , padding : '2px 10px' } } >
{ selectedUser . username } ( 已 分 配 { selectedUser . assigned_count } 个 )
{ selectedUser . username } ( 已 分 配 { currentSelectedUserAssignedCount } 个 )
< / Tag >
< / >
) : (
@@ -299,15 +364,27 @@ export default function AssignmentsPage() {
< / Space >
}
style = { { flex : 1 , display : 'flex' , flexDirection : 'column' , minHeight : 0 } }
bodyStyle = { { flex : 1 , display : 'flex' , flexDirection : 'column' , minHeight : 0 , padding : 0 } }
styles = { { body : { flex : 1 , display : 'flex' , flexDirection : 'column' , minHeight : 0 , padding : 0 } } }
>
{ /* 筛选栏 */ }
< div style = { { padding : '8px 16px' , borderBottom : ` 1px solid ${ token . colorBorderSecondary } ` , display : 'flex' , gap : 8 , alignItems : 'center' , flexShrink : 0 } } >
< div
style = { {
padding : '8px 16px' ,
borderBottom : ` 1px solid ${ token . colorBorderSecondary } ` ,
display : 'flex' ,
gap : 8 ,
alignItems : 'center' ,
flexShrink : 0 ,
} }
>
< Input.Search
placeholder = "搜索用户名"
allowClear
value = { searchText }
onChange = { ( e ) = > setSearchText ( e . target . value ) }
onChange = { ( e ) = > {
setSearchText ( e . target . value ) ;
setCurrentPage ( 1 ) ;
} }
style = { { width : 200 } }
size = "small"
/ >
@@ -316,7 +393,10 @@ export default function AssignmentsPage() {
showSearch
placeholder = "按标签筛选"
value = { filterTag }
onChange = { ( v ) = > setFilterTag ( v || undefined ) }
onChange = { ( v ) = > {
setFilterTag ( v || undefined ) ;
setCurrentPage ( 1 ) ;
} }
filterOption = { ( input , option ) = >
String ( option ? . label || '' ) . toLowerCase ( ) . includes ( input . trim ( ) . toLowerCase ( ) )
}
@@ -330,17 +410,21 @@ export default function AssignmentsPage() {
< Tabs
size = "small"
activeKey = { activeTab }
onChange = { ( key ) = > { setActiveTab ( key ) ; setSelectedRowKeys ( [ ] ) ; } }
onChange = { ( key ) = > {
setActiveTab ( key as 'unassigned' | 'assigned' ) ;
setSelectedRowKeys ( [ ] ) ;
setCurrentPage ( 1 ) ;
} }
style = { { padding : '0 16px' , flexShrink : 0 , marginBottom : 0 } }
items = { [
{
key : 'unassigned' ,
label : ` 未分配 ( ${ unassignedAccounts . length } ) ` ,
label : ` 未分配 ( ${ unassignedCount } ) ` ,
} ,
{
key : 'assigned' ,
label : selectedUser
? ` 已分配给 ${ selectedUser . username } ( ${ assignedAccounts . length } ) `
? ` 已分配给 ${ selectedUser . username } ( ${ currentSelectedUserAssignedCount } ) `
: '已分配账号' ,
disabled : ! selectedUser ,
} ,
@@ -389,12 +473,14 @@ export default function AssignmentsPage() {
{ /* 表格 */ }
< Table
columns = { columns }
dataSource = { displayAccounts }
dataSource = { pagedAccounts }
rowKey = "id"
size = "small"
loading = { tableLoading }
pagination = { {
current : currentPage ,
pageSize ,
total : totalCount ,
size : 'small' ,
showSizeChanger : true ,
showTotal : ( t ) = > ` 共 ${ t } 条 ` ,
@@ -403,7 +489,7 @@ export default function AssignmentsPage() {
if ( size !== pageSize ) {
setPageSize ( size ) ;
localStorage . setItem ( 'assignment_page_size' , String ( size ) ) ;
setCurrentPage ( 1 ) ; // 切换 pageSize 时重置到第1页
setCurrentPage ( 1 ) ;
}
} ,
} }
@@ -411,7 +497,7 @@ export default function AssignmentsPage() {
selectedRowKeys ,
onChange : ( keys ) = > setSelectedRowKeys ( keys as number [ ] ) ,
} }
scroll = { { y : 'calc(100vh - 50 0px)' } }
scroll = { { y : 'calc(100vh - 46 0px)' } }
/ >
< / Card >
< / Col >