Files
smalltown/app/owner/dashboard/page.tsx
2026-03-24 22:54:24 +08:00

493 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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[];
status: string;
reject_reason?: string;
reviewed_at?: 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 className="mt-2">
{house.status === 'pending' && (
<span className="inline-block bg-yellow-100 text-yellow-700 text-xs px-2 py-1 rounded"></span>
)}
{house.status === 'approved' && (
<span className="inline-block bg-green-100 text-green-700 text-xs px-2 py-1 rounded"></span>
)}
{house.status === 'rejected' && (
<span className="inline-block bg-red-100 text-red-700 text-xs px-2 py-1 rounded"></span>
)}
</div>
{house.status === 'rejected' && house.reject_reason && (
<p className="text-red-500 text-xs mt-1">{house.reject_reason}</p>
)}
</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>
);
}