feat: 添加新的工具类别和工具项
- 新增了三个新的工具项:仓库管理器、仓库管理器(特供版)和检查单 - 添加 README.md
This commit is contained in:
parent
7fac962303
commit
6440d171b9
9
LICENSE
Normal file
9
LICENSE
Normal 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
20
README.md
Normal 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` 文件,看看就知道怎么写了
|
5
app/tools/checkout-list/page.tsx
Normal file
5
app/tools/checkout-list/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { CombinedChecklistAppComponent } from "@/components/tools/checkout-list/combined-checklist-app"
|
||||
|
||||
export default function Page() {
|
||||
return <CombinedChecklistAppComponent />
|
||||
}
|
5
app/tools/inventory-management/page.tsx
Normal file
5
app/tools/inventory-management/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import InventoryManagementComponent from "@/components/tools/inventory-management/inventory-management"
|
||||
|
||||
export default function Page() {
|
||||
return <InventoryManagementComponent />
|
||||
}
|
5
app/tools/sp-inventory-management/page.tsx
Normal file
5
app/tools/sp-inventory-management/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import InventoryManagementComponent from "@/components/tools/sp-inventory-management/inventory-management";
|
||||
|
||||
export default function Page() {
|
||||
return <InventoryManagementComponent />;
|
||||
}
|
416
components/tools/checkout-list/combined-checklist-app.tsx
Normal file
416
components/tools/checkout-list/combined-checklist-app.tsx
Normal 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">
|
||||
您确定要删除 "{checklist.name}" 检查单吗?此操作无法撤销。
|
||||
</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>
|
||||
)
|
||||
}
|
668
components/tools/inventory-management/inventory-management.tsx
Normal file
668
components/tools/inventory-management/inventory-management.tsx
Normal 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>
|
||||
您确定要删除仓库 "{warehouseToDelete}" 吗?此操作无法撤销。请输入仓库名称以确认删除。
|
||||
</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>
|
||||
)
|
||||
}
|
1095
components/tools/sp-inventory-management/inventory-management.tsx
Normal file
1095
components/tools/sp-inventory-management/inventory-management.tsx
Normal file
File diff suppressed because it is too large
Load Diff
141
components/ui/alert-dialog.tsx
Normal file
141
components/ui/alert-dialog.tsx
Normal 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
36
components/ui/badge.tsx
Normal 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
76
components/ui/card.tsx
Normal 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
122
components/ui/dialog.tsx
Normal 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
26
components/ui/label.tsx
Normal 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
159
components/ui/select.tsx
Normal 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
120
components/ui/table.tsx
Normal 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
55
components/ui/tabs.tsx
Normal 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
33
components/ui/toast.tsx
Normal 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
32
components/ui/tooltip.tsx
Normal 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 }
|
39
components/ui/use-toast.tsx
Normal file
39
components/ui/use-toast.tsx
Normal 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 };
|
@ -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
193
hooks/use-toast.ts
Normal 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
1872
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user