feat: 城中村租房平台 - 租客浏览、房东发布房源、图片上传
This commit is contained in:
233
app/page.tsx
233
app/page.tsx
@@ -1,65 +1,186 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
interface House {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
price: number;
|
||||
district: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
images: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function HomeContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const [houses, setHouses] = useState<House[]>([]);
|
||||
const [districts, setDistricts] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [district, setDistrict] = useState(searchParams.get("district") || "全部");
|
||||
const [keyword, setKeyword] = useState(searchParams.get("keyword") || "");
|
||||
|
||||
useEffect(() => {
|
||||
fetchDistricts();
|
||||
fetchHouses();
|
||||
}, [district]);
|
||||
|
||||
async function fetchDistricts() {
|
||||
try {
|
||||
const res = await fetch("/api/districts");
|
||||
const data = await res.json();
|
||||
setDistricts(["全部", ...(data.districts || [])]);
|
||||
} catch (error) {
|
||||
console.error("获取地区失败", error);
|
||||
setDistricts(["全部"]);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHouses() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (district !== "全部") params.set("district", district);
|
||||
if (keyword) params.set("keyword", keyword);
|
||||
|
||||
const res = await fetch(`/api/houses?${params}`);
|
||||
const data = await res.json();
|
||||
setHouses(data.houses || []);
|
||||
} catch (error) {
|
||||
console.error("获取房屋失败", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
fetchHouses();
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diff === 0) return "今天";
|
||||
if (diff === 1) return "昨天";
|
||||
if (diff < 7) return `${diff}天前`;
|
||||
return `${Math.floor(diff / 7)}周前`;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<div className="min-h-screen pb-20">
|
||||
<header className="bg-white sticky top-0 z-10 shadow-sm">
|
||||
<div className="px-4 py-3">
|
||||
<h1 className="text-xl font-bold text-orange-500 mb-3">🏠 城中村租房</h1>
|
||||
<form onSubmit={handleSearch} className="flex gap-2">
|
||||
<select
|
||||
value={district}
|
||||
onChange={(e) => setDistrict(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg border border-gray-200 text-sm bg-white flex-shrink-0"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
{districts.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder="搜索区域、小区名称..."
|
||||
className="flex-1 px-3 py-2 rounded-lg border border-gray-200 text-sm"
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<button type="submit" className="px-4 py-2 bg-orange-500 text-white rounded-lg text-sm font-medium">
|
||||
搜索
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="px-4 py-4">
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-xl p-4 animate-pulse">
|
||||
<div className="bg-gray-200 h-40 rounded-lg mb-3"></div>
|
||||
<div className="bg-gray-200 h-5 w-3/4 rounded mb-2"></div>
|
||||
<div className="bg-gray-200 h-4 w-1/2 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : houses.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-6xl mb-4">🏠</div>
|
||||
<p className="text-gray-500 mb-2">暂无房源</p>
|
||||
<p className="text-gray-400 text-sm">看看其他区域吧</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{houses.map((house) => (
|
||||
<Link key={house.id} href={`/house/${house.id}`}>
|
||||
<article className="bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="relative">
|
||||
{house.images && house.images.length > 0 ? (
|
||||
<img
|
||||
src={house.images[0]}
|
||||
alt={house.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-48 bg-gradient-to-br from-orange-100 to-orange-200 flex items-center justify-center text-6xl">
|
||||
🏠
|
||||
</div>
|
||||
)}
|
||||
<span className="absolute top-2 left-2 bg-orange-500 text-white text-xs px-2 py-1 rounded">
|
||||
{house.district}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-1 line-clamp-1">{house.title}</h3>
|
||||
<p className="text-gray-500 text-sm mb-2 line-clamp-1">{house.address}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-orange-500 font-bold text-lg">
|
||||
¥{house.price}<span className="text-gray-400 text-sm font-normal">/月</span>
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs">{formatDate(house.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-6 py-2 flex justify-between items-center max-w-lg mx-auto">
|
||||
<div className="flex flex-col items-center text-orange-500">
|
||||
<span className="text-xl">🏠</span>
|
||||
<span className="text-xs mt-1">找房</span>
|
||||
</div>
|
||||
<Link href="/owner" className="flex flex-col items-center text-gray-400 hover:text-orange-500 transition-colors">
|
||||
<span className="text-xl">🏗️</span>
|
||||
<span className="text-xs mt-1">房东入口</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2 animate-bounce">🏠</div>
|
||||
<p className="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<HomeContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user