forked from EduCraft/curriculum-project-hub
refactor(admin-web): polish Chinese UI and bits-ui controls
Map roles to Chinese labels, remove ADR/spec wording from the surface, and replace native selects/checkboxes/modals with bits-ui Select, Checkbox, Switch, Dialog, Label, and Collapsible.
This commit is contained in:
@@ -5,8 +5,10 @@
|
||||
import { page } from '$app/state';
|
||||
import { session, loadSession, logout, redirectToLogin } from '$lib/session';
|
||||
import type { OrgMembership } from '$lib/api';
|
||||
import { orgRoleLabel } from '$lib/format';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import ToastHost from '$lib/components/ToastHost.svelte';
|
||||
import SelectField from '$lib/components/SelectField.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
let mobileNavOpen = $state(false);
|
||||
@@ -23,7 +25,7 @@
|
||||
{ key: 'projects', label: '项目', icon: 'projects' as const },
|
||||
{ key: 'models', label: '模型', icon: 'models' as const },
|
||||
{ key: 'roles', label: '角色', icon: 'roles' as const },
|
||||
{ key: 'provider', label: 'Provider', icon: 'provider' as const }
|
||||
{ key: 'provider', label: '供应方', icon: 'provider' as const }
|
||||
];
|
||||
|
||||
function isAdmin(org: OrgMembership): boolean {
|
||||
@@ -47,7 +49,6 @@
|
||||
return memberships().filter(isAdmin);
|
||||
}
|
||||
|
||||
/** Org matching the URL slug (if any). */
|
||||
function currentOrg(): OrgMembership | null {
|
||||
const slug = orgSlugFromPath();
|
||||
if (!slug) return null;
|
||||
@@ -68,7 +69,7 @@
|
||||
|
||||
function navHref(key: string): string {
|
||||
const slug = currentOrg()?.slug ?? pickHomeOrg()?.slug;
|
||||
if (!slug) return '/';
|
||||
if (!slug) return '/';
|
||||
if (key === 'overview') return `/admin/org/${slug}`;
|
||||
return `/admin/org/${slug}/${key}`;
|
||||
}
|
||||
@@ -79,9 +80,9 @@
|
||||
return navItems.find((i) => i.key === key)?.label ?? '管理后台';
|
||||
}
|
||||
|
||||
function onOrgSwitch(e: Event) {
|
||||
const sel = e.target as HTMLSelectElement;
|
||||
void goto(`/admin/org/${sel.value}`);
|
||||
function switchOrg(nextSlug: string) {
|
||||
if (!nextSlug || nextSlug === currentOrg()?.slug) return;
|
||||
void goto(`/admin/org/${nextSlug}`);
|
||||
}
|
||||
|
||||
function handleLogout(e: Event) {
|
||||
@@ -89,14 +90,18 @@
|
||||
logout();
|
||||
}
|
||||
|
||||
// Close mobile nav on route change.
|
||||
function orgSelectItems(list: OrgMembership[]) {
|
||||
return list.map((o) => ({
|
||||
value: o.slug,
|
||||
label: `${o.name} · ${orgRoleLabel(o.role)}`
|
||||
}));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
page.url.pathname;
|
||||
mobileNavOpen = false;
|
||||
});
|
||||
|
||||
// If the user has admin rights but the URL is not under a valid org slug,
|
||||
// send them to their home org. Fixes false "未加入组织" on / or wrong paths.
|
||||
$effect(() => {
|
||||
if ($session.loading || !$session.me) return;
|
||||
const admins = adminOrgs();
|
||||
@@ -109,8 +114,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Only auto-redirect when URL is not already a matched *member* org
|
||||
// (member orgs keep their own denied screen). Missing / unknown slug → home.
|
||||
if (!matched) {
|
||||
const target = `/admin/org/${admins[0].slug}`;
|
||||
if (page.url.pathname !== target && !page.url.pathname.startsWith(`${target}/`)) {
|
||||
@@ -179,21 +182,21 @@
|
||||
CPH
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-semibold text-surface-900">Org Admin</div>
|
||||
<div class="truncate text-sm font-semibold text-surface-900">组织后台</div>
|
||||
<div class="truncate text-xs text-surface-400">Curriculum Hub</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pb-3">
|
||||
<label for="org-sel" class="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-surface-500">
|
||||
<div class="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-surface-500">
|
||||
<Icon name="org" class="h-3.5 w-3.5" />
|
||||
组织
|
||||
</label>
|
||||
<select id="org-sel" class="saas-select text-sm" value={org.slug} onchange={onOrgSwitch}>
|
||||
{#each me.organizations as o}
|
||||
<option value={o.slug}>{o.name} · {o.role}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<SelectField
|
||||
items={orgSelectItems(me.organizations)}
|
||||
value={org.slug}
|
||||
onchange={switchOrg}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 space-y-0.5 overflow-y-auto px-2 pb-3">
|
||||
@@ -218,7 +221,7 @@
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium text-surface-800">{me.user.displayName}</div>
|
||||
<div class="truncate text-[11px] text-surface-400">{org.role}</div>
|
||||
<div class="truncate text-[11px] text-surface-400">{orgRoleLabel(org.role)}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -268,16 +271,18 @@
|
||||
<h2 class="mb-2 text-lg font-semibold">无权访问管理后台</h2>
|
||||
<p class="mb-3 text-sm text-surface-500">
|
||||
组织 <strong>{denied.name}</strong>(/{denied.slug})中你的角色是
|
||||
<span class="saas-badge-neutral mx-1">{denied.role}</span>,
|
||||
需要 <strong>OWNER</strong> 或 <strong>ADMIN</strong>。
|
||||
<span class="saas-badge-neutral mx-1">{orgRoleLabel(denied.role)}</span>,
|
||||
需要<strong>所有者</strong>或<strong>管理员</strong>。
|
||||
</p>
|
||||
{#if memberships().length > 1}
|
||||
<label for="org-switch" class="saas-label text-left">切换到其他组织</label>
|
||||
<select id="org-switch" class="saas-select mb-4" onchange={onOrgSwitch}>
|
||||
{#each memberships() as o}
|
||||
<option value={o.slug} selected={o.slug === denied.slug}>{o.name} · {o.role}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="saas-label text-left mb-1.5">切换到其他组织</p>
|
||||
<div class="mb-4">
|
||||
<SelectField
|
||||
items={orgSelectItems(memberships())}
|
||||
value={denied.slug}
|
||||
onchange={switchOrg}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<button class="saas-btn-ghost" onclick={handleLogout}>退出登录</button>
|
||||
</div>
|
||||
@@ -290,7 +295,7 @@
|
||||
<h2 class="mb-2 text-lg font-semibold">无权访问管理后台</h2>
|
||||
<p class="mb-5 text-sm text-surface-500">
|
||||
你加入了 <strong>{denied.name}</strong>,角色是
|
||||
<span class="saas-badge-neutral mx-1">{denied.role}</span>。仅 OWNER/ADMIN 可进入后台。
|
||||
<span class="saas-badge-neutral mx-1">{orgRoleLabel(denied.role)}</span>。仅所有者与管理员可进入后台。
|
||||
</p>
|
||||
{:else}
|
||||
<h2 class="mb-2 text-lg font-semibold">正在跳转…</h2>
|
||||
@@ -308,8 +313,7 @@
|
||||
<div class="saas-status-card">
|
||||
<h2 class="mb-2 text-lg font-semibold">未加入组织</h2>
|
||||
<p class="mb-3 text-sm text-surface-500">
|
||||
飞书账号已登录,但 <code class="font-mono">/api/me</code> 的
|
||||
<code class="font-mono">organizations</code> 为空。
|
||||
飞书账号已登录,但当前账号尚未加入任何组织。
|
||||
</p>
|
||||
<p class="mb-5 text-left text-xs text-surface-400">
|
||||
open_id:
|
||||
|
||||
Reference in New Issue
Block a user