🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
const mockPhotos = Array.from({ length: 8 }, (_, i) => ({
|
||||
id: `p${i}`,
|
||||
color: ["#FEE2E2", "#FEF3C7", "#D1FAE5", "#DBEAFE", "#EDE9FE", "#FCE7F3", "#E0E7FF", "#FFEDD5"][i],
|
||||
emoji: ["🐱", "🐶", "🐱", "🐶", "🐱", "🐱", "🐶", "🐱"][i],
|
||||
date: new Date(2026, 5, i + 1).toISOString(),
|
||||
}));
|
||||
|
||||
export default function AlbumPage() {
|
||||
const [photos] = useState(mockPhotos);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">成长相册</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">记录宠物的成长瞬间</p>
|
||||
</div>
|
||||
<Button variant="primary" size="sm">+ 上传照片</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{photos.map(photo => (
|
||||
<div key={photo.id} className="aspect-square rounded-xl flex items-center justify-center text-4xl" style={{ backgroundColor: photo.color }}>
|
||||
{photo.emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-gray-400">下拉加载更多照片</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
const CATEGORIES = [
|
||||
{ key: "food", icon: "🍖", label: "饮食" },
|
||||
{ key: "exercise", icon: "🏃", label: "运动" },
|
||||
{ key: "excretion", icon: "💩", label: "排泄" },
|
||||
{ key: "weight", icon: "⚖️", label: "体重" },
|
||||
];
|
||||
|
||||
interface LogEntry {
|
||||
id: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
value: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export default function DailyLogPage() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [category, setCategory] = useState("food");
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const addLog = () => {
|
||||
if (!value.trim()) return;
|
||||
const cat = CATEGORIES.find(c => c.key === category)!;
|
||||
setLogs(prev => [{
|
||||
id: Date.now().toString(),
|
||||
category: cat.key,
|
||||
icon: cat.icon,
|
||||
value,
|
||||
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}, ...prev]);
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">日常记录</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">记录宠物的饮食、运动、排泄和体重变化</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Record */}
|
||||
<Card>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
{CATEGORIES.map(c => (
|
||||
<button key={c.key} onClick={() => setCategory(c.key)}
|
||||
className={`flex-1 py-2 rounded-lg text-sm text-center transition-colors ${category === c.key ? "bg-blue-100 text-blue-700 font-medium" : "bg-gray-50 text-gray-500"}`}>
|
||||
{c.icon} {c.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input placeholder="输入记录内容..." value={value} onChange={e => setValue(e.target.value)} onKeyDown={e => e.key === "Enter" && addLog()} />
|
||||
<Button onClick={addLog} size="sm">记录</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Log Timeline */}
|
||||
<div className="space-y-2">
|
||||
{logs.length > 0 ? logs.map(log => (
|
||||
<div key={log.id} className="flex items-center gap-3 px-4 py-2 bg-white rounded-lg border border-gray-100">
|
||||
<span className="text-xl">{log.icon}</span>
|
||||
<span className="flex-1 text-sm text-gray-700">{log.value}</span>
|
||||
<span className="text-xs text-gray-400">{log.time}</span>
|
||||
</div>
|
||||
)) : <EmptyState icon="📝" title="暂无记录" description="使用上方表单记录宠物日常" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
|
||||
const mockHospitals = [
|
||||
{ id: "h1", name: "宠颐生动物医院", address: "朝阳区建国路88号", phone: "010-88886666", rating: 4.8, distance: 1.2, services: ["疫苗", "体检", "手术", "美容"] },
|
||||
{ id: "h2", name: "瑞鹏宠物医院", address: "海淀区中关村大街1号", phone: "010-66668888", rating: 4.6, distance: 2.8, services: ["急诊", "住院", "化验"] },
|
||||
{ id: "h3", name: "佳雯宠物诊所", address: "西城区金融街16号", phone: "010-55556666", rating: 4.5, distance: 3.5, services: ["疫苗", "驱虫", "体检"] },
|
||||
];
|
||||
|
||||
export default function HospitalPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = mockHospitals.filter(h =>
|
||||
h.name.includes(search) || h.address.includes(search)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">附近医院</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">查找附近的宠物医院</p>
|
||||
</div>
|
||||
|
||||
<Input placeholder="搜索医院名称或地址..." value={search} onChange={e => setSearch(e.target.value)} />
|
||||
|
||||
<div className="space-y-3">
|
||||
{filtered.map(h => (
|
||||
<Card key={h.id}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center text-blue-500 flex-shrink-0">🏥</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-gray-800 truncate">{h.name}</h3>
|
||||
<span className="text-xs px-1.5 py-0.5 bg-yellow-100 text-yellow-700 rounded flex-shrink-0">⭐ {h.rating}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">{h.address}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{h.distance}km · {h.phone}</p>
|
||||
<div className="flex gap-1 mt-2">
|
||||
{h.services.map(s => <span key={s} className="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded">{s}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Sidebar } from "@/components/layout/Sidebar";
|
||||
import { BottomNav } from "@/components/layout/BottomNav";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PetCare",
|
||||
description: "一款帮助宠物主人管理宠物健康、日常活动和成长记录的移动应用。",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="bg-gray-50 min-h-screen">
|
||||
<div className="flex">
|
||||
{/* Desktop Sidebar */}
|
||||
<Sidebar items={[{"name":"首页","href":"/home","icon":"🏠"},{"name":"宠物档案","href":"/pet/[id]","icon":"📋"},{"name":"健康日程","href":"/schedule","icon":"📅"},{"name":"日常记录","href":"/daily-log","icon":"✏️"},{"name":"成长相册","href":"/album","icon":"📸"}] } projectName="PetCare" />
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 lg:pl-64 pb-20 lg:pb-0">
|
||||
<div className="max-w-2xl mx-auto px-4 py-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/* Mobile Bottom Nav */}
|
||||
<BottomNav items={[{"name":"首页","href":"/home","icon":"🏠"},{"name":"宠物档案","href":"/pet/[id]","icon":"📋"},{"name":"健康日程","href":"/schedule","icon":"📅"},{"name":"日常记录","href":"/daily-log","icon":"✏️"}] } />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl p-6 text-white">
|
||||
<h1 className="text-2xl font-bold">PetCare</h1>
|
||||
<p className="mt-2 text-blue-100 text-sm">宠物卡片、今日提醒、快捷入口</p>
|
||||
</section>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">快捷入口</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link key="0" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📋</span>
|
||||
<span className="text-sm text-gray-600">宠物档案</span>
|
||||
</Link>
|
||||
<Link key="1" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📅</span>
|
||||
<span className="text-sm text-gray-600">健康日程</span>
|
||||
</Link>
|
||||
<Link key="2" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📝</span>
|
||||
<span className="text-sm text-gray-600">日常记录</span>
|
||||
</Link>
|
||||
<Link key="3" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📸</span>
|
||||
<span className="text-sm text-gray-600">成长相册</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Activity / Stats */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">今日概览</h2>
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">欢迎使用 PetCare</p>
|
||||
<p className="text-xs text-gray-400 mt-1">开始探索功能吧</p>
|
||||
</div>
|
||||
<span className="text-3xl">👋</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import type { Pet, Schedule } from "@/types";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { PetCard } from "@/components/pet/PetCard";
|
||||
import { ScheduleCard } from "@/components/pet/ScheduleCard";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
export default function PetDetailPage() {
|
||||
const params = useParams();
|
||||
const [pet, setPet] = useState<Pet | null>(null);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Replace with actual API call
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
// Simulated data
|
||||
setPet({
|
||||
id: params.id as string,
|
||||
ownerId: "u1",
|
||||
name: "毛球",
|
||||
species: "猫",
|
||||
breed: "英短",
|
||||
birthDate: "2023-03-15",
|
||||
weightKg: 4.5,
|
||||
avatarUrl: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
setSchedules([
|
||||
{ id: "s1", petId: params.id as string, type: "vaccine", title: "年度疫苗", scheduledAt: "2026-07-01T09:00:00Z", completed: false },
|
||||
{ id: "s2", petId: params.id as string, type: "deworming", title: "季度驱虫", scheduledAt: "2026-06-15T10:00:00Z", completed: false },
|
||||
{ id: "s3", petId: params.id as string, type: "checkup", title: "体检", scheduledAt: "2026-05-20T14:00:00Z", completed: true },
|
||||
]);
|
||||
setLoading(false);
|
||||
};
|
||||
loadData();
|
||||
}, [params.id]);
|
||||
|
||||
if (loading) return <div className="flex justify-center py-16"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" /></div>;
|
||||
if (!pet) return <EmptyState icon="🐾" title="未找到宠物" description="该宠物可能已被删除" />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Pet Profile */}
|
||||
<div className="bg-gradient-to-br from-amber-100 to-orange-100 rounded-2xl p-6 text-center">
|
||||
<div className="w-24 h-24 mx-auto rounded-full bg-white shadow flex items-center justify-center text-5xl">
|
||||
{pet.avatarUrl ? <img src={pet.avatarUrl} alt={pet.name} className="w-full h-full rounded-full object-cover" /> : "🐱"}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 mt-3">{pet.name}</h1>
|
||||
<p className="text-sm text-gray-500">{pet.breed} · {pet.weightKg}kg · {pet.species}</p>
|
||||
</div>
|
||||
|
||||
{/* Health Schedule */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">健康日程</h2>
|
||||
<div className="space-y-2">
|
||||
{schedules.length > 0 ? schedules.map(s => <ScheduleCard key={s.id} schedule={s} />) : <EmptyState icon="📅" title="暂无日程" />}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Info */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-800">{pet.weightKg}</p>
|
||||
<p className="text-xs text-gray-500">体重 (kg)</p>
|
||||
</Card>
|
||||
<Card className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-800">{new Date().getFullYear() - new Date(pet.birthDate).getFullYear()}岁</p>
|
||||
<p className="text-xs text-gray-500">年龄</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
const menuItems = [
|
||||
{ icon: "⚙️", label: "设置", href: "#" },
|
||||
{ icon: "🔔", label: "通知", href: "#" },
|
||||
{ icon: "❓", label: "帮助与反馈", href: "#" },
|
||||
{ icon: "📄", label: "关于我们", href: "#" },
|
||||
];
|
||||
|
||||
export default function ProfilePage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* User Info */}
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl p-6 text-white text-center">
|
||||
<div className="w-20 h-20 mx-auto rounded-full bg-white/20 flex items-center justify-center text-3xl backdrop-blur">😊</div>
|
||||
<h1 className="text-xl font-bold mt-3">用户昵称</h1>
|
||||
<p className="text-sm text-blue-100">138****1234</p>
|
||||
<Button variant="ghost" className="mt-3 bg-white/10 text-white hover:bg-white/20" size="sm">编辑资料</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Card className="text-center"><p className="text-xl font-bold text-gray-800">3</p><p className="text-xs text-gray-500">我的宠物</p></Card>
|
||||
<Card className="text-center"><p className="text-xl font-bold text-gray-800">12</p><p className="text-xs text-gray-500">健康日程</p></Card>
|
||||
<Card className="text-center"><p className="text-xl font-bold text-gray-800">56</p><p className="text-xs text-gray-500">日常记录</p></Card>
|
||||
</div>
|
||||
|
||||
{/* Menu */}
|
||||
<Card className="divide-y divide-gray-50 p-0">
|
||||
{menuItems.map((item, i) => (
|
||||
<a key={i} href={item.href} className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors">
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="flex-1 text-sm text-gray-700">{item.label}</span>
|
||||
<svg className="w-4 h-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</a>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Schedule } from "@/types";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { ScheduleCard } from "@/components/pet/ScheduleCard";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
const mockSchedules: Schedule[] = [
|
||||
{ id: "s1", petId: "p1", type: "vaccine", title: "年度疫苗加强针", scheduledAt: "2026-07-01T09:00:00Z", completed: false },
|
||||
{ id: "s2", petId: "p1", type: "deworming", title: "季度驱虫", scheduledAt: "2026-06-15T10:00:00Z", completed: false },
|
||||
{ id: "s3", petId: "p1", type: "checkup", title: "半年度体检", scheduledAt: "2026-05-20T14:00:00Z", completed: true },
|
||||
{ id: "s4", petId: "p1", type: "grooming", title: "美容护理", scheduledAt: "2026-06-10T11:00:00Z", completed: false },
|
||||
];
|
||||
|
||||
export default function SchedulePage() {
|
||||
const [schedules, setSchedules] = useState<Schedule[]>(mockSchedules);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const toggleComplete = (id: string) => {
|
||||
setSchedules(prev => prev.map(s => s.id === id ? { ...s, completed: !s.completed } : s));
|
||||
};
|
||||
|
||||
const pending = schedules.filter(s => !s.completed);
|
||||
const done = schedules.filter(s => s.completed);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">健康日程</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">管理宠物的疫苗、驱虫、体检安排</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowModal(true)} variant="primary" size="sm">+ 添加日程</Button>
|
||||
</div>
|
||||
|
||||
{/* Pending */}
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase mb-2">待完成 ({pending.length})</h2>
|
||||
<div className="space-y-2">
|
||||
{pending.length > 0 ? pending.map(s => <ScheduleCard key={s.id} schedule={s} onToggle={() => toggleComplete(s.id)} />) : <EmptyState icon="✅" title="全部完成" />}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Completed */}
|
||||
{done.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase mb-2">已完成 ({done.length})</h2>
|
||||
<div className="space-y-2">
|
||||
{done.map(s => <ScheduleCard key={s.id} schedule={s} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Modal open={showModal} onClose={() => setShowModal(false)} title="添加日程">
|
||||
<div className="space-y-3">
|
||||
<Input label="标题" placeholder="如:年度疫苗" />
|
||||
<Input label="日期" type="date" />
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="secondary" onClick={() => setShowModal(false)}>取消</Button>
|
||||
<Button variant="primary" onClick={() => setShowModal(false)}>保存</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user