feat(admin): 用户管理新增用户明细抽屉,聚合资金与交易全链路数据
- 用户管理操作列新增「明细」入口,抽屉按权限提供 5 个 Tab:钱包流水、订单记录、提现记录、邀请返利、支付记录 - 后端:后台订单列表新增 user_id 精确过滤(owner_id / renter_id),邀请关系与返利流水新增 inviter_id / invitee_id 过滤 - 修复:返利流水的「被邀请人手机号」筛选此前后端未实现,一直无效 - 资金流水页、支付记录页支持 ?user_id= 预填,可从明细直达 - 顺带将 invitation/dto.go、repository.go 中两处既有缩进错位按 gofmt 规整
This commit is contained in:
@@ -12,33 +12,33 @@ type InviteCodeInfoDTO struct {
|
||||
|
||||
// UserInvitationSummaryDTO 个人中心邀请返利概览
|
||||
type UserInvitationSummaryDTO struct {
|
||||
HasInviteCode bool `json:"has_invite_code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
IsRealnameVerified bool `json:"is_realname_verified"`
|
||||
TotalInvitedCount int64 `json:"total_invited_count"`
|
||||
ActiveInvitedCount int64 `json:"active_invited_count"`
|
||||
PendingInvitedCount int64 `json:"pending_invited_count"`
|
||||
TotalRebateCent int64 `json:"total_rebate_cent"`
|
||||
CurrentMonthEstCent int64 `json:"current_month_est_cent"`
|
||||
FirstMonthRate float64 `json:"first_month_rate"`
|
||||
SecondMonthRate float64 `json:"second_month_rate"`
|
||||
SubsequentRate float64 `json:"subsequent_rate"`
|
||||
HasInviteCode bool `json:"has_invite_code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
IsRealnameVerified bool `json:"is_realname_verified"`
|
||||
TotalInvitedCount int64 `json:"total_invited_count"`
|
||||
ActiveInvitedCount int64 `json:"active_invited_count"`
|
||||
PendingInvitedCount int64 `json:"pending_invited_count"`
|
||||
TotalRebateCent int64 `json:"total_rebate_cent"`
|
||||
CurrentMonthEstCent int64 `json:"current_month_est_cent"`
|
||||
FirstMonthRate float64 `json:"first_month_rate"`
|
||||
SecondMonthRate float64 `json:"second_month_rate"`
|
||||
SubsequentRate float64 `json:"subsequent_rate"`
|
||||
}
|
||||
|
||||
// InvitedUserRecordDTO 邀请好友记录
|
||||
type InvitedUserRecordDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
InviteeID uint64 `json:"invitee_id"`
|
||||
MaskedPhone string `json:"masked_phone"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Status string `json:"status"` // pending=待实名, active=已实名生效
|
||||
CycleStage string `json:"cycle_stage"` // first_month, second_month, subsequent, unverified
|
||||
DaysSinceVerified int `json:"days_since_verified"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
TotalEarnedFromCent int64 `json:"total_earned_from_cent"`
|
||||
ID uint64 `json:"id"`
|
||||
InviteeID uint64 `json:"invitee_id"`
|
||||
MaskedPhone string `json:"masked_phone"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Status string `json:"status"` // pending=待实名, active=已实名生效
|
||||
CycleStage string `json:"cycle_stage"` // first_month, second_month, subsequent, unverified
|
||||
DaysSinceVerified int `json:"days_since_verified"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
TotalEarnedFromCent int64 `json:"total_earned_from_cent"`
|
||||
}
|
||||
|
||||
// RebateLedgerDTO 返利明细记录
|
||||
@@ -76,20 +76,27 @@ type UpdateConfigRequest struct {
|
||||
|
||||
// AdminInvitationQuery 后台查询邀请关系
|
||||
type AdminInvitationQuery struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
InviterPhone string `form:"inviter_phone"`
|
||||
InviteePhone string `form:"invitee_phone"`
|
||||
Status string `form:"status"`
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
InviterPhone string `form:"inviter_phone"`
|
||||
InviteePhone string `form:"invitee_phone"`
|
||||
Status string `form:"status"`
|
||||
DistributionSiteID *uint64 `form:"distribution_site_id"`
|
||||
// InviterID / InviteeID 供用户管理「明细」按用户精确筛选。
|
||||
InviterID uint64 `form:"inviter_id"`
|
||||
InviteeID uint64 `form:"invitee_id"`
|
||||
}
|
||||
|
||||
// AdminRebateLedgerQuery 后台查询返利明细
|
||||
type AdminRebateLedgerQuery struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
OrderNo string `form:"order_no"`
|
||||
InviterPhone string `form:"inviter_phone"`
|
||||
TradeRole string `form:"trade_role"`
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
OrderNo string `form:"order_no"`
|
||||
InviterPhone string `form:"inviter_phone"`
|
||||
InviteePhone string `form:"invitee_phone"`
|
||||
TradeRole string `form:"trade_role"`
|
||||
DistributionSiteID *uint64 `form:"distribution_site_id"`
|
||||
// InviterID / InviteeID 供用户管理「明细」按用户精确筛选。
|
||||
InviterID uint64 `form:"inviter_id"`
|
||||
InviteeID uint64 `form:"invitee_id"`
|
||||
}
|
||||
|
||||
@@ -572,18 +572,18 @@ func (r *Repository) GetUserSummary(ctx context.Context, userID uint64, siteID u
|
||||
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(¤tMonthEstCent).Error
|
||||
|
||||
return &UserInvitationSummaryDTO{
|
||||
HasInviteCode: hasCode,
|
||||
InviteCode: code,
|
||||
InviteLink: fmt.Sprintf("/i/%s", code),
|
||||
IsRealnameVerified: user.RealnameStatus == "verified",
|
||||
TotalInvitedCount: totalCount,
|
||||
ActiveInvitedCount: activeCount,
|
||||
PendingInvitedCount: pendingCount,
|
||||
TotalRebateCent: totalRebateCent,
|
||||
CurrentMonthEstCent: currentMonthEstCent,
|
||||
FirstMonthRate: firstRate,
|
||||
SecondMonthRate: secondRate,
|
||||
SubsequentRate: subsequentRate,
|
||||
HasInviteCode: hasCode,
|
||||
InviteCode: code,
|
||||
InviteLink: fmt.Sprintf("/i/%s", code),
|
||||
IsRealnameVerified: user.RealnameStatus == "verified",
|
||||
TotalInvitedCount: totalCount,
|
||||
ActiveInvitedCount: activeCount,
|
||||
PendingInvitedCount: pendingCount,
|
||||
TotalRebateCent: totalRebateCent,
|
||||
CurrentMonthEstCent: currentMonthEstCent,
|
||||
FirstMonthRate: firstRate,
|
||||
SecondMonthRate: secondRate,
|
||||
SubsequentRate: subsequentRate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -734,6 +734,12 @@ func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvita
|
||||
if query.DistributionSiteID != nil {
|
||||
db = db.Where("ui.distribution_site_id = ?", *query.DistributionSiteID)
|
||||
}
|
||||
if query.InviterID > 0 {
|
||||
db = db.Where("ui.inviter_id = ?", query.InviterID)
|
||||
}
|
||||
if query.InviteeID > 0 {
|
||||
db = db.Where("ui.invitee_id = ?", query.InviteeID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
@@ -804,12 +810,21 @@ func (r *Repository) ListAdminRebateLedgers(ctx context.Context, query AdminReba
|
||||
if p := strings.TrimSpace(query.InviterPhone); p != "" {
|
||||
db = db.Where("inviter.phone = ?", p)
|
||||
}
|
||||
if p := strings.TrimSpace(query.InviteePhone); p != "" {
|
||||
db = db.Where("invitee.phone = ?", p)
|
||||
}
|
||||
if t := strings.TrimSpace(query.TradeRole); t != "" {
|
||||
db = db.Where("irl.trade_role = ?", t)
|
||||
}
|
||||
if query.DistributionSiteID != nil {
|
||||
db = db.Where("irl.distribution_site_id = ?", *query.DistributionSiteID)
|
||||
}
|
||||
if query.InviterID > 0 {
|
||||
db = db.Where("irl.inviter_id = ?", query.InviterID)
|
||||
}
|
||||
if query.InviteeID > 0 {
|
||||
db = db.Where("irl.invitee_id = ?", query.InviteeID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
|
||||
@@ -217,6 +217,8 @@ type AdminOrderQuery struct {
|
||||
HandoffMode string
|
||||
SettlementStatus string
|
||||
OfflineSettlementStatus string
|
||||
// UserID 精确匹配该用户作为号主或租客参与的订单(用户管理「明细」入口使用)。
|
||||
UserID uint64
|
||||
}
|
||||
|
||||
type AuditMeta = auditlog.Meta
|
||||
|
||||
@@ -54,6 +54,7 @@ func parsePagination(c *gin.Context) (int, int) {
|
||||
|
||||
func parseAdminOrderQuery(c *gin.Context) AdminOrderQuery {
|
||||
page, pageSize := parsePagination(c)
|
||||
userID, _ := strconv.ParseUint(strings.TrimSpace(c.Query("user_id")), 10, 64)
|
||||
return AdminOrderQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
@@ -63,5 +64,6 @@ func parseAdminOrderQuery(c *gin.Context) AdminOrderQuery {
|
||||
HandoffMode: strings.TrimSpace(c.Query("handoff_mode")),
|
||||
SettlementStatus: strings.TrimSpace(c.Query("settlement_status")),
|
||||
OfflineSettlementStatus: strings.TrimSpace(c.Query("offline_settlement_status")),
|
||||
UserID: userID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,10 @@ func adminListItemNeedsOwnerIncome(row orderRow) bool {
|
||||
}
|
||||
|
||||
func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
|
||||
if query.UserID > 0 {
|
||||
// 精确命中该用户作为号主或租客参与的订单,避免与 keyword 的数字 ID 模糊命中混淆。
|
||||
db = db.Where("(o.owner_id = ? OR o.renter_id = ?)", query.UserID, query.UserID)
|
||||
}
|
||||
if query.Status != "" {
|
||||
db = db.Where("o.status = ?", query.Status)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
|
||||
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
||||
|
||||
/** 后台邀请关系行(/admin/invitations) */
|
||||
export interface AdminInvitationRow {
|
||||
id: number
|
||||
inviter_id: number
|
||||
inviter_phone: string
|
||||
inviter_nickname: string
|
||||
invitee_id: number
|
||||
invitee_phone: string
|
||||
invitee_nickname: string
|
||||
distribution_site_id: number
|
||||
invite_code: string
|
||||
status: string
|
||||
verified_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 后台返利结算流水行(/admin/invitations/rebates) */
|
||||
export interface AdminRebateRow {
|
||||
id: number
|
||||
order_id: number
|
||||
order_no: string
|
||||
distribution_site_id: number
|
||||
inviter_id: number
|
||||
inviter_phone: string
|
||||
invitee_id: number
|
||||
invitee_phone: string
|
||||
trade_role: string
|
||||
order_profit_basis_cent: number
|
||||
single_side_basis_cent: number
|
||||
rate_applied: number
|
||||
days_since_verified: number
|
||||
cycle_stage: string
|
||||
rebate_amount_cent: number
|
||||
status: string
|
||||
settled_at: string
|
||||
}
|
||||
|
||||
export interface AdminInvitationQuery {
|
||||
inviter_id?: number
|
||||
invitee_id?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export async function fetchAdminInvitations(query: AdminInvitationQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminInvitationRow>>>(
|
||||
'/admin/invitations',
|
||||
{ params: query }
|
||||
)
|
||||
const result = data.data
|
||||
return {
|
||||
items: Array.isArray(result?.items) ? result.items : [],
|
||||
total: Number(result?.total ?? 0),
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminRebates(query: AdminInvitationQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminRebateRow>>>(
|
||||
'/admin/invitations/rebates',
|
||||
{ params: query }
|
||||
)
|
||||
const result = data.data
|
||||
return {
|
||||
items: Array.isArray(result?.items) ? result.items : [],
|
||||
total: Number(result?.total ?? 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.user-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-detail__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.user-detail__identity {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-detail__identity strong {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.user-detail__identity span {
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.user-detail__identity small {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-detail__metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(96px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-detail__metrics > div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-detail__metrics span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-detail__metrics strong {
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tab-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.amount-in {
|
||||
color: #16a34a;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-out {
|
||||
color: #dc2626;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.cell-sub {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tab-toolbar :deep(.el-radio-group) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
fetchAdminInvitations,
|
||||
fetchAdminRebates,
|
||||
type AdminInvitationRow,
|
||||
type AdminRebateRow,
|
||||
} from '@/features/admin/api/adminInvitations'
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
||||
import { fetchAdminWithdrawals, type WithdrawalDetail } from '@/features/admin/api/adminWithdrawal'
|
||||
import type { AdminUserItem } from '@/features/admin/api/adminUsers'
|
||||
import { fetchAdminOrders, type AdminOrderListItem } from '@/features/orders'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import {
|
||||
balanceTypeLabel,
|
||||
ledgerDirectionLabel,
|
||||
orderStatusLabel,
|
||||
walletBizTypeLabel,
|
||||
} from '@/shared/utils/statusLabels'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import AdminTablePagination from './AdminTablePagination.vue'
|
||||
|
||||
type TabKey = 'wallet' | 'orders' | 'withdrawals' | 'invite' | 'payments'
|
||||
type InviteMode = 'rebates' | 'invited' | 'invitedBy'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
user?: AdminUserItem | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const adminSession = useAdminSessionStore()
|
||||
|
||||
const canViewWallet = computed(() => adminSession.hasPermission('wallet:view'))
|
||||
const canViewOrders = computed(() => adminSession.hasPermission('order:list'))
|
||||
const canViewWithdrawals = computed(() => adminSession.hasPermission('withdrawal:list'))
|
||||
const canViewInviteRelations = computed(() => adminSession.hasPermission('distribution:site_view'))
|
||||
const canViewRebates = computed(() => adminSession.hasPermission('distribution:settlement_view'))
|
||||
const canViewInvite = computed(() => canViewInviteRelations.value || canViewRebates.value)
|
||||
const canViewPayments = computed(() => adminSession.hasPermission('wallet:view'))
|
||||
|
||||
const tabs = computed<{ key: TabKey; label: string }[]>(() => {
|
||||
const list: { key: TabKey; label: string }[] = []
|
||||
if (canViewWallet.value) list.push({ key: 'wallet', label: '钱包流水' })
|
||||
if (canViewOrders.value) list.push({ key: 'orders', label: '订单记录' })
|
||||
if (canViewWithdrawals.value) list.push({ key: 'withdrawals', label: '提现记录' })
|
||||
if (canViewInvite.value) list.push({ key: 'invite', label: '邀请返利' })
|
||||
if (canViewPayments.value) list.push({ key: 'payments', label: '支付记录' })
|
||||
return list
|
||||
})
|
||||
|
||||
// 邀请返利页内视图,按接口权限收敛(关系查 distribution:site_view,流水查 distribution:settlement_view)。
|
||||
const inviteModes = computed<{ key: InviteMode; label: string }[]>(() => {
|
||||
const list: { key: InviteMode; label: string }[] = []
|
||||
if (canViewRebates.value) list.push({ key: 'rebates', label: '我获得的返利' })
|
||||
if (canViewInviteRelations.value) {
|
||||
list.push({ key: 'invited', label: '我邀请的好友' })
|
||||
list.push({ key: 'invitedBy', label: '我作为被邀请人' })
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
const activeTab = ref<TabKey>('wallet')
|
||||
const inviteMode = ref<InviteMode>('rebates')
|
||||
|
||||
const walletLoading = ref(false)
|
||||
const walletRows = ref<AdminWalletLedger[]>([])
|
||||
const walletTotal = ref(0)
|
||||
const walletPage = ref(1)
|
||||
const walletPageSize = ref(20)
|
||||
|
||||
const orderLoading = ref(false)
|
||||
const orderRows = ref<AdminOrderListItem[]>([])
|
||||
const orderTotal = ref(0)
|
||||
const orderPage = ref(1)
|
||||
const orderPageSize = ref(20)
|
||||
|
||||
const withdrawalLoading = ref(false)
|
||||
const withdrawalRows = ref<WithdrawalDetail[]>([])
|
||||
const withdrawalTotal = ref(0)
|
||||
const withdrawalPage = ref(1)
|
||||
const withdrawalPageSize = ref(20)
|
||||
|
||||
const rebateLoading = ref(false)
|
||||
const rebateRows = ref<AdminRebateRow[]>([])
|
||||
const rebateTotal = ref(0)
|
||||
const rebatePage = ref(1)
|
||||
const rebatePageSize = ref(20)
|
||||
|
||||
const inviteLoading = ref(false)
|
||||
const inviteRows = ref<AdminInvitationRow[]>([])
|
||||
const inviteTotal = ref(0)
|
||||
const invitePage = ref(1)
|
||||
const invitePageSize = ref(20)
|
||||
|
||||
const paymentLoading = ref(false)
|
||||
const paymentRows = ref<AdminPayment[]>([])
|
||||
const paymentTotal = ref(0)
|
||||
const paymentPage = ref(1)
|
||||
const paymentPageSize = ref(20)
|
||||
|
||||
const userId = computed(() => props.user?.id || 0)
|
||||
const userPhone = computed(() => props.user?.phone || '')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
visible => {
|
||||
if (!visible) return
|
||||
resetState()
|
||||
const first = tabs.value[0]?.key || 'wallet'
|
||||
// 切换标签本身会触发 watch(activeTab) 加载;同标签则显式加载,避免重复请求。
|
||||
if (activeTab.value === first) {
|
||||
void loadActive()
|
||||
} else {
|
||||
activeTab.value = first
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(activeTab, () => {
|
||||
if (props.modelValue) void loadActive()
|
||||
})
|
||||
|
||||
watch(inviteMode, () => {
|
||||
if (!props.modelValue || activeTab.value !== 'invite') return
|
||||
// 切换数据视图时回到第一页,避免沿用上一个视图的页码导致空列表。
|
||||
rebatePage.value = 1
|
||||
invitePage.value = 1
|
||||
void loadInvite()
|
||||
})
|
||||
|
||||
function resetState() {
|
||||
walletPage.value = 1
|
||||
orderPage.value = 1
|
||||
withdrawalPage.value = 1
|
||||
rebatePage.value = 1
|
||||
invitePage.value = 1
|
||||
paymentPage.value = 1
|
||||
walletRows.value = []
|
||||
orderRows.value = []
|
||||
withdrawalRows.value = []
|
||||
rebateRows.value = []
|
||||
inviteRows.value = []
|
||||
paymentRows.value = []
|
||||
walletTotal.value = 0
|
||||
orderTotal.value = 0
|
||||
withdrawalTotal.value = 0
|
||||
rebateTotal.value = 0
|
||||
inviteTotal.value = 0
|
||||
paymentTotal.value = 0
|
||||
// 权限变化时回落到当前可用的第一个视图,否则保留上次选择。
|
||||
if (!inviteModes.value.some(mode => mode.key === inviteMode.value)) {
|
||||
inviteMode.value = inviteModes.value[0]?.key || 'rebates'
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function loadActive() {
|
||||
if (!props.modelValue || userId.value <= 0) return
|
||||
if (activeTab.value === 'wallet') return loadWallet()
|
||||
if (activeTab.value === 'orders') return loadOrders()
|
||||
if (activeTab.value === 'withdrawals') return loadWithdrawals()
|
||||
if (activeTab.value === 'invite') return loadInvite()
|
||||
return loadPayments()
|
||||
}
|
||||
|
||||
async function loadWallet() {
|
||||
walletLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminWalletLedger({
|
||||
user_id: String(userId.value),
|
||||
page: walletPage.value,
|
||||
page_size: walletPageSize.value,
|
||||
})
|
||||
walletRows.value = Array.isArray(result.items) ? result.items : []
|
||||
walletTotal.value = Number(result.total ?? 0)
|
||||
} catch (error) {
|
||||
walletRows.value = []
|
||||
walletTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取钱包流水失败'))
|
||||
} finally {
|
||||
walletLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
orderLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminOrders(orderPage.value, orderPageSize.value, {
|
||||
user_id: userId.value,
|
||||
})
|
||||
orderRows.value = Array.isArray(result.items) ? result.items : []
|
||||
orderTotal.value = Number(result.total ?? 0)
|
||||
} catch (error) {
|
||||
orderRows.value = []
|
||||
orderTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取订单记录失败'))
|
||||
} finally {
|
||||
orderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWithdrawals() {
|
||||
withdrawalLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminWithdrawals({
|
||||
user_id: userId.value,
|
||||
page: withdrawalPage.value,
|
||||
page_size: withdrawalPageSize.value,
|
||||
})
|
||||
withdrawalRows.value = Array.isArray(result.items) ? result.items : []
|
||||
withdrawalTotal.value = Number(result.total ?? 0)
|
||||
} catch (error) {
|
||||
withdrawalRows.value = []
|
||||
withdrawalTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取提现记录失败'))
|
||||
} finally {
|
||||
withdrawalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvite() {
|
||||
if (inviteMode.value === 'rebates') return loadRebates()
|
||||
return loadInvitations()
|
||||
}
|
||||
|
||||
async function loadRebates() {
|
||||
rebateLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminRebates({
|
||||
inviter_id: userId.value,
|
||||
page: rebatePage.value,
|
||||
page_size: rebatePageSize.value,
|
||||
})
|
||||
rebateRows.value = result.items
|
||||
rebateTotal.value = result.total
|
||||
} catch (error) {
|
||||
rebateRows.value = []
|
||||
rebateTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取返利流水失败'))
|
||||
} finally {
|
||||
rebateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvitations() {
|
||||
inviteLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminInvitations({
|
||||
...(inviteMode.value === 'invitedBy'
|
||||
? { invitee_id: userId.value }
|
||||
: { inviter_id: userId.value }),
|
||||
page: invitePage.value,
|
||||
page_size: invitePageSize.value,
|
||||
})
|
||||
inviteRows.value = result.items
|
||||
inviteTotal.value = result.total
|
||||
} catch (error) {
|
||||
inviteRows.value = []
|
||||
inviteTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取邀请关系失败'))
|
||||
} finally {
|
||||
inviteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPayments() {
|
||||
paymentLoading.value = true
|
||||
try {
|
||||
const result = await fetchAdminPayments({
|
||||
user_id: String(userId.value),
|
||||
page: paymentPage.value,
|
||||
page_size: paymentPageSize.value,
|
||||
})
|
||||
paymentRows.value = Array.isArray(result.items) ? result.items : []
|
||||
paymentTotal.value = Number(result.total ?? 0)
|
||||
} catch (error) {
|
||||
paymentRows.value = []
|
||||
paymentTotal.value = 0
|
||||
ElMessage.error(readError(error, '获取支付记录失败'))
|
||||
} finally {
|
||||
paymentLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 依据手机号判定该用户在本单中的角色(列表接口只回传手机号)。 */
|
||||
function orderRoles(row: AdminOrderListItem): string[] {
|
||||
const phone = userPhone.value
|
||||
if (!phone) return []
|
||||
const roles: string[] = []
|
||||
if (row.renter_phone === phone) roles.push('租客')
|
||||
if (row.owner_phone === phone) roles.push('号主')
|
||||
return roles
|
||||
}
|
||||
|
||||
function walletDirectionType(direction: string): 'success' | 'danger' | 'warning' {
|
||||
if (direction === 'in') return 'success'
|
||||
if (direction === 'out') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function withdrawalStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
processing: '待打款',
|
||||
completed: '已打款',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function withdrawalStatusType(
|
||||
status: string
|
||||
): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||
const types: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
|
||||
pending: 'warning',
|
||||
processing: 'primary',
|
||||
completed: 'success',
|
||||
rejected: 'danger',
|
||||
cancelled: 'info',
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
function invitationStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
active: '已实名(生效中)',
|
||||
pending: '待实名(未生效)',
|
||||
invalid: '已失效',
|
||||
}
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function invitationStatusType(status: string): 'success' | 'warning' | 'info' {
|
||||
if (status === 'active') return 'success'
|
||||
if (status === 'pending') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function tradeRoleLabel(role: string) {
|
||||
if (role === 'buyer') return '买家端'
|
||||
if (role === 'seller') return '卖家端'
|
||||
return role || '-'
|
||||
}
|
||||
|
||||
function cycleStageLabel(stage: string) {
|
||||
const labels: Record<string, string> = {
|
||||
first_month: '首月',
|
||||
second_month: '次月',
|
||||
subsequent: '长期',
|
||||
}
|
||||
return labels[stage] || stage || '-'
|
||||
}
|
||||
|
||||
function paymentStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
created: '已创建',
|
||||
paying: '支付中',
|
||||
paid: '已支付',
|
||||
refunding: '退款中',
|
||||
refunded: '已退款',
|
||||
failed: '失败',
|
||||
closed: '已关闭',
|
||||
}
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function paymentStatusType(status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||
if (['paid', 'refunded'].includes(status)) return 'success'
|
||||
if (status === 'failed') return 'danger'
|
||||
if (['paying', 'refunding'].includes(status)) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function paymentBizTypeLabel(type: string) {
|
||||
const labels: Record<string, string> = {
|
||||
order_pay: '订单支付',
|
||||
cancel_refund: '取消退款',
|
||||
admin_close_refund: '客服关闭退款',
|
||||
admin_refund: '人工退款',
|
||||
checkout_refund: '结账退款',
|
||||
deposit_refund: '押金退款',
|
||||
rent_refund: '租金退款',
|
||||
arbitration_refund: '仲裁退款',
|
||||
mohong_pay: '撞车支付',
|
||||
mohong_refund: '撞车退款',
|
||||
}
|
||||
return labels[type] || type || '-'
|
||||
}
|
||||
|
||||
function goOrderDetail(row: AdminOrderListItem) {
|
||||
close()
|
||||
void router.push(adminPath(`orders/${row.id}`))
|
||||
}
|
||||
|
||||
function goWithdrawals() {
|
||||
close()
|
||||
void router.push({
|
||||
path: adminPath('withdrawals'),
|
||||
query: { withdrawal_user_id: String(userId.value) },
|
||||
})
|
||||
}
|
||||
|
||||
function goWalletLedger() {
|
||||
close()
|
||||
void router.push({ path: adminPath('wallet-ledger'), query: { user_id: String(userId.value) } })
|
||||
}
|
||||
|
||||
function goPayments() {
|
||||
close()
|
||||
void router.push({ path: adminPath('payments'), query: { user_id: String(userId.value) } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="modelValue"
|
||||
size="78%"
|
||||
destroy-on-close
|
||||
:title="`用户明细 · ${user?.nickname || user?.phone || userId}`"
|
||||
@update:model-value="(val: boolean) => emit('update:modelValue', val)"
|
||||
>
|
||||
<div v-if="user" class="user-detail">
|
||||
<header class="user-detail__head">
|
||||
<div class="user-detail__identity">
|
||||
<strong>{{ user.nickname || '-' }}</strong>
|
||||
<span>{{ user.phone }}</span>
|
||||
<small>UID: {{ user.id }}</small>
|
||||
</div>
|
||||
<div class="user-detail__metrics">
|
||||
<div>
|
||||
<span>可用余额</span>
|
||||
<strong>{{ formatCentWithSymbol(user.available_balance_cent) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>冻结余额</span>
|
||||
<strong>{{ formatCentWithSymbol(user.frozen_balance_cent) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>成长等级</span>
|
||||
<strong>{{ user.renter_growth_level_name || '普通' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>注册时间</span>
|
||||
<strong>{{ formatDateTime(user.created_at) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-empty v-if="!tabs.length" description="当前账号没有查看资金与订单的权限" />
|
||||
|
||||
<el-tabs v-else v-model="activeTab">
|
||||
<el-tab-pane v-if="canViewWallet" label="钱包流水" name="wallet">
|
||||
<div class="tab-toolbar">
|
||||
<span>共 {{ walletTotal }} 条流水</span>
|
||||
<el-button text type="primary" size="small" @click="goWalletLedger">
|
||||
前往资金流水页
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="walletLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="walletRows"
|
||||
empty-text="暂无钱包流水"
|
||||
>
|
||||
<el-table-column label="时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="walletDirectionType(row.direction)">
|
||||
{{ ledgerDirectionLabel(row.direction) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.direction === 'in' ? 'amount-in' : 'amount-out'">
|
||||
{{ row.direction === 'in' ? '+' : '-'
|
||||
}}{{ formatCentWithSymbol(row.amount_cent) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="业务类型" width="130">
|
||||
<template #default="{ row }">{{ walletBizTypeLabel(row.biz_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额口径" width="100">
|
||||
<template #default="{ row }">{{ balanceTypeLabel(row.balance_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额快照" width="120" align="right">
|
||||
<template #default="{ row }">{{
|
||||
formatCentWithSymbol(row.balance_after_cent)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_no" label="关联订单" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="walletTotal > 0"
|
||||
v-model:current-page="walletPage"
|
||||
v-model:page-size="walletPageSize"
|
||||
:total="walletTotal"
|
||||
:loading="walletLoading"
|
||||
@page-change="loadWallet"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="canViewOrders" label="订单记录" name="orders">
|
||||
<div class="tab-toolbar">
|
||||
<span>共 {{ orderTotal }} 笔订单(作为租客或号主)</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="orderLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="orderRows"
|
||||
empty-text="暂无订单记录"
|
||||
>
|
||||
<el-table-column prop="order_no" label="订单号" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="角色" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-for="role in orderRoles(row)"
|
||||
:key="role"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="role-tag"
|
||||
>
|
||||
{{ role }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="商品" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="130">
|
||||
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租金" width="110" align="right">
|
||||
<template #default="{ row }">{{
|
||||
formatCentWithSymbol(row.rent_amount_cent)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click="goOrderDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="orderTotal > 0"
|
||||
v-model:current-page="orderPage"
|
||||
v-model:page-size="orderPageSize"
|
||||
:total="orderTotal"
|
||||
:loading="orderLoading"
|
||||
@page-change="loadOrders"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="canViewWithdrawals" label="提现记录" name="withdrawals">
|
||||
<div class="tab-toolbar">
|
||||
<span>共 {{ withdrawalTotal }} 条提现申请</span>
|
||||
<el-button text type="primary" size="small" @click="goWithdrawals">
|
||||
前往提现管理
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="withdrawalLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="withdrawalRows"
|
||||
empty-text="暂无提现记录"
|
||||
>
|
||||
<el-table-column
|
||||
prop="withdraw_no"
|
||||
label="提现单号"
|
||||
width="190"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="申请金额" width="120" align="right">
|
||||
<template #default="{ row }">{{ formatCentWithSymbol(row.amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际到账" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatCentWithSymbol(row.actual_amount_cent ?? row.amount_cent) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款账户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.account_name || '-' }} · {{ row.account_no || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="withdrawalStatusType(row.status)">
|
||||
{{ withdrawalStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="withdrawalTotal > 0"
|
||||
v-model:current-page="withdrawalPage"
|
||||
v-model:page-size="withdrawalPageSize"
|
||||
:total="withdrawalTotal"
|
||||
:loading="withdrawalLoading"
|
||||
@page-change="loadWithdrawals"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="canViewInvite" label="邀请返利" name="invite">
|
||||
<div class="tab-toolbar">
|
||||
<el-radio-group v-model="inviteMode" size="small">
|
||||
<el-radio-button v-for="mode in inviteModes" :key="mode.key" :value="mode.key">
|
||||
{{ mode.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span v-if="inviteMode === 'rebates'">共 {{ rebateTotal }} 笔返利</span>
|
||||
<span v-else>共 {{ inviteTotal }} 条邀请关系</span>
|
||||
</div>
|
||||
|
||||
<template v-if="inviteMode === 'rebates'">
|
||||
<el-table
|
||||
v-loading="rebateLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="rebateRows"
|
||||
empty-text="暂无返利流水"
|
||||
>
|
||||
<el-table-column label="入账时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.settled_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_no" label="关联订单" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="交易人(被邀请人)" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<code class="cell-sub">{{ row.invitee_phone || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交易端" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="row.trade_role === 'buyer' ? 'primary' : 'danger'"
|
||||
effect="plain"
|
||||
>
|
||||
{{ tradeRoleLabel(row.trade_role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收益基数" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatCentWithSymbol(row.single_side_basis_cent) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提成比例" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ Math.round((row.rate_applied || 0) * 100) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="周期" width="80">
|
||||
<template #default="{ row }">{{ cycleStageLabel(row.cycle_stage) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="返利金额" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-in">+{{ formatCentWithSymbol(row.rebate_amount_cent) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="rebateTotal > 0"
|
||||
v-model:current-page="rebatePage"
|
||||
v-model:page-size="rebatePageSize"
|
||||
:total="rebateTotal"
|
||||
:loading="rebateLoading"
|
||||
@page-change="loadRebates"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-table
|
||||
v-loading="inviteLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="inviteRows"
|
||||
empty-text="暂无邀请关系"
|
||||
>
|
||||
<el-table-column label="邀请人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.inviter_nickname || '-' }}</div>
|
||||
<code class="cell-sub">{{ row.inviter_phone || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="被邀请人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.invitee_nickname || '-' }}</div>
|
||||
<code class="cell-sub">{{ row.invitee_phone || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用邀请码" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.invite_code || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效状态" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="invitationStatusType(row.status)">
|
||||
{{ invitationStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实名生效时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.verified_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="inviteTotal > 0"
|
||||
v-model:current-page="invitePage"
|
||||
v-model:page-size="invitePageSize"
|
||||
:total="inviteTotal"
|
||||
:loading="inviteLoading"
|
||||
@page-change="loadInvitations"
|
||||
/>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="canViewPayments" label="支付记录" name="payments">
|
||||
<div class="tab-toolbar">
|
||||
<span>共 {{ paymentTotal }} 笔支付/退款单</span>
|
||||
<el-button text type="primary" size="small" @click="goPayments">
|
||||
前往支付记录页
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="paymentLoading"
|
||||
size="small"
|
||||
border
|
||||
:data="paymentRows"
|
||||
empty-text="暂无支付记录"
|
||||
>
|
||||
<el-table-column label="创建时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payment_no" label="支付单号" width="190" show-overflow-tooltip />
|
||||
<el-table-column prop="order_no" label="关联订单" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="业务类型" width="120">
|
||||
<template #default="{ row }">{{ paymentBizTypeLabel(row.biz_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道/方式" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.provider || '-' }} · {{ row.pay_way || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.biz_type.includes('refund') ? 'amount-out' : 'amount-in'">
|
||||
{{ formatCentWithSymbol(row.amount_cent) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="paymentStatusType(row.status)">
|
||||
{{ paymentStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.paid_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminTablePagination
|
||||
v-if="paymentTotal > 0"
|
||||
v-model:current-page="paymentPage"
|
||||
v-model:page-size="paymentPageSize"
|
||||
:total="paymentTotal"
|
||||
:loading="paymentLoading"
|
||||
@page-change="loadPayments"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped src="./AdminUserDetailDrawer.css"></style>
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Wallet,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
@@ -45,7 +46,14 @@ const refundAmount = computed(() =>
|
||||
.reduce((sum, item) => sum + Number(item.amount_cent || 0), 0)
|
||||
)
|
||||
|
||||
onMounted(loadPayments)
|
||||
onMounted(() => {
|
||||
// 支持从用户管理「明细」跳转预填用户,直接查看该用户的支付记录。
|
||||
const routeUserID = useRoute().query.user_id
|
||||
if (typeof routeUserID === 'string' && routeUserID.trim()) {
|
||||
filters.user_id = routeUserID.trim()
|
||||
}
|
||||
void loadPayments()
|
||||
})
|
||||
|
||||
async function loadPayments() {
|
||||
loading.value = true
|
||||
|
||||
@@ -22,13 +22,21 @@ import { realnameStatusLabel, userStatusLabel } from '@/shared/utils/statusLabel
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
import AdminUserDetailDrawer from '../components/AdminUserDetailDrawer.vue'
|
||||
|
||||
const adminSession = useAdminSessionStore()
|
||||
const canAdjustWallet = computed(() => adminSession.hasPermission('user:wallet_adjust'))
|
||||
const canAdjustGrowthPoints = computed(() => adminSession.hasPermission('user:growth_points'))
|
||||
const canViewUserDetail = computed(
|
||||
() =>
|
||||
adminSession.hasPermission('wallet:view') ||
|
||||
adminSession.hasPermission('order:list') ||
|
||||
adminSession.hasPermission('withdrawal:list')
|
||||
)
|
||||
|
||||
const submitting = ref(false)
|
||||
const activeUser = ref<AdminUserItem | null>(null)
|
||||
const detailUser = ref<AdminUserItem | null>(null)
|
||||
const quotaUser = ref<AdminUserItem | null>(null)
|
||||
const realnameUser = ref<AdminUserItem | null>(null)
|
||||
const manualRealnameUser = ref<AdminUserItem | null>(null)
|
||||
@@ -82,6 +90,10 @@ function resetFilters() {
|
||||
queryUsers()
|
||||
}
|
||||
|
||||
function openUserDetail(row: AdminUserItem) {
|
||||
detailUser.value = row
|
||||
}
|
||||
|
||||
function openFreeze(row: AdminUserItem) {
|
||||
activeUser.value = row
|
||||
freezeReason.value = ''
|
||||
@@ -412,9 +424,18 @@ function moneyCent(value: number | string | undefined) {
|
||||
<el-table-column label="注册时间" min-width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="360" fixed="right" align="right">
|
||||
<el-table-column label="操作" width="400" fixed="right" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button
|
||||
v-if="canViewUserDetail"
|
||||
text
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openUserDetail(row)"
|
||||
>
|
||||
明细
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canAdjustWallet"
|
||||
text
|
||||
@@ -752,6 +773,12 @@ function moneyCent(value: number | string | undefined) {
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<AdminUserDetailDrawer
|
||||
:model-value="!!detailUser"
|
||||
:user="detailUser"
|
||||
@update:model-value="(val: boolean) => !val && (detailUser = null)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Wallet,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
@@ -32,6 +33,7 @@ const filters = reactive({
|
||||
order_id: '',
|
||||
biz_type: '',
|
||||
})
|
||||
const route = useRoute()
|
||||
|
||||
const inAmount = computed(() =>
|
||||
ledger.value
|
||||
@@ -44,7 +46,14 @@ const outAmount = computed(() =>
|
||||
.reduce((sum, item) => sum + Number(item.amount_cent || 0), 0)
|
||||
)
|
||||
|
||||
onMounted(loadLedger)
|
||||
onMounted(() => {
|
||||
// 支持从用户管理「明细」跳转预填用户,直接查看该用户的资金流水。
|
||||
const routeUserID = route.query.user_id
|
||||
if (typeof routeUserID === 'string' && routeUserID.trim()) {
|
||||
filters.user_id = routeUserID.trim()
|
||||
}
|
||||
void loadLedger()
|
||||
})
|
||||
|
||||
async function loadLedger() {
|
||||
loading.value = true
|
||||
|
||||
@@ -277,6 +277,8 @@ export interface AdminOrderQuery {
|
||||
handoff_mode?: string
|
||||
settlement_status?: string
|
||||
offline_settlement_status?: 'pending' | 'settled' | ''
|
||||
// 精确按用户过滤:命中该用户作为号主或租客参与的订单。
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
interface SubmitCheckoutRequest {
|
||||
|
||||
Reference in New Issue
Block a user