forked from bai/curriculum-project-hub
chore(database-admin): 删除该前端工程,已被 filelib-web 取代
12628c9 引入它意在替换后端渲染的 /database 页面,但从未接通:具体
路由 /database/dashboard 比 SPA 通配 /database/* 更具体,服务端
handler 永远胜出,SPA 的 dashboard 不可达。工程头注释声称 SPA 已
接管 dashboard、且 /database/config 存在,两者当时都不成立。
hub 的 build 脚本也从未构建它,于是 static.ts 里的 existsSync 守卫
每次部署都失败,这个外壳实际长期处于禁用状态 —— 它没服务过一个请求。
与 filelib-web 合并而非并存的理由:两者共用文件库浏览器、会话层、
toast 宿主与设计令牌,拆开就要把这些全复制一遍(ADR-0029)。
内容可从 git 历史取回。
This commit is contained in:
@@ -1,23 +0,0 @@
|
|||||||
node_modules
|
|
||||||
|
|
||||||
# Output
|
|
||||||
.output
|
|
||||||
.vercel
|
|
||||||
.netlify
|
|
||||||
.wrangler
|
|
||||||
/.svelte-kit
|
|
||||||
/build
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Env
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
!.env.test
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
engine-strict=true
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
build
|
|
||||||
.svelte-kit
|
|
||||||
node_modules
|
|
||||||
package-lock.json
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 120,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
Generated
-2660
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "database-admin",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite dev",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"format:check": "prettier --check ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@skeletonlabs/skeleton": "^4.15.2",
|
|
||||||
"@skeletonlabs/skeleton-svelte": "^4.15.2",
|
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
|
||||||
"@sveltejs/kit": "^2.63.0",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
|
||||||
"bits-ui": "^2.18.1",
|
|
||||||
"prettier": "^3.9.5",
|
|
||||||
"prettier-plugin-svelte": "^4.1.1",
|
|
||||||
"svelte": "^5.56.1",
|
|
||||||
"svelte-check": "^4.6.0",
|
|
||||||
"tailwindcss": "^4.3.2",
|
|
||||||
"typescript": "^6.0.3",
|
|
||||||
"vite": "^8.0.16"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-13
@@ -1,13 +0,0 @@
|
|||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
|
||||||
namespace App {
|
|
||||||
// interface Error {}
|
|
||||||
// interface Locals {}
|
|
||||||
// interface PageData {}
|
|
||||||
// interface PageState {}
|
|
||||||
// interface Platform {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="description" content="Curriculum Project Hub — 数据库管理后台" />
|
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" type="image/svg+xml" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
/* Fallback before CSS bundle */
|
|
||||||
html {
|
|
||||||
font-family: 'Inter', system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<title>Database Admin</title>
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
class="relative min-h-screen overflow-x-hidden bg-gradient-to-br from-slate-50 via-white to-indigo-50 text-slate-700"
|
|
||||||
>
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* Thin API client for the database-admin backend. Same-origin cookie auth,
|
|
||||||
* reusing the platform session (`cph_session`) and the admin plane's /api/me.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class ApiError extends Error {
|
|
||||||
code: string;
|
|
||||||
status: number;
|
|
||||||
constructor(code: string, message: string, status: number) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'ApiError';
|
|
||||||
this.code = code;
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request(method: string, url: string, body?: unknown): Promise<unknown> {
|
|
||||||
const init: RequestInit = {
|
|
||||||
method,
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: body !== undefined ? { 'content-type': 'application/json' } : undefined,
|
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
||||||
};
|
|
||||||
const res = await fetch(url, init);
|
|
||||||
const text = await res.text();
|
|
||||||
let data: unknown = null;
|
|
||||||
if (text !== '') {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(text);
|
|
||||||
} catch {
|
|
||||||
data = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = (data as { error?: { code?: string; message?: string } } | null)?.error;
|
|
||||||
throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = (u: string) => request('GET', u);
|
|
||||||
const post = (u: string, b?: unknown) => request('POST', u, b);
|
|
||||||
|
|
||||||
// --- Types ---
|
|
||||||
|
|
||||||
export interface OrgMembership {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
status: string;
|
|
||||||
role: 'OWNER' | 'ADMIN' | 'MEMBER';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MeResponse {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
feishuOpenId: string;
|
|
||||||
displayName: string;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
};
|
|
||||||
organizations: OrgMembership[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Unauthenticated bootstrap the login page needs: which org to OAuth against + dev toggle. */
|
|
||||||
export interface DatabaseConfig {
|
|
||||||
siloOrganizationSlug: string;
|
|
||||||
devLoginEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- API ---
|
|
||||||
|
|
||||||
export const api = {
|
|
||||||
me: () => get('/api/me') as Promise<MeResponse>,
|
|
||||||
logout: () => post('/auth/logout'),
|
|
||||||
databaseConfig: () => get('/database/config') as Promise<DatabaseConfig>,
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<!-- Animated aurora background blobs, shared by both pages (soft pastels on light). -->
|
|
||||||
<div class="pointer-events-none fixed inset-0 overflow-hidden">
|
|
||||||
<div class="aurora absolute -left-32 -top-32 h-96 w-96 rounded-full bg-violet-300/50"></div>
|
|
||||||
<div class="aurora absolute right-0 top-1/4 h-96 w-96 rounded-full bg-cyan-300/40" style="animation-delay:-6s"></div>
|
|
||||||
<div class="aurora absolute bottom-0 left-1/3 h-96 w-96 rounded-full bg-indigo-300/40" style="animation-delay:-12s"></div>
|
|
||||||
</div>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { OrgMembership } from './api';
|
|
||||||
|
|
||||||
export function isOrgAdmin(org: OrgMembership | null | undefined): boolean {
|
|
||||||
if (!org) return false;
|
|
||||||
const role = String(org.role ?? '').toUpperCase();
|
|
||||||
return role === 'OWNER' || role === 'ADMIN';
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { api, type MeResponse } from './api';
|
|
||||||
|
|
||||||
interface SessionState {
|
|
||||||
loading: boolean;
|
|
||||||
me: MeResponse | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const session = writable<SessionState>({
|
|
||||||
loading: true,
|
|
||||||
me: null,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function loadSession(): Promise<void> {
|
|
||||||
session.update((s) => ({ ...s, loading: true, error: null }));
|
|
||||||
try {
|
|
||||||
const me = await api.me();
|
|
||||||
session.set({ loading: false, me, error: null });
|
|
||||||
} catch (err) {
|
|
||||||
const status = (err as { status?: number }).status;
|
|
||||||
if (status === 401) {
|
|
||||||
session.set({ loading: false, me: null, error: null });
|
|
||||||
void redirectToLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
session.set({
|
|
||||||
loading: false,
|
|
||||||
me: null,
|
|
||||||
error: err instanceof Error ? err.message : String(err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the browser to the login page (`/database/admin`). Unlike admin-web we
|
|
||||||
* don't jump straight to Feishu OAuth: the login page reads /database/config
|
|
||||||
* to build the org-scoped link and to show the dev bypass when enabled.
|
|
||||||
*/
|
|
||||||
export async function redirectToLogin(): Promise<void> {
|
|
||||||
const loginPath = `${base}/admin`;
|
|
||||||
if (window.location.pathname === loginPath) return;
|
|
||||||
await goto(loginPath, { replaceState: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build the org-scoped Feishu OAuth entry point (a backend route, not under base). */
|
|
||||||
export function feishuLoginHref(orgSlug: string, returnTo: string = `${base}/dashboard`): string {
|
|
||||||
return `/auth/feishu/${encodeURIComponent(orgSlug)}?returnTo=${encodeURIComponent(returnTo)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
|
||||||
await api.logout();
|
|
||||||
session.set({ loading: false, me: null, error: null });
|
|
||||||
await redirectToLogin();
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import './app.css';
|
|
||||||
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{@render children()}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { api } from '$lib/api';
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// /database entry — route to dashboard if signed in, else the login page.
|
|
||||||
try {
|
|
||||||
await api.me();
|
|
||||||
await goto(`${base}/dashboard`, { replaceState: true });
|
|
||||||
} catch {
|
|
||||||
await goto(`${base}/admin`, { replaceState: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex min-h-screen items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center gap-3 text-slate-400">
|
|
||||||
<svg class="h-7 w-7 animate-spin text-violet-500" viewBox="0 0 24 24" fill="none">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path
|
|
||||||
class="opacity-90"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<p class="text-sm">正在进入…</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { api, type DatabaseConfig } from '$lib/api';
|
|
||||||
import { feishuLoginHref } from '$lib/session';
|
|
||||||
import Aurora from '$lib/components/Aurora.svelte';
|
|
||||||
|
|
||||||
let config = $state<DatabaseConfig | null>(null);
|
|
||||||
let error = $state<string | null>(null);
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// Already signed in → straight to the dashboard.
|
|
||||||
try {
|
|
||||||
await api.me();
|
|
||||||
await goto(`${base}/dashboard`, { replaceState: true });
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
// Not signed in (401) or backend unreachable — show the login card.
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
config = await api.databaseConfig();
|
|
||||||
} catch (err) {
|
|
||||||
error = err instanceof Error ? err.message : String(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const feishuHref = $derived(config ? feishuLoginHref(config.siloOrganizationSlug) : '#');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Aurora />
|
|
||||||
|
|
||||||
<main class="flex min-h-screen items-center justify-center p-4">
|
|
||||||
<div class="rise glass relative w-full max-w-sm rounded-3xl border border-white/80 p-8 shadow-2xl shadow-indigo-200/50">
|
|
||||||
<div class="mb-7 text-center">
|
|
||||||
<div
|
|
||||||
class="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-500 to-cyan-400 text-2xl font-bold text-white glow-btn"
|
|
||||||
>
|
|
||||||
D
|
|
||||||
</div>
|
|
||||||
<h1 class="text-2xl font-bold tracking-tight grad-text">Database Admin</h1>
|
|
||||||
<p class="mt-2 text-sm text-slate-500">使用飞书登录以管理数据库</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
|
||||||
无法连接后端:{error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={feishuHref}
|
|
||||||
aria-disabled={config ? 'false' : 'true'}
|
|
||||||
class="rise-2 glow-btn group flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-violet-500 to-indigo-500 px-4 py-3.5 text-sm font-semibold text-white transition hover:from-violet-400 hover:to-indigo-400 aria-disabled:pointer-events-none aria-disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.5 3.6L12 11.5 5.5 7.9 12 4.3Z" />
|
|
||||||
</svg>
|
|
||||||
使用飞书登录
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{#if config?.devLoginEnabled}
|
|
||||||
<div class="relative my-6 rise-3">
|
|
||||||
<div class="absolute inset-0 flex items-center"><div class="w-full border-t border-slate-200"></div></div>
|
|
||||||
<div class="relative flex justify-center">
|
|
||||||
<span class="bg-white/70 px-3 text-[11px] font-medium uppercase tracking-[0.2em] text-slate-400">开发模式</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="/database/dev-login"
|
|
||||||
class="rise-3 group flex w-full items-center justify-center gap-2 rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-sm font-semibold text-amber-700 transition hover:border-amber-400 hover:bg-amber-100"
|
|
||||||
>
|
|
||||||
<span>⚡</span> 一键登录管理员
|
|
||||||
</a>
|
|
||||||
<p class="rise-3 mt-2 text-center text-xs text-slate-400">仅开发环境可见 · 跳过飞书 OAuth</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
@source './**/*.{html,js,svelte,ts}';
|
|
||||||
@source '../lib/**/*.{html,js,svelte,ts}';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Design system for the database-admin SPA. Migrated verbatim (in spirit) from
|
|
||||||
* the previous server-rendered pages in hub/src/database/routes/databaseRoutes.ts:
|
|
||||||
* a light aurora / glassmorphism look with violet→cyan gradients and soft rise-in
|
|
||||||
* entrances. Distinct from admin-web's flat industrial theme on purpose.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes aurora {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
transform: translate(6%, -8%) scale(1.15);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
transform: translate(-8%, 6%) scale(0.9);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rise {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(16px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.aurora {
|
|
||||||
filter: blur(80px);
|
|
||||||
animation: aurora 18s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.rise {
|
|
||||||
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
.rise-1 {
|
|
||||||
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
.rise-2 {
|
|
||||||
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both;
|
|
||||||
}
|
|
||||||
.rise-3 {
|
|
||||||
animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both;
|
|
||||||
}
|
|
||||||
.grad-text {
|
|
||||||
background: linear-gradient(120deg, #7c3aed, #0891b2, #4f46e5);
|
|
||||||
background-size: 200% auto;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
animation: shimmer 6s linear infinite;
|
|
||||||
}
|
|
||||||
.glass {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
}
|
|
||||||
.glow-btn {
|
|
||||||
box-shadow: 0 12px 32px -10px rgba(124, 58, 237, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { session, loadSession, logout } from '$lib/session';
|
|
||||||
import { isOrgAdmin } from '$lib/org';
|
|
||||||
import Aurora from '$lib/components/Aurora.svelte';
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
loadSession();
|
|
||||||
});
|
|
||||||
|
|
||||||
// The session is scoped to the silo org, so /api/me returns exactly that org's
|
|
||||||
// membership. Admin gate: only OWNER/ADMIN may use the database console.
|
|
||||||
const me = $derived($session.me);
|
|
||||||
const org = $derived(me?.organizations[0] ?? null);
|
|
||||||
const allowed = $derived(isOrgAdmin(org));
|
|
||||||
const displayName = $derived(me?.user.displayName ?? '');
|
|
||||||
const initial = $derived(displayName.slice(0, 1) || 'U');
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ label: '概览', href: `${base}/dashboard`, active: true, icon: 'M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z' },
|
|
||||||
{ label: '数据表', href: '#', active: false, icon: 'M4 5h16v4H4V5Zm0 6h16v4H4v-4Zm0 6h16v2H4v-2Z' },
|
|
||||||
{ label: '查询', href: '#', active: false, icon: 'm21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z' },
|
|
||||||
{
|
|
||||||
label: '设置',
|
|
||||||
href: '#',
|
|
||||||
active: false,
|
|
||||||
icon: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-3 2 1-2 3-2-1a7 7 0 0 1-2 1l-1 2h-4l-1-2a7 7 0 0 1-2-1l-2 1-2-3 2-1a7 7 0 0 1 0-2l-2-1 2-3 2 1a7 7 0 0 1 2-1l1-2h4l1 2a7 7 0 0 1 2 1l2-1 2 3-2 1a7 7 0 0 1 0 2Z',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{ label: '数据表', value: '—', accent: 'from-violet-200/60 to-transparent' },
|
|
||||||
{ label: '记录数', value: '—', accent: 'from-cyan-200/60 to-transparent' },
|
|
||||||
{ label: '最近查询', value: '—', accent: 'from-indigo-200/60 to-transparent' },
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Aurora />
|
|
||||||
|
|
||||||
{#if $session.loading}
|
|
||||||
<div class="flex min-h-screen items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center gap-3 text-slate-400">
|
|
||||||
<svg class="h-8 w-8 animate-spin text-violet-500" viewBox="0 0 24 24" fill="none">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path
|
|
||||||
class="opacity-90"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<p class="text-sm">加载中…</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if $session.error}
|
|
||||||
<div class="flex min-h-screen items-center justify-center p-4">
|
|
||||||
<div class="rise glass w-full max-w-sm rounded-3xl border border-white/80 p-8 text-center shadow-2xl shadow-indigo-200/50">
|
|
||||||
<h2 class="mb-2 text-lg font-bold text-slate-900">无法连接后端</h2>
|
|
||||||
<p class="mb-5 text-sm text-slate-500">{$session.error}</p>
|
|
||||||
<button
|
|
||||||
class="rounded-xl bg-gradient-to-r from-violet-500 to-indigo-500 px-4 py-2 text-sm font-semibold text-white"
|
|
||||||
onclick={() => loadSession()}>重试</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if !allowed}
|
|
||||||
<div class="flex min-h-screen items-center justify-center p-4">
|
|
||||||
<div class="rise glass w-full max-w-md rounded-3xl border border-white/80 p-8 text-center shadow-2xl shadow-indigo-200/50">
|
|
||||||
<h2 class="mb-2 text-lg font-bold text-slate-900">无权访问</h2>
|
|
||||||
<p class="mb-5 text-sm text-slate-500">数据库管理台仅向组织所有者与管理员开放。</p>
|
|
||||||
<button
|
|
||||||
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50"
|
|
||||||
onclick={() => logout()}>退出登录</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="relative flex min-h-screen">
|
|
||||||
<!-- 左侧菜单栏 -->
|
|
||||||
<aside class="flex w-64 shrink-0 flex-col border-r border-slate-200/80 glass">
|
|
||||||
<div class="flex items-center gap-3 px-5 py-6">
|
|
||||||
<div
|
|
||||||
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 text-lg font-bold text-white glow-btn"
|
|
||||||
>
|
|
||||||
D
|
|
||||||
</div>
|
|
||||||
<span class="text-base font-bold grad-text">Database Admin</span>
|
|
||||||
</div>
|
|
||||||
<nav class="flex flex-1 flex-col gap-1.5 px-3 py-2">
|
|
||||||
{#each navItems as item}
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
class={item.active
|
|
||||||
? 'group flex items-center gap-3 rounded-xl bg-gradient-to-r from-violet-500 to-indigo-500 px-3 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-300/50'
|
|
||||||
: 'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-slate-500 transition hover:bg-slate-100 hover:text-slate-900'}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="h-4 w-4 shrink-0"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.8"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"><path d={item.icon} /></svg
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</nav>
|
|
||||||
<div class="m-3 flex items-center gap-3 rounded-xl border border-slate-200 bg-white/60 px-3 py-3">
|
|
||||||
<div
|
|
||||||
class="flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-indigo-500 text-sm font-semibold text-white"
|
|
||||||
>
|
|
||||||
{initial}
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-[11px] uppercase tracking-wider text-slate-400">已登录</p>
|
|
||||||
<p class="truncate text-sm font-medium text-slate-700">{displayName}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- 右侧内容 -->
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<header class="flex items-center justify-between border-b border-slate-200/80 glass px-8 py-4">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-lg font-bold text-slate-900">概览</h1>
|
|
||||||
<p class="text-xs text-slate-400">欢迎回来,这里是数据库管理台</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onclick={() => logout()}
|
|
||||||
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900"
|
|
||||||
>
|
|
||||||
退出登录
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="flex-1 p-8">
|
|
||||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-3">
|
|
||||||
{#each stats as s, i}
|
|
||||||
<div
|
|
||||||
class="rise-{i +
|
|
||||||
1} group relative overflow-hidden rounded-2xl border border-white/80 glass p-5 shadow-lg shadow-slate-200/50 transition hover:-translate-y-0.5 hover:shadow-xl hover:shadow-indigo-200/50"
|
|
||||||
>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-br {s.accent} opacity-0 transition group-hover:opacity-100"></div>
|
|
||||||
<p class="relative text-sm text-slate-500">{s.label}</p>
|
|
||||||
<p class="relative mt-2 text-3xl font-bold text-slate-900">{s.value}</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="rise-3 mt-6 flex h-64 items-center justify-center rounded-2xl border border-dashed border-slate-300 glass text-sm text-slate-400"
|
|
||||||
>
|
|
||||||
内容区占位 · 后续数据端点挂在 /database/* 并加 guard
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
|
||||||
<rect width="32" height="32" rx="8" fill="#4F46E5"/>
|
|
||||||
<path d="M8 10.5h7.5a3.5 3.5 0 0 1 0 7H11v4H8v-11Zm3 4.5h4.5a1.5 1.5 0 0 0 0-3H11v3Z" fill="white"/>
|
|
||||||
<path d="M20.5 21.5c1.93 0 3.5-1.34 3.5-3s-1.57-3-3.5-3S17 16.84 17 18.5s1.57 3 3.5 3Z" fill="white" opacity=".9"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 353 B |
@@ -1,3 +0,0 @@
|
|||||||
# allow crawling everything by default
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
|
||||||
const config = {
|
|
||||||
preprocess: vitePreprocess(),
|
|
||||||
kit: {
|
|
||||||
adapter: adapter({
|
|
||||||
pages: 'build',
|
|
||||||
assets: 'build',
|
|
||||||
fallback: 'index.html',
|
|
||||||
precompress: false,
|
|
||||||
strict: false,
|
|
||||||
}),
|
|
||||||
// This SPA is served under /database by the Hub. admin-web owns the
|
|
||||||
// root asset paths (/_app, /favicon.svg); a base path moves this app's
|
|
||||||
// assets to /database/_app/* so the two builds never collide.
|
|
||||||
paths: { base: '/database' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rewriteRelativeImportExtensions": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
}
|
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
|
||||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
|
||||||
//
|
|
||||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
|
||||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [tailwindcss(), sveltekit()],
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': 'http://127.0.0.1:8788',
|
|
||||||
'/auth': 'http://127.0.0.1:8788',
|
|
||||||
// Backend owns the /database/* HTTP surface (login page, dashboard,
|
|
||||||
// data routes). Proxy it in dev so those paths hit the real server
|
|
||||||
// instead of the SPA fallback.
|
|
||||||
'/database': 'http://127.0.0.1:8788',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user