feat: 城中村租房平台 - 租客浏览、房东发布房源、图片上传
This commit is contained in:
475
app/owner/dashboard/page.tsx
Normal file
475
app/owner/dashboard/page.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface House {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
price: number;
|
||||
district: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
images: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function OwnerDashboard() {
|
||||
const router = useRouter();
|
||||
const [houses, setHouses] = useState<House[]>([]);
|
||||
const [districts, setDistricts] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingHouse, setEditingHouse] = useState<House | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
price: "",
|
||||
district: "",
|
||||
address: "",
|
||||
phone: "",
|
||||
});
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
async function fetchDistricts() {
|
||||
try {
|
||||
const res = await fetch("/api/districts");
|
||||
const data = await res.json();
|
||||
setDistricts(data.districts || []);
|
||||
if (data.districts?.length > 0) {
|
||||
setFormData(prev => ({ ...prev, district: data.districts[0] }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取地区失败", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me");
|
||||
const data = await res.json();
|
||||
if (!data.user) {
|
||||
router.push("/owner");
|
||||
return;
|
||||
}
|
||||
fetchHouses();
|
||||
} catch (error) {
|
||||
router.push("/owner");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHouses() {
|
||||
try {
|
||||
const res = await fetch("/api/owner/houses");
|
||||
const data = await res.json();
|
||||
setHouses(data.houses || []);
|
||||
} catch (error) {
|
||||
console.error("获取房源失败", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch("/api/auth/me", { method: "DELETE" });
|
||||
router.push("/owner");
|
||||
}
|
||||
|
||||
function openForm(house?: House) {
|
||||
fetchDistricts();
|
||||
if (house) {
|
||||
setEditingHouse(house);
|
||||
setFormData({
|
||||
title: house.title,
|
||||
description: house.description,
|
||||
price: String(house.price),
|
||||
district: house.district,
|
||||
address: house.address,
|
||||
phone: house.phone,
|
||||
});
|
||||
setImages(house.images);
|
||||
} else {
|
||||
setEditingHouse(null);
|
||||
setFormData({
|
||||
title: "",
|
||||
description: "",
|
||||
price: "",
|
||||
district: "",
|
||||
address: "",
|
||||
phone: "",
|
||||
});
|
||||
setImages([]);
|
||||
}
|
||||
setError("");
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const body = {
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
price: Number(formData.price),
|
||||
district: formData.district,
|
||||
address: formData.address,
|
||||
phone: formData.phone,
|
||||
images,
|
||||
};
|
||||
|
||||
let res;
|
||||
if (editingHouse) {
|
||||
res = await fetch(`/api/houses/${editingHouse.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} else {
|
||||
res = await fetch("/api/houses", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setError(data.error || "操作失败");
|
||||
return;
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
fetchHouses();
|
||||
} catch (error) {
|
||||
setError("网络错误,请重试");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("确定要删除这个房源吗?")) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/houses/${id}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
fetchHouses();
|
||||
}
|
||||
} catch (error) {
|
||||
alert("删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
setUploading(true);
|
||||
const newImages: string[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.url) {
|
||||
newImages.push(data.url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传图片失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
setImages(prev => [...prev, ...newImages]);
|
||||
setUploading(false);
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
function removeImage(index: number) {
|
||||
setImages(prev => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pb-20">
|
||||
<header className="bg-white px-4 py-3 flex items-center justify-between shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/" className="text-2xl text-gray-600">
|
||||
←
|
||||
</Link>
|
||||
<h1 className="font-semibold text-gray-900">房源管理</h1>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="text-gray-500 text-sm">
|
||||
退出
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main className="px-4 py-4">
|
||||
<button
|
||||
onClick={() => openForm()}
|
||||
className="w-full py-4 bg-gradient-to-r from-orange-500 to-orange-600 text-white font-semibold rounded-xl shadow-lg mb-4"
|
||||
>
|
||||
+ 发布新房源
|
||||
</button>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="bg-white rounded-xl p-4 animate-pulse">
|
||||
<div className="bg-gray-200 h-32 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-16">
|
||||
<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) => (
|
||||
<div key={house.id} className="bg-white rounded-xl overflow-hidden shadow-sm">
|
||||
<Link href={`/house/${house.id}`}>
|
||||
<div className="flex">
|
||||
{house.images && house.images.length > 0 ? (
|
||||
<img
|
||||
src={house.images[0]}
|
||||
alt={house.title}
|
||||
className="w-28 h-28 object-cover flex-shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-28 h-28 bg-gradient-to-br from-orange-100 to-orange-200 flex items-center justify-center text-3xl flex-shrink-0">
|
||||
🏠
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 p-3 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 truncate">{house.title}</h3>
|
||||
<p className="text-gray-500 text-sm truncate">{house.address}</p>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-orange-500 font-bold">
|
||||
¥{house.price}/月
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs">{house.district}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="border-t border-gray-100 flex">
|
||||
<button
|
||||
onClick={() => openForm(house)}
|
||||
className="flex-1 py-2 text-center text-sm text-gray-600 hover:bg-gray-50"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(house.id)}
|
||||
className="flex-1 py-2 text-center text-sm text-red-500 hover:bg-red-50"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{showForm && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 overflow-y-auto">
|
||||
<div className="min-h-screen py-8 px-4 flex items-start justify-center">
|
||||
<div className="bg-white w-full max-w-md rounded-2xl overflow-hidden">
|
||||
<header className="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-gray-900">
|
||||
{editingHouse ? "编辑房源" : "发布新房源"}
|
||||
</h2>
|
||||
<button onClick={() => setShowForm(false)} className="text-2xl text-gray-400">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 text-red-600 text-sm p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
标题 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="如:南山区科技园精装单间"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
月租(元) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
|
||||
placeholder="1500"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
区域 *
|
||||
</label>
|
||||
<select
|
||||
value={formData.district}
|
||||
onChange={(e) => setFormData({ ...formData, district: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none bg-white"
|
||||
>
|
||||
{districts.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
详细地址 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||
placeholder="如:南山区科技园南区A栋"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
联系电话 *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="13800138000"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
房屋描述
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="描述房屋特点、配套设施、交通情况等..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-orange-500 outline-none resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
上传图片
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<label className="flex-1 flex items-center justify-center py-3 bg-gray-100 rounded-lg cursor-pointer hover:bg-gray-200 transition">
|
||||
<span className="text-sm text-gray-600">📁 选择文件</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex-1 flex items-center justify-center py-3 bg-orange-50 rounded-lg cursor-pointer hover:bg-orange-100 transition">
|
||||
<span className="text-sm text-orange-600">📷 拍照上传</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{uploading && (
|
||||
<div className="text-center text-sm text-gray-500 py-2">
|
||||
上传中...
|
||||
</div>
|
||||
)}
|
||||
{images.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{images.map((url, index) => (
|
||||
<div key={index} className="relative aspect-square rounded-lg overflow-hidden bg-gray-100">
|
||||
<img src={url} alt={`图片${index + 1}`} className="w-full h-full object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeImage(index)}
|
||||
className="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full text-sm flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForm(false)}
|
||||
className="flex-1 py-3 bg-gray-100 text-gray-700 font-medium rounded-xl"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="flex-1 py-3 bg-orange-500 text-white font-medium rounded-xl disabled:opacity-50"
|
||||
>
|
||||
{submitting ? "提交中..." : "保存"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user