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

File diff suppressed because one or more lines are too long

13
frontend/admin/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn Admin</title>
<script type="module" crossorigin src="./assets/index-CLimnK9W.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

13
frontend/admin/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1614
frontend/admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "@quyun/admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}

View File

@@ -0,0 +1,4 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,7 @@
import axios from 'axios'
import { getApiBaseURL } from './tenant'
export const api = axios.create({
baseURL: getApiBaseURL(),
})

2
frontend/admin/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 { router } from './router'
import App from './App.vue'
createApp(App).use(router).mount('#app')

View File

@@ -0,0 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router'
import { getAdminRouterBase } from './tenant'
import DashboardPage from './views/DashboardPage.vue'
export const router = createRouter({
history: createWebHistory(getAdminRouterBase()),
routes: [
{ path: '/', component: DashboardPage },
{ path: '/:pathMatch(.*)*', redirect: '/' },
],
})

View File

@@ -0,0 +1,16 @@
export function getTenantCodeFromPath(pathname = window.location.pathname): string {
const parts = pathname.split('/').filter(Boolean)
if (parts.length < 2 || parts[0] !== 't') return ''
return decodeURIComponent(parts[1] || '').toLowerCase()
}
export function getAdminRouterBase(pathname = window.location.pathname): string {
const tenantCode = getTenantCodeFromPath(pathname)
return `/t/${tenantCode}/admin/`
}
export function getApiBaseURL(pathname = window.location.pathname): string {
const tenantCode = getTenantCodeFromPath(pathname)
return `/t/${tenantCode}/v1`
}

View File

@@ -0,0 +1,20 @@
<template>
<main style="padding: 16px">
<h1 style="font-size: 20px; font-weight: 600">QuyUn Admin</h1>
<p style="margin-top: 8px; color: #666">
Router base: <code>{{ base }}</code>
</p>
<p style="margin-top: 4px; color: #666">
API baseURL: <code>{{ apiBase }}</code>
</p>
</main>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getAdminRouterBase, getApiBaseURL } from '../tenant'
const base = computed(() => getAdminRouterBase())
const apiBase = computed(() => getApiBaseURL())
</script>

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: './',
server: {
port: 5173,
},
})

File diff suppressed because one or more lines are too long

13
frontend/superadmin/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn Super Admin</title>
<script type="module" crossorigin src="./assets/index-CFAqJvax.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn Super Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1614
frontend/superadmin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "@quyun/superadmin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}

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>

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: './',
server: { port: 5175 },
})

File diff suppressed because one or more lines are too long

13
frontend/user/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn</title>
<script type="module" crossorigin src="./assets/index-wt7BFVvy.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

13
frontend/user/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuyUn</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1614
frontend/user/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "@quyun/user",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}

View File

@@ -0,0 +1,4 @@
<template>
<router-view />
</template>

8
frontend/user/src/api.ts Normal file
View File

@@ -0,0 +1,8 @@
import axios from 'axios'
import { getApiBaseURL } from './tenant'
export const api = axios.create({
baseURL: getApiBaseURL(),
withCredentials: true,
})

2
frontend/user/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 { router } from './router'
import App from './App.vue'
createApp(App).use(router).mount('#app')

View File

@@ -0,0 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router'
import { getUserRouterBase } from './tenant'
import HomePage from './views/HomePage.vue'
export const router = createRouter({
history: createWebHistory(getUserRouterBase()),
routes: [
{ path: '/', component: HomePage },
{ path: '/:pathMatch(.*)*', redirect: '/' },
],
})

View File

@@ -0,0 +1,16 @@
export function getTenantCodeFromPath(pathname = window.location.pathname): string {
const parts = pathname.split('/').filter(Boolean)
if (parts.length < 2 || parts[0] !== 't') return ''
return decodeURIComponent(parts[1] || '').toLowerCase()
}
export function getUserRouterBase(pathname = window.location.pathname): string {
const tenantCode = getTenantCodeFromPath(pathname)
return `/t/${tenantCode}/`
}
export function getApiBaseURL(pathname = window.location.pathname): string {
const tenantCode = getTenantCodeFromPath(pathname)
return `/t/${tenantCode}/v1`
}

View File

@@ -0,0 +1,20 @@
<template>
<main style="padding: 16px">
<h1 style="font-size: 20px; font-weight: 600">QuyUn</h1>
<p style="margin-top: 8px; color: #666">
Router base: <code>{{ base }}</code>
</p>
<p style="margin-top: 4px; color: #666">
API baseURL: <code>{{ apiBase }}</code>
</p>
</main>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getApiBaseURL, getUserRouterBase } from '../tenant'
const base = computed(() => getUserRouterBase())
const apiBase = computed(() => getApiBaseURL())
</script>

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: './',
server: {
port: 5174,
},
})