feat: 城中村租房平台 - 租客浏览、房东发布房源、图片上传

This commit is contained in:
Cuishibing
2026-03-22 10:15:31 +08:00
parent f03cf328dd
commit ce9dfae7c5
15 changed files with 1682 additions and 87 deletions

209
app/house/[id]/page.tsx Normal file
View File

@@ -0,0 +1,209 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
interface House {
id: string;
owner: string;
title: string;
description: string;
price: number;
district: string;
address: string;
phone: string;
images: string[];
createdAt: string;
}
export default function HouseDetail() {
const params = useParams();
const router = useRouter();
const [house, setHouse] = useState<House | null>(null);
const [loading, setLoading] = useState(true);
const [currentImage, setCurrentImage] = useState(0);
const [showContact, setShowContact] = useState(false);
useEffect(() => {
fetchHouse();
}, []);
async function fetchHouse() {
try {
const res = await fetch(`/api/houses/${params.id}`);
const data = await res.json();
if (data.house) {
setHouse(data.house);
}
} catch (error) {
console.error("获取房屋详情失败", error);
} finally {
setLoading(false);
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString("zh-CN");
}
if (loading) {
return (
<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>
);
}
if (!house) {
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div className="text-6xl mb-4">😢</div>
<p className="text-gray-500 mb-4"></p>
<Link href="/" className="px-4 py-2 bg-orange-500 text-white rounded-lg">
</Link>
</div>
);
}
const images = house.images && house.images.length > 0 ? house.images : [];
return (
<div className="min-h-screen bg-gray-50 pb-32">
<header className="bg-white sticky top-0 z-20 px-4 py-3 flex items-center gap-4 shadow-sm">
<button onClick={() => router.back()} className="text-2xl">
</button>
<h1 className="font-semibold text-gray-900 truncate flex-1"></h1>
</header>
{images.length > 0 ? (
<div className="relative">
<div className="overflow-x-auto snap-x snap-mandatory flex">
{images.map((img, index) => (
<img
key={index}
src={img}
alt={`${house.title} ${index + 1}`}
className="w-full h-72 object-cover flex-shrink-0 snap-center"
/>
))}
</div>
{images.length > 1 && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
{images.map((_, index) => (
<button
key={index}
onClick={() => setCurrentImage(index)}
className={`w-2 h-2 rounded-full ${
index === currentImage ? "bg-white" : "bg-white/50"
}`}
/>
))}
</div>
)}
</div>
) : (
<div className="w-full h-72 bg-gradient-to-br from-orange-100 to-orange-200 flex items-center justify-center text-8xl">
🏠
</div>
)}
<div className="px-4 py-4">
<div className="bg-white rounded-xl p-4 mb-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-1">{house.title}</h2>
<span className="inline-block bg-orange-100 text-orange-600 text-xs px-2 py-1 rounded">
{house.district}
</span>
</div>
<div className="text-right">
<span className="text-2xl font-bold text-orange-500">¥{house.price}</span>
<span className="text-gray-400 text-sm">/</span>
</div>
</div>
<div className="flex items-center text-gray-500 text-sm mb-3">
<span className="text-lg mr-2">📍</span>
<span>{house.address}</span>
</div>
<div className="flex items-center text-gray-500 text-sm">
<span className="text-lg mr-2">👤</span>
<span>{house.owner}</span>
</div>
</div>
{house.description && (
<div className="bg-white rounded-xl p-4 mb-4 shadow-sm">
<h3 className="font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-600 text-sm leading-relaxed whitespace-pre-wrap">
{house.description}
</p>
</div>
)}
<div className="bg-white rounded-xl p-4 shadow-sm">
<h3 className="font-semibold text-gray-900 mb-3"></h3>
<p className="text-gray-500 text-sm">{formatDate(house.createdAt)}</p>
</div>
</div>
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-4 max-w-lg mx-auto">
<button
onClick={() => setShowContact(true)}
className="w-full py-4 bg-gradient-to-r from-orange-500 to-orange-600 text-white font-semibold rounded-xl shadow-lg active:scale-[0.98] transition-transform"
>
📞
</button>
</div>
{showContact && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-end">
<div className="bg-white w-full rounded-t-3xl p-6 animate-slide-up">
<div className="w-12 h-1 bg-gray-300 rounded-full mx-auto mb-6"></div>
<h3 className="text-xl font-bold text-gray-900 mb-4 text-center"></h3>
<div className="bg-orange-50 rounded-xl p-6 text-center mb-6">
<div className="text-4xl mb-2">📱</div>
<p className="text-3xl font-bold text-orange-500 tracking-wider">{house.phone}</p>
<p className="text-gray-500 text-sm mt-2"></p>
</div>
<div className="flex gap-3">
<a
href={`tel:${house.phone}`}
className="flex-1 py-4 bg-orange-500 text-white font-semibold rounded-xl text-center"
>
</a>
<button
onClick={() => setShowContact(false)}
className="flex-1 py-4 bg-gray-100 text-gray-700 font-semibold rounded-xl"
>
</button>
</div>
</div>
</div>
)}
<style jsx>{`
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
`}</style>
</div>
);
}