feat: 添加新的工具类别和工具项

- 新增了三个新的工具项:仓库管理器、仓库管理器(特供版)和检查单
- 添加 README.md
This commit is contained in:
mei 2025-01-27 08:34:05 +08:00
parent 7fac962303
commit 6440d171b9
23 changed files with 5145 additions and 1 deletions

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 mei
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# Linuxcat Nav
为 Linuxcat 开发的导航站应用,欢迎取用代码
## 说明
### Start
```shell
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
### 工具
`components/tools` 目录下为工具的代码,可以按需删除,同时记得删除 `app/tools` 下对应的 `page.tsx` 文件
### 导航项
`data/` 目录下存放的是导航项的 `yaml` 文件,看看就知道怎么写了

View File

@ -0,0 +1,5 @@
import { CombinedChecklistAppComponent } from "@/components/tools/checkout-list/combined-checklist-app"
export default function Page() {
return <CombinedChecklistAppComponent />
}

View File

@ -0,0 +1,5 @@
import InventoryManagementComponent from "@/components/tools/inventory-management/inventory-management"
export default function Page() {
return <InventoryManagementComponent />
}

View File

@ -0,0 +1,5 @@
import InventoryManagementComponent from "@/components/tools/sp-inventory-management/inventory-management";
export default function Page() {
return <InventoryManagementComponent />;
}

View File

@ -0,0 +1,416 @@
"use client"
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Plus, Check, RotateCcw, Download, Upload, Trash2, Edit2, Save, X } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/components/ui/use-toast";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface ChecklistItem {
id: string
text: string
completed: boolean
}
interface Checklist {
id: string
name: string
items: ChecklistItem[]
}
export function CombinedChecklistAppComponent() {
const [checklists, setChecklists] = useState<Checklist[]>([])
const [currentChecklist, setCurrentChecklist] = useState<string>('')
const [newItemText, setNewItemText] = useState('')
const [newChecklistName, setNewChecklistName] = useState('')
const [editingId, setEditingId] = useState<string | null>(null)
const [editName, setEditName] = useState('')
const { showToast } = useToast()
useEffect(() => {
const savedChecklists = localStorage.getItem('checklists')
if (savedChecklists) {
setChecklists(JSON.parse(savedChecklists))
}
}, [])
useEffect(() => {
localStorage.setItem('checklists', JSON.stringify(checklists))
}, [checklists])
const addChecklist = () => {
if (newChecklistName.trim() === '') return
const newChecklist: Checklist = {
id: Date.now().toString(),
name: newChecklistName,
items: []
}
setChecklists([...checklists, newChecklist])
setNewChecklistName('')
setCurrentChecklist(newChecklist.id)
showToast({
title: "新检查单已创建",
description: `"${newChecklistName}" 已添加到您的检查单列表。`,
})
}
const addItem = () => {
if (newItemText.trim() === '' || currentChecklist === '') return
const newItem: ChecklistItem = {
id: Date.now().toString(),
text: newItemText,
completed: false
}
setChecklists(checklists.map(list =>
list.id === currentChecklist
? { ...list, items: [...list.items, newItem] }
: list
))
setNewItemText('')
}
const toggleItem = (itemId: string) => {
setChecklists(checklists.map(list =>
list.id === currentChecklist
? {
...list,
items: list.items.map(item =>
item.id === itemId
? { ...item, completed: !item.completed }
: item
)
}
: list
))
}
const deleteItem = (itemId: string) => {
setChecklists(checklists.map(list =>
list.id === currentChecklist
? {
...list,
items: list.items.filter(item => item.id !== itemId)
}
: list
))
showToast({
title: "检查项已删除",
description: "该项目已从您的检查单中移除。",
})
}
const resetChecklist = () => {
setChecklists(checklists.map(list =>
list.id === currentChecklist
? { ...list, items: list.items.map(item => ({ ...item, completed: false })) }
: list
))
showToast({
title: "检查单已重置",
description: "所有项目已标记为未完成。",
})
}
const exportData = () => {
const dataStr = JSON.stringify(checklists)
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
const exportFileDefaultName = 'checklists.json'
const linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
showToast({
title: "数据已导出",
description: "您的检查单数据已成功导出为 JSON 文件。",
})
}
const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result
if (typeof content === 'string') {
try {
const importedChecklists = JSON.parse(content)
setChecklists(importedChecklists)
showToast({
title: "数据导入成功",
description: "您的检查单已成功导入。",
})
} catch (error) {
showToast({
title: "导入失败",
description: "无法解析导入的文件。请确保它是有效的 JSON 格式。",
variant: "destructive",
})
}
}
}
reader.readAsText(file)
}
}
const startEditing = (id: string, name: string) => {
setEditingId(id)
setEditName(name)
}
const saveEdit = (id: string) => {
setChecklists(checklists.map(list =>
list.id === id ? { ...list, name: editName } : list
))
setEditingId(null)
showToast({
title: "检查单已更新",
description: `检查单名称已更改为 "${editName}"。`,
})
}
const cancelEdit = () => {
setEditingId(null)
setEditName('')
}
const deleteChecklist = (id: string) => {
setChecklists(checklists.filter(list => list.id !== id))
if (currentChecklist === id) {
setCurrentChecklist('')
}
showToast({
title: "检查单已删除",
description: "该检查单及其所有项目已被移除。",
variant: "destructive",
})
}
return (
<div className="container mx-auto p-4 max-w-4xl">
<motion.h1
className="text-3xl font-bold mb-6 text-center text-primary"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
</motion.h1>
<Tabs defaultValue="checklist" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="checklist"></TabsTrigger>
<TabsTrigger value="manage"></TabsTrigger>
</TabsList>
<TabsContent value="checklist">
<motion.div
className="mb-6 p-4 bg-card rounded-lg shadow-md"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<Label htmlFor="new-checklist" className="text-lg font-semibold mb-2 block"></Label>
<div className="flex mt-1">
<Input
id="new-checklist"
value={newChecklistName}
onChange={(e) => setNewChecklistName(e.target.value)}
placeholder="输入检查单名称"
className="mr-2 flex-grow"
/>
<Button onClick={addChecklist} className="bg-primary hover:bg-primary/90">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</motion.div>
<motion.div
className="mb-6 p-4 bg-card rounded-lg shadow-md"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<Label htmlFor="checklist-select" className="text-lg font-semibold mb-2 block"></Label>
<Select value={currentChecklist} onValueChange={setCurrentChecklist}>
<SelectTrigger id="checklist-select" className="w-full">
<SelectValue placeholder="选择一个检查单" />
</SelectTrigger>
<SelectContent>
{checklists.map(list => (
<SelectItem key={list.id} value={list.id}>{list.name}</SelectItem>
))}
</SelectContent>
</Select>
</motion.div>
{currentChecklist && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="mb-6 p-4 bg-card rounded-lg shadow-md">
<Label htmlFor="new-item" className="text-lg font-semibold mb-2 block"></Label>
<div className="flex mt-1">
<Input
id="new-item"
value={newItemText}
onChange={(e) => setNewItemText(e.target.value)}
placeholder="输入新的检查项"
className="mr-2 flex-grow"
/>
<Button onClick={addItem} className="bg-primary hover:bg-primary/90">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<motion.ul className="space-y-2 mb-6">
<AnimatePresence>
{checklists.find(list => list.id === currentChecklist)?.items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
className={`flex items-center p-3 rounded-lg transition-all duration-300 ${
item.completed ? 'bg-green-100 dark:bg-green-900' : 'bg-card'
} shadow-sm hover:shadow-md`}
>
<motion.div
className={`w-6 h-6 rounded-full border-2 mr-3 flex items-center justify-center cursor-pointer ${
item.completed ? 'border-green-500 bg-green-500' : 'border-gray-400'
}`}
onClick={() => toggleItem(item.id)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{item.completed && <Check className="w-4 h-4 text-white" />}
</motion.div>
<span className={`flex-grow ${item.completed ? 'line-through text-muted-foreground' : ''}`}>
{item.text}
</span>
<motion.button
onClick={() => deleteItem(item.id)}
className="text-destructive hover:text-destructive/90 p-1 rounded-full"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Trash2 className="w-4 h-4" />
</motion.button>
</motion.li>
))}
</AnimatePresence>
</motion.ul>
<div className="flex space-x-2 mb-4">
<Button onClick={resetChecklist} variant="outline" className="flex-1">
<RotateCcw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={exportData} variant="outline" className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="relative flex-1">
<Upload className="w-4 h-4 mr-2" />
<input
type="file"
onChange={importData}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".json"
/>
</Button>
</div>
</motion.div>
)}
</TabsContent>
<TabsContent value="manage">
<motion.div
className="space-y-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<AnimatePresence>
{checklists.map(checklist => (
<motion.div
key={checklist.id}
className="bg-card rounded-lg shadow-md p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{editingId === checklist.id ? (
<div className="flex items-center space-x-2">
<Input
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="flex-grow"
placeholder="输入新的检查单名称"
/>
<Button onClick={() => saveEdit(checklist.id)} size="icon" variant="ghost">
<Save className="h-4 w-4" />
</Button>
<Button onClick={cancelEdit} size="icon" variant="ghost">
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex items-center justify-between">
<span className="text-lg font-medium">{checklist.name}</span>
<div className="space-x-2">
<Button onClick={() => startEditing(checklist.id, checklist.name)} size="icon" variant="ghost">
<Edit2 className="h-4 w-4" />
</Button>
<Dialog>
<DialogTrigger asChild>
<Button size="icon" variant="ghost">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<p className="text-muted-foreground">
&quot;{checklist.name}&quot;
</p>
<div className="flex justify-end space-x-2 mt-4">
<Button variant="outline" onClick={() => {}}></Button>
<Button variant="destructive" onClick={() => deleteChecklist(checklist.id)}></Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
)}
<p className="text-sm text-muted-foreground mt-2">
{checklist.items.length}
{checklist.items.filter(item => item.completed).length}
</p>
</motion.div>
))}
</AnimatePresence>
</motion.div>
{checklists.length === 0 && (
<motion.p
className="text-center text-muted-foreground mt-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
>
</motion.p>
)}
</TabsContent>
</Tabs>
</div>
)
}

View File

@ -0,0 +1,668 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Minus, Package, Trash2, Search, BarChart, Download, Upload, Edit2, Save, Warehouse } from 'lucide-react'
import { motion } from "framer-motion"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import { Badge } from "@/components/ui/badge"
import { Tooltip as ShadTooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
interface InventoryItem {
id: number;
name: string;
quantity: number;
warehouse: string;
location: string;
price: number;
totalValue: number;
}
const fadeIn = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
}
export default function InventoryManagement() {
const [inventory, setInventory] = useState<InventoryItem[]>([])
const [newItemName, setNewItemName] = useState("")
const [newItemQuantity, setNewItemQuantity] = useState("")
const [newItemWarehouse, setNewItemWarehouse] = useState("")
const [searchTerm, setSearchTerm] = useState("")
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [itemToDelete, setItemToDelete] = useState<number | null>(null)
const [editingItemId, setEditingItemId] = useState<number | null>(null)
const [editingItemName, setEditingItemName] = useState("")
const [editingItemPrice, setEditingItemPrice] = useState("")
const [warehouses, setWarehouses] = useState<string[]>([])
const [activeTab, setActiveTab] = useState("inventory")
const [newItemLocation, setNewItemLocation] = useState("")
const [newItemPrice, setNewItemPrice] = useState("")
const [sortColumn, setSortColumn] = useState<keyof InventoryItem>('name')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const [editingItemLocation, setEditingItemLocation] = useState("")
const [editingItemWarehouse, setEditingItemWarehouse] = useState("")
// 组件挂载时从localStorage加载库存和仓库数据
useEffect(() => {
const savedInventory = localStorage.getItem('inventory')
const savedWarehouses = localStorage.getItem('warehouses')
if (savedInventory) setInventory(JSON.parse(savedInventory))
if (savedWarehouses) setWarehouses(JSON.parse(savedWarehouses))
}, [])
// 库存或仓库数据变化时保存到localStorage
useEffect(() => {
localStorage.setItem('inventory', JSON.stringify(inventory))
localStorage.setItem('warehouses', JSON.stringify(warehouses))
}, [inventory, warehouses])
const addItem = () => {
if (newItemName && newItemQuantity && newItemWarehouse && newItemLocation && newItemPrice) {
const quantity = parseInt(newItemQuantity) || 0;
const price = parseFloat(newItemPrice) || 0;
const newItem: InventoryItem = {
id: Date.now(),
name: newItemName,
quantity: quantity,
warehouse: newItemWarehouse,
location: newItemLocation,
price: price,
totalValue: quantity * price,
}
setInventory([...inventory, newItem])
setNewItemName("")
setNewItemQuantity("")
setNewItemWarehouse("")
setNewItemLocation("")
setNewItemPrice("")
}
}
const updateQuantity = (id: number, change: number) => {
setInventory(inventory.map(item => {
if (item.id === id) {
const newQuantity = Math.max(0, (item.quantity || 0) + change);
const price = item.price || 0;
return { ...item, quantity: newQuantity, totalValue: newQuantity * price };
}
return item;
}));
}
const confirmDelete = (id: number) => {
setItemToDelete(id)
setDeleteConfirmOpen(true)
}
const deleteItem = () => {
if (itemToDelete !== null) {
setInventory(inventory.filter(item => item.id !== itemToDelete))
setDeleteConfirmOpen(false)
setItemToDelete(null)
}
}
const startEditing = (id: number, name: string, price: number, location: string, warehouse: string) => {
setEditingItemId(id)
setEditingItemName(name)
setEditingItemPrice(price.toString())
setEditingItemLocation(location)
setEditingItemWarehouse(warehouse)
}
const saveEdit = () => {
if (editingItemId !== null) {
setInventory(inventory.map(item => {
if (item.id === editingItemId) {
const newPrice = parseFloat(editingItemPrice) || item.price;
return {
...item,
name: editingItemName,
price: newPrice,
location: editingItemLocation,
warehouse: editingItemWarehouse,
totalValue: item.quantity * newPrice
}
}
return item
}))
setEditingItemId(null)
setEditingItemName("")
setEditingItemPrice("")
setEditingItemLocation("")
setEditingItemWarehouse("")
}
}
const handleSort = (column: keyof InventoryItem) => {
if (column === sortColumn) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
} else {
setSortColumn(column)
setSortDirection('asc')
}
}
const filteredInventory = inventory.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.warehouse.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase())
)
const sortedInventory = [...filteredInventory].sort((a, b) => {
if (a[sortColumn] < b[sortColumn]) return sortDirection === 'asc' ? -1 : 1
if (a[sortColumn] > b[sortColumn]) return sortDirection === 'asc' ? 1 : -1
return 0
})
const exportData = () => {
const dataStr = JSON.stringify({ inventory, warehouses })
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
const exportFileDefaultName = 'inventory_backup.json'
const linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
}
const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target?.result as string)
setInventory(importedData.inventory)
setWarehouses(importedData.warehouses)
} catch (error) {
console.error('Error parsing imported data:', error)
alert('导入失败,请确保文件格式正确。')
}
}
reader.readAsText(file)
}
}
const getInventoryChartData = () => {
return inventory.map(item => ({
name: item.name,
quantity: item.quantity || 0,
totalValue: (item.quantity || 0) * (item.price || 0),
warehouse: item.warehouse
}))
}
return (
<div className="container mx-auto p-4 space-y-8">
<header className="text-center bg-gradient-to-r from-blue-500 to-purple-600 text-white py-8 rounded-lg shadow-lg">
<h1 className="text-4xl font-bold mb-2"></h1>
<p className="text-xl"></p>
</header>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="inventory"></TabsTrigger>
<TabsTrigger value="analytics"></TabsTrigger>
<TabsTrigger value="warehouses"></TabsTrigger>
</TabsList>
<TabsContent value="inventory">
<div className="grid gap-8 md:grid-cols-2">
<motion.div {...fadeIn}>
<Card className="h-full">
<CardHeader>
<CardTitle className="flex items-center">
<Package className="mr-2" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div>
<Label htmlFor="itemName"></Label>
<Input
id="itemName"
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
placeholder="输入商品名称"
/>
</div>
<div>
<Label htmlFor="itemQuantity"></Label>
<Input
id="itemQuantity"
type="number"
value={newItemQuantity}
onChange={(e) => setNewItemQuantity(e.target.value)}
placeholder="输入数量"
/>
</div>
<div>
<Label htmlFor="itemWarehouse"></Label>
<Select onValueChange={setNewItemWarehouse} value={newItemWarehouse}>
<SelectTrigger>
<SelectValue placeholder="选择仓库" />
</SelectTrigger>
<SelectContent>
{warehouses.map((warehouse) => (
<SelectItem key={warehouse} value={warehouse}>
{warehouse}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="itemLocation"></Label>
<Input
id="itemLocation"
value={newItemLocation}
onChange={(e) => setNewItemLocation(e.target.value)}
placeholder="输入货品位置"
/>
</div>
<div>
<Label htmlFor="itemPrice"></Label>
<Input
id="itemPrice"
type="number"
value={newItemPrice}
onChange={(e) => setNewItemPrice(e.target.value)}
placeholder="输入单价"
/>
</div>
<Button onClick={addItem} className="w-full">
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div {...fadeIn} transition={{ delay: 0.2 }}>
<Card className="h-full">
<CardHeader>
<CardTitle className="flex items-center">
<BarChart className="mr-2" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4 space-y-4">
<div>
<Label htmlFor="search"></Label>
<div className="flex">
<Input
id="search"
type="text"
placeholder="输入商品名称或仓库"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="mr-2"
/>
<Button variant="outline">
<Search className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={exportData}>
<Download className="mr-2 h-4 w-4" />
</Button>
<div>
<Input
type="file"
id="importData"
className="hidden"
onChange={importData}
accept=".json"
/>
<Button variant="outline" onClick={() => document.getElementById('importData')?.click()}>
<Upload className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('name')}>
{sortColumn === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('quantity')}>
{sortColumn === 'quantity' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('warehouse')}>
{sortColumn === 'warehouse' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('location')}>
{sortColumn === 'location' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('price')}>
{sortColumn === 'price' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead>
<Button variant="ghost" onClick={() => handleSort('totalValue')}>
{sortColumn === 'totalValue' && (sortDirection === 'asc' ? '↑' : '↓')}
</Button>
</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedInventory.map((item) => (
<TableRow key={item.id}>
<TableCell>
{editingItemId === item.id ?
<Input
value={editingItemName}
onChange={(e) => setEditingItemName(e.target.value)}
className="w-full"
/> : item.name
}
</TableCell>
<TableCell>
<Badge variant="outline">{item.quantity}</Badge>
</TableCell>
<TableCell>
{editingItemId === item.id ?
<Select value={editingItemWarehouse} onValueChange={setEditingItemWarehouse}>
<SelectTrigger>
<SelectValue placeholder="选择仓库" />
</SelectTrigger>
<SelectContent>
{warehouses.map((warehouse) => (
<SelectItem key={warehouse} value={warehouse}>
{warehouse}
</SelectItem>
))}
</SelectContent>
</Select>
: item.warehouse
}
</TableCell>
<TableCell>
{editingItemId === item.id ?
<Input
value={editingItemLocation}
onChange={(e) => setEditingItemLocation(e.target.value)}
className="w-full"
/> : item.location
}
</TableCell>
<TableCell>
{editingItemId === item.id ?
<Input
type="number"
value={editingItemPrice}
onChange={(e) => setEditingItemPrice(e.target.value)}
className="w-full"
/> :
<TooltipProvider>
<ShadTooltip>
<TooltipTrigger asChild>
<span className="cursor-help">¥{item.price?.toFixed(2) ?? '0.00'}</span>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</ShadTooltip>
</TooltipProvider>
}
</TableCell>
<TableCell>¥{item.totalValue?.toFixed(2) ?? '0.00'}</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => updateQuantity(item.id, 1)}>
<Plus className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => updateQuantity(item.id, -1)}>
<Minus className="h-4 w-4" />
</Button>
{editingItemId === item.id ? (
<Button size="sm" variant="outline" onClick={saveEdit}>
<Save className="h-4 w-4" />
</Button>
) : (
<Button size="sm" variant="outline" onClick={() => startEditing(item.id, item.name, item.price, item.location, item.warehouse)}>
<Edit2 className="h-4 w-4" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => confirmDelete(item.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</TabsContent>
<TabsContent value="analytics">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={400}>
<RechartsBarChart data={getInventoryChartData()}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis yAxisId="left" orientation="left" stroke="#8884d8" />
<YAxis yAxisId="right" orientation="right" stroke="#82ca9d" />
<Tooltip />
<Legend />
<Bar dataKey="quantity" fill="#8884d8" name="库存数量" yAxisId="left" />
<Bar dataKey="totalValue" fill="#82ca9d" name="总价值" yAxisId="right" />
</RechartsBarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="warehouses">
<WarehouseManagement
warehouses={warehouses}
setWarehouses={setWarehouses}
inventory={inventory}
setInventory={setInventory}
/>
</TabsContent>
</Tabs>
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={deleteItem}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
function WarehouseManagement({ warehouses, setWarehouses, inventory, setInventory }: { warehouses: string[], setWarehouses: React.Dispatch<React.SetStateAction<string[]>>, inventory: InventoryItem[], setInventory: React.Dispatch<React.SetStateAction<InventoryItem[]>> }) {
const [newWarehouse, setNewWarehouse] = useState("")
const [editingWarehouse, setEditingWarehouse] = useState<string | null>(null)
const [editedWarehouseName, setEditedWarehouseName] = useState("")
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [warehouseToDelete, setWarehouseToDelete] = useState<string | null>(null)
const [confirmDeleteName, setConfirmDeleteName] = useState("")
const addWarehouse = () => {
if (newWarehouse && !warehouses.includes(newWarehouse)) {
setWarehouses([...warehouses, newWarehouse])
setNewWarehouse("")
}
}
const startEditing = (warehouse: string) => {
setEditingWarehouse(warehouse)
setEditedWarehouseName(warehouse)
}
const saveEdit = () => {
if (editingWarehouse && editedWarehouseName) {
setWarehouses(warehouses.map(w => w === editingWarehouse ? editedWarehouseName : w))
setEditingWarehouse(null)
setEditedWarehouseName("")
setInventory(inventory.map(item =>
item.warehouse === editingWarehouse
? { ...item, warehouse: editedWarehouseName }
: item
))
}
}
const confirmDelete = (warehouse: string) => {
setWarehouseToDelete(warehouse)
setDeleteConfirmOpen(true)
setConfirmDeleteName("")
}
const deleteWarehouse = () => {
if (warehouseToDelete && confirmDeleteName === warehouseToDelete) {
setWarehouses(warehouses.filter(w => w !== warehouseToDelete))
setInventory(inventory.filter(item => item.warehouse !== warehouseToDelete))
setDeleteConfirmOpen(false)
setWarehouseToDelete(null)
setConfirmDeleteName("")
}
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Warehouse className="mr-2" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex space-x-2">
<Input
value={newWarehouse}
onChange={(e) => setNewWarehouse(e.target.value)}
placeholder="输入新仓库名称"
/>
<Button onClick={addWarehouse}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{warehouses.map((warehouse) => (
<TableRow key={warehouse}>
<TableCell>
{editingWarehouse === warehouse ? (
<Input
value={editedWarehouseName}
onChange={(e) => setEditedWarehouseName(e.target.value)}
className="w-full"
/>
) : (
warehouse
)}
</TableCell>
<TableCell>
<div className="flex space-x-2">
{editingWarehouse === warehouse ? (
<Button size="sm" variant="outline" onClick={saveEdit}>
<Save className="h-4 w-4" />
</Button>
) : (
<Button size="sm" variant="outline" onClick={() => startEditing(warehouse)}>
<Edit2 className="h-4 w-4" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => confirmDelete(warehouse)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
&quot{warehouseToDelete}&quot
</AlertDialogDescription>
</AlertDialogHeader>
<div className="my-4">
<Input
value={confirmDeleteName}
onChange={(e) => setConfirmDeleteName(e.target.value)}
placeholder="输入仓库名称"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={deleteWarehouse} disabled={confirmDeleteName !== warehouseToDelete}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

76
components/ui/card.tsx Normal file
View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

122
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

26
components/ui/label.tsx Normal file
View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

159
components/ui/select.tsx Normal file
View File

@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

120
components/ui/table.tsx Normal file
View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

55
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

33
components/ui/toast.tsx Normal file
View File

@ -0,0 +1,33 @@
// @/components/ui/toast.tsx
// 定义 ToastProps 类型
export interface ToastProps {
title: string;
description?: string;
variant?: 'default' | 'success' | 'warning' | 'destructive';
}
export interface ToasterToast extends ToastProps {
id: string;
onOpenChange?: (open: boolean) => void;
}
// 定义 ToastActionElement 类型
export interface ToastActionElement {
label: string;
onClick: () => void;
}
const Toast = ({ title, description, variant = 'default' }: ToastProps) => {
const className = `p-4 rounded-lg shadow-md ${variant === 'success' ? 'bg-green-100' : variant === 'warning' ? 'bg-yellow-100' : variant === 'destructive' ? 'bg-red-100' : 'bg-gray-100'}`;
return (
<div className={className}>
<h3 className="text-lg font-medium">{title}</h3>
{description && <p className="mt-1 text-sm text-gray-700">{description}</p>}
</div>
);
};
export default Toast;

32
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,39 @@
import { useState } from 'react';
import type {
ToastActionElement,
ToastProps,
ToasterToast,
} from '@/components/ui/toast';
const useToast = () => {
const [toasts, setToasts] = useState<ToasterToast[]>([]);
const showToast = (props: ToastProps) => {
const id = Math.random().toString(36).substr(2, 9);
const newToast: ToasterToast = {
...props,
id,
onOpenChange: (open) => {
if (!open) dismiss(id);
},
};
setToasts((prevToasts) => [...prevToasts, newToast]);
};
const removeToast = (id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
};
const dismiss = (id: string) => {
setToasts((prevToasts) =>
prevToasts.map((toast) =>
toast.id === id ? { ...toast, onOpenChange: undefined } : toast
)
);
};
return { toasts, showToast, removeToast };
};
export { useToast, ToastProps, ToastActionElement };

View File

@ -9,4 +9,16 @@
- name: 扩展昵称生成器
url: /tools/CatgirlGenerator
description: 当别人@你时,会在@的文字后面加上一另段文字
subcategory: 其它
- name: 仓库管理器
url: /tools/inventory-management
description: 高效管理家里的众多小破烂
subcategory: 其它
- name: 仓库管理器(特供版)
url: /tools/sp-inventory-management
description: 高效管理您的多仓库商品库存
subcategory: 其它
- name: 检查单
url: /tools/checkout-list
description: 创建一个检查单,看看你有什么没有做
subcategory: 其它

193
hooks/use-toast.ts Normal file
View File

@ -0,0 +1,193 @@
"use client";
// Inspired by react-hot-toast library
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

1872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,18 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.7",
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^11.15.0",
"js-yaml": "^4.1.0",
"lucide-react": "^0.460.0",
@ -27,6 +33,7 @@
"react-dropzone": "^14.3.5",
"react-syntax-highlighter": "^15.6.1",
"react-toastify": "^10.0.6",
"recharts": "^2.15.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
},