refactor(admin): 合并管理后台并移除 apps/agent
- 平台与代理共用 apps/admin,统一登录 manage/auth/login - 按 userType 展示菜单,修复 token 循环跳转 - 删除独立 apps/agent 前端工程 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
30
apps/admin/README.md
Normal file
30
apps/admin/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 统一管理后台(平台 + 代理)
|
||||
|
||||
单一 Vue 3 应用,按登录账号 `userType` 展示不同菜单:
|
||||
|
||||
| 类型 | 演示账号 | 菜单 |
|
||||
|------|----------|------|
|
||||
| 平台管理员 `ADMIN` | admin / Admin@123 | 控制台、玩家、代理、赛事、注单、返水、审计 |
|
||||
| 代理 `AGENT` | agent1 / Agent@123 | 概览、直属玩家、下级代理、注单 |
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
pnpm dev:api # 需先启动 API :3000
|
||||
pnpm dev:admin # http://localhost:5174
|
||||
```
|
||||
|
||||
登录接口:`POST /api/manage/auth/login`
|
||||
|
||||
## 源码结构
|
||||
|
||||
```
|
||||
src/
|
||||
layouts/ManageLayout.vue # 共用布局
|
||||
views/ # 平台端页面
|
||||
views/agent/ # 代理端页面
|
||||
stores/auth.ts # 统一会话
|
||||
router/index.ts # 路由 + 权限守卫
|
||||
```
|
||||
|
||||
后端仍为两套 API 前缀:`/api/admin/*`、`/api/agent/*`(按角色调用)。
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<title>TheBet365 Admin</title>
|
||||
<title>TheBet365 管理后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@thebet365/admin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "统一管理后台(平台管理员 + 代理,单入口登录)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5174",
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import axios from 'axios';
|
||||
import router from './router';
|
||||
import { clearStaffSession } from './stores/auth';
|
||||
|
||||
const api = axios.create({ baseURL: '/api' });
|
||||
api.interceptors.request.use((c) => {
|
||||
const t = localStorage.getItem('admin_token');
|
||||
if (t) c.headers.Authorization = `Bearer ${t}`;
|
||||
return c;
|
||||
|
||||
let handling401 = false;
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const t = localStorage.getItem('manage_token');
|
||||
if (t) config.headers.Authorization = `Bearer ${t}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (err) => {
|
||||
if (err.response?.status === 401 && !handling401) {
|
||||
handling401 = true;
|
||||
clearStaffSession();
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
await router.replace('/login');
|
||||
}
|
||||
handling401 = false;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
},
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
BIN
apps/admin/src/assets/images/bg.png
Normal file
BIN
apps/admin/src/assets/images/bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 MiB |
@@ -1,15 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('admin_token');
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
const menus = [
|
||||
const adminMenus = [
|
||||
{ path: '/', label: '控制台' },
|
||||
{ path: '/users', label: '玩家管理' },
|
||||
{ path: '/agents', label: '代理管理' },
|
||||
@@ -18,6 +16,20 @@ const menus = [
|
||||
{ path: '/cashback', label: '返水管理' },
|
||||
{ path: '/audit', label: '操作日志' },
|
||||
];
|
||||
|
||||
const agentMenus = [
|
||||
{ path: '/', label: '概览' },
|
||||
{ path: '/my-players', label: '直属玩家' },
|
||||
{ path: '/sub-agents', label: '下级代理' },
|
||||
{ path: '/my-bets', label: '注单查询' },
|
||||
];
|
||||
|
||||
const menus = computed(() => (auth.isAdmin.value ? adminMenus : agentMenus));
|
||||
|
||||
function logout() {
|
||||
auth.logout();
|
||||
router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,16 +37,26 @@ const menus = [
|
||||
<el-aside width="200px" style="background: #1a2332">
|
||||
<div style="padding: 20px">
|
||||
<img src="/logo.png" alt="TheBet365" style="height: 56px; width: auto; display: block" />
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888">平台后台</div>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888">{{ auth.portalLabel }}</div>
|
||||
<div v-if="auth.user" style="margin-top: 6px; font-size: 12px; color: #aaa">
|
||||
{{ auth.user.username }}
|
||||
</div>
|
||||
</div>
|
||||
<el-menu background-color="#1a2332" text-color="#ccc" active-text-color="#00a826" :default-active="route.path">
|
||||
<el-menu
|
||||
background-color="#1a2332"
|
||||
text-color="#ccc"
|
||||
active-text-color="#00a826"
|
||||
:default-active="route.path"
|
||||
>
|
||||
<el-menu-item v-for="m in menus" :key="m.path" :index="m.path">
|
||||
<RouterLink :to="m.path" style="color: inherit; width: 100%">{{ m.label }}</RouterLink>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header style="display: flex; justify-content: flex-end; align-items: center; border-bottom: 1px solid #eee">
|
||||
<el-header
|
||||
style="display: flex; justify-content: flex-end; align-items: center; border-bottom: 1px solid #eee"
|
||||
>
|
||||
<el-button @click="logout">退出</el-button>
|
||||
</el-header>
|
||||
<el-main><RouterView /></el-main>
|
||||
@@ -1,23 +1,95 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
export default createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/login', component: () => import('../views/Login.vue') },
|
||||
{ path: '/login', component: () => import('../views/Login.vue'), meta: { public: true } },
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../layouts/AdminLayout.vue'),
|
||||
component: () => import('../layouts/ManageLayout.vue'),
|
||||
meta: { auth: true },
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/Dashboard.vue') },
|
||||
{ path: 'users', component: () => import('../views/Users.vue') },
|
||||
{ path: 'agents', component: () => import('../views/Agents.vue') },
|
||||
{ path: 'matches', component: () => import('../views/Matches.vue') },
|
||||
{ path: 'bets', component: () => import('../views/Bets.vue') },
|
||||
{ path: 'settlement/:id', component: () => import('../views/Settlement.vue') },
|
||||
{ path: 'cashback', component: () => import('../views/Cashback.vue') },
|
||||
{ path: 'audit', component: () => import('../views/Audit.vue') },
|
||||
{ path: '', component: () => import('../views/HomeEntry.vue') },
|
||||
{
|
||||
path: 'users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
component: () => import('../views/Agents.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'matches',
|
||||
component: () => import('../views/Matches.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'bets',
|
||||
component: () => import('../views/Bets.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'settlement/:id',
|
||||
component: () => import('../views/Settlement.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'cashback',
|
||||
component: () => import('../views/Cashback.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'audit',
|
||||
component: () => import('../views/Audit.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'my-players',
|
||||
component: () => import('../views/agent/Players.vue'),
|
||||
meta: { agentOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'sub-agents',
|
||||
component: () => import('../views/agent/SubAgents.vue'),
|
||||
meta: { agentOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'my-bets',
|
||||
component: () => import('../views/agent/Bets.vue'),
|
||||
meta: { agentOnly: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const auth = useAuthStore();
|
||||
const hasToken = !!auth.token.value;
|
||||
const hasUser = !!auth.user.value?.userType;
|
||||
|
||||
if (to.meta.public) {
|
||||
if (hasToken && hasUser) return '/';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasToken || !hasUser) {
|
||||
auth.clearStaffSession();
|
||||
return { path: '/login', query: { redirect: to.fullPath } };
|
||||
}
|
||||
|
||||
if (to.meta.adminOnly && !auth.isAdmin.value) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (to.meta.agentOnly && !auth.isAgent.value) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
84
apps/admin/src/stores/auth.ts
Normal file
84
apps/admin/src/stores/auth.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export type StaffUserType = 'ADMIN' | 'AGENT';
|
||||
|
||||
export interface StaffUser {
|
||||
id: string;
|
||||
username: string;
|
||||
userType: StaffUserType;
|
||||
locale?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'manage_token';
|
||||
const USER_KEY = 'manage_user';
|
||||
|
||||
function loadUser(): StaffUser | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(USER_KEY);
|
||||
return raw ? (JSON.parse(raw) as StaffUser) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLegacyTokens() {
|
||||
if (localStorage.getItem(TOKEN_KEY)) return;
|
||||
const legacyAdmin = localStorage.getItem('admin_token');
|
||||
const legacyAgent = localStorage.getItem('agent_token');
|
||||
if (legacyAdmin) {
|
||||
localStorage.setItem(TOKEN_KEY, legacyAdmin);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify({ userType: 'ADMIN' }));
|
||||
localStorage.removeItem('admin_token');
|
||||
return;
|
||||
}
|
||||
if (legacyAgent) {
|
||||
localStorage.setItem(TOKEN_KEY, legacyAgent);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify({ userType: 'AGENT' }));
|
||||
localStorage.removeItem('agent_token');
|
||||
}
|
||||
}
|
||||
|
||||
migrateLegacyTokens();
|
||||
|
||||
const token = ref(localStorage.getItem(TOKEN_KEY) || '');
|
||||
const user = ref<StaffUser | null>(loadUser());
|
||||
|
||||
export function clearStaffSession() {
|
||||
token.value = '';
|
||||
user.value = null;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('agent_token');
|
||||
}
|
||||
|
||||
export function useAuthStore() {
|
||||
const isAdmin = computed(() => user.value?.userType === 'ADMIN');
|
||||
const isAgent = computed(() => user.value?.userType === 'AGENT');
|
||||
const portalLabel = computed(() => (isAdmin.value ? '平台后台' : '代理后台'));
|
||||
|
||||
function setSession(newToken: string, newUser: StaffUser) {
|
||||
token.value = newToken;
|
||||
user.value = newUser;
|
||||
localStorage.setItem(TOKEN_KEY, newToken);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(newUser));
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('agent_token');
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearStaffSession();
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isAdmin,
|
||||
isAgent,
|
||||
portalLabel,
|
||||
setSession,
|
||||
logout,
|
||||
clearStaffSession,
|
||||
};
|
||||
}
|
||||
12
apps/admin/src/views/HomeEntry.vue
Normal file
12
apps/admin/src/views/HomeEntry.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import AdminDashboard from './Dashboard.vue';
|
||||
import AgentDashboard from './agent/Dashboard.vue';
|
||||
|
||||
const auth = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminDashboard v-if="auth.isAdmin" />
|
||||
<AgentDashboard v-else />
|
||||
</template>
|
||||
@@ -1,21 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAuthStore, type StaffUser } from '../stores/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({ username: 'admin', password: 'Admin@123' });
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const form = ref({ username: '', password: '' });
|
||||
const loading = ref(false);
|
||||
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/auth/login', form.value);
|
||||
localStorage.setItem('admin_token', data.data.token);
|
||||
router.push('/');
|
||||
const { data } = await api.post('/manage/auth/login', form.value);
|
||||
const payload = data.data as { token: string; user: StaffUser };
|
||||
auth.setSession(payload.token, payload.user);
|
||||
const redirect = (route.query.redirect as string) || '/';
|
||||
router.push(redirect);
|
||||
} catch {
|
||||
ElMessage.error('登录失败');
|
||||
ElMessage.error('登录失败,请检查账号与密码');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -27,17 +33,24 @@ async function login() {
|
||||
<el-card style="width: 400px">
|
||||
<div style="text-align: center; margin-bottom: 24px">
|
||||
<img src="/logo.png" alt="TheBet365" style="height: 64px; width: auto" />
|
||||
<h2 style="margin-top: 12px; font-size: 16px; font-weight: 500">平台后台登录</h2>
|
||||
<h2 style="margin-top: 12px; font-size: 16px; font-weight: 500">管理后台登录</h2>
|
||||
<p style="margin-top: 8px; font-size: 12px; color: #888">
|
||||
管理员与代理使用同一入口,系统将根据账号类型进入对应后台
|
||||
</p>
|
||||
</div>
|
||||
<el-form @submit.prevent="login">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" />
|
||||
<el-input v-model="form.username" autocomplete="username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" />
|
||||
<el-input v-model="form.password" type="password" autocomplete="current-password" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" native-type="submit" :loading="loading" style="width: 100%">登录</el-button>
|
||||
</el-form>
|
||||
<p style="margin-top: 16px; font-size: 12px; color: #999; line-height: 1.6">
|
||||
演示账号:admin / Admin@123(平台)<br />
|
||||
agent1 / Agent@123(一级代理)
|
||||
</p>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
25
apps/admin/src/views/agent/Bets.vue
Normal file
25
apps/admin/src/views/agent/Bets.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
35
apps/admin/src/views/agent/Dashboard.vue
Normal file
35
apps/admin/src/views/agent/Dashboard.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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>
|
||||
68
apps/admin/src/views/agent/Players.vue
Normal file
68
apps/admin/src/views/agent/Players.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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>
|
||||
39
apps/admin/src/views/agent/SubAgents.vue
Normal file
39
apps/admin/src/views/agent/SubAgents.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user