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, 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: {
+19 -7
View File
@@ -2,16 +2,16 @@
/** 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'
| 'members' | 'members'
| 'teams' | 'teams'
| 'projects' | 'projects'
| 'provider' | 'provider'
| 'feishu' | 'feishu'
| 'menu' | 'menu'
| 'logout' | 'logout'
| 'org' | 'org'
| 'chevron' | 'chevron'
@@ -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>
+3 -3
View File
@@ -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: '管理',
}; };
+2 -7
View File
@@ -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',
}); });
} }
+2 -2
View File
@@ -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),
}); });
} }
} }
+26 -24
View File
@@ -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 {
@@ -68,7 +68,7 @@
function navHref(key: string): string { function navHref(key: string): string {
const slug = currentOrg()?.slug ?? pickHomeOrg()?.slug; const slug = currentOrg()?.slug ?? pickHomeOrg()?.slug;
if (!slug) return '/'; if (!slug) return '/';
if (key === 'overview') return `/admin/org/${slug}`; if (key === 'overview') return `/admin/org/${slug}`;
return `/admin/org/${slug}/${key}`; return `/admin/org/${slug}/${key}`;
} }
@@ -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('成员已添加');
@@ -128,11 +126,11 @@
<td> <td>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
{#if m.avatarUrl} {#if m.avatarUrl}
<img src={m.avatarUrl} alt="" class="h-7 w-7 border border-surface-300 object-cover" /> <img src={m.avatarUrl} alt="" class="h-7 w-7 border border-surface-300 object-cover" />
{:else} {:else}
<div <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" 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)} {m.displayName.slice(0, 1)}
</div> </div>
{/if} {/if}
@@ -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>
+32 -11
View File
@@ -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] {
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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',
} },
} },
}); });