diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6b10a5b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "next/core-web-vitals", + "next/typescript" + ] +} diff --git a/app/admin/editor/editor-component.tsx b/app/admin/editor/editor-component.tsx index 21821df..bec759c 100644 --- a/app/admin/editor/editor-component.tsx +++ b/app/admin/editor/editor-component.tsx @@ -11,9 +11,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Label } from "@/components/ui/label" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" -import { IconLoader, IconDeviceFloppy, IconEye, IconArrowLeft } from "@tabler/icons-react" +import { IconLoader, IconDeviceFloppy, IconEye, IconArrowLeft, IconX, IconPlus } from "@tabler/icons-react" import { Checkbox } from "@/components/ui/checkbox" import { toast } from "sonner" +import { SidebarTrigger } from "@/components/ui/sidebar" const GET_BLOG = gql` query GetBlog($id: UUID!) { @@ -34,6 +35,28 @@ const GET_BLOG = gql` isActive createdAt updatedAt + tags { + id + name + slug + color + } + } + } +` + +const GET_TAGS_AND_CATEGORIES = gql` + query GetTagsAndCategories { + blogTags { + id + name + slug + color + } + blogCategories { + id + name + slug } } ` @@ -67,6 +90,7 @@ interface BlogData { excerpt: string content: any categoryId?: string + tagIds?: string[] status: 'draft' | 'published' | 'archived' featuredImage?: string metaTitle?: string @@ -75,6 +99,19 @@ interface BlogData { isActive: boolean } +interface Tag { + id: string + name: string + slug: string + color?: string +} + +interface Category { + id: string + name: string + slug: string +} + export default function EditorComponent() { const router = useRouter() const searchParams = useSearchParams() @@ -87,12 +124,15 @@ export default function EditorComponent() { excerpt: '', content: null, status: 'draft', + tagIds: [], isFeatured: false, isActive: true }) const [isLoading, setIsLoading] = React.useState(false) + const { data: tagsAndCategoriesData, loading: tagsAndCategoriesLoading } = useQuery(GET_TAGS_AND_CATEGORIES) + const { data: blogQuery, loading: blogLoading } = useQuery(GET_BLOG, { variables: { id: blogId }, skip: !isEditing, @@ -104,6 +144,7 @@ export default function EditorComponent() { excerpt: data.blog.excerpt || '', content: data.blog.content, categoryId: data.blog.categoryId, + tagIds: data.blog.tags?.map((tag: Tag) => tag.id) || [], status: data.blog.status || 'draft', featuredImage: data.blog.featuredImage, metaTitle: data.blog.metaTitle || '', @@ -149,6 +190,7 @@ export default function EditorComponent() { excerpt: blogData.excerpt || undefined, content: blogData.content, categoryId: blogData.categoryId || undefined, + tagIds: blogData.tagIds?.length ? blogData.tagIds : undefined, status: publishStatus || blogData.status, featuredImage: blogData.featuredImage || undefined, metaTitle: blogData.metaTitle || undefined, @@ -187,7 +229,7 @@ export default function EditorComponent() { setBlogData(prev => ({ ...prev, content })) } - if (blogLoading) { + if (blogLoading || tagsAndCategoriesLoading) { return (
@@ -195,6 +237,9 @@ export default function EditorComponent() { ) } + const availableTags = tagsAndCategoriesData?.blogTags || [] + const availableCategories = tagsAndCategoriesData?.blogCategories || [] + return (
{/* Main Editor Area */} @@ -203,6 +248,7 @@ export default function EditorComponent() {
+ + + ) + })} +
+ )} + + {/* Available Tags */} +
+ +
+ {availableTags.map((tag: Tag) => ( +
+ { + setBlogData(prev => { + const currentTags = prev.tagIds || [] + if (checked) { + return { + ...prev, + tagIds: [...currentTags, tag.id] + } + } else { + return { + ...prev, + tagIds: currentTags.filter(id => id !== tag.id) + } + } + }) + }} + /> +
+
+
+ + + {/* Publish Section */} diff --git a/app/app-sidebar.tsx b/app/app-sidebar.tsx index e84d23a..065f6c0 100644 --- a/app/app-sidebar.tsx +++ b/app/app-sidebar.tsx @@ -134,39 +134,25 @@ function UserAvatar() { ⌘P - - - - Account - ⌘A - - - - - - Notifications - ⌘N - - - - + {/* */} + {/* Upgrade to Pro ⌘U - - + */} + {/* Billing ⌘B - - - + */} + {/* */} + {/* @@ -176,7 +162,7 @@ function UserAvatar() { API Documentation - + */} Log out @@ -203,21 +189,21 @@ function UserAvatar() { export function AppSidebar({ ...props }: React.ComponentProps) { const router = useRouter() const mainNavItems = [ - { - icon: Home, - label: "Home", - onClick: () => console.log("Navigate to home") - }, + // { + // icon: Home, + // label: "Home", + // onClick: () => console.log("Navigate to home") + // }, { icon: Book, - label: "Docs", + label: "Blogs", onClick: () => router.push("/blog") }, - { - icon: Plus, - label: "New", - onClick: () => console.log("Create new project") - } + // { + // icon: Plus, + // label: "New", + // onClick: () => console.log("Create new project") + // } ] return ( diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx index 969bbc8..c3fce15 100644 --- a/app/blog/layout.tsx +++ b/app/blog/layout.tsx @@ -1,13 +1,19 @@ +import { Command } from "lucide-react" import { Navigation } from "../nav" +import FooterSection from "@/components/footer" export default function Layout({ children }: { children: React.ReactNode }) { return (
-
+
+ + +
+
{children}
+
- ) } \ No newline at end of file diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 7cd5ead..8fdebd0 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,50 +1,194 @@ "use client" -import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { gql, useQuery } from "@apollo/client"; import Link from "next/link"; -import { useEffect } from "react"; +import { formatDistanceToNow } from "date-fns"; const BLOG = gql` query Blog { - - blogs { + blogs(filter: { + isActive: true + status: "published" + }, + sort: { + field: "createdAt" + direction: "DESC" + } + ) { items { id title excerpt slug + createdAt + createdBy + categoryId + featuredImage + } + } + } +` + +const INFO = gql` + query Info($tagId: String!, $categoryId: String!, $authorId: String!) { + blogTag(id: $tagId) { + name + slug + color + } + + blogCategory(id: $categoryId) { + name + slug + } + + userWithGroups(id: $authorId) { + user { + username } } - } ` export default function Blog() { - const { data, loading, error } = useQuery(BLOG); - const items = data?.blogs?.items || []; - return
-

Blog

- {loading &&
Loading...
} - {error &&
Error: {error.message}
} - {!loading && items.length === 0 &&
No blogs found
} - {items.map((blog: any) => ( - - ))} -
; + if (loading) { + return ( +
+
+
Loading...
+
+
+ ); + } + + if (error) { + return ( +
+
+
Error: {error.message}
+
+
+ ); + } + + return ( +
+
+
+

+ Latest articles +

+
+ + {items.length === 0 ? ( +
No articles found
+ ) : ( +
+ {items.map((blog: any, index: number) => ( + + ))} +
+ )} +
+
+ ); } -function BlogItem({ blog }: { blog: any }) { - return
+interface BlogItemProps { + blog: { + id: string; + title: string; + excerpt: string; + slug: string; + createdAt: string; + createdBy: string; + categoryId: string; + featuredImage?: string; + }; + featured?: boolean; +} + +function BlogItem({ blog, featured = false }: BlogItemProps) { + const timeAgo = formatDistanceToNow(new Date(blog.createdAt), { addSuffix: true }); + + // 使用 INFO 查询获取详细信息 + const { data: infoData, loading: infoLoading } = useQuery(INFO, { + variables: { + tagId: blog.categoryId || "", + categoryId: blog.categoryId, + authorId: blog.createdBy + }, + skip: !blog.categoryId || !blog.createdBy, + }); + + const category = infoData?.blogCategory; + const author = infoData?.userWithGroups?.user; + const tag = infoData?.blogTag; + + return ( - +
+ {/* Featured Image */} +
+ {blog.featuredImage ? ( + {blog.title} + ) : ( +
+ No image +
+ )} +
+ + {/* Meta Information */} +
+ + {category?.name || tag?.name || 'Article'} + +
+ By + + + {author?.username?.charAt(0)?.toUpperCase() || 'A'} + + + {infoLoading ? 'Loading...' : (author?.username || 'Anonymous')} + + {timeAgo} +
+
+ + {/* Content */} +
+

+ {blog.title} +

+

+ {blog.excerpt || 'No excerpt available...'} +

+
+
-

{blog.excerpt}

-
+ ); } diff --git a/app/login/login-form.tsx b/app/login/login-form.tsx index 647105e..7904ca6 100644 --- a/app/login/login-form.tsx +++ b/app/login/login-form.tsx @@ -11,6 +11,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useUser } from "../user-context" import { useRouter } from "next/navigation" import { useState } from "react" +import { GalleryVerticalEnd } from "lucide-react" const schema = z.object({ username: z.string().min(1, "用户名不能为空"), @@ -20,26 +21,40 @@ const schema = z.object({ export function LoginForm({ className, ...props -}: React.ComponentProps<"form">) { +}: React.ComponentProps<"div">) { const { login } = useUser(); const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [step, setStep] = useState<'username' | 'password'>('username'); + const [username, setUsername] = useState(''); - const form = useForm>({ - resolver: zodResolver(schema), - defaultValues: { - username: "", - password: "", - }, - }) + const usernameForm = useForm<{ username: string }>({ + resolver: zodResolver(z.object({ username: z.string().min(1, "用户名不能为空") })), + defaultValues: { username: "" }, + }); - async function onSubmit(values: z.infer) { + const passwordForm = useForm<{ username: string; password: string }>({ + resolver: zodResolver(z.object({ + username: z.string(), + password: z.string().min(1, "密码不能为空") + })), + defaultValues: { username: "", password: "" }, + }); + + async function onUsernameSubmit(values: { username: string }) { + setUsername(values.username); + setStep('password'); + setError(null); + // 设置密码表单的用户名字段 + passwordForm.setValue('username', values.username); + } + + async function onPasswordSubmit(values: { username: string; password: string }) { try { setIsLoading(true); setError(null); - await login(values); - // clearMap(); + await login({ username, password: values.password }); router.push('/'); } catch (err) { setError(err instanceof Error ? err.message : '登录失败,请重试'); @@ -48,84 +63,197 @@ export function LoginForm({ } } + function goBackToUsername() { + setStep('username'); + setError(null); + passwordForm.reset(); + } + return ( -
- -
-

登录您的账户

-

- 请输入您的用户名和密码登录 -

-
- - {error && ( -
- {error} -
- )} - -
-
- ( - - 用户名 - - - - - - )} /> -
-
- - ( - -
- 密码 - - 忘记密码? - +
+ {step === 'username' ? ( + + +
+
+ +
+
+ Acme Inc. +
+

欢迎来到 Acme Inc.

+
+ 还没有账户?{" "} + + 注册 + +
+
- - - - - - )} /> + {error && ( +
+ {error} +
+ )} -
- -
- - 或者使用 - -
- -
-
- 还没有账户?{" "} - - 注册 - -
- - +
+
+ ( + + 用户名 + + + + + + )} + /> +
+ +
+
+ + 或 + +
+
+ + +
+
+ + + ) : ( +
+ +
+
+ +
+ +
+ Acme Inc. +
+

输入密码

+
+ 登录为 {username} +
+
+ {error && ( +
+ {error} +
+ )} + +
+
+ ( + + 用户名 + + + + + + )} + /> +
+
+ ( + + 密码 + + + + + + )} + /> +
+ +
+
+ +
+
+
+ + )} + +
+ By clicking continue, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + . +
+
) } diff --git a/app/login/page.tsx b/app/login/page.tsx index 0a66ab9..0804328 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -48,28 +48,9 @@ export default function LoginPage() { // } return ( -
-
- -
-
- -
-
-
-
- Image +
+
+
); diff --git a/app/me/account/page.tsx b/app/me/account/page.tsx deleted file mode 100644 index 765d1d8..0000000 --- a/app/me/account/page.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client" - -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" -import { Eye, EyeOff, Save } from "lucide-react" - -export default function Page() { - const [showPassword, setShowPassword] = useState(false) - - return ( -
-
-

账户设置

-

管理您的账户基本设置和安全信息

-
- - - - 账户信息 - 管理您的账户基本设置 - - -
-
-
- -

zhangsan2024

-
- -
- - - -
-
- -

zhangsan@example.com

-
- -
- - - -
- -
- - -
-
- -
- - -
- -
- - -
-
- - -
-
- - - - 语言和地区 - 设置您的语言偏好和时区 - - -
- - -
- -
- - -
- - -
-
-
- ) -} diff --git a/app/me/layout.tsx b/app/me/layout.tsx index 3b6f798..7dc531f 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -5,6 +5,9 @@ import { SidebarProvider, } from "@/components/ui/sidebar" import { redirect } from "next/navigation" +import { Navigation } from "../nav" +import { Command } from "lucide-react" +import FooterSection from "@/components/footer" export default async function Layout({ children }: { children: React.ReactNode }) { @@ -17,19 +20,13 @@ export default async function Layout({ children }: { children: React.ReactNode } } return ( - - - - {children} - - - +
+
+ + +
+ {children} + +
) } \ No newline at end of file diff --git a/app/me/nav-main.tsx b/app/me/nav-main.tsx deleted file mode 100644 index acc88a9..0000000 --- a/app/me/nav-main.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client" - -import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react" - -import { Button } from "@/components/ui/button" -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" -import { useEffect, useState } from "react" - -import { usePathname } from "next/navigation" -import Link from "next/link" - -export function NavMain({ - items, -}: { - items: { - title: string - url: string - icon?: Icon - }[] -}) { - - const pathname = usePathname() - return ( - - - - - - - Quick Create - - - - - - {items.map((item) => ( - - - - - {item.icon && } - {item.title} - - - - ))} - - - - ) -} diff --git a/app/me/notifications/page.tsx b/app/me/notifications/page.tsx deleted file mode 100644 index 942c3a0..0000000 --- a/app/me/notifications/page.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client" - -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Label } from "@/components/ui/label" -import { Switch } from "@/components/ui/switch" -import { Separator } from "@/components/ui/separator" -import { Mail, Bell, Phone, Save } from "lucide-react" - -export default function Page() { - const [notifications, setNotifications] = useState({ - email: true, - push: false, - sms: true, - marketing: false, - }) - - return ( -
-
-

通知设置

-

选择您希望接收的通知类型和频率

-
- - - - 通知偏好 - 选择您希望接收的通知类型 - - -
-
-
- -

接收重要更新和活动通知

-
- setNotifications({ ...notifications, email: checked })} - /> -
- - - -
-
- -

在浏览器中接收实时通知

-
- setNotifications({ ...notifications, push: checked })} - /> -
- - - -
-
- -

接收安全相关的短信提醒

-
- setNotifications({ ...notifications, sms: checked })} - /> -
- - - -
-
- -

接收产品更新和促销信息

-
- setNotifications({ ...notifications, marketing: checked })} - /> -
-
- - -
-
-
- ) -} diff --git a/app/me/page.tsx b/app/me/page.tsx index cf4c9ee..209fc90 100644 --- a/app/me/page.tsx +++ b/app/me/page.tsx @@ -1,22 +1,763 @@ -"use client" +"use client"; -import { redirect, useRouter } from "next/navigation" -import { useUser } from "../user-context" -import { useEffect } from "react" +import * as React from "react"; +import { useState, useRef, useEffect } from "react"; +import { useUser } from "../user-context"; +import { useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { + Settings, + User, + Bell, + Shield, + Database, + Mail, + Globe, + Palette, + Monitor, + Moon, + Sun, + Check, + ChevronDown, + Search, + X, + Save, + RotateCcw +} from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { Textarea } from "@/components/ui/textarea"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +interface Tag { + id: string; + label: string; + color?: string; +} + +interface TagInputProps { + onChange?: (tags: Array) => void; + defaultTags?: Array; + suggestions?: Array; + maxTags?: number; + label?: string; + placeholder?: string; + error?: string; +} + +function useTags({ + onChange, + defaultTags = [], + maxTags = 10, +}: { + onChange?: (tags: Tag[]) => void; + defaultTags?: Tag[]; + maxTags?: number; +}) { + const [tags, setTags] = useState(defaultTags); + + function addTag(tag: Tag) { + if (tags.length >= maxTags) return; + const newTags = [...tags, tag]; + setTags(newTags); + onChange?.(newTags); + return newTags; + } + + function removeTag(tagId: string) { + const newTags = tags.filter((t) => t.id !== tagId); + setTags(newTags); + onChange?.(newTags); + return newTags; + } + + function removeLastTag() { + if (tags.length === 0) return; + return removeTag(tags[tags.length - 1].id); + } + + return { + tags, + setTags, + addTag, + removeTag, + removeLastTag, + hasReachedMax: tags.length >= maxTags, + }; +} + +function useClickOutside( + ref: React.RefObject, + handler: (event: MouseEvent | TouchEvent) => void, + mouseEvent: 'mousedown' | 'mouseup' = 'mousedown' +): void { + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + const el = ref?.current; + const target = event.target; + + if (!el || !target || el.contains(target as Node)) { + return; + } + + handler(event); + }; + + document.addEventListener(mouseEvent, listener); + document.addEventListener('touchstart', listener); + + return () => { + document.removeEventListener(mouseEvent, listener); + document.removeEventListener('touchstart', listener); + }; + }, [ref, handler, mouseEvent]); +} + +const tagStyles = { + base: "inline-flex items-center gap-1.5 px-2 py-0.5 text-sm rounded-md transition-colors duration-150", + colors: { + blue: "bg-blue-50 text-blue-700 border border-blue-200 hover:border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700/30 dark:hover:border-blue-600/50", + purple: "bg-purple-50 text-purple-700 border border-purple-200 hover:border-purple-300 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700/30 dark:hover:border-purple-600/50", + green: "bg-green-50 text-green-700 border border-green-200 hover:border-green-300 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700/30 dark:hover:border-green-600/50", + }, +}; + +function TagInput({ + onChange, + defaultTags = [], + suggestions = [ + { id: "nextjs", label: "Next.js" }, + { id: "react", label: "React" }, + { id: "tailwind", label: "Tailwind" }, + ], + maxTags = 10, + label = "Tags", + placeholder = "Add tags...", + error, +}: TagInputProps) { + const { tags, addTag, removeTag, removeLastTag } = useTags({ + onChange, + defaultTags, + maxTags, + }); + const [input, setInput] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + const inputRef = useRef(null); + const containerRef = useRef(null); + + const filteredSuggestions = suggestions + .filter( + (suggestion: Tag) => + typeof suggestion.label === "string" && + typeof input === "string" && + suggestion.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 && + !tags.some((tag: Tag) => tag.id === suggestion.id) + ) + .slice(0, 5); + + const canAddNewTag = + input.length > 0 && + !suggestions.some((s) => s.label.toLowerCase() === input.toLowerCase()); + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Backspace" && input === "" && tags.length > 0) { + removeLastTag(); + } else if (e.key === "Enter" && input) { + e.preventDefault(); + if (isOpen && filteredSuggestions[selectedIndex]) { + addTag(filteredSuggestions[selectedIndex]); + setInput(""); + setIsOpen(false); + } else if (canAddNewTag) { + addTag({ id: input, label: input }); + setInput(""); + setIsOpen(false); + } + } else if (e.key === "Escape") { + setIsOpen(false); + } + } + + useClickOutside(containerRef as React.RefObject, () => setIsOpen(false)); + + return ( +
+ {label && ( + + )} + +
+ {tags.map((tag) => ( + + {tag.label} + + + ))} + + { + setInput(e.target.value); + setIsOpen(true); + setSelectedIndex(0); + }} + onFocus={() => setIsOpen(true)} + onKeyDown={handleKeyDown} + placeholder={tags.length === 0 ? placeholder : ""} + className="flex-1 min-w-[120px] bg-transparent h-7 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" + /> + + {isOpen && (input || filteredSuggestions.length > 0) && ( +
+
+ + 选择标签或创建新标签 + +
+
+ {filteredSuggestions.map((suggestion, index) => ( + + ))} + {canAddNewTag && ( + + )} +
+
+ )} +
+ + {error && ( +

{error}

+ )} +
+ ); +} + +interface SettingsSectionProps { + icon: React.ReactNode; + title: string; + description: string; + children: React.ReactNode; +} + +function SettingsSection({ icon, title, description, children }: SettingsSectionProps) { + return ( + + + + {icon} + {title} + + {description} + + + {children} + + + ); +} + +interface SettingItemProps { + label: string; + description?: string; + children: React.ReactNode; + badge?: string; +} + +function SettingItem({ label, description, children, badge }: SettingItemProps) { + return ( +
+
+
+ + {badge && {badge}} +
+ {description && ( +

{description}

+ )} +
+
+ {children} +
+
+ ); +} export default function MePage() { - const { isAuthenticated, isLoading, user } = useUser() - const router = useRouter(); + const { isLoading, user } = useUser(); + const router = useRouter(); - useEffect(() => { - if (!isLoading && !isAuthenticated) { - router.push('/login'); - } - }, [isAuthenticated, isLoading, router, user]); + const [settings, setSettings] = useState({ + emailNotifications: true, + pushNotifications: false, + marketingEmails: true, + twoFactorAuth: false, + sessionTimeout: "30", + theme: "system", + language: "zh", + timezone: "Asia/Shanghai", + autoSave: true, + dataRetention: "90", + apiAccess: false, + debugMode: false, + }); + const [profile, setProfile] = useState({ + name: user?.name || "", + email: user?.email || "", + role: "用户", + department: "IT", + bio: "热爱技术的开发者,专注于创造优秀的用户体验。", + }); + + const [tags, setTags] = useState([ + { id: "frontend", label: "前端开发", color: tagStyles.colors.blue }, + { id: "react", label: "React", color: tagStyles.colors.green }, + ]); + + // 认证检查已在 layout.tsx 中处理,无需在此重复检查 + + useEffect(() => { + if (user) { + setProfile(prev => ({ + ...prev, + name: user.name || "", + email: user.email || "", + })); + } + }, [user]); + + const handleSettingChange = (key: string, value: any) => { + setSettings(prev => ({ ...prev, [key]: value })); + }; + + const handleProfileChange = (key: string, value: string) => { + setProfile(prev => ({ ...prev, [key]: value })); + }; + + const handleSave = () => { + console.log("Settings saved:", settings); + console.log("Profile saved:", profile); + console.log("Tags saved:", tags); + // TODO: 实际保存到后端 + }; + + const handleReset = () => { + setSettings({ + emailNotifications: true, + pushNotifications: false, + marketingEmails: true, + twoFactorAuth: false, + sessionTimeout: "30", + theme: "system", + language: "zh", + timezone: "Asia/Shanghai", + autoSave: true, + dataRetention: "90", + apiAccess: false, + debugMode: false, + }); + }; + + if (isLoading) { return ( -
-

Me

+
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+

个人设置

+

+ 管理您的个人资料、偏好设置和账户安全 +

+
+
+ + +
+
- ) + +
+ {/* Profile Section */} +
+ } + title="个人资料" + description="管理您的个人信息和账户设置" + > +
+ + + + {profile.name.split(' ').map(n => n[0] || '用').join('').slice(0, 2)} + + +
+

{profile.name || '用户'}

+

{profile.role}

+ {profile.department} +
+
+ +
+
+ + handleProfileChange('name', e.target.value)} + className="mt-1" + /> +
+ +
+ + handleProfileChange('email', e.target.value)} + className="mt-1" + /> +
+ +
+ + +
+ +
+ +