layout
This commit is contained in:
@@ -6,6 +6,8 @@ import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
|||||||
import { IconClick, CircleToggleButton, CircleButton } from "@/components/ui/buttons";
|
import { IconClick, CircleToggleButton, CircleButton } from "@/components/ui/buttons";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
|
import { Card } from "@/components/ui/Card";
|
||||||
|
|
||||||
interface AlphabetCardProps {
|
interface AlphabetCardProps {
|
||||||
alphabet: Letter[];
|
alphabet: Letter[];
|
||||||
@@ -97,9 +99,8 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
|
<PageLayout className="relative">
|
||||||
<div className="w-full max-w-2xl">
|
{/* 右上角返回按钮 - outside the white card */}
|
||||||
{/* 右上角返回按钮 */}
|
|
||||||
<div className="flex justify-end mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<IconClick
|
<IconClick
|
||||||
size={32}
|
size={32}
|
||||||
@@ -111,7 +112,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 白色主卡片容器 */}
|
{/* 白色主卡片容器 */}
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
|
<Card padding="xl">
|
||||||
{/* 顶部进度指示器和显示选项按钮 */}
|
{/* 顶部进度指示器和显示选项按钮 */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
{/* 当前字母进度 */}
|
{/* 当前字母进度 */}
|
||||||
@@ -204,7 +205,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
<ChevronRight size={24} />
|
<ChevronRight size={24} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{/* 底部操作提示文字 */}
|
{/* 底部操作提示文字 */}
|
||||||
<div className="text-center mt-6 text-white text-sm">
|
<div className="text-center mt-6 text-white text-sm">
|
||||||
@@ -215,7 +216,6 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 全屏触摸事件监听层(用于滑动切换) */}
|
{/* 全屏触摸事件监听层(用于滑动切换) */}
|
||||||
<div
|
<div
|
||||||
@@ -224,6 +224,6 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
onTouchMove={onTouchMove}
|
onTouchMove={onTouchMove}
|
||||||
onTouchEnd={onTouchEnd}
|
onTouchEnd={onTouchEnd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import { Container } from "@/components/ui/Container";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { AlphabetCard } from "./AlphabetCard";
|
import { AlphabetCard } from "./AlphabetCard";
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ export default function Alphabet() {
|
|||||||
// 语言选择界面
|
// 语言选择界面
|
||||||
if (!chosenAlphabet) {
|
if (!chosenAlphabet) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex flex-col items-center justify-center px-4">
|
<PageLayout>
|
||||||
<Container className="p-8 max-w-2xl w-full text-center">
|
|
||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||||
{t("chooseCharacters")}
|
{t("chooseCharacters")}
|
||||||
@@ -105,30 +104,25 @@ export default function Alphabet() {
|
|||||||
</div>
|
</div>
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
if (loadingState === "loading") {
|
if (loadingState === "loading") {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center">
|
<PageLayout>
|
||||||
<Container className="p-8 text-center">
|
<div className="text-2xl text-gray-600 text-center">{t("loading")}</div>
|
||||||
<div className="text-2xl text-gray-600">{t("loading")}</div>
|
</PageLayout>
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误状态
|
// 错误状态
|
||||||
if (loadingState === "error") {
|
if (loadingState === "error") {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center">
|
<PageLayout>
|
||||||
<Container className="p-8 text-center">
|
<div className="text-2xl text-red-600 text-center">{t("loadFailed")}</div>
|
||||||
<div className="text-2xl text-red-600">{t("loadFailed")}</div>
|
</PageLayout>
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Container } from "@/components/ui/Container";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { SearchForm } from "./SearchForm";
|
import { SearchForm } from "./SearchForm";
|
||||||
import { SearchResult } from "./SearchResult";
|
import { SearchResult } from "./SearchResult";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -43,20 +43,17 @@ export default async function DictionaryPage({ searchParams }: DictionaryPagePro
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
|
<PageLayout>
|
||||||
{/* 搜索区域 */}
|
{/* 搜索区域 */}
|
||||||
<div className="flex items-center justify-center px-4 py-12">
|
<div className="mb-8">
|
||||||
<Container className="max-w-3xl w-full p-4">
|
|
||||||
<SearchForm
|
<SearchForm
|
||||||
defaultQueryLang={queryLang}
|
defaultQueryLang={queryLang}
|
||||||
defaultDefinitionLang={definitionLang}
|
defaultDefinitionLang={definitionLang}
|
||||||
/>
|
/>
|
||||||
</Container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索结果区域 */}
|
{/* 搜索结果区域 */}
|
||||||
<div className="flex-1 px-4 pb-12">
|
<div>
|
||||||
<Container className="max-w-3xl w-full p-4">
|
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<SearchResult
|
<SearchResult
|
||||||
searchResult={searchResult}
|
searchResult={searchResult}
|
||||||
@@ -66,14 +63,13 @@ export default async function DictionaryPage({ searchParams }: DictionaryPagePro
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!searchQuery && (
|
{!searchQuery && (
|
||||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
<div className="text-center py-12">
|
||||||
<div className="text-6xl mb-4">📚</div>
|
<div className="text-6xl mb-4">📚</div>
|
||||||
<p className="text-gray-800 text-xl mb-2">{t("welcomeTitle")}</p>
|
<p className="text-gray-800 text-xl mb-2">{t("welcomeTitle")}</p>
|
||||||
<p className="text-gray-600">{t("welcomeHint")}</p>
|
<p className="text-gray-600">{t("welcomeHint")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Folder as Fd } from "lucide-react";
|
import { Folder as Fd } from "lucide-react";
|
||||||
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
|
|
||||||
interface FolderSelectorProps {
|
interface FolderSelectorProps {
|
||||||
folders: TSharedFolderWithTotalPairs[];
|
folders: TSharedFolderWithTotalPairs[];
|
||||||
@@ -15,9 +16,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
|
<PageLayout>
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
|
||||||
{folders.length === 0 ? (
|
{folders.length === 0 ? (
|
||||||
// 空状态 - 显示提示和跳转按钮
|
// 空状态 - 显示提示和跳转按钮
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -87,9 +86,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import { isNonNegativeInteger, SeededRandom } from "@/utils/random";
|
import { isNonNegativeInteger, SeededRandom } from "@/utils/random";
|
||||||
import { TSharedPair } from "@/shared/folder-type";
|
import { TSharedPair } from "@/shared/folder-type";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
|
|
||||||
const myFont = localFont({
|
const myFont = localFont({
|
||||||
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
||||||
@@ -28,11 +29,9 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
|
|
||||||
if (textPairs.length === 0) {
|
if (textPairs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4">
|
<PageLayout maxWidth="md">
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
|
<p className="text-gray-700 text-center">{t("noTextPairs")}</p>
|
||||||
<p className="text-gray-700">{t("noTextPairs")}</p>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +112,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
: [getTextPairs()[index].text1, getTextPairs()[index].text2];
|
: [getTextPairs()[index].text1, getTextPairs()[index].text2];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
|
<PageLayout>
|
||||||
<div className="w-full max-w-2xl">
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
|
||||||
{/* 进度指示器 */}
|
{/* 进度指示器 */}
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<LinkButton onClick={handleIndexClick} className="text-sm">
|
<LinkButton onClick={handleIndexClick} className="text-sm">
|
||||||
@@ -191,9 +188,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
{t("disorder")}
|
{t("disorder")}
|
||||||
</CircleToggleButton>
|
</CircleToggleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Video, FileText } from "lucide-react";
|
import { Video, FileText } from "lucide-react";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { useSrtPlayer } from "./hooks/useSrtPlayer";
|
import { useSrtPlayer } from "./hooks/useSrtPlayer";
|
||||||
import { useSubtitleSync } from "./hooks/useSubtitleSync";
|
import { useSubtitleSync } from "./hooks/useSubtitleSync";
|
||||||
import { useKeyboardShortcuts, createSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts";
|
import { useKeyboardShortcuts, createSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||||
@@ -106,9 +107,7 @@ export default function SrtPlayerPage() {
|
|||||||
const canPlay = state.video.url && state.subtitle.url && state.subtitle.data.length > 0;
|
const canPlay = state.video.url && state.subtitle.url && state.subtitle.data.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<PageLayout>
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
{/* 标题区域 */}
|
{/* 标题区域 */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-800 mb-2">
|
<h1 className="text-4xl font-bold text-gray-800 mb-2">
|
||||||
@@ -119,10 +118,8 @@ export default function SrtPlayerPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
|
||||||
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
|
||||||
{/* 视频播放器区域 */}
|
{/* 视频播放器区域 */}
|
||||||
<div className="aspect-video bg-black relative">
|
<div className="aspect-video bg-black relative rounded-xl overflow-hidden">
|
||||||
{(!state.video.url || !state.subtitle.url || state.subtitle.data.length === 0) && (
|
{(!state.video.url || !state.subtitle.url || state.subtitle.data.length === 0) && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
|
||||||
<div className="text-center text-white">
|
<div className="text-center text-white">
|
||||||
@@ -164,7 +161,7 @@ export default function SrtPlayerPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 控制面板 */}
|
{/* 控制面板 */}
|
||||||
<div className="p-3 bg-gray-50 border-t">
|
<div className="p-3 bg-gray-50 border-t rounded-b-xl">
|
||||||
{/* 上传区域和状态指示器 */}
|
{/* 上传区域和状态指示器 */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
@@ -272,9 +269,6 @@ export default function SrtPlayerPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export default function TranslatorPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-[calc(100vh-64px)] bg-white">
|
||||||
{/* TCard Component */}
|
{/* TCard Component */}
|
||||||
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
|
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
|
||||||
{/* Card Component - Left Side */}
|
{/* Card Component - Left Side */}
|
||||||
@@ -218,6 +218,6 @@ export default function TranslatorPage() {
|
|||||||
{t("translate")}
|
{t("translate")}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useActionState, startTransition } from "react";
|
import { useState, useActionState, startTransition } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Container } from "@/components/ui/Container";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/components/ui/Input";
|
||||||
import { LightButton, LinkButton } from "@/components/ui/buttons";
|
import { LightButton, LinkButton } from "@/components/ui/buttons";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
@@ -125,8 +125,7 @@ export function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
const currentError = mode === 'signin' ? signInState : signUpState;
|
const currentError = mode === 'signin' ? signInState : signUpState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4">
|
<PageLayout>
|
||||||
<Container className="p-8 max-w-md w-full">
|
|
||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t(mode === 'signin' ? 'signIn' : 'signUp')}</h1>
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t(mode === 'signin' ? 'signIn' : 'signUp')}</h1>
|
||||||
@@ -281,7 +280,6 @@ export function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
}
|
}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Container } from "@/components/ui/Container";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
||||||
import { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
import { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -36,8 +36,7 @@ export default async function UserPage({ params }: UserPageProps) {
|
|||||||
const isOwnProfile = session?.user?.username === username || session?.user?.email === username;
|
const isOwnProfile = session?.user?.username === username || session?.user?.email === username;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-gray-50 py-8">
|
<PageLayout>
|
||||||
<Container className="max-w-3xl mx-auto">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -191,7 +190,6 @@ export default async function UserPage({ params }: UserPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/components/ui/Card.tsx
Normal file
73
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Card - 可复用的卡片组件
|
||||||
|
*
|
||||||
|
* 提供应用统一的标准白色卡片样式:
|
||||||
|
* - 白色背景
|
||||||
|
* - 圆角 (rounded-2xl)
|
||||||
|
* - 阴影 (shadow-xl)
|
||||||
|
* - 可配置内边距
|
||||||
|
* - 多种样式变体
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* // 默认卡片
|
||||||
|
* <Card>
|
||||||
|
* <p>卡片内容</p>
|
||||||
|
* </Card>
|
||||||
|
*
|
||||||
|
* // 带边框的卡片
|
||||||
|
* <Card variant="bordered" padding="lg">
|
||||||
|
* <p>带边框的内容</p>
|
||||||
|
* </Card>
|
||||||
|
*
|
||||||
|
* // 无内边距卡片
|
||||||
|
* <Card padding="none">
|
||||||
|
* <img src="image.jpg" alt="完全填充的图片" />
|
||||||
|
* </Card>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type CardVariant = "default" | "bordered" | "elevated";
|
||||||
|
export type CardPadding = "none" | "sm" | "md" | "lg" | "xl";
|
||||||
|
|
||||||
|
export interface CardProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** 额外的 CSS 类名,用于自定义样式 */
|
||||||
|
className?: string;
|
||||||
|
/** 卡片样式变体 */
|
||||||
|
variant?: CardVariant;
|
||||||
|
/** 内边距大小 */
|
||||||
|
padding?: CardPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变体样式映射
|
||||||
|
const variantClasses: Record<CardVariant, string> = {
|
||||||
|
default: "bg-white shadow-xl",
|
||||||
|
bordered: "bg-white border-2 border-gray-200",
|
||||||
|
elevated: "bg-white shadow-2xl",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内边距映射
|
||||||
|
const paddingClasses: Record<CardPadding, string> = {
|
||||||
|
none: "",
|
||||||
|
sm: "p-4",
|
||||||
|
md: "p-6",
|
||||||
|
lg: "p-8",
|
||||||
|
xl: "p-8 md:p-12",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Card({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
variant = "default",
|
||||||
|
padding = "md",
|
||||||
|
}: CardProps) {
|
||||||
|
const baseClasses = "rounded-2xl";
|
||||||
|
const variantClass = variantClasses[variant];
|
||||||
|
const paddingClass = paddingClasses[padding];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClasses} ${variantClass} ${paddingClass} ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,31 +3,103 @@
|
|||||||
*
|
*
|
||||||
* 提供应用统一的标准页面布局:
|
* 提供应用统一的标准页面布局:
|
||||||
* - 绿色背景 (#35786f)
|
* - 绿色背景 (#35786f)
|
||||||
* - 居中的白色圆角卡片
|
* - 最小高度 min-h-[calc(100vh-64px)]
|
||||||
* - 响应式内边距
|
* - 支持多种布局变体
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
|
* // 默认:居中白色卡片布局
|
||||||
* <PageLayout>
|
* <PageLayout>
|
||||||
* <PageHeader title="标题" subtitle="副标题" />
|
* <PageHeader title="标题" subtitle="副标题" />
|
||||||
* <div>页面内容</div>
|
* <div>页面内容</div>
|
||||||
* </PageLayout>
|
* </PageLayout>
|
||||||
|
*
|
||||||
|
* // 全宽布局(无白色卡片)
|
||||||
|
* <PageLayout variant="full-width" maxWidth="3xl">
|
||||||
|
* <div>页面内容</div>
|
||||||
|
* </PageLayout>
|
||||||
|
*
|
||||||
|
* // 全屏布局(用于 translator 等)
|
||||||
|
* <PageLayout variant="fullscreen">
|
||||||
|
* <div>全屏内容</div>
|
||||||
|
* </PageLayout>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
import { Card } from "./Card";
|
||||||
|
|
||||||
|
type PageLayoutVariant = "centered-card" | "full-width" | "fullscreen";
|
||||||
|
type MaxWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
|
||||||
|
type AlignItems = "center" | "start" | "end";
|
||||||
|
|
||||||
interface PageLayoutProps {
|
interface PageLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** 额外的 CSS 类名,用于自定义布局行为 */
|
/** 额外的 CSS 类名,用于自定义布局行为 */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 布局变体 */
|
||||||
|
variant?: PageLayoutVariant;
|
||||||
|
/** 最大宽度(仅对 full-width 变体有效) */
|
||||||
|
maxWidth?: MaxWidth;
|
||||||
|
/** 内容垂直对齐方式(仅对 centered-card 变体有效) */
|
||||||
|
align?: AlignItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageLayout({ children, className = "" }: PageLayoutProps) {
|
// 最大宽度映射
|
||||||
|
const maxWidthClasses: Record<MaxWidth, string> = {
|
||||||
|
sm: "max-w-sm",
|
||||||
|
md: "max-w-md",
|
||||||
|
lg: "max-w-lg",
|
||||||
|
xl: "max-w-xl",
|
||||||
|
"2xl": "max-w-2xl",
|
||||||
|
"3xl": "max-w-3xl",
|
||||||
|
full: "max-w-full",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对齐方式映射
|
||||||
|
const alignClasses: Record<AlignItems, string> = {
|
||||||
|
center: "items-center",
|
||||||
|
start: "items-start",
|
||||||
|
end: "items-end",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageLayout({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
variant = "centered-card",
|
||||||
|
maxWidth = "2xl",
|
||||||
|
align = "center",
|
||||||
|
}: PageLayoutProps) {
|
||||||
|
// 默认变体:居中白色卡片布局
|
||||||
|
if (variant === "centered-card") {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8 ${className}`}>
|
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex ${alignClasses[align]} justify-center px-4 py-8 ${className}`}>
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
<Card padding="lg" className="p-6 md:p-8">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全宽布局:绿色背景,最大宽度容器,无白色卡片
|
||||||
|
if (variant === "full-width") {
|
||||||
|
return (
|
||||||
|
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] px-4 py-8 ${className}`}>
|
||||||
|
<div className={`w-full ${maxWidthClasses[maxWidth]} mx-auto`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏布局:仅绿色背景,无其他限制
|
||||||
|
if (variant === "fullscreen") {
|
||||||
|
return (
|
||||||
|
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export { Container } from './Container';
|
|||||||
export { PageLayout } from './PageLayout';
|
export { PageLayout } from './PageLayout';
|
||||||
export { PageHeader } from './PageHeader';
|
export { PageHeader } from './PageHeader';
|
||||||
export { CardList } from './CardList';
|
export { CardList } from './CardList';
|
||||||
|
export { Card } from './Card';
|
||||||
|
export type { CardProps, CardVariant, CardPadding } from './Card';
|
||||||
|
|
||||||
// 复合组件
|
// 复合组件
|
||||||
export { LocaleSelector } from './LocaleSelector';
|
export { LocaleSelector } from './LocaleSelector';
|
||||||
|
|||||||
Reference in New Issue
Block a user