This commit is contained in:
tsuki 2025-07-28 23:14:43 +08:00
parent 1eca50c6ce
commit 1b35c937c1
8 changed files with 631 additions and 3 deletions

128
app/me/account/page.tsx Normal file
View File

@ -0,0 +1,128 @@
"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 (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground">zhangsan2024</p>
</div>
<Button variant="outline" size="sm">
</Button>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground">zhangsan@example.com</p>
</div>
<Button variant="outline" size="sm">
</Button>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="current-password"></Label>
<div className="relative">
<Input id="current-password" type={showPassword ? "text" : "password"} placeholder="输入当前密码" />
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="new-password"></Label>
<Input id="new-password" type="password" placeholder="输入新密码" />
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password"></Label>
<Input id="confirm-password" type="password" placeholder="再次输入新密码" />
</div>
</div>
<Button className="gap-2">
<Save className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Select defaultValue="zh-cn">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-cn"></SelectItem>
<SelectItem value="zh-tw"></SelectItem>
<SelectItem value="en">English</SelectItem>
<SelectItem value="ja"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="asia-shanghai">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asia-shanghai">Asia/Shanghai (UTC+8)</SelectItem>
<SelectItem value="asia-tokyo">Asia/Tokyo (UTC+9)</SelectItem>
<SelectItem value="america-new-york">America/New_York (UTC-5)</SelectItem>
<SelectItem value="europe-london">Europe/London (UTC+0)</SelectItem>
</SelectContent>
</Select>
</div>
<Button></Button>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,101 @@
"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 (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center gap-2">
<Mail className="h-4 w-4" />
</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={notifications.email}
onCheckedChange={(checked) => setNotifications({ ...notifications, email: checked })}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center gap-2">
<Bell className="h-4 w-4" />
</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={notifications.push}
onCheckedChange={(checked) => setNotifications({ ...notifications, push: checked })}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center gap-2">
<Phone className="h-4 w-4" />
</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={notifications.sms}
onCheckedChange={(checked) => setNotifications({ ...notifications, sms: checked })}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={notifications.marketing}
onCheckedChange={(checked) => setNotifications({ ...notifications, marketing: checked })}
/>
</div>
</div>
<Button className="gap-2">
<Save className="h-4 w-4" />
</Button>
</CardContent>
</Card>
</div>
)
}

149
app/me/profile/page.tsx Normal file
View File

@ -0,0 +1,149 @@
"use client"
import { useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
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 { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Camera, CalendarIcon, Save } from "lucide-react"
import { cn } from "@/lib/utils"
import { format } from "date-fns"
export default function Page() {
const [date, setDate] = useState<Date>()
const [profileData, setProfileData] = useState({
name: "张三",
email: "zhangsan@example.com",
phone: "+86 138 0013 8000",
bio: "这是我的个人简介,我是一名前端开发工程师,热爱技术和创新。",
location: "北京市朝阳区",
website: "https://zhangsan.dev",
company: "科技有限公司",
})
return (
<div className="space-y-6 p-4">
<div>
<h2 className="text-2xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage src="/placeholder.svg?height=80&width=80" />
<AvatarFallback></AvatarFallback>
</Avatar>
<div className="space-y-2">
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Camera className="h-4 w-4" />
</Button>
<p className="text-sm text-muted-foreground">400x400px JPGPNG </p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={profileData.name}
onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<div className="flex items-center space-x-2">
<Input
id="email"
type="email"
value={profileData.email}
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
/>
<Badge variant="secondary"></Badge>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={profileData.phone}
onChange={(e) => setProfileData({ ...profileData, phone: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="company"></Label>
<Input
id="company"
value={profileData.company}
onChange={(e) => setProfileData({ ...profileData, company: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="location"></Label>
<Input
id="location"
value={profileData.location}
onChange={(e) => setProfileData({ ...profileData, location: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="website"></Label>
<Input
id="website"
value={profileData.website}
onChange={(e) => setProfileData({ ...profileData, website: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bio"></Label>
<Textarea
id="bio"
placeholder="介绍一下您自己..."
className="min-h-[100px]"
value={profileData.bio}
onChange={(e) => setProfileData({ ...profileData, bio: e.target.value })}
/>
<p className="text-sm text-muted-foreground">{profileData.bio.length}/500 </p>
</div>
<div className="space-y-2">
<Label></Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("w-full justify-start text-left font-normal", !date && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "yyyy年MM月dd日") : "选择日期"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</div>
<Button className="gap-2">
<Save className="h-4 w-4" />
</Button>
</CardContent>
</Card>
</div>
)
}

125
app/me/security/page.tsx Normal file
View File

@ -0,0 +1,125 @@
"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 { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Save } from "lucide-react"
export default function Page() {
const [security, setSecurity] = useState({
twoFactor: true,
loginAlerts: true,
dataExport: false,
})
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex items-center gap-2">
<Badge variant={security.twoFactor ? "default" : "secondary"}>
{security.twoFactor ? "已启用" : "未启用"}
</Badge>
<Switch
checked={security.twoFactor}
onCheckedChange={(checked) => setSecurity({ ...security, twoFactor: checked })}
/>
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={security.loginAlerts}
onCheckedChange={(checked) => setSecurity({ ...security, loginAlerts: checked })}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={security.dataExport}
onCheckedChange={(checked) => setSecurity({ ...security, dataExport: checked })}
/>
</div>
</div>
<div className="space-y-4 pt-4 border-t">
<h4 className="font-medium"></h4>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="space-y-1">
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">Chrome on Windows </p>
</div>
<Badge variant="outline"></Badge>
</div>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">iPhone</p>
<p className="text-sm text-muted-foreground">Safari on iOS 2</p>
</div>
<Button variant="outline" size="sm">
</Button>
</div>
</div>
</div>
<Button className="gap-2">
<Save className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Button variant="destructive" size="sm">
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

48
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

64
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.5",
@ -1434,6 +1435,42 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
@ -1661,9 +1698,8 @@
}, },
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "http://mirrors.cloud.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.2" "@radix-ui/react-compose-refs": "1.1.2"
}, },
@ -6236,6 +6272,28 @@
"@radix-ui/react-visually-hidden": "1.2.3" "@radix-ui/react-visually-hidden": "1.2.3"
} }
}, },
"@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
"requires": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
}
},
"@radix-ui/react-popper": { "@radix-ui/react-popper": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
@ -6351,7 +6409,7 @@
}, },
"@radix-ui/react-slot": { "@radix-ui/react-slot": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "http://mirrors.cloud.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"requires": { "requires": {
"@radix-ui/react-compose-refs": "1.1.2" "@radix-ui/react-compose-refs": "1.1.2"

View File

@ -23,6 +23,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.5",