🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
@@ -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>
);
}