style(settings): convert dropdown settings menu to full settings page

This commit is contained in:
tianma committed 2026-06-28 00:49:59 +08:00
1 parent c0868dc5cb
commit 8306d8ef3a
3 files changed
+136 -97

No files matched your search

+13 -51
View File
@@ -384,57 +384,19 @@ export const NotebookPane = ({
onSyncNow={onSyncQueuedChanges}
/>
<div className="mt-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex w-full items-center gap-3 rounded-lg border border-slate-200 bg-white p-2 text-left hover:bg-slate-50 hover:border-slate-300 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#809f75] to-[#526d49] text-sm font-semibold text-white uppercase shadow-[0_2px_8px_rgba(98,127,88,0.15)]">
{user?.username?.charAt(0) ?? "L"}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-slate-800 leading-tight">{user?.username ?? "本地用户"}</div>
<div className="truncate text-[10px] font-medium text-slate-400 uppercase tracking-wider mt-0.5">{isOnline ? "在线" : "离线"}</div>
</div>
<Settings className="h-4 w-4 shrink-0 text-slate-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 bg-white border border-slate-200 rounded-md py-1 shadow-md" align="start" side="top" sideOffset={8}>
<div className="px-3 py-2 border-b border-slate-100 mb-1">
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider">个人中心</div>
<div className="text-sm font-semibold text-slate-900 truncate mt-0.5">{user?.username ?? "本地用户"}</div>
</div>
<DropdownMenuItem
className="flex items-center justify-between px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
onSelect={(event) => event.preventDefault()}
>
<span className="select-none font-medium">压缩笔记内图片</span>
<Switch
checked={imageCompressionEnabled}
onCheckedChange={onImageCompressionChange}
aria-label="是否压缩笔记内图片"
/>
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
onClick={onOpenSettings}
>
<KeyRound className="h-4 w-4 text-slate-500" />
<span className="font-medium">MCP / API 设置</span>
</DropdownMenuItem>
{authRequired && (
<>
<DropdownMenuSeparator className="my-1 border-t border-slate-100" />
<DropdownMenuItem
className="flex items-center gap-2 px-3 py-2 text-sm text-rose-600 hover:bg-rose-50 hover:text-rose-700 cursor-pointer outline-none"
disabled={isLoggingOut}
onClick={onLogout}
>
<LogOut className="h-4 w-4" />
<span className="font-medium">{isLoggingOut ? "退出中..." : "退出登录"}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<button
onClick={onOpenSettings}
className="flex w-full items-center gap-3 rounded-lg border border-slate-200 bg-white p-2 text-left hover:bg-slate-50 hover:border-slate-300 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#809f75] to-[#526d49] text-sm font-semibold text-white uppercase shadow-[0_2px_8px_rgba(98,127,88,0.15)]">
{user?.username?.charAt(0) ?? "L"}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-slate-800 leading-tight">{user?.username ?? "本地用户"}</div>
<div className="truncate text-[10px] font-medium text-slate-400 uppercase tracking-wider mt-0.5">{isOnline ? "在线" : "离线"}</div>
</div>
<Settings className="h-4 w-4 shrink-0 text-slate-400" />
</button>
</div>
</footer>
</div>
+114 -45
View File
@@ -1,19 +1,34 @@
import { useState, type FormEvent } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { KeyRound, ShieldCheck, Trash2, ChevronLeft, Plus } from "lucide-react";
import { KeyRound, ShieldCheck, Trash2, ChevronLeft, Plus, User, Image, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { api } from "@/lib/api";
import { cn, formatDateTime } from "@/lib/utils";
import { AppConfirmDialog } from "./dialogs/ConfirmDialogs";
import type { ApiToken } from "@edgeever/shared";
import type { ApiToken, AuthUser } from "@edgeever/shared";
const DEFAULT_TOKEN_SCOPES = ["read:notebooks", "read:memos", "read:tags"];
interface SettingsPaneProps {
user: AuthUser | null;
onClose: () => void;
imageCompressionEnabled: boolean;
onImageCompressionChange: (enabled: boolean) => void;
onLogout: () => void;
isLoggingOut: boolean;
authRequired: boolean;
}
export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
export const SettingsPane = ({
user,
onClose,
imageCompressionEnabled,
onImageCompressionChange,
onLogout,
isLoggingOut,
authRequired,
}: SettingsPaneProps) => {
const queryClient = useQueryClient();
const [name, setName] = useState("MCP Agent");
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(() => new Set(DEFAULT_TOKEN_SCOPES));
@@ -94,55 +109,90 @@ export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
</Button>
<div className="min-w-0">
<h1 className="flex items-center gap-2 text-base font-bold text-slate-900 leading-tight">
<KeyRound className="h-4.5 w-4.5 text-emerald-700" />
API & MCP 设置
<User className="h-4.5 w-4.5 text-emerald-700" />
个人中心 & 设置
</h1>
<p className="mt-0.5 text-[10px] font-medium text-slate-400 uppercase tracking-wider">
管理用于本地 MCP 客户端或第三方 API 访问的身份凭证
管理个人偏好设置、第三方 API 凭证及登录会话
</p>
</div>
</div>
</header>
<div className="flex-1 overflow-y-auto p-6 bg-slate-50/30">
<div className="mx-auto max-w-3xl space-y-6">
{createdToken && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50/70 p-4 shadow-sm animate-fade-in">
<div className="mb-2 flex items-center gap-2 text-sm font-bold text-emerald-900">
<ShieldCheck className="h-5 w-5 text-emerald-700" />
API Token 已成功生成
</div>
<div className="flex gap-2">
<input
className="h-10 min-w-0 flex-1 rounded-lg border border-emerald-200 bg-white px-3.5 font-mono text-xs text-slate-900 outline-none focus:ring-2 focus:ring-emerald-500/20"
readOnly
value={createdToken.token}
/>
<Button
size="md"
variant="solid"
className="bg-emerald-600 hover:bg-emerald-700 text-white"
type="button"
onClick={() => void navigator.clipboard?.writeText(createdToken.token)}
>
复制 Token
</Button>
</div>
<p className="mt-2 text-xs font-medium text-emerald-800">
⚠️ 安全警告:为了您的账户安全,明文 Token 仅在此处展示一次,关闭后将无法再次找回!
</p>
<div className="mx-auto max-w-2xl space-y-6">
{/* Section 1: User Profile */}
<div className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-[#809f75] to-[#526d49] text-xl font-bold text-white uppercase shadow-md">
{user?.username?.charAt(0) ?? "U"}
</div>
)}
<div className="min-w-0 flex-1">
<div className="text-base font-bold text-slate-800">{user?.username ?? "本地用户"}</div>
<div className="mt-1 flex items-center gap-1.5 text-xs text-slate-400 font-medium">
<span className="h-2 w-2 rounded-full bg-emerald-500 inline-block"></span>
在线工作区已连接
</div>
</div>
</div>
{/* Section 2: Preferences */}
<div className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
<Plus className="h-4 w-4 text-emerald-700" />
新建 API Token
<Image className="h-4.5 w-4.5 text-emerald-700" />
偏好设置
</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="flex items-center justify-between gap-4 p-3 rounded-lg bg-slate-50/60 border border-slate-100">
<div className="min-w-0">
<div className="text-sm font-semibold text-slate-800">压缩笔记内图片</div>
<div className="text-xs text-slate-400 mt-0.5">上传大图时在本地进行无损或有损压缩以节省资源占用</div>
</div>
<Switch
checked={imageCompressionEnabled}
onCheckedChange={onImageCompressionChange}
aria-label="是否压缩笔记内图片"
/>
</div>
</div>
{/* Section 3: API & MCP */}
<div className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm space-y-5">
<h2 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<KeyRound className="h-4.5 w-4.5 text-emerald-700" />
API & MCP 授权设置
</h2>
{createdToken && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50/70 p-4 shadow-sm animate-fade-in">
<div className="mb-2 flex items-center gap-2 text-sm font-bold text-emerald-900">
<ShieldCheck className="h-5 w-5 text-emerald-700" />
API Token 已成功生成
</div>
<div className="flex gap-2">
<input
className="h-10 min-w-0 flex-1 rounded-lg border border-emerald-200 bg-white px-3.5 font-mono text-xs text-slate-900 outline-none focus:ring-2 focus:ring-emerald-500/20"
readOnly
value={createdToken.token}
/>
<Button
size="md"
variant="solid"
className="bg-emerald-600 hover:bg-emerald-700 text-white"
type="button"
onClick={() => void navigator.clipboard?.writeText(createdToken.token)}
>
复制 Token
</Button>
</div>
<p className="mt-2 text-xs font-medium text-emerald-800">
⚠️ 安全警告:明文 Token 仅在此处展示一次,关闭后将无法再次找回!
</p>
</div>
)}
<form className="space-y-4 rounded-lg bg-slate-50/60 border border-slate-100 p-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 sm:flex-row">
<input
className="h-10 min-w-0 flex-1 rounded-lg border border-slate-200 bg-slate-50/50 px-3.5 text-sm outline-none transition duration-150 focus:border-[#627f58] focus:bg-white focus:ring-4 focus:ring-[#627f58]/10"
className="h-10 min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-3.5 text-sm outline-none transition duration-150 focus:border-[#627f58] focus:ring-4 focus:ring-[#627f58]/10"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Token 用途描述,例如: Cline Agent"
@@ -150,11 +200,11 @@ export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
<Button
size="md"
variant="solid"
className="h-10 bg-[#627f58] hover:bg-[#526d49] text-white"
className="h-10 bg-[#627f58] hover:bg-[#526d49] text-white shrink-0"
type="submit"
disabled={createMutation.isPending}
>
<KeyRound className="h-4 w-4 mr-1.5" />
<Plus className="h-4 w-4 mr-1" />
生成 Token
</Button>
</div>
@@ -166,7 +216,7 @@ export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
<label
key={scope}
className={cn(
"flex min-h-10 items-center gap-3 rounded-lg border px-3 py-1 cursor-pointer transition-all duration-150",
"flex min-h-9 items-center gap-3 rounded-lg border px-3 py-1 cursor-pointer transition-all duration-150",
selectedScopes.has(scope)
? "border-[#627f58]/30 bg-[#f3f7f1]/50 text-[#526d49]"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50/50"
@@ -184,15 +234,13 @@ export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
</div>
</div>
</form>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 className="text-sm font-bold text-slate-800 mb-4">活跃的 Token 列表</h2>
<div className="space-y-3">
<span className="block text-xs font-semibold text-slate-500">活跃的 Token 列表:</span>
{tokensQuery.isLoading ? (
<div className="py-12 text-center text-sm text-slate-400">正在加载 Token 列表...</div>
<div className="py-8 text-center text-sm text-slate-400">正在加载 Token 列表...</div>
) : tokens.length === 0 ? (
<div className="rounded-lg border border-dashed border-slate-200 px-4 py-12 text-center text-sm text-slate-400">
<div className="rounded-lg border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-slate-400">
暂无活跃的 API Token
</div>
) : (
@@ -228,6 +276,27 @@ export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
)}
</div>
</div>
{/* Section 4: System / Danger zone */}
{authRequired && (
<div className="rounded-xl border border-rose-100 bg-rose-50/30 p-5 shadow-sm">
<h2 className="text-sm font-bold text-rose-900 mb-3 flex items-center gap-2">
<LogOut className="h-4.5 w-4.5 text-rose-700" />
会话管理
</h2>
<p className="text-xs text-slate-400 mb-4">退出登录将清理您在这台设备上的本地 session 状态。</p>
<Button
size="md"
variant="danger"
className="bg-rose-600 hover:bg-rose-700 text-white font-semibold shadow-sm"
disabled={isLoggingOut}
onClick={onLogout}
>
<LogOut className="h-4 w-4 mr-1.5" />
{isLoggingOut ? "安全退出中..." : "退出登录"}
</Button>
</div>
)}
</div>
</div>
+9 -1
View File
@@ -1551,7 +1551,15 @@ export const WorkspaceApp = ({
<section className={cn("min-h-0 min-w-0 bg-white lg:block", activePane === "editor" ? "block" : "hidden")}>
{rightView === "settings" ? (
<SettingsPane onClose={handleCloseSettings} />
<SettingsPane
user={user}
onClose={handleCloseSettings}
imageCompressionEnabled={imageCompressionEnabled}
onImageCompressionChange={setImageCompressionEnabled}
onLogout={onLogout}
isLoggingOut={isLoggingOut}
authRequired={authRequired}
/>
) : rightView === "assets" ? (
<AssetsPane onClose={handleCloseAssets} />
) : (