diff --git a/next.config.ts b/next.config.ts index 90cc73b..6e4e8ad 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,24 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + // 禁用图片优化以避免兼容性问题 + unoptimized: true, + + // 移除可能导致干扰的远程模式配置 + remotePatterns: [], + + // 空的域名列表,避免域名验证问题 + domains: [], + + // 简化配置,专注于本地静态图片 + formats: [], + + // 设置合适的设备尺寸 + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + }, + async redirects() { return [ { diff --git a/next.config.ts.backup b/next.config.ts.backup new file mode 100644 index 0000000..fa3621c --- /dev/null +++ b/next.config.ts.backup @@ -0,0 +1,33 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + images: { + // 对于本地静态图片,不需要远程模式配置 + // 远程图片配置(如果将来需要的话) + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + // 本地图片使用默认配置,不需要自定义loader + // 移除了loader配置,让Next.js使用默认的静态图片处理 + domains: [], + // 启用图片优化 + unoptimized: false, + // 图片质量设置 + formats: ["image/webp", "image/avif"], + }, + async redirects() { + return [ + { + source: "/", + destination: "/login", + permanent: false, + }, + ]; + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json index 8d4d4e7..4cfa3eb 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "next": "16.1.3", "react": "19.2.3", "react-dom": "19.2.3", + "react-masonry-css": "^1.0.16", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -38,6 +39,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "autoprefixer": "^10.4.23", "commitizen": "^4.3.1", "cz-git": "^1.12.0", "eslint": "^9", @@ -47,6 +49,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.2.7", + "postcss": "^8.5.6", "prettier": "^3.8.0", "prettier-plugin-tailwindcss": "^0.7.2", "stylelint": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fccd7c5..6ac9361 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) + react-masonry-css: + specifier: ^1.0.16 + version: 1.0.16(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -63,6 +66,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.8) + autoprefixer: + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) commitizen: specifier: ^4.3.1 version: 4.3.1(@types/node@20.19.30)(typescript@5.9.3) @@ -90,6 +96,9 @@ importers: lint-staged: specifier: ^16.2.7 version: 16.2.7 + postcss: + specifier: ^8.5.6 + version: 8.5.6 prettier: specifier: ^3.8.0 version: 3.8.0 @@ -1324,6 +1333,13 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1937,6 +1953,9 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@12.27.1: resolution: {integrity: sha512-cEAqO69kcZt3gL0TGua8WTgRQfv4J57nqt1zxHtLKwYhAwA0x9kDS/JbMa1hJbwkGY74AGJKvZ9pX/IqWZtZWQ==} peerDependencies: @@ -2921,6 +2940,11 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-masonry-css@1.0.16: + resolution: {integrity: sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==} + peerDependencies: + react: '>=16.0.0' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4627,6 +4651,15 @@ snapshots: at-least-node@1.0.0: {} + autoprefixer@10.4.23(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001764 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -5393,6 +5426,8 @@ snapshots: dependencies: is-callable: 1.2.7 + fraction.js@5.3.4: {} + framer-motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: motion-dom: 12.27.1 @@ -6268,6 +6303,10 @@ snapshots: react-is@16.13.1: {} + react-masonry-css@1.0.16(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): dependencies: react: 19.2.3 diff --git a/postcss.config.mjs b/postcss.config.js similarity index 56% rename from postcss.config.mjs rename to postcss.config.js index 61e3684..de8ec71 100644 --- a/postcss.config.mjs +++ b/postcss.config.js @@ -1,7 +1,6 @@ -const config = { +module.exports = { plugins: { "@tailwindcss/postcss": {}, + autoprefixer: {}, }, }; - -export default config; diff --git a/public/assets/images/0068albMly1hxr5qe9nkvj30j60srac4.jpg b/public/assets/images/0068albMly1hxr5qe9nkvj30j60srac4.jpg new file mode 100644 index 0000000..661e4b5 Binary files /dev/null and b/public/assets/images/0068albMly1hxr5qe9nkvj30j60srac4.jpg differ diff --git a/public/assets/images/5657f0f1f5be44a17c35be830e79df84.jpg b/public/assets/images/5657f0f1f5be44a17c35be830e79df84.jpg new file mode 100644 index 0000000..9f968b0 Binary files /dev/null and b/public/assets/images/5657f0f1f5be44a17c35be830e79df84.jpg differ diff --git a/public/assets/images/701D6B7350809E3BD8C53D4AD822C9A8.png b/public/assets/images/701D6B7350809E3BD8C53D4AD822C9A8.png new file mode 100644 index 0000000..0dac92b Binary files /dev/null and b/public/assets/images/701D6B7350809E3BD8C53D4AD822C9A8.png differ diff --git a/public/assets/images/B0987964CB384E0EE2118B3E992B83A8.png b/public/assets/images/B0987964CB384E0EE2118B3E992B83A8.png new file mode 100644 index 0000000..da51a68 Binary files /dev/null and b/public/assets/images/B0987964CB384E0EE2118B3E992B83A8.png differ diff --git a/public/assets/images/B93FE1FA33B7022C6AB778AAFE55560A.jpg b/public/assets/images/B93FE1FA33B7022C6AB778AAFE55560A.jpg new file mode 100644 index 0000000..1d82898 Binary files /dev/null and b/public/assets/images/B93FE1FA33B7022C6AB778AAFE55560A.jpg differ diff --git a/public/assets/images/a.jpg b/public/assets/images/a.jpg new file mode 100644 index 0000000..3c86a38 Binary files /dev/null and b/public/assets/images/a.jpg differ diff --git a/public/assets/images/c4458f087a48f121be885aa09e996c02.jpg b/public/assets/images/c4458f087a48f121be885aa09e996c02.jpg new file mode 100644 index 0000000..699b944 Binary files /dev/null and b/public/assets/images/c4458f087a48f121be885aa09e996c02.jpg differ diff --git a/public/assets/images/c8b9332f4a3738868e5eab99eeeaef1d.jpg b/public/assets/images/c8b9332f4a3738868e5eab99eeeaef1d.jpg new file mode 100644 index 0000000..e0b76ec Binary files /dev/null and b/public/assets/images/c8b9332f4a3738868e5eab99eeeaef1d.jpg differ diff --git a/src/assets/images/etihw.jpg b/public/assets/images/etihw.jpg similarity index 100% rename from src/assets/images/etihw.jpg rename to public/assets/images/etihw.jpg diff --git a/src/assets/images/white.jpg b/public/assets/images/white.jpg similarity index 100% rename from src/assets/images/white.jpg rename to public/assets/images/white.jpg diff --git a/src/app/dashboard/_components/Card.tsx b/src/app/dashboard/_components/Card.tsx index 5bc9e42..86d288a 100644 --- a/src/app/dashboard/_components/Card.tsx +++ b/src/app/dashboard/_components/Card.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import { useState } from "react"; interface CardProps { title: string; @@ -9,19 +10,65 @@ interface CardProps { } export const Card = ({ title, image, author, likes, content }: CardProps) => { + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + + const handleImageError = (e: React.SyntheticEvent) => { + const target = e.target as HTMLImageElement; + console.error(`图片加载失败详情:`, { + src: image, + naturalWidth: target.naturalWidth, + naturalHeight: target.naturalHeight, + errorMessage: (target as any).error?.message || "未知错误", + }); + setImageError(true); + setImageLoading(false); + }; + + const handleImageLoad = () => { + setImageLoading(false); + console.log(`图片加载成功: ${image}`); + }; + return (
- {image && ( -
+ {image && !imageError && ( +
+ {imageLoading && ( +
+
加载中...
+
+ )} {title}
)} + {imageError && ( +
+ 图片加载失败 + 路径: {image} + +
+ )}

{title}

{content}

diff --git a/src/app/dashboard/_components/CardList.tsx b/src/app/dashboard/_components/CardList.tsx index 95db553..167b335 100644 --- a/src/app/dashboard/_components/CardList.tsx +++ b/src/app/dashboard/_components/CardList.tsx @@ -1,37 +1,41 @@ +// import { useEffect, useState } from "react"; +import Masonry from "react-masonry-css"; + +// import Image from "next/image"; import { Card } from "./Card"; const mockPosts = [ { title: "上海有没有缺正片后勤/机位二的coser老师", - image: "/assets/images/etiwh.jpg", + image: "/assets/images/a.jpg", author: "一个小方", likes: 2, content: "是摄影新人和一个路人coser,如果是熟悉的ip可以来当无偿... ", }, { title: "冬日,阳光,与JK", - image: "/assets/images/white.jpg", + image: "/assets/images/0068albMly1hxr5qe9nkvj30j60srac4.jpg", author: "JunYee", likes: 727, content: "冬天的阳光洒在校园里,JK制服随风飘动...", }, { title: "某安全大厂前端实习二面", - image: null, + image: "/assets/images/701D6B7350809E3BD8C53D4AD822C9A8.png", author: "好困qst", likes: 2, content: "面试官问了Vue响应式原理,我答得有点懵...", }, { title: "北京扫街小分队!2025下半年扫街结算", - image: "/assets/images/etiwh.jpg", + image: "/assets/images/5657f0f1f5be44a17c35be830e79df84.jpg", author: "玉米玉米", likes: 53, content: "记录下每个城市的街头角落,拍出不一样的烟火气。", }, { title: "成都今天有没有来出差,或者来成都旅游的呀", - image: null, + image: "/assets/images/c4458f087a48f121be885aa09e996c02.jpg", author: "小红薯", likes: 3, content: "调休了在家呆着太无聊啦,一起出来玩呀,面基,面基,面基", @@ -40,10 +44,22 @@ const mockPosts = [ export const CardList = () => { return ( -
+ {mockPosts.map((post, index) => ( - +
+ +
))} -
+ ); }; diff --git a/src/app/globals.css b/src/app/globals.css index b0b389a..0c3c9a0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,75 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: "Inter", "Segoe UI", roboto, sans-serif; - --font-mono: ui-monospace, sfmono-regular, menlo, monaco, monospace; - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); - --font-serif: ui-serif, georgia, cambria, serif; - --radius: 0.375rem; - --tracking-tighter: calc(var(--tracking-normal) - 0.05em); - --tracking-tight: calc(var(--tracking-normal) - 0.025em); - --tracking-wide: calc(var(--tracking-normal) + 0.025em); - --tracking-wider: calc(var(--tracking-normal) + 0.05em); - --tracking-widest: calc(var(--tracking-normal) + 0.1em); - --tracking-normal: var(--tracking-normal); - --shadow-2xl: var(--shadow-2xl); - --shadow-xl: var(--shadow-xl); - --shadow-lg: var(--shadow-lg); - --shadow-md: var(--shadow-md); - --shadow: var(--shadow); - --shadow-sm: var(--shadow-sm); - --shadow-xs: var(--shadow-xs); - --shadow-2xs: var(--shadow-2xs); - --spacing: var(--spacing); - --letter-spacing: var(--letter-spacing); - --shadow-offset-y: var(--shadow-offset-y); - --shadow-offset-x: var(--shadow-offset-x); - --shadow-spread: var(--shadow-spread); - --shadow-blur: var(--shadow-blur); - --shadow-opacity: var(--shadow-opacity); - --color-shadow-color: var(--shadow-color); - --color-destructive-foreground: var(--destructive-foreground); -} +/* Tailwind CSS v4 主题变量 */ :root { --radius: 0.375rem; @@ -184,39 +116,65 @@ @layer base { * { - @apply border-border outline-ring/50; + border-color: var(--border); + outline-color: color-mix(in srgb, var(--ring), transparent 50%); } body { - @apply bg-background text-foreground; - + background-color: var(--background); + color: var(--foreground); letter-spacing: var(--tracking-normal); } +} - input[type="password"]::-ms-reveal, - input[type="password"]::-ms-clear { - display: none !important; - } +/* 瀑布流样式 */ +.my-masonry-grid { + display: flex; + margin-left: -1rem; + width: auto; +} + +.my-masonry-grid-column { + padding-left: 1rem; + background-clip: padding-box; +} + +input[type="password"]::-ms-reveal, +input[type="password"]::-ms-clear { + display: none !important; +} + +input[type="password"]::-webkit-credentials-auto-fill-button, +input[type="password"]::-webkit-contacts-auto-fill-button, +input[type="password"]::-webkit-textfield-decoration-container { + visibility: hidden !important; + display: none !important; + pointer-events: none !important; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +textarea:-webkit-autofill, +textarea:-webkit-autofill:hover, +textarea:-webkit-autofill:focus, +select:-webkit-autofill, +select:-webkit-autofill:hover, +select:-webkit-autofill:focus { + -webkit-text-fill-color: var(--foreground) !important; + caret-color: var(--foreground) !important; + box-shadow: 0 0 0 1000px var(--background) inset !important; + transition: background-color 0s ease-in-out 0s; +} - input[type="password"]::-webkit-credentials-auto-fill-button, - input[type="password"]::-webkit-contacts-auto-fill-button, - input[type="password"]::-webkit-textfield-decoration-container { - visibility: hidden !important; - display: none !important; - pointer-events: none !important; +@layer components { + .masonry-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 16px; } - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - textarea:-webkit-autofill, - textarea:-webkit-autofill:hover, - textarea:-webkit-autofill:focus, - select:-webkit-autofill, - select:-webkit-autofill:hover, - select:-webkit-autofill:focus { - -webkit-text-fill-color: var(--foreground) !important; - caret-color: var(--foreground) !important; - box-shadow: 0 0 0 1000px var(--background) inset !important; - transition: background-color 0s ease-in-out 0s; + .masonry-grid-item { + /* 防止图片拉伸 */ + break-inside: avoid; } } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index b73f436..caf2fad 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -6,8 +6,6 @@ import { useState } from "react"; import { LoginForm } from "@/app/login/_components/LoginForm"; import { RegisterForm } from "@/app/login/_components/RegisterForm"; -import etihwImg from "@/assets/images/etihw.jpg"; -import whiteImg from "@/assets/images/white.jpg"; import { Button } from "@/components/ui/button"; export default function LoginPage() { @@ -120,10 +118,12 @@ export default function LoginPage() { transition={{ duration: 0.9, delay: 0.6, ease: "easeInOut" }} > 注册
@@ -155,10 +155,12 @@ export default function LoginPage() { transition={{ duration: 0.9, delay: 0.6, ease: "easeInOut" }} > 登录
diff --git a/src/app/login/page.tsx.backup b/src/app/login/page.tsx.backup new file mode 100644 index 0000000..b73f436 --- /dev/null +++ b/src/app/login/page.tsx.backup @@ -0,0 +1,178 @@ +"use client"; + +import { motion } from "framer-motion"; +import Image from "next/image"; +import { useState } from "react"; + +import { LoginForm } from "@/app/login/_components/LoginForm"; +import { RegisterForm } from "@/app/login/_components/RegisterForm"; +import etihwImg from "@/assets/images/etihw.jpg"; +import whiteImg from "@/assets/images/white.jpg"; +import { Button } from "@/components/ui/button"; + +export default function LoginPage() { + const [isSignUpMode, setIsSignUpMode] = useState(false); + const circlePosition = isSignUpMode + ? { + top: "-10%", + right: "60%", // 注册模式:圆在右侧 + bottom: "initial", + left: "initial", + x: "100%", + y: "-50%", + width: "2000px", // 巨大的尺寸覆盖屏幕 + height: "2000px", + } + : { + top: "-10%", + right: "40%", // 登录模式:圆在左侧 + bottom: "initial", + left: "initial", + x: "0%", + y: "-50%", + width: "2000px", + height: "2000px", + }; + const formPosition = isSignUpMode + ? { left: "25%", top: "50%", x: "-50%", y: "-50%" } + : { left: "75%", top: "50%", x: "-50%", y: "-50%" }; + const loginFormMotion = { + opacity: isSignUpMode ? 0 : 1, + zIndex: isSignUpMode ? 1 : 2, + }; + const registerFormMotion = { + opacity: isSignUpMode ? 1 : 0, + zIndex: isSignUpMode ? 2 : 1, + }; + const leftPanelMotion = isSignUpMode ? { x: -800, opacity: 0 } : { x: 0, opacity: 1 }; + const rightPanelMotion = isSignUpMode ? { x: 0, opacity: 1 } : { x: 800, opacity: 0 }; + + return ( +
+
+ {/* 背景圆圈 */} + + + {/* 表单容器 */} +
+ + {/* 登录表单包装器 */} + + + + + {/* 注册表单包装器 */} + + + + +
+ + {/* 面板容器 */} +
+ {/* 左侧面板(注册提示) - 放置 etihw.jpg */} +
+ +

新朋友?

+

输入您的个人详细信息,开始与我们的旅程

+ +
+ + {/* 图片 - etihw.jpg */} + + 注册 + +
+ + {/* 右侧面板(登录提示)- 放置 white.jpg */} +
+ +

已经是会员?

+

请登录您的账户,继续探索世界

+ +
+ + {/* 图片 - white.jpg */} + + 登录 + +
+
+
+ +
+ + 备案号114514 + +
+
+ ); +} diff --git a/src/lib/imageLoader.ts b/src/lib/imageLoader.ts new file mode 100644 index 0000000..5bcc645 --- /dev/null +++ b/src/lib/imageLoader.ts @@ -0,0 +1,19 @@ +// 自定义图片加载器(暂时保留,但不在next.config.ts中使用) +// 如果将来需要自定义图片处理逻辑,可以在这里实现 + +export default function imageLoader({ + src, + width, + quality, +}: { + src: string; + width: number; + quality?: number; +}) { + // 对于本地静态资源,直接返回原路径 + if (src.startsWith("/assets/")) { + return src; + } + // 对于其他资源,可以添加查询参数 + return `${src}?w=${width}&q=${quality || 75}`; +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4b71201 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,81 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./app/**/*.{js,jsx,ts,tsx}", "./src/**/*.{js,jsx,ts,tsx}"], + darkMode: "class", // 启用 class 模式(.dark) + theme: { + extend: { + colors: { + background: "oklch(0.9885 0.0052 91.4554)", + foreground: "oklch(0.3698 0.0209 48.133)", + card: "oklch(0.9822 0.0071 91.4717)", + popover: "oklch(0.9885 0.0052 91.4554)", + primary: { + DEFAULT: "oklch(0.4341 0.0392 41.9938)", + foreground: "oklch(1 0 0)", + }, + secondary: { + DEFAULT: "oklch(0.92 0.0651 74.3695)", + foreground: "oklch(0.3499 0.0685 40.8288)", + }, + muted: { + DEFAULT: "oklch(0.9675 0.0079 91.4797)", + foreground: "oklch(0.5534 0.0226 48.2913)", + }, + accent: { + DEFAULT: "oklch(0.9531 0.0092 91.4925)", + foreground: "oklch(0.3698 0.0209 48.133)", + }, + destructive: { + DEFAULT: "oklch(0.6271 0.1936 33.339)", + foreground: "oklch(1 0 0)", + }, + border: "oklch(0.9389 0 0)", + input: "oklch(0.9389 0 0)", + ring: "oklch(0.4341 0.0392 41.9938)", + sidebar: { + DEFAULT: "oklch(0.9847 0.0052 91.4556)", + foreground: "oklch(0.4175 0.0244 48.1154)", + primary: "oklch(0.4341 0.0392 41.9938)", + "primary-foreground": "oklch(1 0 0)", + accent: "oklch(0.9599 0.0079 91.4804)", + "accent-foreground": "oklch(0.4341 0.0392 41.9938)", + border: "oklch(0.9542 0 0)", + ring: "oklch(0.4341 0.0392 41.9938)", + }, + }, + borderRadius: { + sm: "calc(0.375rem - 4px)", + md: "calc(0.375rem - 2px)", + lg: "0.375rem", + xl: "calc(0.375rem + 4px)", + "2xl": "calc(0.375rem + 8px)", + "3xl": "calc(0.375rem + 12px)", + "4xl": "calc(0.375rem + 16px)", + }, + fontFamily: { + sans: ['"Inter"', '"Segoe UI"', "roboto", "sans-serif"], + serif: ["ui-serif", "georgia", "cambria", "serif"], + mono: ["ui-monospace", "sfmono-regular", "menlo", "monaco", "monospace"], + }, + letterSpacing: { + tighter: "-0.05em", + tight: "-0.025em", + normal: "-0.01em", + wide: "0.025em", + wider: "0.05em", + widest: "0.1em", + }, + boxShadow: { + "2xs": "0px 1px 2px 0px hsl(0 0% 0% / 1%)", + xs: "0px 1px 2px 0px hsl(0 0% 0% / 1%)", + sm: "0px 1px 2px 0px hsl(0 0% 0% / 3%), 0px 1px 2px -1px hsl(0 0% 0% / 3%)", + DEFAULT: "0px 1px 2px 0px hsl(0 0% 0% / 3%), 0px 1px 2px -1px hsl(0 0% 0% / 3%)", + md: "0px 1px 2px 0px hsl(0 0% 0% / 3%), 0px 2px 4px -1px hsl(0 0% 0% / 3%)", + lg: "0px 1px 2px 0px hsl(0 0% 0% / 3%), 0px 4px 6px -1px hsl(0 0% 0% / 3%)", + xl: "0px 1px 2px 0px hsl(0 0% 0% / 3%), 0px 8px 10px -1px hsl(0 0% 0% / 3%)", + "2xl": "0px 1px 2px 0px hsl(0 0% 0% / 7%)", + }, + }, + }, + plugins: [], +}; diff --git "a/\345\205\263\351\224\256\350\257\215\346\217\220\345\217\226.md" "b/\345\205\263\351\224\256\350\257\215\346\217\220\345\217\226.md" new file mode 100644 index 0000000..3dfe726 --- /dev/null +++ "b/\345\205\263\351\224\256\350\257\215\346\217\220\345\217\226.md" @@ -0,0 +1,212 @@ +# 前端文章关键词提取与发送到后端的实现方案 + +## 实现思路 + +基于你的项目结构,需要实现以下功能: + +1. 在文章发布页面获取用户输入的文章内容 +2. 使用前端方法提取关键词 +3. 将关键词和文章内容一起发送到后端 + +## 具体实现步骤 + +### 1. 在 `src/types/index.ts` 定义数据类型 + +```typescript +export interface Post { + title: string; + content: string; + keywords: string[]; + // 其他字段... +} +``` + +### 2. 创建关键词提取工具函数 + +在 `src/lib/` 下新建 `keywordExtractor.ts`: + +```typescript +/** + * 简单的关键词提取函数 + * @param content 文章内容 + * @param maxKeywords 最大关键词数量,默认5个 + * @returns 关键词数组 + */ +export const extractKeywords = ( + content: string, + maxKeywords: number = 5, +): string[] => { + // 1. 移除标点符号和特殊字符 + const cleanText = content.replace(/[^\w\s\u4e00-\u9fa5]/g, " "); + + // 2. 分词(简单实现,实际项目建议使用jieba等分词库) + const words = cleanText.split(/\s+/).filter((word) => word.length > 1); + + // 3. 统计词频 + const wordCount = new Map(); + words.forEach((word) => { + wordCount.set(word, (wordCount.get(word) || 0) + 1); + }); + + // 4. 按词频排序并取前N个 + return Array.from(wordCount.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, maxKeywords) + .map(([word]) => word); +}; +``` + +### 3. 在发布页面实现关键词提取和提交 + +修改 `src/app/post/page.tsx`: + +```typescript +'use client'; + +import { useState } from 'react'; +import { extractKeywords } from '@/lib/keywordExtractor'; +import { Post } from '@/types'; + +export default function PostPage() { + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [keywords, setKeywords] = useState([]); + + // 实时提取关键词 + const handleContentChange = (e: React.ChangeEvent) => { + const newContent = e.target.value; + setContent(newContent); + setKeywords(extractKeywords(newContent)); + }; + + // 提交文章 + const handleSubmit = async () => { + const postData: Post = { + title, + content, + keywords, + }; + + try { + const response = await fetch('/api/posts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(postData), + }); + + if (!response.ok) { + throw new Error('发布失败'); + } + + // 处理成功响应 + alert('发布成功!'); + } catch (error) { + console.error('发布失败:', error); + alert('发布失败,请重试'); + } + }; + + return ( +
+

发布文章

+ +
+
+ + setTitle(e.target.value)} + className="w-full px-3 py-2 border rounded-md" + /> +
+ +
+ +