feat: 手动充值、邀请码注册与后台管理增强

新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分),
支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏,
并补充前台注册、充值页及多语言错误码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:20:11 +08:00
parent 618fb49511
commit 10485ecfaf
98 changed files with 7908 additions and 856 deletions

View File

@@ -1,150 +1,255 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import SlideVerify, { type SlideVerifyInstance } from 'vue3-slide-verify';
import 'vue3-slide-verify/dist/style.css';
const { t } = useI18n();
const input = ref('');
const code = ref('');
const canvasRef = ref<HTMLCanvasElement | null>(null);
const honeypot = ref('');
const slideRef = ref<SlideVerifyInstance>();
const validated = ref(false);
const showPopup = ref(false);
const errorMsg = ref('');
function generateCode() {
code.value = String(Math.floor(1000 + Math.random() * 9000));
function openPopup() {
if (validated.value) return;
showPopup.value = true;
errorMsg.value = '';
// Refresh captcha after popup renders
nextTick(() => {
slideRef.value?.refresh();
});
}
function drawCaptcha() {
const canvas = canvasRef.value;
if (!canvas) return;
const w = 108;
const h = 44;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.fillStyle = '#7c3aed';
ctx.fillRect(0, 0, w, h);
for (let i = 0; i < 28; i++) {
ctx.fillStyle = `rgba(255,255,255,${0.15 + Math.random() * 0.35})`;
ctx.beginPath();
ctx.arc(Math.random() * w, Math.random() * h, Math.random() * 2.2, 0, Math.PI * 2);
ctx.fill();
}
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = `rgba(255,255,255,${0.2 + Math.random() * 0.3})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(Math.random() * w, Math.random() * h);
ctx.lineTo(Math.random() * w, Math.random() * h);
ctx.stroke();
}
ctx.save();
ctx.translate(w / 2, h / 2);
ctx.rotate((Math.random() - 0.5) * 0.12);
ctx.font = 'italic bold 26px Arial, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(code.value, 0, 1);
ctx.restore();
function closePopup() {
showPopup.value = false;
}
function refresh() {
generateCode();
input.value = '';
drawCaptcha();
function onSuccess() {
validated.value = true;
errorMsg.value = '';
showPopup.value = false;
}
function onFail() {
validated.value = false;
}
function onAgain() {
validated.value = false;
slideRef.value?.refresh();
}
function validate(): boolean {
if (honeypot.value) {
refresh();
return false;
if (!validated.value) {
errorMsg.value = t('auth.captcha_wrong');
}
return input.value.trim() === code.value;
return validated.value;
}
onMounted(refresh);
function refresh() {
validated.value = false;
errorMsg.value = '';
}
defineExpose({ validate, refresh });
</script>
<template>
<div class="captcha-row">
<input
v-model="honeypot"
type="text"
name="website"
tabindex="-1"
autocomplete="off"
class="hp-field"
aria-hidden="true"
/>
<input
v-model="input"
type="text"
inputmode="numeric"
maxlength="4"
class="captcha-input"
:placeholder="t('auth.captcha_placeholder')"
autocomplete="off"
/>
<canvas
ref="canvasRef"
class="captcha-canvas"
:title="t('auth.captcha_refresh')"
role="button"
tabindex="0"
@click="refresh"
@keydown.enter="refresh"
/>
<div class="captcha-trigger-wrapper">
<!-- Trigger row -->
<div class="captcha-trigger" :class="{ verified: validated }" @click="openPopup">
<span v-if="validated" class="captcha-success-icon"></span>
<span class="captcha-trigger-text">
{{ validated ? t('auth.verified') : t('auth.click_to_verify') }}
</span>
</div>
<p v-if="errorMsg && !validated" class="slide-error">{{ errorMsg }}</p>
<!-- Popup overlay -->
<Teleport to="body">
<Transition name="fade">
<div v-if="showPopup" class="captcha-overlay" @click.self="closePopup">
<div class="captcha-popup">
<div class="captcha-popup-header">
<span>{{ t('auth.slide_to_verify') }}</span>
<button class="captcha-popup-close" @click="closePopup"></button>
</div>
<div class="captcha-popup-body">
<SlideVerify
ref="slideRef"
:w="300"
:h="150"
:l="42"
:r="9"
:accuracy="3"
:slider-text="t('auth.slide_to_verify')"
@success="onSuccess"
@fail="onFail"
@again="onAgain"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<style scoped>
.captcha-row {
.captcha-trigger-wrapper {
width: 100%;
}
.captcha-trigger {
display: flex;
gap: 0;
align-items: stretch;
height: 44px;
}
.hp-field {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.captcha-input {
flex: 1;
min-width: 0;
padding: 0 14px;
border: none;
border-radius: 8px 0 0 8px;
background: #ffffff;
color: #111;
font-size: 15px;
font-weight: 500;
outline: none;
}
.captcha-input::placeholder {
color: #9ca3af;
}
.captcha-canvas {
flex-shrink: 0;
width: 108px;
align-items: center;
gap: 8px;
height: 44px;
padding: 0 16px;
border-radius: 8px;
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: block;
border-radius: 0 8px 8px 0;
transition: border-color 0.2s;
user-select: none;
}
.captcha-trigger:active {
border-color: var(--primary, #c8a84e);
}
.captcha-trigger.verified {
border-color: rgba(34, 197, 94, 0.5);
background: rgba(34, 197, 94, 0.08);
cursor: default;
}
.captcha-success-icon {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.captcha-trigger-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
}
.captcha-trigger.verified .captcha-trigger-text {
color: #22c55e;
}
/* Popup */
.captcha-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.captcha-popup {
background: #1e1e1e;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
max-width: 340px;
width: calc(100vw - 40px);
}
.captcha-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.captcha-popup-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.4);
font-size: 16px;
cursor: pointer;
padding: 4px;
line-height: 1;
}
.captcha-popup-close:active {
color: rgba(255, 255, 255, 0.8);
}
.captcha-popup-body {
padding: 16px;
display: flex;
justify-content: center;
}
.captcha-popup-body :deep(.slide-verify) {
width: 300px !important;
}
.captcha-popup-body :deep(.slide-verify-slider) {
width: 300px !important;
}
.captcha-popup-body :deep(.slide-verify-info) {
background-color: #2a2a2a;
color: rgba(255, 255, 255, 0.6);
}
.captcha-popup-body :deep(.slide-verify-slider .slide-verify-slider__text) {
background-color: #2a2a2a;
color: rgba(255, 255, 255, 0.5);
font-size: 13px;
}
.captcha-popup-body :deep(.slide-verify-slider .slide-verify-slider__handler) {
background-color: var(--primary, #c8a84e);
}
.captcha-popup-body :deep(.slide-verify-slider__bg-fill) {
background-color: rgba(200, 168, 78, 0.25);
}
.captcha-popup-body :deep(.slide-verify-slider__icon--success) {
background-color: rgba(34, 197, 94, 0.2);
}
.captcha-popup-body :deep(.slide-verify-slider__icon--fail) {
background-color: rgba(239, 68, 68, 0.2);
}
/* Transition */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-error {
color: var(--danger);
font-size: 12px;
font-weight: 600;
margin: 4px 0 0;
}
</style>