diff --git a/packages/next/app/globals.css b/packages/next/app/globals.css index 5a61b5e..9a2207c 100644 --- a/packages/next/app/globals.css +++ b/packages/next/app/globals.css @@ -114,6 +114,12 @@ a { @apply bg-surface dark:bg-dark-surface text-on-surface dark:text-dark-on-surface; } +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + @supports (font-variation-settings: normal) { :root { font-family: "InterVariable", "MiSans VF", sans-serif; diff --git a/packages/next/app/layout.tsx b/packages/next/app/layout.tsx index b5c975e..25738da 100644 --- a/packages/next/app/layout.tsx +++ b/packages/next/app/layout.tsx @@ -3,8 +3,7 @@ import "./globals.css"; import React from "react"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app" + title: "中V档案馆" }; export default function RootLayout({ @@ -19,7 +18,10 @@ export default function RootLayout({ 中V档案馆 - {children} + + {children} +
+ ); } diff --git a/packages/next/app/page.tsx b/packages/next/app/page.tsx index ba696dd..a7b466b 100644 --- a/packages/next/app/page.tsx +++ b/packages/next/app/page.tsx @@ -1,6 +1,9 @@ +import { Header } from "@/components/shell/Header"; + export default function Home() { return ( <> +

正在施工中……

在搜索栏输入BV号或AV号,可以查询目前数据库收集到的信息~

diff --git a/packages/next/bun.lock b/packages/next/bun.lock index 71ea2c4..5e2826b 100644 --- a/packages/next/bun.lock +++ b/packages/next/bun.lock @@ -4,6 +4,7 @@ "": { "name": "next", "dependencies": { + "framer-motion": "^12.12.2", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -159,6 +160,8 @@ "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "framer-motion": ["framer-motion@12.12.2", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-qCszZCiGWkilL40E3VuhIJJC/CS3SIBl2IHyGK8FU30nOUhTmhBNWPrNFyozAWH/bXxwzi19vJHIGVdALF0LCg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], @@ -195,6 +198,10 @@ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "motion-dom": ["motion-dom@12.12.1", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="], + + "motion-utils": ["motion-utils@12.12.1", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="], diff --git a/packages/next/components/icons/CloseIcon.tsx b/packages/next/components/icons/CloseIcon.tsx new file mode 100644 index 0000000..88b2ffb --- /dev/null +++ b/packages/next/components/icons/CloseIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const CloseIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/icons/HomeIcon.tsx b/packages/next/components/icons/HomeIcon.tsx new file mode 100644 index 0000000..f95154f --- /dev/null +++ b/packages/next/components/icons/HomeIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const HomeIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/icons/InfoIcon.tsx b/packages/next/components/icons/InfoIcon.tsx new file mode 100644 index 0000000..f2b312a --- /dev/null +++ b/packages/next/components/icons/InfoIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const InfoIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/icons/LeftArrow.tsx b/packages/next/components/icons/LeftArrow.tsx new file mode 100644 index 0000000..8d8cfcd --- /dev/null +++ b/packages/next/components/icons/LeftArrow.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +export const LeftArrow: React.FC> = (props) => ( + + + +); diff --git a/packages/next/components/icons/MenuIcon.tsx b/packages/next/components/icons/MenuIcon.tsx new file mode 100644 index 0000000..5977c8c --- /dev/null +++ b/packages/next/components/icons/MenuIcon.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +export const MenuIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/icons/RegisterIcon.tsx b/packages/next/components/icons/RegisterIcon.tsx new file mode 100644 index 0000000..5cd344c --- /dev/null +++ b/packages/next/components/icons/RegisterIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const RegisterIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/icons/RightArrow.tsx b/packages/next/components/icons/RightArrow.tsx new file mode 100644 index 0000000..9c4784b --- /dev/null +++ b/packages/next/components/icons/RightArrow.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const RightArrow: React.FC> = (props) => ( + + + +); diff --git a/packages/next/components/icons/SearchIcon.tsx b/packages/next/components/icons/SearchIcon.tsx new file mode 100644 index 0000000..4f26208 --- /dev/null +++ b/packages/next/components/icons/SearchIcon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const SearchIcon: React.FC> = (props) => ( +
+ + + +
+); diff --git a/packages/next/components/shell/Header.tsx b/packages/next/components/shell/Header.tsx new file mode 100644 index 0000000..bb4bf0e --- /dev/null +++ b/packages/next/components/shell/Header.tsx @@ -0,0 +1,124 @@ +"use client"; + +import TitleLight from "@/public/icons/标题-浅色.svg"; +import TitleDark from "@/public/icons/标题-深色.svg"; +import LogoMobileLight from "@/public/icons/TitleBar Mobile Light.svg"; +import LogoMobileDark from "@/public/icons/TitleBar Mobile Dark.svg"; +import DarkModeImage from "@/components/utils/DarkModeImage"; +import React, { useState } from "react"; +import { NavigationDrawer } from "@/components/shell/NavigatinDrawer"; +import { Portal } from "@/components/utils/Portal"; +import { RegisterIcon } from "@/components/icons/RegisterIcon"; +import { SearchBox } from "@/components/ui/SearchBox"; +import { MenuIcon } from "@/components/icons/MenuIcon"; +import { SearchIcon } from "@/components/icons/SearchIcon"; +import { InfoIcon } from "../icons/InfoIcon"; +import { HomeIcon } from "../icons/HomeIcon"; + +export const HeaderDestop = () => { + return ( +
+
+ + + +
+ + + +
+ 注册 + 关于 +
+
+ ); +}; + +export const HeaderMobile = () => { + const [showDrawer, setShowDrawer] = useState(false); + const [showsearchBox, setShowsearchBox] = useState(false); + return ( + <> + + setShowDrawer(false)}> + + + +
+ {!showsearchBox && ( + + )} + {!showsearchBox && ( +
+ + + +
+ )} + {showsearchBox && setShowsearchBox(false)} />} + {!showsearchBox && ( + + )} +
+ + ); +}; + +export const Header = () => { + return ( + <> + + + + ); +}; diff --git a/packages/next/components/shell/NavigatinDrawer.tsx b/packages/next/components/shell/NavigatinDrawer.tsx new file mode 100644 index 0000000..5e7967b --- /dev/null +++ b/packages/next/components/shell/NavigatinDrawer.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface DrawerProps { + show?: boolean; + onClose: () => void; + children: React.ReactNode; +} + +export const NavigationDrawer = ({ show = false, onClose, children }: DrawerProps) => { + const coverRef = useRef(null); + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if (show && coverRef.current && event.target === coverRef.current) { + onClose(); + } + }; + + window.addEventListener("click", handleOutsideClick); + return () => { + window.removeEventListener("click", handleOutsideClick); + }; + }, [show, onClose]); + + return ( + + {show && ( + <> + {/* Backdrop - Fade in/out */} + + ); +}; + +export default NavigationDrawer; diff --git a/packages/next/components/ui/SearchBox.tsx b/packages/next/components/ui/SearchBox.tsx new file mode 100644 index 0000000..f6e1444 --- /dev/null +++ b/packages/next/components/ui/SearchBox.tsx @@ -0,0 +1,92 @@ +import React, { useState, useRef, useCallback } from "react"; +import { SearchIcon } from "@/components/icons/SearchIcon"; +import { CloseIcon } from "@/components/icons/CloseIcon"; + +interface SearchBoxProps { + close?: () => void; +} + +export const SearchBox: React.FC = ({ close = () => {} }) => { + const [inputValue, setInputValue] = useState(""); + const inputElement = useRef(null); + + const search = useCallback((query: string) => { + if (query.trim()) { + window.location.href = `/song/${query.trim()}/info`; + } + }, []); + + const handleInputChange = useCallback((event: React.ChangeEvent) => { + setInputValue(event.target.value); + }, []); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + search(inputValue); + } + }, + [inputValue, search] + ); + + const handleClear = useCallback(() => { + setInputValue(""); + close(); + }, [close]); + + return ( +
+
+ +
+ +
+
+ +
+ + +
+
+ ); +}; diff --git a/packages/next/components/utils/DarkModeImage.tsx b/packages/next/components/utils/DarkModeImage.tsx new file mode 100644 index 0000000..1aed745 --- /dev/null +++ b/packages/next/components/utils/DarkModeImage.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Image from "next/image"; + +interface Props { + lightSrc: string; + darkSrc: string; + alt?: string; + className?: string; + width?: number; + height?: number; +} + +const DarkModeImage = ({ lightSrc, darkSrc, alt = "", className = "", width, height }: Props) => { + const [isDarkMode, setIsDarkMode] = useState(false); + const [currentSrc, setCurrentSrc] = useState(lightSrc); + const [opacity, setOpacity] = useState(0); + + useEffect(() => { + const handleDarkModeChange = (event: MediaQueryListEvent) => { + setIsDarkMode(event.matches); + setCurrentSrc(event.matches ? darkSrc : lightSrc); + setOpacity(1); + }; + + const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + setIsDarkMode(darkModeMediaQuery.matches); + setCurrentSrc(darkModeMediaQuery.matches ? darkSrc : lightSrc); + setOpacity(1); + + darkModeMediaQuery.addEventListener("change", handleDarkModeChange); + + return () => { + darkModeMediaQuery.removeEventListener("change", handleDarkModeChange); + }; + }, [darkSrc, lightSrc]); + + useEffect(() => { + setCurrentSrc(isDarkMode ? darkSrc : lightSrc); + }, [isDarkMode, darkSrc, lightSrc]); + + return ( + {alt} + ); +}; + +export default DarkModeImage; diff --git a/packages/next/components/utils/Portal.tsx b/packages/next/components/utils/Portal.tsx new file mode 100644 index 0000000..d76d2d7 --- /dev/null +++ b/packages/next/components/utils/Portal.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +export const Portal = ({ children }: { children: React.ReactNode }) => { + const documentNotUndefined = typeof document !== "undefined"; + // Ensure portal root exists in your HTML + const portalRoot = documentNotUndefined ? document.getElementById("portal-root") : null; + + if (!portalRoot) { + return null; + } + + return ReactDOM.createPortal(children, portalRoot); +}; diff --git a/packages/next/package.json b/packages/next/package.json index 765598b..50fadc0 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -10,9 +10,10 @@ "format": "prettier --write ." }, "dependencies": { + "framer-motion": "^12.12.2", + "next": "15.3.2", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.2" + "react-dom": "^19.0.0" }, "devDependencies": { "typescript": "^5", diff --git a/packages/next/public/icons/TitleBar Mobile Dark.svg b/packages/next/public/icons/TitleBar Mobile Dark.svg new file mode 100644 index 0000000..7a48bfb --- /dev/null +++ b/packages/next/public/icons/TitleBar Mobile Dark.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/packages/next/public/icons/TitleBar Mobile Light.svg b/packages/next/public/icons/TitleBar Mobile Light.svg new file mode 100644 index 0000000..87ffa43 --- /dev/null +++ b/packages/next/public/icons/TitleBar Mobile Light.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/packages/next/public/icons/标题-浅色.svg b/packages/next/public/icons/标题-浅色.svg new file mode 100644 index 0000000..c32ce5f --- /dev/null +++ b/packages/next/public/icons/标题-浅色.svg @@ -0,0 +1,15 @@ + + + Created with Pixso. + + + + + + + + + + + + diff --git a/packages/next/public/icons/标题-深色.svg b/packages/next/public/icons/标题-深色.svg new file mode 100644 index 0000000..acd623a --- /dev/null +++ b/packages/next/public/icons/标题-深色.svg @@ -0,0 +1,4 @@ + + + +