style(admin-web): apply Prettier formatting

Run `prettier --write .` across all source files. Changes are purely
formatting: trailing commas, line wrapping at 120 chars, import
reordering, and CSS whitespace. No logic changes. Verified with
`prettier --check .` and `svelte-check` (0 errors, 0 warnings).
This commit is contained in:
2026-07-14 00:10:13 +08:00
parent ae870a9b73
commit cbe569d7e6
27 changed files with 176 additions and 174 deletions
+28 -18
View File
@@ -18,7 +18,7 @@ async function request(method: string, url: string, body?: unknown): Promise<unk
method,
credentials: 'same-origin',
headers: body !== undefined ? { 'content-type': 'application/json' } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined
body: body !== undefined ? JSON.stringify(body) : undefined,
};
const res = await fetch(url, init);
const text = await res.text();
@@ -194,7 +194,11 @@ export const api = {
me: () => get('/api/me') as Promise<MeResponse>,
logout: () => post('/auth/logout'),
org: (slug: string) => get(orgBase(slug)) as Promise<{ organization: { id: string; slug: string; name: string; status: string }; actorRole: string }>,
org: (slug: string) =>
get(orgBase(slug)) as Promise<{
organization: { id: string; slug: string; name: string; status: string };
actorRole: string;
}>,
settings: (slug: string) => get(`${orgBase(slug)}/settings`) as Promise<{ membersCanCreateProjects: boolean }>,
setSettings: (slug: string, body: { membersCanCreateProjects: boolean }) =>
patch(`${orgBase(slug)}/settings`, body) as Promise<{ membersCanCreateProjects: boolean }>,
@@ -202,18 +206,15 @@ export const api = {
members: (slug: string) => get(`${orgBase(slug)}/members`) as Promise<{ members: OrgMember[] }>,
addMember: (slug: string, body: { feishuOpenId: string; displayName?: string; role: string }) =>
post(`${orgBase(slug)}/members`, body) as Promise<OrgMember>,
setMemberRole: (slug: string, userId: string, role: string) =>
patch(`${orgBase(slug)}/members/${userId}`, { role }),
revokeMember: (slug: string, userId: string) =>
post(`${orgBase(slug)}/members/${userId}/revoke`),
setMemberRole: (slug: string, userId: string, role: string) => patch(`${orgBase(slug)}/members/${userId}`, { role }),
revokeMember: (slug: string, userId: string) => post(`${orgBase(slug)}/members/${userId}/revoke`),
teams: (slug: string) => get(`${orgBase(slug)}/teams`) as Promise<{ teams: TeamRow[] }>,
createTeam: (slug: string, body: { slug: string; name: string; description?: string }) =>
post(`${orgBase(slug)}/teams`, body) as Promise<TeamRow>,
updateTeam: (slug: string, teamId: string, body: { name?: string; description?: string | null }) =>
patch(`${orgBase(slug)}/teams/${teamId}`, body) as Promise<TeamRow>,
archiveTeam: (slug: string, teamId: string) =>
post(`${orgBase(slug)}/teams/${teamId}/archive`),
archiveTeam: (slug: string, teamId: string) => post(`${orgBase(slug)}/teams/${teamId}/archive`),
teamMembers: (slug: string, teamId: string) =>
get(`${orgBase(slug)}/teams/${teamId}/members`) as Promise<{ members: TeamMemberRow[] }>,
addTeamMember: (slug: string, teamId: string, body: { userId?: string; feishuOpenId?: string }) =>
@@ -223,23 +224,30 @@ export const api = {
explorer: (slug: string) => get(`${orgBase(slug)}/explorer`) as Promise<ExplorerData>,
createFolder: (slug: string, body: { name: string; parentId?: string; sortKey?: string }) =>
post(`${orgBase(slug)}/folders`, body) as Promise<{ id: string; name: string; parentId: string | null; sortKey: string }>,
post(`${orgBase(slug)}/folders`, body) as Promise<{
id: string;
name: string;
parentId: string | null;
sortKey: string;
}>,
renameFolder: (slug: string, folderId: string, body: { name?: string; sortKey?: string; parentId?: string | null }) =>
patch(`${orgBase(slug)}/folders/${folderId}`, body) as Promise<{ id: string; name: string; parentId: string | null; sortKey: string }>,
patch(`${orgBase(slug)}/folders/${folderId}`, body) as Promise<{
id: string;
name: string;
parentId: string | null;
sortKey: string;
}>,
archiveFolder: (slug: string, folderId: string) =>
post(`${orgBase(slug)}/folders/${folderId}/archive`) as Promise<{ archived: true; folderId: string }>,
createProject: (slug: string, body: { name: string; folderId?: string }) =>
post(`${orgBase(slug)}/projects`, body) as Promise<{ id: string; name: string }>,
project: (slug: string, projectId: string) =>
get(`${orgBase(slug)}/projects/${projectId}`) as Promise<ProjectDetail>,
project: (slug: string, projectId: string) => get(`${orgBase(slug)}/projects/${projectId}`) as Promise<ProjectDetail>,
renameProject: (slug: string, projectId: string, name: string) =>
patch(`${orgBase(slug)}/projects/${projectId}`, { name }),
moveProject: (slug: string, projectId: string, folderId: string | null) =>
patch(`${orgBase(slug)}/projects/${projectId}/folder`, { folderId }),
archiveProject: (slug: string, projectId: string) =>
post(`${orgBase(slug)}/projects/${projectId}/archive`),
archiveBinding: (slug: string, projectId: string) =>
post(`${orgBase(slug)}/projects/${projectId}/binding/archive`),
archiveProject: (slug: string, projectId: string) => post(`${orgBase(slug)}/projects/${projectId}/archive`),
archiveBinding: (slug: string, projectId: string) => post(`${orgBase(slug)}/projects/${projectId}/binding/archive`),
teamAccess: (slug: string, projectId: string) =>
get(`${orgBase(slug)}/projects/${projectId}/team-access`) as Promise<{ access: TeamAccessEntry[] }>,
@@ -249,7 +257,9 @@ export const api = {
del(`${orgBase(slug)}/projects/${projectId}/team-access/${teamId}`),
sessions: (slug: string, projectId: string, limit?: number) =>
get(`${orgBase(slug)}/projects/${projectId}/sessions${limit !== undefined ? `?limit=${limit}` : ''}`) as Promise<{ sessions: SessionSummary[] }>,
get(`${orgBase(slug)}/projects/${projectId}/sessions${limit !== undefined ? `?limit=${limit}` : ''}`) as Promise<{
sessions: SessionSummary[];
}>,
usage: (slug: string, params?: { from?: string; to?: string; folderId?: string }) => {
const q = new URLSearchParams();
if (params?.from) q.set('from', params.from);
@@ -286,5 +296,5 @@ export const api = {
},
) => put(`${orgBase(slug)}/feishu-application-connection`, body) as Promise<FeishuApplicationConnection>,
disableFeishuApplication: (slug: string) =>
del(`${orgBase(slug)}/feishu-application-connection`) as Promise<FeishuApplicationConnection>
del(`${orgBase(slug)}/feishu-application-connection`) as Promise<FeishuApplicationConnection>,
};
@@ -6,7 +6,7 @@
checked = $bindable(false),
disabled = false,
class: className = '',
onchange
onchange,
}: {
checked?: boolean;
disabled?: boolean;
@@ -4,7 +4,7 @@
let {
title = '暂无数据',
description,
action
action,
}: {
title?: string;
description?: string;
@@ -13,7 +13,9 @@
</script>
<div class="saas-empty">
<div class="mb-1 flex h-12 w-12 items-center justify-center border border-surface-300 bg-surface-100 text-surface-600">
<div
class="mb-1 flex h-12 w-12 items-center justify-center border border-surface-300 bg-surface-100 text-surface-600"
>
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
</svg>
@@ -1,7 +1,7 @@
<script lang="ts">
let {
message,
onretry
onretry,
}: {
message: string;
onretry?: () => void;
@@ -7,7 +7,7 @@
folder,
folders,
projects,
slug
slug,
}: {
folder: ExplorerFolder;
folders: ExplorerFolder[];
@@ -8,7 +8,7 @@
folders,
projects,
parentId,
slug
slug,
}: {
folders: ExplorerFolder[];
projects: {
+16 -4
View File
@@ -2,7 +2,7 @@
/** Inline nav icons for the admin shell. */
let {
name,
class: className = 'h-4 w-4'
class: className = 'h-4 w-4',
}: {
name:
| 'overview'
@@ -24,7 +24,11 @@
{#if name === 'overview'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l9-9 9 9M5 10v9a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1v-9" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 12l9-9 9 9M5 10v9a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1v-9"
/>
</svg>
{:else if name === 'members'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
@@ -52,8 +56,16 @@
</svg>
{:else if name === 'provider'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 016.364 6.364l-3.182 3.182a4.5 4.5 0 01-6.364-6.364" />
<path stroke-linecap="round" stroke-linejoin="round" d="M10.81 15.312a4.5 4.5 0 01-6.364-6.364l3.182-3.182a4.5 4.5 0 016.364 6.364" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 016.364 6.364l-3.182 3.182a4.5 4.5 0 01-6.364-6.364"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.81 15.312a4.5 4.5 0 01-6.364-6.364l3.182-3.182a4.5 4.5 0 016.364 6.364"
/>
</svg>
{:else if name === 'feishu'}
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
@@ -6,7 +6,7 @@
open = $bindable(false),
title,
children,
onclose
onclose,
}: {
open?: boolean;
title: string;
@@ -26,7 +26,7 @@
<Dialog.Content class="saas-modal">
<div class="mb-4 flex items-start justify-between gap-3">
<Dialog.Title class="text-lg font-semibold text-surface-900">{title}</Dialog.Title>
<Dialog.Close class="saas-btn-ghost !px-2 !py-1 text-surface-600" aria-label="关闭">
<Dialog.Close class="saas-btn-ghost px-2! py-1! text-surface-600" aria-label="关闭">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -4,7 +4,7 @@
let {
title,
description,
actions
actions,
}: {
title: string;
description?: string;
@@ -10,7 +10,7 @@
class: className = '',
disabled = false,
placeholder = '请选择…',
onchange
onchange,
}: {
items: SelectItem[];
value?: string;
@@ -51,12 +51,7 @@
<Select.Content class="saas-select-content" sideOffset={6} collisionPadding={8}>
<Select.Viewport class="p-1">
{#each items as item (item.value)}
<Select.Item
class="saas-select-item"
value={item.value}
label={item.label}
disabled={item.disabled}
>
<Select.Item class="saas-select-item" value={item.value} label={item.label} disabled={item.disabled}>
{#snippet children({ selected })}
<span class="min-w-0 flex-1 truncate">{item.label}</span>
{#if selected}
@@ -2,7 +2,7 @@
let {
label,
value,
hint
hint,
}: {
label: string;
value: string;
@@ -5,7 +5,7 @@
checked = $bindable(false),
disabled = false,
class: className = '',
onchange
onchange,
}: {
checked?: boolean;
disabled?: boolean;
@@ -4,15 +4,16 @@
const kindClass: Record<string, string> = {
info: 'border-surface-200 bg-surface-50 text-surface-800',
success: 'border-success-200 bg-success-50 text-success-800',
error: 'border-error-200 bg-error-50 text-error-800'
error: 'border-error-200 bg-error-50 text-error-800',
};
</script>
<div class="pointer-events-none fixed inset-x-0 top-0 z-[100] flex flex-col items-end gap-2 p-4">
<div class="pointer-events-none fixed inset-x-0 top-0 z-100 flex flex-col items-end gap-2 p-4">
{#each $toasts as t (t.id)}
<div
class="pointer-events-auto flex max-w-sm items-start gap-3 border px-4 py-3 text-sm shadow-[4px_4px_0_rgb(15_23_42_/_0.12)] {kindClass[t.kind] ??
kindClass.info}"
class="pointer-events-auto flex max-w-sm items-start gap-3 border px-4 py-3 text-sm shadow-[4px_4px_0_rgb(15_23_42/0.12)] {kindClass[
t.kind
] ?? kindClass.info}"
role="status"
>
<p class="min-w-0 flex-1">{t.message}</p>
+3 -3
View File
@@ -15,7 +15,7 @@ export const TOOL_OPTIONS: ToolOption[] = [
{ id: 'send_file', label: '发送文件(飞书)', group: '飞书' },
{ id: 'feishu_read_context', label: '读飞书上下文', group: '飞书' },
{ id: 'feishu_download_resource', label: '下载飞书资源', group: '飞书' },
{ id: 'request_approval', label: '请求审批', group: '飞书' }
{ id: 'request_approval', label: '请求审批', group: '飞书' },
];
/** 组织成员角色(接口枚举保持英文,界面用 orgRoleLabel */
@@ -25,7 +25,7 @@ export type OrgRole = (typeof ORG_ROLES)[number];
export const ORG_ROLE_LABELS: Record<OrgRole, string> = {
OWNER: '所有者',
ADMIN: '管理员',
MEMBER: '成员'
MEMBER: '成员',
};
/** 项目团队授权角色(接口枚举保持英文,界面用 permissionRoleLabel */
@@ -35,5 +35,5 @@ export type PermissionRole = (typeof PERMISSION_ROLES)[number];
export const PERMISSION_ROLE_LABELS: Record<PermissionRole, string> = {
READ: '只读',
EDIT: '编辑',
MANAGE: '管理'
MANAGE: '管理',
};
+2 -7
View File
@@ -1,9 +1,4 @@
import {
ORG_ROLE_LABELS,
PERMISSION_ROLE_LABELS,
type OrgRole,
type PermissionRole
} from './constants';
import { ORG_ROLE_LABELS, PERMISSION_ROLE_LABELS, type OrgRole, type PermissionRole } from './constants';
export function fmtDate(iso: string): string {
if (!iso) return '—';
@@ -14,7 +9,7 @@ export function fmtDate(iso: string): string {
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
});
}
+2 -2
View File
@@ -10,7 +10,7 @@ interface SessionState {
export const session = writable<SessionState>({
loading: true,
me: null,
error: null
error: null,
});
export async function loadSession(): Promise<void> {
@@ -27,7 +27,7 @@ export async function loadSession(): Promise<void> {
session.set({
loading: false,
me: null,
error: err instanceof Error ? err.message : String(err)
error: err instanceof Error ? err.message : String(err),
});
}
}
+25 -23
View File
@@ -24,7 +24,7 @@
{ key: 'teams', label: '团队', icon: 'teams' as const },
{ key: 'projects', label: '项目', icon: 'projects' as const },
{ key: 'provider', label: '供应方', icon: 'provider' as const },
{ key: 'feishu', label: '飞书', icon: 'feishu' as const }
{ key: 'feishu', label: '飞书', icon: 'feishu' as const },
];
function isAdmin(org: OrgMembership): boolean {
@@ -92,7 +92,7 @@
function orgSelectItems(list: OrgMembership[]) {
return list.map((o) => ({
value: o.slug,
label: `${o.name} · ${orgRoleLabel(o.role)}`
label: `${o.name} · ${orgRoleLabel(o.role)}`,
}));
}
@@ -142,7 +142,11 @@
{:else if $session.error}
<div class="saas-status-panel">
<div class="saas-status-card">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center border border-error-300 bg-error-100 text-error-700 font-bold">!</div>
<div
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center border border-error-300 bg-error-100 text-error-700 font-bold"
>
!
</div>
<h2 class="mb-1 text-lg font-semibold">无法连接到后端</h2>
<p class="mb-5 text-sm text-surface-700">{$session.error}</p>
<button class="saas-btn-primary" onclick={() => loadSession()}>重试</button>
@@ -151,7 +155,9 @@
{:else if !$session.me}
<div class="saas-status-panel">
<div class="saas-status-card">
<div class="mx-auto mb-5 flex h-12 w-12 items-center justify-center border border-primary-700 bg-primary-600 text-white font-bold">
<div
class="mx-auto mb-5 flex h-12 w-12 items-center justify-center border border-primary-700 bg-primary-600 text-white font-bold"
>
CPH
</div>
<h1 class="text-xl font-semibold">Curriculum Project Hub</h1>
@@ -177,7 +183,9 @@
{mobileNavOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}"
>
<div class="flex items-center gap-2.5 px-4 py-4">
<div class="flex h-9 w-9 items-center justify-center border border-primary-700 bg-primary-600 text-xs font-bold tracking-wide text-white">
<div
class="flex h-9 w-9 items-center justify-center border border-primary-700 bg-primary-600 text-xs font-bold tracking-wide text-white"
>
CPH
</div>
<div class="min-w-0">
@@ -191,17 +199,14 @@
<Icon name="org" class="h-3.5 w-3.5" />
组织
</div>
<SelectField
items={orgSelectItems(me.organizations)}
value={org.slug}
onchange={switchOrg}
/>
<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">
<p class="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-surface-600">工作台</p>
{#each navItems as item}
{@const active = activeKey() === item.key || (item.key === 'overview' && (activeKey() === '' || activeKey() === 'overview'))}
{@const active =
activeKey() === item.key || (item.key === 'overview' && (activeKey() === '' || activeKey() === 'overview'))}
<a href={navHref(item.key)} class="saas-nav-item" data-active={active ? 'true' : 'false'}>
<Icon name={item.icon} class="h-4 w-4 shrink-0 opacity-80" />
<span>{item.label}</span>
@@ -214,7 +219,9 @@
{#if me.user.avatarUrl}
<img src={me.user.avatarUrl} alt="" class="h-8 w-8 object-cover" />
{:else}
<div class="flex h-8 w-8 items-center justify-center border border-primary-300 bg-primary-100 text-xs font-semibold text-primary-800">
<div
class="flex h-8 w-8 items-center justify-center border border-primary-300 bg-primary-100 text-xs font-semibold text-primary-800"
>
{me.user.displayName.slice(0, 1)}
</div>
{/if}
@@ -238,7 +245,7 @@
<header class="saas-topbar">
<button
type="button"
class="saas-btn-ghost !px-2 md:hidden"
class="saas-btn-ghost px-2! md:hidden"
onclick={() => (mobileNavOpen = !mobileNavOpen)}
aria-label="打开导航"
>
@@ -270,17 +277,14 @@
<h2 class="mb-2 text-lg font-semibold">无权访问管理后台</h2>
<p class="mb-3 text-sm text-surface-700">
组织 <strong>{denied.name}</strong>/{denied.slug})中你的角色是
<span class="saas-badge-neutral mx-1">{orgRoleLabel(denied.role)}</span>
需要<strong>所有者</strong><strong>管理员</strong>
<span class="saas-badge-neutral mx-1">{orgRoleLabel(denied.role)}</span> 需要<strong>所有者</strong><strong
>管理员</strong
>
</p>
{#if memberships().length > 1}
<p class="saas-label text-left mb-1.5">切换到其他组织</p>
<div class="mb-4">
<SelectField
items={orgSelectItems(memberships())}
value={denied.slug}
onchange={switchOrg}
/>
<SelectField items={orgSelectItems(memberships())} value={denied.slug} onchange={switchOrg} />
</div>
{/if}
<button class="saas-btn-ghost" onclick={handleLogout}>退出登录</button>
@@ -311,9 +315,7 @@
<div class="saas-status-panel">
<div class="saas-status-card">
<h2 class="mb-2 text-lg font-semibold">未加入组织</h2>
<p class="mb-3 text-sm text-surface-700">
飞书账号已登录,但当前账号尚未加入任何组织。
</p>
<p class="mb-3 text-sm text-surface-700">飞书账号已登录,但当前账号尚未加入任何组织。</p>
<p class="mb-5 text-left text-xs text-surface-600">
open_id
<code class="break-all font-mono text-surface-600">{$session.me!.user.feishuOpenId}</code>
@@ -11,9 +11,7 @@
import { toastError, toastSuccess } from '$lib/toast';
let orgSlug = $derived(page.params.slug ?? '');
let org = $derived(
$session.me?.organizations.find((o) => o.slug === orgSlug) as OrgMembership | undefined
);
let org = $derived($session.me?.organizations.find((o) => o.slug === orgSlug) as OrgMembership | undefined);
let settings = $state<{ membersCanCreateProjects: boolean } | null>(null);
let usage = $state<Awaited<ReturnType<typeof api.usage>> | null>(null);
@@ -82,15 +80,9 @@
<div class="saas-card-pad sm:col-span-2">
<p class="saas-section-title mb-1">项目自助创建策略</p>
<p class="saas-muted mb-4">
开启后,普通老师可在飞书群自助创建项目;关闭后仅所有者与管理员可建。
</p>
<p class="saas-muted mb-4">开启后,普通老师可在飞书群自助创建项目;关闭后仅所有者与管理员可建。</p>
<div class="flex items-center gap-3 border border-surface-300 bg-surface-100 px-4 py-3">
<SwitchControl
checked={settings.membersCanCreateProjects}
disabled={saving}
onchange={setMembersCanCreate}
/>
<SwitchControl checked={settings.membersCanCreateProjects} disabled={saving} onchange={setMembersCanCreate} />
<div>
<div class="text-sm font-medium text-surface-900">允许成员自助创建项目</div>
<div class="text-xs text-surface-600">普通成员在飞书群中自助建项</div>
@@ -139,9 +131,7 @@
<tr>
<td class="font-medium">{p.projectName}</td>
<td class="tabular-nums">{fmtNum(p.runCount)}</td>
<td class="tabular-nums text-surface-600"
>{fmtNum(p.inputTokens)} / {fmtNum(p.outputTokens)}</td
>
<td class="tabular-nums text-surface-600">{fmtNum(p.inputTokens)} / {fmtNum(p.outputTokens)}</td>
<td class="tabular-nums">{fmtCost(p.costUsd)}</td>
</tr>
{/each}
@@ -158,12 +158,7 @@
</div>
<div>
<Label.Root class="saas-label" for="verification-token">Verification Token(可选)</Label.Root>
<input
id="verification-token"
class="saas-input"
type="password"
bind:value={verificationToken}
/>
<input id="verification-token" class="saas-input" type="password" bind:value={verificationToken} />
</div>
<div>
<Label.Root class="saas-label" for="encrypt-key">Encrypt Key(可选)</Label.Root>
@@ -43,11 +43,9 @@
const m = await api.addMember(slug, {
feishuOpenId: newOpenId.trim(),
role: newRole,
...(newName.trim() ? { displayName: newName.trim() } : {})
...(newName.trim() ? { displayName: newName.trim() } : {}),
});
members = [...members, m].sort(
(a, b) => a.role.localeCompare(b.role) || a.createdAt.localeCompare(b.createdAt)
);
members = [...members, m].sort((a, b) => a.role.localeCompare(b.role) || a.createdAt.localeCompare(b.createdAt));
newOpenId = '';
newName = '';
toastSuccess('成员已添加');
@@ -140,7 +138,7 @@
</div>
</td>
<td class="font-mono text-xs text-surface-700">{m.feishuOpenId}</td>
<td class="min-w-[9rem]">
<td class="min-w-36">
<SelectField
items={roleItems}
value={m.role}
@@ -151,7 +149,7 @@
</td>
<td class="text-surface-700">{fmtDate(m.createdAt)}</td>
<td class="text-right">
<button class="saas-btn-danger !py-1 text-sm" onclick={() => revoke(m)}>移除</button>
<button class="saas-btn-danger py-1! text-sm" onclick={() => revoke(m)}>移除</button>
</td>
</tr>
{/each}
@@ -42,7 +42,7 @@
try {
await api.createFolder(slug, {
name: folderName.trim(),
...(folderParent ? { parentId: folderParent } : {})
...(folderParent ? { parentId: folderParent } : {}),
});
folderName = '';
folderParent = '';
@@ -59,7 +59,7 @@
try {
const res = await api.createProject(slug, {
name: projectName.trim(),
...(projectFolder ? { folderId: projectFolder } : {})
...(projectFolder ? { folderId: projectFolder } : {}),
});
projectName = '';
projectFolder = '';
@@ -85,10 +85,7 @@
function folderItems() {
if (!data) return [{ value: '', label: '(根)' }];
return [
{ value: '', label: '(根)' },
...data.folders.map((f) => ({ value: f.id, label: folderPath(f) }))
];
return [{ value: '', label: '(根)' }, ...data.folders.map((f) => ({ value: f.id, label: folderPath(f) }))];
}
$effect(() => {
@@ -6,7 +6,7 @@
type TeamAccessEntry,
type TeamRow,
type SessionSummary,
type ExplorerData
type ExplorerData,
} from '$lib/api';
import { fmtDate, permissionRoleLabel } from '$lib/format';
import { PERMISSION_ROLES, PERMISSION_ROLE_LABELS } from '$lib/constants';
@@ -43,7 +43,7 @@
api.teamAccess(slug, projectId),
api.sessions(slug, projectId),
api.teams(slug),
api.explorer(slug)
api.explorer(slug),
]);
proj = p;
access = a.access;
@@ -128,17 +128,11 @@
function folderItems() {
const items = [{ value: '', label: '(根)' }];
if (!explorer) return items;
return [
...items,
...explorer.folders.map((f) => ({ value: f.id, label: f.name }))
];
return [...items, ...explorer.folders.map((f) => ({ value: f.id, label: f.name }))];
}
function teamItems() {
return [
{ value: '', label: '选择团队…' },
...teams.map((t) => ({ value: t.id, label: `${t.name}${t.slug}` }))
];
return [{ value: '', label: '选择团队…' }, ...teams.map((t) => ({ value: t.id, label: `${t.name}${t.slug}` }))];
}
$effect(() => {
@@ -152,7 +146,10 @@
<ErrorBanner message={error} onretry={load} />
{:else if proj}
<div class="mb-2">
<a href={`/admin/org/${slug}/projects`} class="inline-flex items-center gap-1 text-sm text-surface-700 hover:text-primary-600">
<a
href={`/admin/org/${slug}/projects`}
class="inline-flex items-center gap-1 text-sm text-surface-700 hover:text-primary-600"
>
← 返回项目列表
</a>
</div>
@@ -160,11 +157,11 @@
{@const detail = proj}
<PageHeader title={detail.name} description={`项目是权限边界;通过团队授予 ${roleChain}。`}>
{#snippet actions()}
<button class="saas-btn-secondary !py-1.5 text-sm" onclick={rename}>重命名</button>
<button class="saas-btn-secondary py-1.5! text-sm" onclick={rename}>重命名</button>
{#if detail.binding}
<button class="saas-btn-secondary !py-1.5 text-sm" onclick={archiveBinding}>解绑飞书群</button>
<button class="saas-btn-secondary py-1.5! text-sm" onclick={archiveBinding}>解绑飞书群</button>
{/if}
<button class="saas-btn-danger !py-1.5 text-sm" onclick={archiveProject}>归档</button>
<button class="saas-btn-danger py-1.5! text-sm" onclick={archiveProject}>归档</button>
{/snippet}
</PageHeader>
@@ -204,7 +201,7 @@
{#if explorer}
<div class="mt-5 flex flex-wrap items-end gap-2 border-t border-surface-100 pt-4">
<div class="min-w-[12rem] flex-1">
<div class="min-w-48 flex-1">
<p class="saas-label">移动到文件夹</p>
<SelectField items={folderItems()} bind:value={moveFolder} />
</div>
@@ -242,7 +239,7 @@
<td class="font-mono text-xs">/{g.teamSlug}</td>
<td><span class="saas-badge-primary">{permissionRoleLabel(g.role)}</span></td>
<td class="text-right">
<button class="saas-btn-danger !py-1 text-xs" onclick={() => revoke(g)}>撤销</button>
<button class="saas-btn-danger py-1! text-xs" onclick={() => revoke(g)}>撤销</button>
</td>
</tr>
{/each}
@@ -62,7 +62,7 @@
saving = true;
const body: { baseUrl: string; authToken: string; anthropicApiKey?: string } = {
baseUrl: url,
authToken: token
authToken: token,
};
const key = anthropicApiKey.trim();
if (key !== '') body.anthropicApiKey = key;
@@ -122,10 +122,7 @@
<td class="text-surface-600">{fmtDate(row.updatedAt)}</td>
<td>
{#if row.mode === 'BYOK'}
<button
class="saas-btn-ghost !px-2 !py-1 text-xs"
onclick={() => startRotate(row)}>轮换</button
>
<button class="saas-btn-ghost px-2! py-1! text-xs" onclick={() => startRotate(row)}>轮换</button>
{:else}
<span class="text-xs text-surface-500">平台管理</span>
{/if}
@@ -146,21 +143,11 @@
<div class="grid gap-5">
<div>
<Label.Root class="saas-label" for="provider-id">供应方 ID</Label.Root>
<input
id="provider-id"
class="saas-input font-mono text-sm"
bind:value={providerId}
placeholder="openrouter"
/>
<input id="provider-id" class="saas-input font-mono text-sm" bind:value={providerId} placeholder="openrouter" />
</div>
<div>
<Label.Root class="saas-label" for="base-url">接口地址</Label.Root>
<input
id="base-url"
class="saas-input"
placeholder="https://openrouter.ai/api"
bind:value={baseUrl}
/>
<input id="base-url" class="saas-input" placeholder="https://openrouter.ai/api" bind:value={baseUrl} />
</div>
<div>
<Label.Root class="saas-label" for="auth-token">访问令牌</Label.Root>
@@ -52,7 +52,7 @@
await api.createTeam(slug, {
slug: teamSlug,
name: newName.trim(),
...(newDesc.trim() ? { description: newDesc.trim() } : {})
...(newDesc.trim() ? { description: newDesc.trim() } : {}),
});
newSlug = '';
newName = '';
@@ -165,10 +165,10 @@
<span class="font-mono text-xs text-surface-600">/{t.slug}</span>
<span class="saas-badge-neutral">{t.memberCount} 成员</span>
<div class="ml-auto flex flex-wrap gap-2">
<Collapsible.Trigger class="saas-btn-secondary !py-1.5 text-sm">
<Collapsible.Trigger class="saas-btn-secondary py-1.5! text-sm">
{expandedId === t.id ? '收起' : '管理成员'}
</Collapsible.Trigger>
<button type="button" class="saas-btn-danger !py-1.5 text-sm" onclick={() => archiveTeam(t)}>
<button type="button" class="saas-btn-danger py-1.5! text-sm" onclick={() => archiveTeam(t)}>
归档
</button>
</div>
@@ -213,7 +213,7 @@
<td class="font-mono text-xs">{m.feishuOpenId}</td>
<td class="text-surface-700">{fmtDate(m.createdAt)}</td>
<td class="text-right">
<button class="saas-btn-danger !py-1 text-xs" onclick={() => revokeMember(t, m)}>
<button class="saas-btn-danger py-1! text-xs" onclick={() => revokeMember(t, m)}>
移除
</button>
</td>
+32 -11
View File
@@ -9,8 +9,8 @@
/* Flat industrial: zero radius, higher-contrast surfaces, CJK-first type */
@theme {
--font-sans:
'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Inter', ui-sans-serif,
system-ui, -apple-system, 'Segoe UI', sans-serif;
'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Inter', ui-sans-serif, system-ui,
-apple-system, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--radius-none: 0;
@@ -30,7 +30,9 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: 'kern' 1, 'liga' 1;
font-feature-settings:
'kern' 1,
'liga' 1;
/* Prefer readable CJK metrics over Latin optical sizing */
text-size-adjust: 100%;
}
@@ -275,7 +277,10 @@
font-weight: 500;
color: var(--color-surface-700);
border-left: 2px solid transparent;
transition: color 0.1s, background-color 0.1s, border-color 0.1s;
transition:
color 0.1s,
background-color 0.1s,
border-color 0.1s;
}
.saas-nav-item:hover {
@@ -378,7 +383,9 @@
font-size: 0.875rem;
line-height: 1.5;
outline: none;
transition: border-color 0.1s, box-shadow 0.1s;
transition:
border-color 0.1s,
box-shadow 0.1s;
}
.saas-input::placeholder,
@@ -411,7 +418,9 @@
font-size: 0.875rem;
line-height: 1.5;
outline: none;
transition: border-color 0.1s, box-shadow 0.1s;
transition:
border-color 0.1s,
box-shadow 0.1s;
cursor: pointer;
text-align: left;
}
@@ -489,7 +498,11 @@
letter-spacing: 0.01em;
border: 1px solid transparent;
cursor: pointer;
transition: background-color 0.1s, color 0.1s, border-color 0.1s, opacity 0.1s;
transition:
background-color 0.1s,
color 0.1s,
border-color 0.1s,
opacity 0.1s;
}
.saas-btn-primary:disabled,
@@ -556,7 +569,9 @@
background: var(--color-surface-50);
color: white;
cursor: pointer;
transition: background 0.1s, border-color 0.1s;
transition:
background 0.1s,
border-color 0.1s;
}
.saas-checkbox[data-state='checked'] {
@@ -566,7 +581,9 @@
.saas-checkbox:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--color-surface-50), 0 0 0 4px var(--color-primary-600);
box-shadow:
0 0 0 2px var(--color-surface-50),
0 0 0 4px var(--color-primary-600);
}
.saas-checkbox[data-disabled] {
@@ -587,7 +604,9 @@
background: var(--color-surface-300);
padding: 0.125rem;
cursor: pointer;
transition: background 0.1s, border-color 0.1s;
transition:
background 0.1s,
border-color 0.1s;
}
.saas-switch[data-state='checked'] {
@@ -597,7 +616,9 @@
.saas-switch:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--color-surface-50), 0 0 0 4px var(--color-primary-600);
box-shadow:
0 0 0 2px var(--color-surface-50),
0 0 0 4px var(--color-primary-600);
}
.saas-switch[data-disabled] {
+3 -3
View File
@@ -10,9 +10,9 @@ const config = {
assets: 'build',
fallback: 'index.html',
precompress: false,
strict: false
})
}
strict: false,
}),
},
};
export default config;
+3 -3
View File
@@ -7,7 +7,7 @@ export default defineConfig({
server: {
proxy: {
'/api': 'http://127.0.0.1:8788',
'/auth': 'http://127.0.0.1:8788'
}
}
'/auth': 'http://127.0.0.1:8788',
},
},
});