feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
121
apps/admin/src/components/RobotVerify.vue
Normal file
121
apps/admin/src/components/RobotVerify.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const input = ref('');
|
||||
const code = ref('');
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const honeypot = ref('');
|
||||
|
||||
function generateCode() {
|
||||
code.value = String(Math.floor(1000 + Math.random() * 9000));
|
||||
}
|
||||
|
||||
function drawCaptcha() {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const w = 108, 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 refresh() {
|
||||
generateCode();
|
||||
input.value = '';
|
||||
drawCaptcha();
|
||||
}
|
||||
|
||||
function validate(): boolean {
|
||||
if (honeypot.value) { refresh(); return false; }
|
||||
return input.value.trim() === code.value;
|
||||
}
|
||||
|
||||
onMounted(refresh);
|
||||
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="Captcha" autocomplete="off" />
|
||||
<canvas ref="canvasRef" class="captcha-canvas"
|
||||
title="点击刷新" role="button" tabindex="0"
|
||||
@click="refresh" @keydown.enter="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
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: 1px solid #333;
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
background: #0d0d0d;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.captcha-input::placeholder { color: #555; }
|
||||
.captcha-input:focus { border-color: rgba(0, 196, 65, 0.6); }
|
||||
|
||||
.captcha-canvas {
|
||||
flex-shrink: 0;
|
||||
width: 108px;
|
||||
height: 44px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
</style>
|
||||
44
apps/admin/src/components/dashboard/EChartPanel.vue
Normal file
44
apps/admin/src/components/dashboard/EChartPanel.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import VChart from 'vue-echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import './echarts-setup';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
option: EChartsOption;
|
||||
height?: string;
|
||||
}>(),
|
||||
{ title: '', height: '300px' },
|
||||
);
|
||||
|
||||
const style = computed(() => ({ height: props.height, width: '100%' }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chart-panel">
|
||||
<div v-if="title" class="chart-title">{{ title }}</div>
|
||||
<v-chart class="chart-canvas" :option="option" :style="style" autoresize />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chart-panel {
|
||||
padding: 16px 18px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #1e1e1e;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.chart-canvas {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
22
apps/admin/src/components/dashboard/echarts-setup.ts
Normal file
22
apps/admin/src/components/dashboard/echarts-setup.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { use } from 'echarts/core';
|
||||
import { BarChart, LineChart, PieChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
GraphicComponent,
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
GraphicComponent,
|
||||
]);
|
||||
Reference in New Issue
Block a user