新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分), 支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏, 并补充前台注册、充值页及多语言错误码。 Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
6.8 KiB
Vue
248 lines
6.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { ElMessage } from 'element-plus';
|
|
import api from '../api';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
import { buildPlayerRegisterUrl, copyText } from '../utils/invite-link';
|
|
import { useAuthStore } from '../stores/auth';
|
|
import RatePercentInput from './RatePercentInput.vue';
|
|
import { decimalRateToPercent, formatRatePercent, percentToDecimalRate } from '../utils/rate-percent';
|
|
|
|
const props = defineProps<{
|
|
inviteCode?: string | null;
|
|
compact?: boolean;
|
|
readonly?: boolean;
|
|
embedded?: boolean;
|
|
hideHistory?: boolean;
|
|
/** When true, code/link only appear after clicking generate (not from session/props). */
|
|
requireGenerate?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
generated: [];
|
|
}>();
|
|
|
|
const { t } = useAdminLocale();
|
|
const auth = useAuthStore();
|
|
const loadedCode = ref<string | null>(null);
|
|
const loadedCashbackRate = ref<string | null>(null);
|
|
const generating = ref(false);
|
|
const cashbackPercent = ref(0);
|
|
const defaultCashbackLoaded = ref(false);
|
|
|
|
const isAdmin = computed(() => auth.isAdmin.value);
|
|
const showCashbackField = computed(() => isAdmin.value && !props.readonly);
|
|
|
|
const effectiveCode = computed(() => {
|
|
if (props.readonly) return props.inviteCode ?? null;
|
|
if (props.requireGenerate) return loadedCode.value;
|
|
return props.inviteCode ?? loadedCode.value;
|
|
});
|
|
|
|
const registerUrl = computed(() =>
|
|
effectiveCode.value ? buildPlayerRegisterUrl(effectiveCode.value) : '',
|
|
);
|
|
|
|
const displayedCashbackRate = computed(() => {
|
|
if (loadedCashbackRate.value != null) return loadedCashbackRate.value;
|
|
if (showCashbackField.value) return percentToDecimalRate(cashbackPercent.value).toString();
|
|
return null;
|
|
});
|
|
|
|
async function loadDefaultCashbackRate() {
|
|
if (!showCashbackField.value) return;
|
|
try {
|
|
const { data } = await api.get('/admin/settings/cashback/platform-direct');
|
|
const payload = data.data as { adminInviteRate?: number | string; platformDirectRate?: number | string };
|
|
cashbackPercent.value = decimalRateToPercent(
|
|
payload?.adminInviteRate ?? payload?.platformDirectRate ?? 0,
|
|
);
|
|
} catch {
|
|
cashbackPercent.value = 0;
|
|
} finally {
|
|
defaultCashbackLoaded.value = true;
|
|
}
|
|
}
|
|
|
|
async function generateInvite() {
|
|
generating.value = true;
|
|
try {
|
|
const body = showCashbackField.value
|
|
? { cashbackRate: percentToDecimalRate(cashbackPercent.value) }
|
|
: undefined;
|
|
const { data } = await api.post('/manage/invite/generate', body);
|
|
const payload = data.data as { inviteCode?: string | null; cashbackRate?: string | null };
|
|
const code = payload.inviteCode ?? null;
|
|
loadedCode.value = code;
|
|
loadedCashbackRate.value = payload.cashbackRate ?? null;
|
|
if (auth.user.value && code) {
|
|
auth.setSession(auth.token.value, { ...auth.user.value, inviteCode: code });
|
|
}
|
|
ElMessage.success(t('invite.generate_ok'));
|
|
emit('generated');
|
|
} catch {
|
|
ElMessage.error(t('invite.generate_failed'));
|
|
} finally {
|
|
generating.value = false;
|
|
}
|
|
}
|
|
|
|
async function copyCode() {
|
|
if (!effectiveCode.value) return;
|
|
const ok = await copyText(effectiveCode.value);
|
|
ElMessage[ok ? 'success' : 'error'](ok ? t('invite.copy_code_ok') : t('invite.copy_failed'));
|
|
}
|
|
|
|
async function copyLink() {
|
|
if (!registerUrl.value) return;
|
|
const ok = await copyText(registerUrl.value);
|
|
ElMessage[ok ? 'success' : 'error'](ok ? t('invite.copy_link_ok') : t('invite.copy_failed'));
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadDefaultCashbackRate();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="invite-panel" :class="{ compact, embedded }">
|
|
<div v-if="!embedded" class="invite-head">
|
|
<span class="invite-title">{{ t('invite.title') }}</span>
|
|
<span class="invite-hint">{{ t('invite.hint') }}</span>
|
|
</div>
|
|
<div v-if="showCashbackField && defaultCashbackLoaded" class="invite-cashback-field">
|
|
<label class="invite-label">{{ t('invite.cashback_rate') }}</label>
|
|
<RatePercentInput v-model="cashbackPercent" />
|
|
<p class="invite-cashback-hint">{{ t('invite.cashback_rate_hint') }}</p>
|
|
</div>
|
|
<div v-if="!readonly" class="invite-actions">
|
|
<el-button type="primary" :loading="generating" @click="generateInvite">
|
|
{{ effectiveCode ? t('invite.regenerate_btn') : t('invite.generate_btn') }}
|
|
</el-button>
|
|
</div>
|
|
<div v-if="effectiveCode" class="invite-body">
|
|
<div v-if="displayedCashbackRate != null" class="invite-row">
|
|
<span class="invite-label">{{ t('invite.cashback_rate') }}</span>
|
|
<span class="invite-rate">{{ formatRatePercent(displayedCashbackRate) }}</span>
|
|
</div>
|
|
<div class="invite-row">
|
|
<span class="invite-label">{{ t('invite.code') }}</span>
|
|
<code class="invite-code">{{ effectiveCode }}</code>
|
|
<el-button size="small" @click="copyCode">{{ t('invite.copy_code') }}</el-button>
|
|
</div>
|
|
<div class="invite-row">
|
|
<span class="invite-label">{{ t('invite.link') }}</span>
|
|
<span class="invite-link">{{ registerUrl }}</span>
|
|
<el-button type="primary" size="small" @click="copyLink">{{ t('invite.copy_link') }}</el-button>
|
|
</div>
|
|
</div>
|
|
<p v-else-if="readonly" class="invite-empty">{{ t('invite.not_generated') }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.invite-panel {
|
|
border: 1px solid var(--el-border-color-light);
|
|
border-radius: 8px;
|
|
padding: 14px 16px;
|
|
background: var(--el-fill-color-blank);
|
|
}
|
|
|
|
.invite-panel.compact {
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.invite-panel.embedded {
|
|
border: none;
|
|
padding: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.invite-head {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.invite-title {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
|
|
.invite-hint {
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.invite-cashback-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
margin-bottom: 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
.invite-cashback-hint {
|
|
margin: 0;
|
|
font-size: 11px;
|
|
color: var(--el-text-color-secondary);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.invite-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.invite-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.invite-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.invite-label {
|
|
min-width: 72px;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.invite-rate {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--el-color-success);
|
|
}
|
|
|
|
.invite-code {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
color: var(--el-color-primary);
|
|
}
|
|
|
|
.invite-link {
|
|
flex: 1;
|
|
min-width: 180px;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-regular);
|
|
word-break: break-all;
|
|
}
|
|
|
|
.invite-empty {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
</style>
|