增加个人中心,查看订单
This commit is contained in:
parent
c06c4f2299
commit
d0b98449e2
193
app/account/page.tsx
Normal file
193
app/account/page.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { Loader2, User, Mail, AlertCircle, PackageCheck, KeyRound, CreditCard } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
|
||||||
|
type Order = {
|
||||||
|
id: number
|
||||||
|
order_no: string
|
||||||
|
product_type: string
|
||||||
|
product_name: string
|
||||||
|
amount: number
|
||||||
|
status: string
|
||||||
|
payment_method?: string
|
||||||
|
trade_no?: string
|
||||||
|
created_at: string
|
||||||
|
paid_at?: string
|
||||||
|
metadata?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccountPage() {
|
||||||
|
const { data: session, status } = useSession()
|
||||||
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated') return
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/orders')
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok && data.success) {
|
||||||
|
setOrders(data.orders || [])
|
||||||
|
} else {
|
||||||
|
setError(data.message || '加载订单失败')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e?.message || '加载订单失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchOrders()
|
||||||
|
}, [status])
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-apple-blue" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 'authenticated' || !session) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center space-y-4">
|
||||||
|
<AlertCircle className="w-10 h-10 text-orange-500" />
|
||||||
|
<p className="text-apple-gray-700">请先登录后查看个人中心</p>
|
||||||
|
<Link
|
||||||
|
href="/auth/signin"
|
||||||
|
className="px-4 py-2 bg-apple-blue text-white rounded-full font-semibold"
|
||||||
|
>
|
||||||
|
去登录
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white">
|
||||||
|
<Header scrolled />
|
||||||
|
<div className="max-w-6xl mx-auto px-4 py-14">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-apple-gray-900">个人中心</h1>
|
||||||
|
<p className="text-apple-gray-600 text-sm mt-2">查看账户信息、订单和激活码</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 mb-8 items-start">
|
||||||
|
<div className="md:col-span-1 bg-gradient-to-br from-apple-blue/10 to-white rounded-2xl p-6 border border-apple-gray-100 apple-shadow-lg">
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-apple-blue text-white flex items-center justify-center">
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-apple-gray-900">{session.user?.name || '未命名用户'}</p>
|
||||||
|
<p className="text-sm text-apple-gray-500">ID: {session.user?.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-apple-gray-700 mb-2">
|
||||||
|
<Mail className="w-4 h-4 text-apple-blue" />
|
||||||
|
<span className="truncate max-w-[220px]">{session.user?.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2 bg-white rounded-2xl p-6 border border-apple-gray-100 apple-shadow-lg">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-apple-gray-900">订单列表</h2>
|
||||||
|
{loading && <Loader2 className="w-4 h-4 animate-spin text-apple-blue" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-600 text-sm mb-3 flex items-center space-x-2">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{orders.length === 0 && !loading ? (
|
||||||
|
<p className="text-apple-gray-600 text-sm">暂无订单</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{orders.map((o) => {
|
||||||
|
const license = o.metadata?.license
|
||||||
|
const licenseError = o.metadata?.licenseError
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={o.id}
|
||||||
|
className="border border-apple-gray-100 rounded-xl p-4 hover:border-apple-blue transition bg-gradient-to-br from-white to-apple-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="font-mono text-sm text-apple-gray-800">{o.order_no}</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
o.status === 'paid'
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: o.status === 'failed'
|
||||||
|
? 'bg-red-100 text-red-600'
|
||||||
|
: 'bg-amber-100 text-amber-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{o.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-apple-gray-700">
|
||||||
|
{o.product_name} · ¥{o.amount} · {o.payment_method || '—'}
|
||||||
|
</div>
|
||||||
|
{o.trade_no && (
|
||||||
|
<div className="text-xs text-apple-gray-500 break-all mt-1">交易号:{o.trade_no}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 激活码区域 */}
|
||||||
|
{o.status === 'paid' && (
|
||||||
|
<div className="mt-3">
|
||||||
|
{license ? (
|
||||||
|
<div className="bg-gradient-to-r from-emerald-50 to-green-50 border border-green-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="flex items-center space-x-1 text-sm text-green-700 font-semibold">
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
<span>激活码</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-apple-gray-500">
|
||||||
|
{license.planName || license.plan}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs break-all text-apple-gray-900">
|
||||||
|
{license.activationCode}
|
||||||
|
</div>
|
||||||
|
{license.isPermanent === false && license.expiresAt && (
|
||||||
|
<div className="text-[11px] text-apple-gray-500 mt-1">
|
||||||
|
到期:{license.expiresAt}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{license.isPermanent && (
|
||||||
|
<div className="text-[11px] text-apple-gray-500 mt-1">永久有效</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : licenseError ? (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-xs text-red-700">
|
||||||
|
激活码生成失败:{licenseError.message || '请联系客服'}{' '}
|
||||||
|
{licenseError.at ? `(${licenseError.at})` : ''}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-xs text-amber-700">
|
||||||
|
激活码生成中,请稍后再查看
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
39
app/api/orders/route.ts
Normal file
39
app/api/orders/route.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getServerSession } from 'next-auth/next'
|
||||||
|
import { authOptions } from '@/lib/authOptions'
|
||||||
|
import { db } from '@/lib/db'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
function parseCallback(raw?: string | null): Record<string, any> {
|
||||||
|
if (!raw) return {}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return { raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session || !session.user?.id) {
|
||||||
|
return NextResponse.json({ message: '请先登录' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = parseInt(session.user.id)
|
||||||
|
const orders = await db.getUserOrders(userId)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
orders: orders.map((o) => ({
|
||||||
|
...o,
|
||||||
|
metadata: parseCallback(o.callback_data),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单列表失败:', error)
|
||||||
|
return NextResponse.json({ message: '获取订单失败' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
const LICENSE_API_URL = process.env.LICENSE_API_URL || 'http://45.249.208.118:9527/api/license/generate'
|
||||||
|
const LICENSE_API_KEY = process.env.LICENSE_API_KEY
|
||||||
|
|
||||||
// 验证嘟噜支付回调MD5签名
|
// 验证嘟噜支付回调MD5签名
|
||||||
function verifySign(params: Record<string, any>, key: string): boolean {
|
function verifySign(params: Record<string, any>, key: string): boolean {
|
||||||
const sign = params.sign
|
const sign = params.sign
|
||||||
@ -42,6 +45,86 @@ function verifySign(params: Record<string, any>, key: string): boolean {
|
|||||||
return calculatedSign === sign.toLowerCase()
|
return calculatedSign === sign.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCallbackData(raw?: string | null): Record<string, any> {
|
||||||
|
if (!raw) return {}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch (e) {
|
||||||
|
return { raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCallbackData(raw: string | null | undefined, patch: Record<string, any>) {
|
||||||
|
const base = parseCallbackData(raw)
|
||||||
|
return JSON.stringify({ ...base, ...patch })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射本地商品类型到许可证类型(按文档:STANDARD / PRO)
|
||||||
|
const productPlanMap: Record<string, 'STANDARD' | 'PRO'> = {
|
||||||
|
standard: 'STANDARD',
|
||||||
|
test: 'STANDARD',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateLicense(order: any) {
|
||||||
|
if (!LICENSE_API_KEY) {
|
||||||
|
console.warn('⚠️ LICENSE_API_KEY 未配置,跳过激活码生成')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = productPlanMap[order.product_type]
|
||||||
|
if (!plan) {
|
||||||
|
console.log('ℹ️ 当前商品不需要生成激活码,product_type:', order.product_type)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const signStr = `${order.order_no}${timestamp}${LICENSE_API_KEY}`
|
||||||
|
const sign = crypto.createHash('md5').update(signStr, 'utf8').digest('hex')
|
||||||
|
|
||||||
|
const user = await db.findUserById(order.user_id).catch(() => null)
|
||||||
|
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
orderNo: order.order_no,
|
||||||
|
plan,
|
||||||
|
customerEmail: user?.email,
|
||||||
|
customerName: user?.name,
|
||||||
|
contactInfo: user?.email,
|
||||||
|
timestamp,
|
||||||
|
sign,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(LICENSE_API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': LICENSE_API_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
let json: any = null
|
||||||
|
try {
|
||||||
|
json = await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 激活码接口返回解析失败:', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok || !json?.success) {
|
||||||
|
console.error('❌ 激活码生成失败:', {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
response: json,
|
||||||
|
})
|
||||||
|
throw new Error(json?.error || res.statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...json.data,
|
||||||
|
plan,
|
||||||
|
requestedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData()
|
const formData = await request.formData()
|
||||||
@ -103,20 +186,43 @@ export async function POST(request: NextRequest) {
|
|||||||
// 验证支付状态
|
// 验证支付状态
|
||||||
// 易支付通常 trade_status = 'TRADE_SUCCESS' 表示支付成功
|
// 易支付通常 trade_status = 'TRADE_SUCCESS' 表示支付成功
|
||||||
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
||||||
|
// 已有元数据
|
||||||
|
const existingMeta = parseCallbackData(order.callback_data)
|
||||||
|
|
||||||
|
// 生成激活码(仅指定商品类型)
|
||||||
|
let licenseData = existingMeta.license
|
||||||
|
let licenseError = existingMeta.licenseError
|
||||||
|
if (!licenseData) {
|
||||||
|
try {
|
||||||
|
licenseData = await generateLicense(order)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 激活码生成异常:', err)
|
||||||
|
licenseError = {
|
||||||
|
message: (err as any)?.message || '激活码生成失败',
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedMeta = mergeCallbackData(order.callback_data, {
|
||||||
|
payment: {
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
tradeStatus,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
...(licenseData ? { license: licenseData } : {}),
|
||||||
|
...(licenseError ? { licenseError } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
// 更新订单状态
|
// 更新订单状态
|
||||||
await db.updateOrderStatus(
|
await db.updateOrderStatus(orderNo, 'paid', tradeNo, paymentMethod, mergedMeta)
|
||||||
orderNo,
|
|
||||||
'paid',
|
|
||||||
tradeNo,
|
|
||||||
paymentMethod,
|
|
||||||
JSON.stringify(params)
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('✅ 订单支付成功:', {
|
console.log('✅ 订单支付成功:', {
|
||||||
orderNo,
|
orderNo,
|
||||||
tradeNo,
|
tradeNo,
|
||||||
amount: order.amount,
|
amount: order.amount,
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
|
licenseGenerated: !!licenseData,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: 这里可以添加其他业务逻辑,比如:
|
// TODO: 这里可以添加其他业务逻辑,比如:
|
||||||
@ -199,14 +305,27 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// 验证支付状态
|
// 验证支付状态
|
||||||
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
if (tradeStatus === 'TRADE_SUCCESS' || params.status === 'success' || params.code === '1') {
|
||||||
|
const existingMeta = parseCallbackData(order.callback_data)
|
||||||
|
let licenseData = existingMeta.license
|
||||||
|
if (!licenseData) {
|
||||||
|
try {
|
||||||
|
licenseData = await generateLicense(order)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 激活码生成异常:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedMeta = mergeCallbackData(order.callback_data, {
|
||||||
|
payment: {
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
tradeStatus,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
...(licenseData ? { license: licenseData } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
// 更新订单状态
|
// 更新订单状态
|
||||||
await db.updateOrderStatus(
|
await db.updateOrderStatus(orderNo, 'paid', tradeNo, paymentMethod, mergedMeta)
|
||||||
orderNo,
|
|
||||||
'paid',
|
|
||||||
tradeNo,
|
|
||||||
paymentMethod,
|
|
||||||
JSON.stringify(params)
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('✅ 订单支付成功:', {
|
console.log('✅ 订单支付成功:', {
|
||||||
orderNo,
|
orderNo,
|
||||||
|
|||||||
@ -5,6 +5,15 @@ import { db } from '@/lib/db'
|
|||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
function parseCallbackData(raw?: string | null): Record<string, any> {
|
||||||
|
if (!raw) return {}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return { raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// 验证登录状态
|
// 验证登录状态
|
||||||
@ -57,6 +66,7 @@ export async function GET(request: NextRequest) {
|
|||||||
trade_no: order.trade_no,
|
trade_no: order.trade_no,
|
||||||
created_at: order.created_at,
|
created_at: order.created_at,
|
||||||
paid_at: order.paid_at,
|
paid_at: order.paid_at,
|
||||||
|
...parseCallbackData(order.callback_data),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ function PaymentSuccessContent() {
|
|||||||
const orderNo = searchParams.get('order_no')
|
const orderNo = searchParams.get('order_no')
|
||||||
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
|
const [orderStatus, setOrderStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
|
||||||
const [orderInfo, setOrderInfo] = useState<any>(null)
|
const [orderInfo, setOrderInfo] = useState<any>(null)
|
||||||
|
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderNo) {
|
if (!orderNo) {
|
||||||
@ -59,6 +60,22 @@ function PaymentSuccessContent() {
|
|||||||
return () => clearInterval(pollInterval)
|
return () => clearInterval(pollInterval)
|
||||||
}, [orderNo])
|
}, [orderNo])
|
||||||
|
|
||||||
|
const handleCopy = async (text?: string) => {
|
||||||
|
if (!text) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
setCopyState('copied')
|
||||||
|
setTimeout(() => setCopyState('idle'), 1500)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('复制失败:', e)
|
||||||
|
setCopyState('error')
|
||||||
|
setTimeout(() => setCopyState('idle'), 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = orderInfo?.license
|
||||||
|
const licenseError = orderInfo?.licenseError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
|
<div className="min-h-screen bg-gradient-to-b from-apple-gray-50 to-white flex items-center justify-center px-4 py-12">
|
||||||
<div className="max-w-md w-full">
|
<div className="max-w-md w-full">
|
||||||
@ -128,6 +145,67 @@ function PaymentSuccessContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 激活码信息 */}
|
||||||
|
{license ? (
|
||||||
|
<div className="bg-gradient-to-r from-emerald-50 to-green-50 border border-green-200 rounded-2xl p-4 mb-6 text-left apple-shadow">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-green-600 font-semibold">激活码已生成</p>
|
||||||
|
<p className="text-xs text-apple-gray-500">请妥善保存,稍后可在此页面再次查看</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(license.activationCode)}
|
||||||
|
className="text-xs px-3 py-1 rounded-full bg-green-600 text-white hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
{copyState === 'copied' ? '已复制' : copyState === 'error' ? '复制失败' : '复制'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white border border-green-100 rounded-xl p-3 space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-apple-gray-600">激活码:</span>
|
||||||
|
<span className="font-mono font-semibold text-apple-gray-900 text-xs break-all">
|
||||||
|
{license.activationCode}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{license.planName && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-apple-gray-600">版本:</span>
|
||||||
|
<span className="font-semibold text-apple-gray-900">{license.planName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{license.isPermanent === false && license.expiresAt && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-apple-gray-600">到期时间:</span>
|
||||||
|
<span className="font-mono text-xs text-apple-gray-900">{license.expiresAt}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{license.isPermanent && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-apple-gray-600">有效期:</span>
|
||||||
|
<span className="font-semibold text-apple-gray-900">永久有效</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : licenseError ? (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6 text-left">
|
||||||
|
<p className="text-red-700 font-semibold mb-1">激活码生成失败</p>
|
||||||
|
<p className="text-xs text-apple-gray-600 mb-1">
|
||||||
|
{licenseError.message || '系统在生成激活码时出现错误,请联系客服处理。'}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-apple-gray-400">
|
||||||
|
时间:{licenseError.at}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-4 mb-6 text-left">
|
||||||
|
<p className="text-amber-700 font-semibold mb-1">激活码生成中</p>
|
||||||
|
<p className="text-xs text-apple-gray-600">
|
||||||
|
支付已成功,系统正在生成激活码。请稍等片刻或稍后重新打开此页面查看。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
|
|||||||
@ -81,12 +81,16 @@ useEffect(() => {
|
|||||||
{/* Login/User Info */}
|
{/* Login/User Info */}
|
||||||
{status === 'authenticated' && session ? (
|
{status === 'authenticated' && session ? (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center space-x-2 px-4 py-2 bg-apple-gray-50 rounded-full">
|
<a
|
||||||
|
href="/account"
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-apple-gray-50 rounded-full hover:bg-apple-gray-100 transition"
|
||||||
|
title="进入个人中心"
|
||||||
|
>
|
||||||
<User className="w-4 h-4 text-apple-blue" />
|
<User className="w-4 h-4 text-apple-blue" />
|
||||||
<span className="text-sm font-medium text-apple-gray-700">
|
<span className="text-sm font-medium text-apple-gray-700 max-w-[180px] truncate">
|
||||||
{session.user?.name || session.user?.email}
|
{session.user?.name || session.user?.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="p-2 text-apple-gray-600 hover:text-red-600 hover:bg-red-50 rounded-full transition-all"
|
className="p-2 text-apple-gray-600 hover:text-red-600 hover:bg-red-50 rounded-full transition-all"
|
||||||
@ -170,12 +174,17 @@ useEffect(() => {
|
|||||||
) : session ? (
|
) : session ? (
|
||||||
// 已登录
|
// 已登录
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center space-x-3 px-4 py-3 bg-apple-gray-50 rounded-xl">
|
<a
|
||||||
|
href="/account"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="flex items-center space-x-3 px-4 py-3 bg-apple-gray-50 rounded-xl hover:bg-apple-gray-100 transition"
|
||||||
|
title="进入个人中心"
|
||||||
|
>
|
||||||
<User className="w-5 h-5 text-apple-blue" />
|
<User className="w-5 h-5 text-apple-blue" />
|
||||||
<span className="text-sm font-medium text-apple-gray-700">
|
<span className="text-sm font-medium text-apple-gray-700 max-w-[200px] truncate">
|
||||||
{session.user?.name || session.user?.email}
|
{session.user?.name || session.user?.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="w-full flex items-center justify-center space-x-2 px-6 py-3 bg-red-50 text-red-600 rounded-full font-medium hover:bg-red-100 transition-all"
|
className="w-full flex items-center justify-center space-x-2 px-6 py-3 bg-red-50 text-red-600 rounded-full font-medium hover:bg-red-100 transition-all"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user