mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
裁剪
swagger 移除 merchant swagger 改造首页内容为通用后台管理系统定位 - 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform' - 更新副标题为 '为二次开发而生' - 更新首页描述为通用平台的特点 - 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建' - 修改展示卡片为技术栈和二次开发相关 - 更新开发者示例代码为通用的注册和 API Key 获取示例 - 更新页脚品牌名为 'Modern Platform' - 调整页脚导航链接为通用平台相关内容 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 裁剪 移除 /api/v1/user/pay-key 相关代码 - 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体 - 删除 User 模型中的 PayKey 字段 - 删除 User.VerifyPayKey 方法 - 删除 EncryptPayKeyFailed 错误常量 - 删除 /api/v1/user/pay-key PUT 路由 - 删除 OAuth 返回中的 IsPayKey 字段 - 删除前端 UserService.updatePayKey 方法 - 删除前端所有支付密钥 UI 和逻辑 - 更新相关的导出和注释 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 api 修正 系统配置 前端裁剪 后端裁剪 init
This commit is contained in:
@@ -4,247 +4,12 @@ import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { motion, useAnimation } from "motion/react"
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
|
||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@/components/ui/carousel"
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Info } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PayLevel, User } from "@/lib/services/auth"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
import services from "@/lib/services"
|
||||
import type { UserPayConfig } from "@/lib/services/admin"
|
||||
|
||||
|
||||
/**
|
||||
* 等级配置
|
||||
*/
|
||||
interface LevelConfig {
|
||||
level: PayLevel
|
||||
name: string
|
||||
nameEn: string
|
||||
minScore: number
|
||||
maxScore: number | null
|
||||
gradient: string
|
||||
textColor: string
|
||||
metalEffect?: boolean
|
||||
dailyLimit: number | null
|
||||
feeRate: number | string
|
||||
scoreRate: number | string
|
||||
}
|
||||
|
||||
/** 等级视觉样式配置(不包含业务数据) */
|
||||
const LEVEL_STYLES = [
|
||||
{
|
||||
level: PayLevel.Ordinary,
|
||||
name: "普通会员",
|
||||
nameEn: "ORDINARY MEMBER",
|
||||
gradient: "from-[#1e3a8a] via-[#2563eb] to-[#1d4ed8] shadow-lg shadow-blue-900/10",
|
||||
textColor: "text-white/95",
|
||||
},
|
||||
{
|
||||
level: PayLevel.Gold,
|
||||
name: "黄金会员",
|
||||
nameEn: "GOLD MEMBER",
|
||||
gradient: "from-[#FDFCF0] via-[#FCE38A] to-[#B45309] shadow-lg shadow-amber-500/10",
|
||||
textColor: "text-[#854D0E]",
|
||||
metalEffect: true,
|
||||
},
|
||||
{
|
||||
level: PayLevel.WhiteGold,
|
||||
name: "白金会员",
|
||||
nameEn: "PLATINUM MEMBER",
|
||||
gradient: "from-[#F8FAFC] via-[#E2E8F0] to-[#94A3B8] shadow-lg shadow-slate-500/10",
|
||||
textColor: "text-slate-800",
|
||||
metalEffect: true,
|
||||
},
|
||||
{
|
||||
level: PayLevel.BlackGold,
|
||||
name: "黑金会员",
|
||||
nameEn: "BLACK MEMBER",
|
||||
gradient: "from-[#020617] via-[#1e293b] to-[#020617] shadow-lg shadow-black/20",
|
||||
textColor: "text-[#FFE5B4]",
|
||||
metalEffect: true,
|
||||
},
|
||||
] as const
|
||||
|
||||
/** 合并 API 数据和视觉样式 */
|
||||
function mergeLevelConfigs(apiConfigs: UserPayConfig[]): LevelConfig[] {
|
||||
return LEVEL_STYLES.map(style => {
|
||||
const apiConfig = apiConfigs.find(c => c.level === style.level)
|
||||
if (apiConfig) {
|
||||
return {
|
||||
...style,
|
||||
minScore: apiConfig.min_score,
|
||||
maxScore: apiConfig.max_score,
|
||||
dailyLimit: apiConfig.daily_limit,
|
||||
feeRate: apiConfig.fee_rate,
|
||||
scoreRate: apiConfig.score_rate,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...style,
|
||||
minScore: 0,
|
||||
maxScore: null,
|
||||
dailyLimit: null,
|
||||
feeRate: 0,
|
||||
scoreRate: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getLevelConfig(score: number, configs: LevelConfig[]): LevelConfig {
|
||||
if (configs.length === 0) {
|
||||
return {
|
||||
level: PayLevel.Ordinary,
|
||||
name: "加载中...",
|
||||
nameEn: "LOADING...",
|
||||
gradient: "from-blue-600 via-blue-500 to-blue-700",
|
||||
textColor: "text-white",
|
||||
minScore: 0,
|
||||
maxScore: null,
|
||||
dailyLimit: null,
|
||||
feeRate: 0,
|
||||
scoreRate: 0,
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = configs.length - 1; i >= 0; i--) {
|
||||
if (score >= configs[i].minScore) {
|
||||
return configs[i]
|
||||
}
|
||||
}
|
||||
return configs[0]
|
||||
}
|
||||
|
||||
function MembershipCard({
|
||||
config,
|
||||
user,
|
||||
score,
|
||||
currentLevel,
|
||||
isActive,
|
||||
levelConfigs,
|
||||
}: {
|
||||
config: LevelConfig
|
||||
user: User
|
||||
score: number
|
||||
currentLevel: LevelConfig
|
||||
isActive: boolean
|
||||
levelConfigs: LevelConfig[]
|
||||
}) {
|
||||
const isAccessible = levelConfigs.findIndex(l => l.level === config.level) <= levelConfigs.findIndex(l => l.level === currentLevel.level)
|
||||
const isDarkCard = config.level === PayLevel.BlackGold || config.level === PayLevel.Ordinary
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-500",
|
||||
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full aspect-[2/1] rounded-xl overflow-hidden bg-gradient-to-br shadow-lg",
|
||||
config.gradient,
|
||||
config.metalEffect && "shadow-2xl"
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-white rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-white rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
|
||||
</div>
|
||||
<div className="absolute inset-0 opacity-[0.15] bg-[url('https://grainy-gradients.vercel.app/noise.svg')]" />
|
||||
|
||||
{config.metalEffect && (
|
||||
<>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/5 via-white/2 to-transparent" />
|
||||
<div className="absolute inset-0 rounded-xl ring-1 ring-inset ring-white/25" />
|
||||
<div className="absolute top-0 left-0 w-full h-1/4 bg-gradient-to-b from-white/15 via-transparent to-transparent rounded-t-xl" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={cn("relative h-full flex flex-col justify-between p-3 sm:p-4 md:p-5", config.textColor)}>
|
||||
<div>
|
||||
<div className="text-[8px] sm:text-[9px] md:text-[10px] font-medium opacity-70 tracking-wider">
|
||||
{config.nameEn}
|
||||
</div>
|
||||
<div className="text-sm sm:text-base md:text-lg font-bold tracking-tight">
|
||||
{config.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5 sm:space-y-1">
|
||||
<div className="text-[8px] sm:text-[9px] md:text-[10px] opacity-70 tracking-wide">平台分数</div>
|
||||
<div className="text-lg sm:text-xl md:text-2xl font-bold tracking-tight tabular-nums">
|
||||
{score.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-between gap-1">
|
||||
<div className="text-[8px] sm:text-[9px] md:text-[10px] opacity-70">
|
||||
LINUX DO <span className="italic font-serif">Credit</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[8px] sm:text-[9px] md:text-[10px] opacity-70">分数范围</div>
|
||||
<div className="text-[8px] sm:text-[9px] md:text-[10px] font-medium tabular-nums">
|
||||
{config.minScore < 0 ? "0.00" : config.minScore.toLocaleString()} - {config.maxScore?.toLocaleString() || "∞"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-3 right-3 sm:top-4 sm:right-4 md:top-5 md:right-5">
|
||||
{isAccessible ? (
|
||||
<Avatar className="size-8 sm:size-9 md:size-10 border-2 border-white/20">
|
||||
<AvatarImage src={user?.avatar_url} alt={user?.nickname} />
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
"text-xs sm:text-sm font-semibold",
|
||||
isDarkCard ? "bg-white/20 text-white" : "bg-black/20 text-black"
|
||||
)}
|
||||
>
|
||||
{user?.nickname?.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full text-[8px] sm:text-[9px] md:text-[10px] font-semibold backdrop-blur-sm",
|
||||
isDarkCard ? "bg-white/20 text-white" : "bg-black/20 text-black"
|
||||
)}
|
||||
>
|
||||
未获得
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getPayLevelLabel(level: PayLevel): string {
|
||||
switch (level) {
|
||||
case PayLevel.BlackGold:
|
||||
return "黑金会员"
|
||||
case PayLevel.WhiteGold:
|
||||
return "白金会员"
|
||||
case PayLevel.Gold:
|
||||
return "黄金会员"
|
||||
case PayLevel.Ordinary:
|
||||
return "普通会员"
|
||||
default:
|
||||
return "未知等级"
|
||||
}
|
||||
}
|
||||
import { Shield } from "lucide-react"
|
||||
|
||||
export function ProfileMain() {
|
||||
const { user, loading, getTrustLevelLabel } = useUser()
|
||||
const [api, setApi] = React.useState<CarouselApi>()
|
||||
const [current, setCurrent] = React.useState(0)
|
||||
const [levelConfigs, setLevelConfigs] = React.useState<LevelConfig[]>([])
|
||||
|
||||
const controls = useAnimation()
|
||||
const isAnimatingRef = React.useRef(false)
|
||||
|
||||
@@ -262,42 +27,6 @@ export function ProfileMain() {
|
||||
}, 650)
|
||||
}
|
||||
|
||||
const score = user?.pay_score ?? 0
|
||||
const currentLevel = getLevelConfig(score, levelConfigs)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchPayConfigs() {
|
||||
const configs = await services.config.getUserPayConfigs()
|
||||
setLevelConfigs(mergeLevelConfigs(configs))
|
||||
}
|
||||
fetchPayConfigs()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
|
||||
setCurrent(api.selectedScrollSnap())
|
||||
api.on("select", () => setCurrent(api.selectedScrollSnap()))
|
||||
}, [api])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || levelConfigs.length === 0 || !currentLevel) return
|
||||
|
||||
const scrollToCurrentLevel = () => {
|
||||
const currentLevelIndex = levelConfigs.findIndex(
|
||||
(config) => config.level === currentLevel.level
|
||||
)
|
||||
if (currentLevelIndex >= 0) {
|
||||
api.scrollTo(currentLevelIndex, true)
|
||||
}
|
||||
}
|
||||
scrollToCurrentLevel()
|
||||
api.on("reInit", scrollToCurrentLevel)
|
||||
return () => {
|
||||
api.off("reInit", scrollToCurrentLevel)
|
||||
}
|
||||
}, [api, currentLevel, levelConfigs])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="py-6 space-y-4">
|
||||
@@ -317,7 +46,7 @@ export function ProfileMain() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6 space-y-6">
|
||||
<div className="py-6 space-y-6 max-w-4xl mx-auto">
|
||||
<div className="font-semibold">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
@@ -334,130 +63,60 @@ export function ProfileMain() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<h2 className="font-medium text-sm text-muted-foreground">会员等级</h2>
|
||||
<div className="space-y-6 bg-card border border-dashed rounded-lg p-6">
|
||||
<div className="border-b pb-4">
|
||||
<h2 className="text-lg font-semibold tracking-tight">基本资料</h2>
|
||||
<p className="text-xs text-muted-foreground">您的个人账户基本信息</p>
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
setApi={setApi}
|
||||
opts={{
|
||||
align: "center",
|
||||
loop: false,
|
||||
containScroll: false,
|
||||
}}
|
||||
className="w-full relative"
|
||||
>
|
||||
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-background to-transparent pointer-events-none z-10" />
|
||||
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-background to-transparent pointer-events-none z-10" />
|
||||
<CarouselContent className="-ml-4">
|
||||
{levelConfigs.map((config, index) => (
|
||||
<CarouselItem
|
||||
key={config.level}
|
||||
className="pl-4 basis-[85%] sm:basis-[70%] md:basis-[65%] lg:basis-[50%] xl:basis-[40%] 2xl:basis-[35%] cursor-pointer"
|
||||
onClick={() => api?.scrollTo(index)}
|
||||
>
|
||||
<MembershipCard
|
||||
config={config}
|
||||
user={user}
|
||||
score={score}
|
||||
currentLevel={currentLevel}
|
||||
isActive={index === current}
|
||||
levelConfigs={levelConfigs}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="left-0" />
|
||||
<CarouselNext className="right-0" />
|
||||
</Carousel>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-medium text-sm text-muted-foreground">基本信息</h2>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start gap-4 sm:gap-6">
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6 pt-2">
|
||||
<motion.div
|
||||
animate={controls}
|
||||
onClick={handleAvatarClick}
|
||||
className="cursor-pointer origin-center"
|
||||
className="cursor-pointer origin-center shrink-0"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<Avatar className="size-14 sm:size-16">
|
||||
<Avatar className="size-20 md:size-24 border-2 border-primary/10 shadow-md">
|
||||
<AvatarImage src={user.avatar_url} alt={user.nickname || user.username} />
|
||||
<AvatarFallback className="text-lg sm:text-xl">
|
||||
{(user.nickname || user.username).slice(0, 2)}
|
||||
<AvatarFallback className="text-2xl bg-indigo-600 text-white font-bold">
|
||||
{(user.nickname || user.username).slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex-1 w-full">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
<div className="flex-1 w-full space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">账户</div>
|
||||
<div className="text-sm font-medium">@{user.username}</div>
|
||||
<div className="text-sm font-semibold">@{user.username}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">昵称</div>
|
||||
<div className="text-sm font-medium">{user.nickname || '未设置'}</div>
|
||||
<div className="text-sm font-semibold">{user.nickname || '未设置'}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">用户ID</div>
|
||||
<div className="text-sm font-medium">{user.id}</div>
|
||||
<div className="text-xs text-muted-foreground">用户ID (UID)</div>
|
||||
<div className="text-sm font-mono font-semibold">{user.id}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">信任等级</div>
|
||||
<div className="text-sm font-medium">{getTrustLevelLabel(user.trust_level)}</div>
|
||||
<div className="text-sm font-semibold">{getTrustLevelLabel(user.trust_level)}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">平台等级</div>
|
||||
<div className="text-sm font-medium">{getPayLevelLabel(user.pay_level)}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">平台分数</div>
|
||||
<div className="text-sm font-medium">{user.pay_score.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">每日流转限额</div>
|
||||
<div className="text-sm font-medium">
|
||||
{currentLevel.dailyLimit ? `${ currentLevel.dailyLimit.toLocaleString() } 积分` : '无限额'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">集市手续费率</div>
|
||||
<div className="text-sm font-medium">
|
||||
{typeof currentLevel.feeRate === 'number'
|
||||
? `${ (currentLevel.feeRate * 100).toFixed(2) }%`
|
||||
: `${ (parseFloat(currentLevel.feeRate as string) * 100).toFixed(2) }%`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">平台分数转化率</div>
|
||||
<div className="text-sm font-medium">
|
||||
{typeof currentLevel.scoreRate === 'number'
|
||||
? `${ (currentLevel.scoreRate * 100).toFixed(2) }%`
|
||||
: `${ (parseFloat(currentLevel.scoreRate as string) * 100).toFixed(2) }%`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
积分基准值
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Info className="h-3 w-3 cursor-help" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" className="w-auto max-w-[280px] p-3">
|
||||
<p className="text-xs">上一次从社区同步的积分基准值</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{parseFloat(user.community_balance).toLocaleString()}
|
||||
<div className="text-xs text-muted-foreground">管理员身份</div>
|
||||
<div className="text-sm font-semibold flex items-center gap-1">
|
||||
{user.is_admin ? (
|
||||
<span className="text-rose-600 flex items-center gap-1">
|
||||
<Shield className="size-3.5" />
|
||||
是
|
||||
</span>
|
||||
) : (
|
||||
<span>否</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,83 +2,11 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { ShieldCheck } from "lucide-react"
|
||||
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { UserService } from "@/lib/services/user"
|
||||
|
||||
export function SecurityMain() {
|
||||
const router = useRouter()
|
||||
const [setupStep, setSetupStep] = React.useState<'password' | 'confirm'>('password')
|
||||
const [payKey, setPayKey] = React.useState("")
|
||||
const [confirmPayKey, setConfirmPayKey] = React.useState("")
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
|
||||
const isPayKeyValid = payKey.length === 6 && /^\d{6}$/.test(payKey)
|
||||
const isConfirmValid = confirmPayKey.length === 6 && /^\d{6}$/.test(confirmPayKey)
|
||||
const passwordsMatch = payKey === confirmPayKey
|
||||
|
||||
/* 安全密码输入 */
|
||||
const handlePayKeyChange = (value: string) => {
|
||||
const numericValue = value.replaceAll(/\D/g, '')
|
||||
setPayKey(numericValue)
|
||||
}
|
||||
|
||||
/* 确认安全密码 */
|
||||
const handleConfirmPayKeyChange = (value: string) => {
|
||||
const numericValue = value.replaceAll(/\D/g, '')
|
||||
setConfirmPayKey(numericValue)
|
||||
}
|
||||
|
||||
const handlePayKeySubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (setupStep === 'password') {
|
||||
if (!isPayKeyValid) {
|
||||
toast.error("安全密码必须为6位数字")
|
||||
return
|
||||
}
|
||||
setSetupStep('confirm')
|
||||
} else {
|
||||
if (!isConfirmValid) {
|
||||
toast.error("确认密码必须为6位数字")
|
||||
return
|
||||
}
|
||||
|
||||
if (!passwordsMatch) {
|
||||
toast.error("两次输入的安全密码不一致")
|
||||
setSetupStep('password')
|
||||
setConfirmPayKey("")
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await UserService.updatePayKey(payKey)
|
||||
toast.success("修改成功", {
|
||||
description: "您的安全密码已成功更新",
|
||||
})
|
||||
|
||||
// 立即退出
|
||||
router.push('/settings')
|
||||
} catch (error) {
|
||||
toast.error("修改失败", {
|
||||
description: error instanceof Error ? error.message : "更新密码时发生错误,请稍后重试",
|
||||
})
|
||||
setSetupStep('password')
|
||||
setConfirmPayKey("")
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6 space-y-6">
|
||||
<div className="font-semibold">
|
||||
@@ -100,111 +28,11 @@ export function SecurityMain() {
|
||||
<div className="space-y-6">
|
||||
<div className="font-medium text-sm text-muted-foreground flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
密码安全
|
||||
安全设置
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-center">
|
||||
<motion.div
|
||||
className="w-full max-w-sm space-y-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={setupStep}
|
||||
initial={{ opacity: 0, x: setupStep === 'confirm' ? 20 : -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 mb-4">
|
||||
<h3 className="text-xl font-bold tracking-tight text-center">
|
||||
{setupStep === 'password' ? '设置新密码' : '确认新密码'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-[320px] mx-auto">
|
||||
{setupStep === 'password'
|
||||
? '请输入新的6位数字安全密码'
|
||||
: '请再次输入密码进行确认'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handlePayKeySubmit} className="space-y-6">
|
||||
<div className="flex justify-center">
|
||||
{setupStep === 'password' ? (
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={payKey}
|
||||
onChange={handlePayKeyChange}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
>
|
||||
<InputOTPGroup className="gap-2">
|
||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||
<InputOTPSlot key={i} index={i} className="w-10 h-10 sm:w-11 sm:h-11 border-input transition-all ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2" />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={confirmPayKey}
|
||||
onChange={handleConfirmPayKeyChange}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
>
|
||||
<InputOTPGroup className="gap-2">
|
||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||
<InputOTPSlot key={i} index={i} className="w-10 h-10 sm:w-11 sm:h-11 text-lg border-input transition-all ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2" />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
{!passwordsMatch && confirmPayKey.length === 6 && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-xs text-red-500 text-center font-medium"
|
||||
>
|
||||
两次输入的密码不一致
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 sm:mx-8">
|
||||
{setupStep === 'confirm' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSetupStep('password')
|
||||
setConfirmPayKey('')
|
||||
}}
|
||||
className="flex-1 h-10 rounded-full tracking-wide text-sm font-bold transition-all active:scale-95"
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className={`flex-1 h-10 rounded-full tracking-wide bg-primary hover:bg-primary/90 text-primary-foreground text-sm font-bold shadow-lg shadow-primary/20 hover:shadow-primary/30 transition-all active:scale-95 ${setupStep === 'password' ? 'w-full' : ''}`}
|
||||
disabled={
|
||||
setupStep === 'password'
|
||||
? !isPayKeyValid
|
||||
: isSubmitting || !isConfirmValid
|
||||
}
|
||||
>
|
||||
{isSubmitting && <Spinner className="mr-2" />}
|
||||
{setupStep === 'password' ? '继续' : '保存修改'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
当前暂无安全配置选项
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user