update
This commit is contained in:
parent
f730fa4472
commit
f78a466c39
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
36
README.md
36
README.md
@ -1,2 +1,36 @@
|
||||
# bsd
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
12
app/api/config/route.ts
Normal file
12
app/api/config/route.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
const file = await fs.readFile(process.cwd() + '/data/config.yaml', 'utf8')
|
||||
return new NextResponse(file, {
|
||||
headers: {
|
||||
'Content-Type': 'text/yaml',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
app/fonts/GeistMonoVF.woff
Normal file
BIN
app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
app/fonts/GeistVF.woff
Normal file
BIN
app/fonts/GeistVF.woff
Normal file
Binary file not shown.
78
app/globals.css
Normal file
78
app/globals.css
Normal file
@ -0,0 +1,78 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
54
app/hooks/useConfig.ts
Normal file
54
app/hooks/useConfig.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import yaml from 'yaml';
|
||||
|
||||
// 定义活动项的接口
|
||||
interface ActivityParticipant {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Activity {
|
||||
title: string;
|
||||
coverImage: string;
|
||||
description: string;
|
||||
date: string;
|
||||
participants: ActivityParticipant[];
|
||||
organizer: string;
|
||||
}
|
||||
|
||||
// 定义展示项的接口
|
||||
interface ShowcaseItem {
|
||||
title: string;
|
||||
image: string;
|
||||
author: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
// 定义配置接口
|
||||
interface Config {
|
||||
activities: Activity[];
|
||||
showcase: ShowcaseItem[];
|
||||
}
|
||||
|
||||
export function useConfig() {
|
||||
const [config, setConfig] = useState<Config>({ activities: [], showcase: [] });
|
||||
|
||||
useEffect(() => {
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const configYaml = await response.text();
|
||||
const parsedConfig: Config = yaml.parse(configYaml);
|
||||
setConfig(parsedConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to load configuration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
return { config };
|
||||
}
|
10
app/inventory/page.tsx
Normal file
10
app/inventory/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import InventoryManagement from '@/components/InventoryManagement';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-br from-blue-100 to-indigo-100 p-4 sm:p-8">
|
||||
<InventoryManagement />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
26
app/layout.tsx
Normal file
26
app/layout.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import './globals.css'
|
||||
import { Inter } from 'next/font/google'
|
||||
import Navbar from '@/components/Navbar'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: '影视星河摄影社团',
|
||||
description: '影视星河摄影社团官方网站',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<Navbar />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
84
app/page.tsx
Normal file
84
app/page.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import ActivityCard from '@/components/ActivityCard'
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useConfig } from './hooks/useConfig'
|
||||
|
||||
export default function Home() {
|
||||
const { config } = useConfig()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
const filteredActivities = config.activities.filter(activity =>
|
||||
activity.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
activity.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
activity.organizer.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play().catch(error => {
|
||||
console.error("视频自动播放失败:", error)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<section className="h-screen relative overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute top-0 left-0 w-full h-full object-cover"
|
||||
>
|
||||
<source src="https://c.mmeiblog.cn/d/share/newbili/影视星河片头.mp4" type="video/mp4" />
|
||||
您的浏览器不支持视频标签。
|
||||
</video>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-black opacity-50"></div>
|
||||
<div className="relative z-10 h-full flex flex-col justify-center items-center text-white">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1 }}
|
||||
className="text-center"
|
||||
>
|
||||
<h1 className="text-4xl font-bold mb-4">欢迎来到影视星河</h1>
|
||||
<p className="text-xl mb-8">用镜头捕捉世界的美</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-16 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-3xl font-bold mb-8 text-center">社团活动</h2>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索活动、描述或组织者..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="max-w-md mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{filteredActivities.map((activity, index) => (
|
||||
<ActivityCard
|
||||
key={index}
|
||||
activity={{
|
||||
...activity,
|
||||
participants: activity.participants.map(participant => participant.name)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
80
app/showcase/page.tsx
Normal file
80
app/showcase/page.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useConfig } from '../hooks/useConfig'
|
||||
|
||||
const ITEMS_PER_PAGE = 6
|
||||
|
||||
export default function Showcase() {
|
||||
const { config } = useConfig()
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const filteredShowcase = config.showcase.filter(photo =>
|
||||
photo.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
photo.author.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const totalPages = Math.ceil(filteredShowcase.length / ITEMS_PER_PAGE)
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
const currentPhotos = filteredShowcase.slice(startIndex, endIndex)
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-100 py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold mb-8 text-center">摄影作品展示</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索作品或作者..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="max-w-md mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{currentPhotos.map((photo, index) => (
|
||||
<div key={index} className="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<div className="relative h-64">
|
||||
<Image
|
||||
src={photo.image}
|
||||
alt={photo.title}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-2">{photo.title}</h2>
|
||||
<p className="text-gray-600 mb-2">作者:{photo.author}</p>
|
||||
<p className="text-gray-500 text-sm">拍摄日期:{photo.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex justify-center items-center space-x-4">
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||
上一页
|
||||
</Button>
|
||||
<span>第 {currentPage} 页,共 {totalPages} 页</span>
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
81
components/ActivityCard.tsx
Normal file
81
components/ActivityCard.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
|
||||
interface ActivityProps {
|
||||
activity: {
|
||||
title: string
|
||||
coverImage: string
|
||||
description: string
|
||||
date: string
|
||||
participants: string[]
|
||||
organizer: string
|
||||
}
|
||||
}
|
||||
|
||||
const ActivityCard: React.FC<ActivityProps> = ({ activity }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<div className="relative h-48">
|
||||
<Image
|
||||
src={activity.coverImage}
|
||||
alt={activity.title}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-semibold mb-2">{activity.title}</h3>
|
||||
<p className="text-gray-600 mb-2">{activity.description}</p>
|
||||
<p className="text-sm text-gray-500 mb-2">日期:{activity.date}</p>
|
||||
<p className="text-sm text-gray-500 mb-2">组织者:{activity.organizer}</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-semibold mb-1">参与者:</p>
|
||||
{activity.participants.length <= 3 ? (
|
||||
<ul className="list-disc list-inside">
|
||||
{activity.participants.map((participant, index) => (
|
||||
<li key={index} className="text-sm text-gray-600">{participant}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div>
|
||||
<ul className="list-disc list-inside">
|
||||
{activity.participants.slice(0, 3).map((participant, index) => (
|
||||
<li key={index} className="text-sm text-gray-600">{participant}</li>
|
||||
))}
|
||||
</ul>
|
||||
{isExpanded && (
|
||||
<ul className="list-disc list-inside mt-2">
|
||||
{activity.participants.slice(3).map((participant, index) => (
|
||||
<li key={index + 3} className="text-sm text-gray-600">{participant}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<button
|
||||
className="mt-2 text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
收起 <ChevronUp className="ml-1 w-4 h-4" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
展开更多 ({activity.participants.length - 3}) <ChevronDown className="ml-1 w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivityCard
|
||||
|
28
components/Alert.tsx
Normal file
28
components/Alert.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface AlertProps {
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'warning';
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Alert({ message, type, onClose }: AlertProps) {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
const bgColor = type === 'success' ? 'bg-green-100' : type === 'error' ? 'bg-red-100' : 'bg-yellow-100';
|
||||
const textColor = type === 'success' ? 'text-green-700' : type === 'error' ? 'text-red-700' : 'text-yellow-700';
|
||||
const borderColor = type === 'success' ? 'border-green-500' : type === 'error' ? 'border-red-500' : 'border-yellow-500';
|
||||
|
||||
return (
|
||||
<div className={`fixed top-4 right-4 p-4 rounded-lg shadow-lg ${bgColor} ${textColor} border-l-4 ${borderColor}`}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
62
components/DeviceForm.tsx
Normal file
62
components/DeviceForm.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState } from 'react';
|
||||
import { Device } from './InventoryManagement';
|
||||
|
||||
interface DeviceFormProps {
|
||||
onAddDevice: (device: Device) => void;
|
||||
}
|
||||
|
||||
export default function DeviceForm({ onAddDevice }: DeviceFormProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [serialNumber, setSerialNumber] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const newDevice: Device = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
quantity: parseInt(quantity),
|
||||
serialNumber,
|
||||
};
|
||||
onAddDevice(newDevice);
|
||||
setName('');
|
||||
setQuantity('');
|
||||
setSerialNumber('');
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-700">添加新设备</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="设备名称"
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={e => setQuantity(e.target.value)}
|
||||
placeholder="数量"
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={serialNumber}
|
||||
onChange={e => setSerialNumber(e.target.value)}
|
||||
placeholder="序列号"
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<button type="submit" className="bg-indigo-500 text-white px-4 py-2 rounded-full hover:bg-indigo-600 transition duration-300">
|
||||
添加设备
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
93
components/DeviceList.tsx
Normal file
93
components/DeviceList.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { Device } from './InventoryManagement';
|
||||
|
||||
interface DeviceListProps {
|
||||
devices: Device[];
|
||||
onUpdateDevice: (device: Device) => void;
|
||||
onDeleteDevice: (id: string) => void;
|
||||
showAlert: (message: string, type: 'success' | 'error' | 'warning') => void;
|
||||
}
|
||||
|
||||
export default function DeviceList({ devices, onUpdateDevice, onDeleteDevice, showAlert }: DeviceListProps) {
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
|
||||
const handleEdit = (device: Device) => {
|
||||
setEditingId(device.id);
|
||||
};
|
||||
|
||||
const handleSave = (device: Device) => {
|
||||
onUpdateDevice(device);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm('确定要删除这个设备吗?')) {
|
||||
onDeleteDevice(id);
|
||||
showAlert('设备已成功删除', 'success');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-700">设备列表</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-indigo-100 text-indigo-700">
|
||||
<th className="p-2 text-left rounded-tl-lg">名称</th>
|
||||
<th className="p-2 text-left">数量</th>
|
||||
<th className="p-2 text-left">序列号</th>
|
||||
<th className="p-2 text-left rounded-tr-lg">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{devices.map(device => (
|
||||
<tr key={device.id} className="border-b border-indigo-100 hover:bg-indigo-50 transition duration-300">
|
||||
<td className="p-2">
|
||||
{editingId === device.id ? (
|
||||
<input
|
||||
type="text"
|
||||
value={device.name}
|
||||
onChange={e => onUpdateDevice({ ...device, name: e.target.value })}
|
||||
className="border rounded p-1 w-full"
|
||||
/>
|
||||
) : (
|
||||
device.name
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === device.id ? (
|
||||
<input
|
||||
type="number"
|
||||
value={device.quantity}
|
||||
onChange={e => onUpdateDevice({ ...device, quantity: parseInt(e.target.value) })}
|
||||
className="border rounded p-1 w-20"
|
||||
/>
|
||||
) : (
|
||||
device.quantity
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">{device.serialNumber}</td>
|
||||
<td className="p-2">
|
||||
{editingId === device.id ? (
|
||||
<button onClick={() => handleSave(device)} className="bg-green-500 text-white px-3 py-1 rounded-full mr-2 hover:bg-green-600 transition duration-300">
|
||||
保存
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => handleEdit(device)} className="bg-blue-500 text-white px-3 py-1 rounded-full mr-2 hover:bg-blue-600 transition duration-300">
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => handleDelete(device.id)} className="bg-red-500 text-white px-3 py-1 rounded-full hover:bg-red-600 transition duration-300">
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
251
components/DistributionRecord.tsx
Normal file
251
components/DistributionRecord.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import { useState } from 'react';
|
||||
import { Device, Distribution } from './InventoryManagement';
|
||||
|
||||
interface DistributionRecordProps {
|
||||
devices: Device[];
|
||||
distributions: Distribution[];
|
||||
onAddDistribution: (distribution: Distribution) => void;
|
||||
onUpdateDistribution: (distribution: Distribution) => void;
|
||||
onDeleteDistribution: (id: string) => void;
|
||||
showAlert: (message: string, type: 'success' | 'error' | 'warning') => void;
|
||||
}
|
||||
|
||||
export default function DistributionRecord({
|
||||
devices,
|
||||
distributions,
|
||||
onAddDistribution,
|
||||
onUpdateDistribution,
|
||||
onDeleteDistribution,
|
||||
showAlert,
|
||||
}: DistributionRecordProps) {
|
||||
const [deviceId, setDeviceId] = useState('');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const newDistribution: Distribution = {
|
||||
id: Date.now().toString(),
|
||||
deviceId,
|
||||
recipient,
|
||||
startDate,
|
||||
endDate,
|
||||
quantity: parseInt(quantity),
|
||||
};
|
||||
if (isDeviceAvailable(deviceId, parseInt(quantity))) {
|
||||
onAddDistribution(newDistribution);
|
||||
resetForm();
|
||||
showAlert('设备分发成功', 'success');
|
||||
} else {
|
||||
showAlert('设备数量不足,无法分发', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setDeviceId('');
|
||||
setRecipient('');
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setQuantity('');
|
||||
};
|
||||
|
||||
const handleEdit = (distribution: Distribution) => {
|
||||
setEditingId(distribution.id);
|
||||
setDeviceId(distribution.deviceId);
|
||||
setRecipient(distribution.recipient);
|
||||
setStartDate(distribution.startDate);
|
||||
setEndDate(distribution.endDate);
|
||||
setQuantity(distribution.quantity.toString());
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (editingId) {
|
||||
const updatedDistribution: Distribution = {
|
||||
id: editingId,
|
||||
deviceId,
|
||||
recipient,
|
||||
startDate,
|
||||
endDate,
|
||||
quantity: parseInt(quantity),
|
||||
};
|
||||
onUpdateDistribution(updatedDistribution);
|
||||
setEditingId(null);
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm('确定要删除这条分发记录吗?')) {
|
||||
onDeleteDistribution(id);
|
||||
showAlert('分发记录已成功删除', 'success');
|
||||
}
|
||||
};
|
||||
|
||||
const isDeviceAvailable = (deviceId: string, quantity: number) => {
|
||||
const device = devices.find(d => d.id === deviceId);
|
||||
return device ? device.quantity >= quantity : false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-700">分发记录</h2>
|
||||
<form onSubmit={handleSubmit} className="mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<select
|
||||
value={deviceId}
|
||||
onChange={e => setDeviceId(e.target.value)}
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
>
|
||||
<option value="">选择设备</option>
|
||||
{devices.map(device => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.name} (可用: {device.quantity})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={recipient}
|
||||
onChange={e => setRecipient(e.target.value)}
|
||||
placeholder="领用人"
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={e => setQuantity(e.target.value)}
|
||||
placeholder="数量"
|
||||
required
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 text-white px-4 py-2 rounded-full hover:bg-indigo-600 transition duration-300"
|
||||
disabled={!isDeviceAvailable(deviceId, parseInt(quantity))}
|
||||
>
|
||||
添加分发记录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-indigo-100 text-indigo-700">
|
||||
<th className="p-2 text-left rounded-tl-lg">设备</th>
|
||||
<th className="p-2 text-left">领用人</th>
|
||||
<th className="p-2 text-left">开始日期</th>
|
||||
<th className="p-2 text-left">结束日期</th>
|
||||
<th className="p-2 text-left">数量</th>
|
||||
<th className="p-2 text-left rounded-tr-lg">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{distributions.map(distribution => (
|
||||
<tr key={distribution.id} className="border-b border-indigo-100 hover:bg-indigo-50 transition duration-300">
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<select
|
||||
value={deviceId}
|
||||
onChange={e => setDeviceId(e.target.value)}
|
||||
className="border rounded p-1 w-full"
|
||||
>
|
||||
{devices.map(device => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
devices.find(d => d.id === distribution.deviceId)?.name
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<input
|
||||
type="text"
|
||||
value={recipient}
|
||||
onChange={e => setRecipient(e.target.value)}
|
||||
className="border rounded p-1 w-full"
|
||||
/>
|
||||
) : (
|
||||
distribution.recipient
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
className="border rounded p-1 w-full"
|
||||
/>
|
||||
) : (
|
||||
distribution.startDate
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
className="border rounded p-1 w-full"
|
||||
/>
|
||||
) : (
|
||||
distribution.endDate
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={e => setQuantity(e.target.value)}
|
||||
className="border rounded p-1 w-20"
|
||||
/>
|
||||
) : (
|
||||
distribution.quantity
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{editingId === distribution.id ? (
|
||||
<button onClick={handleSave} className="bg-green-500 text-white px-3 py-1 rounded-full mr-2 hover:bg-green-600 transition duration-300">
|
||||
保存
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => handleEdit(distribution)} className="bg-blue-500 text-white px-3 py-1 rounded-full mr-2 hover:bg-blue-600 transition duration-300">
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => handleDelete(distribution.id)} className="bg-red-500 text-white px-3 py-1 rounded-full hover:bg-red-600 transition duration-300">
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
152
components/InventoryManagement.tsx
Normal file
152
components/InventoryManagement.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import DeviceList from './DeviceList';
|
||||
import DeviceForm from './DeviceForm';
|
||||
import DistributionRecord from './DistributionRecord';
|
||||
import { saveToLocalStorage, getFromLocalStorage, exportData } from '../utils/localStorage';
|
||||
import Alert from './Alert';
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
serialNumber: string;
|
||||
}
|
||||
|
||||
export interface Distribution {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
recipient: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export default function Device() {
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [distributions, setDistributions] = useState<Distribution[]>([]);
|
||||
const [alert, setAlert] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const savedDevices = getFromLocalStorage('devices');
|
||||
const savedDistributions = getFromLocalStorage('distributions');
|
||||
if (savedDevices) setDevices(savedDevices);
|
||||
if (savedDistributions) setDistributions(savedDistributions);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
saveToLocalStorage('devices', devices);
|
||||
}, [devices]);
|
||||
|
||||
useEffect(() => {
|
||||
saveToLocalStorage('distributions', distributions);
|
||||
}, [distributions]);
|
||||
|
||||
const handleAddDevice = (device: Device) => {
|
||||
setDevices([...devices, device]);
|
||||
};
|
||||
|
||||
const handleUpdateDevice = (updatedDevice: Device) => {
|
||||
setDevices(devices.map(d => d.id === updatedDevice.id ? updatedDevice : d));
|
||||
};
|
||||
|
||||
const handleDeleteDevice = (id: string) => {
|
||||
setDevices(devices.filter(d => d.id !== id));
|
||||
setDistributions(distributions.filter(d => d.deviceId !== id));
|
||||
showAlert('设备已成功删除', 'success');
|
||||
};
|
||||
|
||||
const handleAddDistribution = (distribution: Distribution) => {
|
||||
const device = devices.find(d => d.id === distribution.deviceId);
|
||||
if (device && device.quantity >= distribution.quantity) {
|
||||
setDistributions([...distributions, distribution]);
|
||||
handleUpdateDevice({ ...device, quantity: device.quantity - distribution.quantity });
|
||||
showAlert('设备分发成功', 'success');
|
||||
} else {
|
||||
showAlert('设备数量不足,无法分发', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDistribution = (updatedDistribution: Distribution) => {
|
||||
const oldDistribution = distributions.find(d => d.id === updatedDistribution.id);
|
||||
if (oldDistribution) {
|
||||
const device = devices.find(d => d.id === updatedDistribution.deviceId);
|
||||
if (device) {
|
||||
const quantityDiff = oldDistribution.quantity - updatedDistribution.quantity;
|
||||
handleUpdateDevice({ ...device, quantity: device.quantity + quantityDiff });
|
||||
}
|
||||
}
|
||||
setDistributions(distributions.map(d => d.id === updatedDistribution.id ? updatedDistribution : d));
|
||||
};
|
||||
|
||||
const handleDeleteDistribution = (id: string) => {
|
||||
const distribution = distributions.find(d => d.id === id);
|
||||
if (distribution) {
|
||||
const device = devices.find(d => d.id === distribution.deviceId);
|
||||
if (device) {
|
||||
handleUpdateDevice({ ...device, quantity: device.quantity + distribution.quantity });
|
||||
}
|
||||
}
|
||||
setDistributions(distributions.filter(d => d.id !== id));
|
||||
};
|
||||
|
||||
const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
const importedData = JSON.parse(content);
|
||||
if (importedData.devices) setDevices(importedData.devices);
|
||||
if (importedData.distributions) setDistributions(importedData.distributions);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
exportData({ devices, distributions }, 'inventory_data.json');
|
||||
};
|
||||
|
||||
const showAlert = (message: string, type: 'success' | 'error' | 'warning') => {
|
||||
setAlert({ message, type });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto bg-white rounded-lg shadow-lg p-6 space-y-8">
|
||||
<h1 className="text-3xl font-bold text-center text-indigo-700 mb-8">摄影社团库存管理系统</h1>
|
||||
<div className="flex justify-center space-x-4 mb-8">
|
||||
<label className="bg-blue-500 text-white px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 transition duration-300">
|
||||
导入数据
|
||||
<input type="file" onChange={handleImport} className="hidden" />
|
||||
</label>
|
||||
<button onClick={handleExport} className="bg-green-500 text-white px-4 py-2 rounded-full hover:bg-green-600 transition duration-300">
|
||||
导出数据
|
||||
</button>
|
||||
</div>
|
||||
<DeviceForm onAddDevice={handleAddDevice} />
|
||||
<DeviceList
|
||||
devices={devices}
|
||||
onUpdateDevice={handleUpdateDevice}
|
||||
onDeleteDevice={handleDeleteDevice}
|
||||
showAlert={showAlert} // 修复这里
|
||||
/>
|
||||
<DistributionRecord
|
||||
devices={devices}
|
||||
distributions={distributions}
|
||||
onAddDistribution={handleAddDistribution}
|
||||
onUpdateDistribution={handleUpdateDistribution}
|
||||
onDeleteDistribution={handleDeleteDistribution}
|
||||
showAlert={showAlert} // 修复这里
|
||||
/>
|
||||
{alert && (
|
||||
<Alert
|
||||
message={alert.message}
|
||||
type={alert.type}
|
||||
onClose={() => setAlert(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
65
components/Navbar.tsx
Normal file
65
components/Navbar.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
|
||||
const Navbar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Link href="/" className="text-xl font-bold text-gray-800">
|
||||
影视星河
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<Link href="/" className="text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 border-transparent hover:border-gray-300">
|
||||
首页
|
||||
</Link>
|
||||
<Link href="/showcase" className="text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 border-transparent hover:border-gray-300">
|
||||
作品展示
|
||||
</Link>
|
||||
<Link href="/inventory" className="text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 border-transparent hover:border-gray-300">
|
||||
库存管理
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:hidden flex items-center">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
>
|
||||
<span className="sr-only">打开主菜单</span>
|
||||
{isOpen ? (
|
||||
<X className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<Menu className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="sm:hidden">
|
||||
<div className="pt-2 pb-3 space-y-1">
|
||||
<Link href="/" className="text-gray-900 block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-50">
|
||||
首页
|
||||
</Link>
|
||||
<Link href="/showcase" className="text-gray-900 block px-3 py-2 rounded-md text-base font-medium hover:bg-gray-50">
|
||||
作品展示
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
|
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
22
components/ui/input.tsx
Normal file
22
components/ui/input.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
77
data/config.yaml
Normal file
77
data/config.yaml
Normal file
@ -0,0 +1,77 @@
|
||||
activities:
|
||||
- title: 月度摄影漫步
|
||||
coverImage: /images/photo-walk.jpg
|
||||
description: 加入我们的月度城市摄影漫步。捕捉城市景观和街头场景。
|
||||
date: 2023-07-15
|
||||
participants:
|
||||
- 张三
|
||||
- 李四
|
||||
- 王五
|
||||
- 赵六
|
||||
- 钱七
|
||||
organizer: 刘老师
|
||||
|
||||
- title: 人像摄影工作坊
|
||||
coverImage: /images/portrait-workshop.jpg
|
||||
description: 在专业摄影师的指导下,学习人像摄影的艺术。
|
||||
date: 2023-07-22
|
||||
participants:
|
||||
- 陈一
|
||||
- 周二
|
||||
- 吴三
|
||||
organizer: 黄教授
|
||||
|
||||
- title: 自然摄影探险
|
||||
coverImage: /images/nature-expedition.jpg
|
||||
description: 探索大自然,在周末的自然摄影探险中捕捉壮丽的风景。
|
||||
date: 2023-08-05
|
||||
participants:
|
||||
- 郑八
|
||||
- 孙九
|
||||
- 杨十
|
||||
- 朱十一
|
||||
- 秦十二
|
||||
- 许十三
|
||||
organizer: 林导师
|
||||
|
||||
- title: 照片编辑大师班
|
||||
coverImage: /images/editing-masterclass.jpg
|
||||
description: 通过我们的照片编辑大师班,使用行业标准软件提升您的后期处理技能。
|
||||
date: 2023-08-12
|
||||
participants:
|
||||
- 冯一
|
||||
- 蒋二
|
||||
- 沈三
|
||||
organizer: 马老师
|
||||
|
||||
showcase:
|
||||
- title: 海滩日落
|
||||
image: /images/sunset-beach.jpg
|
||||
author: 张美丽
|
||||
date: 2023-05-15
|
||||
|
||||
- title: 城市倒影
|
||||
image: /images/urban-reflections.jpg
|
||||
author: 李强
|
||||
date: 2023-04-22
|
||||
|
||||
- title: 山峰之巅
|
||||
image: /images/mountain-majesty.jpg
|
||||
author: 王小明
|
||||
date: 2023-06-01
|
||||
|
||||
- title: 陌生人的肖像
|
||||
image: /images/portrait-stranger.jpg
|
||||
author: 刘静
|
||||
date: 2023-05-07
|
||||
|
||||
- title: 秋色
|
||||
image: /images/autumn-colors.jpg
|
||||
author: 陈雨
|
||||
date: 2023-10-10
|
||||
|
||||
- title: 城市夜景
|
||||
image: /images/night-city.jpg
|
||||
author: 赵阳
|
||||
date: 2023-07-18
|
||||
|
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
5456
package-lock.json
generated
Normal file
5456
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "bsd-sy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.14.3",
|
||||
"fs": "^0.0.1-security",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "14.2.16",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"yaml": "^2.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.16",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
63
tailwind.config.ts
Normal file
63
tailwind.config.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
export default config;
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
9
types/activity.ts
Normal file
9
types/activity.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface Activity {
|
||||
title: string
|
||||
coverImage: string
|
||||
description: string
|
||||
date: string
|
||||
participants: string[]
|
||||
organizer: string
|
||||
}
|
||||
|
20
utils/localStorage.ts
Normal file
20
utils/localStorage.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export const saveToLocalStorage = (key: string, data: any) => {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const getFromLocalStorage = (key: string) => {
|
||||
const data = localStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
};
|
||||
|
||||
export const exportData = (data: any, filename: string) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user