Files
webman-buildadmin/web/src/views/backend/channel/index.vue
zhenhui f6197a9af5 1.优化渠道管理中直属投注额度和总投注额度
2.管理员管理中三个菜单数据显示限制
2026-05-30 15:53:36 +08:00

1983 lines
67 KiB
Vue

<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('channel.quick Search Fields') })"
></TableHeader>
<div class="channel-top-actions">
<div class="channel-stats-cards">
<el-card shadow="never" class="channel-stat-card">
<div class="label">{{ t('channel.settle_stats_channel_total') }}</div>
<div class="value">{{ settleStats.channel_total }}</div>
</el-card>
<el-card shadow="never" class="channel-stat-card">
<div class="label">{{ t('channel.settle_stats_enabled') }}</div>
<div class="value">{{ settleStats.enabled_count }}</div>
</el-card>
<el-card shadow="never" class="channel-stat-card">
<div class="label">{{ t('channel.settle_stats_pending_dividend') }}</div>
<div class="value">{{ settleStats.carryover_positive_count }}</div>
</el-card>
<el-card shadow="never" class="channel-stat-card">
<div class="label">{{ t('channel.settle_stats_pending_amount') }}</div>
<div class="value">{{ settleStats.carryover_positive_total }}</div>
</el-card>
<el-card
shadow="never"
class="channel-stat-card"
:class="{ 'channel-stat-card-clickable': auth('index') }"
@click="onCompanyTotalBetCardClick"
>
<div class="label">{{ t('channel.settle_stats_company_total_bet') }}</div>
<div class="value">{{ settleStats.company_total_bet_amount }}</div>
</el-card>
<el-card
shadow="never"
class="channel-stat-card"
:class="{ 'channel-stat-card-clickable': auth('index') }"
@click="onPaidDividendCardClick"
>
<div class="label">{{ t('channel.settle_stats_paid_dividend') }}</div>
<div class="value">{{ settleStats.paid_dividend_total }}</div>
</el-card>
</div>
<div class="channel-action-row">
<el-radio-group v-model="settleFilterMode" size="small" @change="onSettleFilterChange">
<el-radio-button label="all">{{ t('channel.settle_filter_all') }}</el-radio-button>
<el-radio-button label="with_balance">{{ t('channel.settle_filter_with_balance') }}</el-radio-button>
<el-radio-button label="no_balance">{{ t('channel.settle_filter_no_balance') }}</el-radio-button>
<el-radio-button label="enabled">{{ t('channel.settle_filter_enabled') }}</el-radio-button>
<el-radio-button label="disabled">{{ t('channel.settle_filter_disabled') }}</el-radio-button>
</el-radio-group>
<el-button v-if="auth('batchSettlePending')" type="warning" @click="onBatchSettlePending">
{{ t('channel.batch_settle_pending') }}
</el-button>
</div>
</div>
<Table ref="tableRef"></Table>
<PopupForm />
<el-dialog
class="manual-settle-dialog"
:close-on-click-modal="false"
:model-value="manualSettle.visible"
:width="manualSettleDialogWidth"
align-center
append-to-body
destroy-on-close
@close="closeManualSettleDialog"
@opened="dismissFloatingTooltips"
>
<template #header>
<div class="title">{{ t('channel.manual_settle') }}</div>
</template>
<div v-loading="manualSettle.previewLoading" class="manual-settle-dialog-body">
<el-descriptions
class="manual-settle-summary"
:column="manualSettleSummaryColumns"
border
:size="manualSettleViewportMobile ? 'small' : 'default'"
>
<el-descriptions-item :label="t('channel.manual_settle_settlement_no')">
{{ manualSettle.form.settlement_no || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_period_start')">
{{ manualSettle.form.period_start_at || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_period_end')">
{{ manualSettle.form.period_end_at || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_total_bet')">
{{ manualSettle.form.total_bet_amount || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_total_payout')">
{{ manualSettle.form.total_payout_amount || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_platform_profit')">
{{ manualSettle.form.platform_profit_amount || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_commission_rate')">
{{ manualSettle.form.commission_rate || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_calc_base')">
{{ manualSettle.form.calc_base_amount || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('channel.manual_settle_commission_amount')">
<span class="manual-settle-summary-highlight">{{ manualSettle.form.commission_amount || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
<el-form
:model="manualSettle.form"
:label-width="manualSettleFormLabelWidth"
label-position="top"
:size="manualSettleFormSize"
class="manual-settle-form"
>
<el-form-item :label="t('channel.share_config')" class="manual-settle-form-item-full manual-settle-split-form-item">
<div class="manual-settle-split-block">
<div
class="manual-settle-split-table-scroll"
:class="{ 'is-mobile': manualSettleViewportMobile }"
>
<div
class="manual-settle-split-table-inner"
:class="{ 'is-mobile': manualSettleViewportMobile }"
:style="manualSettleSplitInnerStyle"
>
<el-table
:data="manualSettleSplitTree"
row-key="admin_id"
:tree-props="{ children: 'children' }"
default-expand-all
border
:size="manualSettleTableSize"
:fit="!manualSettleViewportMobile"
class="manual-settle-split-table"
:class="{ 'is-mobile': manualSettleViewportMobile, 'w100': !manualSettleViewportMobile }"
:table-layout="manualSettleTableLayout"
>
<el-table-column
prop="admin_username"
:label="t('channel.admin__username')"
:min-width="manualSettleViewportMobile ? undefined : manualSettleSplitCol.adminWidth"
:width="manualSettleViewportMobile ? manualSettleSplitCol.adminWidth : undefined"
:show-overflow-tooltip="!manualSettleViewportMobile"
/>
<el-table-column
prop="settlement_base_amount"
:label="t('channel.manual_settle_col_base')"
:width="manualSettleSplitCol.baseWidth"
align="center"
:formatter="formatSplitAmountCell"
/>
<el-table-column
prop="share_rate"
:label="t('channel.manual_settle_col_share')"
:width="manualSettleSplitCol.shareWidth"
align="center"
>
<template #default="scope">{{ scope.row.share_rate }}%</template>
</el-table-column>
<el-table-column
prop="commission_amount"
:label="t('channel.manual_settle_col_gross')"
:width="manualSettleSplitCol.grossWidth"
align="center"
:formatter="formatSplitAmountCell"
/>
<el-table-column
prop="commission_share_percent"
:label="t('channel.manual_settle_col_commission_share')"
:width="manualSettleSplitCol.commissionShareWidth"
align="center"
>
<template #default="scope">{{ formatCommissionSharePercent(scope.row) }}</template>
</el-table-column>
<el-table-column
prop="handling_fee"
:label="t('channel.manual_settle_col_fee')"
:width="manualSettleSplitCol.feeWidth"
align="center"
:formatter="formatSplitAmountCell"
/>
<el-table-column
:label="t('channel.manual_settle_col_net')"
:width="manualSettleSplitCol.netWidth"
align="center"
>
<template #default="scope">{{ formatNetCommission(scope.row) }}</template>
</el-table-column>
</el-table>
</div>
<p v-if="manualSettleViewportMobile" class="manual-settle-split-scroll-tip">
{{ t('channel.manual_settle_split_scroll_tip') }}
</p>
</div>
<el-collapse v-model="manualSettleCalcCollapse" class="manual-settle-calc-collapse">
<el-collapse-item :title="t('channel.manual_settle_calc_title')" name="calc">
<ul class="manual-settle-calc-list">
<li v-for="(line, idx) in manualSettleCalcDescLines" :key="idx">{{ line }}</li>
</ul>
</el-collapse-item>
</el-collapse>
</div>
</el-form-item>
<el-form-item :label="t('channel.manual_settle_remark')" class="manual-settle-form-item-full">
<el-input v-model="manualSettle.form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="manual-settle-footer">
<el-button @click="closeManualSettleDialog">{{ t('Cancel') }}</el-button>
<el-button type="primary" :disabled="manualSettle.previewLoading" :loading="manualSettle.loading" @click="submitManualSettle">
{{ t('channel.manual_settle_submit') }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
class="channel-record-dialog"
:close-on-click-modal="false"
:model-value="dividendDialog.visible"
width="92%"
@close="closeDividendDialog"
>
<template #header>
<div class="title">{{ t('channel.dividend_record_dialog_title') }}</div>
</template>
<div v-loading="dividendDialog.loading" class="channel-record-dialog__body">
<div class="channel-record-table-wrap">
<el-table :data="dividendDialog.list" border size="small" class="channel-record-table">
<el-table-column prop="settlement_no" :label="t('channel.dividend_settlement_no')" min-width="150" show-overflow-tooltip />
<el-table-column prop="channel_name" :label="t('channel.dividend_channel_name')" min-width="100" />
<el-table-column prop="admin_username" :label="t('channel.dividend_admin_username')" min-width="100" />
<el-table-column prop="commission_amount" :label="t('channel.dividend_commission_amount')" min-width="100" align="right" />
<el-table-column :label="t('channel.dividend_period_range')" min-width="180">
<template #default="scope">
{{ scope.row.period_start_at || '-' }} ~ {{ scope.row.period_end_at || '-' }}
</template>
</el-table-column>
<el-table-column prop="settled_at" :label="t('channel.dividend_settled_at')" min-width="150" />
</el-table>
</div>
<div class="channel-record-pagination">
<el-pagination
background
layout="total, prev, pager, next"
:total="dividendDialog.total"
:page-size="dividendDialog.limit"
:current-page="dividendDialog.page"
@current-change="onDividendPageChange"
/>
</div>
</div>
</el-dialog>
<el-dialog
class="channel-record-dialog channel-bet-record-dialog"
:close-on-click-modal="false"
:model-value="betRecordDialog.visible"
width="92%"
@close="closeBetRecordDialog"
>
<template #header>
<div class="title">{{ betRecordDialog.title }}</div>
</template>
<div v-loading="betRecordDialog.loading" class="channel-record-dialog__body">
<div class="channel-bet-summary">
<el-card shadow="never" class="channel-bet-summary-card channel-stat-card">
<div class="label">{{ t('channel.bet_record_summary_count') }}</div>
<div class="value">{{ betRecordDialog.summary.record_count }}</div>
</el-card>
<el-card shadow="never" class="channel-bet-summary-card channel-stat-card">
<div class="label">{{ t('channel.bet_record_summary_bet') }}</div>
<div class="value">{{ betRecordDialog.summary.total_bet_amount }}</div>
</el-card>
<el-card shadow="never" class="channel-bet-summary-card channel-stat-card">
<div class="label">{{ t('channel.bet_record_summary_win') }}</div>
<div class="value">{{ betRecordDialog.summary.total_win_amount }}</div>
</el-card>
</div>
<el-form :inline="true" size="small" class="channel-bet-filter" @submit.prevent="onBetRecordSearch">
<el-form-item :label="t('channel.bet_record_period_no')">
<el-input
v-model="betRecordDialog.filters.period_no"
clearable
:placeholder="t('Fuzzy query')"
class="channel-bet-filter-input"
@keyup.enter="onBetRecordSearch"
/>
</el-form-item>
<el-form-item :label="t('channel.bet_record_user_username')">
<el-input
v-model="betRecordDialog.filters.user_keyword"
clearable
:placeholder="t('Fuzzy query')"
class="channel-bet-filter-input"
@keyup.enter="onBetRecordSearch"
/>
</el-form-item>
<el-form-item :label="t('channel.bet_record_result_number')">
<el-input
v-model="betRecordDialog.filters.result_number"
clearable
class="channel-bet-filter-input--short"
@keyup.enter="onBetRecordSearch"
/>
</el-form-item>
<el-form-item :label="t('channel.bet_record_pick_numbers')">
<el-input
v-model="betRecordDialog.filters.pick_number"
clearable
:placeholder="t('channel.bet_record_pick_filter_placeholder')"
class="channel-bet-filter-input--short"
@keyup.enter="onBetRecordSearch"
/>
</el-form-item>
<el-form-item :label="t('channel.bet_record_win_hit')">
<el-select
v-model="betRecordDialog.filters.win_hit"
clearable
:placeholder="t('Please select field', { field: t('channel.bet_record_win_hit') })"
class="channel-bet-filter-select"
>
<el-option :label="t('channel.bet_record_win_hit_won')" value="won" />
<el-option :label="t('channel.bet_record_win_hit_lost')" value="lost" />
<el-option :label="t('channel.bet_record_win_hit_pending')" value="pending" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onBetRecordSearch">{{ t('Search') }}</el-button>
<el-button @click="onBetRecordReset">{{ t('Reset') }}</el-button>
</el-form-item>
</el-form>
<div class="channel-record-table-wrap">
<el-table
:data="betRecordDialog.list"
border
size="small"
class="channel-bet-record-table channel-record-table"
>
<el-table-column
prop="period_no"
:label="t('channel.bet_record_period_no')"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="user_username"
:label="t('channel.bet_record_user_username')"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="betRecordDialog.mode === 'company'"
prop="channel_name"
:label="t('channel.bet_record_channel_name')"
min-width="100"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="pick_numbers"
:label="t('channel.bet_record_pick_numbers')"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="result_number"
:label="t('channel.bet_record_result_number')"
min-width="90"
align="center"
header-align="center"
/>
<el-table-column
:label="t('channel.bet_record_win_hit')"
min-width="90"
align="center"
header-align="center"
>
<template #default="scope">
<el-tag
v-if="scope.row.win_hit"
:type="betRecordWinHitTagType(scope.row.win_hit)"
effect="dark"
size="small"
>
{{ formatBetRecordWinHit(scope.row.win_hit) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
prop="total_amount"
:label="t('channel.bet_record_total_amount')"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="win_amount"
:label="t('channel.bet_record_win_amount')"
min-width="100"
align="center"
header-align="center"
/>
</el-table>
</div>
<div class="channel-record-pagination">
<el-pagination
background
layout="total, prev, pager, next"
:total="betRecordDialog.total"
:page-size="betRecordDialog.limit"
:current-page="betRecordDialog.page"
@current-change="onBetRecordPageChange"
/>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, provide, reactive, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { auth } from '/@/utils/common'
import { defaultOptButtons } from '/@/components/table'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
import createAxios from '/@/utils/axios'
defineOptions({
name: 'channel',
})
const { t } = useI18n()
const MANUAL_SETTLE_MOBILE_BREAKPOINT = 768
const manualSettleViewportMobile = ref(typeof window !== 'undefined' ? window.innerWidth <= MANUAL_SETTLE_MOBILE_BREAKPOINT : false)
const manualSettleCalcCollapse = ref<string[]>([])
const manualSettleDialogWidth = computed(() => (manualSettleViewportMobile.value ? '96%' : '920px'))
const manualSettleSummaryColumns = computed(() => (manualSettleViewportMobile.value ? 1 : 3))
const manualSettleFormSize = computed(() => (manualSettleViewportMobile.value ? 'small' : 'default'))
const manualSettleTableSize = computed(() => (manualSettleViewportMobile.value ? 'small' : 'default'))
const manualSettleFormLabelWidth = computed(() => 'auto')
const manualSettleTableLayout = computed(() => 'fixed')
const manualSettleSplitCol = computed(() => {
if (manualSettleViewportMobile.value) {
return {
adminWidth: 120,
baseWidth: 72,
shareWidth: 58,
grossWidth: 72,
commissionShareWidth: 62,
feeWidth: 64,
netWidth: 72,
}
}
return {
adminWidth: 132,
baseWidth: 88,
shareWidth: 72,
grossWidth: 88,
commissionShareWidth: 76,
feeWidth: 80,
netWidth: 88,
}
})
const manualSettleSplitTableMinWidth = computed(() => {
const col = manualSettleSplitCol.value
return (
col.adminWidth +
col.baseWidth +
col.shareWidth +
col.grossWidth +
col.commissionShareWidth +
col.feeWidth +
col.netWidth +
32
)
})
const manualSettleSplitInnerStyle = computed(() => {
if (!manualSettleViewportMobile.value) {
return {}
}
return {
width: `${manualSettleSplitTableMinWidth.value}px`,
}
})
const syncManualSettleViewport = () => {
if (typeof window === 'undefined') {
return
}
manualSettleViewportMobile.value = window.innerWidth <= MANUAL_SETTLE_MOBILE_BREAKPOINT
}
const dismissFloatingTooltips = () => {
if (typeof document === 'undefined') {
return
}
document.querySelectorAll('.el-popper[role="tooltip"]').forEach((node) => node.remove())
const active = document.activeElement
if (active instanceof HTMLElement) {
active.blur()
}
}
const tableRef = useTemplateRef('tableRef')
let optButtons: OptButton[] = [
{
render: 'tipButton',
name: 'manualSettle',
title: 'channel.manual_settle',
text: '',
type: 'warning',
icon: 'el-icon-Clock',
class: 'table-row-manual-settle',
disabledTip: true,
display: () => auth('manualSettle'),
click: (row: TableRow) => {
void openManualSettleDialog(row)
},
},
]
optButtons = optButtons.concat(defaultOptButtons(['edit', 'delete']))
const formatAmountInt = (_row: any, _column: any, cellValue: number | string | null) => {
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
const num = Number(cellValue)
if (Number.isNaN(num)) return '-'
return `${num}`
}
const formatCommissionSharePercent = (row: { commission_amount?: string | number; commission_share_percent?: string | number }) => {
const preset = row.commission_share_percent
if (preset !== null && preset !== undefined && preset !== '') {
const n = Number(preset)
if (Number.isFinite(n)) {
return `${n.toFixed(2)}%`
}
}
const total = Number(manualSettle.form.commission_amount ?? 0)
const gross = Number(row.commission_amount ?? 0)
if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(gross) || gross <= 0) {
return '0.00%'
}
return `${((gross / total) * 100).toFixed(2)}%`
}
const formatSplitAmountCell = (_row: anyObj, _column: any, cellValue: unknown) => {
if (cellValue === null || cellValue === undefined || cellValue === '') {
return '-'
}
const n = Number(cellValue)
if (!Number.isFinite(n)) {
return String(cellValue)
}
return n.toFixed(2)
}
const formatAmount2 = (_row: any, _column: any, cellValue: number | string | null) => {
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
const num = Number(cellValue)
if (Number.isNaN(num)) return '-'
return num.toFixed(2)
}
const formatBetRecordWinHit = (code: unknown) => {
const key = code === null || code === undefined ? '' : String(code)
if (!key) return '-'
const i18nKey = `channel.bet_record_win_hit_${key}`
const text = t(i18nKey)
return text !== i18nKey ? text : key
}
const betRecordWinHitTagType = (code: unknown): 'success' | 'info' | 'warning' | 'danger' => {
const key = code === null || code === undefined ? '' : String(code)
const map: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
won: 'success',
lost: 'info',
pending: 'warning',
}
return map[key] ?? 'info'
}
const formatSettleDay = (row: anyObj) => {
if (row.settle_cycle === 'weekly') {
return t(`channel.weekday ${row.settle_weekday ?? 1}`)
}
if (row.settle_cycle === 'monthly') {
return `${row.settle_monthday ?? 1}${t('channel.day_suffix')}`
}
return t('channel.settle_day_daily')
}
const manualSettle = reactive({
visible: false,
loading: false,
previewLoading: false,
channelId: 0,
form: {
settlement_no: '',
period_start_at: '',
period_end_at: '',
total_bet_amount: '',
total_payout_amount: '',
platform_profit_amount: '',
commission_rate: '',
calc_base_amount: '',
commission_amount: '',
agent_mode: 'turnover' as string,
settlement_handling_fee: '0.00',
commission_split: [] as Array<{
admin_id: number
admin_username: string
parent_admin_id?: number
level?: number
settlement_base_amount?: string
share_rate: string
commission_amount: string
commission_share_percent?: string
handling_fee_rate: number | string
handling_fee?: string
net_commission_amount?: string
}>,
remark: '',
},
})
const settleFilterMode = ref<'all' | 'with_balance' | 'no_balance' | 'enabled' | 'disabled'>('all')
const settleStats = reactive({
channel_total: 0,
enabled_count: 0,
disabled_count: 0,
carryover_positive_count: 0,
carryover_total: '0.00',
carryover_positive_total: '0.00',
paid_dividend_total: '0.00',
company_total_bet_amount: '0.00',
})
const dividendDialog = reactive({
visible: false,
loading: false,
page: 1,
limit: 20,
total: 0,
list: [] as anyObj[],
})
const createBetRecordFilters = () => ({
period_no: '',
user_keyword: '',
result_number: '',
pick_number: '',
win_hit: '' as '' | 'won' | 'lost' | 'pending',
})
const betRecordDialog = reactive({
visible: false,
loading: false,
mode: '' as '' | 'direct' | 'company',
channelId: 0,
title: '',
page: 1,
limit: 20,
total: 0,
list: [] as anyObj[],
filters: createBetRecordFilters(),
summary: {
record_count: 0,
total_bet_amount: '0.00',
total_win_amount: '0.00',
},
})
const resetManualSettleForm = () => {
manualSettle.form.settlement_no = ''
manualSettle.form.period_start_at = ''
manualSettle.form.period_end_at = ''
manualSettle.form.total_bet_amount = ''
manualSettle.form.total_payout_amount = ''
manualSettle.form.platform_profit_amount = ''
manualSettle.form.commission_rate = ''
manualSettle.form.calc_base_amount = ''
manualSettle.form.commission_amount = ''
manualSettle.form.commission_split = []
manualSettle.form.agent_mode = 'turnover'
manualSettle.form.settlement_handling_fee = '0.00'
manualSettle.form.remark = ''
}
type ManualSettleSplitRow = {
admin_id: number
admin_username: string
parent_admin_id?: number
level?: number
settlement_base_amount?: string
share_rate: string
commission_amount: string
commission_share_percent?: string
handling_fee_rate: number | string
handling_fee?: string
net_commission_amount?: string
children?: ManualSettleSplitRow[]
}
const buildCommissionSplitTree = (flat: ManualSettleSplitRow[]): ManualSettleSplitRow[] => {
if (!flat.length) {
return []
}
const nodeMap = new Map<number, ManualSettleSplitRow>()
for (const row of flat) {
nodeMap.set(row.admin_id, { ...row, children: [] })
}
const roots: ManualSettleSplitRow[] = []
for (const row of flat) {
const node = nodeMap.get(row.admin_id)
if (!node) {
continue
}
const parentId = Number(row.parent_admin_id ?? 0)
if (parentId > 0 && nodeMap.has(parentId)) {
const parent = nodeMap.get(parentId)
if (parent) {
if (!parent.children) {
parent.children = []
}
parent.children.push(node)
}
continue
}
roots.push(node)
}
const pruneEmptyChildren = (nodes: ManualSettleSplitRow[]) => {
for (const node of nodes) {
if (node.children && node.children.length > 0) {
pruneEmptyChildren(node.children)
} else {
delete node.children
}
}
}
pruneEmptyChildren(roots)
return roots
}
const manualSettleSplitTree = computed(() => buildCommissionSplitTree(manualSettle.form.commission_split))
const manualSettleCalcDescLines = computed(() => {
const mode = manualSettle.form.agent_mode === 'affiliate' ? 'affiliate' : 'turnover'
const feeRate = manualSettle.form.settlement_handling_fee
const prefix =
mode === 'affiliate'
? [
t('channel.manual_settle_calc_intro_affiliate_1'),
t('channel.manual_settle_calc_intro_affiliate_2'),
t('channel.manual_settle_calc_intro_affiliate_3'),
]
: [
t('channel.manual_settle_calc_intro_turnover_1'),
t('channel.manual_settle_calc_intro_turnover_2'),
t('channel.manual_settle_calc_intro_turnover_3'),
]
return [
...prefix,
t('channel.manual_settle_calc_tree_1'),
t('channel.manual_settle_calc_tree_2'),
t('channel.manual_settle_calc_tree_3'),
t('channel.manual_settle_calc_handling_fee', { rate: feeRate }),
]
})
const closeManualSettleDialog = () => {
manualSettle.visible = false
resetManualSettleForm()
}
const openManualSettleDialog = async (row: TableRow) => {
dismissFloatingTooltips()
syncManualSettleViewport()
manualSettle.channelId = row.id
resetManualSettleForm()
manualSettle.visible = true
await nextTick()
dismissFloatingTooltips()
manualSettle.previewLoading = true
try {
const res = await createAxios(
{
url: '/admin/channel/manualSettlePreview',
method: 'get',
params: { id: row.id },
},
{ showErrorMessage: true }
)
if (res.code !== 1 || !res.data) {
manualSettle.visible = false
return
}
const d = res.data as anyObj
manualSettle.form.settlement_no = d.settlement_no ?? ''
manualSettle.form.period_start_at = d.period_start_at ?? ''
manualSettle.form.period_end_at = d.period_end_at ?? ''
manualSettle.form.total_bet_amount = d.total_bet_amount ?? ''
manualSettle.form.total_payout_amount = d.total_payout_amount ?? ''
manualSettle.form.platform_profit_amount = d.platform_profit_amount ?? ''
manualSettle.form.commission_rate = d.commission_rate ?? ''
manualSettle.form.calc_base_amount = d.calc_base_amount ?? ''
manualSettle.form.commission_amount = d.commission_amount ?? ''
manualSettle.form.agent_mode = d.agent_mode ?? 'turnover'
manualSettle.form.settlement_handling_fee = d.settlement_handling_fee ?? '0.00'
manualSettle.form.commission_split = normalizeManualSettleSplit(d.commission_split, d.settlement_handling_fee)
manualSettle.form.remark = `${t('channel.manual_settle')}-CH${row.id}`
} catch {
manualSettle.visible = false
} finally {
manualSettle.previewLoading = false
}
}
const calcHandlingFeeByPercent = (gross: number, ratePercent: number) => {
const g = Number.isFinite(gross) && gross > 0 ? gross : 0
let rate = Number.isFinite(ratePercent) ? ratePercent : 0
if (rate < 0) rate = 0
if (rate > 100) rate = 100
return Math.round(g * rate * 100) / 10000
}
const applyManualSettleRowAmounts = (row: {
commission_amount?: string | number
handling_fee_rate?: string | number
handling_fee?: string | number
net_commission_amount?: string
}) => {
const gross = Number(row.commission_amount ?? 0)
const rate = Number(row.handling_fee_rate ?? 0)
const feeAmount = calcHandlingFeeByPercent(gross, rate)
row.handling_fee = feeAmount.toFixed(2)
row.net_commission_amount = Math.max(0, (Number.isFinite(gross) ? gross : 0) - feeAmount).toFixed(2)
}
const normalizeManualSettleSplit = (rows: unknown, defaultFeeRate: unknown) => {
const feeDefault = Number(defaultFeeRate)
const baseRate = Number.isFinite(feeDefault) && feeDefault >= 0 ? Math.min(100, feeDefault) : 0
if (!Array.isArray(rows)) {
return []
}
return rows.map((row: anyObj) => {
let rate = Number(row.handling_fee_rate ?? row.handling_fee ?? baseRate)
if (!Number.isFinite(rate) || rate < 0) {
rate = baseRate
}
if (rate > 100) {
rate = 100
}
const normalized = {
...row,
handling_fee_rate: rate,
}
applyManualSettleRowAmounts(normalized)
return normalized
})
}
const formatNetCommission = (row: {
commission_amount?: string | number
handling_fee_rate?: string | number
net_commission_amount?: string
}) => {
if (row.net_commission_amount !== undefined && row.net_commission_amount !== '') {
return row.net_commission_amount
}
const gross = Number(row.commission_amount ?? 0)
const rate = Number(row.handling_fee_rate ?? 0)
const net = Math.max(0, (Number.isFinite(gross) ? gross : 0) - calcHandlingFeeByPercent(gross, rate))
return net.toFixed(2)
}
const submitManualSettle = async () => {
if (!manualSettle.channelId) return
manualSettle.loading = true
try {
await createAxios(
{
url: '/admin/channel/manualSettle',
method: 'post',
data: {
id: manualSettle.channelId,
remark: manualSettle.form.remark,
commission_split: manualSettle.form.commission_split.map((row) => ({
admin_id: row.admin_id,
handling_fee_rate: row.handling_fee_rate ?? 0,
})),
},
},
{ showSuccessMessage: true }
)
closeManualSettleDialog()
baTable.onTableHeaderAction('refresh', { event: 'manual-settle' })
} finally {
manualSettle.loading = false
}
}
const onBatchSettlePending = async () => {
await createAxios(
{
url: '/admin/channel/batchSettlePending',
method: 'post',
},
{ showSuccessMessage: true }
)
await loadSettleStats()
baTable.onTableHeaderAction('refresh', { event: 'batch-settle' })
}
const onSettleFilterChange = () => {
baTable.getData()
}
const loadSettleStats = async () => {
const res = await createAxios({ url: '/admin/channel/settleStats', method: 'get' })
if (res.code !== 1 || !res.data) {
return
}
settleStats.channel_total = Number(res.data.channel_total ?? 0)
settleStats.enabled_count = Number(res.data.enabled_count ?? 0)
settleStats.disabled_count = Number(res.data.disabled_count ?? 0)
settleStats.carryover_positive_count = Number(res.data.carryover_positive_count ?? 0)
settleStats.carryover_total = String(res.data.carryover_total ?? '0.00')
settleStats.carryover_positive_total = String(res.data.carryover_positive_total ?? '0.00')
settleStats.paid_dividend_total = String(res.data.paid_dividend_total ?? '0.00')
settleStats.company_total_bet_amount = String(res.data.company_total_bet_amount ?? '0.00')
}
const loadDividendRecords = async () => {
dividendDialog.loading = true
try {
const res = await createAxios({
url: '/admin/channel/dividendRecordList',
method: 'get',
params: { page: dividendDialog.page, limit: dividendDialog.limit },
})
if (res.code !== 1 || !res.data) {
return
}
dividendDialog.list = Array.isArray(res.data.list) ? res.data.list : []
dividendDialog.total = Number(res.data.total ?? 0)
} finally {
dividendDialog.loading = false
}
}
const onPaidDividendCardClick = () => {
if (!auth('index')) {
return
}
dividendDialog.page = 1
dividendDialog.visible = true
void loadDividendRecords()
}
const onCompanyTotalBetCardClick = () => {
if (!auth('index')) {
return
}
resetBetRecordDialog()
betRecordDialog.mode = 'company'
betRecordDialog.channelId = 0
betRecordDialog.title = t('channel.company_bet_record_dialog_title')
betRecordDialog.visible = true
void loadBetRecords()
}
const closeDividendDialog = () => {
dividendDialog.visible = false
dividendDialog.list = []
dividendDialog.total = 0
dividendDialog.page = 1
}
const onDividendPageChange = (page: number) => {
dividendDialog.page = page
void loadDividendRecords()
}
const resetBetRecordDialog = () => {
betRecordDialog.mode = ''
betRecordDialog.channelId = 0
betRecordDialog.title = ''
betRecordDialog.page = 1
betRecordDialog.total = 0
betRecordDialog.list = []
betRecordDialog.summary = {
record_count: 0,
total_bet_amount: '0.00',
total_win_amount: '0.00',
}
Object.assign(betRecordDialog.filters, createBetRecordFilters())
}
const buildBetRecordFilterParams = () => {
const f = betRecordDialog.filters
const params: Record<string, string> = {}
if (f.period_no.trim()) {
params.period_no = f.period_no.trim()
}
if (f.user_keyword.trim()) {
params.user_keyword = f.user_keyword.trim()
}
if (f.result_number.trim()) {
params.result_number = f.result_number.trim()
}
if (f.pick_number.trim()) {
params.pick_number = f.pick_number.trim()
}
if (f.win_hit) {
params.win_hit = f.win_hit
}
return params
}
const loadBetRecords = async () => {
if (!betRecordDialog.mode) {
return
}
if (betRecordDialog.mode === 'direct' && !betRecordDialog.channelId) {
return
}
const url =
betRecordDialog.mode === 'direct'
? '/admin/channel/directBetRecordList'
: '/admin/channel/companyBetRecordList'
betRecordDialog.loading = true
try {
const params: Record<string, string | number> = {
page: betRecordDialog.page,
limit: betRecordDialog.limit,
...buildBetRecordFilterParams(),
}
if (betRecordDialog.mode === 'direct') {
params.channel_id = betRecordDialog.channelId
}
const res = await createAxios({
url,
method: 'get',
params,
})
if (res.code !== 1 || !res.data) {
return
}
betRecordDialog.list = Array.isArray(res.data.list) ? res.data.list : []
betRecordDialog.total = Number(res.data.total ?? 0)
const summary = res.data.summary
if (summary && typeof summary === 'object') {
betRecordDialog.summary.record_count = Number(summary.record_count ?? 0)
betRecordDialog.summary.total_bet_amount = String(summary.total_bet_amount ?? '0.00')
betRecordDialog.summary.total_win_amount = String(summary.total_win_amount ?? '0.00')
}
} finally {
betRecordDialog.loading = false
}
}
const openBetRecordDialog = (mode: 'direct' | 'company', row: TableRow) => {
if (!auth('index')) {
return
}
resetBetRecordDialog()
betRecordDialog.mode = mode
betRecordDialog.channelId = Number(row.id ?? 0)
betRecordDialog.title =
mode === 'direct'
? `${t('channel.direct_bet_record_dialog_title')} - ${row.name ?? row.id}`
: t('channel.company_bet_record_dialog_title')
betRecordDialog.visible = true
void loadBetRecords()
}
const closeBetRecordDialog = () => {
betRecordDialog.visible = false
resetBetRecordDialog()
}
const onBetRecordPageChange = (page: number) => {
betRecordDialog.page = page
void loadBetRecords()
}
const onBetRecordSearch = () => {
betRecordDialog.page = 1
void loadBetRecords()
}
const onBetRecordReset = () => {
Object.assign(betRecordDialog.filters, createBetRecordFilters())
onBetRecordSearch()
}
const openDirectBetDialog = (row: TableRow) => {
void openBetRecordDialog('direct', row)
}
const baTable = new baTableClass(
new baTableApi('/admin/channel/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('channel.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{
label: t('channel.code'),
prop: 'code',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
operator: 'LIKE',
},
{
label: t('channel.name'),
prop: 'name',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
operator: 'LIKE',
},
{
label: t('channel.agent_mode'),
prop: 'agent_mode',
align: 'center',
minWidth: 120,
operator: 'eq',
sortable: false,
render: 'tag',
custom: {
turnover: 'primary',
affiliate: 'success',
},
replaceValue: {
turnover: t('channel.agent_mode turnover'),
affiliate: t('channel.agent_mode affiliate'),
},
},
{
label: t('channel.carryover_balance'),
prop: 'carryover_balance',
align: 'center',
minWidth: 130,
sortable: false,
operator: 'RANGE',
formatter: formatAmountInt,
},
{
label: t('channel.affiliate_contract_no'),
prop: 'affiliate_contract_no',
align: 'center',
minWidth: 140,
sortable: false,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
},
{
label: t('channel.settle_cycle'),
prop: 'settle_cycle',
align: 'center',
width: 110,
operator: 'eq',
sortable: false,
render: 'tag',
custom: { daily: 'info', weekly: 'primary', monthly: 'success' },
replaceValue: {
daily: t('channel.settle_cycle daily'),
weekly: t('channel.settle_cycle weekly'),
monthly: t('channel.settle_cycle monthly'),
},
},
{
label: t('channel.settle_weekday'),
prop: 'settle_day',
align: 'center',
width: 110,
operator: false,
sortable: false,
formatter: (row: anyObj) => formatSettleDay(row),
},
{
label: t('channel.settle_time'),
prop: 'settle_time',
align: 'center',
width: 100,
operator: 'LIKE',
sortable: false,
},
{
label: t('channel.affiliate_effective_start_at'),
prop: 'affiliate_effective_start_at',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('channel.affiliate_effective_end_at'),
prop: 'affiliate_effective_end_at',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('channel.user_count'),
prop: 'user_count',
align: 'center',
sortable: false,
operator: 'RANGE',
},
{
label: t('channel.direct_bet_amount'),
prop: 'direct_bet_amount',
align: 'center',
minWidth: 120,
sortable: false,
operator: false,
render: 'tag',
formatter: formatAmount2,
customRenderAttr: {
tag: ({ row }: { row: TableRow }) => ({
class: auth('index') ? 'channel-direct-bet-tag' : '',
onClick: auth('index') ? () => openDirectBetDialog(row) : undefined,
}),
},
},
{
label: t('channel.profit_amount'),
prop: 'profit_amount',
align: 'center',
minWidth: 110,
sortable: false,
operator: 'RANGE',
formatter: formatAmountInt,
},
{
label: t('channel.total_profit_amount'),
prop: 'total_profit_amount',
align: 'center',
minWidth: 110,
sortable: false,
operator: 'RANGE',
formatter: formatAmountInt,
},
{
label: t('channel.commission_pool_amount'),
prop: 'commission_pool_amount',
align: 'center',
minWidth: 110,
sortable: false,
operator: 'RANGE',
formatter: formatAmountInt,
},
{
label: t('channel.status'),
prop: 'status',
align: 'center',
operator: 'eq',
sortable: false,
render: 'switch',
replaceValue: { '0': t('channel.status 0'), '1': t('channel.status 1') },
},
{
label: t('channel.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('channel.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 160, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
dblClickNotEditColumn: [undefined, 'status'],
},
{
defaultItems: {
status: '1',
agent_mode: 'turnover',
settlement_handling_fee: 0,
settle_cycle: 'weekly',
settle_weekday: 1,
settle_monthday: 1,
settle_time: '02:00:00',
},
}
)
baTable.before.getData = () => {
const filter = baTable.table.filter || {}
const searchRaw = filter.search
const search = Array.isArray(searchRaw) ? searchRaw.filter((item: any) => item && item.field !== 'carryover_balance' && item.field !== 'status') : []
if (settleFilterMode.value === 'with_balance') {
search.push({ field: 'carryover_balance', operator: 'gt', val: 0 })
} else if (settleFilterMode.value === 'no_balance') {
search.push({ field: 'carryover_balance', operator: 'elt', val: 0 })
} else if (settleFilterMode.value === 'enabled') {
search.push({ field: 'status', operator: 'eq', val: 1 })
} else if (settleFilterMode.value === 'disabled') {
search.push({ field: 'status', operator: 'eq', val: 0 })
}
filter.search = search
baTable.table.filter = filter
}
baTable.after.getData = () => {
void loadSettleStats()
}
provide('baTable', baTable)
onMounted(() => {
syncManualSettleViewport()
window.addEventListener('resize', syncManualSettleViewport)
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
void loadSettleStats()
})
onUnmounted(() => {
window.removeEventListener('resize', syncManualSettleViewport)
})
</script>
<style scoped lang="scss">
.mb-12 {
margin-bottom: 12px;
}
.share-total-row {
margin-top: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.share-group-single {
font-size: 12px;
line-height: 1.4;
color: var(--el-text-color-regular);
}
.share-group-empty {
color: var(--el-text-color-placeholder);
}
.channel-top-actions {
margin: 8px 0 12px;
}
.channel-stats-cards {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.channel-stats-cards :deep(.el-card__body) {
padding: 10px 8px;
}
.channel-stat-card {
text-align: center;
min-width: 0;
}
.channel-stat-card-clickable {
cursor: pointer;
transition: box-shadow 0.2s ease;
}
.channel-stat-card-clickable:hover {
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
}
.channel-stat-card-clickable .value {
color: var(--el-color-primary);
}
:deep(.channel-direct-bet-tag) {
cursor: pointer;
}
.channel-bet-filter {
margin-bottom: 12px;
padding: 10px 12px 2px;
background: var(--el-fill-color-lighter);
border-radius: 6px;
}
.channel-bet-filter :deep(.el-form-item) {
margin-bottom: 8px;
margin-right: 12px;
}
.channel-bet-filter-input {
width: 140px;
}
.channel-bet-filter-input--short {
width: 100px;
}
.channel-bet-filter-select {
width: 120px;
}
.channel-bet-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.channel-bet-summary-card {
text-align: center;
}
.channel-bet-summary-card .label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.channel-bet-summary-card .value {
margin-top: 6px;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.channel-bet-record-table :deep(.el-table__cell) {
text-align: center;
}
.channel-bet-record-table :deep(.cell) {
display: flex;
align-items: center;
justify-content: center;
}
.channel-record-pagination {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.channel-stat-card .label {
font-size: 11px;
color: var(--el-text-color-secondary);
line-height: 1.3;
word-break: break-all;
}
.channel-stat-card .value {
margin-top: 4px;
font-size: 17px;
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.2;
}
.channel-action-row {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
/* 手动结算弹窗:可滚动(勿使用 ba-operate-dialog 固定 58vh 高度) */
:deep(.manual-settle-dialog.el-dialog) {
display: flex !important;
flex-direction: column;
width: 92% !important;
max-width: 920px;
max-height: 90vh;
margin: 5vh auto !important;
padding-bottom: 0;
overflow: hidden;
}
:deep(.manual-settle-dialog .el-dialog__header) {
flex-shrink: 0;
padding: 14px 20px;
margin-right: 0;
}
:deep(.manual-settle-dialog .el-dialog__header .title) {
font-size: 16px;
font-weight: 600;
}
:deep(.manual-settle-dialog .el-dialog__body) {
flex: 1 1 auto;
min-height: 0;
height: auto !important;
max-height: calc(90vh - 116px);
overflow-y: auto !important;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
padding: 16px 20px 18px;
}
:deep(.manual-settle-dialog .el-dialog__footer) {
flex-shrink: 0;
position: static;
width: auto;
box-shadow: none;
border-top: 1px solid var(--el-border-color-lighter);
}
.manual-settle-dialog-body {
min-height: 0;
overflow: visible;
}
:deep(.manual-settle-dialog-body .el-loading-mask) {
border-radius: 4px;
}
.manual-settle-summary {
margin-bottom: 16px;
}
.manual-settle-summary :deep(.el-descriptions__label) {
width: 148px;
font-size: 13px;
color: var(--el-text-color-secondary);
font-weight: 500;
}
.manual-settle-summary :deep(.el-descriptions__content) {
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-primary);
}
.manual-settle-summary-highlight {
font-size: 15px;
font-weight: 600;
color: var(--el-color-primary);
}
.manual-settle-form {
display: block;
min-width: 0;
max-width: 100%;
}
.manual-settle-form :deep(.el-form-item) {
margin-bottom: 14px;
}
.manual-settle-split-form-item :deep(.el-form-item__label) {
font-size: 14px;
font-weight: 600;
line-height: 1.5;
padding-bottom: 8px;
color: var(--el-text-color-primary);
}
.manual-settle-form-item-full {
grid-column: 1 / -1;
min-width: 0;
}
.manual-settle-form-item-full :deep(.el-form-item__content) {
min-width: 0;
max-width: 100%;
}
.manual-settle-split-block {
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
.manual-settle-split-table-scroll {
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
.manual-settle-split-table-scroll.is-mobile {
overflow-x: scroll;
overflow-y: hidden;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-x;
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
background: var(--el-fill-color-blank);
}
.manual-settle-split-table-inner.is-mobile {
display: inline-block;
max-width: none;
vertical-align: top;
}
.manual-settle-split-table-scroll.is-mobile :deep(.el-table) {
width: 100% !important;
}
.manual-settle-split-table-scroll.is-mobile :deep(.el-table__inner-wrapper) {
width: 100% !important;
}
.manual-settle-split-table-scroll.is-mobile :deep(.el-table__body-wrapper),
.manual-settle-split-table-scroll.is-mobile :deep(.el-table__header-wrapper) {
overflow: visible !important;
}
.manual-settle-split-table-scroll.is-mobile :deep(.el-scrollbar__wrap) {
overflow: visible !important;
}
.manual-settle-split-table-scroll.is-mobile :deep(.el-scrollbar__bar.is-horizontal) {
display: none !important;
}
.manual-settle-split-scroll-tip {
margin: 6px 0 0;
font-size: 11px;
line-height: 1.4;
color: var(--el-text-color-secondary);
text-align: center;
}
.manual-settle-split-table:not(.is-mobile) {
width: 100%;
}
.manual-settle-split-table :deep(.el-table__header th.el-table__cell),
.manual-settle-split-table :deep(.el-table__body td.el-table__cell) {
padding: 10px 8px;
}
.manual-settle-split-table :deep(.el-table__header .cell),
.manual-settle-split-table :deep(.el-table__body .cell) {
padding: 0 4px;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.manual-settle-split-table :deep(.el-table__header .cell) {
white-space: normal;
font-weight: 600;
color: var(--el-text-color-primary);
}
.manual-settle-split-table.is-mobile :deep(.el-table__header th.el-table__cell),
.manual-settle-split-table.is-mobile :deep(.el-table__body td.el-table__cell) {
padding: 5px 3px;
}
.manual-settle-split-table.is-mobile :deep(.el-table__header .cell),
.manual-settle-split-table.is-mobile :deep(.el-table__body .cell) {
font-size: 11px;
line-height: 1.3;
white-space: nowrap;
word-break: keep-all;
overflow-wrap: normal;
}
.manual-settle-split-table.is-mobile :deep(.el-table__body td:first-child .cell) {
white-space: nowrap;
word-break: keep-all;
}
.manual-settle-calc-collapse {
margin-top: 12px;
border: none;
}
.manual-settle-calc-collapse :deep(.el-collapse-item__header) {
height: 40px;
line-height: 40px;
font-size: 14px;
font-weight: 500;
border-bottom: none;
background: var(--el-fill-color-light);
border-radius: 6px;
padding: 0 12px;
}
.manual-settle-calc-collapse :deep(.el-collapse-item__wrap) {
border-bottom: none;
}
.manual-settle-calc-collapse :deep(.el-collapse-item__content) {
padding: 10px 12px 4px;
}
.manual-settle-calc-list {
margin: 0;
padding-left: 20px;
line-height: 1.65;
font-size: 13px;
color: var(--el-text-color-regular);
}
.manual-settle-calc-list li + li {
margin-top: 4px;
}
.manual-settle-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 4px 0 0;
}
.manual-settle-footer .el-button {
min-width: 88px;
}
/* 渠道记录弹窗:可滚动、适配移动端(勿使用 ba-operate-dialog 固定高度) */
:deep(.channel-record-dialog.el-dialog) {
display: flex;
flex-direction: column;
max-height: 92vh;
margin: 4vh auto !important;
padding-bottom: 0;
overflow: hidden;
width: 92% !important;
max-width: 1080px;
}
:deep(.channel-record-dialog.channel-bet-record-dialog.el-dialog) {
max-width: 1080px;
}
:deep(.channel-record-dialog .el-dialog__header) {
flex-shrink: 0;
padding: 12px 16px;
margin-right: 0;
}
:deep(.channel-record-dialog .el-dialog__body) {
flex: 1;
min-height: 0;
height: auto !important;
max-height: calc(92vh - 56px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding: 12px 14px 16px;
}
.channel-record-dialog__body {
min-height: 0;
}
.channel-record-table-wrap {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.channel-record-table {
min-width: 640px;
}
.channel-bet-record-table {
min-width: 880px;
}
@media (max-width: 900px) {
.channel-stats-cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.manual-settle-summary :deep(.el-descriptions__label) {
width: auto;
font-size: 12px;
}
.manual-settle-summary :deep(.el-descriptions__content) {
font-size: 13px;
}
}
@media (max-width: 768px) {
:deep(.manual-settle-dialog.el-dialog) {
width: 96% !important;
max-width: none;
max-height: 94vh;
margin: 3vh auto !important;
}
:deep(.manual-settle-dialog .el-dialog__header) {
padding: 10px 12px;
}
:deep(.manual-settle-dialog .el-dialog__body) {
max-height: calc(94vh - 96px);
padding: 12px 12px 14px;
}
:deep(.manual-settle-dialog .el-dialog__footer) {
padding: 8px 10px 10px;
}
.manual-settle-summary {
margin-bottom: 12px;
}
.manual-settle-form :deep(.el-form-item) {
margin-bottom: 10px;
}
.manual-settle-split-form-item :deep(.el-form-item__label) {
font-size: 13px;
padding-bottom: 6px;
}
.manual-settle-form-item-full :deep(.el-form-item__content) {
width: 100%;
max-width: 100%;
min-width: 0;
}
.manual-settle-split-block {
max-width: 100%;
min-width: 0;
overflow: hidden;
margin: 0;
padding-bottom: 0;
}
.manual-settle-split-table-scroll.is-mobile {
max-width: 100%;
}
.manual-settle-calc-collapse :deep(.el-collapse-item__header) {
font-size: 12px;
}
.manual-settle-calc-list {
font-size: 11px;
padding-left: 16px;
}
.manual-settle-footer {
flex-wrap: wrap;
justify-content: stretch;
}
.manual-settle-footer .el-button {
flex: 1;
margin: 0;
}
:deep(.channel-record-dialog.el-dialog) {
width: 96% !important;
max-height: 94vh;
margin: 3vh auto !important;
}
:deep(.channel-record-dialog .el-dialog__body) {
max-height: calc(94vh - 52px);
padding: 10px 10px 14px;
}
.channel-bet-summary {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
margin-bottom: 10px;
}
.channel-bet-summary-card :deep(.el-card__body) {
padding: 8px 4px;
}
.channel-bet-summary-card .label {
font-size: 10px;
line-height: 1.3;
}
.channel-bet-summary-card .value {
margin-top: 4px;
font-size: 13px;
word-break: break-all;
}
.channel-bet-filter {
padding: 8px 10px 0;
}
.channel-bet-filter :deep(.el-form-item) {
display: flex;
width: 100%;
margin-right: 0;
}
.channel-bet-filter :deep(.el-form-item__label) {
width: 72px !important;
padding-right: 6px;
}
.channel-bet-filter :deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
.channel-bet-filter-input,
.channel-bet-filter-input--short,
.channel-bet-filter-select {
width: 100%;
}
.channel-bet-filter :deep(.el-form-item:last-child .el-form-item__content) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.channel-record-pagination {
justify-content: center;
}
.channel-record-pagination :deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
row-gap: 6px;
}
}
</style>