feat(admin,api,player): 代理层级管理、额度上下分与玩家钱包详情
新增代理管理器与二级代理体系,完善信用额度/上下分上下文与冻结策略;代理端玩家与子代理管理增强;玩家端新增钱包详情页与交易筛选优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
137
apps/admin/src/components/AgentCreditContext.vue
Normal file
137
apps/admin/src/components/AgentCreditContext.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { formatAmount, formatAmountFull, shouldCompactAmount } from '../utils/format-amount';
|
||||
import {
|
||||
maxCreditIncreaseAmount,
|
||||
projectedCreditLimit,
|
||||
type AgentCreditAdjustContext,
|
||||
} from '../utils/agent-credit-context';
|
||||
|
||||
const props = defineProps<{
|
||||
context: AgentCreditAdjustContext | null;
|
||||
loading?: boolean;
|
||||
adjustAmount?: number;
|
||||
}>();
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
|
||||
const maxIncrease = computed(() => maxCreditIncreaseAmount(props.context));
|
||||
const afterLimit = computed(() => projectedCreditLimit(props.context, props.adjustAmount ?? 0));
|
||||
|
||||
function fmt(value: string | null | undefined) {
|
||||
if (value == null || value === '') return '—';
|
||||
return formatAmount(value);
|
||||
}
|
||||
|
||||
function fmtFull(value: string | null | undefined) {
|
||||
if (value == null || value === '') return '';
|
||||
return shouldCompactAmount(value) ? formatAmountFull(value) : '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="credit-context">
|
||||
<div v-if="context?.target" class="credit-context-section">
|
||||
<div class="credit-context-title">{{ t('credit.context.target_section') }}</div>
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item :label="t('user.col.username')">
|
||||
{{ context.target.username }}
|
||||
<span v-if="context.target.level != null" class="level-tag">L{{ context.target.level }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.available_credit')">
|
||||
<span class="c-green">{{ fmt(context.target.availableCredit) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.credit_limit')">{{ fmt(context.target.creditLimit) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.used_credit')">{{ fmt(context.target.usedCredit) }}</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
v-if="context.target.directPlayerLiability != null"
|
||||
:label="t('credit.context.direct_liability')"
|
||||
>
|
||||
{{ fmt(context.target.directPlayerLiability) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
v-if="context.target.childAgentExposure != null"
|
||||
:label="t('credit.context.child_exposure')"
|
||||
>
|
||||
{{ fmt(context.target.childAgentExposure) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="context?.parent" class="credit-context-section">
|
||||
<div class="credit-context-title">
|
||||
{{ t('credit.context.parent_section', { name: context.parent.username }) }}
|
||||
</div>
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item :label="t('agent.field.credit_limit')">{{ fmt(context.parent.creditLimit) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.used_credit')">{{ fmt(context.parent.usedCredit) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.available_credit')" :span="2">
|
||||
<span class="c-green">{{ fmt(context.parent.availableCredit) }}</span>
|
||||
<span v-if="fmtFull(context.parent.availableCredit)" class="amount-full-hint">
|
||||
({{ fmtFull(context.parent.availableCredit) }})
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('credit.context.max_increase')" :span="2">
|
||||
<span class="c-amber">{{ fmt(String(maxIncrease ?? 0)) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-else-if="context?.target && !loading"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="credit-context-alert"
|
||||
:title="t('credit.context.no_parent')"
|
||||
/>
|
||||
|
||||
<div v-if="afterLimit != null && adjustAmount !== 0" class="credit-context-preview">
|
||||
{{ t('credit.context.after_adjust') }}:
|
||||
<span class="c-amber">{{ fmt(afterLimit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.credit-context {
|
||||
margin-bottom: 16px;
|
||||
min-height: 48px;
|
||||
}
|
||||
.credit-context-section + .credit-context-section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.credit-context-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.credit-context-alert {
|
||||
margin-top: 0;
|
||||
}
|
||||
.credit-context-preview {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
.level-tag {
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
.c-green {
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
.c-amber {
|
||||
color: #e6a23c;
|
||||
font-weight: 600;
|
||||
}
|
||||
.amount-full-hint {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
116
apps/admin/src/components/WalletTransferContext.vue
Normal file
116
apps/admin/src/components/WalletTransferContext.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { formatAmount, formatAmountFull, shouldCompactAmount } from '../utils/format-amount';
|
||||
import { depositAmountCap, type WalletTransferContext } from '../utils/wallet-transfer-context';
|
||||
|
||||
const props = defineProps<{
|
||||
context: WalletTransferContext | null;
|
||||
mode: 'deposit' | 'withdraw';
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
|
||||
const credit = computed(() => props.context?.credit ?? null);
|
||||
const player = computed(() => props.context?.player ?? null);
|
||||
|
||||
const depositCapText = computed(() => {
|
||||
if (!props.context?.credit) return '—';
|
||||
const cap = depositAmountCap(props.context);
|
||||
if (cap === undefined) return t('transfer.context.unlimited');
|
||||
return formatAmount(String(cap));
|
||||
});
|
||||
|
||||
function fmt(value: string | null | undefined) {
|
||||
if (value == null || value === '') return '—';
|
||||
return formatAmount(value);
|
||||
}
|
||||
|
||||
function fmtFull(value: string | null | undefined) {
|
||||
if (value == null || value === '') return '';
|
||||
return shouldCompactAmount(value) ? formatAmountFull(value) : '';
|
||||
}
|
||||
|
||||
function limitLabel(value: string | null) {
|
||||
return value ? fmt(value) : t('transfer.context.unlimited');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="transfer-context">
|
||||
<div v-if="player" class="transfer-context-section">
|
||||
<div class="transfer-context-title">{{ t('transfer.context.player_section') }}</div>
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item :label="t('user.col.username')">{{ player.username }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('user.field.available')">
|
||||
<span class="c-green">{{ fmt(player.availableBalance) }}</span>
|
||||
<span v-if="fmtFull(player.availableBalance)" class="amount-full-hint">
|
||||
({{ fmtFull(player.availableBalance) }})
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="mode === 'withdraw'" :label="t('transfer.context.withdrawable')" :span="2">
|
||||
{{ fmt(player.availableBalance) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="Number(player.frozenBalance) > 0" :label="t('user.field.frozen_balance')">
|
||||
{{ fmt(player.frozenBalance) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'deposit' && credit" class="transfer-context-section">
|
||||
<div class="transfer-context-title">
|
||||
{{ t('transfer.context.agent_section', { name: credit.agentUsername, level: credit.agentLevel }) }}
|
||||
</div>
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item :label="t('agent.field.credit_limit')">{{ fmt(credit.creditLimit) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.used_credit')">{{ fmt(credit.usedCredit) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.available_credit')">
|
||||
<span class="c-green">{{ fmt(credit.availableCredit) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('transfer.context.deposit_cap')">
|
||||
<span class="c-amber">{{ depositCapText }}</span>
|
||||
</el-descriptions-item>
|
||||
<template v-if="credit.appliesDepositLimits">
|
||||
<el-descriptions-item :label="t('agent.field.max_single_deposit')">
|
||||
{{ limitLabel(credit.maxSingleDeposit) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('agent.field.max_daily_deposit')">
|
||||
{{ limitLabel(credit.maxDailyDeposit) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="credit.dailyDepositUsed != null" :label="t('transfer.context.daily_used')" :span="2">
|
||||
{{ fmt(credit.dailyDepositUsed) }}
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
<el-descriptions-item v-else :span="2">
|
||||
<span class="field-hint">{{ t('transfer.context.admin_credit_only') }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-else-if="mode === 'deposit' && player && !credit && !loading"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="transfer-context-alert"
|
||||
:title="t('transfer.context.no_agent')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.transfer-context { margin-bottom: 16px; min-height: 48px; }
|
||||
.transfer-context-section + .transfer-context-section { margin-top: 12px; }
|
||||
.transfer-context-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.transfer-context-alert { margin-bottom: 0; }
|
||||
.c-green { color: #67c23a; font-weight: 600; }
|
||||
.c-amber { color: #e6a23c; font-weight: 600; }
|
||||
.amount-full-hint { font-size: 11px; color: #666; margin-left: 4px; }
|
||||
.field-hint { font-size: 12px; color: #666; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user