feat: setup frontend project with bun and dynamic data fetching

- Create new frontend project directory with Next.js 15 + React 19
- Migrate from pnpm to bun for faster package management
- Add TanStack Query + Axios for dynamic data fetching
- Create comprehensive Makefile with development commands
- Setup API layer with query hooks and error handling
- Configure environment variables and bun settings
- Add TypeScript type checking and project documentation
- Update CLAUDE.md with bun-specific development workflow
This commit is contained in:
xujiang
2025-07-08 15:28:26 +08:00
parent d06caee35a
commit 3d197eb7e3
87 changed files with 8329 additions and 0 deletions

View File

@ -0,0 +1,432 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Mail,
Phone,
MapPin,
Clock,
Send,
CheckCircle,
Instagram,
Twitter,
Linkedin,
Camera,
Users,
Heart,
Star,
MessageCircle,
} from "lucide-react"
export function ContactView() {
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
service: "",
message: "",
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate form submission
await new Promise((resolve) => setTimeout(resolve, 2000))
setIsSubmitting(false)
setIsSubmitted(true)
// Reset form after 3 seconds
setTimeout(() => {
setIsSubmitted(false)
setFormData({
name: "",
email: "",
phone: "",
service: "",
message: "",
})
}, 3000)
}
const services = [
{
title: "人像摄影",
description: "个人写真、情侣照、家庭照",
price: "¥800-2000/次",
icon: Users,
features: ["专业化妆", "多套服装", "精修10张", "原片全送"],
},
{
title: "婚礼摄影",
description: "婚礼全程跟拍记录",
price: "¥3000-8000/天",
icon: Heart,
features: ["双机位拍摄", "全天跟拍", "精修50张", "婚礼相册"],
},
{
title: "商业摄影",
description: "产品拍摄、企业宣传",
price: "¥1500-5000/次",
icon: Camera,
features: ["产品精修", "多角度拍摄", "商用授权", "快速交付"],
},
{
title: "活动摄影",
description: "会议、庆典、聚会拍摄",
price: "¥1000-3000/次",
icon: Star,
features: ["现场抓拍", "关键时刻", "快速出片", "团队合影"],
},
]
const contactInfo = [
{
icon: Mail,
label: "邮箱",
value: "zhang.minghua@email.com",
link: "mailto:zhang.minghua@email.com",
},
{
icon: Phone,
label: "电话",
value: "+86 138 0000 0000",
link: "tel:+8613800000000",
},
{
icon: MapPin,
label: "地址",
value: "北京市朝阳区798艺术区",
link: "https://maps.google.com",
},
{
icon: Clock,
label: "工作时间",
value: "周一至周日 9:00-21:00",
link: null,
},
]
const socialLinks = [
{ icon: Instagram, label: "Instagram", link: "https://instagram.com" },
{ icon: Twitter, label: "微博", link: "https://weibo.com" },
{ icon: Linkedin, label: "LinkedIn", link: "https://linkedin.com" },
]
const workflowSteps = [
{
step: "01",
title: "初步沟通",
description: "了解您的需求和想法,确定拍摄风格和时间",
},
{
step: "02",
title: "方案制定",
description: "制定详细的拍摄方案,包括地点、服装、道具等",
},
{
step: "03",
title: "正式拍摄",
description: "按照方案进行专业拍摄,确保每个细节完美",
},
{
step: "04",
title: "后期制作",
description: "精心后期处理,呈现最佳视觉效果",
},
{
step: "05",
title: "作品交付",
description: "按时交付高质量的最终作品",
},
]
return (
<div className="max-w-6xl mx-auto space-y-16">
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-4xl md:text-6xl font-light text-gray-900 mb-4"></h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto"></p>
</div>
{/* Contact Form and Info */}
<div className="grid lg:grid-cols-2 gap-12">
{/* Contact Form */}
<div>
<h2 className="text-2xl font-light text-gray-900 mb-6"></h2>
<Card className="border-0 shadow-lg">
<CardContent className="p-8">
{!isSubmitted ? (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-2 gap-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full"
placeholder="请输入您的姓名"
/>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<Input
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
className="w-full"
placeholder="请输入您的电话"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full"
placeholder="请输入您的邮箱"
/>
</div>
<div>
<label htmlFor="service" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
id="service"
name="service"
value={formData.service}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent"
>
<option value=""></option>
<option value="portrait"></option>
<option value="wedding"></option>
<option value="commercial"></option>
<option value="event"></option>
<option value="other"></option>
</select>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<Textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
required
rows={5}
className="w-full"
placeholder="请详细描述您的拍摄需求、时间、地点等信息..."
/>
</div>
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
</>
)}
</Button>
</form>
) : (
<div className="text-center py-8">
<CheckCircle className="h-16 w-16 text-green-500 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-600">24</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Contact Information */}
<div className="space-y-8">
<div>
<h2 className="text-2xl font-light text-gray-900 mb-6"></h2>
<div className="space-y-4">
{contactInfo.map((info) => (
<div key={info.label} className="flex items-center gap-4">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
<info.icon className="h-5 w-5 text-gray-600" />
</div>
<div>
<div className="text-sm text-gray-500">{info.label}</div>
{info.link ? (
<a
href={info.link}
className="text-gray-900 hover:text-gray-600 transition-colors"
target={info.link.startsWith("http") ? "_blank" : undefined}
rel={info.link.startsWith("http") ? "noopener noreferrer" : undefined}
>
{info.value}
</a>
) : (
<div className="text-gray-900">{info.value}</div>
)}
</div>
</div>
))}
</div>
</div>
{/* Social Links */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="flex gap-4">
{socialLinks.map((social) => (
<a
key={social.label}
href={social.link}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200 transition-colors"
>
<social.icon className="h-5 w-5 text-gray-600" />
</a>
))}
</div>
</div>
{/* Quick Response */}
<Card className="border-0 shadow-sm bg-blue-50">
<CardContent className="p-6">
<MessageCircle className="h-8 w-8 text-blue-600 mb-3" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-600 text-sm">
224
</p>
</CardContent>
</Card>
</div>
</div>
{/* Services */}
<div>
<h2 className="text-3xl font-light text-gray-900 mb-12 text-center"></h2>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{services.map((service) => (
<Card key={service.title} className="border-0 shadow-sm hover:shadow-lg transition-shadow">
<CardContent className="p-6">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<service.icon className="h-6 w-6 text-gray-600" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">{service.title}</h3>
<p className="text-gray-600 text-sm mb-3">{service.description}</p>
<div className="text-lg font-medium text-gray-900 mb-4">{service.price}</div>
<div className="space-y-1">
{service.features.map((feature) => (
<div key={feature} className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full"></div>
<span className="text-xs text-gray-600">{feature}</span>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
</div>
{/* Workflow */}
<div className="bg-gray-50 rounded-2xl p-8 md:p-12">
<h2 className="text-3xl font-light text-gray-900 mb-12 text-center"></h2>
<div className="grid md:grid-cols-5 gap-6">
{workflowSteps.map((step, index) => (
<div key={step.step} className="text-center">
<div className="w-16 h-16 bg-gray-900 text-white rounded-full flex items-center justify-center mx-auto mb-4 text-lg font-medium">
{step.step}
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">{step.title}</h3>
<p className="text-gray-600 text-sm">{step.description}</p>
{index < workflowSteps.length - 1 && (
<div className="hidden md:block absolute top-8 left-full w-full h-0.5 bg-gray-200 transform translate-x-4"></div>
)}
</div>
))}
</div>
</div>
{/* FAQ */}
<div>
<h2 className="text-3xl font-light text-gray-900 mb-8 text-center"></h2>
<div className="grid md:grid-cols-2 gap-6">
<Card className="border-0 shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm">
2-4
</p>
</CardContent>
</Card>
<Card className="border-0 shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm">
7-14
</p>
</CardContent>
</Card>
<Card className="border-0 shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm">
</p>
</CardContent>
</Card>
<Card className="border-0 shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm">
</p>
</CardContent>
</Card>
</div>
</div>
</div>
)
}