From 331257e8f45bcd2394f375e93939e43d6c7f045d Mon Sep 17 00:00:00 2001 From: moux1024 <403053463@qq.com> Date: Fri, 19 Sep 2025 20:33:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E9=82=80=E8=AF=B7?= =?UTF-8?q?=E8=B5=A0=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/checkin.ts | 39 --- api/signin.ts | 36 +++ app/share/page.tsx | 232 ++++++++++++++---- app/signup/page.tsx | 24 +- components/layout/sidebar.tsx | 4 +- .../{checkin-box.tsx => signin-box.tsx} | 68 +++-- components/layout/top-bar.tsx | 26 +- lib/auth.ts | 61 +++++ next.config.js | 10 + package.json | 4 +- 10 files changed, 358 insertions(+), 146 deletions(-) delete mode 100644 api/checkin.ts create mode 100644 api/signin.ts rename components/layout/{checkin-box.tsx => signin-box.tsx} (75%) diff --git a/api/checkin.ts b/api/checkin.ts deleted file mode 100644 index 906ba9e..0000000 --- a/api/checkin.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { get, post } from './request' -import { ApiResponse } from './common' - -/** - * 签到数据接口 - */ -export interface CheckinData { - hasCheckedInToday: boolean - points: number - lastCheckinDate: string | null - pointsHistory: Array<{ date: string; points: number; expiryDate: string }> -} - -/** - * 签到响应接口 - */ -export interface CheckinResponse { - success: boolean - points: number - message: string -} - -/** - * 获取用户签到状态和积分信息 - * @returns Promise - */ -export const getCheckinStatus = async (): Promise => { - const response = await get>('/api/user/checkin/status') - return response.data -} - -/** - * 执行签到操作 - * @returns Promise - */ -export const performCheckin = async (): Promise => { - const response = await post>('/api/user/checkin', {}) - return response.data -} diff --git a/api/signin.ts b/api/signin.ts new file mode 100644 index 0000000..2922540 --- /dev/null +++ b/api/signin.ts @@ -0,0 +1,36 @@ +import { get, post } from './request' +import { ApiResponse } from './common' + +/** + * 签到数据接口 + */ +export interface SigninData { + has_signin: boolean + credits: number + // signin_record: Array<{ date: string; points: number; expiryDate: string }> +} + +/** + * 签到响应接口 + */ +export interface SigninResponse { + credits: number +} + +/** + * 获取用户签到状态和积分信息 + * @returns Promise + */ +export const getSigninStatus = async (): Promise => { + const response = await get>('/api/user_fission/check_today_signin') + return response +} + +/** + * 执行签到操作 + * @returns Promise + */ +export const performSignin = async (): Promise => { + const response = await post>('/api/user_fission/signin', {}) + return response +} diff --git a/app/share/page.tsx b/app/share/page.tsx index 7057788..9444b20 100644 --- a/app/share/page.tsx +++ b/app/share/page.tsx @@ -1,6 +1,7 @@ "use client"; import React from 'react'; +import { get } from '@/api/request'; import { DashboardLayout } from "@/components/layout/dashboard-layout"; /** @@ -8,12 +9,43 @@ import { DashboardLayout } from "@/components/layout/dashboard-layout"; * Sections: Invite Flow, My Invitation Code, Invite Records (with pagination) */ +/** + * 邀请记录项 + */ type InviteRecord = { - id: string; - invitedUsername: string; - registeredAt: number; // epoch ms - rewardA: string; // reward item 1 (content TBD) - rewardB: string; // reward item 2 (content TBD) + user_email: string; + user_id: string; + user_name: string; + reg_time: number; + purchase_status: number; + reward_status: number; + invite_reward: number; + pay_reward: number; +}; + +/** + * 分页信息 + */ +type PaginationInfo = { + page: number; + page_size: number; + total: number; + total_pages: number; + has_next: boolean; + has_prev: boolean; +}; + +/** + * 邀请记录API响应 + */ +type InviteRecordsResponse = { + successful: boolean; + code: number; + message: string; + data: { + invited_list: InviteRecord[]; + pagination: PaginationInfo; + }; }; const PAGE_SIZE = 10; @@ -37,39 +69,79 @@ function formatLocalTime(epochMs: number): string { } /** - * Generate mocked invite records for demo purpose only. - * @param {number} count - number of items - * @returns {InviteRecord[]} - mocked records + * 从后端获取邀请记录 + * @returns {Promise} */ -function generateMockRecords(count: number): InviteRecord[] { - const now = Date.now(); - return Array.from({ length: count }).map((_, index) => ({ - id: String(index + 1), - invitedUsername: `user_${index + 1}`, - registeredAt: now - index * 36_000, // different times - rewardA: index % 3 === 0 ? '+100 points' : '—', - rewardB: index % 5 === 0 ? '3-day membership' : '—', - })); +async function fetchInviteRecords(): Promise { + const res = await get('/api/user_fission/query_invite_record'); + return res; } export default function SharePage(): JSX.Element { // Mocked data (to be replaced by real API integration later) - const [inviteCode] = React.useState('VF-ABCD-1234'); - const [invitedCount] = React.useState(37); + const [inviteCode, setInviteCode] = React.useState(''); + const [invitedCount, setInvitedCount] = React.useState(0); + const [totalInviteCredits, setTotalInviteCredits] = React.useState(0); const [copyState, setCopyState] = React.useState<'idle' | 'copied' | 'error'>('idle'); - const [records] = React.useState(() => generateMockRecords(47)); + const [records, setRecords] = React.useState([]); + const [pagination, setPagination] = React.useState({ + page: 1, + page_size: 20, + total: 0, + total_pages: 1, + has_next: false, + has_prev: false + }); const [pageIndex, setPageIndex] = React.useState(0); + const [expandedRowIds, setExpandedRowIds] = React.useState>(() => new Set()); - const totalPages = Math.max(1, Math.ceil(records.length / PAGE_SIZE)); - const pagedRecords = React.useMemo(() => { - const start = pageIndex * PAGE_SIZE; - return records.slice(start, start + PAGE_SIZE); - }, [records, pageIndex]); + React.useEffect(() => { + let mounted = true; + (async () => { + try { + const response = await fetchInviteRecords(); + if (mounted && response.successful) { + setRecords(response.data.invited_list); + setPagination(response.data.pagination); + } + } catch { + // 保持静默失败,页面仍可用 + } + // 获取邀请统计信息 + try { + const res = await get('/api/user_fission/my_invite_stats'); + const stats = res?.data ?? {}; + if (mounted) { + if (typeof stats.total_invited === 'number') { + setInvitedCount(stats.total_invited); + } + if (typeof stats.total_invite_credits === 'number') { + setTotalInviteCredits(stats.total_invite_credits); + } + } + } catch { + // 保持静默失败 + } + try { + const res = await get('/api/user_fission/my_invite_code'); + const code = res?.data?.invite_code ?? res?.data?.inviteCode ?? ''; + if (mounted && typeof code === 'string') setInviteCode(code); + } catch { + // 保持静默失败 + } + })(); + return () => { + mounted = false; + }; + }, []); + + const totalPages = pagination.total_pages; + const pagedRecords = records; // 后端已经分页,直接使用records const handleCopy = React.useCallback(async () => { try { - await navigator.clipboard.writeText(inviteCode); + await navigator.clipboard.writeText(location.origin + '/signup?inviteCode=' + inviteCode); setCopyState('copied'); window.setTimeout(() => setCopyState('idle'), 1600); } catch { @@ -78,8 +150,18 @@ export default function SharePage(): JSX.Element { } }, [inviteCode]); - const canPrev = pageIndex > 0; - const canNext = pageIndex < totalPages - 1; + + const toggleRow = React.useCallback((id: string) => { + setExpandedRowIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }, []); return ( @@ -101,22 +183,22 @@ export default function SharePage(): JSX.Element {
  1. - Step 1 - Share + Step 1 + Share

    Copy your invitation code and share it with friends.

  2. - Step 2 - Register + Step 2 + Register

    Friends register and enter your invitation code.

  3. - Step 3 - Reward + Step 3 + Reward

    You both receive rewards after successful registration.

  4. @@ -125,7 +207,7 @@ export default function SharePage(): JSX.Element { {/* Section 2: My Invitation Code */}
    -
    +

    My Invitation Code

    @@ -144,9 +226,14 @@ export default function SharePage(): JSX.Element {

    Share this code. Your friends can enter it during registration.

    +
    + Total Credits + {totalInviteCredits} + All credits earned from invitations. +
    Invited Friends - {invitedCount} + {invitedCount} Points detail will be available soon.
    @@ -162,20 +249,20 @@ export default function SharePage(): JSX.Element { className="inline-flex h-8 items-center justify-center rounded border border-gray-300 bg-white px-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50" type="button" onClick={() => setPageIndex((p) => Math.max(0, p - 1))} - disabled={!canPrev} + disabled={!pagination.has_prev} aria-label="Previous page" > Prev - {pageIndex + 1} / {totalPages} + {pagination.page} / {pagination.total_pages}
    - +
    - - - - + + + - {pagedRecords.map((r) => ( - - - - - - - ))} + {pagedRecords.map((r) => { + const isExpanded = expandedRowIds.has(r.user_id); + return ( + + + + + + + {isExpanded && ( + + + + )} + + ); + })}
    Invited UsernameRegistered AtRegistration RewardFirst Payment RewardInvited UsernameRegistered AtReward
    {r.invitedUsername}{formatLocalTime(r.registeredAt)}{r.rewardA}{r.rewardB}
    {r.user_name}{formatLocalTime(r.reg_time * 1000)} +
    +
    + {r.invite_reward + r.pay_reward} +
    + +
    +
    + + +
    + + + + + + + + + + + +
    {r.invite_reward}Register
    {r.pay_reward}First Payment
    +
    +
    diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 6e1e195..9293874 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -3,7 +3,7 @@ import React, { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import { signInWithGoogle, registerUser, sendVerificationLink } from "@/lib/auth"; +import { signInWithGoogle, sendVerificationLink, registerUserWithInvite } from "@/lib/auth"; import { GradientText } from "@/components/ui/gradient-text"; import { GoogleLoginButton } from "@/components/ui/google-login-button"; import { Eye, EyeOff, Mail } from "lucide-react"; @@ -31,6 +31,16 @@ export default function SignupPage() { // Handle scroll indicator for small screens React.useEffect(() => { + try { + const url = new URL(window.location.href); + const codeFromUrl = url.searchParams.get("inviteCode"); + const codeFromSession = sessionStorage.getItem("inviteCode"); + const code = codeFromUrl || codeFromSession || ""; + if (code) { + setInviteCode(code); + sessionStorage.setItem("inviteCode", code); + } + } catch (err) {} const handleScroll = () => { const scrollableElement = document.querySelector('.signup-form > div:nth-child(2)'); const formElement = document.querySelector('.signup-form'); @@ -208,19 +218,23 @@ export default function SignupPage() { try { // Use new registration API - await registerUser({ - userName: name, + const response = await registerUserWithInvite({ + name: name, email, password, - inviteCode: inviteCode || undefined, + invite_code: inviteCode || undefined, }); + // Clear inviteCode after successful registration + try { + sessionStorage.removeItem("inviteCode"); + } catch {} // Show activation modal instead of redirecting to login setShowActivationModal(true); setResendCooldown(60); } catch (error: any) { console.error("Signup error:", error); - setFormError(error.message||error.msg || "Registration failed, please try again"); + setFormError(error.message || "Registration failed, please try again"); } finally { setIsSubmitting(false); } diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index aba8bb0..f915590 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -20,7 +20,8 @@ import { PanelsLeftBottom, ArrowLeftToLine, BookHeart, - PanelRightClose + PanelRightClose, + Gift } from 'lucide-react'; interface SidebarProps { @@ -33,6 +34,7 @@ const navigationItems = [ title: 'Main', items: [ { name: 'My Portfolio', href: '/movies', icon: BookHeart }, + { name: 'Share', href: '/share', icon: Gift }, ], } ]; diff --git a/components/layout/checkin-box.tsx b/components/layout/signin-box.tsx similarity index 75% rename from components/layout/checkin-box.tsx rename to components/layout/signin-box.tsx index 52f221b..3ab76af 100644 --- a/components/layout/checkin-box.tsx +++ b/components/layout/signin-box.tsx @@ -4,15 +4,13 @@ import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Coins, Trophy, HelpCircle } from "lucide-react" -import { getCheckinStatus, performCheckin, CheckinData } from "@/api/checkin" +import { getSigninStatus, performSignin, SigninData } from "@/api/signin" -export default function CheckinPage() { - const [checkinData, setCheckinData] = useState({ - hasCheckedInToday: false, - points: 0, - lastCheckinDate: null, - pointsHistory: [], +export default function SigninPage() { + const [signinData, setSigninData] = useState({ + has_signin: false, + credits: 0 }) const [isLoading, setIsLoading] = useState(false) const [showTip, setShowTip] = useState(false) @@ -20,15 +18,15 @@ export default function CheckinPage() { /** - * Fetch checkin status + * Fetch signin status */ - const fetchCheckinStatus = async () => { + const fetchSigninStatus = async () => { try { setIsInitialLoading(true) - const data = await getCheckinStatus() - setCheckinData(data) + const data = await getSigninStatus() + setSigninData(data.data as SigninData) } catch (error) { - console.error('Failed to fetch checkin status:', error) + console.error('Failed to fetch signin status:', error) // Keep default state } finally { setIsInitialLoading(false) @@ -36,25 +34,25 @@ export default function CheckinPage() { } useEffect(() => { - fetchCheckinStatus() + fetchSigninStatus() }, []) /** - * Perform checkin operation + * Perform signin operation */ - const handleCheckin = async () => { - if (checkinData.hasCheckedInToday) return + const handleSignin = async () => { + if (signinData.has_signin) return try { setIsLoading(true) - const response = await performCheckin() + const response = await performSignin() - if (response.success) { - // Refresh status after successful checkin - await fetchCheckinStatus() + if (response.successful) { + // Refresh status after successful signin + await fetchSigninStatus() } } catch (error) { - console.error('Checkin failed:', error) + console.error('Signin failed:', error) } finally { setIsLoading(false) } @@ -79,14 +77,14 @@ export default function CheckinPage() { return (
    - {/* Checkin status card */} + {/* Signin status card */}

    - Daily Check-in + Daily Sign-in

    -

    Check in to earn credits, credits valid for 7 days

    +

    Sign in to earn credits. Credits are valid for 7 days

    - {/* Check-in button */} + {/* Sign-in button */} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 304e797..850c9c6 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -33,7 +33,7 @@ import { } from "@/lib/stripe"; import UserCard from "@/components/common/userCard"; import { showInsufficientPointsNotification } from "@/utils/notifications"; -import CheckinBox from "./checkin-box"; +import SigninBox from "./signin-box"; interface User { id: string; @@ -61,7 +61,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe const [isLoadingSubscription, setIsLoadingSubscription] = useState(false); const [isBuyingTokens, setIsBuyingTokens] = useState(false); const [customAmount, setCustomAmount] = useState(""); - const [isCheckinModalOpen, setIsCheckinModalOpen] = useState(false); + const [isSigninModalOpen, setIsSigninModalOpen] = useState(false); // 获取用户订阅信息 const fetchSubscriptionInfo = async () => { @@ -249,8 +249,8 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe /** * 处理签到功能,打开签到modal */ - const handleCheckin = () => { - setIsCheckinModalOpen(true); + const handleSignin = () => { + setIsSigninModalOpen(true); }; return ( @@ -408,16 +408,16 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe {currentUser.email}

    - {/* Check-in entry */} - {/*
    + {/* Sign-in entry */} +
    -
    */} +
    {/* AI 积分 */} @@ -552,15 +552,15 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe )} - {/* Check-in Modal */} - + {/* Sign-in Modal */} +
    - +
    diff --git a/lib/auth.ts b/lib/auth.ts index a4c5229..fe2b0b1 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -8,6 +8,26 @@ const JAVA_BASE_URL = process.env.NEXT_PUBLIC_JAVA_URL || ''; const TOKEN_KEY = 'token'; const USER_KEY = 'currentUser'; +/** + * 注册用户响应数据 + */ +type RegisterUserData = { + user_id: string; + email: string; + name: string; + invite_code: string; +}; + +/** + * 注册用户API响应 + */ +type RegisterUserResponse = { + code: number; + message: string; + data: RegisterUserData; + successful: boolean; +}; + /** * 登录用户 */ @@ -370,7 +390,48 @@ export const registerUser = async ({ } }; +export const registerUserWithInvite = async ({ + name, + password, + email, + invite_code, +}: { + name: string; + password: string; + email: string; + invite_code?: string; +}): Promise => { + try { + const response = await fetch(`${BASE_URL}/api/user_fission/register_with_invite`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + password, + email, + invite_code, + }), + }); + const data = await response.json(); + if(!data.successful){ + throw new Error(data.message || '注册失败'); + } + return data as RegisterUserResponse; + } catch (error) { + console.error('Register with invite failed:', error); + throw error; + } +}; + +/** + * 发送验证链接 + * @param {string} email - 邮箱 + * @returns {Promise} 发送验证链接结果 + */ export const sendVerificationLink = async (email: string) => { try { const response = await fetch(`${JAVA_BASE_URL}/api/user/sendVerificationLink?email=${email}`); diff --git a/next.config.js b/next.config.js index e581774..b72d9b6 100644 --- a/next.config.js +++ b/next.config.js @@ -30,6 +30,16 @@ const nextConfig = { }, }; } + + // 优化缓存配置,解决缓存错误问题 + if (dev) { + config.cache = { + type: 'memory', + // 设置内存缓存大小限制(512MB) + maxGenerations: 1, + }; + } + return config; }, diff --git a/package.json b/package.json index eb110bd..f8f0d62 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "start": "next start", "lint": "next lint", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "clear-cache": "bash scripts/clear-cache.sh", + "dev:clean": "npm run clear-cache && npm run dev" }, "dependencies": { "@dnd-kit/core": "^6.3.1",