This commit is contained in:
2025-12-15 17:55:32 +08:00
commit 28ab17324d
170 changed files with 18373 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<template>
<header style="display: flex; gap: 12px; padding: 12px 16px; border-bottom: 1px solid #eee">
<strong>Super Admin</strong>
<router-link to="/">统计</router-link>
<router-link to="/tenants">租户</router-link>
<router-link to="/roles">角色</router-link>
<span style="flex: 1"></span>
<input v-model="token" placeholder="Bearer token可选" style="width: 320px" />
<button @click="applyToken">应用</button>
</header>
<router-view />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { setSuperToken } from './api'
const token = ref('')
function applyToken() {
setSuperToken(token.value)
}
</script>

View File

@@ -0,0 +1,15 @@
import axios from 'axios'
export const api = axios.create({
baseURL: '/super/v1',
})
export function setSuperToken(token: string) {
const t = token.trim()
if (!t) {
delete api.defaults.headers.common.Authorization
return
}
api.defaults.headers.common.Authorization = `Bearer ${t}`
}

2
frontend/superadmin/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).mount('#app')

View File

@@ -0,0 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router'
import DashboardPage from './views/DashboardPage.vue'
import TenantsPage from './views/TenantsPage.vue'
import RolesPage from './views/RolesPage.vue'
export const router = createRouter({
history: createWebHistory('/super/'),
routes: [
{ path: '/', component: DashboardPage },
{ path: '/tenants', component: TenantsPage },
{ path: '/roles', component: RolesPage },
{ path: '/:pathMatch(.*)*', redirect: '/' },
],
})

View File

@@ -0,0 +1,19 @@
<template>
<main style="padding: 16px">
<h1 style="font-size: 18px; font-weight: 600">统计</h1>
<pre style="margin-top: 12px; background: #f7f7f7; padding: 12px; border-radius: 8px">{{ data }}</pre>
</main>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api'
const data = ref<any>(null)
onMounted(async () => {
const resp = await api.get('/statistics')
data.value = resp.data
})
</script>

View File

@@ -0,0 +1,76 @@
<template>
<main style="padding: 16px">
<h1 style="font-size: 18px; font-weight: 600">角色类型</h1>
<p style="margin-top: 6px; color: #666">仅管理固定角色<code>user</code> / <code>tenant_admin</code> / <code>super_admin</code></p>
<table style="margin-top: 12px; width: 100%; border-collapse: collapse">
<thead>
<tr>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Code</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Name</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Status</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Updated</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="it in items" :key="it.code">
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.code }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">
<input v-model="it._editName" style="width: 240px" />
</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">
<select v-model.number="it._editStatus">
<option :value="0">0 (enabled)</option>
<option :value="1">1 (disabled)</option>
</select>
</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.updated_at }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">
<button @click="save(it)">保存</button>
</td>
</tr>
</tbody>
</table>
</main>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api'
type Role = {
code: string
name: string
status: number
updated_at: string
_editName: string
_editStatus: number
}
const items = ref<Role[]>([])
async function load() {
const resp = await api.get('/roles')
const list = (resp.data.items || []) as any[]
items.value = list.map((it) => ({
code: it.code,
name: it.name,
status: it.status,
updated_at: it.updated_at,
_editName: it.name,
_editStatus: it.status,
}))
}
async function save(it: Role) {
await api.put(`/roles/${encodeURIComponent(it.code)}`, {
name: it._editName,
status: it._editStatus,
})
await load()
}
onMounted(load)
</script>

View File

@@ -0,0 +1,47 @@
<template>
<main style="padding: 16px">
<h1 style="font-size: 18px; font-weight: 600">租户</h1>
<div style="margin-top: 12px; display: flex; gap: 8px">
<input v-model="keyword" placeholder="keyword" />
<button @click="load">查询</button>
</div>
<table style="margin-top: 12px; width: 100%; border-collapse: collapse">
<thead>
<tr>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">ID</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Code</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Name</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Admins</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Admin Expire At(max)</th>
<th style="text-align: left; border-bottom: 1px solid #eee; padding: 8px">Status</th>
</tr>
</thead>
<tbody>
<tr v-for="it in items" :key="it.id">
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.id }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.tenant_code }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.name }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.admin_count }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.admin_expire_at || '-' }}</td>
<td style="border-bottom: 1px solid #f2f2f2; padding: 8px">{{ it.status }}</td>
</tr>
</tbody>
</table>
</main>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api'
const keyword = ref('')
const items = ref<any[]>([])
async function load() {
const resp = await api.get('/tenants', { params: { page: 1, limit: 50, keyword: keyword.value } })
items.value = resp.data.items || []
}
onMounted(load)
</script>