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