新增代理管理器与二级代理体系,完善信用额度/上下分上下文与冻结策略;代理端玩家与子代理管理增强;玩家端新增钱包详情页与交易筛选优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
726 lines
23 KiB
Vue
726 lines
23 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue';
|
||
import type { TableColumnCtx } from 'element-plus';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||
import { resolveApiError } from '../i18n/form-validation';
|
||
import api from '../api';
|
||
|
||
interface CashbackBatch {
|
||
id: string;
|
||
batchNo: string;
|
||
periodStart: string;
|
||
periodEnd: string;
|
||
playerCount: number;
|
||
totalAmount: string;
|
||
totalEffectiveStake?: string;
|
||
totalBetCount?: number;
|
||
status: string;
|
||
operatorUsername?: string | null;
|
||
confirmedAt?: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
interface CashbackPreviewItem {
|
||
userId: string;
|
||
username: string;
|
||
agentUsername: string | null;
|
||
effectiveStake: string;
|
||
betCount: number;
|
||
rate: string;
|
||
amount: string;
|
||
}
|
||
|
||
interface CashbackPreview {
|
||
batch: CashbackBatch;
|
||
items: CashbackPreviewItem[];
|
||
totalAmount: string;
|
||
totalEffectiveStake?: string;
|
||
totalBetCount?: number;
|
||
avgRate?: string;
|
||
replacedPreviewCount?: number;
|
||
}
|
||
|
||
const { t, localeTag } = useAdminLocale();
|
||
|
||
const preview = ref<CashbackPreview | null>(null);
|
||
const rulesVisible = ref(false);
|
||
const loading = ref(false);
|
||
const historyLoading = ref(false);
|
||
const detailLoading = ref(false);
|
||
const detailVisible = ref(false);
|
||
const detail = ref<CashbackPreview | null>(null);
|
||
|
||
const history = ref<CashbackBatch[]>([]);
|
||
const historyTotal = ref(0);
|
||
const historyPage = ref(1);
|
||
const historyPageSize = ref(10);
|
||
const historyStatus = ref('');
|
||
|
||
const period = ref({
|
||
start: new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
|
||
end: new Date().toISOString().slice(0, 10),
|
||
});
|
||
|
||
const previewItems = computed(() => preview.value?.items ?? []);
|
||
const detailItems = computed(() => detail.value?.items ?? []);
|
||
|
||
function formatRate(value: string | number | null | undefined) {
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n)) return '—';
|
||
return `${(n * 100).toFixed(2)}%`;
|
||
}
|
||
|
||
function formatPeriodDate(value: string) {
|
||
if (!value) return '—';
|
||
return value.slice(0, 10);
|
||
}
|
||
|
||
function formatPeriodRange(row: Pick<CashbackBatch, 'periodStart' | 'periodEnd'>) {
|
||
return `${formatPeriodDate(row.periodStart)} — ${formatPeriodDate(row.periodEnd)}`;
|
||
}
|
||
|
||
function formatTime(value: string | null | undefined) {
|
||
if (!value) return '—';
|
||
return new Date(value).toLocaleString(localeTag.value, {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
|
||
function statusLabel(status: string) {
|
||
const key = `cashback.status.${status}`;
|
||
const label = t(key);
|
||
return label === key ? status : label;
|
||
}
|
||
|
||
function statusTagType(status: string) {
|
||
if (status === 'CONFIRMED') return 'success';
|
||
if (status === 'CANCELLED') return 'info';
|
||
return 'warning';
|
||
}
|
||
|
||
function tableSummary(param: {
|
||
columns: TableColumnCtx<CashbackPreviewItem>[];
|
||
data: CashbackPreviewItem[];
|
||
}) {
|
||
const { columns, data } = param;
|
||
const stakeSum = data.reduce((s, row) => s + Number(row.effectiveStake), 0);
|
||
const betSum = data.reduce((s, row) => s + Number(row.betCount ?? 0), 0);
|
||
const amountSum = data.reduce((s, row) => s + Number(row.amount), 0);
|
||
|
||
return columns.map((col, index) => {
|
||
if (index === 0) return t('cashback.table_total');
|
||
if (col.property === 'betCount') return String(betSum);
|
||
if (col.property === 'effectiveStake') return formatAmount(stakeSum);
|
||
if (col.property === 'amount') return formatAmount(amountSum);
|
||
return '';
|
||
});
|
||
}
|
||
|
||
async function loadHistory() {
|
||
historyLoading.value = true;
|
||
try {
|
||
const { data } = await api.get('/admin/cashbacks', {
|
||
params: {
|
||
page: historyPage.value,
|
||
pageSize: historyPageSize.value,
|
||
status: historyStatus.value || undefined,
|
||
},
|
||
});
|
||
history.value = (data.data.items ?? []) as CashbackBatch[];
|
||
historyTotal.value = data.data.total ?? 0;
|
||
} catch (err) {
|
||
ElMessage.error(resolveApiError(err, t, 'msg.load_failed'));
|
||
} finally {
|
||
historyLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function generatePreview() {
|
||
loading.value = true;
|
||
try {
|
||
const { data } = await api.post('/admin/cashbacks/preview', {
|
||
periodStart: period.value.start,
|
||
periodEnd: period.value.end,
|
||
});
|
||
preview.value = data.data as CashbackPreview;
|
||
const replaced = preview.value.replacedPreviewCount ?? 0;
|
||
if (replaced > 0) {
|
||
ElMessage.warning(t('msg.cashback_preview_replaced', { n: replaced }));
|
||
} else {
|
||
ElMessage.success(t('msg.cashback_preview_ready'));
|
||
}
|
||
await loadHistory();
|
||
} catch (err) {
|
||
ElMessage.error(resolveApiError(err, t, 'msg.save_failed'));
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function confirmBatchId(batchId: string) {
|
||
try {
|
||
await ElMessageBox.confirm(t('cashback.confirm_prompt'), t('common.confirm'), {
|
||
type: 'warning',
|
||
confirmButtonText: t('cashback.confirm_issue'),
|
||
cancelButtonText: t('common.cancel'),
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await api.post(`/admin/cashbacks/${batchId}/confirm`);
|
||
ElMessage.success(t('msg.cashback_issued'));
|
||
if (preview.value?.batch.id === batchId) preview.value = null;
|
||
if (detail.value?.batch.id === batchId) detailVisible.value = false;
|
||
await loadHistory();
|
||
} catch (err) {
|
||
ElMessage.error(resolveApiError(err, t, 'msg.save_failed'));
|
||
}
|
||
}
|
||
|
||
async function cancelBatchId(batchId: string) {
|
||
try {
|
||
await ElMessageBox.confirm(t('cashback.cancel_prompt'), t('common.confirm'), {
|
||
type: 'warning',
|
||
confirmButtonText: t('cashback.cancel_issue'),
|
||
cancelButtonText: t('common.cancel'),
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await api.post(`/admin/cashbacks/${batchId}/cancel`);
|
||
ElMessage.success(t('msg.cashback_cancelled'));
|
||
if (preview.value?.batch.id === batchId) preview.value = null;
|
||
if (detail.value?.batch.id === batchId) detailVisible.value = false;
|
||
await loadHistory();
|
||
} catch (err) {
|
||
ElMessage.error(resolveApiError(err, t, 'msg.save_failed'));
|
||
}
|
||
}
|
||
|
||
async function confirm() {
|
||
if (!preview.value?.batch) return;
|
||
await confirmBatchId(preview.value.batch.id);
|
||
}
|
||
|
||
async function openDetail(batchId: string) {
|
||
detailLoading.value = true;
|
||
detailVisible.value = true;
|
||
detail.value = null;
|
||
try {
|
||
const { data } = await api.get(`/admin/cashbacks/${batchId}`);
|
||
detail.value = data.data as CashbackPreview;
|
||
} catch (err) {
|
||
detailVisible.value = false;
|
||
ElMessage.error(resolveApiError(err, t, 'msg.load_failed'));
|
||
} finally {
|
||
detailLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function onHistoryPageChange(p: number) {
|
||
historyPage.value = p;
|
||
loadHistory();
|
||
}
|
||
|
||
function onHistorySizeChange(size: number) {
|
||
historyPageSize.value = size;
|
||
historyPage.value = 1;
|
||
loadHistory();
|
||
}
|
||
|
||
function onHistoryStatusChange() {
|
||
historyPage.value = 1;
|
||
loadHistory();
|
||
}
|
||
|
||
onMounted(loadHistory);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-scroll">
|
||
<el-card class="tool-card" shadow="never">
|
||
<div class="filter-row">
|
||
<el-form inline>
|
||
<el-form-item :label="t('cashback.start_date')">
|
||
<el-date-picker v-model="period.start" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('cashback.end_date')">
|
||
<el-date-picker v-model="period.end" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="loading" @click="generatePreview">
|
||
{{ t('cashback.preview_btn') }}
|
||
</el-button>
|
||
<button
|
||
type="button"
|
||
class="rules-help-btn"
|
||
:aria-label="t('cashback.rules_title')"
|
||
:title="t('cashback.rules_title')"
|
||
@click="rulesVisible = true"
|
||
>
|
||
?
|
||
</button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-dialog
|
||
v-model="rulesVisible"
|
||
:title="t('cashback.rules_title')"
|
||
width="560px"
|
||
destroy-on-close
|
||
>
|
||
<ul class="rules-list">
|
||
<li>{{ t('cashback.rule_period') }}</li>
|
||
<li>{{ t('cashback.rule_eligible') }}</li>
|
||
<li>{{ t('cashback.rule_formula') }}</li>
|
||
<li>{{ t('cashback.rule_rate') }}</li>
|
||
<li>{{ t('cashback.rule_flow') }}</li>
|
||
<li class="rules-note">{{ t('cashback.rule_note_zero') }}</li>
|
||
</ul>
|
||
</el-dialog>
|
||
|
||
<template v-if="preview">
|
||
<el-card class="preview-card" shadow="never">
|
||
<div class="preview-head">
|
||
<div>
|
||
<div class="preview-title">{{ t('cashback.preview_title') }}</div>
|
||
<div class="preview-meta">
|
||
<span>{{ t('cashback.batch_no') }}:{{ preview.batch.batchNo }}</span>
|
||
<span class="meta-sep">·</span>
|
||
<span>{{ formatPeriodRange(preview.batch) }}</span>
|
||
</div>
|
||
</div>
|
||
<el-tag :type="statusTagType(preview.batch.status)" size="small">
|
||
{{ statusLabel(preview.batch.status) }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<el-row :gutter="12" class="preview-stats">
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ preview.batch.playerCount ?? 0 }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.players') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value pstat-green">{{ formatAmount(preview.totalAmount) }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.total') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ formatAmount(preview.totalEffectiveStake ?? 0) }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.effective_stake') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ preview.totalBetCount ?? 0 }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.bet_count') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ formatRate(preview.avgRate) }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.avg_rate') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ previewItems.length }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.lines') }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</el-card>
|
||
|
||
<el-card class="data-card" shadow="never">
|
||
<div class="table-title">{{ t('cashback.table_title') }}</div>
|
||
<div class="table-wrap">
|
||
<el-table
|
||
:data="previewItems"
|
||
stripe
|
||
class="cashback-table"
|
||
show-summary
|
||
:summary-method="tableSummary"
|
||
:empty-text="t('cashback.empty_items')"
|
||
>
|
||
<el-table-column type="index" :label="t('cashback.col.index')" width="56" align="center" />
|
||
<el-table-column
|
||
prop="username"
|
||
:label="t('cashback.col.player')"
|
||
min-width="120"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="agentUsername"
|
||
:label="t('cashback.col.agent')"
|
||
min-width="120"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.agentUsername || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="betCount"
|
||
:label="t('cashback.col.bet_count')"
|
||
width="88"
|
||
align="right"
|
||
/>
|
||
<el-table-column
|
||
prop="effectiveStake"
|
||
:label="t('cashback.col.effective_stake')"
|
||
min-width="120"
|
||
align="right"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip :content="formatAmountFull(row.effectiveStake)" placement="top">
|
||
<span>{{ formatAmount(row.effectiveStake) }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="rate" :label="t('cashback.col.rate')" width="96" align="right">
|
||
<template #default="{ row }">{{ formatRate(row.rate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="amount" :label="t('cashback.col.amount')" min-width="120" align="right">
|
||
<template #default="{ row }">
|
||
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
|
||
<span class="amount-cell">{{ formatAmount(row.amount) }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<div v-if="preview.batch.status === 'PREVIEW'" class="preview-actions">
|
||
<el-button type="success" :disabled="previewItems.length === 0" @click="confirm">
|
||
{{ t('cashback.confirm_issue') }}
|
||
</el-button>
|
||
<el-button type="danger" plain @click="cancelBatchId(preview.batch.id)">
|
||
{{ t('cashback.cancel_issue') }}
|
||
</el-button>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
|
||
<el-card class="data-card history-card" shadow="never">
|
||
<div class="history-head">
|
||
<div class="table-title">{{ t('cashback.history_title') }}</div>
|
||
<el-select
|
||
v-model="historyStatus"
|
||
clearable
|
||
:placeholder="t('common.all')"
|
||
style="width: 140px"
|
||
@change="onHistoryStatusChange"
|
||
>
|
||
<el-option :label="t('cashback.status.PREVIEW')" value="PREVIEW" />
|
||
<el-option :label="t('cashback.status.CONFIRMED')" value="CONFIRMED" />
|
||
<el-option :label="t('cashback.status.CANCELLED')" value="CANCELLED" />
|
||
</el-select>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<el-table v-loading="historyLoading" :data="history" stripe>
|
||
<template #empty>
|
||
<AdminTableEmpty :text="t('cashback.history_empty')" />
|
||
</template>
|
||
<el-table-column prop="batchNo" :label="t('cashback.batch_no')" min-width="140" show-overflow-tooltip />
|
||
<el-table-column :label="t('cashback.col.period')" min-width="190">
|
||
<template #default="{ row }">{{ formatPeriodRange(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.col.status')" width="96" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="playerCount" :label="t('cashback.stat.players')" width="96" align="right" />
|
||
<el-table-column prop="totalBetCount" :label="t('cashback.col.bet_count')" width="88" align="right">
|
||
<template #default="{ row }">{{ row.totalBetCount ?? 0 }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.stat.effective_stake')" min-width="120" align="right">
|
||
<template #default="{ row }">{{ formatAmount(row.totalEffectiveStake ?? 0) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.stat.total')" min-width="110" align="right">
|
||
<template #default="{ row }">
|
||
<span class="amount-cell">{{ formatAmount(row.totalAmount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.col.created_at')" min-width="150">
|
||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.col.confirmed_at')" min-width="150">
|
||
<template #default="{ row }">{{ formatTime(row.confirmedAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('cashback.col.operator')" min-width="100" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.operatorUsername || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('common.actions')" width="200" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" @click="openDetail(row.id)">
|
||
{{ t('cashback.view_detail') }}
|
||
</el-button>
|
||
<template v-if="row.status === 'PREVIEW'">
|
||
<el-button link type="success" @click="confirmBatchId(row.id)">
|
||
{{ t('cashback.confirm_issue') }}
|
||
</el-button>
|
||
<el-button link type="danger" @click="cancelBatchId(row.id)">
|
||
{{ t('cashback.cancel_issue') }}
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div class="pager">
|
||
<el-pagination
|
||
v-model:current-page="historyPage"
|
||
v-model:page-size="historyPageSize"
|
||
:total="historyTotal"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
background
|
||
@current-change="onHistoryPageChange"
|
||
@size-change="onHistorySizeChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-dialog
|
||
v-model="detailVisible"
|
||
:title="t('cashback.detail_title')"
|
||
width="920px"
|
||
destroy-on-close
|
||
>
|
||
<div v-loading="detailLoading">
|
||
<template v-if="detail">
|
||
<div class="detail-meta">
|
||
<span>{{ t('cashback.batch_no') }}:{{ detail.batch.batchNo }}</span>
|
||
<span class="meta-sep">·</span>
|
||
<span>{{ formatPeriodRange(detail.batch) }}</span>
|
||
<el-tag :type="statusTagType(detail.batch.status)" size="small" class="detail-tag">
|
||
{{ statusLabel(detail.batch.status) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="detail-summary">{{ t('cashback.detail_summary') }}</div>
|
||
<el-row :gutter="12" class="preview-stats detail-stats">
|
||
<el-col :span="8">
|
||
<div class="pstat pstat-compact">
|
||
<div class="pstat-value">{{ detail.batch.playerCount ?? 0 }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.players') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="pstat pstat-compact">
|
||
<div class="pstat-value pstat-green">{{ formatAmount(detail.totalAmount) }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.total') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="pstat pstat-compact">
|
||
<div class="pstat-value">{{ detail.totalBetCount ?? 0 }}</div>
|
||
<div class="pstat-label">{{ t('cashback.stat.bet_count') }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<div class="table-wrap">
|
||
<el-table
|
||
:data="detailItems"
|
||
stripe
|
||
max-height="360"
|
||
show-summary
|
||
:summary-method="tableSummary"
|
||
>
|
||
<el-table-column type="index" :label="t('cashback.col.index')" width="56" align="center" />
|
||
<el-table-column prop="username" :label="t('cashback.col.player')" min-width="110" />
|
||
<el-table-column prop="agentUsername" :label="t('cashback.col.agent')" min-width="100">
|
||
<template #default="{ row }">{{ row.agentUsername || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="betCount" :label="t('cashback.col.bet_count')" width="88" align="right" />
|
||
<el-table-column prop="effectiveStake" :label="t('cashback.col.effective_stake')" min-width="110" align="right">
|
||
<template #default="{ row }">{{ formatAmount(row.effectiveStake) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="rate" :label="t('cashback.col.rate')" width="96" align="right">
|
||
<template #default="{ row }">{{ formatRate(row.rate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="amount" :label="t('cashback.col.amount')" min-width="110" align="right">
|
||
<template #default="{ row }">
|
||
<span class="amount-cell">{{ formatAmount(row.amount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div v-if="detail?.batch.status === 'PREVIEW'" class="detail-actions">
|
||
<el-button type="success" :disabled="detailItems.length === 0" @click="confirmBatchId(detail.batch.id)">
|
||
{{ t('cashback.confirm_issue') }}
|
||
</el-button>
|
||
<el-button type="danger" plain @click="cancelBatchId(detail.batch.id)">
|
||
{{ t('cashback.cancel_issue') }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.tool-card { margin-bottom: 16px; border-radius: 12px; }
|
||
|
||
.rules-help-btn {
|
||
margin-left: 10px;
|
||
width: 28px;
|
||
height: 28px;
|
||
padding: 0;
|
||
border-radius: 50%;
|
||
border: 1px solid #3a3a3a;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
color: #aaa;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
vertical-align: middle;
|
||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||
}
|
||
|
||
.rules-help-btn:hover {
|
||
color: var(--green-glow);
|
||
border-color: rgba(47, 181, 106, 0.45);
|
||
background: rgba(47, 181, 106, 0.08);
|
||
}
|
||
|
||
.rules-list {
|
||
margin: 0;
|
||
padding-left: 18px;
|
||
font-size: 13px;
|
||
line-height: 1.75;
|
||
color: #ccc;
|
||
}
|
||
|
||
.rules-note {
|
||
color: #888;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.preview-card,
|
||
.data-card {
|
||
border-radius: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.history-card {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.preview-head,
|
||
.history-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.preview-title,
|
||
.table-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.preview-meta,
|
||
.detail-meta {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
|
||
.detail-tag {
|
||
margin-left: 8px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.detail-summary {
|
||
margin: 12px 0 8px;
|
||
font-size: 13px;
|
||
color: #aaa;
|
||
}
|
||
|
||
.meta-sep {
|
||
margin: 0 6px;
|
||
}
|
||
|
||
.preview-stats {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.detail-stats {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pstat {
|
||
padding: 14px 12px;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid #2a2a2a;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pstat-compact {
|
||
padding: 12px 10px;
|
||
}
|
||
|
||
.pstat-value {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.pstat-green {
|
||
color: var(--green-glow);
|
||
text-shadow: 0 0 20px rgba(47, 181, 106, 0.35);
|
||
}
|
||
|
||
.pstat-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.amount-cell {
|
||
color: var(--green-glow);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.preview-actions,
|
||
.detail-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pager {
|
||
margin-top: 16px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
</style>
|