diff --git a/public/images/collect.svg b/public/images/collect.svg deleted file mode 100644 index 7f029d5..0000000 --- a/public/images/collect.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/comment.svg b/public/images/comment.svg deleted file mode 100644 index 2540879..0000000 --- a/public/images/comment.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/footer_sw.svg b/public/images/footer_sw.svg deleted file mode 100644 index 98dab2f..0000000 --- a/public/images/footer_sw.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/footer_x.svg b/public/images/footer_x.svg deleted file mode 100644 index eb12cfe..0000000 --- a/public/images/footer_x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/history.svg b/public/images/history.svg deleted file mode 100644 index dce15df..0000000 --- a/public/images/history.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/home.svg b/public/images/home.svg deleted file mode 100644 index 4cd9253..0000000 --- a/public/images/home.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/icon_collect.svg b/public/images/icon_collect.svg deleted file mode 100644 index ca9a097..0000000 --- a/public/images/icon_collect.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/icon_like.svg b/public/images/icon_like.svg deleted file mode 100644 index 94ddf6a..0000000 --- a/public/images/icon_like.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/icon_share.svg b/public/images/icon_share.svg deleted file mode 100644 index edfdbec..0000000 --- a/public/images/icon_share.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/more_arrow.svg b/public/images/more_arrow.svg deleted file mode 100644 index f86c8ac..0000000 --- a/public/images/more_arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/my_dz.png b/public/images/my_dz.png deleted file mode 100644 index 5a6356b..0000000 Binary files a/public/images/my_dz.png and /dev/null differ diff --git a/public/images/my_sc.png b/public/images/my_sc.png deleted file mode 100644 index d520ee5..0000000 Binary files a/public/images/my_sc.png and /dev/null differ diff --git a/public/images/my_sz.png b/public/images/my_sz.png deleted file mode 100644 index 4d89bef..0000000 Binary files a/public/images/my_sz.png and /dev/null differ diff --git a/public/images/nav_x.svg b/public/images/nav_x.svg deleted file mode 100644 index 2f16bac..0000000 --- a/public/images/nav_x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/playing.svg b/public/images/playing.svg deleted file mode 100644 index 40a1cc9..0000000 --- a/public/images/playing.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/images/profile.svg b/public/images/profile.svg deleted file mode 100644 index 57b8d01..0000000 --- a/public/images/profile.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/screen.svg b/public/images/screen.svg deleted file mode 100644 index 9f435ad..0000000 --- a/public/images/screen.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/images/share_h5.svg b/public/images/share_h5.svg deleted file mode 100644 index c0c903d..0000000 --- a/public/images/share_h5.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/tag_mf.png b/public/images/tag_mf.png deleted file mode 100644 index 331a8fb..0000000 Binary files a/public/images/tag_mf.png and /dev/null differ diff --git a/public/images/unscreen.svg b/public/images/unscreen.svg deleted file mode 100644 index 405c041..0000000 --- a/public/images/unscreen.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index cbba754..0000000 Binary files a/public/logo.png and /dev/null differ diff --git a/src/app/favorites/page.tsx b/src/app/favorites/page.tsx deleted file mode 100644 index 41dfc6a..0000000 --- a/src/app/favorites/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { FavoritesPage } from "@/components/favorites/favorites-page"; - -export const metadata: Metadata = { - title: "我的收藏 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx deleted file mode 100644 index 3d3cb6a..0000000 --- a/src/app/history/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { HistoryPage } from "@/components/history/history-page"; - -export const metadata: Metadata = { - title: "观看历史 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/app/likes/page.tsx b/src/app/likes/page.tsx deleted file mode 100644 index e8a7ff2..0000000 --- a/src/app/likes/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { LikesPage } from "@/components/likes/likes-page"; - -export const metadata: Metadata = { - title: "我的点赞 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx deleted file mode 100644 index 3d9a5e9..0000000 --- a/src/app/profile/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { ProfilePage } from "@/components/profile/profile-page"; - -export const metadata: Metadata = { - title: "个人中心 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx deleted file mode 100644 index 1d3c33a..0000000 --- a/src/app/settings/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { SettingsPage } from "@/components/settings/settings-page"; - -export const metadata: Metadata = { - title: "账号设置 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/app/settings/profile/page.tsx b/src/app/settings/profile/page.tsx deleted file mode 100644 index fb64563..0000000 --- a/src/app/settings/profile/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { EditProfilePage } from "@/components/settings/edit-profile-page"; - -export const metadata: Metadata = { - title: "编辑个人资料 - 海角社区", - robots: { index: false, follow: false }, -}; - -export default function Page() { - return ; -} diff --git a/src/components/profile/use-mock-user.ts b/src/components/auth/use-mock-user.ts similarity index 100% rename from src/components/profile/use-mock-user.ts rename to src/components/auth/use-mock-user.ts diff --git a/src/components/collection/collection-page.tsx b/src/components/collection/collection-page.tsx deleted file mode 100644 index 6fc7c16..0000000 --- a/src/components/collection/collection-page.tsx +++ /dev/null @@ -1,212 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useRef, useState } from "react"; - -import { EmptyState } from "@/components/common/empty-state"; -import { Pagination } from "@/components/common/pagination"; -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import { H5ShortDramaCard, ShortDramaCard } from "@/components/short-drama/short-drama-card"; -import { H5VideoCard, VideoCard } from "@/components/video/video-card"; - -type CollectionPageProps = { - title: string; - subtitle: string; - breadcrumb: string; - types: readonly T[]; - mobileTypes?: readonly T[]; - scopes: readonly S[]; - defaultScope: S; - totalPages: number; - getItems: (type: T, scope: S, page: number) => ShortDramaItem[]; - clearable?: boolean; - showMobileScopes?: boolean; - showPcScopes?: boolean; - showVideoScopes?: boolean; - showMobileEpisodes?: boolean; - emptyText: string; -}; - -type FilterPillsProps = { - options: readonly T[]; - value: T; - onChange: (value: T) => void; - className?: string; - itemClassName: string; -}; - -function FilterPills({ options, value, onChange, className = "", itemClassName }: FilterPillsProps) { - return ( -
- {options.map((option) => ( - - ))} -
- ); -} - -export function CollectionPage({ - title, - subtitle, - breadcrumb, - types, - mobileTypes = types, - scopes, - defaultScope, - totalPages, - getItems, - clearable = false, - showMobileScopes = true, - showPcScopes = true, - showVideoScopes = true, - showMobileEpisodes = true, - emptyText, -}: CollectionPageProps) { - const router = useRouter(); - const [type, setType] = useState(types[0]); - const [scope, setScope] = useState(defaultScope); - const [page, setPage] = useState(1); - const [cleared, setCleared] = useState(false); - const topRef = useRef(null); - const items = cleared ? [] : getItems(type, scope, page); - const isVideo = type === "视频"; - const PcCard = isVideo ? VideoCard : ShortDramaCard; - const MobileCard = isVideo ? H5VideoCard : H5ShortDramaCard; - - const changeType = (next: T) => { - setType(next); - setPage(1); - }; - const changeScope = (next: S) => { - setScope(next); - setPage(1); - }; - const changePage = (next: number) => { - setPage(next); - topRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - const goBack = () => { - if (window.history.length > 1) router.back(); - else router.push("/profile"); - }; - - return ( -
-
- -

{title}

- {clearable ? ( - - ) : null} -
- -
- - -
-
-

{title}

-

{subtitle}

-
- {clearable ? ( - - ) : null} -
- -
- - - {isVideo && !showVideoScopes ? null : ( - - )} -
- - {items.length === 0 ? ( - - ) : ( - <> -
- {items.map((item) => ( - - ))} -
- -
- {items.map((item) => ( - - ))} -
- - - - )} -
-
- ); -} diff --git a/src/components/common/comments/comment-panel.tsx b/src/components/common/comments/comment-panel.tsx deleted file mode 100644 index 7cb76f3..0000000 --- a/src/components/common/comments/comment-panel.tsx +++ /dev/null @@ -1,284 +0,0 @@ -"use client"; - -import { useState, type FormEvent } from "react"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { useMockUser } from "@/components/profile/use-mock-user"; -import type { CommentInfo } from "@/lib/api/types"; - -import { COMMENT_MAX_LENGTH, COMMENT_SELF_NAME, COMMENT_SORTS, formatCommentTime, type CommentTarget } from "./data"; -import { CommentReplyPreview } from "./comment-reply-preview"; -import { HeartIcon } from "./heart-icon"; -import { InlineReplyBox } from "./inline-reply-box"; -import { useComments } from "./use-comments"; - -const fallbackAvatar = "/images/default_avatar.svg"; - -const panelStyles = { - detail: { - section: "rounded-2xl bg-[rgba(24,23,46,0.88)] outline-[#383657]", - item: "py-4", - more: "mt-4 h-[43px] rounded-[7px]", - }, - play: { - section: "rounded-xl bg-[rgba(19,15,22,0.91)] outline-[rgba(199,145,89,0.7)]", - item: "py-3", - more: "mt-3 h-8 rounded-lg", - }, -}; - -type ReplyTarget = { - id: number; - userName: string; -}; - -function CommentComposer({ - onSubmit, - posting, - error, -}: { - onSubmit: (content: string) => Promise; - posting: boolean; - error: string | null; -}) { - const { user } = useMockUser(); - const avatar = user?.avatar || fallbackAvatar; - const displayName = user?.name || COMMENT_SELF_NAME; - const [text, setText] = useState(""); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - const content = text.trim(); - if (!content || posting) return; - const ok = await onSubmit(content); - if (ok) setText(""); - }; - - return ( -
-
- - - - - {error && !posting ?

{error}

: null} -
- ); -} - -/** PC 评论面板(详情 / 播放页)— GET /api/app/comment/list */ -export function CommentPanel({ - variant = "detail", - target, -}: { - variant?: keyof typeof panelStyles; - target: CommentTarget | null; -}) { - const { - sort, - setSort, - comments, - total, - hasMore, - loading, - loadingMore, - error, - loadMore, - toggleLike, - post, - posting, - postError, - reload, - } = useComments({ target }); - const styles = panelStyles[variant]; - const [replyTo, setReplyTo] = useState(null); - const [expandedIds, setExpandedIds] = useState>(() => new Set()); - - const startReply = (comment: CommentInfo) => { - const id = Number(comment.id); - if (!Number.isInteger(id) || id < 1) return; - setReplyTo({ id, userName: (comment.userName || "").trim() || "用户" }); - }; - - const toggleExpanded = (commentId: number) => { - setExpandedIds((current) => { - const next = new Set(current); - if (next.has(commentId)) next.delete(commentId); - else next.add(commentId); - return next; - }); - }; - - const submitRoot = async (content: string) => post(content); - - const submitReply = async (content: string) => { - if (!replyTo) return false; - const ok = await post(content, { parentsId: replyTo.id }); - if (ok) setReplyTo(null); - return ok; - }; - - return ( -
-
-

评论 ({total})

-
- {COMMENT_SORTS.map((item) => ( - - ))} -
-
- - - - {loading ? ( -

评论加载中…

- ) : error ? ( -
-

{error}

- -
- ) : comments.length === 0 ? ( -

暂无评论

- ) : ( -
    - {comments.map((comment) => { - const liked = Boolean(comment.isLike); - const commentId = Number(comment.id) || 0; - const expanded = expandedIds.has(commentId); - const replies = comment.subComment ?? []; - const isReplying = replyTo?.id === commentId; - return ( -
  • - -
    -

    {comment.userName}

    -

    {comment.text}

    -
    - {formatCommentTime(comment.createdAt)} - - -
    - {isReplying && replyTo ? ( - setReplyTo(null)} - /> - ) : null} - {expanded && replies.length > 1 ? ( -
    - {replies.map((reply) => ( -

    - {reply.replyUserName - ? `${reply.userName || ""} 回复 ${reply.replyUserName}:${reply.text || ""}` - : `${reply.userName || ""}:${reply.text || ""}`} -

    - ))} - -
    - ) : ( - 1 ? () => toggleExpanded(commentId) : undefined} - /> - )} -
    -
  • - ); - })} -
- )} - - {!loading && !error && comments.length > 0 ? ( - - ) : null} -
- ); -} diff --git a/src/components/common/comments/comment-reply-preview.tsx b/src/components/common/comments/comment-reply-preview.tsx deleted file mode 100644 index 84934f5..0000000 --- a/src/components/common/comments/comment-reply-preview.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { CommentInfo } from "@/lib/api/types"; - -/** 直接展示接口 subComment / commentCount,不做字段改名 */ -export function CommentReplyPreview({ - comment, - className = "", - textClassName = "", - onMore, -}: { - comment: CommentInfo; - className?: string; - textClassName?: string; - onMore?: () => void; -}) { - const first = comment.subComment?.[0]; - const more = Number(comment.commentCount) || comment.subComment?.length || 0; - if (!first && more < 1) return null; - - const replyLabel = first - ? first.replyUserName - ? `${first.userName || ""} 回复 ${first.replyUserName}:${first.text || ""}` - : `${first.userName || ""}:${first.text || ""}` - : `共 ${more} 条回复`; - - return ( -
-

{replyLabel}

- {more > 0 && onMore ? ( - - ) : null} -
- ); -} diff --git a/src/components/common/comments/data.ts b/src/components/common/comments/data.ts deleted file mode 100644 index ce79ffd..0000000 --- a/src/components/common/comments/data.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { COMMENT_OBJECT_TYPE, type CommentInfo, type CommentObjectType } from "@/lib/api/types"; - -export type { CommentInfo }; - -export type CommentTarget = { - objectId: number; - objectType: CommentObjectType; -}; - -export const COMMENT_SORTS = ["热门", "最新"] as const; -export type CommentSort = (typeof COMMENT_SORTS)[number]; - -export const COMMENT_SELF_NAME = "剧迷1024"; -export const COMMENT_MAX_LENGTH = 200; -export const COMMENT_PAGE_SIZE = 6; -/** 列表请求超时,避免上游卡住拖住 UI */ -export const COMMENT_FETCH_TIMEOUT_MS = 8000; - -export function sortToOrder(sort: CommentSort): 1 | 2 { - return sort === "热门" ? 1 : 2; -} - -/** 过滤无效评论,保留接口原字段 */ -export function filterCommentList(list: CommentInfo[] | null | undefined): CommentInfo[] { - return (list ?? []).filter((item) => { - const id = Number(item?.id); - return Number.isInteger(id) && id > 0; - }); -} - -/** 本地乐观插入用,字段名与 CommentInfo 一致 */ -export function createSelfComment( - text: string, - options?: { userName?: string; avatar?: string }, -): CommentInfo { - return { - id: -Date.now(), - userName: (options?.userName || "").trim() || COMMENT_SELF_NAME, - avatar: (options?.avatar || "").trim() || "/images/default_avatar.svg", - text, - createdAt: new Date().toISOString(), - countLike: 0, - isLike: false, - commentCount: 0, - subComment: [], - }; -} - -export function formatCommentCount(value: number) { - return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : String(value); -} - -/** 仅做展示格式化,不改字段名;空则回退原串 */ -export function formatCommentTime(value?: string | null) { - const raw = (value || "").trim(); - if (!raw) return ""; - const date = new Date(raw); - if (Number.isNaN(date.getTime())) return raw; - const pad = (n: number) => String(n).padStart(2, "0"); - return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${pad(date.getHours())}:${pad(date.getMinutes())}`; -} - -export function commentTarget( - objectId: number | string | null | undefined, - objectType: CommentObjectType, -): CommentTarget | null { - const id = Number(objectId); - if (!Number.isInteger(id) || id < 1) return null; - return { objectId: id, objectType }; -} - -export function playletCommentTarget(playletId: number | string | null | undefined) { - return commentTarget(playletId, COMMENT_OBJECT_TYPE.playlet); -} - -export function playletChapterCommentTarget(chapterId: number | string | null | undefined) { - return commentTarget(chapterId, COMMENT_OBJECT_TYPE.playletChapter); -} - -export function videoCommentTarget(videoId: number | string | null | undefined) { - return commentTarget(videoId, COMMENT_OBJECT_TYPE.video); -} diff --git a/src/components/common/comments/h5-comment-sheet.tsx b/src/components/common/comments/h5-comment-sheet.tsx deleted file mode 100644 index a5379d8..0000000 --- a/src/components/common/comments/h5-comment-sheet.tsx +++ /dev/null @@ -1,204 +0,0 @@ -"use client"; - -import { useState, type FormEvent, type ReactNode } from "react"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; - -import { COMMENT_MAX_LENGTH, COMMENT_SORTS, formatCommentTime, type CommentTarget } from "./data"; -import { CommentReplyPreview } from "./comment-reply-preview"; -import { HeartIcon } from "./heart-icon"; -import { InlineReplyBox } from "./inline-reply-box"; -import { useComments } from "./use-comments"; - -function SheetHead({ title, onClose, children }: { title: ReactNode; onClose: () => void; children?: ReactNode }) { - return ( -
- -

{title}

- {children ?? } -
- ); -} - -/** H5 短剧播放页评论底部弹层 */ -export function H5CommentSheet({ onClose, target }: { onClose: () => void; target: CommentTarget | null }) { - const { - sort, - setSort, - comments, - total, - hasMore, - loading, - loadingMore, - error, - loadMore, - toggleLike, - post, - posting, - postError, - reload, - } = useComments({ target }); - const [text, setText] = useState(""); - const [replyTo, setReplyTo] = useState<{ id: number; userName: string } | null>(null); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - const content = text.trim(); - if (!content || posting || replyTo) return; - const ok = await post(content); - if (ok) setText(""); - }; - - const submitReply = async (content: string) => { - if (!replyTo) return false; - const ok = await post(content, { parentsId: replyTo.id }); - if (ok) setReplyTo(null); - return ok; - }; - - return ( -
-
- -
- {COMMENT_SORTS.map((item) => ( - - ))} -
-
-
-
- {loading ? ( -

评论加载中…

- ) : error ? ( -
-

{error}

- -
- ) : comments.length === 0 ? ( -

暂无评论

- ) : ( -
    - {comments.map((comment) => { - const liked = Boolean(comment.isLike); - const commentId = Number(comment.id) || 0; - const isReplying = replyTo?.id === commentId; - return ( -
  • - -
    -

    {comment.userName}

    -

    {comment.text}

    -
    - {formatCommentTime(comment.createdAt)} - - -
    - {isReplying && replyTo ? ( - setReplyTo(null)} - /> - ) : null} - -
    -
  • - ); - })} -
- )} - {!loading && !error && comments.length > 0 ? ( - - ) : null} -
-
- {!replyTo && postError ?

{postError}

: null} -
- setText(event.target.value)} - placeholder="发表评论…" - aria-label="评论内容" - className="h-9 min-w-0 flex-1 rounded-full bg-white/5 px-4 text-sm text-white outline-none placeholder:text-white/35 disabled:opacity-60" - /> - -
-
-
- ); -} diff --git a/src/components/common/comments/h5-comments-tab.tsx b/src/components/common/comments/h5-comments-tab.tsx deleted file mode 100644 index 374061e..0000000 --- a/src/components/common/comments/h5-comments-tab.tsx +++ /dev/null @@ -1,209 +0,0 @@ -"use client"; - -import { useState, type FormEvent, type ReactNode } from "react"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; - -import { COMMENT_MAX_LENGTH, COMMENT_SORTS, formatCommentCount, formatCommentTime } from "./data"; -import { CommentReplyPreview } from "./comment-reply-preview"; -import { InlineReplyBox } from "./inline-reply-box"; -import type { UseCommentsResult } from "./use-comments"; - -type H5CommentsTabProps = { - /** 由页面 useComments 托管,便于 Tab 标题同步总数 */ - state: UseCommentsResult; - footerExtra?: ReactNode; -}; - -/** H5 视频播放页「评论」Tab */ -export function H5CommentsTab({ state, footerExtra }: H5CommentsTabProps) { - const { - sort, - setSort, - comments, - total, - hasMore, - loading, - loadingMore, - error, - loadMore, - toggleLike, - post, - posting, - postError, - reload, - } = state; - const [text, setText] = useState(""); - const [replyTo, setReplyTo] = useState<{ id: number; userName: string } | null>(null); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - const content = text.trim(); - if (!content || posting || replyTo) return; - const ok = await post(content); - if (ok) setText(""); - }; - - const submitReply = async (content: string) => { - if (!replyTo) return false; - const ok = await post(content, { parentsId: replyTo.id }); - if (ok) setReplyTo(null); - return ok; - }; - - return ( -
-
-
-

{total}条评论

-
- {COMMENT_SORTS.map((item) => ( - - ))} -
-
- - {loading ? ( -

评论加载中…

- ) : error ? ( -
-

{error}

- -
- ) : comments.length === 0 ? ( -

暂无评论

- ) : ( -
    - {comments.map((comment) => { - const liked = Boolean(comment.isLike); - const commentId = Number(comment.id) || 0; - const isReplying = replyTo?.id === commentId; - return ( -
  • - -
    -

    {comment.userName}

    -

    {comment.text}

    -
    -
    - {formatCommentTime(comment.createdAt)} - -
    - -
    - {isReplying && replyTo ? ( - setReplyTo(null)} - /> - ) : null} - -
    -
  • - ); - })} -
- )} - - {!loading && !error && comments.length > 0 ? ( - - ) : null} -
-
- {!replyTo && postError ?

{postError}

: null} -
- -
-
- {footerExtra} -
- ); -} diff --git a/src/components/common/comments/heart-icon.tsx b/src/components/common/comments/heart-icon.tsx deleted file mode 100644 index ae77c6d..0000000 --- a/src/components/common/comments/heart-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export function HeartIcon({ filled, className = "size-[18px]" }: { filled: boolean; className?: string }) { - return ( - - ); -} diff --git a/src/components/common/comments/index.ts b/src/components/common/comments/index.ts deleted file mode 100644 index 54c8887..0000000 --- a/src/components/common/comments/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export { - COMMENT_FETCH_TIMEOUT_MS, - COMMENT_MAX_LENGTH, - COMMENT_PAGE_SIZE, - COMMENT_SELF_NAME, - COMMENT_SORTS, - commentTarget, - createSelfComment, - filterCommentList, - formatCommentCount, - formatCommentTime, - playletChapterCommentTarget, - playletCommentTarget, - sortToOrder, - videoCommentTarget, - type CommentSort, - type CommentTarget, -} from "./data"; -export { CommentPanel } from "./comment-panel"; -export { CommentReplyPreview } from "./comment-reply-preview"; -export { H5CommentSheet } from "./h5-comment-sheet"; -export { H5CommentsTab } from "./h5-comments-tab"; -export { HeartIcon } from "./heart-icon"; -export { useComments, type UseCommentsOptions, type UseCommentsResult } from "./use-comments"; -export type { CommentInfo } from "@/lib/api/types"; diff --git a/src/components/common/comments/inline-reply-box.tsx b/src/components/common/comments/inline-reply-box.tsx deleted file mode 100644 index 1771089..0000000 --- a/src/components/common/comments/inline-reply-box.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState, type FormEvent } from "react"; - -import { COMMENT_MAX_LENGTH } from "./data"; - -/** 挂在单条评论下方的回复输入框 */ -export function InlineReplyBox({ - userName, - posting, - error, - onSubmit, - onCancel, - compact = false, -}: { - userName: string; - posting: boolean; - error?: string | null; - onSubmit: (content: string) => Promise; - onCancel: () => void; - compact?: boolean; -}) { - const [text, setText] = useState(""); - const inputRef = useRef(null); - - useEffect(() => { - inputRef.current?.focus(); - }, []); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - const content = text.trim(); - if (!content || posting) return; - const ok = await onSubmit(content); - if (ok) setText(""); - }; - - return ( -
-
- - 回复 @{userName} - - -
-
- - -
- {error ?

{error}

: null} -
- ); -} diff --git a/src/components/common/comments/use-comments.ts b/src/components/common/comments/use-comments.ts deleted file mode 100644 index f4c6fa9..0000000 --- a/src/components/common/comments/use-comments.ts +++ /dev/null @@ -1,315 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useRef, useState } from "react"; -import { toast } from "@heroui/react"; - -import { openLoginDialog } from "@/components/auth/login-dialog"; -import { getCommentList, sendComment, setCommentLike } from "@/lib/api/comment"; -import type { CommentInfo } from "@/lib/api/types"; -import { handleUnauthorized } from "@/lib/auth/handle-unauthorized"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -import { - COMMENT_FETCH_TIMEOUT_MS, - COMMENT_PAGE_SIZE, - filterCommentList, - sortToOrder, - type CommentSort, - type CommentTarget, -} from "./data"; - -export type UseCommentsOptions = { - target: CommentTarget | null; - initialSort?: CommentSort; - pageSize?: number; -}; - -export type UseCommentsResult = { - sort: CommentSort; - setSort: (sort: CommentSort) => void; - comments: CommentInfo[]; - total: number; - hasMore: boolean; - loading: boolean; - loadingMore: boolean; - posting: boolean; - error: string | null; - postError: string | null; - loadMore: () => void; - toggleLike: (comment: CommentInfo) => Promise; - post: (text: string, options?: { parentsId?: number }) => Promise; - reload: () => void; -}; - -function formatCommentError(err: unknown, fallback = "评论加载失败") { - if (err instanceof DOMException && err.name === "TimeoutError") return "加载超时,请重试"; - if (err instanceof Error) { - if (/aborted|timeout|超时/i.test(err.message) || err.name === "AbortError") return "加载超时,请重试"; - return err.message || fallback; - } - return fallback; -} - -function abortAfter(ms: number) { - if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") { - return AbortSignal.timeout(ms); - } - const controller = new AbortController(); - setTimeout(() => controller.abort(), ms); - return controller.signal; -} - -function mergeSignals(a: AbortSignal, b: AbortSignal) { - if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function") { - return AbortSignal.any([a, b]); - } - const controller = new AbortController(); - const onAbort = () => controller.abort(); - if (a.aborted || b.aborted) { - controller.abort(); - return controller.signal; - } - a.addEventListener("abort", onAbort, { once: true }); - b.addEventListener("abort", onAbort, { once: true }); - return controller.signal; -} - -async function resolveAuthToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -function patchComment(list: CommentInfo[], id: number, patch: Partial): CommentInfo[] { - return list.map((item) => (Number(item.id) === id ? { ...item, ...patch } : item)); -} - -export function useComments({ - target, - initialSort = "热门", - pageSize = COMMENT_PAGE_SIZE, -}: UseCommentsOptions): UseCommentsResult { - const [sort, setSort] = useState(initialSort); - const [comments, setComments] = useState([]); - const [total, setTotal] = useState(0); - const [pageNum, setPageNum] = useState(1); - const [loading, setLoading] = useState(Boolean(target)); - const [loadingMore, setLoadingMore] = useState(false); - const [posting, setPosting] = useState(false); - const [error, setError] = useState(null); - const [postError, setPostError] = useState(null); - const [reloadKey, setReloadKey] = useState(0); - const fetchSeq = useRef(0); - const likingIds = useRef(new Set()); - - const objectId = target?.objectId ?? 0; - const objectType = target?.objectType; - const enabled = Boolean(target && objectId > 0 && objectType != null); - - useEffect(() => { - const controller = new AbortController(); - let cancelled = false; - - void (async () => { - await Promise.resolve(); - if (cancelled) return; - - if (!enabled || objectType == null) { - setComments([]); - setTotal(0); - setLoading(false); - setError(null); - return; - } - - const seq = ++fetchSeq.current; - setLoading(true); - setError(null); - setPageNum(1); - likingIds.current.clear(); - - try { - const data = await getCommentList( - { - objectId, - objectType, - pageNum: 1, - pageSize, - sortOrder: sortToOrder(sort), - }, - { - token: getAuthToken(), - signal: mergeSignals(controller.signal, abortAfter(COMMENT_FETCH_TIMEOUT_MS)), - }, - ); - if (cancelled || seq !== fetchSeq.current) return; - setComments(filterCommentList(data?.list)); - setTotal(Number(data?.totalCount) || 0); - setPageNum(1); - } catch (err: unknown) { - if (cancelled || seq !== fetchSeq.current || controller.signal.aborted) return; - setComments([]); - setTotal(0); - setError(formatCommentError(err)); - } finally { - if (!cancelled && seq === fetchSeq.current) setLoading(false); - } - })(); - - return () => { - cancelled = true; - controller.abort(); - }; - }, [enabled, objectId, objectType, sort, pageSize, reloadKey]); - - const hasMore = comments.length < total; - - const loadMore = useCallback(() => { - if (!enabled || objectType == null || loading || loadingMore || !hasMore) return; - - const nextPage = pageNum + 1; - const controller = new AbortController(); - setLoadingMore(true); - - void getCommentList( - { - objectId, - objectType, - pageNum: nextPage, - pageSize, - sortOrder: sortToOrder(sort), - }, - { - token: getAuthToken(), - signal: mergeSignals(controller.signal, abortAfter(COMMENT_FETCH_TIMEOUT_MS)), - }, - ) - .then((data) => { - const mapped = filterCommentList(data?.list); - setComments((current) => { - const seen = new Set(current.map((item) => Number(item.id))); - return [...current, ...mapped.filter((item) => !seen.has(Number(item.id)))]; - }); - setTotal(Number(data?.totalCount) || total); - setPageNum(nextPage); - }) - .catch((err: unknown) => { - if (controller.signal.aborted) return; - setError(formatCommentError(err, "加载更多失败")); - }) - .finally(() => setLoadingMore(false)); - }, [enabled, objectId, objectType, loading, loadingMore, hasMore, pageNum, pageSize, sort, total]); - - const toggleLike = useCallback(async (comment: CommentInfo) => { - const commentId = Number(comment.id); - if (!Number.isInteger(commentId) || commentId < 1) return; - if (likingIds.current.has(commentId)) return; - - const token = await resolveAuthToken(); - if (!token) { - openLoginDialog(); - return; - } - - const nextIsLike = !Boolean(comment.isLike); - const nextCountLike = Math.max(0, (Number(comment.countLike) || 0) + (nextIsLike ? 1 : -1)); - const snapshot = { isLike: comment.isLike, countLike: comment.countLike }; - - likingIds.current.add(commentId); - setComments((current) => patchComment(current, commentId, { isLike: nextIsLike, countLike: nextCountLike })); - - try { - const result = await setCommentLike( - { commentId, isLike: nextIsLike }, - token, - abortAfter(COMMENT_FETCH_TIMEOUT_MS), - ); - setComments((current) => - patchComment(current, commentId, { - isLike: result?.isLike ?? nextIsLike, - countLike: typeof result?.countLike === "number" ? result.countLike : nextCountLike, - }), - ); - } catch (err: unknown) { - setComments((current) => patchComment(current, commentId, snapshot)); - handleUnauthorized(err); - } finally { - likingIds.current.delete(commentId); - } - }, []); - - const post = useCallback( - async (text: string, options?: { parentsId?: number }) => { - const trimmed = text.trim(); - if (!trimmed || posting) return false; - if (!enabled || objectType == null) { - setPostError("无法发布评论"); - return false; - } - - setPostError(null); - const token = await resolveAuthToken(); - if (!token) { - openLoginDialog(); - return false; - } - - setPosting(true); - - try { - await sendComment( - { - objectId, - objectType, - text: trimmed, - parentsId: options?.parentsId ?? 0, - }, - token, - abortAfter(COMMENT_FETCH_TIMEOUT_MS), - ); - - // 审核通过前不写入列表(对齐 1045) - toast.success("发布成功,评论审核后才会展示"); - return true; - } catch (err: unknown) { - if (handleUnauthorized(err)) { - setPostError("登录已过期或没有权限,请重新登录"); - return false; - } - const message = formatCommentError(err, "发表失败,请稍后重试"); - setPostError(message); - toast.danger(message); - return false; - } finally { - setPosting(false); - } - }, - [enabled, objectId, objectType, posting], - ); - - const reload = useCallback(() => setReloadKey((value) => value + 1), []); - - return { - sort, - setSort, - comments, - total, - hasMore, - loading, - loadingMore, - posting, - error, - postError, - loadMore, - toggleLike, - post, - reload, - }; -} diff --git a/src/components/drama-detail/episodes.tsx b/src/components/drama-detail/episodes.tsx deleted file mode 100644 index fb8000f..0000000 --- a/src/components/drama-detail/episodes.tsx +++ /dev/null @@ -1,306 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useState } from "react"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { CornerFlagBadge, resolveContentBadge } from "@/components/home/section-heading"; -import { dramaHref } from "@/components/short-drama/data"; - -import { - getEpisodeChunks, - getEpisodeRanges, - getEpisodeState, - totalEpisodeOf, - updateStatusLabel, - type DramaDetail, - type RelatedDrama, -} from "./data"; - -type EpisodeProps = { - detail: DramaDetail; - current: number; - onSelect: (episode: number) => void; -}; - -function LockIcon({ className = "" }: { className?: string }) { - return ( - - ); -} - -/** 免费/未解锁均进播放页;未解锁由播放页在播放器上盖下载遮罩 */ -function useEpisodeClick(_detail: DramaDetail, onSelect: (episode: number) => void) { - return (episode: number) => onSelect(episode); -} - -/** 与播放页「更多好剧」一致的三列推荐卡片 */ -export function MorePlayletsGrid({ items }: { items: RelatedDrama[] }) { - if (!items.length) { - return

暂无更多推荐

; - } - - return ( -
- {items.map((item) => { - const badge = resolveContentBadge(item); - return ( - -
- - -
- {item.title} - - {item.tagNames?.[0]} - {item.tagNames?.[0] && item.totalEpisode ? " · " : ""} - {item.totalEpisode ? `${item.totalEpisode}集` : ""} - - - ); - })} -
- ); -} - -export function EpisodePanel({ detail, current, onSelect }: EpisodeProps) { - const totalEpisode = totalEpisodeOf(detail); - const ranges = getEpisodeRanges(totalEpisode, 20); - const [rangeIndex, setRangeIndex] = useState(-1); - const range = ranges[rangeIndex]; - const start = range?.start ?? 1; - const end = range?.end ?? totalEpisode; - const handleClick = useEpisodeClick(detail, onSelect); - - return ( -
-
-

选集

-

共 {totalEpisode} 集

-
- {[{ label: "全部", index: -1 }, ...ranges.map((item, index) => ({ label: item.label, index }))].map( - (item) => ( - - ), - )} -
-
-
- {Array.from({ length: end - start + 1 }, (_, index) => start + index).map((episode) => { - const locked = getEpisodeState(episode, detail) === "locked"; - const playing = episode === current; - return ( - - ); - })} -
-
- ); -} - -export function H5EpisodeStrip({ detail, onSelect, current }: Omit & { current?: number }) { - const totalEpisode = totalEpisodeOf(detail); - const ranges = getEpisodeChunks(totalEpisode, 30); - const [rangeIndex, setRangeIndex] = useState(0); - const { start, end } = ranges[rangeIndex] ?? ranges[0]; - const handleClick = useEpisodeClick(detail, onSelect); - const hasPreview = Boolean(detail.preview?.trim()); - const previewChapter = detail.chapterList?.find( - (item) => item.title?.trim() === "预告" || Number(item.currentEpisode) === 0, - ); - const showPreview = hasPreview || Boolean(previewChapter); - - return ( -
-
-

剧集

-
- - {updateStatusLabel(detail.updateStatus)} 共{totalEpisode}集 - - -
-
- -
- {ranges.map((item, index) => ( - - ))} - - 更多好剧 - -
- -
- {showPreview && rangeIndex === 0 ? ( - - ) : null} - - {Array.from({ length: end - start + 1 }, (_, index) => start + index).map((episode) => { - const locked = getEpisodeState(episode, detail) === "locked"; - const playing = current != null && episode === current; - - if (playing) { - return ( - - ); - } - - if (locked) { - return ( - - ); - } - - return ( - - ); - })} -
-
- ); -} - -/** H5 详情:热门推荐 — 一行横向滚动 */ -export function H5MorePlaylets({ items }: { items: RelatedDrama[] }) { - if (!items.length) return null; - - return ( -
-

热门推荐

-
- {items.map((item) => { - const badge = resolveContentBadge(item); - return ( - -
- - -
- {item.title} - - {item.tagNames?.[0]} - {item.tagNames?.[0] && item.totalEpisode ? " · " : ""} - {item.totalEpisode ? `${item.totalEpisode}集` : ""} - - - ); - })} -
-
- ); -} diff --git a/src/components/drama-detail/related-aside.tsx b/src/components/drama-detail/related-aside.tsx deleted file mode 100644 index 2cb2e26..0000000 --- a/src/components/drama-detail/related-aside.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import Link from "next/link"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { formatHeat } from "@/components/ranking/data"; -import { dramaHref } from "@/components/short-drama/data"; -import { IMG_PLACEHOLDER } from "@/lib/image/decrypt"; - -import type { RelatedDrama } from "./data"; - -export function RelatedAside({ items }: { items: RelatedDrama[] }) { - if (!items.length) return null; - - return ( - - ); -} diff --git a/src/components/drama-play/play-episode-panel.tsx b/src/components/drama-play/play-episode-panel.tsx deleted file mode 100644 index f45001e..0000000 --- a/src/components/drama-play/play-episode-panel.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useState } from "react"; - -import { - dramaDetailHref, - getEpisodeChunks, - getEpisodeState, - playHref, - totalEpisodeOf, - type DramaDetail, -} from "@/components/drama-detail/data"; - -import { ChevronIcon } from "./icons"; - -export const EPISODE_CHUNK = 30; - -function LockIcon({ className = "size-[18px]" }: { className?: string }) { - return ( - - ); -} - -function FreeBadge() { - return ( - - 免费 - - ); -} - -export function useEpisodeRange(detail: DramaDetail, current: number) { - const ranges = getEpisodeChunks(totalEpisodeOf(detail), EPISODE_CHUNK); - const [rangeIndex, setRangeIndex] = useState(() => Math.floor((current - 1) / EPISODE_CHUNK)); - const { start, end } = ranges[rangeIndex] ?? ranges[0]; - const episodes = Array.from({ length: end - start + 1 }, (_, index) => start + index); - return { ranges, rangeIndex, setRangeIndex, episodes }; -} - -export function PlayEpisodePanel({ - detail, - current, - wide = false, -}: { - detail: DramaDetail; - current: number; - wide?: boolean; -}) { - const { ranges, rangeIndex, setRangeIndex, episodes } = useEpisodeRange(detail, current); - - return ( - - ); -} diff --git a/src/components/drama-play/use-report-playlet-history.ts b/src/components/drama-play/use-report-playlet-history.ts deleted file mode 100644 index f8e1231..0000000 --- a/src/components/drama-play/use-report-playlet-history.ts +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { useCallback, useRef } from "react"; - -import { reportPlayletHistory } from "@/lib/api/playlet-actions"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -async function ensureToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -/** 播放成功后上报观看历史;同一 playletId+chapterId 只报一次 */ -export function useReportPlayletHistory(playletId: number, chapterId?: number) { - const reportedKeyRef = useRef(null); - - return useCallback(async () => { - if (!Number.isInteger(playletId) || playletId < 1) return; - if (!Number.isInteger(chapterId) || !chapterId || chapterId < 1) return; - - const key = `${playletId}:${chapterId}`; - if (reportedKeyRef.current === key) return; - reportedKeyRef.current = key; - - const token = await ensureToken(); - if (!token) { - reportedKeyRef.current = null; - return; - } - - try { - await reportPlayletHistory({ playletId, chapterId }, token); - } catch (error) { - reportedKeyRef.current = null; - if (process.env.NODE_ENV === "development") { - const message = error instanceof Error ? error.message : String(error); - console.warn(`[playlet/history] report failed: ${message}`); - } - } - }, [playletId, chapterId]); -} diff --git a/src/components/favorites/data.ts b/src/components/favorites/data.ts deleted file mode 100644 index 579857a..0000000 --- a/src/components/favorites/data.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { pickPlayletCardTags, pickVideoCardTags } from "@/components/common/card-tags"; -import { resolveContentBadge } from "@/components/home/section-heading"; -import { formatHeat } from "@/components/ranking/data"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import type { FavoriteVideoListItem, PlayletData } from "@/lib/api/types"; - -export const favoriteTypes = ["短剧", "视频"] as const; - -export type FavoriteType = (typeof favoriteTypes)[number]; - -export const FAVORITES_PAGE_SIZE = 24; - -/** 短剧收藏列表项 → 卡片(接口 PlayletData) */ -export function mapFavoritePlayletItem(item: PlayletData): ShortDramaItem | null { - const id = Number(item.id); - if (!Number.isInteger(id) || id < 1) return null; - return { - kind: "drama", - id: String(id), - slug: item.slug, - title: (item.title || "").trim() || "未命名短剧", - subtitle: (item.desc || "").trim(), - tags: pickPlayletCardTags(item), - heat: formatHeat(item.heatValue ?? item.countBrowse), - episodes: item.totalEpisode ? `${item.totalEpisode}集` : "", - badge: resolveContentBadge(item), - cover: (item.cover || "").trim(), - }; -} - -/** 视频收藏列表项 → 卡片 */ -export function mapFavoriteVideoItem(item: FavoriteVideoListItem): ShortDramaItem | null { - const id = Number(item.id); - if (!Number.isInteger(id) || id < 1) return null; - return { - kind: "drama", - id: String(id), - slug: item.slug, - title: (item.title || "").trim() || "未命名视频", - subtitle: "", - tags: pickVideoCardTags(item), - heat: formatHeat(item.watchNum), - episodes: "", - cover: (item.coverImg || "").trim(), - }; -} diff --git a/src/components/favorites/favorites-page.tsx b/src/components/favorites/favorites-page.tsx deleted file mode 100644 index fd0ea91..0000000 --- a/src/components/favorites/favorites-page.tsx +++ /dev/null @@ -1,221 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; - -import { EmptyState } from "@/components/common/empty-state"; -import { Pagination } from "@/components/common/pagination"; -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import { H5ShortDramaCard, ShortDramaCard } from "@/components/short-drama/short-drama-card"; -import { H5VideoCard, VideoCard } from "@/components/video/video-card"; -import { ApiError } from "@/lib/api/client"; -import { getPlayletFavoriteList } from "@/lib/api/playlet-actions"; -import { getVideoFavoriteList } from "@/lib/api/video-actions"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -import { - FAVORITES_PAGE_SIZE, - favoriteTypes, - mapFavoritePlayletItem, - mapFavoriteVideoItem, - type FavoriteType, -} from "./data"; - -async function resolveToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -/** 我的收藏:短剧 GET /api/app/playlet/favorite/list · 视频 GET /api/app/video/favorite/list */ -export function FavoritesPage() { - const router = useRouter(); - const topRef = useRef(null); - const [type, setType] = useState("短剧"); - const [page, setPage] = useState(1); - const [items, setItems] = useState([]); - const [totalPages, setTotalPages] = useState(1); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [reloadKey, setReloadKey] = useState(0); - - useEffect(() => { - let cancelled = false; - void (async () => { - await Promise.resolve(); - if (cancelled) return; - setLoading(true); - setError(null); - try { - const token = await resolveToken(); - if (cancelled) return; - if (!token) { - setItems([]); - setTotalPages(1); - setError("请先登录后查看收藏"); - return; - } - - if (type === "短剧") { - const data = await getPlayletFavoriteList({ pageNum: page, pageSize: FAVORITES_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapFavoritePlayletItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / FAVORITES_PAGE_SIZE) || 1)); - } else { - const data = await getVideoFavoriteList({ pageNum: page, pageSize: FAVORITES_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapFavoriteVideoItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / FAVORITES_PAGE_SIZE) || 1)); - } - } catch (err) { - if (cancelled) return; - setItems([]); - setTotalPages(1); - setError(err instanceof ApiError ? err.message : "加载失败,请稍后重试"); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, [type, page, reloadKey]); - - const changeType = (next: FavoriteType) => { - setType(next); - setPage(1); - }; - - const changePage = (next: number) => { - setPage(next); - topRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - const goBack = () => { - if (window.history.length > 1) router.back(); - else router.push("/profile"); - }; - - const isVideo = type === "视频"; - const PcCard = isVideo ? VideoCard : ShortDramaCard; - const MobileCard = isVideo ? H5VideoCard : H5ShortDramaCard; - const empty = !loading && !error && items.length === 0; - - return ( -
-
- -

我的收藏

-
- -
- - -
-

我的收藏

-

继续关注喜欢的内容

-
- -
- {favoriteTypes.map((option) => ( - - ))} -
- -
- {favoriteTypes.map((option) => ( - - ))} -
- - {loading ? ( -

加载中…

- ) : error ? ( - setReloadKey((value) => value + 1)} - className="rounded-2xl px-4 py-12 lg:rounded-3xl lg:py-24" - /> - ) : empty ? ( - - ) : ( - <> -
- {items.map((item) => ( - - ))} -
- -
- {items.map((item) => ( - - ))} -
- - - - )} -
-
- ); -} diff --git a/src/components/history/data.ts b/src/components/history/data.ts deleted file mode 100644 index 519375b..0000000 --- a/src/components/history/data.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { pickPlayletCardTags, pickVideoCardTags } from "@/components/common/card-tags"; -import { playHref } from "@/components/drama-detail/data"; -import { formatHeat } from "@/components/ranking/data"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import { videoHref } from "@/components/video/video-card"; -import type { PlayletHistoryItem, VideoHistoryItem } from "@/lib/api/types"; - -export const historyTypes = ["短剧", "视频"] as const; -export type HistoryType = (typeof historyTypes)[number]; - -export const HISTORY_PAGE_SIZE = 24; - -export function formatWatchedAt(value?: string | null) { - const raw = (value || "").trim(); - if (!raw) return ""; - const date = new Date(raw); - if (Number.isNaN(date.getTime())) return raw; - const pad = (n: number) => String(n).padStart(2, "0"); - return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${pad(date.getHours())}:${pad(date.getMinutes())}`; -} - -/** 短剧历史 → 卡片展示(保留接口字段语义,仅做展示文案) */ -export function mapPlayletHistoryItem(item: PlayletHistoryItem): ShortDramaItem | null { - const id = Number(item.id); - if (!Number.isInteger(id) || id < 1) return null; - const episode = Number(item.episode) || 0; - const href = episode > 0 ? playHref(id, episode, item.slug) : undefined; - return { - kind: "drama", - id: String(id), - slug: item.slug, - title: (item.title || "").trim() || "未命名短剧", - subtitle: (item.desc || "").trim(), - tags: pickPlayletCardTags(item), - heat: formatHeat(item.heatValue ?? item.countBrowse), - episodes: episode > 0 ? `看到第${episode}集` : item.totalEpisode ? `全${item.totalEpisode}集` : "", - cover: (item.cover || "").trim(), - href, - }; -} - -/** 视频历史 → 卡片展示 */ -export function mapVideoHistoryItem(item: VideoHistoryItem): ShortDramaItem | null { - const id = Number(item.id); - if (!Number.isInteger(id) || id < 1) return null; - return { - kind: "drama", - id: String(id), - slug: item.slug, - title: (item.title || "").trim() || "未命名视频", - subtitle: (item.desc || "").trim(), - tags: pickVideoCardTags(item), - heat: formatHeat(item.watchNum), - episodes: formatWatchedAt(item.watchedAt), - cover: (item.coverImg || "").trim(), - href: videoHref(String(id), item.slug), - }; -} diff --git a/src/components/history/history-page.tsx b/src/components/history/history-page.tsx deleted file mode 100644 index e6bce09..0000000 --- a/src/components/history/history-page.tsx +++ /dev/null @@ -1,310 +0,0 @@ -"use client"; - -import { AlertDialog, Button, toast } from "@heroui/react"; -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; - -import { EmptyState } from "@/components/common/empty-state"; -import { Pagination } from "@/components/common/pagination"; -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; -import { H5ShortDramaCard, ShortDramaCard } from "@/components/short-drama/short-drama-card"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import { H5VideoCard, VideoCard } from "@/components/video/video-card"; -import { ApiError } from "@/lib/api/client"; -import { clearPlayletHistory, getPlayletHistoryList } from "@/lib/api/playlet-actions"; -import { clearVideoHistory, getVideoHistoryList } from "@/lib/api/video-actions"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -import { - HISTORY_PAGE_SIZE, - historyTypes, - mapPlayletHistoryItem, - mapVideoHistoryItem, - type HistoryType, -} from "./data"; - -async function resolveToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -/** 观看历史:短剧 GET /api/app/playlet/history/list · 视频 GET /api/app/video/history/list */ -export function HistoryPage() { - const router = useRouter(); - const topRef = useRef(null); - const [type, setType] = useState("短剧"); - const [page, setPage] = useState(1); - const [items, setItems] = useState([]); - const [totalPages, setTotalPages] = useState(1); - const [loading, setLoading] = useState(true); - const [clearing, setClearing] = useState(false); - const [confirmOpen, setConfirmOpen] = useState(false); - const [error, setError] = useState(null); - const [reloadKey, setReloadKey] = useState(0); - - useEffect(() => { - let cancelled = false; - void (async () => { - await Promise.resolve(); - if (cancelled) return; - setLoading(true); - setError(null); - try { - const token = await resolveToken(); - if (cancelled) return; - if (!token) { - setItems([]); - setTotalPages(1); - setError("请先登录后查看观看历史"); - return; - } - - if (type === "短剧") { - const data = await getPlayletHistoryList({ pageNum: page, pageSize: HISTORY_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapPlayletHistoryItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / HISTORY_PAGE_SIZE) || 1)); - } else { - const data = await getVideoHistoryList({ pageNum: page, pageSize: HISTORY_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapVideoHistoryItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / HISTORY_PAGE_SIZE) || 1)); - } - } catch (err) { - if (cancelled) return; - setItems([]); - setTotalPages(1); - setError(err instanceof ApiError ? err.message : "加载失败,请稍后重试"); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, [type, page, reloadKey]); - - const changeType = (next: HistoryType) => { - setType(next); - setPage(1); - }; - - const changePage = (next: number) => { - setPage(next); - topRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - const openClearConfirm = () => { - if (clearing || items.length === 0) return; - setConfirmOpen(true); - }; - - const clearHistory = async () => { - if (clearing || items.length === 0) return; - const label = type === "短剧" ? "短剧" : "视频"; - const token = await resolveToken(); - if (!token) { - setConfirmOpen(false); - toast.danger("请先登录后再清空"); - return; - } - - setClearing(true); - setError(null); - try { - // DELETE /api/app/playlet/history · DELETE /api/app/video/history - const result = type === "短剧" ? await clearPlayletHistory(token) : await clearVideoHistory(token); - const deleted = Number(result?.deletedCount) || 0; - setItems([]); - setTotalPages(1); - setPage(1); - setConfirmOpen(false); - setReloadKey((value) => value + 1); - toast.success(deleted > 0 ? `已清空${deleted}条${label}历史` : `${label}历史已清空`); - } catch (err) { - const message = err instanceof ApiError ? err.message : "清空失败,请稍后重试"; - setError(message); - toast.danger(message); - } finally { - setClearing(false); - } - }; - - const goBack = () => { - if (window.history.length > 1) router.back(); - else router.push("/profile"); - }; - - const isVideo = type === "视频"; - const PcCard = isVideo ? VideoCard : ShortDramaCard; - const MobileCard = isVideo ? H5VideoCard : H5ShortDramaCard; - const empty = !loading && !error && items.length === 0; - const canClear = !clearing && items.length > 0; - const clearLabel = type === "短剧" ? "短剧" : "视频"; - - return ( -
-
- -

观看历史

- -
- -
- - -
-
-

观看历史

-

找回最近观看的剧集

-
- -
- -
- {historyTypes.map((option) => ( - - ))} -
- - {loading ? ( -

加载中…

- ) : error ? ( - setReloadKey((value) => value + 1)} - className="rounded-2xl px-4 py-12 lg:rounded-3xl lg:py-24" - /> - ) : empty ? ( - - ) : ( - <> -
- {items.map((item) => ( - - ))} -
- -
- {items.map((item) => ( - - ))} -
- - - - )} -
- - { - if (clearing) return; - setConfirmOpen(open); - }} - > - - - - - - - 清空观看历史 - - - -

- 确定清空全部{clearLabel}观看历史?此操作不可恢复。 -

-
- - - - -
-
-
-
-
- ); -} diff --git a/src/components/layout/header-user-chip.tsx b/src/components/layout/header-user-chip.tsx deleted file mode 100644 index 96e0dac..0000000 --- a/src/components/layout/header-user-chip.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import Link from "next/link"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { useMockUser } from "@/components/profile/use-mock-user"; - -const fallbackAvatar = "/images/default_avatar.svg"; - -export function HeaderUserChip() { - const { user } = useMockUser(); - const avatar = user?.avatar || fallbackAvatar; - const displayName = user?.name || "游客"; - - return ( - - - {displayName} - - ); -} diff --git a/src/components/likes/likes-page.tsx b/src/components/likes/likes-page.tsx deleted file mode 100644 index 971fdb9..0000000 --- a/src/components/likes/likes-page.tsx +++ /dev/null @@ -1,220 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; - -import { EmptyState } from "@/components/common/empty-state"; -import { Pagination } from "@/components/common/pagination"; -import { - FAVORITES_PAGE_SIZE, - favoriteTypes, - mapFavoritePlayletItem, - mapFavoriteVideoItem, - type FavoriteType, -} from "@/components/favorites/data"; -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; -import type { ShortDramaItem } from "@/components/short-drama/data"; -import { H5ShortDramaCard, ShortDramaCard } from "@/components/short-drama/short-drama-card"; -import { H5VideoCard, VideoCard } from "@/components/video/video-card"; -import { ApiError } from "@/lib/api/client"; -import { getPlayletLikeList } from "@/lib/api/playlet-actions"; -import { getVideoLikeList } from "@/lib/api/video-actions"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -async function resolveToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -/** 我的点赞:短剧 GET /api/app/playlet/like/list · 视频 GET /api/app/video/like/list */ -export function LikesPage() { - const router = useRouter(); - const topRef = useRef(null); - const [type, setType] = useState("短剧"); - const [page, setPage] = useState(1); - const [items, setItems] = useState([]); - const [totalPages, setTotalPages] = useState(1); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [reloadKey, setReloadKey] = useState(0); - - useEffect(() => { - let cancelled = false; - void (async () => { - await Promise.resolve(); - if (cancelled) return; - setLoading(true); - setError(null); - try { - const token = await resolveToken(); - if (cancelled) return; - if (!token) { - setItems([]); - setTotalPages(1); - setError("请先登录后查看点赞"); - return; - } - - if (type === "短剧") { - const data = await getPlayletLikeList({ pageNum: page, pageSize: FAVORITES_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapFavoritePlayletItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / FAVORITES_PAGE_SIZE) || 1)); - } else { - const data = await getVideoLikeList({ pageNum: page, pageSize: FAVORITES_PAGE_SIZE }, token); - if (cancelled) return; - const list = (data?.list ?? []) - .map(mapFavoriteVideoItem) - .filter((item): item is ShortDramaItem => Boolean(item)); - const total = Number(data?.totalCount) || 0; - setItems(list); - setTotalPages(Math.max(1, Math.ceil(total / FAVORITES_PAGE_SIZE) || 1)); - } - } catch (err) { - if (cancelled) return; - setItems([]); - setTotalPages(1); - setError(err instanceof ApiError ? err.message : "加载失败,请稍后重试"); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, [type, page, reloadKey]); - - const changeType = (next: FavoriteType) => { - setType(next); - setPage(1); - }; - - const changePage = (next: number) => { - setPage(next); - topRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - const goBack = () => { - if (window.history.length > 1) router.back(); - else router.push("/profile"); - }; - - const isVideo = type === "视频"; - const PcCard = isVideo ? VideoCard : ShortDramaCard; - const MobileCard = isVideo ? H5VideoCard : H5ShortDramaCard; - const empty = !loading && !error && items.length === 0; - - return ( -
-
- -

我的点赞

-
- -
- - -
-

我的点赞

-

我喜欢的我都点赞

-
- -
- {favoriteTypes.map((option) => ( - - ))} -
- -
- {favoriteTypes.map((option) => ( - - ))} -
- - {loading ? ( -

加载中…

- ) : error ? ( - setReloadKey((value) => value + 1)} - className="rounded-2xl px-4 py-12 lg:rounded-3xl lg:py-24" - /> - ) : empty ? ( - - ) : ( - <> -
- {items.map((item) => ( - - ))} -
- -
- {items.map((item) => ( - - ))} -
- - - - )} -
-
- ); -} diff --git a/src/components/nav/nav-email-card.tsx b/src/components/nav/nav-tg-card.tsx similarity index 100% rename from src/components/nav/nav-email-card.tsx rename to src/components/nav/nav-tg-card.tsx diff --git a/src/components/profile/profile-h5.tsx b/src/components/profile/profile-h5.tsx deleted file mode 100644 index 3b5f0de..0000000 --- a/src/components/profile/profile-h5.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { openLoginDialog } from "@/components/auth/login-dialog"; -import { getPlayletFavoriteList, getPlayletLikeList } from "@/lib/api/playlet-actions"; -import { getVideoFavoriteList, getVideoLikeList } from "@/lib/api/video-actions"; -import { ensureGuestSession, getAuthToken } from "@/lib/auth/session"; - -import { useMockUser } from "./use-mock-user"; - -const fallbackAvatar = "/images/default_avatar.svg"; - -type MenuItem = { - label: string; - icon: string; - href: string; - extra?: string; - small?: boolean; - highlight?: boolean; -}; - -const staticMenuItems: MenuItem[] = [ - { label: "我的点赞", icon: "/images/icon_like.svg", href: "/likes", small: true }, - { label: "我的收藏", icon: "/images/collect.svg", href: "/favorites" }, - { label: "观看历史", icon: "/images/history.svg", href: "/history", extra: "继续观看" }, - { label: "回家的路", icon: "/images/home.svg", href: "/way-home", highlight: true }, - { label: "账号设置", icon: "/images/profile.svg", href: "/settings" }, - { label: "问题反馈", icon: "/images/wtfk.svg", href: "/feedback" }, -]; - -function formatBuCount(count: number | null) { - if (count == null) return ""; - return `${count}部`; -} - -async function resolveToken() { - let token = getAuthToken(); - if (!token) { - try { - await ensureGuestSession(); - } catch { - /* ignore */ - } - token = getAuthToken(); - } - return token; -} - -async function readTotal( - loader: (query: { pageNum: number; pageSize: number }, token: string) => Promise<{ totalCount?: number } | null>, - token: string, -) { - try { - const data = await loader({ pageNum: 1, pageSize: 1 }, token); - return Number(data?.totalCount) || 0; - } catch (error) { - console.warn("[profile/counts]", error); - return 0; - } -} - -function Avatar({ src }: { src: string }) { - return ( - - ); -} - -const guestActionBtn = - "cursor-pointer flex h-7 shrink-0 items-center justify-center gap-1 rounded-full bg-white px-2 text-[#120F1C]"; - -function GuestCard({ name, avatar, id }: { name: string; avatar: string; id?: string }) { - return ( -
- -
-

{name}

-
- - {id ? `ID:${id}` : "游客登录中…"} - - - -
-

- 登录正式账号后可同步收藏、追剧、观看历史和金币余额。 -

-
-
- ); -} - -function UserCard({ name, avatar, id, onSwitch }: { name: string; avatar: string; id: string; onSwitch: () => void }) { - return ( -
-
- -
-

{name}

-
- ID:{id} - -
-
-
- -
- ); -} - -export function ProfileH5() { - const router = useRouter(); - const { user, logout } = useMockUser({ refreshOnMount: true }); - const [likeCount, setLikeCount] = useState(null); - const [favoriteCount, setFavoriteCount] = useState(null); - - useEffect(() => { - let cancelled = false; - - void (async () => { - const token = await resolveToken(); - if (!token || cancelled) { - if (!cancelled) { - setLikeCount(0); - setFavoriteCount(0); - } - return; - } - - const [playletLike, videoLike, playletFav, videoFav] = await Promise.all([ - readTotal(getPlayletLikeList, token), - readTotal(getVideoLikeList, token), - readTotal(getPlayletFavoriteList, token), - readTotal(getVideoFavoriteList, token), - ]); - - if (cancelled) return; - - setLikeCount(playletLike + videoLike); - setFavoriteCount(playletFav + videoFav); - })(); - - return () => { - cancelled = true; - }; - }, [user?.id]); - - const goBack = () => { - if (window.history.length > 1) router.back(); - else router.push("/"); - }; - - const switchAccount = () => { - logout(); - openLoginDialog(); - }; - - const menuItems = staticMenuItems.map((item) => { - if (item.label === "我的点赞") return { ...item, extra: formatBuCount(likeCount) }; - if (item.label === "我的收藏") return { ...item, extra: formatBuCount(favoriteCount) }; - return item; - }); - - return ( -
-
- -

我的

-
- -
- {user?.isRegister ? ( - - ) : ( - - )} - - -
-
- ); -} diff --git a/src/components/profile/profile-hero.tsx b/src/components/profile/profile-hero.tsx deleted file mode 100644 index 9bbbef1..0000000 --- a/src/components/profile/profile-hero.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { openLoginDialog } from "@/components/auth/login-dialog"; - -import { useMockUser } from "./use-mock-user"; - -const fallbackAvatar = "/images/default_avatar.svg"; - -const primaryBtn = - "cursor-pointer flex h-[42px] items-center justify-center gap-2 rounded-full bg-white px-4 text-[#120F1C]"; -const secondaryBtn = - "cursor-pointer flex h-[42px] items-center justify-center rounded-full bg-white/10 px-4 text-sm leading-5 font-semibold text-white outline outline-1 -outline-offset-1 outline-white/20 hover:bg-white/15"; - -export function ProfileHero() { - const { user, logout } = useMockUser({ refreshOnMount: true }); - const isGuest = !user || !user.isRegister; - - const switchAccount = () => { - logout(); - openLoginDialog(); - }; - - return ( -
- - {isGuest ? ( -
-

- {user?.name ?? "游客"} -

-
- {user ? `ID:${user.id}` : "游客登录中…"} - 登录正式账号后可同步收藏、追剧、观看历史和金币余额。 -
-
- - -
-
- ) : ( -
-
-

- {user.name} -

-

{user.accountNo || user.email}

-
- -
- )} -
- ); -} diff --git a/src/components/profile/profile-page.tsx b/src/components/profile/profile-page.tsx deleted file mode 100644 index f6e4a04..0000000 --- a/src/components/profile/profile-page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import Link from "next/link"; - -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; - -import { ProfileH5 } from "./profile-h5"; -import { ProfileHero } from "./profile-hero"; - -const entries = [ - { - title: "我的点赞", - description: "我喜欢的我都点赞", - href: "/likes", - image: "/images/my_dz.png", - imageClassName: "h-[93px] w-[98px]", - }, - { - title: "追剧与收藏", - description: "继续关注喜欢的内容", - href: "/favorites", - image: "/images/my_sc.png", - imageClassName: "h-[100px] w-[110px]", - }, - { - title: "账号设置", - description: "资料与安全设置", - href: "/settings", - image: "/images/my_sz.png", - imageClassName: "h-[100px] w-[98px]", - }, -]; - -export function ProfilePage() { - return ( -
- - -
-
-

ACCOUNT

-

个人中心

-

欢迎回来,继续你的追剧旅程

-
- - - -
- {entries.map((entry) => ( - -
-

{entry.title}

-

- {entry.description} -

-
-
- - -
- - ))} -
-
-
- ); -} diff --git a/src/components/settings/change-password-dialog.tsx b/src/components/settings/change-password-dialog.tsx deleted file mode 100644 index 08bc24a..0000000 --- a/src/components/settings/change-password-dialog.tsx +++ /dev/null @@ -1,193 +0,0 @@ -"use client"; - -import { useEffect, useState, type FormEvent } from "react"; - -import { - CREDENTIAL_MAX_LEN, - isValidCredential, - PASSWORD_RULE_HINT, -} from "@/lib/auth/credentials"; -import { updateUserProfile } from "@/lib/auth/session"; - -type Field = "oldPassword" | "newPassword" | "confirmPassword"; - -const fields: { key: Field; label: string; placeholder: string }[] = [ - { key: "oldPassword", label: "旧密码", placeholder: "请输入旧密码" }, - { key: "newPassword", label: "新密码", placeholder: "请输入新密码" }, - { key: "confirmPassword", label: "再次确认新密码", placeholder: "请再次输入新密码" }, -]; - -const emptyValues: Record = { oldPassword: "", newPassword: "", confirmPassword: "" }; - -function validate(values: Record) { - if (!values.oldPassword) return "请输入旧密码"; - if (!isValidCredential(values.newPassword)) return PASSWORD_RULE_HINT; - if (values.newPassword === values.oldPassword) return "新密码不能与旧密码相同"; - if (values.confirmPassword !== values.newPassword) return "两次输入的新密码不一致"; - return ""; -} - -export function ChangePasswordDialog({ open, onClose }: { open: boolean; onClose: () => void }) { - const [values, setValues] = useState(emptyValues); - const [error, setError] = useState(""); - const [saving, setSaving] = useState(false); - - useEffect(() => { - if (!open) return; - const onKeyDown = (event: KeyboardEvent) => event.key === "Escape" && !saving && onClose(); - const { overflow } = document.body.style; - document.body.style.overflow = "hidden"; - window.addEventListener("keydown", onKeyDown); - return () => { - document.body.style.overflow = overflow; - window.removeEventListener("keydown", onKeyDown); - }; - }, [open, onClose, saving]); - - if (!open) return null; - - const close = () => { - if (saving) return; - setValues(emptyValues); - setError(""); - onClose(); - }; - - const submit = async (event: FormEvent) => { - event.preventDefault(); - const message = validate(values); - if (message) { - setError(message); - return; - } - - setSaving(true); - setError(""); - try { - await updateUserProfile({ - oldPassword: values.oldPassword, - newPassword: values.newPassword, - }); - setValues(emptyValues); - onClose(); - } catch (err) { - setError(err instanceof Error ? err.message : "修改密码失败"); - } finally { - setSaving(false); - } - }; - - return ( -
event.target === event.currentTarget && close()} - > -
-
- 海角社区 -
-

- 修改密码 -

-

新密码需为 4-20 位,不能包含空格,且不同于旧密码

-
- {fields.map((field) => ( - { - setValues((current) => ({ ...current, [field.key]: value })); - setError(""); - }} - /> - ))} - {error ?

{error}

: null} -
- - -
- - -
-
- ); -} - -function PasswordField({ - label, - placeholder, - value, - onChange, -}: { - label: string; - placeholder: string; - value: string; - onChange: (value: string) => void; -}) { - const [visible, setVisible] = useState(false); - - return ( - - ); -} diff --git a/src/components/settings/edit-profile-page.tsx b/src/components/settings/edit-profile-page.tsx deleted file mode 100644 index db761ae..0000000 --- a/src/components/settings/edit-profile-page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useEffect, useState, type FormEvent, type ReactNode } from "react"; - -import { BackButton } from "@/components/common/back-button"; -import { DecryptedImage } from "@/components/common/decrypted-image"; -import { useMockUser } from "@/components/profile/use-mock-user"; -import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb"; -import { useMockProfile } from "@/components/settings/use-mock-profile"; -import { updateUserProfile } from "@/lib/auth/session"; - -const fallbackAvatar = "/images/default_avatar.svg"; - -type ProfileEditFormProps = { - onSaved?: () => void; - actions?: ReactNode; - className?: string; -}; - -export function ProfileEditForm({ onSaved, actions, className }: ProfileEditFormProps) { - const { user } = useMockUser({ refreshOnMount: true }); - const { profile, saveProfile } = useMockProfile(); - const [nicknameDraft, setNicknameDraft] = useState(null); - const [bioDraft, setBioDraft] = useState(null); - const [saving, setSaving] = useState(false); - const [message, setMessage] = useState<{ text: string; error: boolean } | null>(null); - - const nickname = nicknameDraft ?? user?.name ?? profile.nickname; - const bio = bioDraft ?? profile.bio; - const avatar = user?.avatar ?? profile.avatar ?? fallbackAvatar; - - const submit = async (event: FormEvent) => { - event.preventDefault(); - - const trimmed = nickname.trim(); - if (!trimmed) { - setMessage({ text: "请输入昵称", error: true }); - return; - } - - const nickChanged = trimmed !== (user?.name || ""); - const bioChanged = bio.trim() !== (profile.bio || "").trim(); - - if (nickChanged && !user?.accountNo) { - setMessage({ text: "请先注册账号后再修改资料", error: true }); - return; - } - - if (!nickChanged && !bioChanged) { - setMessage({ text: "暂无需要保存的修改", error: true }); - return; - } - - setSaving(true); - setMessage(null); - try { - if (nickChanged) { - await updateUserProfile({ nickName: trimmed }); - } - saveProfile({ - nickname: trimmed, - bio: bio.trim(), - avatar: null, - }); - setNicknameDraft(null); - setBioDraft(null); - setMessage({ text: "已保存", error: false }); - onSaved?.(); - } catch (error) { - setMessage({ - text: error instanceof Error ? error.message : "保存失败", - error: true, - }); - } finally { - setSaving(false); - } - }; - - return ( -
-
-
- 个人头像 - 当前头像 -
- -
- - - -