初始化足球投注平台 MVP Monorepo
包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
5
apps/agent/index.html
Normal file
5
apps/agent/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8" /><title>TheBet365 Agent</title></head>
|
||||
<body><div id="app"></div><script type="module" src="/src/main.ts"></script></body>
|
||||
</html>
|
||||
23
apps/agent/package.json
Normal file
23
apps/agent/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@thebet365/agent",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5175",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.9.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
1
apps/agent/src/App.vue
Normal file
1
apps/agent/src/App.vue
Normal file
@@ -0,0 +1 @@
|
||||
<script setup lang="ts"><template><router-view /></template></script>
|
||||
9
apps/agent/src/api.ts
Normal file
9
apps/agent/src/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({ baseURL: '/api' });
|
||||
api.interceptors.request.use((c) => {
|
||||
const t = localStorage.getItem('agent_token');
|
||||
if (t) c.headers.Authorization = `Bearer ${t}`;
|
||||
return c;
|
||||
});
|
||||
export default api;
|
||||
29
apps/agent/src/layouts/AgentLayout.vue
Normal file
29
apps/agent/src/layouts/AgentLayout.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView, RouterLink, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
function logout() {
|
||||
localStorage.removeItem('agent_token');
|
||||
router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container style="min-height: 100vh">
|
||||
<el-aside width="180px" style="background: #1a2332">
|
||||
<div style="padding: 16px; color: #00a826; font-weight: 700">代理后台</div>
|
||||
<el-menu background-color="#1a2332" text-color="#ccc" active-text-color="#00a826">
|
||||
<el-menu-item index="/"><RouterLink to="/" style="color: inherit">概览</RouterLink></el-menu-item>
|
||||
<el-menu-item index="/players"><RouterLink to="/players" style="color: inherit">直属玩家</RouterLink></el-menu-item>
|
||||
<el-menu-item index="/agents"><RouterLink to="/agents" style="color: inherit">下级代理</RouterLink></el-menu-item>
|
||||
<el-menu-item index="/bets"><RouterLink to="/bets" style="color: inherit">注单查询</RouterLink></el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header style="text-align: right; line-height: 60px">
|
||||
<el-button @click="logout">退出</el-button>
|
||||
</el-header>
|
||||
<el-main><RouterView /></el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
7
apps/agent/src/main.ts
Normal file
7
apps/agent/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
createApp(App).use(router).use(ElementPlus).mount('#app');
|
||||
18
apps/agent/src/router/index.ts
Normal file
18
apps/agent/src/router/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/login', component: () => import('./views/Login.vue') },
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('./layouts/AgentLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('./views/Dashboard.vue') },
|
||||
{ path: 'players', component: () => import('./views/Players.vue') },
|
||||
{ path: 'agents', component: () => import('./views/SubAgents.vue') },
|
||||
{ path: 'bets', component: () => import('./views/Bets.vue') },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
23
apps/agent/src/views/Bets.vue
Normal file
23
apps/agent/src/views/Bets.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
|
||||
const bets = ref<unknown[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get('/agent/bets');
|
||||
bets.value = data.data.items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>下级注单</h2>
|
||||
<el-table :data="bets">
|
||||
<el-table-column prop="betNo" label="注单号" />
|
||||
<el-table-column label="玩家">
|
||||
<template #default="{ row }">{{ (row as { user?: { username: string } }).user?.username }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stake" label="投注额" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
</el-table>
|
||||
</template>
|
||||
21
apps/agent/src/views/Dashboard.vue
Normal file
21
apps/agent/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
|
||||
const summary = ref<Record<string, unknown>>({});
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get('/agent/reports/summary');
|
||||
summary.value = data.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>代理概览</h2>
|
||||
<el-row :gutter="16" style="margin-top: 16px">
|
||||
<el-col :span="6"><el-statistic title="授信额度" :value="Number((summary.profile as { creditLimit?: string })?.creditLimit) || 0" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="已用额度" :value="Number((summary.profile as { usedCredit?: string })?.usedCredit) || 0" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="直属玩家" :value="(summary.directPlayerCount as number) || 0" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="今日投注" :value="Number(summary.todayStake) || 0" :precision="2" /></el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
32
apps/agent/src/views/Login.vue
Normal file
32
apps/agent/src/views/Login.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({ username: 'agent1', password: 'Agent@123' });
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
const { data } = await api.post('/agent/auth/login', form.value);
|
||||
localStorage.setItem('agent_token', data.data.token);
|
||||
router.push('/');
|
||||
} catch {
|
||||
ElMessage.error('登录失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center">
|
||||
<el-card style="width: 360px">
|
||||
<h2 style="text-align: center">代理后台登录</h2>
|
||||
<el-form @submit.prevent="login" style="margin-top: 20px">
|
||||
<el-input v-model="form.username" placeholder="用户名" style="margin-bottom: 12px" />
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" style="margin-bottom: 12px" />
|
||||
<el-button type="primary" native-type="submit" style="width: 100%">登录</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
66
apps/agent/src/views/Players.vue
Normal file
66
apps/agent/src/views/Players.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const players = ref<unknown[]>([]);
|
||||
const form = ref({ username: '', password: 'Player@123' });
|
||||
const depositForm = ref({ playerId: '', amount: 100, requestId: '' });
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
const { data } = await api.get('/agent/players');
|
||||
players.value = data.data;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
await api.post('/agent/players', form.value);
|
||||
ElMessage.success('玩家已创建');
|
||||
load();
|
||||
}
|
||||
|
||||
async function deposit() {
|
||||
depositForm.value.requestId = `dep-${Date.now()}`;
|
||||
await api.post(`/agent/players/${depositForm.value.playerId}/deposit`, {
|
||||
amount: depositForm.value.amount,
|
||||
requestId: depositForm.value.requestId,
|
||||
});
|
||||
ElMessage.success('上分成功');
|
||||
load();
|
||||
}
|
||||
|
||||
async function withdraw(playerId: string, amount: number) {
|
||||
await api.post(`/agent/players/${playerId}/withdraw`, {
|
||||
amount,
|
||||
requestId: `wd-${Date.now()}`,
|
||||
});
|
||||
ElMessage.success('下分成功');
|
||||
load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>直属玩家</h2>
|
||||
<el-form inline style="margin-bottom: 16px">
|
||||
<el-input v-model="form.username" placeholder="用户名" />
|
||||
<el-button type="primary" @click="create">创建玩家</el-button>
|
||||
</el-form>
|
||||
<el-form inline style="margin-bottom: 16px">
|
||||
<el-input v-model="depositForm.playerId" placeholder="玩家ID" style="width: 100px" />
|
||||
<el-input-number v-model="depositForm.amount" :min="1" />
|
||||
<el-button type="success" @click="deposit">上分</el-button>
|
||||
</el-form>
|
||||
<el-table :data="players">
|
||||
<el-table-column prop="id" label="ID" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column label="余额">
|
||||
<template #default="{ row }">{{ (row as { wallet?: { availableBalance: string } }).wallet?.availableBalance }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="withdraw((row as { id: string }).id, 50)">下分50</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
37
apps/agent/src/views/SubAgents.vue
Normal file
37
apps/agent/src/views/SubAgents.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const agents = ref<unknown[]>([]);
|
||||
const form = ref({ username: '', password: 'Agent@123', creditLimit: 10000 });
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
const { data } = await api.get('/agent/agents');
|
||||
agents.value = data.data;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
await api.post('/agent/agents', form.value);
|
||||
ElMessage.success('下级代理已创建');
|
||||
load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>下级代理(仅一级代理可见)</h2>
|
||||
<el-form inline style="margin-bottom: 16px">
|
||||
<el-input v-model="form.username" placeholder="用户名" />
|
||||
<el-input-number v-model="form.creditLimit" />
|
||||
<el-button type="primary" @click="create">创建二级代理</el-button>
|
||||
</el-form>
|
||||
<el-table :data="agents">
|
||||
<el-table-column label="用户名">
|
||||
<template #default="{ row }">{{ (row as { user?: { username: string } }).user?.username }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creditLimit" label="额度" />
|
||||
<el-table-column prop="usedCredit" label="已用" />
|
||||
</el-table>
|
||||
</template>
|
||||
5
apps/agent/src/vite-env.d.ts
vendored
Normal file
5
apps/agent/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<object, object, unknown>;
|
||||
export default component;
|
||||
}
|
||||
1
apps/agent/tsconfig.json
Normal file
1
apps/agent/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true }, "include": ["src/**/*.ts", "src/**/*.vue"] }
|
||||
10
apps/agent/vite.config.ts
Normal file
10
apps/agent/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5175,
|
||||
proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } },
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user