forked from EduCraft/curriculum-project-hub
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:
@@ -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: {
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
/** Inline nav icons for the admin shell. */
|
||||
let {
|
||||
name,
|
||||
class: className = 'h-4 w-4'
|
||||
class: className = 'h-4 w-4',
|
||||
}: {
|
||||
name:
|
||||
| 'overview'
|
||||
| 'members'
|
||||
| 'teams'
|
||||
| 'projects'
|
||||
| 'provider'
|
||||
| 'feishu'
|
||||
| 'menu'
|
||||
| 'provider'
|
||||
| 'feishu'
|
||||
| 'menu'
|
||||
| 'logout'
|
||||
| 'org'
|
||||
| 'chevron'
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '管理',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -68,7 +68,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}`;
|
||||
}
|
||||
@@ -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('成员已添加');
|
||||
@@ -128,11 +126,11 @@
|
||||
<td>
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if m.avatarUrl}
|
||||
<img src={m.avatarUrl} alt="" class="h-7 w-7 border border-surface-300 object-cover" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center border border-primary-300 bg-primary-100 text-xs font-semibold text-primary-800"
|
||||
>
|
||||
<img src={m.avatarUrl} alt="" class="h-7 w-7 border border-surface-300 object-cover" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center border border-primary-300 bg-primary-100 text-xs font-semibold text-primary-800"
|
||||
>
|
||||
{m.displayName.slice(0, 1)}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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>
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -10,9 +10,9 @@ const config = {
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: false
|
||||
})
|
||||
}
|
||||
strict: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user