style(settings): convert dropdown settings menu to full settings page
This commit is contained in:
1 parent
c0868dc5cb
commit
8306d8ef3a
3 files changed
+136
-97
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user