🎉 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,38 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface NavItem {
name: string;
href: string;
icon: string;
}
export function BottomNav({ items }: { items: NavItem[] }) {
const pathname = usePathname();
if (!items.length) return null;
return (
<nav className="lg:hidden fixed bottom-0 inset-x-0 bg-white border-t border-gray-100 z-40">
<div className="flex items-center justify-around h-16">
{items.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={`flex flex-col items-center gap-0.5 px-3 py-1 ${
active ? "text-blue-600" : "text-gray-400"
}`}
>
<span className="text-xl">{item.icon}</span>
<span className="text-[10px]">{item.name}</span>
</Link>
);
})}
</div>
</nav>
);
}
@@ -0,0 +1,44 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface NavItem {
name: string;
href: string;
icon: string;
}
interface SidebarProps {
items: NavItem[];
projectName: string;
}
export function Sidebar({ items, projectName }: SidebarProps) {
const pathname = usePathname();
return (
<aside className="hidden lg:flex lg:flex-col lg:w-64 lg:fixed lg:inset-y-0 bg-white border-r border-gray-100">
<div className="px-6 py-5 border-b border-gray-50">
<h1 className="text-lg font-bold text-gray-800">{projectName}</h1>
</div>
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
{items.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
active ? "bg-blue-50 text-blue-700 font-medium" : "text-gray-600 hover:bg-gray-50"
}`}
>
<span className="text-lg">{item.icon}</span>
<span>{item.name}</span>
</Link>
);
})}
</nav>
</aside>
);
}
@@ -0,0 +1,28 @@
import type { Pet } from "@/types";
import { Card } from "@/components/ui/Card";
interface PetCardProps {
pet: Pet;
onClick?: () => void;
}
export function PetCard({ pet, onClick }: PetCardProps) {
return (
<Card onClick={onClick} className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-amber-100 to-orange-200 flex items-center justify-center text-2xl overflow-hidden flex-shrink-0">
{pet.avatarUrl ? (
<img src={pet.avatarUrl} alt={pet.name} className="w-full h-full object-cover" />
) : (
"🐾"
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-800 truncate">{pet.name}</h3>
<p className="text-sm text-gray-500">{pet.breed || pet.species} · {pet.weightKg}kg</p>
</div>
<svg className="w-5 h-5 text-gray-300 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Card>
);
}
@@ -0,0 +1,34 @@
import type { Schedule } from "@/types";
interface ScheduleCardProps {
schedule: Schedule;
onToggle?: () => void;
}
export function ScheduleCard({ schedule, onToggle }: ScheduleCardProps) {
const typeIcons: Record<string, string> = {
vaccine: "💉",
deworming: "💊",
checkup: "🩺",
grooming: "✂️",
};
const date = new Date(schedule.scheduledAt);
const isOverdue = date < new Date() && !schedule.completed;
return (
<div className={`flex items-center gap-3 p-3 rounded-lg border ${schedule.completed ? "bg-gray-50 border-gray-100" : isOverdue ? "bg-red-50 border-red-200" : "bg-white border-gray-100"}`}>
<span className="text-2xl">{typeIcons[schedule.type] || "📅"}</span>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium ${schedule.completed ? "text-gray-400 line-through" : "text-gray-800"}`}>{schedule.title}</p>
<p className="text-xs text-gray-500">{date.toLocaleDateString("zh-CN", { month: "short", day: "numeric" })}</p>
</div>
<button
onClick={onToggle}
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${schedule.completed ? "bg-green-500 border-green-500" : "border-gray-300"}`}
>
{schedule.completed && <svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>}
</button>
</div>
);
}
@@ -0,0 +1,31 @@
"use client";
import { type ButtonHTMLAttributes } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "sm" | "md" | "lg";
loading?: boolean;
}
export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) {
const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const variants = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400",
};
const sizes = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base",
};
return (
<button className={`${base} ${variants[variant]} ${sizes[size]} ${className}`} disabled={disabled || loading} {...props}>
{loading && <svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>}
{children}
</button>
);
}
@@ -0,0 +1,18 @@
import type { ReactNode } from "react";
interface CardProps {
children: ReactNode;
className?: string;
onClick?: () => void;
}
export function Card({ children, className = "", onClick }: CardProps) {
return (
<div
className={`bg-white rounded-xl shadow-sm border border-gray-100 p-4 ${onClick ? "cursor-pointer hover:shadow-md transition-shadow" : ""} ${className}`}
onClick={onClick}
>
{children}
</div>
);
}
@@ -0,0 +1,19 @@
import type { ReactNode } from "react";
interface EmptyStateProps {
icon?: string;
title: string;
description?: string;
action?: ReactNode;
}
export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<span className="text-5xl mb-4">{icon}</span>
<h3 className="text-lg font-medium text-gray-700 mb-1">{title}</h3>
{description && <p className="text-sm text-gray-500 mb-4">{description}</p>}
{action}
</div>
);
}
@@ -0,0 +1,22 @@
"use client";
import { type InputHTMLAttributes } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
export function Input({ label, error, className = "", id, ...props }: InputProps) {
return (
<div className="w-full">
{label && <label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">{label}</label>}
<input
id={id}
className={`w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${error ? "border-red-300" : "border-gray-300"} ${className}`}
{...props}
/>
{error && <p className="mt-1 text-xs text-red-500">{error}</p>}
</div>
);
}
@@ -0,0 +1,33 @@
"use client";
import { useEffect, type ReactNode } from "react";
interface ModalProps {
open: boolean;
onClose: () => void;
title: string;
children: ReactNode;
}
export function Modal({ open, onClose, title, children }: ModalProps) {
useEffect(() => {
if (open) document.body.style.overflow = "hidden";
else document.body.style.overflow = "";
return () => { document.body.style.overflow = ""; };
}, [open]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
<div className="relative bg-white rounded-2xl shadow-xl max-w-lg w-full mx-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">{title}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">&times;</button>
</div>
{children}
</div>
</div>
);
}