@@ -5,13 +5,46 @@
>
< template # filters >
< div class = "bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-4" >
< div class = "flex flex-wrap items-center justify-between gap-3" >
< div class = "text-sm text-gray-600" > 共找到 { { total } } 个下属账号 < / div >
< div c lass = "flex items-center gap-2 " >
< el-button class = "toolbar-btn toolbar-btn-secondary" :loading = "listLoading" @click ="loadList" > 刷新列表 < / el -button >
< el-button class = "toolbar-btn toolbar-btn-primary" type = "primary" :loading = "invLoading" @click ="createInvite" > 生成新邀请 < / el -button >
< FilterSection >
< div class = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" >
< FilterItem label = "备注 " >
< el-input
v-model = "filters.remark"
placeholder = "模糊搜索备注"
clearable
@input ="handleFilterChange"
class = "w-full"
/ >
< / FilterItem >
< FilterItem label = "手机号" >
< el-input
v-model = "filters.phone"
placeholder = "模糊搜索手机号"
clearable
maxlength = "11"
@input ="handleFilterChange"
class = "w-full"
/ >
< / FilterItem >
< FilterItem label = "下属公司" >
< el-input
v-model = "filters.company_name"
placeholder = "模糊搜索公司名称"
clearable
@input ="handleFilterChange"
class = "w-full"
/ >
< / FilterItem >
< / div >
< / div >
< template # stats >
共找到 { { total } } 个下属账号
< / template >
< template # buttons >
< el-button class = "toolbar-btn toolbar-btn-secondary" @click ="resetFilters" > 重置 < / el -button >
< el-button class = "toolbar-btn toolbar-btn-secondary" :loading = "listLoading" @click ="loadList" > 刷新列表 < / el -button >
< el-button class = "toolbar-btn toolbar-btn-primary" type = "primary" :loading = "invLoading" @click ="createInvite" > 复制邀请链接 < / el -button >
< / template >
< / FilterSection >
< / div >
< / template >
@@ -46,12 +79,41 @@
< span class = "font-medium text-gray-700" > { { row . phone || '-' } } < / span >
< / template >
< / el-table-column >
< el-table-column prop = "company_name" label = "下属公司" min -width = " 22 0 " show -overflow -tooltip >
< el-table-column prop = "company_name" label = "下属公司" min -width = " 20 0 " show -overflow -tooltip >
< template # default = "{ row }" >
< el-tag v-if = "row.is_certified" type="success" size="small" > {{ row.company_name }} < / el -tag >
< el-tag v-else type = "info" size = "small" > 未认证 < / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "remark" label = "备注" min -width = " 180 " >
< template # default = "{ row }" >
< el-input
v-model = "row.remark"
placeholder = "添加备注"
size = "small"
clearable
:disabled = "row._remarkSaving"
@blur ="saveRemark(row)"
@keyup.enter ="($event.target)?.blur()"
/ >
< / template >
< / el-table-column >
< el-table-column label = "产品与剩余额度" min -width = " 280 " >
< template # default = "{ row }" >
< div v-if = "!row.product_quotas?.length" class="text-xs text-gray-400" > 暂无额度 < / div >
< div v-else class = "flex flex-wrap gap-1" >
< el-tag
v-for = "item in row.product_quotas"
:key = "item.product_id"
size = "small"
type = "info"
class = "quota-tag"
>
{ { item . product _name } } : { { item . available _quota } }
< / el-tag >
< / div >
< / template >
< / el-table-column >
< el-table-column prop = "registered_at" label = "注册时间" width = "220" >
< template # default = "{ row }" >
< div class = "text-sm text-gray-600" > { { formatDateTime ( row . registered _at ) } } < / div >
@@ -62,9 +124,17 @@
< span class = "font-semibold text-emerald-600" > ¥ { { formatMoney ( row . balance ) } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "操作" width = "11 0" fixed = "right" >
< el-table-column label = "操作" width = "20 0" fixed = "right" >
< template # default = "{ row }" >
< div class = "flex items-center gap-1" >
< div class = "flex flex-wrap items-center gap-1" >
< el-button
class = "table-action-btn action-calls"
text
bg
@click ="openApiCalls(row)"
>
调用记录
< / el-button >
< el-button
class = "table-action-btn action-allocate"
: class = "{ 'action-disabled': !row.is_certified }"
@@ -194,12 +264,116 @@
< el-button class = "dialog-btn dialog-btn-secondary" @click ="allocRecordVisible = false" > 关闭 < / el -button >
< / template >
< / el-dialog >
< el-dialog
v-model = "apiCallsVisible"
: title = "`下属调用记录${apiCallsTarget.company ? ' - ' + apiCallsTarget.company : ''}`"
width = "960px"
@close ="resetApiCallsDialog"
>
< div class = "space-y-3" >
< div class = "grid grid-cols-1 md:grid-cols-3 gap-3" >
< el-input
v-model = "apiCallsFilters.transaction_id"
placeholder = "交易ID"
clearable
@keyup.enter ="searchApiCalls"
/ >
< el-input
v-model = "apiCallsFilters.product_name"
placeholder = "产品名称"
clearable
@keyup.enter ="searchApiCalls"
/ >
< el-select v-model = "apiCallsFilters.status" placeholder="调用状态" clearable class="w-full" >
< el -option label = "全部" value = "" / >
< el-option label = "成功" value = "success" / >
< el-option label = "失败" value = "failed" / >
< el-option label = "处理中" value = "pending" / >
< / el-select >
< / div >
< div class = "flex items-center justify-between gap-2" >
< span class = "text-sm text-gray-500" > 共 { { apiCallsTotal } } 条记录 < / span >
< div class = "flex gap-2" >
< el-button class = "toolbar-btn toolbar-btn-secondary" @click ="resetApiCallsFilters" > 重置 < / el -button >
< el-button class = "toolbar-btn toolbar-btn-primary" type = "primary" :loading = "apiCallsLoading" @click ="searchApiCalls" > 查询 < / el -button >
< / div >
< / div >
< el-table v-loading = "apiCallsLoading" :data="apiCalls" border stripe max-height="420" size="small" >
< el -table -column prop = "transaction_id" label = "交易ID" min -width = " 170 " show -overflow -tooltip / >
< el-table-column prop = "product_name" label = "接口名称" min -width = " 140 " show -overflow -tooltip / >
< el-table-column prop = "status" label = "状态" width = "90" >
< template # default = "{ row }" >
< el-tag :type = "getApiCallStatusType(row.status)" size = "small" > { { getApiCallStatusText ( row . status ) } } < / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "translated_error_msg" label = "错误信息" min -width = " 160 " show -overflow -tooltip >
< template # default = "{ row }" >
< span > { { row . translated _error _msg || row . error _msg || '-' } } < / span >
< / template >
< / el-table-column >
< el-table-column prop = "client_ip" label = "客户端IP" width = "130" / >
< el-table-column prop = "start_at" label = "调用时间" width = "170" >
< template # default = "{ row }" >
{ { formatDateTime ( row . start _at ) } }
< / template >
< / el-table-column >
< el-table-column label = "操作" width = "90" fixed = "right" >
< template # default = "{ row }" >
< el-button size = "small" type = "primary" link @click ="viewApiCallDetail(row)" > 详情 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< div class = "pagination-wrapper !py-0" >
< el-pagination
v-if = "apiCallsTotal > 0"
v -model :current-page = "apiCallsPage"
v -model :page-size = "apiCallsPageSize"
: page -sizes = " [ 10 , 20 , 50 ] "
:total = "apiCallsTotal"
layout = "total, sizes, prev, pager, next"
@ size -change = " loadChildApiCalls "
@ current -change = " loadChildApiCalls "
/ >
< / div >
< / div >
< template # footer >
< el-button class = "dialog-btn dialog-btn-secondary" @click ="apiCallsVisible = false" > 关闭 < / el -button >
< / template >
< / el-dialog >
< el-dialog v-model = "apiCallDetailVisible" title="调用详情" width="760px" >
< div v-if = "selectedApiCall" class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm" >
< div > < span class = "text-gray-500" > 交易ID : < / span > { { selectedApiCall . transaction _id } } < / div >
< div >
< span class = "text-gray-500" > 状态 : < / span >
< el-tag :type = "getApiCallStatusType(selectedApiCall.status)" size = "small" >
{ { getApiCallStatusText ( selectedApiCall . status ) } }
< / el-tag >
< / div >
< div > < span class = "text-gray-500" > 接口名称 : < / span > { { selectedApiCall . product _name || '-' } } < / div >
< div > < span class = "text-gray-500" > 客户端IP : < / span > { { selectedApiCall . client _ip || '-' } } < / div >
< div > < span class = "text-gray-500" > 调用时间 : < / span > { { formatDateTime ( selectedApiCall . start _at ) } } < / div >
< div > < span class = "text-gray-500" > 完成时间 : < / span > { { selectedApiCall . end _at ? formatDateTime ( selectedApiCall . end _at ) : '-' } } < / div >
< div class = "md:col-span-2" >
< span class = "text-gray-500" > 错误信息 : < / span >
{ { selectedApiCall . translated _error _msg || selectedApiCall . error _msg || '-' } }
< / div >
< / div >
< template # footer >
< el-button class = "dialog-btn dialog-btn-secondary" @click ="apiCallDetailVisible = false" > 关闭 < / el -button >
< / template >
< / el-dialog >
< / template >
< / ListPageLayout >
< / template >
< script setup >
import { financeApi , subordinateApi , subscriptionApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
import { useUserStore } from '@/stores/user'
@@ -214,6 +388,11 @@ const page = ref(1)
const pageSize = ref ( 20 )
const listLoading = ref ( false )
const invLoading = ref ( false )
const filters = reactive ( {
remark : '' ,
phone : '' ,
company _name : ''
} )
const allocVisible = ref ( false )
const allocLoading = ref ( false )
@@ -240,6 +419,21 @@ const allocationPageSize = ref(10)
const parentSubscriptions = ref ( [ ] )
const apiCallsVisible = ref ( false )
const apiCallsLoading = ref ( false )
const apiCalls = ref ( [ ] )
const apiCallsTotal = ref ( 0 )
const apiCallsPage = ref ( 1 )
const apiCallsPageSize = ref ( 10 )
const apiCallsTarget = ref ( { childId : '' , company : '' , phone : '' } )
const apiCallsFilters = reactive ( {
transaction _id : '' ,
product _name : '' ,
status : ''
} )
const apiCallDetailVisible = ref ( false )
const selectedApiCall = ref ( null )
const lastInvite = ref ( '' )
const allocTotalAmount = computed ( ( ) => {
const selected = allocProducts . value . find ( ( item ) => item . product _id === allocForm . value . productId )
@@ -270,9 +464,22 @@ const formatMoney = (value) => {
const loadList = async ( ) => {
listLoading . value = true
try {
const res = await subordinateApi . listSubordinates ( { page : page . value , page _size : pageSize . value } )
const params = {
page : page . value ,
page _size : pageSize . value
}
if ( filters . remark . trim ( ) ) params . remark = filters . remark . trim ( )
if ( filters . phone . trim ( ) ) params . phone = filters . phone . trim ( )
if ( filters . company _name . trim ( ) ) params . company _name = filters . company _name . trim ( )
const res = await subordinateApi . listSubordinates ( params )
if ( res ? . success && res . data ) {
list . value = res . data . items || [ ]
list . value = ( res . data . items || [ ] ) . map ( ( item ) => ( {
... item ,
remark : item . remark || '' ,
_remarkOriginal : item . remark || '' ,
_remarkSaving : false
} ) )
total . value = res . data . total || 0
}
} catch ( e ) {
@@ -282,13 +489,51 @@ const loadList = async () => {
}
}
const handleFilterChange = ( ) => {
page . value = 1
loadList ( )
}
const resetFilters = ( ) => {
filters . remark = ''
filters . phone = ''
filters . company _name = ''
page . value = 1
loadList ( )
}
const saveRemark = async ( row ) => {
if ( row . _remarkSaving ) return
const current = ( row . remark || '' ) . trim ( )
const original = ( row . _remarkOriginal || '' ) . trim ( )
if ( current === original ) return
row . _remarkSaving = true
try {
const res = await subordinateApi . updateRemark ( {
child _user _id : row . child _user _id ,
remark : current
} )
if ( res ? . success ) {
row . remark = current
row . _remarkOriginal = current
ElMessage . success ( '备注已保存' )
}
} catch ( e ) {
row . remark = row . _remarkOriginal
ElMessage . error ( e ? . response ? . data ? . message || e ? . message || '备注保存失败' )
} finally {
row . _remarkSaving = false
}
}
const createInvite = async ( ) => {
invLoading . value = true
try {
const res = await subordinateApi . createInvitation ( { } )
if ( res ? . success && res . data ) {
lastInvite . value = res . data . invite _url || res . data . invite _token
ElMessage . success ( '邀请已生成,请复制链接发送给下属 ' )
ElMessage . success ( '邀请链接已复制,可邀请多名下属重复使用 ' )
await navigator . clipboard . writeText ( res . data . invite _url || res . data . invite _token ) . catch ( ( ) => { } )
}
} catch ( e ) {
@@ -462,6 +707,99 @@ const loadAllocationRecords = async () => {
}
}
const getApiCallStatusType = ( status ) => {
switch ( status ) {
case 'success' :
return 'success'
case 'failed' :
return 'danger'
case 'pending' :
return 'warning'
default :
return 'info'
}
}
const getApiCallStatusText = ( status ) => {
switch ( status ) {
case 'success' :
return '成功'
case 'failed' :
return '失败'
case 'pending' :
return '处理中'
default :
return '未知'
}
}
const resetApiCallsDialog = ( ) => {
apiCalls . value = [ ]
apiCallsTotal . value = 0
apiCallsPage . value = 1
apiCallsPageSize . value = 10
apiCallsTarget . value = { childId : '' , company : '' , phone : '' }
apiCallsFilters . transaction _id = ''
apiCallsFilters . product _name = ''
apiCallsFilters . status = ''
selectedApiCall . value = null
apiCallDetailVisible . value = false
}
const resetApiCallsFilters = ( ) => {
apiCallsFilters . transaction _id = ''
apiCallsFilters . product _name = ''
apiCallsFilters . status = ''
apiCallsPage . value = 1
loadChildApiCalls ( )
}
const searchApiCalls = ( ) => {
apiCallsPage . value = 1
loadChildApiCalls ( )
}
const loadChildApiCalls = async ( ) => {
if ( ! apiCallsTarget . value . childId ) return
apiCallsLoading . value = true
try {
const params = {
child _user _id : apiCallsTarget . value . childId ,
page : apiCallsPage . value ,
page _size : apiCallsPageSize . value
}
if ( apiCallsFilters . transaction _id . trim ( ) ) params . transaction _id = apiCallsFilters . transaction _id . trim ( )
if ( apiCallsFilters . product _name . trim ( ) ) params . product _name = apiCallsFilters . product _name . trim ( )
if ( apiCallsFilters . status ) params . status = apiCallsFilters . status
const res = await subordinateApi . listChildApiCalls ( params )
if ( res ? . success && res . data ) {
apiCalls . value = res . data . items || [ ]
apiCallsTotal . value = res . data . total || 0
}
} catch ( e ) {
ElMessage . error ( e ? . response ? . data ? . message || e ? . message || '加载调用记录失败' )
} finally {
apiCallsLoading . value = false
}
}
const openApiCalls = async ( row ) => {
apiCallsTarget . value = {
childId : row . child _user _id ,
company : row . is _certified ? row . company _name : '' ,
phone : row . phone || ''
}
apiCallsPage . value = 1
apiCallsVisible . value = true
await loadChildApiCalls ( )
}
const viewApiCallDetail = ( row ) => {
selectedApiCall . value = row
apiCallDetailVisible . value = true
}
const handleSizeChange = ( size ) => {
pageSize . value = size
page . value = 1
@@ -541,6 +879,10 @@ onUnmounted(() => {
color : # 2563 eb ;
}
. action - calls {
color : # 7 c3aed ;
}
. action - subscribe {
color : # 059669 ;
}
@@ -588,10 +930,19 @@ onUnmounted(() => {
font - weight : 700 ;
}
. quota - tag {
max - width : 100 % ;
white - space : normal ;
height : auto ;
line - height : 1.4 ;
padding - top : 2 px ;
padding - bottom : 2 px ;
}
@ media ( max - width : 768 px ) {
: deep ( . el - table ) {
font - size : 12 px ;
min - width : 86 0 px ;
min - width : 9 80px ;
}
: deep ( . el - table th ) ,